feat: add executeDelete and updateRestrictedByForeignKey to query
This commit is contained in:
1 parent
0cebee24b2
commit
da6a9cb289
1 file changed
+157
+157
@@ -788,3 +788,160 @@ pub const Table = struct {
|
||||
try plan.append(.{ .table = try self.allocator.dupe(u8, self.name), .pk = try pk.dupe(self.allocator) });
|
||||
}
|
||||
|
||||
/// The actual row/index removal `delete` used to do inline before
|
||||
/// cascading existed - no FOREIGN KEY check here at all, since by the
|
||||
/// time this runs `planCascade` has already proven the entire plan is
|
||||
/// safe to execute in order. Takes `pk` rather than a decoded row
|
||||
/// because `CascadeAction` only carries the two, and re-fetching here
|
||||
/// (rather than threading the `Row` `planCascade` already read) is
|
||||
/// what lets `delete` stay tolerant of a row that no longer exists by
|
||||
/// execution time - unreachable in practice (nothing between planning
|
||||
/// and executing can delete a row out from under this single-threaded
|
||||
/// call), but cheap to make robust to rather than to assume away.
|
||||
fn executeDelete(self: *Table, pk: Value) !void {
|
||||
var old = try self.get(pk) orelse return;
|
||||
defer old.deinit();
|
||||
|
||||
for (self.indexes.fields.items) |indexedField| {
|
||||
if (old.get(indexedField)) |v| {
|
||||
try idx.removeEntry(self.db, self.allocator, self.name, indexedField, v, pk);
|
||||
}
|
||||
}
|
||||
|
||||
for (self.composite.specs.items) |spec| {
|
||||
var values: [maxCompositeFields]Value = undefined;
|
||||
var complete = true;
|
||||
for (spec.fields, 0..) |f, i| {
|
||||
values[i] = old.get(f) orelse {
|
||||
complete = false;
|
||||
break;
|
||||
};
|
||||
}
|
||||
if (complete) try idx.removeCompositeEntry(self.db, self.allocator, self.name, spec.name, values[0..spec.fields.len], pk);
|
||||
}
|
||||
|
||||
const key = try idx.rowKey(self.allocator, self.name, pk);
|
||||
defer self.allocator.free(key);
|
||||
try self.db.delete(key);
|
||||
}
|
||||
|
||||
/// SQL's `ON DELETE RESTRICT`/`CASCADE` (see `planCascade` above) has an
|
||||
/// easy-to-miss counterpart: this codebase never leaves a child dangling
|
||||
/// when its parent ROW disappears, but until now nothing stopped a `put`
|
||||
/// from changing the parent's *value* out from under a still-live child -
|
||||
/// `refTable.dropIndex(refField)` and `updateWhere`/direct `put` changing
|
||||
/// a `refField` are exactly the "carpet pulled from under the child"
|
||||
/// cases `createForeignKey`'s own doc comment already named as open
|
||||
/// gaps. Reuses the identical scan and self-reference exclusion as
|
||||
/// `planCascade` (a value that stops existing under this pk is exactly
|
||||
/// as dangerous to a live reference as a row that stops existing
|
||||
/// entirely), but the trigger is "this pk's value for `refField` is
|
||||
/// about to change", not "this pk is about to be deleted" - so `old` may
|
||||
/// be null (a fresh insert has nothing to pull out from under anyone)
|
||||
/// and a field whose new value equals its old value is exempt, the same
|
||||
/// "unchanged is always safe" exemption `put`'s own UNIQUE check makes.
|
||||
/// Unlike `planCascade`, this ALWAYS restricts regardless of the
|
||||
/// constraint's `onDelete` mode - there is no `ON UPDATE CASCADE` here,
|
||||
/// only `ON DELETE CASCADE`, so even a `cascade`-mode FK still blocks a
|
||||
/// value change under a live reference.
|
||||
fn updateRestrictedByForeignKey(self: *Table, pk: Value, old: ?*const Row, newRow: *const Row) !bool {
|
||||
var referencedBy = try ReferencedBySet.load(self.allocator, self.db, self.name);
|
||||
defer referencedBy.deinit();
|
||||
for (referencedBy.specs.items) |spec| {
|
||||
const oldValue = if (old) |o| o.get(spec.refField) else null;
|
||||
if (oldValue == null or oldValue.? == .none) continue;
|
||||
const newValue = newRow.get(spec.refField);
|
||||
if (newValue != null and Value.eql(oldValue.?, newValue.?)) continue;
|
||||
|
||||
var child = try Table.open(self.db, self.allocator, spec.childTable);
|
||||
defer child.deinit();
|
||||
const rows = try child.scanAll();
|
||||
defer freeRows(self.allocator, rows);
|
||||
const selfReferencing = std.mem.eql(u8, spec.childTable, self.name);
|
||||
for (rows) |*r| {
|
||||
if (selfReferencing) {
|
||||
if (r.get(pkField)) |rowPk| {
|
||||
if (Value.eql(rowPk, pk)) continue;
|
||||
}
|
||||
}
|
||||
const childValue = r.get(spec.childField) orelse continue;
|
||||
if (childValue == .none) continue;
|
||||
if (Value.eql(childValue, oldValue.?)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Deletes `pk`, honoring every `ON DELETE` mode registered against
|
||||
/// this table by some OTHER table's `createForeignKey` (see
|
||||
/// `idx.OnDelete`'s doc comment): a `restrict`-mode reference still
|
||||
/// pointing at this row aborts the whole call with
|
||||
/// `error.ForeignKeyInUse` and nothing is mutated; a `cascade`-mode
|
||||
/// one instead removes the referencing rows too, recursively, before
|
||||
/// this row itself goes - see `planCascade`'s doc comment for why
|
||||
/// that has to happen in two passes (plan every removal first, then
|
||||
/// execute the plan) rather than deleting as it discovers each one.
|
||||
/// A `pk` that does not exist is a silent no-op, same as before
|
||||
/// cascading existed.
|
||||
pub fn delete(self: *Table, pk: Value) !void {
|
||||
var visited = CascadeVisited.init(self.allocator);
|
||||
defer visited.deinit();
|
||||
var plan = std.ArrayList(CascadeAction).init(self.allocator);
|
||||
defer {
|
||||
for (plan.items) |a| a.deinit(self.allocator);
|
||||
plan.deinit();
|
||||
}
|
||||
|
||||
try self.planCascade(pk, &visited, &plan);
|
||||
|
||||
for (plan.items) |action| {
|
||||
var t = try Table.open(self.db, self.allocator, action.table);
|
||||
defer t.deinit();
|
||||
try t.executeDelete(action.pk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies `sets` on top of every row matching `pred` (or every row, if
|
||||
/// null) - SQL's `UPDATE <table> SET ... WHERE pred`. Deliberately not a
|
||||
/// bespoke in-place mutation path: each match is merged with `sets`
|
||||
/// (a `set` entry overrides a same-named existing field, or is appended
|
||||
/// if the row does not have that field yet) and written back through
|
||||
/// `put`, the exact same call an external caller doing `get` then `put`
|
||||
/// by hand would make - so every secondary/composite index touched by a
|
||||
/// changed field is maintained exactly like any other write, with zero
|
||||
/// special-casing here. Uses `selectAllMatches` rather than `selectPage`
|
||||
/// because every match must be visited regardless of any page window -
|
||||
/// there is no `LIMIT` on an `UPDATE`. Returns the number of rows
|
||||
/// updated.
|
||||
pub fn updateWhere(self: *Table, pred: ?Predicate, sets: []const Field) !usize {
|
||||
const matches = try self.selectAllMatches(pred);
|
||||
defer freeRows(self.allocator, matches);
|
||||
|
||||
for (matches) |*row| {
|
||||
const pk = row.get(pkField) orelse continue;
|
||||
|
||||
var merged = Row.init(self.allocator);
|
||||
defer merged.deinit();
|
||||
for (row.fields.items) |f| {
|
||||
if (std.mem.eql(u8, f.name, pkField)) continue;
|
||||
try merged.set(f.name, f.value);
|
||||
}
|
||||
for (sets) |f| try merged.set(f.name, f.value);
|
||||
|
||||
try self.put(pk, merged.fields.items);
|
||||
}
|
||||
return matches.len;
|
||||
}
|
||||
|
||||
/// Deletes every row matching `pred` (or every row, if null) - SQL's
|
||||
/// `DELETE FROM <table> WHERE pred`. Mirrors `updateWhere`: materializes
|
||||
/// the full match set with `selectAllMatches` (a `DELETE` has no
|
||||
/// `LIMIT` either, so partial-scan short-circuiting is wrong here same
|
||||
/// as for update) then removes each match through the existing single-row
|
||||
/// `delete`, so every secondary/composite index touched by the deleted
|
||||
/// row is maintained exactly like a caller doing `get` then `delete` by
|
||||
/// hand would - zero special-casing. Returns the number of rows deleted.
|
||||
pub fn deleteWhere(self: *Table, pred: ?Predicate) !usize {
|
||||
const matches = try self.selectAllMatches(pred);
|
||||
defer freeRows(self.allocator, matches);
|
||||
|
||||
Reference in new issue
Block a user