feat: add hasForeignKey and foreignKeyTargetExists to query
This commit is contained in:
1 parent
5512aa824a
commit
7de513e158
1 file changed
+155
+155
@@ -321,3 +321,158 @@ pub const Table = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (!self.indexes.has(field)) {
|
||||
for (rows) |*r| {
|
||||
const pk = r.get(pkField) orelse continue;
|
||||
const value = r.get(field) orelse continue;
|
||||
try idx.addEntry(self.db, self.allocator, self.name, field, value, pk);
|
||||
}
|
||||
try self.indexes.add(field);
|
||||
try self.indexes.save(self.db, self.name);
|
||||
}
|
||||
|
||||
try self.unique.add(field);
|
||||
try self.unique.save(self.db, self.name);
|
||||
}
|
||||
|
||||
pub fn hasForeignKey(self: *const Table, field: []const u8) bool {
|
||||
return self.foreignKeys.has(field);
|
||||
}
|
||||
|
||||
/// Whether `value` names an existing row of `spec.refTable` under
|
||||
/// `spec.refField` - `query.pkField` is checked via a plain `get`
|
||||
/// (the primary key is inherently unique, no secondary index needed),
|
||||
/// anything else via `idx.scanEq` against `spec.refTable`'s own index
|
||||
/// entries (required to carry a UNIQUE constraint by
|
||||
/// `Table.createForeignKey`, so at most one owner is expected here -
|
||||
/// this only asks "does at least one exist", not "is it exactly one",
|
||||
/// since a constraint installed correctly can only ever produce zero or
|
||||
/// one).
|
||||
fn foreignKeyTargetExists(self: *Table, refTableName: []const u8, refField: []const u8, value: Value) !bool {
|
||||
if (std.mem.eql(u8, refField, pkField)) {
|
||||
var refTable = try Table.open(self.db, self.allocator, refTableName);
|
||||
defer refTable.deinit();
|
||||
var found = try refTable.get(value) orelse return false;
|
||||
found.deinit();
|
||||
return true;
|
||||
}
|
||||
const owners = try idx.scanEq(self.db, self.allocator, refTableName, refField, value);
|
||||
defer {
|
||||
for (owners) |v| v.free(self.allocator);
|
||||
self.allocator.free(owners);
|
||||
}
|
||||
return owners.len > 0;
|
||||
}
|
||||
|
||||
/// Installs a FOREIGN KEY constraint: every row's (non-null) `field`
|
||||
/// value must, from this point on, name a value that exists as
|
||||
/// `refField` on `refTable` - SQL's `FOREIGN KEY (field) REFERENCES
|
||||
/// refTable(refField)`. `refField` must be either `pkField` or a field
|
||||
/// already carrying `refTable`'s own UNIQUE constraint (see
|
||||
/// `foreignKeyTargetExists`'s doc comment for why); anything else is
|
||||
/// refused with `error.ForeignKeyRequiresUniqueTarget` before touching
|
||||
/// any row, the same "refuse before mutating" shape `createUniqueIndex`
|
||||
/// uses for dirty data. Existing rows are checked against the target
|
||||
/// before the constraint is recorded - a row whose current `field`
|
||||
/// value has no matching parent aborts the whole install with
|
||||
/// `error.ForeignKeyViolation`, leaving nothing installed.
|
||||
///
|
||||
/// Registers the reverse relationship in `refTable`'s own
|
||||
/// `ReferencedBySet` (see its doc comment) so `Table.delete` can later
|
||||
/// enforce `onDelete` against it (SQL's `ON DELETE RESTRICT` or `ON
|
||||
/// DELETE CASCADE`, see `idx.OnDelete`'s doc comment) - this used to be
|
||||
/// a documented, unfixed gap ("no registry of dependent tables exists,
|
||||
/// since tables are opened ad hoc by name and never catalogued
|
||||
/// anywhere") before `ReferencedBySet` closed it.
|
||||
///
|
||||
/// `onDelete` only governs `Table.delete`/`Table.deleteWhere`'s plain
|
||||
/// (non-transactional) path - `txn_table.zig`'s `TableTxn.delete` reads
|
||||
/// the same `ReferencedBySet` but does not look at this field at all,
|
||||
/// so it continues to unconditionally restrict any delete that would
|
||||
/// orphan a reference even for a `cascade` constraint. That is a
|
||||
/// deliberately conservative gap, not an oversight: refusing is always
|
||||
/// a safe fallback for a feature that has not been built yet, whereas
|
||||
/// silently NOT cascading inside a transaction would leave a dangling
|
||||
/// reference behind. Only `Table.put`'s `updateRestrictedByForeignKey`
|
||||
/// check is unaffected by `onDelete` in EITHER path - there is no `ON
|
||||
/// UPDATE CASCADE` here, so a value change under a live reference is
|
||||
/// always restricted regardless of the FK's delete mode.
|
||||
///
|
||||
/// Known gap that registry does NOT close, same documented-rather-
|
||||
/// than-fixed shape as `TableTxn`'s missing composite/unique
|
||||
/// enforcement: nothing here stops a later `refTable.dropIndex
|
||||
/// (refField)` from removing the UNIQUE constraint this foreign key
|
||||
/// depends on - doing so leaves the constraint silently checking
|
||||
/// against an unconstrained (possibly duplicate-holding) column via
|
||||
/// `scanEq`, which still answers "does at least one owner exist"
|
||||
/// correctly but no longer guarantees at most one.
|
||||
pub fn createForeignKey(self: *Table, field: []const u8, refTable: []const u8, refField: []const u8, onDelete: idx.OnDelete) !void {
|
||||
if (!idx.validIdentifier(field)) return error.InvalidIdentifier;
|
||||
if (!idx.validIdentifier(refTable)) return error.InvalidIdentifier;
|
||||
if (!idx.validIdentifier(refField)) return error.InvalidIdentifier;
|
||||
if (self.foreignKeys.has(field)) return;
|
||||
|
||||
if (!std.mem.eql(u8, refField, pkField)) {
|
||||
var ref = try Table.open(self.db, self.allocator, refTable);
|
||||
defer ref.deinit();
|
||||
if (!ref.hasUniqueIndex(refField)) return error.ForeignKeyRequiresUniqueTarget;
|
||||
}
|
||||
|
||||
const rows = try self.scanAll();
|
||||
defer freeRows(self.allocator, rows);
|
||||
for (rows) |*r| {
|
||||
const value = r.get(field) orelse continue;
|
||||
if (value == .none) continue;
|
||||
if (!try self.foreignKeyTargetExists(refTable, refField, value)) return error.ForeignKeyViolation;
|
||||
}
|
||||
|
||||
try self.foreignKeys.add(field, refTable, refField, onDelete);
|
||||
try self.foreignKeys.save(self.db, self.name);
|
||||
|
||||
// Records the reverse relationship on `refTable` so a later delete
|
||||
// against it (see `Table.delete`'s RESTRICT check) can discover
|
||||
// this constraint without a global table catalog - see
|
||||
// `ReferencedBySet`'s doc comment. Loaded, mutated, and saved
|
||||
// transiently rather than cached on a `Table` field: unlike
|
||||
// `foreignKeys`/`unique`/`checks` (always mutated through the same
|
||||
// handle that holds them), `referencedBy` is mutated as a side
|
||||
// effect of some OTHER table's `createForeignKey`/`dropForeignKey`
|
||||
// call - a `refTable` handle the CALLER already has open (the
|
||||
// ordinary "open customers, open orders, wire up the FK between
|
||||
// them" shape) would otherwise go stale the moment this runs,
|
||||
// silently under-enforcing every delete against it thereafter.
|
||||
// Works unchanged for a self-referencing FK (`refTable ==
|
||||
// self.name`) too, since it never touches `self`'s own cached
|
||||
// sets at all - it only ever reads/writes `refTable`'s on-disk
|
||||
// entry by name.
|
||||
var revSet = try ReferencedBySet.load(self.allocator, self.db, refTable);
|
||||
defer revSet.deinit();
|
||||
try revSet.add(self.name, field, refField, onDelete);
|
||||
try revSet.save(self.db, refTable);
|
||||
}
|
||||
|
||||
/// Lifts a FOREIGN KEY and its reverse `ReferencedBySet` entry on
|
||||
/// whichever table it pointed at, symmetric with `createForeignKey`
|
||||
/// installing both. The target's name is copied out before
|
||||
/// `self.foreignKeys.remove` frees the spec's own strings, since the
|
||||
/// reverse cleanup below still needs it afterwards. See
|
||||
/// `createForeignKey`'s doc comment on why the reverse entry is always
|
||||
/// loaded/saved transiently rather than through a cached `Table` field.
|
||||
pub fn dropForeignKey(self: *Table, field: []const u8) !void {
|
||||
const existing = self.foreignKeys.find(field) orelse return;
|
||||
const refTable = try self.allocator.dupe(u8, existing.refTable);
|
||||
defer self.allocator.free(refTable);
|
||||
|
||||
_ = self.foreignKeys.remove(field);
|
||||
try self.foreignKeys.save(self.db, self.name);
|
||||
|
||||
var revSet = try ReferencedBySet.load(self.allocator, self.db, refTable);
|
||||
defer revSet.deinit();
|
||||
_ = revSet.remove(self.name, field);
|
||||
try revSet.save(self.db, refTable);
|
||||
}
|
||||
|
||||
pub fn hasCheckConstraint(self: *const Table, name: []const u8) bool {
|
||||
return self.checks.has(name);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user