feat: add UniqueSet and load to index

This commit is contained in:
dualfroz committed 2026-09-04 16:28:57 +02:00
1 parent 2e1b027571
commit 51cf300adb
1 file changed
+167
+167
View File
@@ -306,3 +306,170 @@ pub const IndexSet = struct {
} }
}; };
/// Persisted set of field names carrying a UNIQUE constraint on a table -
/// same on-disk shape as `IndexSet` (own meta key, own namespace), kept as
/// a separate type because the two sets have different invariants: every
/// member of `UniqueSet` is also required to be a member of the table's
/// `IndexSet` (enforced by `Table.createUniqueIndex`/`Table.dropIndex`),
/// since `Table.put` detects a colliding value by reusing the field's
/// regular index entries via `scanEq` rather than a full table scan - a
/// unique field with no backing index would make every write pay for a
/// full scan just to check the constraint.
pub const UniqueSet = struct {
allocator: Allocator,
fields: std.ArrayList([]u8),
pub fn load(allocator: Allocator, db: *Db, table: []const u8) !UniqueSet {
const key = try uniqueMetaKey(allocator, table);
defer allocator.free(key);
var set = UniqueSet{ .allocator = allocator, .fields = std.ArrayList([]u8).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) {
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 name = try allocator.dupe(u8, raw[pos .. pos + len]);
pos += len;
try set.fields.append(name);
}
return set;
}
pub fn save(self: *const UniqueSet, db: *Db, table: []const u8) !void {
const key = try uniqueMetaKey(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.fields.items.len), .little);
try out.appendSlice(&countBuf);
for (self.fields.items) |name| {
var lenBuf: [4]u8 = undefined;
std.mem.writeInt(u32, &lenBuf, @intCast(name.len), .little);
try out.appendSlice(&lenBuf);
try out.appendSlice(name);
}
try db.put(key, out.items);
}
pub fn has(self: *const UniqueSet, field: []const u8) bool {
for (self.fields.items) |f| {
if (std.mem.eql(u8, f, field)) return true;
}
return false;
}
pub fn add(self: *UniqueSet, field: []const u8) !void {
if (self.has(field)) return;
const copy = try self.allocator.dupe(u8, field);
try self.fields.append(copy);
}
/// Returns true if the field was present and has been removed.
pub fn remove(self: *UniqueSet, field: []const u8) bool {
for (self.fields.items, 0..) |f, i| {
if (std.mem.eql(u8, f, field)) {
self.allocator.free(f);
_ = self.fields.orderedRemove(i);
return true;
}
}
return false;
}
pub fn deinit(self: *UniqueSet) void {
for (self.fields.items) |f| self.allocator.free(f);
self.fields.deinit();
}
};
/// One FOREIGN KEY constraint: `field` on the owning table must, on every
/// row where it is present and non-none, name a value that exists as
/// `refField` on `refTable` (SQL's `FOREIGN KEY (field) REFERENCES
/// refTable(refField)`). `refField` is either `query.pkField` (the
/// referenced table's primary key, checked via a plain `get`) or a field
/// carrying its own UNIQUE constraint on `refTable` (checked via `scanEq`) -
/// `Table.createForeignKey` requires one of the two before installing,
/// exactly like a real database refusing a foreign key against a column
/// with no unique/primary-key backing, since otherwise "the value exists"
/// would not pin down a single unambiguous parent row.
/// SQL's `ON DELETE` clause: what a FOREIGN KEY does when the row it points
/// at is deleted. `restrict` (the SQL default, and this codebase's only
/// behavior before cascading deletes existed) refuses the delete outright
/// while a referencing row still exists; `cascade` instead deletes every
/// referencing row too, recursively (a cascaded child can itself have
/// dependents), before the parent row itself is removed. Stored as a plain
/// byte in `ForeignKeySpec`/`ReferencedBySpec` rather than a bare bool so a
/// third mode (e.g. `set_null`) can be added later without another format
/// bump.
pub const OnDelete = enum(u8) {
restrict = 0,
cascade = 1,
};
fn onDeleteFromByte(b: u8) !OnDelete {
return switch (b) {
0 => .restrict,
1 => .cascade,
else => error.Malformed,
};
}
pub const ForeignKeySpec = struct {
field: []u8,
refTable: []u8,
refField: []u8,
onDelete: OnDelete,
};
/// Persisted set of FOREIGN KEY constraints on a table, at most one per
/// owning field - same on-disk shape family as `IndexSet`/`UniqueSet` (own
/// meta key, own namespace) but each entry carries two extra strings
/// (`refTable`/`refField`) plus an `onDelete` mode byte instead of being a
/// bare field name.
pub const ForeignKeySet = struct {
allocator: Allocator,
specs: std.ArrayList(ForeignKeySpec),
pub fn load(allocator: Allocator, db: *Db, table: []const u8) !ForeignKeySet {
const key = try foreignKeyMetaKey(allocator, table);
defer allocator.free(key);
var set = ForeignKeySet{ .allocator = allocator, .specs = std.ArrayList(ForeignKeySpec).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 field = try readLenPrefixed(allocator, raw, &pos);
errdefer allocator.free(field);
const refTable = try readLenPrefixed(allocator, raw, &pos);
errdefer allocator.free(refTable);
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(.{ .field = field, .refTable = refTable, .refField = refField, .onDelete = onDelete });
}
return set;
}