Skip to documentation
SLOP

tiny.sql.plan

Reference tiny.sql plan

Defined in tiny.sql.

API (9)

Actions

Public operations.

Types and contracts

Public types and contracts.

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

Source

Called byCallsprivate sourcelib.sql.src.statement.executeprepareRelationprivate sourcelib.sql.src.planprepareRelationWithValidationplanprepareRelationShape
Static calls · unresolved targets: 0 · external targets: 0.

Source: lib/sql/src/plan.zig

zig
const std = @import("std");const branch = @import("branch.zig");const catalog_mod = @import("catalog.zig");const file = @import("file.zig");const index_mod = @import("index.zig");const relation_mod = @import("relation.zig");const row = @import("row.zig");const session_mod = @import("session/root.zig");const trace = @import("trace.zig");const version = @import("version.zig");const wal = @import("wal.zig");const Allocator = std.mem.Allocator;const PlanError = error{    PlanChanged,};pub const Error = catalog_mod.Error || version.Error || session_mod.DatabaseError || PlanError;pub const PlanKey = struct {    relation: version.Hash,    schema: version.Hash,    stats: version.Hash,    parameters: version.Hash,    pub fn fromRoot(root: *const version.RelationRoot, parameters: version.Hash) PlanKey {        return .{            .relation = root.hash,            .schema = root.schema,            .stats = root.stats.hash,            .parameters = parameters,        };    }    pub fn fromRelationKey(relation_key: version.RelationKey, parameters: version.Hash) PlanKey {        return .{            .relation = relation_key.hash,            .schema = relation_key.schema,            .stats = relation_key.stats,            .parameters = parameters,        };    }    pub fn same(left: PlanKey, right: PlanKey) bool {        return version.same(left.relation, right.relation) and            version.same(left.schema, right.schema) and            version.same(left.stats, right.stats) and            version.same(left.parameters, right.parameters);    }};pub const Validation = enum {    content,    shape,};pub fn emptyParameterShape() version.Hash {    return version.emptyHash("sql.parameters.none");}fn shapeKey(name: []const u8, handle: *const catalog_mod.RelationHandle, stats: ?*const catalog_mod.RelationStats, parameters: version.Hash) PlanKey {    const schema_hash = version.schemaHash(handle.definitions, handle.index_definitions);    const stats_hash = statsShapeHash(stats);    var relation = ShapeBuilder.init("sql.plan.relation.shape");    relation.bytes(name);    relation.hash(schema_hash);    relation.hash(stats_hash);    relation.writeU32(handle.relation.table.rows.root_page);    relation.writeU64(handle.specs.len);    for (handle.specs) |spec| {        relation.writeU32(spec.root_page);        relation.writeU64(spec.fields.len);        for (spec.fields) |field| relation.writeU64(field);        relation.writeU64(spec.columns.len);        for (spec.columns) |column| relation.writeU64(@backingInt(column.collation));    }    return .{        .relation = relation.finish(),        .schema = schema_hash,        .stats = stats_hash,        .parameters = parameters,    };}fn statsShapeHash(stats: ?*const catalog_mod.RelationStats) version.Hash {    const relation_stats = stats orelse return version.emptyHash("sql.stats.none");    var builder = ShapeBuilder.init("sql.plan.stats.shape");    builder.writeU32(relation_stats.table_root_page);    builder.summary(relation_stats.table);    builder.writeU64(relation_stats.indexes.len);    for (relation_stats.indexes) |index_stats| {        builder.bytes(index_stats.name);        builder.writeU32(index_stats.root_page);        builder.summary(index_stats.summary);        builder.distribution(index_stats.distribution);    }    return builder.finish();}const ShapeBuilder = struct {    hasher: std.crypto.hash.sha2.Sha256,    fn init(tag: []const u8) ShapeBuilder {        var builder = ShapeBuilder{ .hasher = std.crypto.hash.sha2.Sha256.init(.{}) };        builder.bytes(tag);        return builder;    }    fn finish(self: *ShapeBuilder) version.Hash {        var digest: version.Hash = undefined;        self.hasher.final(&digest);        return digest;    }    fn bytes(self: *ShapeBuilder, value: []const u8) void {        self.writeU64(value.len);        self.hasher.update(value);    }    fn hash(self: *ShapeBuilder, value: version.Hash) void {        self.hasher.update(&value);    }    fn writeU32(self: *ShapeBuilder, value: u32) void {        var encoded: [4]u8 = undefined;        std.mem.writeInt(u32, &encoded, value, .big);        self.hasher.update(&encoded);    }    fn writeU64(self: *ShapeBuilder, value: anytype) void {        var encoded: [8]u8 = undefined;        std.mem.writeInt(u64, &encoded, @intCast(value), .big);        self.hasher.update(&encoded);    }    fn summary(self: *ShapeBuilder, value: @import("tree.zig").Summary) void {        self.writeU64(value.branch_pages);        self.writeU64(value.leaf_pages);        self.writeU64(value.overflow_pages);        self.writeU64(value.entries);        self.writeU64(value.inline_records);        self.writeU64(value.overflow_records);        self.writeU64(value.max_depth);        self.writeU64(value.key_bytes);        self.writeU64(value.record_bytes);        self.writeU64(value.value_bytes);    }    fn distribution(self: *ShapeBuilder, value: catalog_mod.IndexDistribution) void {        self.writeU64(value.distinct_values);        self.writeU64(value.max_equal);        self.samples(value.samples);        self.bytes(value.sample_keys);        self.writeU64(value.prefixes.len);        for (value.prefixes) |prefix| {            self.writeU64(prefix.field_count);            self.writeU64(prefix.distinct_values);            self.writeU64(prefix.max_equal);            self.samples(prefix.samples);            self.bytes(prefix.sample_keys);        }    }    fn samples(self: *ShapeBuilder, values: []const catalog_mod.IndexSample) void {        self.writeU64(values.len);        for (values) |sample| {            self.bytes(sample.key);            self.writeU64(sample.less_than);            self.writeU64(sample.equal_count);            self.writeU64(sample.less_distinct);        }    }};pub const RelationExecution = struct {    reader: relation_mod.Reader,    pub fn get(self: RelationExecution, allocator: Allocator, rowid: i64) Error!?[]u8 {        return try self.reader.get(allocator, rowid);    }    pub fn scan(        self: RelationExecution,        target: *relation_mod.Scan,        allocator: Allocator,        start: ?i64,        end: ?i64,    ) Error!void {        try self.reader.scan(target, allocator, start, end);    }    pub fn lookup(        self: RelationExecution,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        prefix: []const row.Value,    ) Error!void {        try self.reader.lookup(target, allocator, index_slot, prefix);    }    pub fn indexScan(        self: RelationExecution,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        start: ?[]const row.Value,        end: ?[]const row.Value,    ) Error!void {        try self.reader.indexScan(target, allocator, index_slot, start, end);    }    pub fn indexRange(        self: RelationExecution,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        start: ?index_mod.Bound,        end: ?index_mod.Bound,    ) Error!void {        try self.reader.indexRange(target, allocator, index_slot, start, end);    }};pub const RelationRead = struct {    lease: file.ReadLease,    execution: RelationExecution,    pub fn deinit(self: *RelationRead) void {        self.lease.deinit();        self.* = undefined;    }    pub fn borrow(self: *const RelationRead) RelationExecution {        return self.execution;    }    pub fn get(self: *const RelationRead, allocator: Allocator, rowid: i64) Error!?[]u8 {        return try self.execution.get(allocator, rowid);    }    pub fn scan(        self: *const RelationRead,        target: *relation_mod.Scan,        allocator: Allocator,        start: ?i64,        end: ?i64,    ) Error!void {        try self.execution.scan(target, allocator, start, end);    }    pub fn lookup(        self: *const RelationRead,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        prefix: []const row.Value,    ) Error!void {        try self.execution.lookup(target, allocator, index_slot, prefix);    }    pub fn indexScan(        self: *const RelationRead,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        start: ?[]const row.Value,        end: ?[]const row.Value,    ) Error!void {        try self.execution.indexScan(target, allocator, index_slot, start, end);    }    pub fn indexRange(        self: *const RelationRead,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        start: ?index_mod.Bound,        end: ?index_mod.Bound,    ) Error!void {        try self.execution.indexRange(target, allocator, index_slot, start, end);    }};pub const PreparedRelation = struct {    allocator: Allocator,    catalog: catalog_mod.Catalog,    name: []u8,    schema: catalog_mod.Schema,    handle: catalog_mod.RelationHandle,    stats: ?catalog_mod.RelationStats,    root: ?version.RelationRoot,    key: PlanKey,    validation: Validation = .content,    pub fn deinit(self: *PreparedRelation) void {        if (self.root) |*root| root.deinit();        if (self.stats) |*stats| stats.deinit();        self.handle.deinit();        self.allocator.free(self.name);        self.* = undefined;    }    pub fn validate(self: *PreparedRelation) Error!void {        const phase = trace.scope("plan.validate_relation");        defer phase.end();        const current = try self.currentCacheKey();        if (!PlanKey.same(current, self.key)) return error.PlanChanged;    }    pub fn execute(self: *PreparedRelation) Error!RelationRead {        const phase = trace.scope("plan.execute_relation");        defer phase.end();        try self.validate();        var lease = try self.handle.relation.space.database.beginRead();        errdefer lease.deinit();        return .{            .lease = lease,            .execution = .{                .reader = try self.handle.relation.reader(lease.snapshot()),            },        };    }    pub fn relationStats(self: *const PreparedRelation) ?*const catalog_mod.RelationStats {        if (self.stats) |*stats| return stats;        return null;    }    pub fn cacheKey(self: *const PreparedRelation) PlanKey {        return self.key;    }    pub fn currentCacheKey(self: *PreparedRelation) Error!PlanKey {        var state = try self.catalog.readRelation(self.allocator, self.name);        defer state.deinit();        return switch (self.validation) {            .content => content_key: {                const relation_key = try version.relationKey(                    self.name,                    &state.handle,                    state.relationStats(),                );                break :content_key PlanKey.fromRelationKey(relation_key, self.key.parameters);            },            .shape => shapeKey(                self.name,                &state.handle,                state.relationStats(),                self.key.parameters,            ),        };    }    pub fn matchesRoot(self: *const PreparedRelation, root: *const version.RelationRoot) bool {        if (self.root == null) return false;        return PlanKey.same(self.key, PlanKey.fromRoot(root, self.key.parameters));    }    pub fn writeSession(self: *PreparedRelation) Error!session_mod.RelationSession {        const phase = trace.scope("plan.write_session");        defer phase.end();        var relation_session = try session_mod.RelationSession.open(self.allocator, &self.catalog, self.name);        errdefer relation_session.deinit();        if (!self.matchesRoot(&relation_session.root)) return error.PlanChanged;        return relation_session;    }    pub fn refresh(self: *PreparedRelation) Error!void {        var state = try self.catalog.readRelation(self.allocator, self.name);        errdefer state.deinit();        var root: ?version.RelationRoot = null;        errdefer if (root) |*relation_root| relation_root.deinit();        const key = switch (self.validation) {            .content => key: {                root = try version.relationRootMaintained(                    self.allocator,                    self.name,                    state.schema,                    &state.handle,                    state.relationStats(),                );                break :key PlanKey.fromRoot(&root.?, self.key.parameters);            },            .shape => shapeKey(                self.name,                &state.handle,                state.relationStats(),                self.key.parameters,            ),        };        if (self.root) |*old_root| old_root.deinit();        if (self.stats) |*old_stats| old_stats.deinit();        self.handle.deinit();        self.schema = state.schema;        self.handle = state.handle;        self.stats = state.stats;        self.root = root;        self.key = key;    }    pub fn get(self: *PreparedRelation, allocator: Allocator, rowid: i64) Error!?[]u8 {        var execution = try self.execute();        defer execution.deinit();        return try execution.get(allocator, rowid);    }    pub fn scan(        self: *PreparedRelation,        target: *relation_mod.Scan,        allocator: Allocator,        start: ?i64,        end: ?i64,    ) Error!void {        var execution = try self.execute();        defer execution.deinit();        try execution.scan(target, allocator, start, end);    }    pub fn lookup(        self: *PreparedRelation,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        prefix: []const row.Value,    ) Error!void {        var execution = try self.execute();        defer execution.deinit();        try execution.lookup(target, allocator, index_slot, prefix);    }    pub fn indexScan(        self: *PreparedRelation,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        start: ?[]const row.Value,        end: ?[]const row.Value,    ) Error!void {        var execution = try self.execute();        defer execution.deinit();        try execution.indexScan(target, allocator, index_slot, start, end);    }    pub fn indexRange(        self: *PreparedRelation,        target: *index_mod.Scan,        allocator: Allocator,        index_slot: usize,        start: ?index_mod.Bound,        end: ?index_mod.Bound,    ) Error!void {        var execution = try self.execute();        defer execution.deinit();        try execution.indexRange(target, allocator, index_slot, start, end);    }};pub fn prepareRelation(catalog: *const catalog_mod.Catalog, allocator: Allocator, name: []const u8, parameters: version.Hash) Error!PreparedRelation {    return try prepareRelationWithValidation(catalog, allocator, name, parameters, .content);}pub fn prepareRelationShape(catalog: *const catalog_mod.Catalog, allocator: Allocator, name: []const u8, parameters: version.Hash) Error!PreparedRelation {    return try prepareRelationWithValidation(catalog, allocator, name, parameters, .shape);}fn prepareRelationWithValidation(catalog: *const catalog_mod.Catalog, allocator: Allocator, name: []const u8, parameters: version.Hash, validation: Validation) Error!PreparedRelation {    const phase = trace.scope("plan.prepare_relation");    defer phase.end();    const owned_name = try allocator.dupe(u8, name);    errdefer allocator.free(owned_name);    var state = try catalog.readRelation(allocator, name);    errdefer state.deinit();    var root: ?version.RelationRoot = null;    errdefer if (root) |*relation_root| relation_root.deinit();    const key = switch (validation) {        .content => key: {            root = try version.relationRootMaintained(                allocator,                name,                state.schema,                &state.handle,                state.relationStats(),            );            break :key PlanKey.fromRoot(&root.?, parameters);        },        .shape => shapeKey(name, &state.handle, state.relationStats(), parameters),    };    return .{        .allocator = allocator,        .catalog = catalog.*,        .name = owned_name,        .schema = state.schema,        .handle = state.handle,        .stats = state.stats,        .root = root,        .key = key,        .validation = validation,    };}fn preparedPut(prepared: *PreparedRelation, allocator: Allocator, rowid: i64, values: []const row.Value, options: file.CommitOptions) Error!session_mod.RelationFlush {    var relation_session = try prepared.writeSession();    var relation_session_live = true;    errdefer if (relation_session_live) relation_session.deinit();    try relation_session.put(rowid, values);    const flush = try flushPreparedRelation(prepared, allocator, &relation_session, &relation_session_live, options);    try prepared.refresh();    return flush;}fn preparedPutEncoded(prepared: *PreparedRelation, allocator: Allocator, rowid: i64, bytes: []const u8, options: file.CommitOptions) Error!session_mod.RelationFlush {    var relation_session = try prepared.writeSession();    var relation_session_live = true;    errdefer if (relation_session_live) relation_session.deinit();    try relation_session.putEncoded(rowid, bytes);    const flush = try flushPreparedRelation(prepared, allocator, &relation_session, &relation_session_live, options);    try prepared.refresh();    return flush;}fn preparedDelete(prepared: *PreparedRelation, allocator: Allocator, rowid: i64, options: file.CommitOptions) Error!session_mod.RelationFlush {    var relation_session = try prepared.writeSession();    var relation_session_live = true;    errdefer if (relation_session_live) relation_session.deinit();    try relation_session.delete(rowid);    const flush = try flushPreparedRelation(prepared, allocator, &relation_session, &relation_session_live, options);    try prepared.refresh();    return flush;}fn flushPreparedRelation(prepared: *PreparedRelation, allocator: Allocator, relation_session: *session_mod.RelationSession, relation_session_live: *bool, options: file.CommitOptions) Error!session_mod.RelationFlush {    var root = try version.databaseRootMaintained(        allocator,        &prepared.catalog,        version.ConflictRoot.empty().hash,    );    var root_live = true;    errdefer if (root_live) root.deinit();    const commit = version.Commit.init(root.hash, &.{});    const root_hash = root.hash;    var database_session = session_mod.DatabaseSession.initWithRoot(allocator, branch.checkout(.{        .name = "prepared",        .target = commit.hash,    }, root_hash), &root);    root_live = false;    defer database_session.deinit();    const limits = try relation_session.stagingLimits();    var workspace = try session_mod.DatabaseWrite.Workspace.allocate(        allocator,        limits,    );    defer workspace.deallocate(allocator);    var write = try database_session.beginWrite(        &workspace,        allocator,        limits,        options,    );    defer write.deinit();    try write.stageRelation(relation_session);    relation_session_live.* = false;    var database_flush = try write.flush();    defer database_flush.deinit();    return database_flush.onlyRelation();}test "prepared relation ignores unrelated catalog schema version bumps" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "plan.db", .wal = "plan.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 420 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    const indexes = [_]catalog_mod.IndexDefinition{.{        .name = "items_value",        .fields = &.{0},    }};    const created = try catalog.createRelation(std.testing.allocator, .{        .name = "items",        .indexes = &indexes,    }, .{ .durability = .buffered });    try std.testing.expectEqual(@as(u64, 1), created.schema.version);    {        var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());        defer prepared.deinit();        try std.testing.expectEqual(created.schema, prepared.schema);        _ = try preparedPut(&prepared, std.testing.allocator, 1, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });        try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator));        var lookup: index_mod.Scan = undefined;        try prepared.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 7 }});        defer lookup.deinit();        try std.testing.expectEqual(@as(i64, 1), (try lookup.next()).?.rowid);        try std.testing.expect(try lookup.next() == null);        const second = try catalog.createRelation(std.testing.allocator, .{            .name = "users",        }, .{ .durability = .buffered });        try std.testing.expectEqual(@as(u64, 2), second.schema.version);        try prepared.validate();        var execution = try prepared.execute();        defer execution.deinit();        const bytes = (try execution.get(std.testing.allocator, 1)).?;        defer std.testing.allocator.free(bytes);        const view = try row.View.init(bytes);        try std.testing.expectEqual(@as(i64, 7), (try view.column(0)).integer);        try std.testing.expectEqualStrings("seven", (try view.column(1)).text);        try std.testing.expect(PlanKey.same(prepared.cacheKey(), try prepared.currentCacheKey()));        _ = try preparedPut(&prepared, std.testing.allocator, 2, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered });        const second_bytes = (try prepared.get(std.testing.allocator, 2)).?;        defer std.testing.allocator.free(second_bytes);        const second_view = try row.View.init(second_bytes);        try std.testing.expectEqual(@as(i64, 9), (try second_view.column(0)).integer);    }    {        var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());        defer prepared.deinit();        try std.testing.expectEqual(@as(u64, 2), prepared.schema.version);        var execution = try prepared.execute();        defer execution.deinit();        const bytes = (try execution.get(std.testing.allocator, 1)).?;        defer std.testing.allocator.free(bytes);        const view = try row.View.init(bytes);        try std.testing.expectEqual(@as(i64, 7), (try view.column(0)).integer);        try std.testing.expectEqualStrings("seven", (try view.column(1)).text);    }}test "prepared relation detects external relation root changes" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "plan-root.db", .wal = "plan-root.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 420 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    const indexes = [_]catalog_mod.IndexDefinition{.{        .name = "items_value",        .fields = &.{0},    }};    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "items",        .indexes = &indexes,    }, .{ .durability = .buffered });    var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());    defer prepared.deinit();    const before = prepared.cacheKey();    var handle = try catalog.openRelation(std.testing.allocator, "items");    defer handle.deinit();    _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });    const current = try prepared.currentCacheKey();    try std.testing.expect(!PlanKey.same(before, current));    try std.testing.expect(!version.same(before.relation, current.relation));    try std.testing.expect(version.same(before.schema, current.schema));    try std.testing.expect(version.same(before.stats, current.stats));    try std.testing.expectError(error.PlanChanged, prepared.execute());    try std.testing.expectError(error.PlanChanged, prepared.get(std.testing.allocator, 1));    var stale_scan: index_mod.Scan = undefined;    try std.testing.expectError(        error.PlanChanged,        prepared.lookup(&stale_scan, std.testing.allocator, 0, &.{.{ .integer = 7 }}),    );    try std.testing.expectError(        error.PlanChanged,        prepared.indexScan(&stale_scan, std.testing.allocator, 0, null, null),    );    try std.testing.expectError(error.PlanChanged, prepared.writeSession());    var fresh = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());    defer fresh.deinit();    const bytes = (try fresh.get(std.testing.allocator, 1)).?;    defer std.testing.allocator.free(bytes);    const view = try row.View.init(bytes);    try std.testing.expectEqual(@as(i64, 7), (try view.column(0)).integer);}test "prepared relation data changes preserve captured schema" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "plan.db", .wal = "plan.wal" },        .header = recoveredHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 220 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    const created = try catalog.createRelation(std.testing.allocator, .{        .name = "items",    }, .{ .durability = .buffered });    var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());    defer prepared.deinit();    _ = try preparedPut(&prepared, std.testing.allocator, 1, &.{.{ .integer = 11 }}, .{ .durability = .buffered });    var encoded_buffer: [32]u8 = undefined;    const encoded = try row.encode(&encoded_buffer, &.{.{ .integer = 22 }});    _ = try preparedPutEncoded(&prepared, std.testing.allocator, 2, encoded, .{ .durability = .buffered });    _ = try preparedDelete(&prepared, std.testing.allocator, 1, .{ .durability = .buffered });    try prepared.validate();    try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator));    try std.testing.expectEqual(created.schema, prepared.schema);    const bytes = (try prepared.get(std.testing.allocator, 2)).?;    defer std.testing.allocator.free(bytes);    const view = try row.View.init(bytes);    try std.testing.expectEqual(@as(i64, 22), (try view.column(0)).integer);}test "prepared relation cache key changes after data root changes" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "plan-key.db", .wal = "plan-key.wal" },        .header = recoveredHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 220 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "items",    }, .{ .durability = .buffered });    var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());    defer prepared.deinit();    const before = prepared.cacheKey();    try std.testing.expect(PlanKey.same(before, try prepared.currentCacheKey()));    _ = try preparedPut(&prepared, std.testing.allocator, 1, &.{.{ .integer = 11 }}, .{ .durability = .buffered });    const after = prepared.cacheKey();    try std.testing.expect(PlanKey.same(after, try prepared.currentCacheKey()));    try std.testing.expect(!PlanKey.same(before, after));    try std.testing.expect(version.same(before.schema, after.schema));    try std.testing.expect(version.same(before.stats, after.stats));    try std.testing.expect(!version.same(before.relation, after.relation));}test "prepared relation cache key includes parameter shape" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "plan-parameters.db", .wal = "plan-parameters.wal" },        .header = recoveredHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 220 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "items",    }, .{ .durability = .buffered });    const first_shape = version.emptyHash("sql.parameters.first");    const second_shape = version.emptyHash("sql.parameters.second");    var first = try prepareRelation(&catalog, std.testing.allocator, "items", first_shape);    defer first.deinit();    var second = try prepareRelation(&catalog, std.testing.allocator, "items", second_shape);    defer second.deinit();    const first_key = first.cacheKey();    const second_key = second.cacheKey();    try std.testing.expect(version.same(first_key.relation, second_key.relation));    try std.testing.expect(version.same(first_key.schema, second_key.schema));    try std.testing.expect(version.same(first_key.stats, second_key.stats));    try std.testing.expect(!version.same(first_key.parameters, second_key.parameters));    try std.testing.expect(!PlanKey.same(first_key, second_key));}test "prepared relation invalidates by stats root without schema change" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "plan.db", .wal = "plan.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 360 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    const indexes = [_]catalog_mod.IndexDefinition{.{        .name = "items_value",        .fields = &.{0},    }};    const created = try catalog.createRelation(std.testing.allocator, .{        .name = "items",        .indexes = &indexes,    }, .{ .durability = .buffered });    var stale = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());    defer stale.deinit();    try std.testing.expect(stale.relationStats() == null);    _ = try preparedPut(&stale, std.testing.allocator, 1, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });    _ = try preparedPut(&stale, std.testing.allocator, 2, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered });    const analyzed = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered });    try std.testing.expectEqual(created.schema, analyzed.schema);    try std.testing.expectError(error.PlanChanged, stale.validate());    try std.testing.expect(stale.relationStats() == null);    var fresh = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());    defer fresh.deinit();    const stats = fresh.relationStats().?;    try std.testing.expectEqual(@as(usize, 2), stats.table.entries);    try std.testing.expectEqual(@as(usize, 1), stats.indexes.len);    try std.testing.expectEqual(@as(usize, 2), stats.index("items_value").?.summary.entries);    try std.testing.expect(!PlanKey.same(stale.cacheKey(), fresh.cacheKey()));    try std.testing.expect(!version.same(stale.cacheKey().stats, fresh.cacheKey().stats));}fn testingHeader() wal.Header {    return .{        .sequence = 1201,        .salt = .{ .first = 0x1212_eeee, .second = 0x3434_ffff },    };}fn recoveredHeader() wal.Header {    return .{        .sequence = 1202,        .salt = .{ .first = 0x5656_dddd, .second = 0x7878_cccc },    };}

Source: lib/sql/src/root.zig:33

zig
pub const plan = @import("plan.zig");

Audit

Definitions4
Public names4
Members2
Version26.7.0
Revisiondaab053ee433