lib/sql/src/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const branch = @import("branch.zig");
3 const catalog_mod = @import("catalog.zig");
4 const file = @import("file.zig");
5 const index_mod = @import("index.zig");
6 const relation_mod = @import("relation.zig");
7 const row = @import("row.zig");
8 const session_mod = @import("session/root.zig");
9 const trace = @import("trace.zig");
10 const version = @import("version.zig");
11 const wal = @import("wal.zig");
12
13 const Allocator = std.mem.Allocator;
14
15 const PlanError = error{
16 PlanChanged,
17 };
18
19 pub const Error = catalog_mod.Error || version.Error || session_mod.DatabaseError || PlanError;
20
21 pub const PlanKey = struct {
22 relation: version.Hash,
23 schema: version.Hash,
24 stats: version.Hash,
25 parameters: version.Hash,
26
27 pub fn fromRoot(root: *const version.RelationRoot, parameters: version.Hash) PlanKey {
28 return .{
29 .relation = root.hash,
30 .schema = root.schema,
31 .stats = root.stats.hash,
32 .parameters = parameters,
33 };
34 }
35
36 pub fn fromRelationKey(relation_key: version.RelationKey, parameters: version.Hash) PlanKey {
37 return .{
38 .relation = relation_key.hash,
39 .schema = relation_key.schema,
40 .stats = relation_key.stats,
41 .parameters = parameters,
42 };
43 }
44
45 pub fn same(left: PlanKey, right: PlanKey) bool {
46 return version.same(left.relation, right.relation) and
47 version.same(left.schema, right.schema) and
48 version.same(left.stats, right.stats) and
49 version.same(left.parameters, right.parameters);
50 }
51 };
52
53 pub const Validation = enum {
54 content,
55 shape,
56 };
57
58 pub fn emptyParameterShape() version.Hash {
59 return version.emptyHash("sql.parameters.none");
60 }
61
62 fn shapeKey(name: []const u8, handle: *const catalog_mod.RelationHandle, stats: ?*const catalog_mod.RelationStats, parameters: version.Hash) PlanKey {
63 const schema_hash = version.schemaHash(handle.definitions, handle.index_definitions);
64 const stats_hash = statsShapeHash(stats);
65 var relation = ShapeBuilder.init("sql.plan.relation.shape");
66 relation.bytes(name);
67 relation.hash(schema_hash);
68 relation.hash(stats_hash);
69 relation.writeU32(handle.relation.table.rows.root_page);
70 relation.writeU64(handle.specs.len);
71 for (handle.specs) |spec| {
72 relation.writeU32(spec.root_page);
73 relation.writeU64(spec.fields.len);
74 for (spec.fields) |field| relation.writeU64(field);
75 relation.writeU64(spec.columns.len);
76 for (spec.columns) |column| relation.writeU64(@backingInt(column.collation));
77 }
78 return .{
79 .relation = relation.finish(),
80 .schema = schema_hash,
81 .stats = stats_hash,
82 .parameters = parameters,
83 };
84 }
85
86 fn statsShapeHash(stats: ?*const catalog_mod.RelationStats) version.Hash {
87 const relation_stats = stats orelse return version.emptyHash("sql.stats.none");
88 var builder = ShapeBuilder.init("sql.plan.stats.shape");
89 builder.writeU32(relation_stats.table_root_page);
90 builder.summary(relation_stats.table);
91 builder.writeU64(relation_stats.indexes.len);
92 for (relation_stats.indexes) |index_stats| {
93 builder.bytes(index_stats.name);
94 builder.writeU32(index_stats.root_page);
95 builder.summary(index_stats.summary);
96 builder.distribution(index_stats.distribution);
97 }
98 return builder.finish();
99 }
100
101 const ShapeBuilder = struct {
102 hasher: std.crypto.hash.sha2.Sha256,
103
104 fn init(tag: []const u8) ShapeBuilder {
105 var builder = ShapeBuilder{ .hasher = std.crypto.hash.sha2.Sha256.init(.{}) };
106 builder.bytes(tag);
107 return builder;
108 }
109
110 fn finish(self: *ShapeBuilder) version.Hash {
111 var digest: version.Hash = undefined;
112 self.hasher.final(&digest);
113 return digest;
114 }
115
116 fn bytes(self: *ShapeBuilder, value: []const u8) void {
117 self.writeU64(value.len);
118 self.hasher.update(value);
119 }
120
121 fn hash(self: *ShapeBuilder, value: version.Hash) void {
122 self.hasher.update(&value);
123 }
124
125 fn writeU32(self: *ShapeBuilder, value: u32) void {
126 var encoded: [4]u8 = undefined;
127 std.mem.writeInt(u32, &encoded, value, .big);
128 self.hasher.update(&encoded);
129 }
130
131 fn writeU64(self: *ShapeBuilder, value: anytype) void {
132 var encoded: [8]u8 = undefined;
133 std.mem.writeInt(u64, &encoded, @intCast(value), .big);
134 self.hasher.update(&encoded);
135 }
136
137 fn summary(self: *ShapeBuilder, value: @import("tree.zig").Summary) void {
138 self.writeU64(value.branch_pages);
139 self.writeU64(value.leaf_pages);
140 self.writeU64(value.overflow_pages);
141 self.writeU64(value.entries);
142 self.writeU64(value.inline_records);
143 self.writeU64(value.overflow_records);
144 self.writeU64(value.max_depth);
145 self.writeU64(value.key_bytes);
146 self.writeU64(value.record_bytes);
147 self.writeU64(value.value_bytes);
148 }
149
150 fn distribution(self: *ShapeBuilder, value: catalog_mod.IndexDistribution) void {
151 self.writeU64(value.distinct_values);
152 self.writeU64(value.max_equal);
153 self.samples(value.samples);
154 self.bytes(value.sample_keys);
155 self.writeU64(value.prefixes.len);
156 for (value.prefixes) |prefix| {
157 self.writeU64(prefix.field_count);
158 self.writeU64(prefix.distinct_values);
159 self.writeU64(prefix.max_equal);
160 self.samples(prefix.samples);
161 self.bytes(prefix.sample_keys);
162 }
163 }
164
165 fn samples(self: *ShapeBuilder, values: []const catalog_mod.IndexSample) void {
166 self.writeU64(values.len);
167 for (values) |sample| {
168 self.bytes(sample.key);
169 self.writeU64(sample.less_than);
170 self.writeU64(sample.equal_count);
171 self.writeU64(sample.less_distinct);
172 }
173 }
174 };
175
176 pub const RelationExecution = struct {
177 reader: relation_mod.Reader,
178
179 pub fn get(self: RelationExecution, allocator: Allocator, rowid: i64) Error!?[]u8 {
180 return try self.reader.get(allocator, rowid);
181 }
182
183 pub fn scan(
184 self: RelationExecution,
185 target: *relation_mod.Scan,
186 allocator: Allocator,
187 start: ?i64,
188 end: ?i64,
189 ) Error!void {
190 try self.reader.scan(target, allocator, start, end);
191 }
192
193 pub fn lookup(
194 self: RelationExecution,
195 target: *index_mod.Scan,
196 allocator: Allocator,
197 index_slot: usize,
198 prefix: []const row.Value,
199 ) Error!void {
200 try self.reader.lookup(target, allocator, index_slot, prefix);
201 }
202
203 pub fn indexScan(
204 self: RelationExecution,
205 target: *index_mod.Scan,
206 allocator: Allocator,
207 index_slot: usize,
208 start: ?[]const row.Value,
209 end: ?[]const row.Value,
210 ) Error!void {
211 try self.reader.indexScan(target, allocator, index_slot, start, end);
212 }
213
214 pub fn indexRange(
215 self: RelationExecution,
216 target: *index_mod.Scan,
217 allocator: Allocator,
218 index_slot: usize,
219 start: ?index_mod.Bound,
220 end: ?index_mod.Bound,
221 ) Error!void {
222 try self.reader.indexRange(target, allocator, index_slot, start, end);
223 }
224 };
225
226 pub const RelationRead = struct {
227 lease: file.ReadLease,
228 execution: RelationExecution,
229
230 pub fn deinit(self: *RelationRead) void {
231 self.lease.deinit();
232 self.* = undefined;
233 }
234
235 pub fn borrow(self: *const RelationRead) RelationExecution {
236 return self.execution;
237 }
238
239 pub fn get(self: *const RelationRead, allocator: Allocator, rowid: i64) Error!?[]u8 {
240 return try self.execution.get(allocator, rowid);
241 }
242
243 pub fn scan(
244 self: *const RelationRead,
245 target: *relation_mod.Scan,
246 allocator: Allocator,
247 start: ?i64,
248 end: ?i64,
249 ) Error!void {
250 try self.execution.scan(target, allocator, start, end);
251 }
252
253 pub fn lookup(
254 self: *const RelationRead,
255 target: *index_mod.Scan,
256 allocator: Allocator,
257 index_slot: usize,
258 prefix: []const row.Value,
259 ) Error!void {
260 try self.execution.lookup(target, allocator, index_slot, prefix);
261 }
262
263 pub fn indexScan(
264 self: *const RelationRead,
265 target: *index_mod.Scan,
266 allocator: Allocator,
267 index_slot: usize,
268 start: ?[]const row.Value,
269 end: ?[]const row.Value,
270 ) Error!void {
271 try self.execution.indexScan(target, allocator, index_slot, start, end);
272 }
273
274 pub fn indexRange(
275 self: *const RelationRead,
276 target: *index_mod.Scan,
277 allocator: Allocator,
278 index_slot: usize,
279 start: ?index_mod.Bound,
280 end: ?index_mod.Bound,
281 ) Error!void {
282 try self.execution.indexRange(target, allocator, index_slot, start, end);
283 }
284 };
285
286 pub const PreparedRelation = struct {
287 allocator: Allocator,
288 catalog: catalog_mod.Catalog,
289 name: []u8,
290 schema: catalog_mod.Schema,
291 handle: catalog_mod.RelationHandle,
292 stats: ?catalog_mod.RelationStats,
293 root: ?version.RelationRoot,
294 key: PlanKey,
295 validation: Validation = .content,
296
297 pub fn deinit(self: *PreparedRelation) void {
298 if (self.root) |*root| root.deinit();
299 if (self.stats) |*stats| stats.deinit();
300 self.handle.deinit();
301 self.allocator.free(self.name);
302 self.* = undefined;
303 }
304
305 pub fn validate(self: *PreparedRelation) Error!void {
306 const phase = trace.scope("plan.validate_relation");
307 defer phase.end();
308
309 const current = try self.currentCacheKey();
310 if (!PlanKey.same(current, self.key)) return error.PlanChanged;
311 }
312
313 pub fn execute(self: *PreparedRelation) Error!RelationRead {
314 const phase = trace.scope("plan.execute_relation");
315 defer phase.end();
316
317 try self.validate();
318 var lease = try self.handle.relation.space.database.beginRead();
319 errdefer lease.deinit();
320 return .{
321 .lease = lease,
322 .execution = .{
323 .reader = try self.handle.relation.reader(lease.snapshot()),
324 },
325 };
326 }
327
328 pub fn relationStats(self: *const PreparedRelation) ?*const catalog_mod.RelationStats {
329 if (self.stats) |*stats| return stats;
330 return null;
331 }
332
333 pub fn cacheKey(self: *const PreparedRelation) PlanKey {
334 return self.key;
335 }
336
337 pub fn currentCacheKey(self: *PreparedRelation) Error!PlanKey {
338 var state = try self.catalog.readRelation(self.allocator, self.name);
339 defer state.deinit();
340 return switch (self.validation) {
341 .content => content_key: {
342 const relation_key = try version.relationKey(
343 self.name,
344 &state.handle,
345 state.relationStats(),
346 );
347 break :content_key PlanKey.fromRelationKey(relation_key, self.key.parameters);
348 },
349 .shape => shapeKey(
350 self.name,
351 &state.handle,
352 state.relationStats(),
353 self.key.parameters,
354 ),
355 };
356 }
357
358 pub fn matchesRoot(self: *const PreparedRelation, root: *const version.RelationRoot) bool {
359 if (self.root == null) return false;
360 return PlanKey.same(self.key, PlanKey.fromRoot(root, self.key.parameters));
361 }
362
363 pub fn writeSession(self: *PreparedRelation) Error!session_mod.RelationSession {
364 const phase = trace.scope("plan.write_session");
365 defer phase.end();
366
367 var relation_session = try session_mod.RelationSession.open(self.allocator, &self.catalog, self.name);
368 errdefer relation_session.deinit();
369 if (!self.matchesRoot(&relation_session.root)) return error.PlanChanged;
370 return relation_session;
371 }
372
373 pub fn refresh(self: *PreparedRelation) Error!void {
374 var state = try self.catalog.readRelation(self.allocator, self.name);
375 errdefer state.deinit();
376 var root: ?version.RelationRoot = null;
377 errdefer if (root) |*relation_root| relation_root.deinit();
378 const key = switch (self.validation) {
379 .content => key: {
380 root = try version.relationRootMaintained(
381 self.allocator,
382 self.name,
383 state.schema,
384 &state.handle,
385 state.relationStats(),
386 );
387 break :key PlanKey.fromRoot(&root.?, self.key.parameters);
388 },
389 .shape => shapeKey(
390 self.name,
391 &state.handle,
392 state.relationStats(),
393 self.key.parameters,
394 ),
395 };
396
397 if (self.root) |*old_root| old_root.deinit();
398 if (self.stats) |*old_stats| old_stats.deinit();
399 self.handle.deinit();
400 self.schema = state.schema;
401 self.handle = state.handle;
402 self.stats = state.stats;
403 self.root = root;
404 self.key = key;
405 }
406
407 pub fn get(self: *PreparedRelation, allocator: Allocator, rowid: i64) Error!?[]u8 {
408 var execution = try self.execute();
409 defer execution.deinit();
410 return try execution.get(allocator, rowid);
411 }
412
413 pub fn scan(
414 self: *PreparedRelation,
415 target: *relation_mod.Scan,
416 allocator: Allocator,
417 start: ?i64,
418 end: ?i64,
419 ) Error!void {
420 var execution = try self.execute();
421 defer execution.deinit();
422 try execution.scan(target, allocator, start, end);
423 }
424
425 pub fn lookup(
426 self: *PreparedRelation,
427 target: *index_mod.Scan,
428 allocator: Allocator,
429 index_slot: usize,
430 prefix: []const row.Value,
431 ) Error!void {
432 var execution = try self.execute();
433 defer execution.deinit();
434 try execution.lookup(target, allocator, index_slot, prefix);
435 }
436
437 pub fn indexScan(
438 self: *PreparedRelation,
439 target: *index_mod.Scan,
440 allocator: Allocator,
441 index_slot: usize,
442 start: ?[]const row.Value,
443 end: ?[]const row.Value,
444 ) Error!void {
445 var execution = try self.execute();
446 defer execution.deinit();
447 try execution.indexScan(target, allocator, index_slot, start, end);
448 }
449
450 pub fn indexRange(
451 self: *PreparedRelation,
452 target: *index_mod.Scan,
453 allocator: Allocator,
454 index_slot: usize,
455 start: ?index_mod.Bound,
456 end: ?index_mod.Bound,
457 ) Error!void {
458 var execution = try self.execute();
459 defer execution.deinit();
460 try execution.indexRange(target, allocator, index_slot, start, end);
461 }
462 };
463
464 pub fn prepareRelation(catalog: *const catalog_mod.Catalog, allocator: Allocator, name: []const u8, parameters: version.Hash) Error!PreparedRelation {
465 return try prepareRelationWithValidation(catalog, allocator, name, parameters, .content);
466 }
467
468 pub fn prepareRelationShape(catalog: *const catalog_mod.Catalog, allocator: Allocator, name: []const u8, parameters: version.Hash) Error!PreparedRelation {
469 return try prepareRelationWithValidation(catalog, allocator, name, parameters, .shape);
470 }
471
472 fn prepareRelationWithValidation(catalog: *const catalog_mod.Catalog, allocator: Allocator, name: []const u8, parameters: version.Hash, validation: Validation) Error!PreparedRelation {
473 const phase = trace.scope("plan.prepare_relation");
474 defer phase.end();
475
476 const owned_name = try allocator.dupe(u8, name);
477 errdefer allocator.free(owned_name);
478 var state = try catalog.readRelation(allocator, name);
479 errdefer state.deinit();
480 var root: ?version.RelationRoot = null;
481 errdefer if (root) |*relation_root| relation_root.deinit();
482 const key = switch (validation) {
483 .content => key: {
484 root = try version.relationRootMaintained(
485 allocator,
486 name,
487 state.schema,
488 &state.handle,
489 state.relationStats(),
490 );
491 break :key PlanKey.fromRoot(&root.?, parameters);
492 },
493 .shape => shapeKey(name, &state.handle, state.relationStats(), parameters),
494 };
495 return .{
496 .allocator = allocator,
497 .catalog = catalog.*,
498 .name = owned_name,
499 .schema = state.schema,
500 .handle = state.handle,
501 .stats = state.stats,
502 .root = root,
503 .key = key,
504 .validation = validation,
505 };
506 }
507
508 fn preparedPut(prepared: *PreparedRelation, allocator: Allocator, rowid: i64, values: []const row.Value, options: file.CommitOptions) Error!session_mod.RelationFlush {
509 var relation_session = try prepared.writeSession();
510 var relation_session_live = true;
511 errdefer if (relation_session_live) relation_session.deinit();
512 try relation_session.put(rowid, values);
513 const flush = try flushPreparedRelation(prepared, allocator, &relation_session, &relation_session_live, options);
514 try prepared.refresh();
515 return flush;
516 }
517
518 fn preparedPutEncoded(prepared: *PreparedRelation, allocator: Allocator, rowid: i64, bytes: []const u8, options: file.CommitOptions) Error!session_mod.RelationFlush {
519 var relation_session = try prepared.writeSession();
520 var relation_session_live = true;
521 errdefer if (relation_session_live) relation_session.deinit();
522 try relation_session.putEncoded(rowid, bytes);
523 const flush = try flushPreparedRelation(prepared, allocator, &relation_session, &relation_session_live, options);
524 try prepared.refresh();
525 return flush;
526 }
527
528 fn preparedDelete(prepared: *PreparedRelation, allocator: Allocator, rowid: i64, options: file.CommitOptions) Error!session_mod.RelationFlush {
529 var relation_session = try prepared.writeSession();
530 var relation_session_live = true;
531 errdefer if (relation_session_live) relation_session.deinit();
532 try relation_session.delete(rowid);
533 const flush = try flushPreparedRelation(prepared, allocator, &relation_session, &relation_session_live, options);
534 try prepared.refresh();
535 return flush;
536 }
537
538 fn flushPreparedRelation(prepared: *PreparedRelation, allocator: Allocator, relation_session: *session_mod.RelationSession, relation_session_live: *bool, options: file.CommitOptions) Error!session_mod.RelationFlush {
539 var root = try version.databaseRootMaintained(
540 allocator,
541 &prepared.catalog,
542 version.ConflictRoot.empty().hash,
543 );
544 var root_live = true;
545 errdefer if (root_live) root.deinit();
546 const commit = version.Commit.init(root.hash, &.{});
547 const root_hash = root.hash;
548 var database_session = session_mod.DatabaseSession.initWithRoot(allocator, branch.checkout(.{
549 .name = "prepared",
550 .target = commit.hash,
551 }, root_hash), &root);
552 root_live = false;
553 defer database_session.deinit();
554
555 const limits = try relation_session.stagingLimits();
556 var workspace = try session_mod.DatabaseWrite.Workspace.allocate(
557 allocator,
558 limits,
559 );
560 defer workspace.deallocate(allocator);
561 var write = try database_session.beginWrite(
562 &workspace,
563 allocator,
564 limits,
565 options,
566 );
567 defer write.deinit();
568 try write.stageRelation(relation_session);
569 relation_session_live.* = false;
570 var database_flush = try write.flush();
571 defer database_flush.deinit();
572 return database_flush.onlyRelation();
573 }
574
575 test "prepared relation ignores unrelated catalog schema version bumps" {
576 var tmp = std.testing.tmpDir(.{});
577 defer tmp.cleanup();
578
579 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
580 .paths = .{ .database = "plan.db", .wal = "plan.wal" },
581 .header = testingHeader(),
582 });
583 defer database.deinit();
584 try database.reserve(.{ .wal_frames = 420 });
585
586 var catalog = try catalog_mod.Catalog.open(&database, .{});
587 const indexes = [_]catalog_mod.IndexDefinition{.{
588 .name = "items_value",
589 .fields = &.{0},
590 }};
591 const created = try catalog.createRelation(std.testing.allocator, .{
592 .name = "items",
593 .indexes = &indexes,
594 }, .{ .durability = .buffered });
595 try std.testing.expectEqual(@as(u64, 1), created.schema.version);
596
597 {
598 var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
599 defer prepared.deinit();
600 try std.testing.expectEqual(created.schema, prepared.schema);
601
602 _ = try preparedPut(&prepared, std.testing.allocator, 1, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });
603 try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator));
604
605 var lookup: index_mod.Scan = undefined;
606 try prepared.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 7 }});
607 defer lookup.deinit();
608 try std.testing.expectEqual(@as(i64, 1), (try lookup.next()).?.rowid);
609 try std.testing.expect(try lookup.next() == null);
610
611 const second = try catalog.createRelation(std.testing.allocator, .{
612 .name = "users",
613 }, .{ .durability = .buffered });
614 try std.testing.expectEqual(@as(u64, 2), second.schema.version);
615
616 try prepared.validate();
617 var execution = try prepared.execute();
618 defer execution.deinit();
619 const bytes = (try execution.get(std.testing.allocator, 1)).?;
620 defer std.testing.allocator.free(bytes);
621 const view = try row.View.init(bytes);
622 try std.testing.expectEqual(@as(i64, 7), (try view.column(0)).integer);
623 try std.testing.expectEqualStrings("seven", (try view.column(1)).text);
624 try std.testing.expect(PlanKey.same(prepared.cacheKey(), try prepared.currentCacheKey()));
625
626 _ = try preparedPut(&prepared, std.testing.allocator, 2, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered });
627 const second_bytes = (try prepared.get(std.testing.allocator, 2)).?;
628 defer std.testing.allocator.free(second_bytes);
629 const second_view = try row.View.init(second_bytes);
630 try std.testing.expectEqual(@as(i64, 9), (try second_view.column(0)).integer);
631 }
632
633 {
634 var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
635 defer prepared.deinit();
636 try std.testing.expectEqual(@as(u64, 2), prepared.schema.version);
637
638 var execution = try prepared.execute();
639 defer execution.deinit();
640 const bytes = (try execution.get(std.testing.allocator, 1)).?;
641 defer std.testing.allocator.free(bytes);
642 const view = try row.View.init(bytes);
643 try std.testing.expectEqual(@as(i64, 7), (try view.column(0)).integer);
644 try std.testing.expectEqualStrings("seven", (try view.column(1)).text);
645 }
646 }
647
648 test "prepared relation detects external relation root changes" {
649 var tmp = std.testing.tmpDir(.{});
650 defer tmp.cleanup();
651
652 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
653 .paths = .{ .database = "plan-root.db", .wal = "plan-root.wal" },
654 .header = testingHeader(),
655 });
656 defer database.deinit();
657 try database.reserve(.{ .wal_frames = 420 });
658
659 var catalog = try catalog_mod.Catalog.open(&database, .{});
660 const indexes = [_]catalog_mod.IndexDefinition{.{
661 .name = "items_value",
662 .fields = &.{0},
663 }};
664 _ = try catalog.createRelation(std.testing.allocator, .{
665 .name = "items",
666 .indexes = &indexes,
667 }, .{ .durability = .buffered });
668
669 var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
670 defer prepared.deinit();
671 const before = prepared.cacheKey();
672
673 var handle = try catalog.openRelation(std.testing.allocator, "items");
674 defer handle.deinit();
675 _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });
676
677 const current = try prepared.currentCacheKey();
678 try std.testing.expect(!PlanKey.same(before, current));
679 try std.testing.expect(!version.same(before.relation, current.relation));
680 try std.testing.expect(version.same(before.schema, current.schema));
681 try std.testing.expect(version.same(before.stats, current.stats));
682 try std.testing.expectError(error.PlanChanged, prepared.execute());
683 try std.testing.expectError(error.PlanChanged, prepared.get(std.testing.allocator, 1));
684 var stale_scan: index_mod.Scan = undefined;
685 try std.testing.expectError(
686 error.PlanChanged,
687 prepared.lookup(&stale_scan, std.testing.allocator, 0, &.{.{ .integer = 7 }}),
688 );
689 try std.testing.expectError(
690 error.PlanChanged,
691 prepared.indexScan(&stale_scan, std.testing.allocator, 0, null, null),
692 );
693 try std.testing.expectError(error.PlanChanged, prepared.writeSession());
694
695 var fresh = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
696 defer fresh.deinit();
697 const bytes = (try fresh.get(std.testing.allocator, 1)).?;
698 defer std.testing.allocator.free(bytes);
699 const view = try row.View.init(bytes);
700 try std.testing.expectEqual(@as(i64, 7), (try view.column(0)).integer);
701 }
702
703 test "prepared relation data changes preserve captured schema" {
704 var tmp = std.testing.tmpDir(.{});
705 defer tmp.cleanup();
706
707 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
708 .paths = .{ .database = "plan.db", .wal = "plan.wal" },
709 .header = recoveredHeader(),
710 });
711 defer database.deinit();
712 try database.reserve(.{ .wal_frames = 220 });
713
714 var catalog = try catalog_mod.Catalog.open(&database, .{});
715 const created = try catalog.createRelation(std.testing.allocator, .{
716 .name = "items",
717 }, .{ .durability = .buffered });
718 var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
719 defer prepared.deinit();
720
721 _ = try preparedPut(&prepared, std.testing.allocator, 1, &.{.{ .integer = 11 }}, .{ .durability = .buffered });
722 var encoded_buffer: [32]u8 = undefined;
723 const encoded = try row.encode(&encoded_buffer, &.{.{ .integer = 22 }});
724 _ = try preparedPutEncoded(&prepared, std.testing.allocator, 2, encoded, .{ .durability = .buffered });
725 _ = try preparedDelete(&prepared, std.testing.allocator, 1, .{ .durability = .buffered });
726
727 try prepared.validate();
728 try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator));
729 try std.testing.expectEqual(created.schema, prepared.schema);
730
731 const bytes = (try prepared.get(std.testing.allocator, 2)).?;
732 defer std.testing.allocator.free(bytes);
733 const view = try row.View.init(bytes);
734 try std.testing.expectEqual(@as(i64, 22), (try view.column(0)).integer);
735 }
736
737 test "prepared relation cache key changes after data root changes" {
738 var tmp = std.testing.tmpDir(.{});
739 defer tmp.cleanup();
740
741 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
742 .paths = .{ .database = "plan-key.db", .wal = "plan-key.wal" },
743 .header = recoveredHeader(),
744 });
745 defer database.deinit();
746 try database.reserve(.{ .wal_frames = 220 });
747
748 var catalog = try catalog_mod.Catalog.open(&database, .{});
749 _ = try catalog.createRelation(std.testing.allocator, .{
750 .name = "items",
751 }, .{ .durability = .buffered });
752 var prepared = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
753 defer prepared.deinit();
754
755 const before = prepared.cacheKey();
756 try std.testing.expect(PlanKey.same(before, try prepared.currentCacheKey()));
757
758 _ = try preparedPut(&prepared, std.testing.allocator, 1, &.{.{ .integer = 11 }}, .{ .durability = .buffered });
759 const after = prepared.cacheKey();
760 try std.testing.expect(PlanKey.same(after, try prepared.currentCacheKey()));
761 try std.testing.expect(!PlanKey.same(before, after));
762 try std.testing.expect(version.same(before.schema, after.schema));
763 try std.testing.expect(version.same(before.stats, after.stats));
764 try std.testing.expect(!version.same(before.relation, after.relation));
765 }
766
767 test "prepared relation cache key includes parameter shape" {
768 var tmp = std.testing.tmpDir(.{});
769 defer tmp.cleanup();
770
771 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
772 .paths = .{ .database = "plan-parameters.db", .wal = "plan-parameters.wal" },
773 .header = recoveredHeader(),
774 });
775 defer database.deinit();
776 try database.reserve(.{ .wal_frames = 220 });
777
778 var catalog = try catalog_mod.Catalog.open(&database, .{});
779 _ = try catalog.createRelation(std.testing.allocator, .{
780 .name = "items",
781 }, .{ .durability = .buffered });
782
783 const first_shape = version.emptyHash("sql.parameters.first");
784 const second_shape = version.emptyHash("sql.parameters.second");
785 var first = try prepareRelation(&catalog, std.testing.allocator, "items", first_shape);
786 defer first.deinit();
787 var second = try prepareRelation(&catalog, std.testing.allocator, "items", second_shape);
788 defer second.deinit();
789
790 const first_key = first.cacheKey();
791 const second_key = second.cacheKey();
792 try std.testing.expect(version.same(first_key.relation, second_key.relation));
793 try std.testing.expect(version.same(first_key.schema, second_key.schema));
794 try std.testing.expect(version.same(first_key.stats, second_key.stats));
795 try std.testing.expect(!version.same(first_key.parameters, second_key.parameters));
796 try std.testing.expect(!PlanKey.same(first_key, second_key));
797 }
798
799 test "prepared relation invalidates by stats root without schema change" {
800 var tmp = std.testing.tmpDir(.{});
801 defer tmp.cleanup();
802
803 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
804 .paths = .{ .database = "plan.db", .wal = "plan.wal" },
805 .header = testingHeader(),
806 });
807 defer database.deinit();
808 try database.reserve(.{ .wal_frames = 360 });
809
810 var catalog = try catalog_mod.Catalog.open(&database, .{});
811 const indexes = [_]catalog_mod.IndexDefinition{.{
812 .name = "items_value",
813 .fields = &.{0},
814 }};
815 const created = try catalog.createRelation(std.testing.allocator, .{
816 .name = "items",
817 .indexes = &indexes,
818 }, .{ .durability = .buffered });
819
820 var stale = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
821 defer stale.deinit();
822 try std.testing.expect(stale.relationStats() == null);
823
824 _ = try preparedPut(&stale, std.testing.allocator, 1, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });
825 _ = try preparedPut(&stale, std.testing.allocator, 2, &.{ .{ .integer = 9 }, .{ .text = "nine" } }, .{ .durability = .buffered });
826 const analyzed = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered });
827 try std.testing.expectEqual(created.schema, analyzed.schema);
828 try std.testing.expectError(error.PlanChanged, stale.validate());
829 try std.testing.expect(stale.relationStats() == null);
830
831 var fresh = try prepareRelation(&catalog, std.testing.allocator, "items", emptyParameterShape());
832 defer fresh.deinit();
833 const stats = fresh.relationStats().?;
834 try std.testing.expectEqual(@as(usize, 2), stats.table.entries);
835 try std.testing.expectEqual(@as(usize, 1), stats.indexes.len);
836 try std.testing.expectEqual(@as(usize, 2), stats.index("items_value").?.summary.entries);
837 try std.testing.expect(!PlanKey.same(stale.cacheKey(), fresh.cacheKey()));
838 try std.testing.expect(!version.same(stale.cacheKey().stats, fresh.cacheKey().stats));
839 }
840
841 fn testingHeader() wal.Header {
842 return .{
843 .sequence = 1201,
844 .salt = .{ .first = 0x1212_eeee, .second = 0x3434_ffff },
845 };
846 }
847
848 fn recoveredHeader() wal.Header {
849 return .{
850 .sequence = 1202,
851 .salt = .{ .first = 0x5656_dddd, .second = 0x7878_cccc },
852 };
853 }