diff --git a/src/index.zig b/src/index.zig index e418ab2..4790266 100644 --- a/src/index.zig +++ b/src/index.zig @@ -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; + } +