feat: add compositeIndexEntryKey and compositeIndexValueBound to index
This commit is contained in:
1 parent
4784acc242
commit
2e1b027571
1 file changed
+153
+153
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user