feat: add createCheckConstraint and dropCheckConstraint to query
This commit is contained in:
1 parent
7de513e158
commit
b20d2b95ce
1 file changed
+159
+159
@@ -476,3 +476,162 @@ pub const Table = struct {
|
|||||||
return self.checks.has(name);
|
return self.checks.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Installs a named CHECK constraint - SQL's `CHECK (source)` - built on
|
||||||
|
/// the exact same predicate language `table select`/`table join` already
|
||||||
|
/// use (`expr.zig`'s recursive-descent parser over `Predicate`). `source`
|
||||||
|
/// is parsed once up front so a typo is rejected before anything is
|
||||||
|
/// persisted (`error.InvalidCheckExpression` wraps any `expr.ParseError`,
|
||||||
|
/// since a caller that only sees this constraint's name/source has no use
|
||||||
|
/// for which specific parse error fired), then evaluated against every
|
||||||
|
/// existing row - the same "refuse before mutating, existing data always
|
||||||
|
/// wins" shape `createUniqueIndex`/`createForeignKey` use, so installing
|
||||||
|
/// a CHECK that current data already violates fails outright rather than
|
||||||
|
/// silently starting to enforce only from the next write on.
|
||||||
|
pub fn createCheckConstraint(self: *Table, name: []const u8, source: []const u8) !void {
|
||||||
|
if (!idx.validIdentifier(name)) return error.InvalidIdentifier;
|
||||||
|
if (self.checks.has(name)) return;
|
||||||
|
|
||||||
|
var parsed = exprmod.parse(self.allocator, source) catch return error.InvalidCheckExpression;
|
||||||
|
defer parsed.deinit();
|
||||||
|
|
||||||
|
const rows = try self.scanAll();
|
||||||
|
defer freeRows(self.allocator, rows);
|
||||||
|
for (rows) |*r| {
|
||||||
|
if (!parsed.predicate.evaluate(r)) return error.CheckViolation;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.checks.add(name, source);
|
||||||
|
try self.checks.save(self.db, self.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dropCheckConstraint(self: *Table, name: []const u8) !void {
|
||||||
|
if (!self.checks.remove(name)) return;
|
||||||
|
try self.checks.save(self.db, self.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a multi-column secondary index named `name` over `fields`, in
|
||||||
|
/// order - order matters, since only a leading ("leftmost") prefix of it
|
||||||
|
/// can be used to narrow a query (see `findCompositeMatch`). Rows
|
||||||
|
/// missing any of the fields are skipped entirely, both here and when
|
||||||
|
/// staying in sync on `put`/`delete`, since a composite entry with a
|
||||||
|
/// missing column would not be a well-formed tuple.
|
||||||
|
pub fn createCompositeIndex(self: *Table, name: []const u8, fields: []const []const u8) !void {
|
||||||
|
if (!idx.validIdentifier(name)) return error.InvalidIdentifier;
|
||||||
|
if (fields.len < 2 or fields.len > maxCompositeFields) return error.InvalidCompositeIndex;
|
||||||
|
for (fields) |f| if (!idx.validIdentifier(f)) return error.InvalidIdentifier;
|
||||||
|
if (self.composite.has(name)) return;
|
||||||
|
|
||||||
|
const rows = try self.scanAll();
|
||||||
|
defer freeRows(self.allocator, rows);
|
||||||
|
var values: [maxCompositeFields]Value = undefined;
|
||||||
|
for (rows) |*r| {
|
||||||
|
const pk = r.get(pkField) orelse continue;
|
||||||
|
var complete = true;
|
||||||
|
for (fields, 0..) |f, i| {
|
||||||
|
values[i] = r.get(f) orelse {
|
||||||
|
complete = false;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!complete) continue;
|
||||||
|
try idx.addCompositeEntry(self.db, self.allocator, self.name, name, values[0..fields.len], pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.composite.add(name, fields);
|
||||||
|
try self.composite.save(self.db, self.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dropCompositeIndex(self: *Table, name: []const u8) !void {
|
||||||
|
if (!self.composite.remove(name)) return;
|
||||||
|
try idx.dropAllCompositeEntries(self.db, self.allocator, self.name, name);
|
||||||
|
try self.composite.save(self.db, self.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasCompositeIndex(self: *const Table, name: []const u8) bool {
|
||||||
|
return self.composite.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fully replaces the row stored at `pk` with `fields`, syncing every
|
||||||
|
/// registered secondary index against whatever the row held before.
|
||||||
|
pub fn put(self: *Table, pk: Value, fields: []const Field) !void {
|
||||||
|
var old = try self.get(pk);
|
||||||
|
defer if (old) |*o| o.deinit();
|
||||||
|
|
||||||
|
var newRow = Row.init(self.allocator);
|
||||||
|
defer newRow.deinit();
|
||||||
|
try newRow.set(pkField, pk);
|
||||||
|
for (fields) |f| try newRow.set(f.name, f.value);
|
||||||
|
|
||||||
|
const encoded = try newRow.encode(self.allocator);
|
||||||
|
defer self.allocator.free(encoded);
|
||||||
|
const key = try idx.rowKey(self.allocator, self.name, pk);
|
||||||
|
defer self.allocator.free(key);
|
||||||
|
|
||||||
|
// CHECK constraints run first, ahead of UNIQUE/FOREIGN KEY and any
|
||||||
|
// index or row mutation - a row that fails a CHECK never gets far
|
||||||
|
// enough to matter whether it would also have violated UNIQUE, same
|
||||||
|
// "cheapest, purely-local check first" ordering a real database
|
||||||
|
// uses. Re-parsed from source on every put (no cached Predicate is
|
||||||
|
// kept on Table), the same cost `table select`/`table join` already
|
||||||
|
// pay for their own expr text on every call.
|
||||||
|
for (self.checks.specs.items) |spec| {
|
||||||
|
var parsed = exprmod.parse(self.allocator, spec.source) catch return error.InvalidCheckExpression;
|
||||||
|
defer parsed.deinit();
|
||||||
|
if (!parsed.predicate.evaluate(&newRow)) return error.CheckViolation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNIQUE constraint check runs before any index or row mutation
|
||||||
|
// below, so a violation leaves everything untouched - the same
|
||||||
|
// all-or-nothing shape callers already get from a plain `put`
|
||||||
|
// either succeeding outright or failing outright. A field that
|
||||||
|
// did not change value from the row's own previous value is
|
||||||
|
// exempt (it already legitimately owns that value), which is why
|
||||||
|
// updating unrelated fields on a row that already holds a unique
|
||||||
|
// value never spuriously conflicts with itself.
|
||||||
|
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 owners = try idx.scanEq(self.db, self.allocator, self.name, field, newValue);
|
||||||
|
defer {
|
||||||
|
for (owners) |v| v.free(self.allocator);
|
||||||
|
self.allocator.free(owners);
|
||||||
|
}
|
||||||
|
for (owners) |ownerPk| {
|
||||||
|
if (!Value.eql(ownerPk, pk)) return error.UniqueViolation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOREIGN KEY check, same "before any mutation" placement as UNIQUE
|
||||||
|
// above. Unlike UNIQUE, an unchanged value is NOT exempt: nothing in
|
||||||
|
// this codebase stops the referenced row from having been deleted
|
||||||
|
// out from under an already-valid reference since the last write
|
||||||
|
// (no catalog of dependent tables exists to block that delete - see
|
||||||
|
// `Table.createForeignKey`'s doc comment), so every put re-checks
|
||||||
|
// every constrained field's current target rather than trusting a
|
||||||
|
// value that merely did not change this time.
|
||||||
|
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 - must run before any
|
||||||
|
// index or row mutation below, same all-or-nothing placement as
|
||||||
|
// every other constraint check in this function.
|
||||||
|
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 idx.removeEntry(self.db, self.allocator, self.name, indexedField, oldValue.?, pk);
|
||||||
|
}
|
||||||
|
if (newValue) |nv| {
|
||||||
|
if (oldValue == null or !Value.eql(oldValue.?, nv)) {
|
||||||
|
try idx.addEntry(self.db, self.allocator, self.name, indexedField, nv, pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user