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