feat: add EqCursor and next to index
This commit is contained in:
1 parent
cfd2fd1f5d
commit
910afabd97
1 file changed
+158
+158
@@ -1110,3 +1110,161 @@ pub fn scanCompositeRangeCursor(
|
|||||||
break :blk endBuf.?;
|
break :blk endBuf.?;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return .{ .it = try db.scanRange(allocator, .{
|
||||||
|
.start = start,
|
||||||
|
.end = end,
|
||||||
|
.startInclusive = true,
|
||||||
|
.endInclusive = false,
|
||||||
|
}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lazy cursor over an equality index lookup: decodes one primary key at a
|
||||||
|
/// time from the underlying `Db.RangeIterator` instead of materializing the
|
||||||
|
/// whole match set up front. This is what lets `Table.selectPage` honour a
|
||||||
|
/// small `limit` without reading and decoding every index entry for a value
|
||||||
|
/// that may have thousands of matches.
|
||||||
|
pub const EqCursor = struct {
|
||||||
|
it: dbmod.RangeIterator,
|
||||||
|
|
||||||
|
pub fn next(self: *EqCursor, allocator: Allocator) !?Value {
|
||||||
|
const entry = try self.it.next() orelse return null;
|
||||||
|
return try row.decodeValue(allocator, entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *EqCursor) void {
|
||||||
|
self.it.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Point lookup, streaming: opens the underlying scan but decodes nothing
|
||||||
|
/// until `EqCursor.next` is called.
|
||||||
|
pub fn scanEqCursor(db: *Db, allocator: Allocator, table: []const u8, field: []const u8, value: Value) !EqCursor {
|
||||||
|
const prefix = try indexValueBound(allocator, table, field, value);
|
||||||
|
defer allocator.free(prefix);
|
||||||
|
return .{ .it = try db.scanPrefix(allocator, prefix) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Point lookup: all primary keys currently indexed under `value`.
|
||||||
|
pub fn scanEq(db: *Db, allocator: Allocator, table: []const u8, field: []const u8, value: Value) ![]Value {
|
||||||
|
var cursor = try scanEqCursor(db, allocator, table, field, value);
|
||||||
|
defer cursor.deinit();
|
||||||
|
|
||||||
|
var results = std.ArrayList(Value).init(allocator);
|
||||||
|
errdefer {
|
||||||
|
for (results.items) |v| v.free(allocator);
|
||||||
|
results.deinit();
|
||||||
|
}
|
||||||
|
while (try cursor.next(allocator)) |pk| try results.append(pk);
|
||||||
|
return results.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Bound = struct {
|
||||||
|
value: Value,
|
||||||
|
inclusive: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Lazy cursor over a range index lookup, same rationale as `EqCursor`.
|
||||||
|
pub const RangeCursor = struct {
|
||||||
|
it: dbmod.RangeIterator,
|
||||||
|
|
||||||
|
pub fn next(self: *RangeCursor, allocator: Allocator) !?Value {
|
||||||
|
const entry = try self.it.next() orelse return null;
|
||||||
|
return try row.decodeValue(allocator, entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *RangeCursor) void {
|
||||||
|
self.it.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Range lookup over `table.field`'s index, streaming: primary keys whose
|
||||||
|
/// indexed value falls within [low, high] (either side may be open/null),
|
||||||
|
/// decoded one at a time as the caller consumes the cursor.
|
||||||
|
pub fn scanRangeCursor(
|
||||||
|
db: *Db,
|
||||||
|
allocator: Allocator,
|
||||||
|
table: []const u8,
|
||||||
|
field: []const u8,
|
||||||
|
low: ?Bound,
|
||||||
|
high: ?Bound,
|
||||||
|
) !RangeCursor {
|
||||||
|
const prefix = try indexPrefix(allocator, table, field);
|
||||||
|
defer allocator.free(prefix);
|
||||||
|
|
||||||
|
var startBuf: ?[]u8 = null;
|
||||||
|
defer if (startBuf) |b| allocator.free(b);
|
||||||
|
var endBuf: ?[]u8 = null;
|
||||||
|
defer if (endBuf) |b| allocator.free(b);
|
||||||
|
|
||||||
|
// Every real index-entry key is `indexValueBound(value) ++ orderedPk`, a
|
||||||
|
// strict byte-extension of the bare value bound (pk encodings are never
|
||||||
|
// empty). So a key with value == threshold always compares greater than
|
||||||
|
// `indexValueBound(threshold)` itself - inclusive/exclusive thresholds
|
||||||
|
// are therefore expressed by choosing which side of that gap to start
|
||||||
|
// or stop the scan at, not by the start/endInclusive flags.
|
||||||
|
const start = if (low) |b| blk: {
|
||||||
|
const bound = try indexValueBound(allocator, table, field, b.value);
|
||||||
|
if (b.inclusive) {
|
||||||
|
startBuf = bound;
|
||||||
|
} else {
|
||||||
|
defer allocator.free(bound);
|
||||||
|
startBuf = try prefixUpperBound(allocator, bound) orelse try allocator.dupe(u8, bound);
|
||||||
|
}
|
||||||
|
break :blk startBuf.?;
|
||||||
|
} else prefix;
|
||||||
|
|
||||||
|
const end = if (high) |b| blk: {
|
||||||
|
const bound = try indexValueBound(allocator, table, field, b.value);
|
||||||
|
if (b.inclusive) {
|
||||||
|
defer allocator.free(bound);
|
||||||
|
endBuf = try prefixUpperBound(allocator, bound) orelse try allocator.dupe(u8, bound);
|
||||||
|
} else {
|
||||||
|
endBuf = bound;
|
||||||
|
}
|
||||||
|
break :blk endBuf.?;
|
||||||
|
} else blk: {
|
||||||
|
endBuf = try prefixUpperBound(allocator, prefix) orelse try allocator.dupe(u8, prefix);
|
||||||
|
break :blk endBuf.?;
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{ .it = try db.scanRange(allocator, .{
|
||||||
|
.start = start,
|
||||||
|
.end = end,
|
||||||
|
.startInclusive = true,
|
||||||
|
.endInclusive = false,
|
||||||
|
}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Range lookup over `table.field`'s index: all primary keys whose indexed
|
||||||
|
/// value falls within [low, high] (either side may be open/null).
|
||||||
|
pub fn scanRange(
|
||||||
|
db: *Db,
|
||||||
|
allocator: Allocator,
|
||||||
|
table: []const u8,
|
||||||
|
field: []const u8,
|
||||||
|
low: ?Bound,
|
||||||
|
high: ?Bound,
|
||||||
|
) ![]Value {
|
||||||
|
var cursor = try scanRangeCursor(db, allocator, table, field, low, high);
|
||||||
|
defer cursor.deinit();
|
||||||
|
|
||||||
|
var results = std.ArrayList(Value).init(allocator);
|
||||||
|
errdefer {
|
||||||
|
for (results.items) |v| v.free(allocator);
|
||||||
|
results.deinit();
|
||||||
|
}
|
||||||
|
while (try cursor.next(allocator)) |pk| try results.append(pk);
|
||||||
|
return results.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rowKey and indexEntryKey stay within their own namespace" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const rk = try rowKey(allocator, "users", .{ .int = 1 });
|
||||||
|
defer allocator.free(rk);
|
||||||
|
const ik = try indexEntryKey(allocator, "users", "email", .{ .text = "a@b.c" }, .{ .int = 1 });
|
||||||
|
defer allocator.free(ik);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(@as(u8, 'R'), rk[0]);
|
||||||
|
try std.testing.expectEqual(@as(u8, 'I'), ik[0]);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user