tiny.sql.index
Defined in tiny.sql.
API (14)
Actions
Public operations.
Reader.lookupReader.lookupPayloadsReader.openReader.rangeReader.scan: Starts a scan of the entries fromstartup toendintarget.Reader.scanPayloadsReader.summarize
Types and contracts
Public types and contracts.
Source
Source: lib/sql/src/index.zig
zig
const std = @import("std");const file = @import("file.zig");const key = @import("key.zig");const page = @import("page.zig");const row = @import("row.zig");const trace = @import("trace.zig");const tree = @import("tree.zig");const wal = @import("wal.zig");const Allocator = std.mem.Allocator;pub const Error = tree.Error || row.Error || key.Error;pub const Options = struct { tree: tree.Options, columns: []const row.Column = &.{},};pub const Entry = struct { rowid: i64, key: []const u8, payload: []const u8 = "",};pub const Bound = struct { values: []const row.Value, inclusive: bool,};pub const Reader = struct { entries: tree.Reader, columns: []const row.Column, pub fn open(snapshot: file.Snapshot, options: Options) Error!Reader { return .{ .entries = try tree.Reader.open(snapshot, options.tree), .columns = options.columns, }; } /// Starts a scan of the entries from `start` up to `end` in `target`. pub fn scan( self: *const Reader, target: *Scan, allocator: Allocator, start: ?[]const row.Value, end: ?[]const row.Value, ) Error!void { const phase = trace.scope("index.scan"); defer phase.end(); try self.range( target, allocator, if (start) |values| .{ .values = values, .inclusive = true } else null, if (end) |values| .{ .values = values, .inclusive = false } else null, ); } pub fn range( self: *const Reader, target: *Scan, allocator: Allocator, start: ?Bound, end: ?Bound, ) Error!void { const phase = trace.scope("index.range"); defer phase.end(); try self.rangeProjection(target, allocator, start, end, .key); } pub fn scanPayloads( self: *const Reader, target: *Scan, allocator: Allocator, start: ?[]const row.Value, end: ?[]const row.Value, ) Error!void { const phase = trace.scope("index.scan_payloads"); defer phase.end(); try self.rangeProjection( target, allocator, if (start) |values| .{ .values = values, .inclusive = true } else null, if (end) |values| .{ .values = values, .inclusive = false } else null, .value, ); } pub fn lookup( self: *const Reader, target: *Scan, allocator: Allocator, prefix: []const row.Value, ) Error!void { const phase = trace.scope("index.lookup"); defer phase.end(); try self.lookupProjection(target, allocator, prefix, .key); } pub fn lookupPayloads( self: *const Reader, target: *Scan, allocator: Allocator, prefix: []const row.Value, ) Error!void { const phase = trace.scope("index.lookup_payloads"); defer phase.end(); try self.lookupProjection(target, allocator, prefix, .value); } pub fn summarize(self: *const Reader) Error!tree.Summary { const phase = trace.scope("index.summarize"); defer phase.end(); return try self.entries.summarize(); } fn rangeProjection( self: *const Reader, target: *Scan, allocator: Allocator, start: ?Bound, end: ?Bound, projection: tree.Projection, ) Error!void { var start_bytes: [page.size]u8 = undefined; var start_end_bytes: [page.size]u8 = undefined; var end_bytes: [page.size]u8 = undefined; var end_end_bytes: [page.size]u8 = undefined; const start_key = if (start) |bound| start_key: { const prefix = try key.encodeIndexPrefix(&start_bytes, bound.values, self.columns); break :start_key if (bound.inclusive) prefix else try key.encodePrefixEnd(&start_end_bytes, prefix); } else null; const end_key = if (end) |bound| end_key: { const prefix = try key.encodeIndexPrefix(&end_bytes, bound.values, self.columns); break :end_key if (bound.inclusive) try key.encodePrefixEnd(&end_end_bytes, prefix) else prefix; } else null; try self.entries.scan(&target.entries, allocator, start_key, end_key, projection); } fn lookupProjection( self: *const Reader, target: *Scan, allocator: Allocator, prefix: []const row.Value, projection: tree.Projection, ) Error!void { var start_bytes: [page.size]u8 = undefined; var end_bytes: [page.size]u8 = undefined; const start_key = try key.encodeIndexPrefix(&start_bytes, prefix, self.columns); const end_key = try key.encodePrefixEnd(&end_bytes, start_key); try self.entries.scan(&target.entries, allocator, start_key, end_key, projection); }};pub const Index = struct { entries: tree.Tree, columns: []const row.Column, pub fn open(database: *file.Database, options: Options) Error!Index { return .{ .entries = try tree.Tree.open(database, options.tree), .columns = options.columns, }; } pub fn reader(self: *const Index, snapshot: file.Snapshot) Error!Reader { return .{ .entries = try self.entries.reader(snapshot), .columns = self.columns, }; } pub fn put(self: *Index, rowid: i64, values: []const row.Value, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("index.put"); defer phase.end(); var write = try tree.Write.beginTree(&self.entries); defer write.deinit(); try self.putPayloadIn(&write, rowid, values, ""); return try write.commit(options); } pub fn putIn(self: *Index, write: *tree.Write, rowid: i64, values: []const row.Value) Error!void { try self.putPayloadIn(write, rowid, values, ""); } pub fn putPayload(self: *Index, rowid: i64, values: []const row.Value, payload: []const u8, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("index.put_payload"); defer phase.end(); var write = try tree.Write.beginTree(&self.entries); defer write.deinit(); try self.putPayloadIn(&write, rowid, values, payload); return try write.commit(options); } pub fn putPayloadIn(self: *Index, write: *tree.Write, rowid: i64, values: []const row.Value, payload: []const u8) Error!void { var key_bytes: [page.size]u8 = undefined; const encoded = key.encodeIndex(&key_bytes, values, self.columns, rowid) catch |err| switch (err) { error.OutputTooSmall => return error.KeyTooLarge, else => return err, }; try write.put(&self.entries, encoded, payload); } pub fn delete(self: *Index, rowid: i64, values: []const row.Value, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("index.delete"); defer phase.end(); var write = try tree.Write.beginTree(&self.entries); defer write.deinit(); try self.deleteIn(&write, rowid, values); return try write.commit(options); } pub fn deleteIn(self: *Index, write: *tree.Write, rowid: i64, values: []const row.Value) Error!void { var key_bytes: [page.size]u8 = undefined; const encoded = key.encodeIndex(&key_bytes, values, self.columns, rowid) catch |err| switch (err) { error.OutputTooSmall => return error.KeyTooLarge, else => return err, }; try write.delete(&self.entries, encoded); } pub fn scan( self: *const Index, target: *Scan, allocator: Allocator, start: ?[]const row.Value, end: ?[]const row.Value, ) Error!void { var read = try self.entries.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.scan(target, allocator, start, end); } pub fn range( self: *const Index, target: *Scan, allocator: Allocator, start: ?Bound, end: ?Bound, ) Error!void { var read = try self.entries.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.range(target, allocator, start, end); } pub fn scanPayloads( self: *const Index, target: *Scan, allocator: Allocator, start: ?[]const row.Value, end: ?[]const row.Value, ) Error!void { var read = try self.entries.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.scanPayloads(target, allocator, start, end); } pub fn lookup( self: *const Index, target: *Scan, allocator: Allocator, prefix: []const row.Value, ) Error!void { var read = try self.entries.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.lookup(target, allocator, prefix); } pub fn lookupPayloads( self: *const Index, target: *Scan, allocator: Allocator, prefix: []const row.Value, ) Error!void { var read = try self.entries.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.lookupPayloads(target, allocator, prefix); } pub fn summarize(self: *const Index) Error!tree.Summary { var read = try self.entries.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); return try opened.summarize(); } pub fn summarizeIn(self: *const Index, write: *const tree.Write) Error!tree.Summary { const phase = trace.scope("index.summarize_in"); defer phase.end(); return try self.entries.summarizeIn(write); }};pub const Scan = struct { entries: tree.Scan, pub fn deinit(self: *Scan) void { self.entries.deinit(); self.* = undefined; } pub fn next(self: *Scan) Error!?Entry { if (try self.entries.next()) |entry| { return .{ .rowid = try key.decodeIndexRowId(entry.key), .key = entry.key, .payload = entry.bytes, }; } return null; }};test "secondary index stores duplicate values in rowid order" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "index.db", .wal = "index.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); var index = try Index.open(&database, .{ .tree = .{ .meta_page = 3, .root_page = 4 } }); _ = try index.put(3, &.{.{ .text = "orange" }}, .{ .durability = .buffered }); _ = try index.put(2, &.{.{ .text = "apple" }}, .{ .durability = .buffered }); _ = try index.put(-1, &.{.{ .text = "apple" }}, .{ .durability = .buffered }); _ = try index.put(7, &.{.{ .text = "pear" }}, .{ .durability = .buffered }); try database.syncWal(); } var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "index.db", .wal = "index.wal" }, .header = recoveredHeader(), }); defer reopened.deinit(); var index = try Index.open(&reopened, .{ .tree = .{ .meta_page = 3, .root_page = 4 } }); var scan: Scan = undefined; try index.scan(&scan, std.testing.allocator, null, null); defer scan.deinit(); try std.testing.expectEqual(@as(i64, -1), (try scan.next()).?.rowid); try std.testing.expectEqual(@as(i64, 2), (try scan.next()).?.rowid); try std.testing.expectEqual(@as(i64, 3), (try scan.next()).?.rowid); try std.testing.expectEqual(@as(i64, 7), (try scan.next()).?.rowid); try std.testing.expect(try scan.next() == null); var lookup: Scan = undefined; try index.lookup(&lookup, std.testing.allocator, &.{.{ .text = "apple" }}); defer lookup.deinit(); try std.testing.expectEqual(@as(i64, -1), (try lookup.next()).?.rowid); try std.testing.expectEqual(@as(i64, 2), (try lookup.next()).?.rowid); try std.testing.expect(try lookup.next() == null); _ = try index.delete(-1, &.{.{ .text = "apple" }}, .{ .durability = .buffered }); var remaining: Scan = undefined; try index.lookup(&remaining, std.testing.allocator, &.{.{ .text = "apple" }}); defer remaining.deinit(); try std.testing.expectEqual(@as(i64, 2), (try remaining.next()).?.rowid); try std.testing.expect(try remaining.next() == null);}test "secondary index stores payloads in rowid order" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "index.db", .wal = "index.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); var index = try Index.open(&database, .{ .tree = .{ .meta_page = 3, .root_page = 4 } }); _ = try index.putPayload(4, &.{.{ .text = "apple" }}, "four", .{ .durability = .buffered }); _ = try index.putPayload(1, &.{.{ .text = "apple" }}, "one", .{ .durability = .buffered }); _ = try index.putPayload(3, &.{.{ .text = "orange" }}, "three", .{ .durability = .buffered }); var lookup: Scan = undefined; try index.lookupPayloads(&lookup, std.testing.allocator, &.{.{ .text = "apple" }}); defer lookup.deinit(); const first = (try lookup.next()).?; try std.testing.expectEqual(@as(i64, 1), first.rowid); try std.testing.expectEqualStrings("one", first.payload); const second = (try lookup.next()).?; try std.testing.expectEqual(@as(i64, 4), second.rowid); try std.testing.expectEqualStrings("four", second.payload); try std.testing.expect(try lookup.next() == null); var key_lookup: Scan = undefined; try index.lookup(&key_lookup, std.testing.allocator, &.{.{ .text = "apple" }}); defer key_lookup.deinit(); try std.testing.expectEqual(@as(usize, 0), (try key_lookup.next()).?.payload.len);}test "secondary index range scan uses encoded value bounds" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "index.db", .wal = "index.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); var index = try Index.open(&database, .{ .tree = .{ .meta_page = 3, .root_page = 4 } }); _ = try index.put(10, &.{.{ .integer = -3 }}, .{ .durability = .buffered }); _ = try index.put(11, &.{.{ .integer = 0 }}, .{ .durability = .buffered }); _ = try index.put(12, &.{.{ .integer = 1 }}, .{ .durability = .buffered }); _ = try index.put(13, &.{.{ .integer = 3 }}, .{ .durability = .buffered }); var scan: Scan = undefined; try index.scan(&scan, std.testing.allocator, &.{.{ .integer = 0 }}, &.{.{ .integer = 3 }}); defer scan.deinit(); try std.testing.expectEqual(@as(i64, 11), (try scan.next()).?.rowid); try std.testing.expectEqual(@as(i64, 12), (try scan.next()).?.rowid); try std.testing.expect(try scan.next() == null); var inclusive: Scan = undefined; try index.range( &inclusive, std.testing.allocator, .{ .values = &.{.{ .integer = 0 }}, .inclusive = true }, .{ .values = &.{.{ .integer = 3 }}, .inclusive = true }, ); defer inclusive.deinit(); try std.testing.expectEqual(@as(i64, 11), (try inclusive.next()).?.rowid); try std.testing.expectEqual(@as(i64, 12), (try inclusive.next()).?.rowid); try std.testing.expectEqual(@as(i64, 13), (try inclusive.next()).?.rowid); try std.testing.expect(try inclusive.next() == null); var exclusive_start: Scan = undefined; try index.range( &exclusive_start, std.testing.allocator, .{ .values = &.{.{ .integer = 0 }}, .inclusive = false }, null, ); defer exclusive_start.deinit(); try std.testing.expectEqual(@as(i64, 12), (try exclusive_start.next()).?.rowid); try std.testing.expectEqual(@as(i64, 13), (try exclusive_start.next()).?.rowid); try std.testing.expect(try exclusive_start.next() == null);}test "secondary index honors configured text collation" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "index.db", .wal = "index.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); const columns = [_]row.Column{.{ .collation = .nocase }}; var index = try Index.open(&database, .{ .tree = .{ .meta_page = 3, .root_page = 4 }, .columns = &columns, }); _ = try index.put(2, &.{.{ .text = "Alpha" }}, .{ .durability = .buffered }); _ = try index.put(1, &.{.{ .text = "alpha" }}, .{ .durability = .buffered }); var lookup: Scan = undefined; try index.lookup(&lookup, std.testing.allocator, &.{.{ .text = "ALPHA" }}); defer lookup.deinit(); try std.testing.expectEqual(@as(i64, 1), (try lookup.next()).?.rowid); try std.testing.expectEqual(@as(i64, 2), (try lookup.next()).?.rowid); try std.testing.expect(try lookup.next() == null);}fn testingHeader() wal.Header { return .{ .sequence = 801, .salt = .{ .first = 0x7171_9191, .second = 0x5353_6363 }, };}fn recoveredHeader() wal.Header { return .{ .sequence = 802, .salt = .{ .first = 0x8888_aaaa, .second = 0xbbbb_9999 }, };}Source: lib/sql/src/root.zig:29
zig
pub const index = @import("index.zig");Audit
| Definitions | 12 |
|---|---|
| Public names | 12 |
| Members | 6 |
| Version | 26.7.0 |
| Revision | daab053ee433 |