feat: add deletionRestrictedByForeignKey
This commit is contained in:
1 parent
5acea0fb1c
commit
f16fc2ab58
1 file changed
+161
@@ -149,3 +149,164 @@ pub const TableTxn = struct {
|
||||
if (!parsed.predicate.evaluate(&newRow)) return error.CheckViolation;
|
||||
}
|
||||
|
||||
// UNIQUE and FOREIGN KEY checks run before any staged mutation
|
||||
// below, same all-or-nothing placement as `query.zig`'s `Table.put`:
|
||||
// a violation leaves the transaction's write set exactly as it was
|
||||
// before this call, not staged with a partial row. Both checks go
|
||||
// through `Transaction.scanPrefix`, so a UNIQUE value or FK target
|
||||
// staged by an EARLIER `put` call in this same still-open
|
||||
// transaction (a different row, possibly a different `TableTxn`
|
||||
// bound to the same `Transaction`) is caught here already, not left
|
||||
// to surface as `error.Conflict` at `commit()`.
|
||||
for (self.unique.fields.items) |field| {
|
||||
const newValue = newRow.get(field) orelse continue;
|
||||
const oldValue = if (old) |*o| o.get(field) else null;
|
||||
if (oldValue != null and Value.eql(oldValue.?, newValue)) continue;
|
||||
const prefix = try idx.indexValueBound(self.allocator, self.name, field, newValue);
|
||||
defer self.allocator.free(prefix);
|
||||
const entries = try self.txn.scanPrefix(self.allocator, prefix);
|
||||
defer txnmod.Transaction.freeEntries(self.allocator, entries);
|
||||
for (entries) |e| {
|
||||
const ownerPk = try rowmod.decodeValue(self.allocator, e.value);
|
||||
defer ownerPk.free(self.allocator);
|
||||
if (!Value.eql(ownerPk, pk)) return error.UniqueViolation;
|
||||
}
|
||||
}
|
||||
|
||||
for (self.foreignKeys.specs.items) |spec| {
|
||||
const newValue = newRow.get(spec.field) orelse continue;
|
||||
if (newValue == .none) continue;
|
||||
if (!try self.foreignKeyTargetExists(spec.refTable, spec.refField, newValue)) return error.ForeignKeyViolation;
|
||||
}
|
||||
|
||||
// ON DELETE RESTRICT's update-side counterpart, see
|
||||
// `updateRestrictedByForeignKey`'s doc comment - same all-or-nothing
|
||||
// placement as every other constraint check above.
|
||||
if (try self.updateRestrictedByForeignKey(pk, if (old) |*o| o else null, &newRow)) return error.ForeignKeyInUse;
|
||||
|
||||
for (self.indexes.fields.items) |indexedField| {
|
||||
const newValue = newRow.get(indexedField);
|
||||
const oldValue = if (old) |*o| o.get(indexedField) else null;
|
||||
if (oldValue != null and (newValue == null or !Value.eql(oldValue.?, newValue.?))) {
|
||||
try self.stageIndexRemove(indexedField, oldValue.?, pk);
|
||||
}
|
||||
if (newValue) |nv| {
|
||||
if (oldValue == null or !Value.eql(oldValue.?, nv)) {
|
||||
try self.stageIndexAdd(indexedField, nv, pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try self.txn.put(key, encoded);
|
||||
}
|
||||
|
||||
/// Mirrors `query.zig`'s `Table.deletionRestrictedByForeignKey` (SQL's
|
||||
/// `ON DELETE RESTRICT`), reading through `self.txn.scanPrefix` instead
|
||||
/// of a plain `Db` scan - the one thing this version can do that the
|
||||
/// plain `Table` path cannot: a child row staged earlier in this SAME
|
||||
/// uncommitted transaction (via a sibling `TableTxn` on the child
|
||||
/// table, sharing this `Transaction`) already counts as a live
|
||||
/// reference, not just whatever is durably committed. `referencedBy`
|
||||
/// itself is still loaded fresh from `self.txn.db` rather than cached
|
||||
/// (schema metadata - like `indexes`/`unique`/`foreignKeys` above - is
|
||||
/// only ever mutated through `query.zig`'s `Table`, never through a
|
||||
/// `TableTxn`, but unlike those, `ReferencedBySet` is written by some
|
||||
/// OTHER table's `createForeignKey` call, so a `TableTxn` opened before
|
||||
/// that ran cannot rely on whatever it loaded at `open()` time either -
|
||||
/// see `Table.createForeignKey`'s doc comment for the same reasoning).
|
||||
fn deletionRestrictedByForeignKey(self: *TableTxn, pk: Value, old: *const Row) !bool {
|
||||
var referencedBy = try idx.ReferencedBySet.load(self.allocator, self.txn.db, self.name);
|
||||
defer referencedBy.deinit();
|
||||
|
||||
for (referencedBy.specs.items) |spec| {
|
||||
const value = old.get(spec.refField) orelse continue;
|
||||
if (value == .none) continue;
|
||||
|
||||
const prefix = try idx.rowPrefix(self.allocator, spec.childTable);
|
||||
defer self.allocator.free(prefix);
|
||||
const entries = try self.txn.scanPrefix(self.allocator, prefix);
|
||||
defer Transaction.freeEntries(self.allocator, entries);
|
||||
|
||||
const selfReferencing = std.mem.eql(u8, spec.childTable, self.name);
|
||||
for (entries) |entry| {
|
||||
var r = try Row.decode(self.allocator, entry.value);
|
||||
defer r.deinit();
|
||||
if (selfReferencing) {
|
||||
if (r.get(pkField)) |rowPk| {
|
||||
if (Value.eql(rowPk, pk)) continue;
|
||||
}
|
||||
}
|
||||
const childValue = r.get(spec.childField) orelse continue;
|
||||
if (childValue == .none) continue;
|
||||
if (Value.eql(childValue, value)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// `TableTxn.put`'s counterpart to `deletionRestrictedByForeignKey`,
|
||||
/// mirroring `query.zig`'s `Table.updateRestrictedByForeignKey` (see its
|
||||
/// doc comment for why an update needs the same RESTRICT as a delete)
|
||||
/// but reading through `self.txn.scanPrefix` like the delete-restrict
|
||||
/// version above it, so a child row staged earlier in this same
|
||||
/// uncommitted transaction already counts as a live reference. `old` may
|
||||
/// be null (a fresh insert has nothing to pull out from under anyone),
|
||||
/// and a `refField` whose new value equals its old value is exempt, the
|
||||
/// same exemption `put`'s own UNIQUE check above makes.
|
||||
fn updateRestrictedByForeignKey(self: *TableTxn, pk: Value, old: ?*const Row, newRow: *const Row) !bool {
|
||||
var referencedBy = try idx.ReferencedBySet.load(self.allocator, self.txn.db, self.name);
|
||||
defer referencedBy.deinit();
|
||||
|
||||
for (referencedBy.specs.items) |spec| {
|
||||
const oldValue = if (old) |o| o.get(spec.refField) else null;
|
||||
if (oldValue == null or oldValue.? == .none) continue;
|
||||
const newValue = newRow.get(spec.refField);
|
||||
if (newValue != null and Value.eql(oldValue.?, newValue.?)) continue;
|
||||
|
||||
const prefix = try idx.rowPrefix(self.allocator, spec.childTable);
|
||||
defer self.allocator.free(prefix);
|
||||
const entries = try self.txn.scanPrefix(self.allocator, prefix);
|
||||
defer Transaction.freeEntries(self.allocator, entries);
|
||||
|
||||
const selfReferencing = std.mem.eql(u8, spec.childTable, self.name);
|
||||
for (entries) |entry| {
|
||||
var r = try Row.decode(self.allocator, entry.value);
|
||||
defer r.deinit();
|
||||
if (selfReferencing) {
|
||||
if (r.get(pkField)) |rowPk| {
|
||||
if (Value.eql(rowPk, pk)) continue;
|
||||
}
|
||||
}
|
||||
const childValue = r.get(spec.childField) orelse continue;
|
||||
if (childValue == .none) continue;
|
||||
if (Value.eql(childValue, oldValue.?)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn delete(self: *TableTxn, pk: Value) !void {
|
||||
var old = try self.get(pk) orelse return;
|
||||
defer old.deinit();
|
||||
|
||||
if (try self.deletionRestrictedByForeignKey(pk, &old)) return error.ForeignKeyInUse;
|
||||
|
||||
for (self.indexes.fields.items) |indexedField| {
|
||||
if (old.get(indexedField)) |v| {
|
||||
try self.stageIndexRemove(indexedField, v, pk);
|
||||
}
|
||||
}
|
||||
|
||||
const key = try idx.rowKey(self.allocator, self.name, pk);
|
||||
defer self.allocator.free(key);
|
||||
try self.txn.delete(key);
|
||||
}
|
||||
|
||||
fn stageIndexAdd(self: *TableTxn, field: []const u8, value: Value, pk: Value) !void {
|
||||
const key = try idx.indexEntryKey(self.allocator, self.name, field, value, pk);
|
||||
defer self.allocator.free(key);
|
||||
const pkBytes = try rowmod.encodeValue(self.allocator, pk);
|
||||
defer self.allocator.free(pkBytes);
|
||||
try self.txn.put(key, pkBytes);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user