feat: add Table and open to query
This commit is contained in:
1 parent
fd91d6174c
commit
5512aa824a
1 file changed
+157
+157
@@ -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, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user