feat: add CascadeAction and deinit to query
This commit is contained in:
1 parent
b20d2b95ce
commit
0cebee24b2
1 file changed
+153
+153
@@ -635,3 +635,156 @@ pub const Table = struct {
|
||||
}
|
||||
}
|
||||
|
||||
for (self.composite.specs.items) |spec| {
|
||||
var oldValues: [maxCompositeFields]Value = undefined;
|
||||
var oldComplete = old != null;
|
||||
if (old) |*o| {
|
||||
for (spec.fields, 0..) |f, i| {
|
||||
oldValues[i] = o.get(f) orelse {
|
||||
oldComplete = false;
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
var newValues: [maxCompositeFields]Value = undefined;
|
||||
var newComplete = true;
|
||||
for (spec.fields, 0..) |f, i| {
|
||||
newValues[i] = newRow.get(f) orelse {
|
||||
newComplete = false;
|
||||
break;
|
||||
};
|
||||
}
|
||||
const unchanged = oldComplete and newComplete and valueTupleEql(oldValues[0..spec.fields.len], newValues[0..spec.fields.len]);
|
||||
if (unchanged) continue;
|
||||
if (oldComplete) try idx.removeCompositeEntry(self.db, self.allocator, self.name, spec.name, oldValues[0..spec.fields.len], pk);
|
||||
if (newComplete) try idx.addCompositeEntry(self.db, self.allocator, self.name, spec.name, newValues[0..spec.fields.len], pk);
|
||||
}
|
||||
|
||||
try self.db.put(key, encoded);
|
||||
}
|
||||
|
||||
pub fn get(self: *Table, pk: Value) !?Row {
|
||||
const key = try idx.rowKey(self.allocator, self.name, pk);
|
||||
defer self.allocator.free(key);
|
||||
const raw = try self.db.get(self.allocator, key) orelse return null;
|
||||
defer self.allocator.free(raw);
|
||||
return try Row.decode(self.allocator, raw);
|
||||
}
|
||||
|
||||
/// One (table, pk) a cascading delete has decided to remove, in the
|
||||
/// order `planCascade` discovered it - deepest dependents first, the
|
||||
/// row that was actually asked for last. Owns copies of both fields
|
||||
/// independently of whatever `Row`/`Table` produced them, since the
|
||||
/// scan that found them is long gone (and possibly a different
|
||||
/// `Table` handle entirely) by the time `delete` executes the plan.
|
||||
const CascadeAction = struct {
|
||||
table: []u8,
|
||||
pk: Value,
|
||||
|
||||
fn deinit(self: *const CascadeAction, allocator: Allocator) void {
|
||||
allocator.free(self.table);
|
||||
self.pk.free(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
/// Cycle guard for `planCascade`: every (table, pk) already visited
|
||||
/// this call, so a cascade that loops back on itself (two tables
|
||||
/// `ON DELETE CASCADE`-ing at each other, or a longer cycle through
|
||||
/// three or more) terminates instead of recursing forever. Linear
|
||||
/// scan is fine here - a cascading delete's fan-out is bounded by how
|
||||
/// much data actually exists, and this runs once per row touched, not
|
||||
/// once per row scanned.
|
||||
const CascadeVisited = struct {
|
||||
allocator: Allocator,
|
||||
entries: std.ArrayList(CascadeAction),
|
||||
|
||||
fn init(allocator: Allocator) CascadeVisited {
|
||||
return .{ .allocator = allocator, .entries = std.ArrayList(CascadeAction).init(allocator) };
|
||||
}
|
||||
|
||||
fn contains(self: *const CascadeVisited, table: []const u8, pk: Value) bool {
|
||||
for (self.entries.items) |e| {
|
||||
if (std.mem.eql(u8, e.table, table) and Value.eql(e.pk, pk)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn add(self: *CascadeVisited, table: []const u8, pk: Value) !void {
|
||||
const tableCopy = try self.allocator.dupe(u8, table);
|
||||
errdefer self.allocator.free(tableCopy);
|
||||
const pkCopy = try pk.dupe(self.allocator);
|
||||
try self.entries.append(.{ .table = tableCopy, .pk = pkCopy });
|
||||
}
|
||||
|
||||
fn deinit(self: *CascadeVisited) void {
|
||||
for (self.entries.items) |e| e.deinit(self.allocator);
|
||||
self.entries.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// Recursively works out everything a delete of `pk` must remove,
|
||||
/// WITHOUT deleting anything yet, appending each decision to `plan` in
|
||||
/// dependent-before-dependency order (see `CascadeAction`'s doc
|
||||
/// comment) so `delete` can execute it leaves-first. This two-phase
|
||||
/// plan/execute split is what keeps a cascading delete all-or-nothing:
|
||||
/// a `restrict`-mode violation discovered three levels deep must abort
|
||||
/// with NOTHING mutated, exactly like the single-table check this
|
||||
/// replaced - if planning and deleting were interleaved, an earlier
|
||||
/// sibling subtree could already be gone by the time a later one fails.
|
||||
///
|
||||
/// For every `ReferencedBySet` entry naming a table with a live row
|
||||
/// pointing at `old`'s value for the referenced field (see the
|
||||
/// previous version of this function for why the child is opened and
|
||||
/// scanned in full rather than through `idx.scanEq`, and why a
|
||||
/// self-referencing FK excludes `pk` itself from matching): `restrict`
|
||||
/// aborts the whole plan with `error.ForeignKeyInUse`; `cascade`
|
||||
/// recurses into that child row first, so it lands in `plan` ahead of
|
||||
/// `self` (a child must be gone before its parent is, or the delete
|
||||
/// order would itself violate the very constraint being enforced).
|
||||
///
|
||||
/// `visited` is checked (and updated) BEFORE any of that per-row work,
|
||||
/// so a cycle (`planCascade(A)` reaching `planCascade(B)` reaching
|
||||
/// `planCascade(A)` again) short-circuits the re-entrant call instead
|
||||
/// of looping - the ORIGINAL call for that (table, pk) is still the one
|
||||
/// that appends it to `plan`, once its own scan finishes, so nothing is
|
||||
/// silently dropped from the plan by the cycle.
|
||||
fn planCascade(self: *Table, pk: Value, visited: *CascadeVisited, plan: *std.ArrayList(CascadeAction)) !void {
|
||||
if (visited.contains(self.name, pk)) return;
|
||||
var old = try self.get(pk) orelse return;
|
||||
defer old.deinit();
|
||||
try visited.add(self.name, pk);
|
||||
|
||||
var referencedBy = try ReferencedBySet.load(self.allocator, self.db, self.name);
|
||||
defer referencedBy.deinit();
|
||||
for (referencedBy.specs.items) |spec| {
|
||||
const value = old.get(spec.refField) orelse continue;
|
||||
if (value == .none) 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, value)) continue;
|
||||
|
||||
switch (spec.onDelete) {
|
||||
.restrict => return error.ForeignKeyInUse,
|
||||
.cascade => {
|
||||
const childPk = r.get(pkField) orelse continue;
|
||||
try child.planCascade(childPk, visited, plan);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try plan.append(.{ .table = try self.allocator.dupe(u8, self.name), .pk = try pk.dupe(self.allocator) });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user