From 133f4efa0a9de1c22a8507754c716d5a184a3390 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 17:31:47 +0200 Subject: [PATCH] feat: add CompositeIndexSpec and free to index --- src/index.zig | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/index.zig b/src/index.zig index 398397b..0cbb37f 100644 --- a/src/index.zig +++ b/src/index.zig @@ -790,3 +790,161 @@ fn appendLenPrefixed(out: *std.ArrayList(u8), value: []const u8) !void { 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); +} +