Skip to documentation
SLOP

tiny.sql.relation

Reference tiny.sql relation

Defined in tiny.sql.

API (27)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

No direct callersNo direct callstiny.sqlrelation
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callstest sourcelib.sql.src.relationtest: relation reader scans and looks...relation.Readeropen
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsprivate sourcelib.sql.src.relation.RelationaccumulateEditRowViewcolumnCountRowViewcursorRowViewinitrowencoderowencodedSizerelationapplyUpdate
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsNo direct callersrelationfreeAssignmentsrelationcloneAssignments
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callsrelationcloneAssignmentsrelationfreeAssignments
Static calls · unresolved targets: 0 · external targets: 1.

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

Definitions24
Public names24
Members16
Version26.7.0
Revisiondaab053ee433