From fff7ffeb782b9d37325b941ab50cd73c9017c21e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 17:09:48 +0200 Subject: [PATCH] feat: add deinit and ReferencedBySpec to index --- src/index.zig | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/index.zig b/src/index.zig index ff26b2a..398397b 100644 --- a/src/index.zig +++ b/src/index.zig @@ -634,3 +634,159 @@ pub const CheckConstraintSet = struct { return false; } + pub fn deinit(self: *CheckConstraintSet) void { + for (self.specs.items) |spec| { + self.allocator.free(spec.name); + self.allocator.free(spec.source); + } + self.specs.deinit(); + } +}; + +/// One reverse FOREIGN KEY entry, recorded on the REFERENCED table rather +/// than the referencing one: some other table `childTable` has a +/// `FOREIGN KEY (childField) REFERENCES (refField)`. This is +/// the registry `Table.createForeignKey`'s doc comment calls out as +/// missing ("tables are opened ad hoc by name and never catalogued +/// anywhere") - without it, deleting a parent row has no way to discover +/// which other tables might still be pointing at it, since `ForeignKeySet` +/// is only ever stored on the referencing (child) side. +pub const ReferencedBySpec = struct { + childTable: []u8, + childField: []u8, + refField: []u8, + onDelete: OnDelete, +}; + +/// Persisted set of reverse FK entries on a table - same on-disk shape +/// family as `ForeignKeySet` (own meta key, own namespace, three +/// length-prefixed strings plus an `onDelete` mode byte per entry), but +/// populated by a DIFFERENT table's `createForeignKey`/`dropForeignKey` +/// call rather than by mutations against this table directly. `onDelete` +/// is duplicated here rather than looked up from the child's own +/// `ForeignKeySet` on every delete, so `Table.delete`'s cascade planner +/// (see `query.zig`'s `planCascade`) can decide restrict-vs-cascade +/// without opening and loading the child a second time just to answer +/// that. `add` is keyed by the (childTable, childField) pair - a child +/// field can only ever carry one FOREIGN KEY (enforced by +/// `ForeignKeySet.has` at the child), so at most one reverse entry per +/// pair can ever be correct; re-adding the same pair (only reachable by +/// first dropping and recreating the child's constraint, possibly with a +/// different `refField`/`onDelete`) replaces it in place instead of +/// accumulating a stale duplicate. +pub const ReferencedBySet = struct { + allocator: Allocator, + specs: std.ArrayList(ReferencedBySpec), + + pub fn load(allocator: Allocator, db: *Db, table: []const u8) !ReferencedBySet { + const key = try referencedByMetaKey(allocator, table); + defer allocator.free(key); + + var set = ReferencedBySet{ .allocator = allocator, .specs = std.ArrayList(ReferencedBySpec).init(allocator) }; + errdefer set.deinit(); + + const raw = try db.get(allocator, key) orelse return set; + defer allocator.free(raw); + + var pos: usize = 0; + if (pos + 4 > raw.len) return error.Malformed; + const count = std.mem.readInt(u32, raw[pos..][0..4], .little); + pos += 4; + var i: u32 = 0; + while (i < count) : (i += 1) { + const childTable = try readLenPrefixed(allocator, raw, &pos); + errdefer allocator.free(childTable); + const childField = try readLenPrefixed(allocator, raw, &pos); + errdefer allocator.free(childField); + const refField = try readLenPrefixed(allocator, raw, &pos); + errdefer allocator.free(refField); + if (pos + 1 > raw.len) return error.Malformed; + const onDelete = try onDeleteFromByte(raw[pos]); + pos += 1; + try set.specs.append(.{ .childTable = childTable, .childField = childField, .refField = refField, .onDelete = onDelete }); + } + return set; + } + + pub fn save(self: *const ReferencedBySet, db: *Db, table: []const u8) !void { + const key = try referencedByMetaKey(self.allocator, table); + defer self.allocator.free(key); + + var out = std.ArrayList(u8).init(self.allocator); + defer out.deinit(); + var countBuf: [4]u8 = undefined; + std.mem.writeInt(u32, &countBuf, @intCast(self.specs.items.len), .little); + try out.appendSlice(&countBuf); + for (self.specs.items) |spec| { + try appendLenPrefixed(&out, spec.childTable); + try appendLenPrefixed(&out, spec.childField); + try appendLenPrefixed(&out, spec.refField); + try out.append(@intFromEnum(spec.onDelete)); + } + try db.put(key, out.items); + } + + fn indexOf(self: *const ReferencedBySet, childTable: []const u8, childField: []const u8) ?usize { + for (self.specs.items, 0..) |spec, i| { + if (std.mem.eql(u8, spec.childTable, childTable) and std.mem.eql(u8, spec.childField, childField)) return i; + } + return null; + } + + pub fn add(self: *ReferencedBySet, childTable: []const u8, childField: []const u8, refField: []const u8, onDelete: OnDelete) !void { + const refFieldCopy = try self.allocator.dupe(u8, refField); + errdefer self.allocator.free(refFieldCopy); + if (self.indexOf(childTable, childField)) |i| { + self.allocator.free(self.specs.items[i].refField); + self.specs.items[i].refField = refFieldCopy; + self.specs.items[i].onDelete = onDelete; + return; + } + const spec = ReferencedBySpec{ + .childTable = try self.allocator.dupe(u8, childTable), + .childField = try self.allocator.dupe(u8, childField), + .refField = refFieldCopy, + .onDelete = onDelete, + }; + try self.specs.append(spec); + } + + /// Returns true if a reverse entry for (childTable, childField) was + /// present and has been removed. + pub fn remove(self: *ReferencedBySet, childTable: []const u8, childField: []const u8) bool { + const i = self.indexOf(childTable, childField) orelse return false; + const spec = self.specs.items[i]; + self.allocator.free(spec.childTable); + self.allocator.free(spec.childField); + self.allocator.free(spec.refField); + _ = self.specs.orderedRemove(i); + return true; + } + + pub fn deinit(self: *ReferencedBySet) void { + for (self.specs.items) |spec| { + self.allocator.free(spec.childTable); + self.allocator.free(spec.childField); + self.allocator.free(spec.refField); + } + self.specs.deinit(); + } +}; + +fn readLenPrefixed(allocator: Allocator, raw: []const u8, pos: *usize) ![]u8 { + if (pos.* + 4 > raw.len) return error.Malformed; + const len = std.mem.readInt(u32, raw[pos.*..][0..4], .little); + pos.* += 4; + if (pos.* + len > raw.len) return error.Malformed; + const value = try allocator.dupe(u8, raw[pos.* .. pos.* + len]); + pos.* += len; + return value; +} + +fn appendLenPrefixed(out: *std.ArrayList(u8), value: []const u8) !void { + var lenBuf: [4]u8 = undefined; + std.mem.writeInt(u32, &lenBuf, @intCast(value.len), .little); + try out.appendSlice(&lenBuf); + try out.appendSlice(value); +} +