diff --git a/src/index.zig b/src/index.zig index 9960974..e418ab2 100644 --- a/src/index.zig +++ b/src/index.zig @@ -153,3 +153,156 @@ pub fn compositeIndexPrefix(allocator: Allocator, table: []const u8, indexName: return out.toOwnedSlice(); } +pub fn compositeIndexEntryKey(allocator: Allocator, table: []const u8, indexName: []const u8, values: []const Value, pk: Value) ![]u8 { + var out = std.ArrayList(u8).init(allocator); + errdefer out.deinit(); + try out.append(compositeIndexTag); + try appendSegment(&out, table); + try appendSegment(&out, indexName); + for (values) |v| try row.appendOrdered(&out, v); + try row.appendOrdered(&out, pk); + return out.toOwnedSlice(); +} + +/// Bound for a leftmost-prefix scan of a composite index: `values.len` may +/// be less than the index's full column count, in which case the scan +/// matches every entry whose leading columns equal `values` regardless of +/// what the remaining (unconstrained) columns hold - see the module-level +/// keyspace comment for why the concatenation makes this a correct prefix. +pub fn compositeIndexValueBound(allocator: Allocator, table: []const u8, indexName: []const u8, values: []const Value) ![]u8 { + var out = std.ArrayList(u8).init(allocator); + errdefer out.deinit(); + try out.append(compositeIndexTag); + try appendSegment(&out, table); + try appendSegment(&out, indexName); + for (values) |v| try row.appendOrdered(&out, v); + return out.toOwnedSlice(); +} + +/// Bound for a composite index scan that pins `eqValues` as the leading +/// columns (leftmost prefix, same as `compositeIndexValueBound`) and then +/// appends one more ordered value for a range condition on the very next +/// column - the byte-extension argument from the module-level keyspace +/// comment applies here exactly as it does for a bare `indexValueBound`, so +/// `scanCompositeRangeCursor` can reuse the same inclusive/exclusive +/// gap trick `scanRangeCursor` uses for single-field indexes. +fn compositeIndexRangeBound(allocator: Allocator, table: []const u8, indexName: []const u8, eqValues: []const Value, extra: Value) ![]u8 { + var out = std.ArrayList(u8).init(allocator); + errdefer out.deinit(); + try out.append(compositeIndexTag); + try appendSegment(&out, table); + try appendSegment(&out, indexName); + for (eqValues) |v| try row.appendOrdered(&out, v); + try row.appendOrdered(&out, extra); + return out.toOwnedSlice(); +} + +/// Smallest byte string strictly greater than every string with `prefix` as +/// a prefix, used as the exclusive upper bound of a prefix scan. Returns +/// null when the prefix is all 0xFF bytes (unbounded above, e.g. empty +/// table/field names never hit this in practice). +pub fn prefixUpperBound(allocator: Allocator, prefix: []const u8) !?[]u8 { + var end = prefix.len; + while (end > 0 and prefix[end - 1] == 0xff) end -= 1; + if (end == 0) return null; + const upper = try allocator.alloc(u8, end); + @memcpy(upper, prefix[0..end]); + upper[end - 1] += 1; + return upper; +} + +/// Whether a table name or field name is safe to embed as a NUL-delimited +/// keyspace segment: table/field identifiers may not themselves contain +/// NUL bytes or they would corrupt the segment boundaries above. +pub fn validIdentifier(name: []const u8) bool { + if (name.len == 0) return false; + for (name) |c| { + if (c == sep) return false; + } + return true; +} + +/// Persisted set of field names that have a secondary index on a table. +/// Loaded once per Table.open and written back to the Db's metadata +/// namespace whenever an index is created or dropped, so indexes survive +/// process restarts without any external schema file. +pub const IndexSet = struct { + allocator: Allocator, + fields: std.ArrayList([]u8), + + pub fn load(allocator: Allocator, db: *Db, table: []const u8) !IndexSet { + const key = try metaKey(allocator, table); + defer allocator.free(key); + + var set = IndexSet{ .allocator = allocator, .fields = std.ArrayList([]u8).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 count = std.mem.readInt(u32, raw[pos..][0..4], .little); + pos += 4; + var i: u32 = 0; + while (i < count) : (i += 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; + const name = try allocator.dupe(u8, raw[pos .. pos + len]); + pos += len; + try set.fields.append(name); + } + return set; + } + + pub fn save(self: *const IndexSet, db: *Db, table: []const u8) !void { + const key = try metaKey(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.fields.items.len), .little); + try out.appendSlice(&countBuf); + for (self.fields.items) |name| { + var lenBuf: [4]u8 = undefined; + std.mem.writeInt(u32, &lenBuf, @intCast(name.len), .little); + try out.appendSlice(&lenBuf); + try out.appendSlice(name); + } + try db.put(key, out.items); + } + + pub fn has(self: *const IndexSet, field: []const u8) bool { + for (self.fields.items) |f| { + if (std.mem.eql(u8, f, field)) return true; + } + return false; + } + + pub fn add(self: *IndexSet, field: []const u8) !void { + if (self.has(field)) return; + const copy = try self.allocator.dupe(u8, field); + try self.fields.append(copy); + } + + /// Returns true if the field was present and has been removed. + pub fn remove(self: *IndexSet, field: []const u8) bool { + for (self.fields.items, 0..) |f, i| { + if (std.mem.eql(u8, f, field)) { + self.allocator.free(f); + _ = self.fields.orderedRemove(i); + return true; + } + } + return false; + } + + pub fn deinit(self: *IndexSet) void { + for (self.fields.items) |f| self.allocator.free(f); + self.fields.deinit(); + } +}; +