tiny.sql.relation
Defined in tiny.sql.
API (27)
Actions
Public operations.
Reader.getReader.getIntoReader.indexRangeReader.indexScanReader.lastRowIdReader.lookupReader.openReader.scanReader.scanProjectedReader.summarizeReader.valueLengthapplyUpdatecloneAssignmentsfreeAssignments
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
Source
Source: lib/sql/src/relation.zig
zig
const std = @import("std");const file = @import("file.zig");const index_mod = @import("index.zig");const key = @import("key.zig");const page = @import("page.zig");const row = @import("row.zig");const space_mod = @import("space.zig");const table_mod = @import("table.zig");const trace = @import("trace.zig");const tree = @import("tree.zig");const wal = @import("wal.zig");const Allocator = std.mem.Allocator;pub const Error = space_mod.Error || table_mod.Error || index_mod.Error || row.Error || Allocator.Error || error{ IndexOutOfBounds, SecondaryIndexCorrupt, TooManyIndexes, TooManyIndexFields,};pub const max_indexes: usize = 8;pub const max_index_fields: usize = 8;pub const IndexSpec = struct { root_page: u32, fields: []const usize, columns: []const row.Column = &.{},};pub const Scan = table_mod.Scan;pub const Edit = union(enum) { put: Put, update: Update, delete: i64, pub const Put = struct { rowid: i64, bytes: []const u8, }; pub const Assignment = struct { column: usize, value: row.Value, }; pub const Update = struct { rowid: i64, assignments: []const Assignment, };};pub fn cloneAssignments(allocator: Allocator, assignments: []const Edit.Assignment) Error![]Edit.Assignment { const cloned = try allocator.alloc(Edit.Assignment, assignments.len); var count: usize = 0; errdefer freeAssignments(allocator, cloned[0..count]); for (assignments, cloned) |assignment, *target| { target.* = .{ .column = assignment.column, .value = switch (assignment.value) { .nil, .integer => assignment.value, .text => |text| .{ .text = try allocator.dupe(u8, text) }, .blob => |blob| .{ .blob = try allocator.dupe(u8, blob) }, }, }; count += 1; } return cloned;}pub fn freeAssignments(allocator: Allocator, assignments: []const Edit.Assignment) void { for (assignments) |assignment| switch (assignment.value) { .nil, .integer => {}, .text => |text| allocator.free(text), .blob => |blob| allocator.free(blob), }; allocator.free(assignments);}pub fn applyUpdate(allocator: Allocator, bytes: []const u8, assignments: []const Edit.Assignment) Error![]u8 { const view = try row.View.init(bytes); const count = view.columnCount(); const values = try allocator.alloc(row.Value, count); defer allocator.free(values); var cursor = view.cursor(); for (values, 0..) |*value, column| value.* = try cursor.column(column); for (assignments) |assignment| { if (assignment.column >= count) return error.ColumnOutOfBounds; values[assignment.column] = assignment.value; } const size = try row.encodedSize(values); const merged = try allocator.alloc(u8, size); errdefer allocator.free(merged); _ = try row.encode(merged, values); return merged;}pub const Options = struct { table_root: u32 = 2, indexes: []const IndexSpec = &.{},};const Indexed = struct { index: index_mod.Index, fields: []const usize,};const ReadIndexed = struct { index: index_mod.Reader, fields: []const usize,};pub const Summary = struct { table: tree.Summary, indexes: [max_indexes]tree.Summary, index_count: usize,};pub const Reader = struct { space: space_mod.Reader, table: table_mod.Reader, indexes: [max_indexes]ReadIndexed = undefined, index_count: usize, pub fn open(space: *const space_mod.Reader, options: Options) Error!Reader { if (options.indexes.len > max_indexes) return error.TooManyIndexes; var reader = Reader{ .space = space.*, .table = try space.rowidTable(options.table_root), .index_count = options.indexes.len, }; for (options.indexes, 0..) |spec, offset| { if (spec.fields.len > max_index_fields) return error.TooManyIndexFields; reader.indexes[offset] = .{ .index = try space.index(spec.root_page, spec.columns), .fields = spec.fields, }; } return reader; } pub fn lastRowId(self: *const Reader) Error!?i64 { return try self.table.lastRowId(); } pub fn get(self: *const Reader, allocator: Allocator, rowid: i64) Error!?[]u8 { return try self.table.get(allocator, rowid); } pub fn valueLength(self: *const Reader, rowid: i64) Error!?usize { return try self.table.valueLength(rowid); } pub fn getInto(self: *const Reader, rowid: i64, target: []u8) Error!?[]u8 { return try self.table.getInto(rowid, target); } pub fn scan( self: *const Reader, target: *Scan, allocator: Allocator, start: ?i64, end: ?i64, ) Error!void { try self.table.scan(target, allocator, start, end); } pub fn scanProjected( self: *const Reader, target: *Scan, allocator: Allocator, start: ?i64, end: ?i64, projection: table_mod.Projection, ) Error!void { try self.table.scanProjected(target, allocator, start, end, projection); } pub fn lookup( self: *const Reader, target: *index_mod.Scan, allocator: Allocator, index_slot: usize, prefix: []const row.Value, ) Error!void { if (index_slot >= self.index_count) return error.IndexOutOfBounds; try self.indexes[index_slot].index.lookup(target, allocator, prefix); } pub fn indexScan( self: *const Reader, target: *index_mod.Scan, allocator: Allocator, index_slot: usize, start: ?[]const row.Value, end: ?[]const row.Value, ) Error!void { if (index_slot >= self.index_count) return error.IndexOutOfBounds; try self.indexes[index_slot].index.scan(target, allocator, start, end); } pub fn indexRange( self: *const Reader, target: *index_mod.Scan, allocator: Allocator, index_slot: usize, start: ?index_mod.Bound, end: ?index_mod.Bound, ) Error!void { if (index_slot >= self.index_count) return error.IndexOutOfBounds; try self.indexes[index_slot].index.range(target, allocator, start, end); } pub fn summarize(self: *const Reader) Error!Summary { var summary = Summary{ .table = try self.table.summarize(), .indexes = undefined, .index_count = self.index_count, }; for (0..self.index_count) |offset| { summary.indexes[offset] = try self.indexes[offset].index.summarize(); } return summary; }};const AppliedEdit = struct { rowid: i64, base: ?[]u8, current: ?[]const u8, merged: ?[]u8 = null, fn deinit(self: *AppliedEdit, allocator: Allocator) void { if (self.merged) |bytes| allocator.free(bytes); if (self.base) |bytes| allocator.free(bytes); self.* = undefined; } fn adoptMerged(self: *AppliedEdit, allocator: Allocator, bytes: []u8) void { if (self.merged) |previous| allocator.free(previous); self.merged = bytes; self.current = bytes; }};pub const Relation = struct { space: space_mod.Space, table: table_mod.Table, indexes: [max_indexes]Indexed = undefined, index_count: usize, pub fn open(space: *const space_mod.Space, options: Options) Error!Relation { if (options.indexes.len > max_indexes) return error.TooManyIndexes; var relation = Relation{ .space = space.*, .table = try space.rowidTable(options.table_root), .index_count = options.indexes.len, }; for (options.indexes, 0..) |spec, offset| { if (spec.fields.len > max_index_fields) return error.TooManyIndexFields; relation.indexes[offset] = .{ .index = try space.index(spec.root_page, spec.columns), .fields = spec.fields, }; } return relation; } pub fn reader(self: *const Relation, snapshot: file.Snapshot) Error!Reader { const opened_space = try self.space.reader(snapshot); var opened = Reader{ .space = opened_space, .table = try opened_space.rowidTable(self.table.rows.root_page), .index_count = self.index_count, }; for (0..self.index_count) |offset| { opened.indexes[offset] = .{ .index = try opened_space.index( self.indexes[offset].index.entries.root_page, self.indexes[offset].index.columns, ), .fields = self.indexes[offset].fields, }; } return opened; } pub fn put(self: *Relation, allocator: Allocator, rowid: i64, values: []const row.Value, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("relation.put"); defer phase.end(); const bytes = try allocator.alloc(u8, try row.encodedSize(values)); defer allocator.free(bytes); const encoded = try row.encode(bytes, values); return try self.applyEdits(allocator, &.{.{ .put = .{ .rowid = rowid, .bytes = encoded, } }}, options); } pub fn putEncoded(self: *Relation, allocator: Allocator, rowid: i64, bytes: []const u8, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("relation.put_encoded"); defer phase.end(); return try self.applyEdits(allocator, &.{.{ .put = .{ .rowid = rowid, .bytes = bytes, } }}, options); } pub fn update(self: *Relation, allocator: Allocator, rowid: i64, assignments: []const Edit.Assignment, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("relation.update"); defer phase.end(); return try self.applyEdits(allocator, &.{.{ .update = .{ .rowid = rowid, .assignments = assignments, } }}, options); } pub fn delete(self: *Relation, allocator: Allocator, rowid: i64, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("relation.delete"); defer phase.end(); return try self.applyEdits(allocator, &.{.{ .delete = rowid }}, options); } pub fn applyEdits(self: *Relation, allocator: Allocator, edits: []const Edit, options: file.CommitOptions) Error!file.Commit { const phase = trace.scope("relation.apply_edits"); defer phase.end(); var write = try self.space.beginWrite(); defer write.deinit(); try self.applyEditsIn(allocator, &write, edits); return try write.commit(options); } pub fn applyEditsIn( self: *Relation, allocator: Allocator, write: *tree.Write, edits: []const Edit, ) Error!void { const phase = trace.scope("relation.apply_edits_in"); defer phase.end(); try write.claimBatch(self.table.rows.root_page); var states: std.AutoArrayHashMapUnmanaged(i64, AppliedEdit) = .empty; defer { for (states.values()) |*state| state.deinit(allocator); states.deinit(allocator); } const base_reader = try self.reader(write.snapshot); for (edits) |edit| { try accumulateEdit(allocator, &base_reader, &states, edit); } for (states.values()) |state| try self.applyEditStateIn(write, state); } pub fn lastRowId(self: *const Relation) Error!?i64 { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); return try opened.lastRowId(); } pub fn get(self: *const Relation, allocator: Allocator, rowid: i64) Error!?[]u8 { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); return try opened.get(allocator, rowid); } pub fn valueLength(self: *const Relation, rowid: i64) Error!?usize { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); return try opened.valueLength(rowid); } pub fn getInto(self: *const Relation, rowid: i64, target: []u8) Error!?[]u8 { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); return try opened.getInto(rowid, target); } pub fn scan( self: *const Relation, target: *Scan, allocator: Allocator, start: ?i64, end: ?i64, ) Error!void { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.scan(target, allocator, start, end); } pub fn scanProjected( self: *const Relation, target: *Scan, allocator: Allocator, start: ?i64, end: ?i64, projection: table_mod.Projection, ) Error!void { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.scanProjected(target, allocator, start, end, projection); } pub fn lookup( self: *const Relation, target: *index_mod.Scan, allocator: Allocator, index_slot: usize, prefix: []const row.Value, ) Error!void { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.lookup(target, allocator, index_slot, prefix); } pub fn indexScan( self: *const Relation, target: *index_mod.Scan, allocator: Allocator, index_slot: usize, start: ?[]const row.Value, end: ?[]const row.Value, ) Error!void { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.indexScan(target, allocator, index_slot, start, end); } pub fn indexRange( self: *const Relation, target: *index_mod.Scan, allocator: Allocator, index_slot: usize, start: ?index_mod.Bound, end: ?index_mod.Bound, ) Error!void { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); try opened.indexRange(target, allocator, index_slot, start, end); } pub fn summarize(self: *const Relation) Error!Summary { var read = try self.space.database.beginRead(); defer read.deinit(); const opened = try self.reader(read.snapshot()); return try opened.summarize(); } pub fn validateIndexes(self: *const Relation, allocator: Allocator) Error!void { const phase = trace.scope("relation.validate_indexes"); defer phase.end(); var table_scan: Scan = undefined; try self.scan(&table_scan, allocator, null, null); defer table_scan.deinit(); while (try table_scan.next()) |entry| { const view = try entry.view(); var index_offset: usize = 0; while (index_offset < self.index_count) : (index_offset += 1) { const indexed = &self.indexes[index_offset]; var projected: [max_index_fields]row.Value = undefined; const values = try projectView(view, indexed.fields, &projected); if (!try indexContainsRowid(allocator, &indexed.index, values, entry.rowid)) return error.SecondaryIndexCorrupt; } } var index_offset: usize = 0; while (index_offset < self.index_count) : (index_offset += 1) { const indexed = &self.indexes[index_offset]; var index_scan: index_mod.Scan = undefined; try indexed.index.scan(&index_scan, allocator, null, null); defer index_scan.deinit(); while (try index_scan.next()) |entry| { const bytes = (try self.table.get(allocator, entry.rowid)) orelse return error.SecondaryIndexCorrupt; defer allocator.free(bytes); const view = try row.View.init(bytes); var projected: [max_index_fields]row.Value = undefined; const values = try projectView(view, indexed.fields, &projected); var decoded_values: [max_index_fields]row.Value = undefined; var scratch: [page.size]u8 = undefined; const decoded = try key.decodeIndex(decoded_values[0..], scratch[0..], entry.key); const valid = decoded.rowid == entry.rowid and valuesEqual(values, decoded.values, indexed.index.columns); if (!valid) return error.SecondaryIndexCorrupt; } } } fn putViewIndexesIn(self: *Relation, write: *tree.Write, rowid: i64, view: row.View) Error!void { var offset: usize = 0; while (offset < self.index_count) : (offset += 1) { var projected: [max_index_fields]row.Value = undefined; const indexed = try projectView(view, self.indexes[offset].fields, &projected); try self.indexes[offset].index.putIn(write, rowid, indexed); } } fn deleteIndexesIn(self: *Relation, write: *tree.Write, rowid: i64, view: row.View) Error!void { var offset: usize = 0; while (offset < self.index_count) : (offset += 1) { var projected: [max_index_fields]row.Value = undefined; const indexed = try projectView(view, self.indexes[offset].fields, &projected); try self.indexes[offset].index.deleteIn(write, rowid, indexed); } } fn accumulateEdit( allocator: Allocator, base_reader: *const Reader, states: *std.AutoArrayHashMapUnmanaged(i64, AppliedEdit), edit: Edit, ) Error!void { const state = try appliedEdit( allocator, base_reader, states, editRowid(edit), ); switch (edit) { .put => |put_edit| { _ = try row.View.init(put_edit.bytes); state.current = put_edit.bytes; }, .update => |update_edit| { const current = state.current orelse return error.KeyNotFound; state.adoptMerged(allocator, try applyUpdate(allocator, current, update_edit.assignments)); }, .delete => { if (state.current == null) return error.KeyNotFound; state.current = null; }, } } fn appliedEdit( allocator: Allocator, base_reader: *const Reader, states: *std.AutoArrayHashMapUnmanaged(i64, AppliedEdit), rowid: i64, ) Error!*AppliedEdit { if (states.getPtr(rowid)) |state| return state; const base = try base_reader.get(allocator, rowid); errdefer if (base) |bytes| allocator.free(bytes); try states.putNoClobber(allocator, rowid, .{ .rowid = rowid, .base = base, .current = if (base) |bytes| bytes else null, }); return states.getPtr(rowid).?; } fn applyEditStateIn(self: *Relation, write: *tree.Write, state: AppliedEdit) Error!void { if (state.base) |base| { if (state.current) |current| { if (std.mem.eql(u8, base, current)) return; try self.deleteIndexesIn(write, state.rowid, try row.View.init(base)); try self.table.putEncodedIn(write, state.rowid, current); try self.putViewIndexesIn(write, state.rowid, try row.View.init(current)); } else { try self.deleteIndexesIn(write, state.rowid, try row.View.init(base)); try self.table.deleteIn(write, state.rowid); } } else if (state.current) |current| { try self.table.putEncodedIn(write, state.rowid, current); try self.putViewIndexesIn(write, state.rowid, try row.View.init(current)); } }};fn editRowid(edit: Edit) i64 { return switch (edit) { .put => |put| put.rowid, .update => |update| update.rowid, .delete => |rowid| rowid, };}fn indexContainsRowid(allocator: Allocator, index: *const index_mod.Index, values: []const row.Value, rowid: i64) Error!bool { var lookup: index_mod.Scan = undefined; try index.lookup(&lookup, allocator, values); defer lookup.deinit(); while (try lookup.next()) |entry| { if (entry.rowid == rowid) return true; } return false;}fn projectView(view: row.View, fields: []const usize, target: *[max_index_fields]row.Value) Error![]row.Value { if (fields.len > target.len) return error.TooManyIndexFields; return try view.project(fields, target[0..]);}fn valuesEqual(left: []const row.Value, right: []const row.Value, columns: []const row.Column) bool { if (left.len != right.len) return false; for (left, right, 0..) |left_value, right_value, offset| { const collation = if (offset < columns.len) columns[offset].collation else row.Collation.binary; if (row.compareValues(left_value, right_value, collation) != .eq) return false; } return true;}test "relation reader scans and looks up one fixed snapshot" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-reader.db", .wal = "relation-reader.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space_options = space_mod.Options{ .roots = &.{ .{ .root_page = 2, .identity_page = 4 }, .{ .root_page = 3, .identity_page = 5 }, }, }; const space = try space_mod.Space.open(&database, space_options); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; const options = Options{ .table_root = 2, .indexes = &specs }; var mutable = try Relation.open(&space, options); _ = try mutable.put(std.testing.allocator, 1, &.{.{ .text = "alpha" }}, .{}); _ = try mutable.put(std.testing.allocator, 2, &.{.{ .text = "beta" }}, .{}); var read = try database.beginRead(); defer read.deinit(); const read_space = try space_mod.Reader.open(read.snapshot(), space_options); const reader = try Reader.open(&read_space, options); _ = try mutable.put(std.testing.allocator, 3, &.{.{ .text = "alpha" }}, .{}); try std.testing.expectEqual(@as(?i64, 2), try reader.lastRowId()); var scan: Scan = undefined; try reader.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.expect(try scan.next() == null); const found = (try reader.get(std.testing.allocator, 2)).?; defer std.testing.allocator.free(found); try std.testing.expectEqual(found.len, (try reader.valueLength(2)).?); var row_buffer: [32]u8 = undefined; try std.testing.expectEqualSlices(u8, found, (try reader.getInto(2, &row_buffer)).?); var lookup: index_mod.Scan = undefined; try reader.lookup(&lookup, std.testing.allocator, 0, &.{.{ .text = "alpha" }}); defer lookup.deinit(); try std.testing.expectEqual(@as(i64, 1), (try lookup.next()).?.rowid); try std.testing.expect(try lookup.next() == null); const summary = try reader.summarize(); try std.testing.expectEqual(@as(usize, 2), summary.table.entries); try std.testing.expectEqual(@as(usize, 2), summary.indexes[0].entries); try std.testing.expectEqual(@as(u64, 2), (try reader.table.rows.identity()).entries); const index_tree = try reader.space.tree(3); try std.testing.expectEqual(@as(u64, 2), (try index_tree.identity()).entries); try std.testing.expect(!@hasDecl(tree.Reader, "put")); try std.testing.expect(!@hasDecl(table_mod.Reader, "put")); try std.testing.expect(!@hasDecl(index_mod.Reader, "put")); try std.testing.expect(!@hasDecl(space_mod.Reader, "beginWrite")); try std.testing.expect(!@hasDecl(Reader, "put")); try std.testing.expect(!@hasDecl(Reader, "applyEdits"));}test "relation maintains secondary index through replace and reopen" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation.db", .wal = "relation.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); _ = try relation.put(std.testing.allocator, 7, &.{ .{ .integer = 1 }, .{ .text = "one" } }, .{ .durability = .buffered }); _ = try relation.put(std.testing.allocator, 7, &.{ .{ .integer = 2 }, .{ .text = "two" } }, .{ .durability = .buffered }); try database.syncWal(); } var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation.db", .wal = "relation.wal" }, .header = recoveredHeader(), }); defer reopened.deinit(); const space = try space_mod.Space.open(&reopened, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); var old_lookup: index_mod.Scan = undefined; try relation.lookup(&old_lookup, std.testing.allocator, 0, &.{.{ .integer = 1 }}); defer old_lookup.deinit(); try std.testing.expect(try old_lookup.next() == null); var new_lookup: index_mod.Scan = undefined; try relation.lookup(&new_lookup, std.testing.allocator, 0, &.{.{ .integer = 2 }}); defer new_lookup.deinit(); const entry = (try new_lookup.next()).?; try std.testing.expectEqual(@as(i64, 7), entry.rowid); try std.testing.expect(try new_lookup.next() == null); const bytes = (try relation.get(std.testing.allocator, 7)).?; defer std.testing.allocator.free(bytes); const view = try row.View.init(bytes); try std.testing.expectEqual(@as(i64, 2), (try view.column(0)).integer); try std.testing.expectEqualStrings("two", (try view.column(1)).text);}test "relation delete removes table row and secondary index entry" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation.db", .wal = "relation.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); _ = try relation.put(std.testing.allocator, 4, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered }); _ = try relation.delete(std.testing.allocator, 4, .{ .durability = .buffered }); const missing = try relation.get(std.testing.allocator, 4); if (missing) |bytes| std.testing.allocator.free(bytes); try std.testing.expect(missing == null); var lookup: index_mod.Scan = undefined; try relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 9 }}); defer lookup.deinit(); try std.testing.expect(try lookup.next() == null); try std.testing.expectError(error.KeyNotFound, relation.delete(std.testing.allocator, 4, .{ .durability = .buffered }));}test "relation applies edit batches to table and secondary indexes" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-batch.db", .wal = "relation-batch.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 220 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); var one_buffer: [64]u8 = undefined; var two_buffer: [64]u8 = undefined; var three_buffer: [64]u8 = undefined; const one = try row.encode(&one_buffer, &.{ .{ .integer = 1 }, .{ .text = "one" } }); const two = try row.encode(&two_buffer, &.{ .{ .integer = 2 }, .{ .text = "two" } }); const three = try row.encode(&three_buffer, &.{ .{ .integer = 3 }, .{ .text = "three" } }); _ = try relation.applyEdits(std.testing.allocator, &.{ .{ .put = .{ .rowid = 1, .bytes = one } }, .{ .put = .{ .rowid = 2, .bytes = two } }, }, .{ .durability = .buffered }); _ = try relation.applyEdits(std.testing.allocator, &.{ .{ .delete = 1 }, .{ .put = .{ .rowid = 2, .bytes = three } }, }, .{ .durability = .buffered }); const missing = try relation.get(std.testing.allocator, 1); if (missing) |bytes| std.testing.allocator.free(bytes); try std.testing.expect(missing == null); const found = (try relation.get(std.testing.allocator, 2)).?; defer std.testing.allocator.free(found); const view = try row.View.init(found); try std.testing.expectEqual(@as(i64, 3), (try view.column(0)).integer); try std.testing.expectEqualStrings("three", (try view.column(1)).text); var old_lookup: index_mod.Scan = undefined; try relation.lookup(&old_lookup, std.testing.allocator, 0, &.{.{ .integer = 2 }}); defer old_lookup.deinit(); try std.testing.expect(try old_lookup.next() == null); var new_lookup: index_mod.Scan = undefined; try relation.lookup(&new_lookup, std.testing.allocator, 0, &.{.{ .integer = 3 }}); defer new_lookup.deinit(); try std.testing.expectEqual(@as(i64, 2), (try new_lookup.next()).?.rowid); try std.testing.expect(try new_lookup.next() == null); _ = try relation.applyEdits(std.testing.allocator, &.{ .{ .put = .{ .rowid = 9, .bytes = one } }, .{ .delete = 9 }, }, .{ .durability = .buffered }); { var write = try space.beginWrite(); defer write.deinit(); try relation.applyEditsIn( std.testing.allocator, &write, &.{.{ .put = .{ .rowid = 10, .bytes = one } }}, ); try std.testing.expectError( error.WriteBatchRepeated, relation.applyEditsIn( std.testing.allocator, &write, &.{.{ .delete = 10 }}, ), ); } const discarded = try relation.get(std.testing.allocator, 10); if (discarded) |bytes| std.testing.allocator.free(bytes); try std.testing.expect(discarded == null); var no_op_lookup: index_mod.Scan = undefined; try relation.lookup(&no_op_lookup, std.testing.allocator, 0, &.{.{ .integer = 1 }}); defer no_op_lookup.deinit(); try std.testing.expect(try no_op_lookup.next() == null);}test "relation update merges assigned columns and maintains secondary index" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-update.db", .wal = "relation-update.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); _ = try relation.put(std.testing.allocator, 4, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered }); _ = try relation.update(std.testing.allocator, 4, &.{.{ .column = 0, .value = .{ .integer = 12 } }}, .{ .durability = .buffered }); const found = (try relation.get(std.testing.allocator, 4)).?; defer std.testing.allocator.free(found); const view = try row.View.init(found); try std.testing.expectEqual(@as(i64, 12), (try view.column(0)).integer); try std.testing.expectEqualStrings("nine", (try view.column(1)).text); var old_lookup: index_mod.Scan = undefined; try relation.lookup(&old_lookup, std.testing.allocator, 0, &.{.{ .integer = 9 }}); defer old_lookup.deinit(); try std.testing.expect(try old_lookup.next() == null); var new_lookup: index_mod.Scan = undefined; try relation.lookup(&new_lookup, std.testing.allocator, 0, &.{.{ .integer = 12 }}); defer new_lookup.deinit(); try std.testing.expectEqual(@as(i64, 4), (try new_lookup.next()).?.rowid); try std.testing.expect(try new_lookup.next() == null); try relation.validateIndexes(std.testing.allocator);}test "relation update requires an existing row and known columns" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-update-missing.db", .wal = "relation-update-missing.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{.{ .root_page = 2 }} }); var relation = try Relation.open(&space, .{ .table_root = 2 }); try std.testing.expectError(error.KeyNotFound, relation.update(std.testing.allocator, 7, &.{.{ .column = 0, .value = .{ .integer = 1 } }}, .{ .durability = .buffered })); _ = try relation.put(std.testing.allocator, 7, &.{ .{ .integer = 1 }, .{ .text = "one" } }, .{ .durability = .buffered }); try std.testing.expectError(error.ColumnOutOfBounds, relation.update(std.testing.allocator, 7, &.{.{ .column = 2, .value = .{ .integer = 5 } }}, .{ .durability = .buffered })); var delete_then_update = [_]Edit{ .{ .delete = 7 }, .{ .update = .{ .rowid = 7, .assignments = &.{.{ .column = 0, .value = .{ .integer = 5 } }} } }, }; try std.testing.expectError(error.KeyNotFound, relation.applyEdits(std.testing.allocator, &delete_then_update, .{ .durability = .buffered }));}test "relation applies update edits within batches" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-update-batch.db", .wal = "relation-update-batch.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 220 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); var fresh_buffer: [64]u8 = undefined; const fresh = try row.encode(&fresh_buffer, &.{ .{ .integer = 1 }, .{ .text = "one" } }); _ = try relation.applyEdits(std.testing.allocator, &.{ .{ .put = .{ .rowid = 3, .bytes = fresh } }, .{ .update = .{ .rowid = 3, .assignments = &.{.{ .column = 0, .value = .{ .integer = 2 } }} } }, .{ .update = .{ .rowid = 3, .assignments = &.{.{ .column = 1, .value = .{ .text = "two" } }} } }, }, .{ .durability = .buffered }); const found = (try relation.get(std.testing.allocator, 3)).?; defer std.testing.allocator.free(found); const view = try row.View.init(found); try std.testing.expectEqual(@as(i64, 2), (try view.column(0)).integer); try std.testing.expectEqualStrings("two", (try view.column(1)).text); var stale_lookup: index_mod.Scan = undefined; try relation.lookup(&stale_lookup, std.testing.allocator, 0, &.{.{ .integer = 1 }}); defer stale_lookup.deinit(); try std.testing.expect(try stale_lookup.next() == null); var live_lookup: index_mod.Scan = undefined; try relation.lookup(&live_lookup, std.testing.allocator, 0, &.{.{ .integer = 2 }}); defer live_lookup.deinit(); try std.testing.expectEqual(@as(i64, 3), (try live_lookup.next()).?.rowid); try std.testing.expect(try live_lookup.next() == null); try relation.validateIndexes(std.testing.allocator);}test "relation validates secondary indexes against table rows" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-validate.db", .wal = "relation-validate.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); _ = try relation.put(std.testing.allocator, 4, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered }); try relation.validateIndexes(std.testing.allocator); _ = try relation.indexes[0].index.delete(4, &.{.{ .integer = 9 }}, .{ .durability = .buffered }); try std.testing.expectError(error.SecondaryIndexCorrupt, relation.validateIndexes(std.testing.allocator));}test "relation rejects orphaned secondary index entries" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-orphan.db", .wal = "relation-orphan.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); _ = try relation.put(std.testing.allocator, 4, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered }); _ = try relation.indexes[0].index.put(99, &.{.{ .integer = 99 }}, .{ .durability = .buffered }); try std.testing.expectError(error.SecondaryIndexCorrupt, relation.validateIndexes(std.testing.allocator));}test "relation rejects mismatched secondary index entries" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-mismatch.db", .wal = "relation-mismatch.wal", }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } }, }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs, }); _ = try relation.put( std.testing.allocator, 4, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered }, ); _ = try relation.indexes[0].index.put( 4, &.{.{ .integer = 99 }}, .{ .durability = .buffered }, ); try std.testing.expectError( error.SecondaryIndexCorrupt, relation.validateIndexes(std.testing.allocator), );}test "relation rejects an index key larger than a page and stays writable" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-widekey.db", .wal = "relation-widekey.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 512 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{ .{ .root_page = 2 }, .{ .root_page = 3 } } }); const specs = [_]IndexSpec{.{ .root_page = 3, .fields = &.{0} }}; var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &specs }); const sizes = [_]usize{ 512, 1024, 1900, 8000, 1000 }; const expected = [sizes.len]?anyerror{ null, null, null, error.KeyTooLarge, null }; var outcomes: [sizes.len]?anyerror = undefined; for (sizes, 0..) |size, ordinal| { const text = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(text); @memset(text, 'x'); text[0] = @intCast('a' + ordinal); const values = [_]row.Value{ .{ .text = text }, .{ .integer = @intCast(size) } }; const encoded = try std.testing.allocator.alloc(u8, try row.encodedSize(&values)); defer std.testing.allocator.free(encoded); _ = try row.encode(encoded, &values); outcomes[ordinal] = null; _ = relation.applyEdits(std.testing.allocator, &.{.{ .put = .{ .rowid = @intCast(ordinal + 1), .bytes = encoded, } }}, .{ .durability = .buffered }) catch |err| { outcomes[ordinal] = err; }; } try std.testing.expectEqualSlices(?anyerror, &expected, &outcomes); const missing = try relation.get(std.testing.allocator, 4); if (missing) |bytes| std.testing.allocator.free(bytes); try std.testing.expect(missing == null); const accepted = (try relation.get(std.testing.allocator, 5)).?; defer std.testing.allocator.free(accepted); try relation.validateIndexes(std.testing.allocator);}test "relation put accepts rows larger than a page" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "relation-widerow.db", .wal = "relation-widerow.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 512 }); const space = try space_mod.Space.open(&database, .{ .roots = &.{.{ .root_page = 2 }} }); var relation = try Relation.open(&space, .{ .table_root = 2, .indexes = &.{} }); const sizes = [_]usize{ 4000, page.size, page.size * 4 }; for (sizes, 0..) |size, ordinal| { const text = try std.testing.allocator.alloc(u8, size); defer std.testing.allocator.free(text); @memset(text, 'y'); const values = [_]row.Value{ .{ .text = text }, .{ .integer = @intCast(size) } }; _ = try relation.put(std.testing.allocator, @intCast(ordinal + 1), &values, .{ .durability = .buffered }); const stored = (try relation.get(std.testing.allocator, @intCast(ordinal + 1))).?; defer std.testing.allocator.free(stored); const view = try row.View.init(stored); try std.testing.expectEqualStrings(text, (try view.column(0)).text); }}fn testingHeader() wal.Header { return .{ .sequence = 1001, .salt = .{ .first = 0x9191_a2a2, .second = 0x6363_b4b4 }, };}fn recoveredHeader() wal.Header { return .{ .sequence = 1002, .salt = .{ .first = 0xaaaa_7777, .second = 0xbbbb_8888 }, };}Source: lib/sql/src/root.zig:36
zig
pub const relation = @import("relation.zig");Audit
| Definitions | 24 |
|---|---|
| Public names | 24 |
| Members | 16 |
| Version | 26.7.0 |
| Revision | daab053ee433 |