Files
cobalt/src/query.zig
T

948 lines
45 KiB
Zig

const std = @import("std");
const dbmod = @import("db.zig");
const rowmod = @import("row.zig");
const idx = @import("index.zig");
const exprmod = @import("expr.zig");
const Allocator = std.mem.Allocator;
const Db = dbmod.Db;
const Value = rowmod.Value;
const Row = rowmod.Row;
const Field = rowmod.Field;
const IndexSet = idx.IndexSet;
const CompositeIndexSet = idx.CompositeIndexSet;
const CompositeIndexSpec = idx.CompositeIndexSpec;
const UniqueSet = idx.UniqueSet;
const ForeignKeySet = idx.ForeignKeySet;
const CheckConstraintSet = idx.CheckConstraintSet;
const ReferencedBySet = idx.ReferencedBySet;
/// Reserved field name a Table stores the primary key under, so a decoded
/// Row is self-sufficient for callers that only have the Row (e.g. results
/// of a full table scan) without needing to separately decode it from the
/// row key's escaped ordered bytes.
pub const pkField = "__pk";
pub const Condition = struct {
field: []const u8,
value: Value,
};
/// Paging window for `Table.selectPage`: skip the first `offset` matches,
/// return at most `limit` of them (0 = unlimited).
pub const Page = struct {
offset: usize = 0,
limit: usize = 0,
};
/// One column of a `Table.selectPageOrdered` sort key: order matched rows
/// by `field` (via `rowmod.compare`'s total order across value types). A
/// row missing `field` entirely sorts as `Value.none`, which `compare`
/// places before every other tag - so rows lacking the sort column
/// consistently group at the front (ascending) or back (descending)
/// instead of erroring. `selectPageOrdered` takes a `[]const OrderBy`
/// (empty = unsorted): columns after the first only break ties left by
/// the ones before them, the same lexicographic rule SQL's `ORDER BY a, b`
/// follows - the sort is stable, so a `[]const OrderBy` with a single
/// entry that leaves ties unresolved behaves exactly as before this type
/// gained multi-column support (rows keep their relative scan order:
/// ascending pk for a full scan, index order for an indexed lookup).
pub const OrderBy = struct {
field: []const u8,
descending: bool = false,
};
/// Which access strategy `Table.selectPage` would take for a given
/// predicate, as reported by `Table.explain`. Mirrors the three real
/// branches of `selectPage`'s decision ladder: an unindexed full scan, a
/// single-field index (`eq` narrows to one key, `lt`/`lte`/`gt`/`gte`
/// narrow to a range), or a composite index matched on a leftmost prefix
/// of its columns (`_eq` when the prefix is the whole match, `_range` when
/// the column right after the prefix also had a range bound).
pub const PlanKind = enum(u8) {
full_scan,
index_eq,
index_range,
composite_eq,
composite_range,
};
/// `indexName` is an owned copy the caller must free with `Plan.deinit` -
/// null (and `matchedFields`/`totalFields` both 0) for `.full_scan`, which
/// names no index. For `.index_eq`/`.index_range` it is the single indexed
/// field name and `matchedFields == totalFields == 1`. For
/// `.composite_eq`/`.composite_range` it is the composite index's name and
/// `matchedFields` counts how many of the index's `totalFields` leading
/// columns the query actually pinned down (a strict prefix match still
/// narrows the scan, same leftmost-prefix rule a real multi-column B-tree
/// index gives you).
pub const Plan = struct {
kind: PlanKind,
indexName: ?[]const u8 = null,
matchedFields: usize = 0,
totalFields: usize = 0,
pub fn deinit(self: *Plan, allocator: Allocator) void {
if (self.indexName) |n| allocator.free(n);
self.* = undefined;
}
};
/// Predicate AST evaluated against a decoded Row. Leaves compare a named
/// field to a literal Value; And/Or/Not compose leaves and other predicates.
/// Predicate does not own the memory of any Value or field-name slice it
/// references - callers must keep those alive for as long as the predicate
/// tree is used.
pub const Predicate = union(enum) {
eq: Condition,
ne: Condition,
lt: Condition,
lte: Condition,
gt: Condition,
gte: Condition,
like: Condition,
/// True when `field` is absent from the row entirely, or present but
/// holding an explicit `Value.none` - a sparse row's "never set" and a
/// row that set the field to none are indistinguishable to a caller
/// asking "does this row have a real value here", same as SQL NULL.
is_null: []const u8,
is_not_null: []const u8,
and_: []const Predicate,
or_: []const Predicate,
not_: *const Predicate,
fn isNullish(v: ?Value) bool {
const val = v orelse return true;
return val == .none;
}
pub fn evaluate(self: Predicate, row: *const Row) bool {
return switch (self) {
.eq => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) == .eq else false,
.ne => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) != .eq else true,
.lt => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) == .lt else false,
.lte => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) != .gt else false,
.gt => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) == .gt else false,
.gte => |c| if (row.get(c.field)) |v| rowmod.compare(v, c.value) != .lt else false,
.like => |c| blk: {
const rowVal = row.get(c.field) orelse break :blk false;
if (rowVal != .text or c.value != .text) break :blk false;
break :blk matchLike(c.value.text, rowVal.text);
},
.is_null => |field| isNullish(row.get(field)),
.is_not_null => |field| !isNullish(row.get(field)),
.and_ => |preds| blk: {
for (preds) |p| {
if (!p.evaluate(row)) break :blk false;
}
break :blk true;
},
.or_ => |preds| blk: {
for (preds) |p| {
if (p.evaluate(row)) break :blk true;
}
break :blk false;
},
.not_ => |p| !p.evaluate(row),
};
}
};
/// SQL-style LIKE matching: `%` matches any run of characters (including
/// none), `_` matches exactly one character, every other byte in `pattern`
/// must match `text` literally. No escape character - a pattern that needs a
/// literal `%`/`_` cannot express it, same simplification `expr.zig`'s other
/// operators already make (no escaping in the predicate language at all).
/// Iterative two-pointer wildcard matcher (not recursive backtracking) so a
/// pattern like "a%a%a%a%a%a%b" against a long non-matching text stays
/// linear-ish instead of exponential: on mismatch after a `%`, only the
/// text pointer rewinds-and-retries, the pattern never re-walks from
/// scratch.
fn matchLike(pattern: []const u8, text: []const u8) bool {
var pIdx: usize = 0;
var tIdx: usize = 0;
var starIdx: ?usize = null;
var matchIdx: usize = 0;
while (tIdx < text.len) {
if (pIdx < pattern.len and (pattern[pIdx] == '_' or pattern[pIdx] == text[tIdx])) {
pIdx += 1;
tIdx += 1;
} else if (pIdx < pattern.len and pattern[pIdx] == '%') {
starIdx = pIdx;
matchIdx = tIdx;
pIdx += 1;
} else if (starIdx) |si| {
pIdx = si + 1;
matchIdx += 1;
tIdx = matchIdx;
} else {
return false;
}
}
while (pIdx < pattern.len and pattern[pIdx] == '%') : (pIdx += 1) {}
return pIdx == pattern.len;
}
test "matchLike handles literal, wildcard, and single-char patterns" {
try std.testing.expect(matchLike("warsaw", "warsaw"));
try std.testing.expect(!matchLike("warsaw", "krakow"));
try std.testing.expect(matchLike("", ""));
try std.testing.expect(!matchLike("a", ""));
try std.testing.expect(matchLike("%", "anything"));
try std.testing.expect(matchLike("%", ""));
try std.testing.expect(matchLike("A%", "Ada"));
try std.testing.expect(!matchLike("A%", "Boris"));
try std.testing.expect(matchLike("%son", "Johnson"));
try std.testing.expect(!matchLike("%son", "Sonata"));
try std.testing.expect(matchLike("%ada%", "the ada language"));
try std.testing.expect(matchLike("_da", "ada"));
try std.testing.expect(!matchLike("_da", "aada"));
try std.testing.expect(matchLike("a_a_a", "ababa"));
try std.testing.expect(matchLike("%a%b%c%", "xaxxbxxxcx"));
try std.testing.expect(!matchLike("%a%b%c%", "xxbxxxax"));
try std.testing.expect(matchLike("a%%a", "aXYZa"));
}
/// A table is a named partition of a single Db's keyspace (see index.zig for
/// the exact key layout) plus a set of secondary indexes maintained on
/// write. Rows are whole-row replace semantics: `put` fully overwrites
/// whatever was stored at `pk`, syncing every registered index against the
/// old and new field values in the same call.
/// Composite indexes are capped at this many columns: generous for any
/// realistic multi-column index while keeping the planner's per-spec match
/// buffer (see `findCompositeMatch`) a fixed-size stack array.
pub const maxCompositeFields = 8;
pub const Table = struct {
db: *Db,
allocator: Allocator,
name: []u8,
indexes: IndexSet,
composite: CompositeIndexSet,
unique: UniqueSet,
foreignKeys: ForeignKeySet,
checks: CheckConstraintSet,
pub fn open(db: *Db, allocator: Allocator, name: []const u8) !Table {
if (!idx.validIdentifier(name)) return error.InvalidIdentifier;
return .{
.db = db,
.allocator = allocator,
.name = try allocator.dupe(u8, name),
.indexes = try IndexSet.load(allocator, db, name),
.composite = try CompositeIndexSet.load(allocator, db, name),
.unique = try UniqueSet.load(allocator, db, name),
.foreignKeys = try ForeignKeySet.load(allocator, db, name),
.checks = try CheckConstraintSet.load(allocator, db, name),
};
}
pub fn deinit(self: *Table) void {
self.allocator.free(self.name);
self.indexes.deinit();
self.composite.deinit();
self.unique.deinit();
self.foreignKeys.deinit();
self.checks.deinit();
}
pub fn createIndex(self: *Table, field: []const u8) !void {
if (!idx.validIdentifier(field)) return error.InvalidIdentifier;
if (self.indexes.has(field)) return;
// Backfill: every existing row's current value for `field` becomes
// an index entry before the index is advertised as usable.
const rows = try self.scanAll();
defer freeRows(self.allocator, rows);
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);
}
/// Dropping an index also drops any UNIQUE constraint riding on it -
/// `Table.put`'s constraint check relies on the field's index entries
/// existing (see `createUniqueIndex`), so a unique field with no
/// index left behind would silently stop being enforced instead of
/// erroring, which is worse than making the drop explicit here.
pub fn dropIndex(self: *Table, field: []const u8) !void {
if (!self.indexes.remove(field)) return;
try idx.dropAllEntries(self.db, self.allocator, self.name, field);
try self.indexes.save(self.db, self.name);
if (self.unique.remove(field)) try self.unique.save(self.db, self.name);
}
pub fn hasIndex(self: *const Table, field: []const u8) bool {
return self.indexes.has(field);
}
pub fn hasUniqueIndex(self: *const Table, field: []const u8) bool {
return self.unique.has(field);
}
/// Installs a UNIQUE constraint on `field` - SQL's `CREATE UNIQUE
/// INDEX`. Refuses outright if the table already holds two rows with
/// the same (non-null) value for `field`: existing data always wins
/// over a constraint that could never have held for it, exactly like
/// a real database refusing to add a unique index over dirty data.
/// Backfills the plain secondary index too when it is not already
/// present, since `put`'s duplicate check below is a `scanEq` against
/// those very index entries rather than a full table scan - a unique
/// field with no backing index would make every write pay for a full
/// scan just to enforce the constraint.
pub fn createUniqueIndex(self: *Table, field: []const u8) !void {
if (!idx.validIdentifier(field)) return error.InvalidIdentifier;
if (self.unique.has(field)) return;
const rows = try self.scanAll();
defer freeRows(self.allocator, rows);
{
var seen = std.StringHashMap(void).init(self.allocator);
defer {
var it = seen.keyIterator();
while (it.next()) |k| self.allocator.free(k.*);
seen.deinit();
}
for (rows) |*r| {
const value = r.get(field) orelse continue;
var encoded = std.ArrayList(u8).init(self.allocator);
defer encoded.deinit();
try rowmod.appendOrdered(&encoded, value);
if (seen.contains(encoded.items)) return error.UniqueViolation;
const owned = try self.allocator.dupe(u8, encoded.items);
errdefer self.allocator.free(owned);
try seen.put(owned, {});
}
}
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);
}
/// 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);
}
}
}
for (self.composite.specs.items) |spec| {
var oldValues: [maxCompositeFields]Value = undefined;
var oldComplete = old != null;
if (old) |*o| {
for (spec.fields, 0..) |f, i| {
oldValues[i] = o.get(f) orelse {
oldComplete = false;
break;
};
}
}
var newValues: [maxCompositeFields]Value = undefined;
var newComplete = true;
for (spec.fields, 0..) |f, i| {
newValues[i] = newRow.get(f) orelse {
newComplete = false;
break;
};
}
const unchanged = oldComplete and newComplete and valueTupleEql(oldValues[0..spec.fields.len], newValues[0..spec.fields.len]);
if (unchanged) continue;
if (oldComplete) try idx.removeCompositeEntry(self.db, self.allocator, self.name, spec.name, oldValues[0..spec.fields.len], pk);
if (newComplete) try idx.addCompositeEntry(self.db, self.allocator, self.name, spec.name, newValues[0..spec.fields.len], pk);
}
try self.db.put(key, encoded);
}
pub fn get(self: *Table, pk: Value) !?Row {
const key = try idx.rowKey(self.allocator, self.name, pk);
defer self.allocator.free(key);
const raw = try self.db.get(self.allocator, key) orelse return null;
defer self.allocator.free(raw);
return try Row.decode(self.allocator, raw);
}
/// One (table, pk) a cascading delete has decided to remove, in the
/// order `planCascade` discovered it - deepest dependents first, the
/// row that was actually asked for last. Owns copies of both fields
/// independently of whatever `Row`/`Table` produced them, since the
/// scan that found them is long gone (and possibly a different
/// `Table` handle entirely) by the time `delete` executes the plan.
const CascadeAction = struct {
table: []u8,
pk: Value,
fn deinit(self: *const CascadeAction, allocator: Allocator) void {
allocator.free(self.table);
self.pk.free(allocator);
}
};
/// Cycle guard for `planCascade`: every (table, pk) already visited
/// this call, so a cascade that loops back on itself (two tables
/// `ON DELETE CASCADE`-ing at each other, or a longer cycle through
/// three or more) terminates instead of recursing forever. Linear
/// scan is fine here - a cascading delete's fan-out is bounded by how
/// much data actually exists, and this runs once per row touched, not
/// once per row scanned.
const CascadeVisited = struct {
allocator: Allocator,
entries: std.ArrayList(CascadeAction),
fn init(allocator: Allocator) CascadeVisited {
return .{ .allocator = allocator, .entries = std.ArrayList(CascadeAction).init(allocator) };
}
fn contains(self: *const CascadeVisited, table: []const u8, pk: Value) bool {
for (self.entries.items) |e| {
if (std.mem.eql(u8, e.table, table) and Value.eql(e.pk, pk)) return true;
}
return false;
}
fn add(self: *CascadeVisited, table: []const u8, pk: Value) !void {
const tableCopy = try self.allocator.dupe(u8, table);
errdefer self.allocator.free(tableCopy);
const pkCopy = try pk.dupe(self.allocator);
try self.entries.append(.{ .table = tableCopy, .pk = pkCopy });
}
fn deinit(self: *CascadeVisited) void {
for (self.entries.items) |e| e.deinit(self.allocator);
self.entries.deinit();
}
};
/// Recursively works out everything a delete of `pk` must remove,
/// WITHOUT deleting anything yet, appending each decision to `plan` in
/// dependent-before-dependency order (see `CascadeAction`'s doc
/// comment) so `delete` can execute it leaves-first. This two-phase
/// plan/execute split is what keeps a cascading delete all-or-nothing:
/// a `restrict`-mode violation discovered three levels deep must abort
/// with NOTHING mutated, exactly like the single-table check this
/// replaced - if planning and deleting were interleaved, an earlier
/// sibling subtree could already be gone by the time a later one fails.
///
/// For every `ReferencedBySet` entry naming a table with a live row
/// pointing at `old`'s value for the referenced field (see the
/// previous version of this function for why the child is opened and
/// scanned in full rather than through `idx.scanEq`, and why a
/// self-referencing FK excludes `pk` itself from matching): `restrict`
/// aborts the whole plan with `error.ForeignKeyInUse`; `cascade`
/// recurses into that child row first, so it lands in `plan` ahead of
/// `self` (a child must be gone before its parent is, or the delete
/// order would itself violate the very constraint being enforced).
///
/// `visited` is checked (and updated) BEFORE any of that per-row work,
/// so a cycle (`planCascade(A)` reaching `planCascade(B)` reaching
/// `planCascade(A)` again) short-circuits the re-entrant call instead
/// of looping - the ORIGINAL call for that (table, pk) is still the one
/// that appends it to `plan`, once its own scan finishes, so nothing is
/// silently dropped from the plan by the cycle.
fn planCascade(self: *Table, pk: Value, visited: *CascadeVisited, plan: *std.ArrayList(CascadeAction)) !void {
if (visited.contains(self.name, pk)) return;
var old = try self.get(pk) orelse return;
defer old.deinit();
try visited.add(self.name, pk);
var referencedBy = try ReferencedBySet.load(self.allocator, self.db, self.name);
defer referencedBy.deinit();
for (referencedBy.specs.items) |spec| {
const value = old.get(spec.refField) orelse continue;
if (value == .none) continue;
var child = try Table.open(self.db, self.allocator, spec.childTable);
defer child.deinit();
const rows = try child.scanAll();
defer freeRows(self.allocator, rows);
const selfReferencing = std.mem.eql(u8, spec.childTable, self.name);
for (rows) |*r| {
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)) continue;
switch (spec.onDelete) {
.restrict => return error.ForeignKeyInUse,
.cascade => {
const childPk = r.get(pkField) orelse continue;
try child.planCascade(childPk, visited, plan);
},
}
}
}
try plan.append(.{ .table = try self.allocator.dupe(u8, self.name), .pk = try pk.dupe(self.allocator) });
}
/// The actual row/index removal `delete` used to do inline before
/// cascading existed - no FOREIGN KEY check here at all, since by the
/// time this runs `planCascade` has already proven the entire plan is
/// safe to execute in order. Takes `pk` rather than a decoded row
/// because `CascadeAction` only carries the two, and re-fetching here
/// (rather than threading the `Row` `planCascade` already read) is
/// what lets `delete` stay tolerant of a row that no longer exists by
/// execution time - unreachable in practice (nothing between planning
/// and executing can delete a row out from under this single-threaded
/// call), but cheap to make robust to rather than to assume away.
fn executeDelete(self: *Table, pk: Value) !void {
var old = try self.get(pk) orelse return;
defer old.deinit();
for (self.indexes.fields.items) |indexedField| {
if (old.get(indexedField)) |v| {
try idx.removeEntry(self.db, self.allocator, self.name, indexedField, v, pk);
}
}
for (self.composite.specs.items) |spec| {
var values: [maxCompositeFields]Value = undefined;
var complete = true;
for (spec.fields, 0..) |f, i| {
values[i] = old.get(f) orelse {
complete = false;
break;
};
}
if (complete) try idx.removeCompositeEntry(self.db, self.allocator, self.name, spec.name, values[0..spec.fields.len], pk);
}
const key = try idx.rowKey(self.allocator, self.name, pk);
defer self.allocator.free(key);
try self.db.delete(key);
}
/// SQL's `ON DELETE RESTRICT`/`CASCADE` (see `planCascade` above) has an
/// easy-to-miss counterpart: this codebase never leaves a child dangling
/// when its parent ROW disappears, but until now nothing stopped a `put`
/// from changing the parent's *value* out from under a still-live child -
/// `refTable.dropIndex(refField)` and `updateWhere`/direct `put` changing
/// a `refField` are exactly the "carpet pulled from under the child"
/// cases `createForeignKey`'s own doc comment already named as open
/// gaps. Reuses the identical scan and self-reference exclusion as
/// `planCascade` (a value that stops existing under this pk is exactly
/// as dangerous to a live reference as a row that stops existing
/// entirely), but the trigger is "this pk's value for `refField` is
/// about to change", not "this pk is about to be deleted" - so `old` may
/// be null (a fresh insert has nothing to pull out from under anyone)
/// and a field whose new value equals its old value is exempt, the same
/// "unchanged is always safe" exemption `put`'s own UNIQUE check makes.
/// Unlike `planCascade`, this ALWAYS restricts regardless of the
/// constraint's `onDelete` mode - there is no `ON UPDATE CASCADE` here,
/// only `ON DELETE CASCADE`, so even a `cascade`-mode FK still blocks a
/// value change under a live reference.
fn updateRestrictedByForeignKey(self: *Table, pk: Value, old: ?*const Row, newRow: *const Row) !bool {
var referencedBy = try ReferencedBySet.load(self.allocator, self.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;
var child = try Table.open(self.db, self.allocator, spec.childTable);
defer child.deinit();
const rows = try child.scanAll();
defer freeRows(self.allocator, rows);
const selfReferencing = std.mem.eql(u8, spec.childTable, self.name);
for (rows) |*r| {
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;
}
/// Deletes `pk`, honoring every `ON DELETE` mode registered against
/// this table by some OTHER table's `createForeignKey` (see
/// `idx.OnDelete`'s doc comment): a `restrict`-mode reference still
/// pointing at this row aborts the whole call with
/// `error.ForeignKeyInUse` and nothing is mutated; a `cascade`-mode
/// one instead removes the referencing rows too, recursively, before
/// this row itself goes - see `planCascade`'s doc comment for why
/// that has to happen in two passes (plan every removal first, then
/// execute the plan) rather than deleting as it discovers each one.
/// A `pk` that does not exist is a silent no-op, same as before
/// cascading existed.
pub fn delete(self: *Table, pk: Value) !void {
var visited = CascadeVisited.init(self.allocator);
defer visited.deinit();
var plan = std.ArrayList(CascadeAction).init(self.allocator);
defer {
for (plan.items) |a| a.deinit(self.allocator);
plan.deinit();
}
try self.planCascade(pk, &visited, &plan);
for (plan.items) |action| {
var t = try Table.open(self.db, self.allocator, action.table);
defer t.deinit();
try t.executeDelete(action.pk);
}
}
/// Applies `sets` on top of every row matching `pred` (or every row, if
/// null) - SQL's `UPDATE <table> SET ... WHERE pred`. Deliberately not a
/// bespoke in-place mutation path: each match is merged with `sets`
/// (a `set` entry overrides a same-named existing field, or is appended
/// if the row does not have that field yet) and written back through
/// `put`, the exact same call an external caller doing `get` then `put`
/// by hand would make - so every secondary/composite index touched by a
/// changed field is maintained exactly like any other write, with zero
/// special-casing here. Uses `selectAllMatches` rather than `selectPage`
/// because every match must be visited regardless of any page window -
/// there is no `LIMIT` on an `UPDATE`. Returns the number of rows
/// updated.
pub fn updateWhere(self: *Table, pred: ?Predicate, sets: []const Field) !usize {
const matches = try self.selectAllMatches(pred);
defer freeRows(self.allocator, matches);
for (matches) |*row| {
const pk = row.get(pkField) orelse continue;
var merged = Row.init(self.allocator);
defer merged.deinit();
for (row.fields.items) |f| {
if (std.mem.eql(u8, f.name, pkField)) continue;
try merged.set(f.name, f.value);
}
for (sets) |f| try merged.set(f.name, f.value);
try self.put(pk, merged.fields.items);
}
return matches.len;
}
/// Deletes every row matching `pred` (or every row, if null) - SQL's
/// `DELETE FROM <table> WHERE pred`. Mirrors `updateWhere`: materializes
/// the full match set with `selectAllMatches` (a `DELETE` has no
/// `LIMIT` either, so partial-scan short-circuiting is wrong here same
/// as for update) then removes each match through the existing single-row
/// `delete`, so every secondary/composite index touched by the deleted
/// row is maintained exactly like a caller doing `get` then `delete` by
/// hand would - zero special-casing. Returns the number of rows deleted.
pub fn deleteWhere(self: *Table, pred: ?Predicate) !usize {
const matches = try self.selectAllMatches(pred);
defer freeRows(self.allocator, matches);