feat: add UniqueSet and load to index
This commit is contained in:
1 parent
2e1b027571
commit
51cf300adb
1 file changed
+167
+167
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user