feat: add CompositeIndexSpec and free to index

This commit is contained in:
dualfroz committed 2026-09-04 17:31:47 +02:00
1 parent fff7ffeb78
commit 133f4efa0a
1 file changed
+158
+158
View File
@@ -790,3 +790,161 @@ fn appendLenPrefixed(out: *std.ArrayList(u8), value: []const u8) !void {
try out.appendSlice(value); try out.appendSlice(value);
} }
/// One multi-column ("composite") index: a caller-chosen name (used as the
/// keyspace discriminator, so it must be a `validIdentifier` like a table or
/// field name) plus the ordered list of fields it indexes. Field order is
/// significant - it defines both the on-disk key layout and which leading
/// ("leftmost") prefix of columns a query can use for a narrowed lookup.
pub const CompositeIndexSpec = struct {
name: []u8,
fields: [][]u8,
fn free(self: *const CompositeIndexSpec, allocator: Allocator) void {
allocator.free(self.name);
for (self.fields) |f| allocator.free(f);
allocator.free(self.fields);
}
};
/// Persisted set of composite indexes on a table, the multi-column
/// counterpart to `IndexSet`. Stored under a separate metadata key
/// ("CINDEXES" vs "INDEXES") so single-field and composite indexes never
/// share or shadow each other's schema record.
pub const CompositeIndexSet = struct {
allocator: Allocator,
specs: std.ArrayList(CompositeIndexSpec),
pub fn load(allocator: Allocator, db: *Db, table: []const u8) !CompositeIndexSet {
const key = try compositeMetaKey(allocator, table);
defer allocator.free(key);
var set = CompositeIndexSet{ .allocator = allocator, .specs = std.ArrayList(CompositeIndexSpec).init(allocator) };
errdefer set.deinit();
const raw = try db.get(allocator, key) orelse return set;
defer allocator.free(raw);
var pos: usize = 0;
if (pos + 4 > raw.len) return error.Malformed;
const specCount = std.mem.readInt(u32, raw[pos..][0..4], .little);
pos += 4;
var i: u32 = 0;
while (i < specCount) : (i += 1) {
if (pos + 4 > raw.len) return error.Malformed;
const nameLen = std.mem.readInt(u32, raw[pos..][0..4], .little);
pos += 4;
if (pos + nameLen > raw.len) return error.Malformed;
const name = try allocator.dupe(u8, raw[pos .. pos + nameLen]);
errdefer allocator.free(name);
pos += nameLen;
if (pos + 4 > raw.len) return error.Malformed;
const fieldCount = std.mem.readInt(u32, raw[pos..][0..4], .little);
pos += 4;
const fields = try allocator.alloc([]u8, fieldCount);
var filled: usize = 0;
errdefer {
for (fields[0..filled]) |f| allocator.free(f);
allocator.free(fields);
}
var j: u32 = 0;
while (j < fieldCount) : (j += 1) {
if (pos + 4 > raw.len) return error.Malformed;
const len = std.mem.readInt(u32, raw[pos..][0..4], .little);
pos += 4;
if (pos + len > raw.len) return error.Malformed;
fields[j] = try allocator.dupe(u8, raw[pos .. pos + len]);
filled += 1;
pos += len;
}
try set.specs.append(.{ .name = name, .fields = fields });
}
return set;
}
pub fn save(self: *const CompositeIndexSet, db: *Db, table: []const u8) !void {
const key = try compositeMetaKey(self.allocator, table);
defer self.allocator.free(key);
var out = std.ArrayList(u8).init(self.allocator);
defer out.deinit();
var countBuf: [4]u8 = undefined;
std.mem.writeInt(u32, &countBuf, @intCast(self.specs.items.len), .little);
try out.appendSlice(&countBuf);
for (self.specs.items) |spec| {
var lenBuf: [4]u8 = undefined;
std.mem.writeInt(u32, &lenBuf, @intCast(spec.name.len), .little);
try out.appendSlice(&lenBuf);
try out.appendSlice(spec.name);
std.mem.writeInt(u32, &lenBuf, @intCast(spec.fields.len), .little);
try out.appendSlice(&lenBuf);
for (spec.fields) |f| {
std.mem.writeInt(u32, &lenBuf, @intCast(f.len), .little);
try out.appendSlice(&lenBuf);
try out.appendSlice(f);
}
}
try db.put(key, out.items);
}
pub fn find(self: *const CompositeIndexSet, name: []const u8) ?*const CompositeIndexSpec {
for (self.specs.items) |*s| {
if (std.mem.eql(u8, s.name, name)) return s;
}
return null;
}
pub fn has(self: *const CompositeIndexSet, name: []const u8) bool {
return self.find(name) != null;
}
pub fn add(self: *CompositeIndexSet, name: []const u8, fields: []const []const u8) !void {
if (self.has(name)) return;
const nameCopy = try self.allocator.dupe(u8, name);
errdefer self.allocator.free(nameCopy);
const fieldsCopy = try self.allocator.alloc([]u8, fields.len);
var filled: usize = 0;
errdefer {
for (fieldsCopy[0..filled]) |f| self.allocator.free(f);
self.allocator.free(fieldsCopy);
}
for (fields, 0..) |f, i| {
fieldsCopy[i] = try self.allocator.dupe(u8, f);
filled += 1;
}
try self.specs.append(.{ .name = nameCopy, .fields = fieldsCopy });
}
/// Returns true if a composite index by that name was present and has
/// been removed.
pub fn remove(self: *CompositeIndexSet, name: []const u8) bool {
for (self.specs.items, 0..) |*s, i| {
if (std.mem.eql(u8, s.name, name)) {
s.free(self.allocator);
_ = self.specs.orderedRemove(i);
return true;
}
}
return false;
}
pub fn deinit(self: *CompositeIndexSet) void {
for (self.specs.items) |*s| s.free(self.allocator);
self.specs.deinit();
}
};
/// Adds one index entry mapping `value` -> `pk` under `table.field`. The
/// entry's stored value is the plain (non order-preserving) encoding of the
/// primary key, so lookups can recover the exact original Value instead of
/// re-parsing the escaped ordered key bytes.
pub fn addEntry(db: *Db, allocator: Allocator, table: []const u8, field: []const u8, value: Value, pk: Value) !void {
const key = try indexEntryKey(allocator, table, field, value, pk);
defer allocator.free(key);
const pkBytes = try row.encodeValue(allocator, pk);
defer allocator.free(pkBytes);
try db.put(key, pkBytes);
}