feat: add deinit and ReferencedBySpec to index

This commit is contained in:
dualfroz committed 2026-09-04 17:09:48 +02:00
1 parent 43b27efecc
commit fff7ffeb78
1 file changed
+156
+156
View File
@@ -634,3 +634,159 @@ pub const CheckConstraintSet = struct {
return false; 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 <this table>(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);
}