From 5512aa824ab081bee96b20399660dee70813b9be Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 19:55:32 +0200 Subject: [PATCH] feat: add Table and open to query --- src/query.zig | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/query.zig b/src/query.zig index b76cab2..9106799 100644 --- a/src/query.zig +++ b/src/query.zig @@ -164,3 +164,160 @@ fn matchLike(pattern: []const u8, text: []const u8) bool { 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, {}); + } + } +