tiny.sql.plan
Defined in tiny.sql.
API (9)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
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
| Definitions | 4 |
|---|---|
| Public names | 4 |
| Members | 2 |
| Version | 26.7.0 |
| Revision | daab053ee433 |