feat: add Table and open to query

This commit is contained in:
dualfroz committed 2026-09-08 19:55:32 +02:00
1 parent fd91d6174c
commit 5512aa824a
1 file changed
+157
+157
View File
@@ -164,3 +164,160 @@ fn matchLike(pattern: []const u8, text: []const u8) bool {
var starIdx: ?usize = null; var starIdx: ?usize = null;
var matchIdx: usize = 0; 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, {});
}
}