lib/sql/src/catalog.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const simd = @import("simd");
3 const file = @import("file.zig");
4 const index_mod = @import("index.zig");
5 const key = @import("key.zig");
6 const page = @import("page.zig");
7 const relation_mod = @import("relation.zig");
8 const row = @import("row.zig");
9 const space_mod = @import("space.zig");
10 const table = @import("table.zig");
11 const trace = @import("trace.zig");
12 const tree = @import("tree.zig");
13 const wal = @import("wal.zig");
14
15 const Bytes = simd.ScalableTag(u8);
16
17 const Allocator = std.mem.Allocator;
18
19 const CatalogError = error{
20 CatalogCorrupt,
21 ObjectExists,
22 RelationNotFound,
23 SchemaVersionOverflow,
24 TooManyColumns,
25 UnsupportedCatalogFormat,
26 };
27
28 pub const Error = relation_mod.Error || table.Error || CatalogError;
29
30 pub const default_meta_page: u32 = 1;
31 pub const default_root_page: u32 = 2;
32 pub const format_version: u32 = 1;
33
34 const schema_kind: i64 = 0;
35 const relation_kind: i64 = 1;
36 const index_kind: i64 = 2;
37 const stats_kind: i64 = 3;
38 const schema_rowid: i64 = 0;
39 const first_object_rowid: i64 = 1;
40 const schema_row_columns: usize = 6;
41 const catalog_row_columns: usize = 7;
42 const schema_blob_bytes = 12;
43 const summary_blob_bytes = 80;
44 const distribution_header_bytes = 6;
45 const distribution_prefix_header_bytes = 20;
46 const distribution_sample_header_bytes = 26;
47 const field_bytes = 4;
48 const column_name_len_bytes = 2;
49 const default_len_bytes = 2;
50 const schema_name = "schema";
51 const distribution_format: u32 = 3;
52
53 pub const max_columns: usize = 64;
54 pub const max_column_name_bytes: usize = page.size;
55 pub const max_column_default_bytes: usize = page.size;
56 pub const max_index_distribution_samples: usize = 32;
57
58 pub const Options = struct {
59 meta_page: u32 = default_meta_page,
60 root_page: u32 = default_root_page,
61 };
62
63 pub const Schema = struct {
64 format: u32 = format_version,
65 version: u64 = 0,
66 };
67
68 pub const Commit = struct {
69 storage: file.Commit,
70 schema: Schema,
71 };
72
73 pub const IndexDefinition = struct {
74 name: []const u8,
75 fields: []const usize,
76 columns: []const row.Column = &.{},
77 };
78
79 pub const ColumnDefinition = struct {
80 name: []const u8,
81 column: row.Column = .{},
82 default: row.Value = .nil,
83 };
84
85 pub const RelationDefinition = struct {
86 name: []const u8,
87 columns: []const ColumnDefinition = &.{},
88 indexes: []const IndexDefinition = &.{},
89 };
90
91 fn RelationHandleType(comptime Relation: type) type {
92 return struct {
93 allocator: Allocator,
94 relation: Relation,
95 specs: []relation_mod.IndexSpec,
96 index_definitions: []IndexDefinition,
97 fields: []usize,
98 columns: []row.Column,
99 definitions: []ColumnDefinition,
100 index_names: []u8,
101 names: []u8,
102 defaults: []u8,
103
104 pub fn deinit(self: *@This()) void {
105 self.allocator.free(self.defaults);
106 self.allocator.free(self.names);
107 self.allocator.free(self.index_names);
108 self.allocator.free(self.definitions);
109 self.allocator.free(self.columns);
110 self.allocator.free(self.fields);
111 self.allocator.free(self.index_definitions);
112 self.allocator.free(self.specs);
113 self.* = undefined;
114 }
115 };
116 }
117
118 pub const RelationHandle = RelationHandleType(relation_mod.Relation);
119 pub const ReadRelationHandle = RelationHandleType(relation_mod.Reader);
120
121 pub const IndexStats = struct {
122 name: []u8,
123 root_page: u32,
124 summary: tree.Summary,
125 distribution: IndexDistribution = .{},
126 };
127
128 pub const IndexDistribution = struct {
129 distinct_values: usize = 0,
130 max_equal: usize = 0,
131 samples: []IndexSample = &.{},
132 sample_keys: []u8 = &.{},
133 prefixes: []IndexPrefixDistribution = &.{},
134 };
135
136 pub const IndexPrefixDistribution = struct {
137 field_count: usize = 0,
138 distinct_values: usize = 0,
139 max_equal: usize = 0,
140 samples: []IndexSample = &.{},
141 sample_keys: []u8 = &.{},
142 };
143
144 pub const IndexSample = struct {
145 key: []const u8,
146 less_than: usize,
147 equal_count: usize,
148 less_distinct: usize,
149 };
150
151 pub const RelationStats = struct {
152 allocator: Allocator,
153 table_root_page: u32,
154 table: tree.Summary,
155 indexes: []IndexStats,
156
157 pub fn deinit(self: *RelationStats) void {
158 for (self.indexes) |*index_stats| {
159 self.allocator.free(index_stats.name);
160 freeIndexDistribution(self.allocator, index_stats.distribution);
161 }
162 self.allocator.free(self.indexes);
163 self.* = undefined;
164 }
165
166 pub fn index(self: *const RelationStats, name: []const u8) ?*const IndexStats {
167 for (self.indexes) |*index_stats| {
168 if (std.mem.eql(u8, index_stats.name, name)) return index_stats;
169 }
170 return null;
171 }
172 };
173
174 pub const PreparedRelationStats = struct {
175 allocator: Allocator,
176 stats: RelationStats,
177 distribution_blobs: [][]u8,
178
179 pub fn deinit(self: *PreparedRelationStats) void {
180 for (self.distribution_blobs) |bytes| {
181 if (bytes.len != 0) self.allocator.free(bytes);
182 }
183 self.allocator.free(self.distribution_blobs);
184 self.stats.deinit();
185 self.* = undefined;
186 }
187 };
188
189 const PreparedIndexStats = struct {
190 stats: IndexStats,
191 distribution_blob: []u8,
192 };
193
194 pub const MaterializedRelation = struct {
195 relation: relation_mod.Relation,
196 table_root: space_mod.RootSpec,
197 index_roots: [relation_mod.max_indexes]space_mod.RootSpec = undefined,
198 index_count: usize = 0,
199 table_stats_rowid: ?i64 = null,
200 index_stats_rowids: [relation_mod.max_indexes]i64 = undefined,
201 };
202
203 pub const Materialization = struct {
204 allocator: Allocator,
205 catalog: *const Catalog,
206 roots: [space_mod.max_roots]space_mod.RootSpec,
207 root_count: usize,
208 schema: table.Table,
209 write: tree.Write,
210 current_schema: Schema,
211 next_rowid: i64,
212 schema_dirty: bool = false,
213 dropped_relations: std.ArrayList([]u8) = .empty,
214 created_objects: std.ArrayList([]u8) = .empty,
215
216 pub fn deinit(self: *Materialization) void {
217 for (self.created_objects.items) |name| self.allocator.free(name);
218 self.created_objects.deinit(self.allocator);
219 for (self.dropped_relations.items) |name| self.allocator.free(name);
220 self.dropped_relations.deinit(self.allocator);
221 self.write.deinit();
222 self.* = undefined;
223 }
224
225 pub fn treeWrite(self: *Materialization) *tree.Write {
226 return &self.write;
227 }
228
229 pub fn dropRelation(self: *Materialization, name: []const u8) Error!void {
230 if (self.relationDropped(name)) return error.RelationNotFound;
231 const owned_name = try self.allocator.dupe(u8, name);
232 errdefer self.allocator.free(owned_name);
233 try self.dropped_relations.ensureUnusedCapacity(self.allocator, 1);
234
235 var found = false;
236 var state = CatalogState{};
237 var scan: table.Scan = undefined;
238 try self.catalog.schema.scan(&scan, self.allocator, null, null);
239 defer scan.deinit();
240 while (try scan.next()) |entry| {
241 var scratch = CatalogScratch{};
242 switch (try state.record(entry, &scratch)) {
243 .schema => {},
244 .object => |object| {
245 if (std.mem.eql(u8, object.table_name, name)) {
246 if (object.kind == .relation) found = true;
247 try self.schema.deleteIn(&self.write, entry.rowid);
248 self.removeRoot(object.root_page);
249 }
250 },
251 .stats => |stats| {
252 if (std.mem.eql(u8, stats.table_name, name)) {
253 try self.schema.deleteIn(&self.write, entry.rowid);
254 }
255 },
256 }
257 }
258 _ = try state.schemaOrDefault();
259 if (!found) return error.RelationNotFound;
260 self.dropped_relations.appendAssumeCapacity(owned_name);
261 self.schema_dirty = true;
262 }
263
264 pub fn clearRelationStats(self: *Materialization, name: []const u8) Error!void {
265 var found = false;
266 var state = CatalogState{};
267 var scan: table.Scan = undefined;
268 try self.catalog.schema.scan(&scan, self.allocator, null, null);
269 defer scan.deinit();
270 while (try scan.next()) |entry| {
271 var scratch = CatalogScratch{};
272 switch (try state.record(entry, &scratch)) {
273 .schema => {},
274 .object => |object| {
275 if (object.kind == .relation and
276 std.mem.eql(u8, object.name, name))
277 {
278 found = true;
279 }
280 },
281 .stats => |stats| {
282 if (std.mem.eql(u8, stats.table_name, name)) {
283 try self.schema.deleteIn(&self.write, entry.rowid);
284 }
285 },
286 }
287 }
288 _ = try state.schemaOrDefault();
289 if (!found) return error.RelationNotFound;
290 }
291
292 pub fn createRelation(
293 self: *Materialization,
294 definition: RelationDefinition,
295 prepared_stats: ?*const PreparedRelationStats,
296 ) Error!MaterializedRelation {
297 try self.prepareRelationCreation(definition, prepared_stats);
298 var materialized = try self.allocateMaterializedRelation(definition);
299 try self.writeRelationCatalog(definition, &materialized, prepared_stats);
300 materialized.relation = try self.openMaterializedRelation(
301 definition,
302 &materialized,
303 );
304 self.schema_dirty = true;
305 return materialized;
306 }
307
308 fn prepareRelationCreation(
309 self: *Materialization,
310 definition: RelationDefinition,
311 prepared_stats: ?*const PreparedRelationStats,
312 ) Error!void {
313 try validateDefinition(definition);
314 if (self.root_count + 1 + definition.indexes.len > self.roots.len) {
315 return error.TooManyRoots;
316 }
317 const stats_rows = if (prepared_stats == null)
318 @as(usize, 0)
319 else
320 1 + definition.indexes.len;
321 const catalog_rows = 1 + definition.indexes.len + stats_rows;
322 _ = std.math.add(
323 i64,
324 self.next_rowid,
325 @intCast(catalog_rows),
326 ) catch return error.CatalogCorrupt;
327 try self.ensureObjectNameAvailable(definition.name);
328 for (definition.indexes) |index_definition| {
329 try self.ensureObjectNameAvailable(index_definition.name);
330 }
331 if (prepared_stats) |prepared| try validatePreparedStats(definition, prepared);
332
333 try self.retainCreatedObject(definition.name);
334 for (definition.indexes) |index_definition| {
335 try self.retainCreatedObject(index_definition.name);
336 }
337 }
338
339 fn allocateMaterializedRelation(
340 self: *Materialization,
341 definition: RelationDefinition,
342 ) Error!MaterializedRelation {
343 const table_root = try allocateCatalogRoot(&self.write, &self.roots, &self.root_count);
344 var index_roots: [relation_mod.max_indexes]space_mod.RootSpec = undefined;
345 for (definition.indexes, 0..) |_, index_offset| {
346 index_roots[index_offset] = try allocateCatalogRoot(
347 &self.write,
348 &self.roots,
349 &self.root_count,
350 );
351 }
352 return .{
353 .relation = undefined,
354 .table_root = table_root,
355 .index_roots = index_roots,
356 .index_count = definition.indexes.len,
357 };
358 }
359
360 fn writeRelationCatalog(
361 self: *Materialization,
362 definition: RelationDefinition,
363 materialized: *MaterializedRelation,
364 prepared_stats: ?*const PreparedRelationStats,
365 ) Error!void {
366 var definitions_buffer: [page.size]u8 = undefined;
367 const definitions = try encodeDefinitions(&definitions_buffer, definition.columns);
368 try putCatalogRow(
369 &self.schema,
370 &self.write,
371 self.takeRowid(),
372 relation_kind,
373 definition.name,
374 definition.name,
375 materialized.table_root.root_page,
376 materialized.table_root.identity_page,
377 definitions,
378 "",
379 );
380 try self.writeIndexCatalog(definition, materialized);
381 if (prepared_stats) |prepared| {
382 try self.writePreparedStatsCatalog(definition, materialized, prepared);
383 }
384 }
385
386 fn writeIndexCatalog(
387 self: *Materialization,
388 definition: RelationDefinition,
389 materialized: *const MaterializedRelation,
390 ) Error!void {
391 for (
392 definition.indexes,
393 materialized.index_roots[0..materialized.index_count],
394 ) |index_definition, index_root| {
395 var fields_buffer: [relation_mod.max_index_fields * field_bytes]u8 = undefined;
396 var collations_buffer: [relation_mod.max_index_fields]u8 = undefined;
397 const fields = try encodeFields(&fields_buffer, index_definition.fields);
398 const collations = try encodeCollations(
399 &collations_buffer,
400 index_definition.fields.len,
401 index_definition.columns,
402 );
403 try putCatalogRow(
404 &self.schema,
405 &self.write,
406 self.takeRowid(),
407 index_kind,
408 index_definition.name,
409 definition.name,
410 index_root.root_page,
411 index_root.identity_page,
412 fields,
413 collations,
414 );
415 }
416 }
417
418 fn writePreparedStatsCatalog(
419 self: *Materialization,
420 definition: RelationDefinition,
421 materialized: *MaterializedRelation,
422 prepared: *const PreparedRelationStats,
423 ) Error!void {
424 var summary_buffer: [summary_blob_bytes]u8 = undefined;
425 materialized.table_stats_rowid = self.takeRowid();
426 try putCatalogRow(
427 &self.schema,
428 &self.write,
429 materialized.table_stats_rowid.?,
430 stats_kind,
431 definition.name,
432 definition.name,
433 materialized.table_root.root_page,
434 0,
435 try encodeSummary(&summary_buffer, prepared.stats.table),
436 "",
437 );
438 for (
439 definition.indexes,
440 materialized.index_roots[0..materialized.index_count],
441 prepared.stats.indexes,
442 prepared.distribution_blobs,
443 0..,
444 ) |index_definition, index_root, index_stats, distribution_blob, index_offset| {
445 materialized.index_stats_rowids[index_offset] = self.takeRowid();
446 try putCatalogRow(
447 &self.schema,
448 &self.write,
449 materialized.index_stats_rowids[index_offset],
450 stats_kind,
451 index_definition.name,
452 definition.name,
453 index_root.root_page,
454 0,
455 try encodeSummary(&summary_buffer, index_stats.summary),
456 distribution_blob,
457 );
458 }
459 }
460
461 fn openMaterializedRelation(
462 self: *Materialization,
463 definition: RelationDefinition,
464 materialized: *const MaterializedRelation,
465 ) Error!relation_mod.Relation {
466 const relation_space = try space_mod.Space.open(self.catalog.database, .{
467 .meta_page = self.catalog.meta_page,
468 .roots = self.roots[0..self.root_count],
469 .reserved_page_max = self.write.reserved_page_max,
470 });
471 var specs: [relation_mod.max_indexes]relation_mod.IndexSpec = undefined;
472 for (
473 definition.indexes,
474 materialized.index_roots[0..materialized.index_count],
475 specs[0..definition.indexes.len],
476 ) |index_definition, index_root, *spec| {
477 spec.* = .{
478 .root_page = index_root.root_page,
479 .fields = index_definition.fields,
480 .columns = index_definition.columns,
481 };
482 }
483 return try relation_mod.Relation.open(&relation_space, .{
484 .table_root = materialized.table_root.root_page,
485 .indexes = specs[0..definition.indexes.len],
486 });
487 }
488
489 pub fn refreshRelationStats(
490 self: *Materialization,
491 definition: RelationDefinition,
492 materialized: *const MaterializedRelation,
493 prepared: *const PreparedRelationStats,
494 ) Error!void {
495 try validatePreparedStats(definition, prepared);
496 if (materialized.index_count != definition.indexes.len) return error.CatalogCorrupt;
497 const table_rowid = materialized.table_stats_rowid orelse return error.CatalogCorrupt;
498 const table_summary = try materialized.relation.table.summarizeIn(&self.write);
499 if (!sameLogicalSummary(table_summary, prepared.stats.table)) return error.CatalogCorrupt;
500
501 var summary_buffer: [summary_blob_bytes]u8 = undefined;
502 try putCatalogRow(
503 &self.schema,
504 &self.write,
505 table_rowid,
506 stats_kind,
507 definition.name,
508 definition.name,
509 materialized.table_root.root_page,
510 0,
511 try encodeSummary(&summary_buffer, table_summary),
512 "",
513 );
514
515 for (
516 definition.indexes,
517 materialized.index_roots[0..materialized.index_count],
518 prepared.stats.indexes,
519 prepared.distribution_blobs,
520 0..,
521 ) |index_definition, index_root, index_stats, distribution_blob, index_offset| {
522 var relation_index = try materialized.relation.space.index(
523 index_root.root_page,
524 index_definition.columns,
525 );
526 const index_summary = try relation_index.summarizeIn(&self.write);
527 if (!sameLogicalSummary(index_summary, index_stats.summary)) {
528 return error.CatalogCorrupt;
529 }
530 try putCatalogRow(
531 &self.schema,
532 &self.write,
533 materialized.index_stats_rowids[index_offset],
534 stats_kind,
535 index_definition.name,
536 definition.name,
537 index_root.root_page,
538 0,
539 try encodeSummary(&summary_buffer, index_summary),
540 distribution_blob,
541 );
542 }
543 }
544
545 pub fn commit(self: *Materialization, options: file.CommitOptions) Error!Commit {
546 const next_schema = if (self.schema_dirty)
547 try bumpSchemaVersion(self.current_schema)
548 else
549 self.current_schema;
550 if (self.schema_dirty) try putSchemaRow(&self.schema, &self.write, next_schema);
551 return .{
552 .storage = try self.write.commit(options),
553 .schema = next_schema,
554 };
555 }
556
557 fn relationDropped(self: *const Materialization, name: []const u8) bool {
558 for (self.dropped_relations.items) |dropped| {
559 if (std.mem.eql(u8, dropped, name)) return true;
560 }
561 return false;
562 }
563
564 fn objectCreated(self: *const Materialization, name: []const u8) bool {
565 for (self.created_objects.items) |created| {
566 if (std.mem.eql(u8, created, name)) return true;
567 }
568 return false;
569 }
570
571 fn ensureObjectNameAvailable(self: *const Materialization, name: []const u8) Error!void {
572 if (self.objectCreated(name)) return error.ObjectExists;
573 var state = CatalogState{};
574 var scan: table.Scan = undefined;
575 try self.catalog.schema.scan(&scan, self.allocator, null, null);
576 defer scan.deinit();
577 while (try scan.next()) |entry| {
578 var scratch = CatalogScratch{};
579 switch (try state.record(entry, &scratch)) {
580 .schema, .stats => {},
581 .object => |object| {
582 if (std.mem.eql(u8, object.name, name) and
583 !self.relationDropped(object.table_name))
584 {
585 return error.ObjectExists;
586 }
587 },
588 }
589 }
590 _ = try state.schemaOrDefault();
591 }
592
593 fn retainCreatedObject(self: *Materialization, name: []const u8) Error!void {
594 const owned_name = try self.allocator.dupe(u8, name);
595 errdefer self.allocator.free(owned_name);
596 try self.created_objects.append(self.allocator, owned_name);
597 }
598
599 fn removeRoot(self: *Materialization, root_page: u32) void {
600 var index: usize = 0;
601 while (index < self.root_count) : (index += 1) {
602 if (self.roots[index].root_page != root_page) continue;
603 self.root_count -= 1;
604 self.roots[index] = self.roots[self.root_count];
605 return;
606 }
607 unreachable;
608 }
609
610 fn takeRowid(self: *Materialization) i64 {
611 const rowid = self.next_rowid;
612 self.next_rowid += 1;
613 return rowid;
614 }
615 };
616
617 pub const RelationNames = struct {
618 allocator: Allocator,
619 names: [][]u8,
620
621 pub fn deinit(self: *RelationNames) void {
622 for (self.names) |name| self.allocator.free(name);
623 self.allocator.free(self.names);
624 self.* = undefined;
625 }
626 };
627
628 pub const Reader = struct {
629 snapshot: file.Snapshot,
630 meta_page: u32,
631 root_page: u32,
632 schema: table.Reader,
633
634 pub fn open(snapshot: file.Snapshot, options: Options) Error!Reader {
635 try validateOptions(options);
636 return .{
637 .snapshot = snapshot,
638 .meta_page = options.meta_page,
639 .root_page = options.root_page,
640 .schema = try table.Reader.open(snapshot, .{
641 .tree = catalogTreeOptions(options),
642 }),
643 };
644 }
645
646 pub fn schemaState(self: *const Reader, allocator: Allocator) Error!Schema {
647 return try readSchemaState(&self.schema, allocator);
648 }
649
650 pub fn openRelation(
651 self: *const Reader,
652 allocator: Allocator,
653 name: []const u8,
654 ) Error!ReadRelationHandle {
655 return try openRelationFrom(
656 ReadRelationHandle,
657 space_mod.Reader,
658 relation_mod.Reader,
659 self.snapshot,
660 &self.schema,
661 self.meta_page,
662 self.root_page,
663 allocator,
664 name,
665 );
666 }
667
668 pub fn relationNames(self: *const Reader, allocator: Allocator) Error!RelationNames {
669 return try readRelationNames(&self.schema, allocator);
670 }
671
672 pub fn relationStats(
673 self: *const Reader,
674 allocator: Allocator,
675 name: []const u8,
676 ) Error!?RelationStats {
677 return try readRelationStats(&self.schema, allocator, name);
678 }
679
680 /// Returns what `schemaState`, `openRelation` and `relationStats` return
681 /// for `name`, read in one catalog pass.
682 pub fn readRelation(
683 self: *const Reader,
684 allocator: Allocator,
685 name: []const u8,
686 ) Error!ReadRelationState {
687 return try readRelationFrom(
688 ReadRelationHandle,
689 space_mod.Reader,
690 relation_mod.Reader,
691 self.snapshot,
692 &self.schema,
693 self.meta_page,
694 self.root_page,
695 allocator,
696 name,
697 );
698 }
699 };
700
701 pub const Catalog = struct {
702 database: *file.Database,
703 meta_page: u32,
704 root_page: u32,
705 schema: table.Table,
706
707 pub fn open(database: *file.Database, options: Options) Error!Catalog {
708 try validateOptions(options);
709 return .{
710 .database = database,
711 .meta_page = options.meta_page,
712 .root_page = options.root_page,
713 .schema = try table.Table.open(database, .{
714 .tree = catalogTreeOptions(options),
715 }),
716 };
717 }
718
719 pub fn schemaState(self: *const Catalog, allocator: Allocator) Error!Schema {
720 return try readSchemaState(&self.schema, allocator);
721 }
722
723 pub fn beginMaterialization(self: *const Catalog, allocator: Allocator) Error!Materialization {
724 var roots: [space_mod.max_roots]space_mod.RootSpec = undefined;
725 var root_count: usize = 0;
726 try addRoot(&roots, &root_count, .{ .root_page = self.root_page });
727
728 var state = CatalogState{};
729 var scan: table.Scan = undefined;
730 try self.schema.scan(&scan, allocator, null, null);
731 defer scan.deinit();
732 while (try scan.next()) |entry| {
733 var scratch = CatalogScratch{};
734 switch (try state.record(entry, &scratch)) {
735 .schema, .stats => {},
736 .object => |object| try addRoot(&roots, &root_count, .{
737 .root_page = object.root_page,
738 .identity_page = object.identity_page,
739 }),
740 }
741 }
742 const current_schema = try state.schemaOrDefault();
743 if (state.max_rowid == std.math.maxInt(i64)) return error.CatalogCorrupt;
744
745 const space = try space_mod.Space.open(self.database, .{
746 .meta_page = self.meta_page,
747 .roots = roots[0..root_count],
748 });
749 var write = try space.beginWrite();
750 errdefer write.deinit();
751 return .{
752 .allocator = allocator,
753 .catalog = self,
754 .roots = roots,
755 .root_count = root_count,
756 .schema = try space.rowidTable(self.root_page),
757 .write = write,
758 .current_schema = current_schema,
759 .next_rowid = @max(state.max_rowid + 1, first_object_rowid),
760 };
761 }
762
763 pub fn prepareRelationStats(
764 _: *const Catalog,
765 allocator: Allocator,
766 definition: RelationDefinition,
767 puts: []const relation_mod.Edit.Put,
768 table_summary: tree.Summary,
769 index_summaries: []const tree.Summary,
770 ) Error!PreparedRelationStats {
771 try validateDefinition(definition);
772 if (definition.indexes.len != index_summaries.len) return error.CatalogCorrupt;
773
774 var previous_rowid: ?i64 = null;
775 for (puts) |put| {
776 _ = try row.View.init(put.bytes);
777 if (previous_rowid) |previous| {
778 if (put.rowid <= previous) return error.CatalogCorrupt;
779 }
780 previous_rowid = put.rowid;
781 }
782
783 const indexes = try allocator.alloc(IndexStats, definition.indexes.len);
784 var index_count: usize = 0;
785 errdefer {
786 for (indexes[0..index_count]) |*index_stats| {
787 allocator.free(index_stats.name);
788 freeIndexDistribution(allocator, index_stats.distribution);
789 }
790 allocator.free(indexes);
791 }
792
793 const distribution_blobs = try allocator.alloc([]u8, definition.indexes.len);
794 var blob_count: usize = 0;
795 errdefer {
796 for (distribution_blobs[0..blob_count]) |bytes| {
797 if (bytes.len != 0) allocator.free(bytes);
798 }
799 allocator.free(distribution_blobs);
800 }
801
802 for (
803 definition.indexes,
804 index_summaries,
805 indexes,
806 distribution_blobs,
807 0..,
808 ) |index_definition, summary, *index_stats, *distribution_blob, index_offset| {
809 const prepared = try prepareIndexStats(
810 allocator,
811 index_definition,
812 puts,
813 summary,
814 index_offset,
815 );
816 index_stats.* = prepared.stats;
817 distribution_blob.* = prepared.distribution_blob;
818 index_count += 1;
819 blob_count += 1;
820 }
821
822 return .{
823 .allocator = allocator,
824 .stats = .{
825 .allocator = allocator,
826 .table_root_page = 0,
827 .table = table_summary,
828 .indexes = indexes,
829 },
830 .distribution_blobs = distribution_blobs,
831 };
832 }
833
834 pub fn createRelation(self: *const Catalog, allocator: Allocator, definition: RelationDefinition, options: file.CommitOptions) Error!Commit {
835 const phase = trace.scope("catalog.create_relation");
836 defer phase.end();
837
838 try validateDefinition(definition);
839
840 var roots: [space_mod.max_roots]space_mod.RootSpec = undefined;
841 var root_count: usize = 0;
842 try addRoot(&roots, &root_count, .{ .root_page = self.root_page });
843
844 var state = CatalogState{};
845 {
846 var scan: table.Scan = undefined;
847 try self.schema.scan(&scan, allocator, null, null);
848 defer scan.deinit();
849 while (try scan.next()) |entry| {
850 var scratch = CatalogScratch{};
851 switch (try state.record(entry, &scratch)) {
852 .schema => {},
853 .object => |object| {
854 if (std.mem.eql(u8, object.name, definition.name)) return error.ObjectExists;
855 for (definition.indexes) |index_definition| {
856 if (std.mem.eql(u8, object.name, index_definition.name)) return error.ObjectExists;
857 }
858 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
859 },
860 .stats => {},
861 }
862 }
863 }
864 const next_schema = try bumpSchemaVersion(try state.schemaOrDefault());
865
866 const space = try space_mod.Space.open(self.database, .{
867 .meta_page = self.meta_page,
868 .roots = roots[0..root_count],
869 });
870 var schema = try space.rowidTable(self.root_page);
871 var write = try space.beginWrite();
872 defer write.deinit();
873
874 const table_root = try allocateCatalogRoot(&write, &roots, &root_count);
875 var index_roots: [relation_mod.max_indexes]space_mod.RootSpec = undefined;
876 for (definition.indexes, 0..) |_, index_offset| {
877 index_roots[index_offset] = try allocateCatalogRoot(&write, &roots, &root_count);
878 }
879
880 try putSchemaRow(&schema, &write, next_schema);
881
882 var definitions_buffer: [page.size]u8 = undefined;
883 const definitions = try encodeDefinitions(&definitions_buffer, definition.columns);
884 var next_rowid = @max(state.max_rowid + 1, first_object_rowid);
885 try putCatalogRow(&schema, &write, next_rowid, relation_kind, definition.name, definition.name, table_root.root_page, table_root.identity_page, definitions, "");
886 next_rowid += 1;
887
888 for (definition.indexes, 0..) |index_definition, index_offset| {
889 var fields_buffer: [relation_mod.max_index_fields * field_bytes]u8 = undefined;
890 var collations_buffer: [relation_mod.max_index_fields]u8 = undefined;
891 const fields = try encodeFields(&fields_buffer, index_definition.fields);
892 const collations = try encodeCollations(&collations_buffer, index_definition.fields.len, index_definition.columns);
893 try putCatalogRow(
894 &schema,
895 &write,
896 next_rowid,
897 index_kind,
898 index_definition.name,
899 definition.name,
900 index_roots[index_offset].root_page,
901 index_roots[index_offset].identity_page,
902 fields,
903 collations,
904 );
905 next_rowid += 1;
906 }
907
908 return .{
909 .storage = try write.commit(options),
910 .schema = next_schema,
911 };
912 }
913
914 pub fn createIndex(self: *const Catalog, allocator: Allocator, table_name: []const u8, definition: IndexDefinition, options: file.CommitOptions) Error!Commit {
915 const phase = trace.scope("catalog.create_index");
916 defer phase.end();
917
918 if (definition.name.len == 0) return error.CatalogCorrupt;
919 if (definition.fields.len == 0 or definition.fields.len > relation_mod.max_index_fields) return error.TooManyIndexFields;
920 if (definition.columns.len > definition.fields.len) return error.CatalogCorrupt;
921
922 var roots: [space_mod.max_roots]space_mod.RootSpec = undefined;
923 var root_count: usize = 0;
924 try addRoot(&roots, &root_count, .{ .root_page = self.root_page });
925
926 var table_root: ?u32 = null;
927 var table_definitions: [max_columns]ColumnDefinition = undefined;
928 var table_names: [max_column_name_bytes]u8 = undefined;
929 var table_defaults: [max_column_default_bytes]u8 = undefined;
930 var table_definition_count: usize = 0;
931 var table_index_count: usize = 0;
932 var old_stats: std.ArrayList(i64) = .empty;
933 defer old_stats.deinit(allocator);
934 var state = CatalogState{};
935 var scan: table.Scan = undefined;
936 try self.schema.scan(&scan, allocator, null, null);
937 defer scan.deinit();
938 while (try scan.next()) |entry| {
939 var scratch = CatalogScratch{};
940 switch (try state.record(entry, &scratch)) {
941 .schema => {},
942 .object => |object| switch (object.kind) {
943 .relation => {
944 if (std.mem.eql(u8, object.name, definition.name)) return error.ObjectExists;
945 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
946 if (std.mem.eql(u8, object.name, table_name)) {
947 if (table_root != null) return error.CatalogCorrupt;
948 table_root = object.root_page;
949 table_definition_count = object.definitions.len;
950 _ = try copyDefinitions(&table_definitions, &table_names, &table_defaults, object.definitions);
951 }
952 },
953 .index => {
954 if (std.mem.eql(u8, object.name, definition.name)) return error.ObjectExists;
955 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
956 if (std.mem.eql(u8, object.table_name, table_name)) table_index_count += 1;
957 },
958 .schema => unreachable,
959 .stats => unreachable,
960 },
961 .stats => |stats| {
962 if (std.mem.eql(u8, stats.table_name, table_name)) try old_stats.append(allocator, entry.rowid);
963 },
964 }
965 }
966 const next_schema = try bumpSchemaVersion(try state.schemaOrDefault());
967 const root_page = table_root orelse return error.RelationNotFound;
968 if (table_index_count >= relation_mod.max_indexes) return error.TooManyIndexes;
969 for (definition.fields) |field| {
970 if (field >= table_definition_count) return error.ColumnOutOfBounds;
971 }
972
973 const space = try space_mod.Space.open(self.database, .{
974 .meta_page = self.meta_page,
975 .roots = roots[0..root_count],
976 });
977 var table_rows = try space.rowidTable(root_page);
978 var schema = try space.rowidTable(self.root_page);
979 var write = try space.beginWrite();
980 defer write.deinit();
981
982 const index_root = try allocateCatalogRoot(&write, &roots, &root_count);
983 const index_space = try space_mod.Space.open(self.database, .{
984 .meta_page = self.meta_page,
985 .roots = roots[0..root_count],
986 });
987 var relation_index = try index_space.index(index_root.root_page, definition.columns);
988 var table_scan: table.Scan = undefined;
989 try table_rows.scan(&table_scan, allocator, null, null);
990 defer table_scan.deinit();
991 while (try table_scan.next()) |entry| {
992 const view = try entry.view();
993 var projected: [relation_mod.max_index_fields]row.Value = undefined;
994 try relation_index.putIn(&write, entry.rowid, try view.project(definition.fields, &projected));
995 }
996
997 for (old_stats.items) |rowid| try schema.deleteIn(&write, rowid);
998 try putSchemaRow(&schema, &write, next_schema);
999
1000 var fields_buffer: [relation_mod.max_index_fields * field_bytes]u8 = undefined;
1001 var collations_buffer: [relation_mod.max_index_fields]u8 = undefined;
1002 const fields = try encodeFields(&fields_buffer, definition.fields);
1003 const collations = try encodeCollations(&collations_buffer, definition.fields.len, definition.columns);
1004 try putCatalogRow(
1005 &schema,
1006 &write,
1007 @max(state.max_rowid + 1, first_object_rowid),
1008 index_kind,
1009 definition.name,
1010 table_name,
1011 index_root.root_page,
1012 index_root.identity_page,
1013 fields,
1014 collations,
1015 );
1016
1017 return .{
1018 .storage = try write.commit(options),
1019 .schema = next_schema,
1020 };
1021 }
1022
1023 pub fn dropRelation(self: *const Catalog, allocator: Allocator, name: []const u8, options: file.CommitOptions) Error!Commit {
1024 const phase = trace.scope("catalog.drop_relation");
1025 defer phase.end();
1026
1027 var roots: [space_mod.max_roots]space_mod.RootSpec = undefined;
1028 var root_count: usize = 0;
1029 try addRoot(&roots, &root_count, .{ .root_page = self.root_page });
1030
1031 var rowids: std.ArrayList(i64) = .empty;
1032 defer rowids.deinit(allocator);
1033 var found_relation = false;
1034 var state = CatalogState{};
1035
1036 var scan: table.Scan = undefined;
1037 try self.schema.scan(&scan, allocator, null, null);
1038 defer scan.deinit();
1039 while (try scan.next()) |entry| {
1040 var scratch = CatalogScratch{};
1041 switch (try state.record(entry, &scratch)) {
1042 .schema => {},
1043 .object => |object| switch (object.kind) {
1044 .relation => {
1045 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
1046 if (std.mem.eql(u8, object.name, name)) {
1047 if (found_relation) return error.CatalogCorrupt;
1048 found_relation = true;
1049 try rowids.append(allocator, entry.rowid);
1050 }
1051 },
1052 .index => {
1053 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
1054 if (std.mem.eql(u8, object.table_name, name)) try rowids.append(allocator, entry.rowid);
1055 },
1056 .schema => unreachable,
1057 .stats => unreachable,
1058 },
1059 .stats => |stats| {
1060 if (std.mem.eql(u8, stats.table_name, name)) try rowids.append(allocator, entry.rowid);
1061 },
1062 }
1063 }
1064 const next_schema = try bumpSchemaVersion(try state.schemaOrDefault());
1065 if (!found_relation) return error.RelationNotFound;
1066
1067 const space = try space_mod.Space.open(self.database, .{
1068 .meta_page = self.meta_page,
1069 .roots = roots[0..root_count],
1070 });
1071 var schema = try space.rowidTable(self.root_page);
1072 var write = try space.beginWrite();
1073 defer write.deinit();
1074
1075 for (rowids.items) |rowid| try schema.deleteIn(&write, rowid);
1076 try putSchemaRow(&schema, &write, next_schema);
1077
1078 return .{
1079 .storage = try write.commit(options),
1080 .schema = next_schema,
1081 };
1082 }
1083
1084 pub fn openRelation(
1085 self: *const Catalog,
1086 allocator: Allocator,
1087 name: []const u8,
1088 ) Error!RelationHandle {
1089 return try openRelationFrom(
1090 RelationHandle,
1091 space_mod.Space,
1092 relation_mod.Relation,
1093 self.database,
1094 &self.schema,
1095 self.meta_page,
1096 self.root_page,
1097 allocator,
1098 name,
1099 );
1100 }
1101
1102 pub fn relationNames(self: *const Catalog, allocator: Allocator) Error!RelationNames {
1103 return try readRelationNames(&self.schema, allocator);
1104 }
1105
1106 pub fn analyzeRelation(self: *const Catalog, allocator: Allocator, name: []const u8, options: file.CommitOptions) Error!Commit {
1107 const phase = trace.scope("catalog.analyze_relation");
1108 defer phase.end();
1109
1110 var roots: [space_mod.max_roots]space_mod.RootSpec = undefined;
1111 var root_count: usize = 0;
1112 try addRoot(&roots, &root_count, .{ .root_page = self.root_page });
1113
1114 var table_root: ?u32 = null;
1115 var indexes: std.ArrayList(AnalyzeIndex) = .empty;
1116 defer indexes.deinit(allocator);
1117 defer freeAnalyzeIndexes(allocator, indexes.items);
1118 var old_stats: std.ArrayList(i64) = .empty;
1119 defer old_stats.deinit(allocator);
1120 var state = CatalogState{};
1121
1122 var scan: table.Scan = undefined;
1123 try self.schema.scan(&scan, allocator, null, null);
1124 defer scan.deinit();
1125 while (try scan.next()) |entry| {
1126 var scratch = CatalogScratch{};
1127 switch (try state.record(entry, &scratch)) {
1128 .schema => {},
1129 .object => |object| switch (object.kind) {
1130 .relation => {
1131 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
1132 if (std.mem.eql(u8, object.name, name)) table_root = object.root_page;
1133 },
1134 .index => {
1135 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
1136 if (std.mem.eql(u8, object.table_name, name)) {
1137 if (indexes.items.len >= relation_mod.max_indexes) return error.TooManyIndexes;
1138 const owned_name = try allocator.dupe(u8, object.name);
1139 var analyzed_index = AnalyzeIndex{
1140 .name = owned_name,
1141 .root_page = object.root_page,
1142 .field_count = object.fields.len,
1143 };
1144 @memcpy(analyzed_index.fields[0..object.fields.len], object.fields);
1145 @memcpy(analyzed_index.columns[0..object.columns.len], object.columns);
1146 indexes.append(allocator, analyzed_index) catch |err| {
1147 allocator.free(owned_name);
1148 return err;
1149 };
1150 }
1151 },
1152 .schema => unreachable,
1153 .stats => unreachable,
1154 },
1155 .stats => |stats| {
1156 if (std.mem.eql(u8, stats.table_name, name)) try old_stats.append(allocator, entry.rowid);
1157 },
1158 }
1159 }
1160 const current_schema = try state.schemaOrDefault();
1161 const root_page = table_root orelse return error.RelationNotFound;
1162
1163 const space = try space_mod.Space.open(self.database, .{
1164 .meta_page = self.meta_page,
1165 .roots = roots[0..root_count],
1166 });
1167 var schema = try space.rowidTable(self.root_page);
1168 var relation_table = try space.rowidTable(root_page);
1169 const table_summary = try relation_table.summarize();
1170 var index_summaries: [relation_mod.max_indexes]tree.Summary = undefined;
1171 for (indexes.items, 0..) |index_object, index_offset| {
1172 var relation_index = try space.index(index_object.root_page, index_object.indexColumns());
1173 index_summaries[index_offset] = try relation_index.summarize();
1174 indexes.items[index_offset].distribution_blob = try analyzeIndexDistribution(allocator, &relation_index);
1175 }
1176
1177 var write = try space.beginWrite();
1178 defer write.deinit();
1179 for (old_stats.items) |rowid| try schema.deleteIn(&write, rowid);
1180
1181 var summary_buffer: [summary_blob_bytes]u8 = undefined;
1182 var next_rowid = @max(state.max_rowid + 1, first_object_rowid);
1183 try putCatalogRow(
1184 &schema,
1185 &write,
1186 next_rowid,
1187 stats_kind,
1188 name,
1189 name,
1190 root_page,
1191 0,
1192 try encodeSummary(&summary_buffer, table_summary),
1193 "",
1194 );
1195 next_rowid += 1;
1196
1197 for (indexes.items, 0..) |index_object, index_offset| {
1198 try putCatalogRow(
1199 &schema,
1200 &write,
1201 next_rowid,
1202 stats_kind,
1203 index_object.name,
1204 name,
1205 index_object.root_page,
1206 0,
1207 try encodeSummary(&summary_buffer, index_summaries[index_offset]),
1208 index_object.distribution_blob,
1209 );
1210 next_rowid += 1;
1211 }
1212
1213 return .{
1214 .storage = try write.commit(options),
1215 .schema = current_schema,
1216 };
1217 }
1218
1219 pub fn clearRelationStats(self: *const Catalog, allocator: Allocator, name: []const u8, options: file.CommitOptions) Error!?Commit {
1220 const phase = trace.scope("catalog.clear_relation_stats");
1221 defer phase.end();
1222
1223 var roots: [space_mod.max_roots]space_mod.RootSpec = undefined;
1224 var root_count: usize = 0;
1225 try addRoot(&roots, &root_count, .{ .root_page = self.root_page });
1226
1227 var old_stats: std.ArrayList(i64) = .empty;
1228 defer old_stats.deinit(allocator);
1229 var found_relation = false;
1230 var state = CatalogState{};
1231
1232 var scan: table.Scan = undefined;
1233 try self.schema.scan(&scan, allocator, null, null);
1234 defer scan.deinit();
1235 while (try scan.next()) |entry| {
1236 var scratch = CatalogScratch{};
1237 switch (try state.record(entry, &scratch)) {
1238 .schema => {},
1239 .object => |object| switch (object.kind) {
1240 .relation => {
1241 try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page });
1242 if (std.mem.eql(u8, object.name, name)) {
1243 if (found_relation) return error.CatalogCorrupt;
1244 found_relation = true;
1245 }
1246 },
1247 .index => try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }),
1248 .schema => unreachable,
1249 .stats => unreachable,
1250 },
1251 .stats => |stats| {
1252 if (std.mem.eql(u8, stats.table_name, name)) try old_stats.append(allocator, entry.rowid);
1253 },
1254 }
1255 }
1256 const current_schema = try state.schemaOrDefault();
1257 if (!found_relation) return error.RelationNotFound;
1258 if (old_stats.items.len == 0) return null;
1259
1260 const space = try space_mod.Space.open(self.database, .{
1261 .meta_page = self.meta_page,
1262 .roots = roots[0..root_count],
1263 });
1264 var schema = try space.rowidTable(self.root_page);
1265 var write = try space.beginWrite();
1266 defer write.deinit();
1267
1268 for (old_stats.items) |rowid| try schema.deleteIn(&write, rowid);
1269
1270 return .{
1271 .storage = try write.commit(options),
1272 .schema = current_schema,
1273 };
1274 }
1275
1276 pub fn relationStats(self: *const Catalog, allocator: Allocator, name: []const u8) Error!?RelationStats {
1277 return try readRelationStats(&self.schema, allocator, name);
1278 }
1279
1280 /// Returns what `schemaState`, `openRelation` and `relationStats` return
1281 /// for `name`, read in one catalog pass over one snapshot.
1282 pub fn readRelation(
1283 self: *const Catalog,
1284 allocator: Allocator,
1285 name: []const u8,
1286 ) Error!RelationState {
1287 return try readRelationFrom(
1288 RelationHandle,
1289 space_mod.Space,
1290 relation_mod.Relation,
1291 self.database,
1292 &self.schema,
1293 self.meta_page,
1294 self.root_page,
1295 allocator,
1296 name,
1297 );
1298 }
1299 };
1300
1301 fn RelationStateType(comptime Handle: type) type {
1302 return struct {
1303 /// The schema state of the catalog pass that read the relation.
1304 schema: Schema,
1305 handle: Handle,
1306 stats: ?RelationStats,
1307
1308 pub fn relationStats(self: *const @This()) ?*const RelationStats {
1309 if (self.stats) |*stats| return stats;
1310 return null;
1311 }
1312
1313 pub fn deinit(self: *@This()) void {
1314 if (self.stats) |*stats| stats.deinit();
1315 self.handle.deinit();
1316 self.* = undefined;
1317 }
1318 };
1319 }
1320
1321 /// The schema state, handle and statistics of one relation.
1322 pub const RelationState = RelationStateType(RelationHandle);
1323 /// The schema state, read handle and statistics of one relation.
1324 pub const ReadRelationState = RelationStateType(ReadRelationHandle);
1325
1326 fn validateOptions(options: Options) Error!void {
1327 if (options.meta_page == 0) return error.InvalidPageId;
1328 if (options.root_page == 0) return error.InvalidPageId;
1329 if (options.meta_page == options.root_page) return error.InvalidPageId;
1330 }
1331
1332 fn catalogTreeOptions(options: Options) tree.Options {
1333 return .{
1334 .meta_page = options.meta_page,
1335 .root_page = options.root_page,
1336 .reserved_page_max = options.root_page,
1337 };
1338 }
1339
1340 fn readSchemaState(schema: anytype, allocator: Allocator) Error!Schema {
1341 const phase = trace.scope("catalog.schema_state");
1342 defer phase.end();
1343
1344 return try scanCatalog(schema, allocator, SchemaRows{});
1345 }
1346
1347 /// Reads every catalog row once and returns the schema state. Each relation
1348 /// object, index object and statistics row goes to `rows`, which copies what
1349 /// it keeps before the pass reads the next row.
1350 fn scanCatalog(schema: anytype, allocator: Allocator, rows: anytype) Error!Schema {
1351 var state = CatalogState{};
1352 var scan: table.Scan = undefined;
1353 try schema.scan(&scan, allocator, null, null);
1354 defer scan.deinit();
1355 while (try scan.next()) |entry| {
1356 var scratch = CatalogScratch{};
1357 switch (try state.record(entry, &scratch)) {
1358 .schema => {},
1359 .object => |object| try rows.visitObject(object),
1360 .stats => |stats| try rows.visitStats(stats),
1361 }
1362 }
1363 return try state.schemaOrDefault();
1364 }
1365
1366 /// Keeps no rows, for a pass that only reads the schema state.
1367 const SchemaRows = struct {
1368 fn visitObject(_: SchemaRows, _: CatalogObject) Error!void {}
1369 fn visitStats(_: SchemaRows, _: CatalogStats) Error!void {}
1370 };
1371
1372 const RelationMetadata = struct {
1373 allocator: Allocator,
1374 specs: []relation_mod.IndexSpec,
1375 index_definitions: []IndexDefinition,
1376 fields: []usize,
1377 columns: []row.Column,
1378 definitions: []ColumnDefinition,
1379 index_names: []u8,
1380 names: []u8,
1381 defaults: []u8,
1382
1383 fn deinit(self: *RelationMetadata) void {
1384 self.allocator.free(self.defaults);
1385 self.allocator.free(self.names);
1386 self.allocator.free(self.index_names);
1387 self.allocator.free(self.definitions);
1388 self.allocator.free(self.columns);
1389 self.allocator.free(self.fields);
1390 self.allocator.free(self.index_definitions);
1391 self.allocator.free(self.specs);
1392 self.* = undefined;
1393 }
1394 };
1395
1396 const RelationOpenPlan = struct {
1397 name: []const u8,
1398 roots: [space_mod.max_roots]space_mod.RootSpec = undefined,
1399 root_count: usize = 0,
1400 table_root: ?u32 = null,
1401 index_roots: [relation_mod.max_indexes]u32 = undefined,
1402 index_name_stack: [relation_mod.max_indexes][max_column_name_bytes]u8 = undefined,
1403 index_name_lengths: [relation_mod.max_indexes]usize = undefined,
1404 fields_stack: [relation_mod.max_indexes][relation_mod.max_index_fields]usize = undefined,
1405 columns_stack: [relation_mod.max_indexes][relation_mod.max_index_fields]row.Column = undefined,
1406 field_counts: [relation_mod.max_indexes]usize = undefined,
1407 definition_stack: [max_columns]ColumnDefinition = undefined,
1408 name_stack: [max_column_name_bytes]u8 = undefined,
1409 default_stack: [max_column_default_bytes]u8 = undefined,
1410 definition_count: usize = 0,
1411 name_count: usize = 0,
1412 default_count: usize = 0,
1413 index_count: usize = 0,
1414 total_index_name_bytes: usize = 0,
1415 total_fields: usize = 0,
1416
1417 fn init(root_page: u32, name: []const u8) Error!RelationOpenPlan {
1418 var plan = RelationOpenPlan{ .name = name };
1419 try addRoot(&plan.roots, &plan.root_count, .{ .root_page = root_page });
1420 return plan;
1421 }
1422
1423 fn visitObject(self: *RelationOpenPlan, object: CatalogObject) Error!void {
1424 switch (object.kind) {
1425 .relation => if (std.mem.eql(u8, object.name, self.name)) {
1426 self.table_root = object.root_page;
1427 self.definition_count = object.definitions.len;
1428 const copied = try copyDefinitions(
1429 &self.definition_stack,
1430 &self.name_stack,
1431 &self.default_stack,
1432 object.definitions,
1433 );
1434 self.name_count = copied.names;
1435 self.default_count = copied.defaults;
1436 try self.addObjectRoot(object);
1437 },
1438 .index => if (std.mem.eql(u8, object.table_name, self.name)) {
1439 try self.recordIndex(object);
1440 },
1441 .schema, .stats => unreachable,
1442 }
1443 }
1444
1445 fn visitStats(_: *RelationOpenPlan, _: CatalogStats) Error!void {}
1446
1447 fn recordIndex(self: *RelationOpenPlan, object: CatalogObject) Error!void {
1448 if (self.index_count >= relation_mod.max_indexes) return error.TooManyIndexes;
1449 const offset = self.index_count;
1450 if (object.name.len > self.index_name_stack[offset].len) return error.CatalogCorrupt;
1451 self.index_roots[offset] = object.root_page;
1452 @memcpy(self.index_name_stack[offset][0..object.name.len], object.name);
1453 self.index_name_lengths[offset] = object.name.len;
1454 self.field_counts[offset] = object.fields.len;
1455 @memcpy(self.fields_stack[offset][0..object.fields.len], object.fields);
1456 @memcpy(self.columns_stack[offset][0..object.columns.len], object.columns);
1457 self.total_index_name_bytes += object.name.len;
1458 self.total_fields += object.fields.len;
1459 try self.addObjectRoot(object);
1460 self.index_count += 1;
1461 }
1462
1463 fn addObjectRoot(self: *RelationOpenPlan, object: CatalogObject) Error!void {
1464 try addRoot(&self.roots, &self.root_count, .{
1465 .root_page = object.root_page,
1466 .identity_page = object.identity_page,
1467 });
1468 }
1469
1470 fn allocate(self: *const RelationOpenPlan, allocator: Allocator) Error!RelationMetadata {
1471 const specs = try allocator.alloc(relation_mod.IndexSpec, self.index_count);
1472 errdefer allocator.free(specs);
1473 const indexes = try allocator.alloc(IndexDefinition, self.index_count);
1474 errdefer allocator.free(indexes);
1475 const fields = try allocator.alloc(usize, self.total_fields);
1476 errdefer allocator.free(fields);
1477 const columns = try allocator.alloc(row.Column, self.total_fields);
1478 errdefer allocator.free(columns);
1479 const definitions = try allocator.alloc(ColumnDefinition, self.definition_count);
1480 errdefer allocator.free(definitions);
1481 const index_names = try allocator.alloc(u8, self.total_index_name_bytes);
1482 errdefer allocator.free(index_names);
1483 const names = try allocator.alloc(u8, self.name_count);
1484 errdefer allocator.free(names);
1485 const defaults = try allocator.alloc(u8, self.default_count);
1486 errdefer allocator.free(defaults);
1487 self.copyIndexes(specs, indexes, fields, columns, index_names);
1488 copyOpenedDefinitions(
1489 definitions,
1490 names,
1491 defaults,
1492 self.definition_stack[0..self.definition_count],
1493 );
1494 return .{
1495 .allocator = allocator,
1496 .specs = specs,
1497 .index_definitions = indexes,
1498 .fields = fields,
1499 .columns = columns,
1500 .definitions = definitions,
1501 .index_names = index_names,
1502 .names = names,
1503 .defaults = defaults,
1504 };
1505 }
1506
1507 fn copyIndexes(
1508 self: *const RelationOpenPlan,
1509 specs: []relation_mod.IndexSpec,
1510 definitions: []IndexDefinition,
1511 fields: []usize,
1512 columns: []row.Column,
1513 names: []u8,
1514 ) void {
1515 var field_offset: usize = 0;
1516 var name_offset: usize = 0;
1517 var offset: usize = 0;
1518 while (offset < self.index_count) : (offset += 1) {
1519 const count = self.field_counts[offset];
1520 @memcpy(fields[field_offset..][0..count], self.fields_stack[offset][0..count]);
1521 @memcpy(columns[field_offset..][0..count], self.columns_stack[offset][0..count]);
1522 const name = names[name_offset..][0..self.index_name_lengths[offset]];
1523 @memcpy(name, self.index_name_stack[offset][0..name.len]);
1524 specs[offset] = .{
1525 .root_page = self.index_roots[offset],
1526 .fields = fields[field_offset..][0..count],
1527 .columns = columns[field_offset..][0..count],
1528 };
1529 definitions[offset] = .{
1530 .name = name,
1531 .fields = fields[field_offset..][0..count],
1532 .columns = columns[field_offset..][0..count],
1533 };
1534 field_offset += count;
1535 name_offset += name.len;
1536 }
1537 }
1538 };
1539
1540 fn openRelationFrom(
1541 comptime Handle: type,
1542 comptime Space: type,
1543 comptime Relation: type,
1544 source: anytype,
1545 schema: anytype,
1546 meta_page: u32,
1547 catalog_root: u32,
1548 allocator: Allocator,
1549 name: []const u8,
1550 ) Error!Handle {
1551 const phase = trace.scope("catalog.open_relation");
1552 defer phase.end();
1553
1554 var plan = try RelationOpenPlan.init(catalog_root, name);
1555 _ = try scanCatalog(schema, allocator, &plan);
1556 return try openPlannedRelation(Handle, Space, Relation, source, meta_page, &plan, allocator);
1557 }
1558
1559 fn readRelationFrom(
1560 comptime Handle: type,
1561 comptime Space: type,
1562 comptime Relation: type,
1563 source: anytype,
1564 schema: anytype,
1565 meta_page: u32,
1566 catalog_root: u32,
1567 allocator: Allocator,
1568 name: []const u8,
1569 ) Error!RelationStateType(Handle) {
1570 const phase = trace.scope("catalog.read_relation");
1571 defer phase.end();
1572
1573 var plan = try RelationOpenPlan.init(catalog_root, name);
1574 var stats_rows = RelationStatsRows{ .allocator = allocator, .name = name };
1575 defer stats_rows.deinit();
1576 const schema_state = try scanCatalog(schema, allocator, RelationRows{
1577 .plan = &plan,
1578 .stats = &stats_rows,
1579 });
1580 var handle = try openPlannedRelation(
1581 Handle,
1582 Space,
1583 Relation,
1584 source,
1585 meta_page,
1586 &plan,
1587 allocator,
1588 );
1589 errdefer handle.deinit();
1590 return .{ .schema = schema_state, .handle = handle, .stats = try stats_rows.finish() };
1591 }
1592
1593 fn openPlannedRelation(
1594 comptime Handle: type,
1595 comptime Space: type,
1596 comptime Relation: type,
1597 source: anytype,
1598 meta_page: u32,
1599 plan: *const RelationOpenPlan,
1600 allocator: Allocator,
1601 ) Error!Handle {
1602 const root_page = plan.table_root orelse return error.RelationNotFound;
1603 var metadata = try plan.allocate(allocator);
1604 errdefer metadata.deinit();
1605 const space = try Space.open(source, .{
1606 .meta_page = meta_page,
1607 .roots = plan.roots[0..plan.root_count],
1608 });
1609 const opened = try Relation.open(&space, .{
1610 .table_root = root_page,
1611 .indexes = metadata.specs,
1612 });
1613 return .{
1614 .allocator = allocator,
1615 .relation = opened,
1616 .specs = metadata.specs,
1617 .index_definitions = metadata.index_definitions,
1618 .fields = metadata.fields,
1619 .columns = metadata.columns,
1620 .definitions = metadata.definitions,
1621 .index_names = metadata.index_names,
1622 .names = metadata.names,
1623 .defaults = metadata.defaults,
1624 };
1625 }
1626
1627 fn readRelationNames(schema: anytype, allocator: Allocator) Error!RelationNames {
1628 const phase = trace.scope("catalog.relation_names");
1629 defer phase.end();
1630
1631 var rows = RelationNameRows{ .allocator = allocator };
1632 errdefer rows.deinit();
1633 _ = try scanCatalog(schema, allocator, &rows);
1634 return .{
1635 .allocator = allocator,
1636 .names = try rows.names.toOwnedSlice(allocator),
1637 };
1638 }
1639
1640 /// Collects the name of every relation during a catalog pass.
1641 const RelationNameRows = struct {
1642 allocator: Allocator,
1643 names: std.ArrayList([]u8) = .empty,
1644
1645 fn deinit(self: *RelationNameRows) void {
1646 for (self.names.items) |name| self.allocator.free(name);
1647 self.names.deinit(self.allocator);
1648 self.* = undefined;
1649 }
1650
1651 fn visitObject(self: *RelationNameRows, object: CatalogObject) Error!void {
1652 switch (object.kind) {
1653 .relation => {
1654 const owned = try self.allocator.dupe(u8, object.name);
1655 self.names.append(self.allocator, owned) catch |err| {
1656 self.allocator.free(owned);
1657 return err;
1658 };
1659 },
1660 .index => {},
1661 .schema, .stats => unreachable,
1662 }
1663 }
1664
1665 fn visitStats(_: *RelationNameRows, _: CatalogStats) Error!void {}
1666 };
1667
1668 fn readRelationStats(
1669 schema: anytype,
1670 allocator: Allocator,
1671 name: []const u8,
1672 ) Error!?RelationStats {
1673 const phase = trace.scope("catalog.relation_stats");
1674 defer phase.end();
1675
1676 var rows = RelationStatsRows{ .allocator = allocator, .name = name };
1677 defer rows.deinit();
1678 _ = try scanCatalog(schema, allocator, &rows);
1679 return try rows.finish();
1680 }
1681
1682 /// Collects the statistics rows of one relation during a catalog pass.
1683 const RelationStatsRows = struct {
1684 allocator: Allocator,
1685 name: []const u8,
1686 table_root: ?u32 = null,
1687 table_stats_root: ?u32 = null,
1688 table_summary: ?tree.Summary = null,
1689 matching_stats_seen: bool = false,
1690 index_stats: std.ArrayList(IndexStats) = .empty,
1691
1692 fn deinit(self: *RelationStatsRows) void {
1693 freeIndexStats(self.allocator, self.index_stats.items);
1694 self.index_stats.deinit(self.allocator);
1695 self.* = undefined;
1696 }
1697
1698 fn visitObject(self: *RelationStatsRows, object: CatalogObject) Error!void {
1699 switch (object.kind) {
1700 .relation => if (std.mem.eql(u8, object.name, self.name)) {
1701 self.table_root = object.root_page;
1702 },
1703 .index => {},
1704 .schema, .stats => unreachable,
1705 }
1706 }
1707
1708 fn visitStats(self: *RelationStatsRows, stats: CatalogStats) Error!void {
1709 if (!std.mem.eql(u8, stats.table_name, self.name)) return;
1710 self.matching_stats_seen = true;
1711 if (std.mem.eql(u8, stats.name, self.name)) {
1712 if (self.table_summary != null) return error.CatalogCorrupt;
1713 if (stats.distribution_blob.len != 0) return error.CatalogCorrupt;
1714 self.table_summary = stats.summary;
1715 self.table_stats_root = stats.root_page;
1716 } else try appendIndexStats(self.allocator, &self.index_stats, stats);
1717 }
1718
1719 /// Returns the statistics of the relation once the pass is over. The
1720 /// result owns the index statistics the rows collected.
1721 fn finish(self: *RelationStatsRows) Error!?RelationStats {
1722 const root_page = self.table_root orelse return error.RelationNotFound;
1723 if (self.table_summary == null) {
1724 if (self.matching_stats_seen) return error.CatalogCorrupt;
1725 return null;
1726 }
1727 if (self.table_stats_root.? != root_page) return error.CatalogCorrupt;
1728 return .{
1729 .allocator = self.allocator,
1730 .table_root_page = root_page,
1731 .table = self.table_summary.?,
1732 .indexes = try self.index_stats.toOwnedSlice(self.allocator),
1733 };
1734 }
1735 };
1736
1737 /// Hands each catalog row to the open plan and the statistics of one
1738 /// relation, so one pass reads both.
1739 const RelationRows = struct {
1740 plan: *RelationOpenPlan,
1741 stats: *RelationStatsRows,
1742
1743 fn visitObject(self: RelationRows, object: CatalogObject) Error!void {
1744 try self.plan.visitObject(object);
1745 try self.stats.visitObject(object);
1746 }
1747
1748 fn visitStats(self: RelationRows, stats: CatalogStats) Error!void {
1749 try self.stats.visitStats(stats);
1750 }
1751 };
1752
1753 fn appendIndexStats(
1754 allocator: Allocator,
1755 index_stats: *std.ArrayList(IndexStats),
1756 stats: CatalogStats,
1757 ) Error!void {
1758 if (containsIndexStats(index_stats.items, stats.name)) return error.CatalogCorrupt;
1759 const owned_name = try allocator.dupe(u8, stats.name);
1760 errdefer allocator.free(owned_name);
1761 const distribution = if (stats.distribution_blob.len == 0)
1762 IndexDistribution{}
1763 else
1764 try decodeIndexDistribution(allocator, stats.distribution_blob);
1765 errdefer freeIndexDistribution(allocator, distribution);
1766 try index_stats.append(allocator, .{
1767 .name = owned_name,
1768 .root_page = stats.root_page,
1769 .summary = stats.summary,
1770 .distribution = distribution,
1771 });
1772 }
1773
1774 const CatalogKind = enum {
1775 schema,
1776 relation,
1777 index,
1778 stats,
1779 };
1780
1781 const CatalogEntry = union(enum) {
1782 schema: Schema,
1783 object: CatalogObject,
1784 stats: CatalogStats,
1785 };
1786
1787 const CatalogObject = struct {
1788 kind: CatalogKind,
1789 name: []const u8,
1790 table_name: []const u8,
1791 root_page: u32,
1792 identity_page: u32,
1793 fields: []const usize,
1794 columns: []const row.Column,
1795 definitions: []const ColumnDefinition,
1796 };
1797
1798 const CatalogStats = struct {
1799 name: []const u8,
1800 table_name: []const u8,
1801 root_page: u32,
1802 summary: tree.Summary,
1803 distribution_blob: []const u8,
1804 };
1805
1806 const AnalyzeIndex = struct {
1807 name: []u8,
1808 root_page: u32,
1809 fields: [relation_mod.max_index_fields]usize = undefined,
1810 columns: [relation_mod.max_index_fields]row.Column = undefined,
1811 field_count: usize = 0,
1812 distribution_blob: []u8 = &.{},
1813
1814 fn indexColumns(self: *const AnalyzeIndex) []const row.Column {
1815 return self.columns[0..self.field_count];
1816 }
1817 };
1818
1819 const CatalogScratch = struct {
1820 fields: [relation_mod.max_index_fields]usize = undefined,
1821 columns: [relation_mod.max_index_fields]row.Column = undefined,
1822 definitions: [max_columns]ColumnDefinition = undefined,
1823 names: [max_column_name_bytes]u8 = undefined,
1824 defaults: [max_column_default_bytes]u8 = undefined,
1825 };
1826
1827 const CatalogState = struct {
1828 schema: ?Schema = null,
1829 data_seen: bool = false,
1830 max_rowid: i64 = 0,
1831
1832 fn record(self: *CatalogState, entry: table.Entry, scratch: *CatalogScratch) Error!CatalogEntry {
1833 const parsed = try catalogEntry(entry.rowid, entry.bytes, scratch);
1834 switch (parsed) {
1835 .schema => |schema| {
1836 if (self.schema != null) return error.CatalogCorrupt;
1837 if (schema.format != format_version) return error.UnsupportedCatalogFormat;
1838 self.schema = schema;
1839 },
1840 .object => {
1841 self.data_seen = true;
1842 self.max_rowid = @max(self.max_rowid, entry.rowid);
1843 },
1844 .stats => {
1845 self.data_seen = true;
1846 self.max_rowid = @max(self.max_rowid, entry.rowid);
1847 },
1848 }
1849 return parsed;
1850 }
1851
1852 fn schemaOrDefault(self: CatalogState) Error!Schema {
1853 const schema = self.schema orelse {
1854 if (self.data_seen) return error.CatalogCorrupt;
1855 return .{};
1856 };
1857 return schema;
1858 }
1859 };
1860
1861 pub fn validateDefinition(definition: RelationDefinition) Error!void {
1862 if (definition.name.len == 0) return error.CatalogCorrupt;
1863 if (definition.columns.len > max_columns) return error.TooManyColumns;
1864 for (definition.columns, 0..) |column, column_offset| {
1865 if (column.name.len == 0) return error.CatalogCorrupt;
1866 if (column.name.len > std.math.maxInt(u16)) return error.CatalogCorrupt;
1867 if ((try row.encodedSize(&.{column.default})) > std.math.maxInt(u16)) return error.CatalogCorrupt;
1868 for (definition.columns[0..column_offset]) |previous_column| {
1869 if (std.ascii.eqlIgnoreCase(column.name, previous_column.name)) return error.CatalogCorrupt;
1870 }
1871 }
1872 if (definition.indexes.len > relation_mod.max_indexes) return error.TooManyIndexes;
1873 for (definition.indexes, 0..) |index_definition, index_offset| {
1874 if (index_definition.name.len == 0) return error.CatalogCorrupt;
1875 if (std.mem.eql(u8, index_definition.name, definition.name)) return error.ObjectExists;
1876 for (definition.indexes[0..index_offset]) |previous_index| {
1877 if (std.mem.eql(u8, index_definition.name, previous_index.name)) return error.ObjectExists;
1878 }
1879 if (index_definition.fields.len > relation_mod.max_index_fields) return error.TooManyIndexFields;
1880 if (index_definition.columns.len > index_definition.fields.len) return error.CatalogCorrupt;
1881 if (definition.columns.len > 0) {
1882 for (index_definition.fields) |field| {
1883 if (field >= definition.columns.len) return error.ColumnOutOfBounds;
1884 }
1885 }
1886 }
1887 }
1888
1889 fn validatePreparedStats(
1890 definition: RelationDefinition,
1891 prepared: *const PreparedRelationStats,
1892 ) Error!void {
1893 if (prepared.stats.indexes.len != definition.indexes.len) return error.CatalogCorrupt;
1894 if (prepared.distribution_blobs.len != definition.indexes.len) return error.CatalogCorrupt;
1895 for (definition.indexes, prepared.stats.indexes) |index_definition, index_stats| {
1896 if (!std.mem.eql(u8, index_definition.name, index_stats.name)) return error.CatalogCorrupt;
1897 }
1898 }
1899
1900 fn sameLogicalSummary(left: tree.Summary, right: tree.Summary) bool {
1901 return left.entries == right.entries and
1902 left.key_bytes == right.key_bytes and
1903 left.value_bytes == right.value_bytes;
1904 }
1905
1906 fn bumpSchemaVersion(schema: Schema) Error!Schema {
1907 if (schema.version == std.math.maxInt(u64)) return error.SchemaVersionOverflow;
1908 return .{
1909 .format = schema.format,
1910 .version = schema.version + 1,
1911 };
1912 }
1913
1914 fn putSchemaRow(schema: *table.Table, write: *tree.Write, catalog_schema: Schema) Error!void {
1915 var schema_buffer: [schema_blob_bytes]u8 = undefined;
1916 try schema.putIn(write, schema_rowid, &.{
1917 .{ .integer = schema_kind },
1918 .{ .text = schema_name },
1919 .{ .text = "" },
1920 .{ .integer = 0 },
1921 .{ .blob = encodeSchema(&schema_buffer, catalog_schema) },
1922 .{ .blob = "" },
1923 });
1924 }
1925
1926 fn putCatalogRow(schema: *table.Table, write: *tree.Write, rowid: i64, kind: i64, name: []const u8, table_name: []const u8, root_page: u32, identity_page: u32, fields: []const u8, collations: []const u8) Error!void {
1927 try schema.putIn(write, rowid, &.{
1928 .{ .integer = kind },
1929 .{ .text = name },
1930 .{ .text = table_name },
1931 .{ .integer = rootAsInteger(root_page) },
1932 .{ .blob = fields },
1933 .{ .blob = collations },
1934 .{ .integer = identityAsInteger(identity_page) },
1935 });
1936 }
1937
1938 fn catalogEntry(rowid: i64, bytes: []const u8, scratch: *CatalogScratch) Error!CatalogEntry {
1939 const view = try row.View.init(bytes);
1940 const kind = try catalogKind(try view.column(0));
1941 return switch (kind) {
1942 .schema => .{ .schema = try catalogSchema(rowid, view) },
1943 .relation, .index => .{ .object = try catalogObject(rowid, kind, view, scratch) },
1944 .stats => .{ .stats = try catalogStats(rowid, view) },
1945 };
1946 }
1947
1948 fn catalogSchema(rowid: i64, view: row.View) Error!Schema {
1949 var cursor = view.cursor();
1950 if (view.columnCount() != schema_row_columns) return error.UnsupportedCatalogFormat;
1951 if (rowid != schema_rowid) return error.CatalogCorrupt;
1952 if (!std.mem.eql(u8, try textValue(try cursor.column(1)), schema_name)) return error.CatalogCorrupt;
1953 if ((try textValue(try cursor.column(2))).len != 0) return error.CatalogCorrupt;
1954 if (try integerValue(try cursor.column(3)) != 0) return error.CatalogCorrupt;
1955 const schema_blob = try blobValue(try cursor.column(4));
1956 if ((try blobValue(try cursor.column(5))).len != 0) return error.CatalogCorrupt;
1957 return try decodeSchema(schema_blob);
1958 }
1959
1960 fn catalogObject(rowid: i64, kind: CatalogKind, view: row.View, scratch: *CatalogScratch) Error!CatalogObject {
1961 var cursor = view.cursor();
1962 if (view.columnCount() != catalog_row_columns) return error.UnsupportedCatalogFormat;
1963 if (rowid < first_object_rowid) return error.CatalogCorrupt;
1964 const name = try textValue(try cursor.column(1));
1965 const table_name = try textValue(try cursor.column(2));
1966 const root_page = try rootValue(try cursor.column(3));
1967 const first_blob = try blobValue(try cursor.column(4));
1968 const second_blob = try blobValue(try cursor.column(5));
1969 const identity_page = try rootValue(try cursor.column(6));
1970 const fields = if (kind == .index) try decodeFields(first_blob, &scratch.fields) else scratch.fields[0..0];
1971 const columns = if (kind == .index) try decodeCollations(second_blob, fields.len, &scratch.columns) else scratch.columns[0..0];
1972 const definitions = if (kind == .relation) try decodeDefinitions(first_blob, &scratch.definitions, &scratch.names, &scratch.defaults) else scratch.definitions[0..0];
1973 if (kind == .relation and second_blob.len != 0) return error.CatalogCorrupt;
1974 return .{
1975 .kind = kind,
1976 .name = name,
1977 .table_name = table_name,
1978 .root_page = root_page,
1979 .identity_page = identity_page,
1980 .fields = fields,
1981 .columns = columns,
1982 .definitions = definitions,
1983 };
1984 }
1985
1986 fn catalogStats(rowid: i64, view: row.View) Error!CatalogStats {
1987 var cursor = view.cursor();
1988 if (view.columnCount() != catalog_row_columns) return error.UnsupportedCatalogFormat;
1989 if (rowid < first_object_rowid) return error.CatalogCorrupt;
1990 const name = try textValue(try cursor.column(1));
1991 const table_name = try textValue(try cursor.column(2));
1992 const root_page = try rootValue(try cursor.column(3));
1993 const summary = try decodeSummary(try blobValue(try cursor.column(4)));
1994 const distribution_blob = try blobValue(try cursor.column(5));
1995 if (name.len == 0 or table_name.len == 0) return error.CatalogCorrupt;
1996 return .{
1997 .name = name,
1998 .table_name = table_name,
1999 .root_page = root_page,
2000 .summary = summary,
2001 .distribution_blob = distribution_blob,
2002 };
2003 }
2004
2005 fn catalogKind(value: row.Value) Error!CatalogKind {
2006 return switch (try integerValue(value)) {
2007 schema_kind => .schema,
2008 relation_kind => .relation,
2009 index_kind => .index,
2010 stats_kind => .stats,
2011 else => error.CatalogCorrupt,
2012 };
2013 }
2014
2015 fn integerValue(value: row.Value) Error!i64 {
2016 return switch (value) {
2017 .integer => |integer| integer,
2018 else => error.CatalogCorrupt,
2019 };
2020 }
2021
2022 fn textValue(value: row.Value) Error![]const u8 {
2023 return switch (value) {
2024 .text => |text| text,
2025 else => error.CatalogCorrupt,
2026 };
2027 }
2028
2029 fn blobValue(value: row.Value) Error![]const u8 {
2030 return switch (value) {
2031 .blob => |blob| blob,
2032 else => error.CatalogCorrupt,
2033 };
2034 }
2035
2036 fn rootValue(value: row.Value) Error!u32 {
2037 const integer = try integerValue(value);
2038 if (integer <= 0) return error.InvalidPageId;
2039 if (integer > std.math.maxInt(u32)) return error.InvalidPageId;
2040 return @intCast(integer);
2041 }
2042
2043 fn rootAsInteger(root_page: u32) i64 {
2044 return @intCast(root_page);
2045 }
2046
2047 fn identityAsInteger(identity_page: u32) i64 {
2048 return @intCast(identity_page);
2049 }
2050
2051 fn encodeSchema(target: *[schema_blob_bytes]u8, schema: Schema) []const u8 {
2052 std.mem.writeInt(u32, target[0..4], schema.format, .big);
2053 std.mem.writeInt(u64, target[4..12], schema.version, .big);
2054 return target[0..];
2055 }
2056
2057 fn decodeSchema(bytes: []const u8) Error!Schema {
2058 if (bytes.len != schema_blob_bytes) return error.CatalogCorrupt;
2059 return .{
2060 .format = std.mem.readInt(u32, bytes[0..4], .big),
2061 .version = std.mem.readInt(u64, bytes[4..12], .big),
2062 };
2063 }
2064
2065 fn encodeSummary(target: *[summary_blob_bytes]u8, summary: tree.Summary) Error![]const u8 {
2066 try writeSummaryField(target, 0, summary.branch_pages);
2067 try writeSummaryField(target, 1, summary.leaf_pages);
2068 try writeSummaryField(target, 2, summary.overflow_pages);
2069 try writeSummaryField(target, 3, summary.entries);
2070 try writeSummaryField(target, 4, summary.inline_records);
2071 try writeSummaryField(target, 5, summary.overflow_records);
2072 try writeSummaryField(target, 6, summary.max_depth);
2073 try writeSummaryField(target, 7, summary.key_bytes);
2074 try writeSummaryField(target, 8, summary.record_bytes);
2075 try writeSummaryField(target, 9, summary.value_bytes);
2076 return target[0..];
2077 }
2078
2079 fn decodeSummary(bytes: []const u8) Error!tree.Summary {
2080 if (bytes.len != summary_blob_bytes) return error.CatalogCorrupt;
2081 return .{
2082 .branch_pages = try readSummaryField(bytes, 0),
2083 .leaf_pages = try readSummaryField(bytes, 1),
2084 .overflow_pages = try readSummaryField(bytes, 2),
2085 .entries = try readSummaryField(bytes, 3),
2086 .inline_records = try readSummaryField(bytes, 4),
2087 .overflow_records = try readSummaryField(bytes, 5),
2088 .max_depth = try readSummaryField(bytes, 6),
2089 .key_bytes = try readSummaryField(bytes, 7),
2090 .record_bytes = try readSummaryField(bytes, 8),
2091 .value_bytes = try readSummaryField(bytes, 9),
2092 };
2093 }
2094
2095 fn writeSummaryField(target: *[summary_blob_bytes]u8, field: usize, value: usize) Error!void {
2096 if (value > std.math.maxInt(u64)) return error.CatalogCorrupt;
2097 std.mem.writeInt(u64, target[field * 8 ..][0..8], @intCast(value), .big);
2098 }
2099
2100 fn readSummaryField(bytes: []const u8, field: usize) Error!usize {
2101 const value = std.mem.readInt(u64, bytes[field * 8 ..][0..8], .big);
2102 if (value > std.math.maxInt(usize)) return error.CatalogCorrupt;
2103 return @intCast(value);
2104 }
2105
2106 const DistributionMeasure = struct {
2107 entries: usize = 0,
2108 distinct_values: usize = 0,
2109 max_equal: usize = 0,
2110 };
2111
2112 fn analyzeIndexDistribution(allocator: Allocator, relation_index: *const index_mod.Index) Error![]u8 {
2113 var source = LiveDistributionSource{ .index = relation_index };
2114 return try analyzeDistribution(allocator, &source);
2115 }
2116
2117 const LiveDistributionSource = struct {
2118 index: *const index_mod.Index,
2119
2120 const Scan = LiveDistributionScan;
2121
2122 fn columns(self: *const LiveDistributionSource) []const row.Column {
2123 return self.index.columns;
2124 }
2125
2126 fn scan(
2127 self: *const LiveDistributionSource,
2128 target: *LiveDistributionScan,
2129 allocator: Allocator,
2130 ) Error!void {
2131 try self.index.scan(&target.scan_value, allocator, null, null);
2132 }
2133 };
2134
2135 const LiveDistributionScan = struct {
2136 scan_value: index_mod.Scan,
2137
2138 fn deinit(self: *LiveDistributionScan) void {
2139 self.scan_value.deinit();
2140 }
2141
2142 fn next(self: *LiveDistributionScan) Error!?[]const u8 {
2143 const entry = (try self.scan_value.next()) orelse return null;
2144 return entry.key;
2145 }
2146 };
2147
2148 const PreparedDistributionKeys = struct {
2149 allocator: Allocator,
2150 bytes: []u8,
2151 keys: [][]const u8,
2152
2153 fn deinit(self: *PreparedDistributionKeys) void {
2154 self.allocator.free(self.keys);
2155 if (self.bytes.len != 0) self.allocator.free(self.bytes);
2156 self.* = undefined;
2157 }
2158 };
2159
2160 const PreparedDistributionSource = struct {
2161 columns_value: []const row.Column,
2162 keys: []const []const u8,
2163
2164 const Scan = PreparedDistributionScan;
2165
2166 fn columns(self: *const PreparedDistributionSource) []const row.Column {
2167 return self.columns_value;
2168 }
2169
2170 fn scan(
2171 self: *const PreparedDistributionSource,
2172 target: *PreparedDistributionScan,
2173 allocator: Allocator,
2174 ) Error!void {
2175 _ = allocator;
2176 target.* = .{ .keys = self.keys };
2177 }
2178 };
2179
2180 const PreparedDistributionScan = struct {
2181 keys: []const []const u8,
2182 offset: usize = 0,
2183
2184 fn deinit(self: *PreparedDistributionScan) void {
2185 self.* = undefined;
2186 }
2187
2188 fn next(self: *PreparedDistributionScan) Error!?[]const u8 {
2189 if (self.offset == self.keys.len) return null;
2190 const bytes = self.keys[self.offset];
2191 self.offset += 1;
2192 return bytes;
2193 }
2194 };
2195
2196 fn prepareIndexStats(
2197 allocator: Allocator,
2198 definition: IndexDefinition,
2199 puts: []const relation_mod.Edit.Put,
2200 summary: tree.Summary,
2201 index_offset: usize,
2202 ) Error!PreparedIndexStats {
2203 std.debug.assert(index_offset < relation_mod.max_indexes);
2204 var sorted_keys = try prepareDistributionKeys(allocator, definition, puts);
2205 defer sorted_keys.deinit();
2206 var source = PreparedDistributionSource{
2207 .columns_value = definition.columns,
2208 .keys = sorted_keys.keys,
2209 };
2210 const distribution_blob = try analyzeDistribution(allocator, &source);
2211 errdefer if (distribution_blob.len != 0) allocator.free(distribution_blob);
2212 const distribution = if (distribution_blob.len == 0)
2213 IndexDistribution{}
2214 else
2215 try decodeIndexDistribution(allocator, distribution_blob);
2216 errdefer freeIndexDistribution(allocator, distribution);
2217 return .{
2218 .stats = .{
2219 .name = try allocator.dupe(u8, definition.name),
2220 .root_page = @intCast(index_offset + 1),
2221 .summary = summary,
2222 .distribution = distribution,
2223 },
2224 .distribution_blob = distribution_blob,
2225 };
2226 }
2227
2228 fn prepareDistributionKeys(
2229 allocator: Allocator,
2230 definition: IndexDefinition,
2231 puts: []const relation_mod.Edit.Put,
2232 ) Error!PreparedDistributionKeys {
2233 var total_bytes: usize = 0;
2234 for (puts) |put| {
2235 const view = try row.View.init(put.bytes);
2236 var projected: [relation_mod.max_index_fields]row.Value = undefined;
2237 const values = try view.project(definition.fields, projected[0..]);
2238 var buffer: [page.size]u8 = undefined;
2239 const encoded = key.encodeIndex(
2240 &buffer,
2241 values,
2242 definition.columns,
2243 put.rowid,
2244 ) catch |err| switch (err) {
2245 error.OutputTooSmall => return error.KeyTooLarge,
2246 else => return err,
2247 };
2248 total_bytes = std.math.add(usize, total_bytes, encoded.len) catch return error.KeyTooLarge;
2249 }
2250
2251 const bytes = if (total_bytes == 0) @as([]u8, &.{}) else try allocator.alloc(u8, total_bytes);
2252 errdefer if (bytes.len != 0) allocator.free(bytes);
2253 const keys = try allocator.alloc([]const u8, puts.len);
2254 errdefer allocator.free(keys);
2255
2256 var cursor: usize = 0;
2257 for (puts, keys) |put, *encoded_key| {
2258 const view = try row.View.init(put.bytes);
2259 var projected: [relation_mod.max_index_fields]row.Value = undefined;
2260 const values = try view.project(definition.fields, projected[0..]);
2261 encoded_key.* = key.encodeIndex(
2262 bytes[cursor..],
2263 values,
2264 definition.columns,
2265 put.rowid,
2266 ) catch |err| switch (err) {
2267 error.OutputTooSmall => return error.KeyTooLarge,
2268 else => return err,
2269 };
2270 cursor += encoded_key.*.len;
2271 }
2272 std.debug.assert(cursor == bytes.len);
2273 std.mem.sort([]const u8, keys, {}, encodedKeyLessThan);
2274 return .{
2275 .allocator = allocator,
2276 .bytes = bytes,
2277 .keys = keys,
2278 };
2279 }
2280
2281 fn encodedKeyLessThan(_: void, left: []const u8, right: []const u8) bool {
2282 return simd.order(Bytes, left, right) == .lt;
2283 }
2284
2285 fn analyzeDistribution(allocator: Allocator, source: anytype) Error![]u8 {
2286 const prefix_total = source.columns().len;
2287 if (prefix_total == 0) return try allocator.alloc(u8, 0);
2288
2289 const leading_measure = try measureIndexDistribution(allocator, source, 1);
2290 if (leading_measure.entries == 0) return try allocator.alloc(u8, 0);
2291
2292 var encoded: std.ArrayList(u8) = .empty;
2293 errdefer encoded.deinit(allocator);
2294 try appendInt(u32, &encoded, allocator, distribution_format);
2295 try appendUsizeAsU16(&encoded, allocator, prefix_total);
2296
2297 try appendPrefixDistribution(&encoded, allocator, source, 1, leading_measure);
2298 var prefix_count: usize = 2;
2299 while (prefix_count <= prefix_total) : (prefix_count += 1) {
2300 const measure = try measureIndexDistribution(allocator, source, prefix_count);
2301 try appendPrefixDistribution(&encoded, allocator, source, prefix_count, measure);
2302 }
2303 return try encoded.toOwnedSlice(allocator);
2304 }
2305
2306 fn appendPrefixDistribution(
2307 encoded: *std.ArrayList(u8),
2308 allocator: Allocator,
2309 source: anytype,
2310 prefix_count: usize,
2311 measure: DistributionMeasure,
2312 ) Error!void {
2313 if (measure.entries == 0) return error.CatalogCorrupt;
2314
2315 var samples: std.ArrayList(u8) = .empty;
2316 defer samples.deinit(allocator);
2317
2318 const sample_target = @min(max_index_distribution_samples, measure.entries);
2319 var target_index: usize = 0;
2320 var sample_count: usize = 0;
2321 var scan: @TypeOf(source.*).Scan = undefined;
2322 try source.scan(&scan, allocator);
2323 defer scan.deinit();
2324
2325 var current_key: [page.size]u8 = undefined;
2326 var current_len: usize = 0;
2327 var current_valid = false;
2328 var run_start: usize = 0;
2329 var run_count: usize = 0;
2330 var ordinal: usize = 0;
2331 var distinct_ordinal: usize = 0;
2332
2333 while (try scan.next()) |bytes| {
2334 var prefix_buffer: [page.size]u8 = undefined;
2335 const prefix = try indexPrefixKey(&prefix_buffer, source.columns(), bytes, prefix_count);
2336 if (!current_valid) {
2337 current_valid = true;
2338 current_len = prefix.len;
2339 @memcpy(current_key[0..prefix.len], prefix);
2340 run_start = ordinal;
2341 run_count = 1;
2342 } else if (std.mem.eql(u8, current_key[0..current_len], prefix)) {
2343 run_count += 1;
2344 } else {
2345 try appendDistributionSamples(&samples, allocator, measure.entries, sample_target, &target_index, &sample_count, current_key[0..current_len], run_start, run_count, distinct_ordinal);
2346 distinct_ordinal += 1;
2347 current_len = prefix.len;
2348 @memcpy(current_key[0..prefix.len], prefix);
2349 run_start = ordinal;
2350 run_count = 1;
2351 }
2352 ordinal += 1;
2353 }
2354 if (current_valid) try appendDistributionSamples(&samples, allocator, measure.entries, sample_target, &target_index, &sample_count, current_key[0..current_len], run_start, run_count, distinct_ordinal);
2355
2356 try appendUsizeAsU16(encoded, allocator, prefix_count);
2357 try appendUsizeAsU64(encoded, allocator, measure.distinct_values);
2358 try appendUsizeAsU64(encoded, allocator, measure.max_equal);
2359 try appendUsizeAsU16(encoded, allocator, sample_count);
2360 try encoded.appendSlice(allocator, samples.items);
2361 }
2362
2363 fn measureIndexDistribution(
2364 allocator: Allocator,
2365 source: anytype,
2366 prefix_count: usize,
2367 ) Error!DistributionMeasure {
2368 var measure = DistributionMeasure{};
2369 var scan: @TypeOf(source.*).Scan = undefined;
2370 try source.scan(&scan, allocator);
2371 defer scan.deinit();
2372
2373 var current_key: [page.size]u8 = undefined;
2374 var current_len: usize = 0;
2375 var current_valid = false;
2376 var run_count: usize = 0;
2377
2378 while (try scan.next()) |bytes| {
2379 var prefix_buffer: [page.size]u8 = undefined;
2380 const prefix = try indexPrefixKey(&prefix_buffer, source.columns(), bytes, prefix_count);
2381 measure.entries += 1;
2382 if (!current_valid) {
2383 current_valid = true;
2384 current_len = prefix.len;
2385 @memcpy(current_key[0..prefix.len], prefix);
2386 run_count = 1;
2387 } else if (std.mem.eql(u8, current_key[0..current_len], prefix)) {
2388 run_count += 1;
2389 } else {
2390 measure.distinct_values += 1;
2391 measure.max_equal = @max(measure.max_equal, run_count);
2392 current_len = prefix.len;
2393 @memcpy(current_key[0..prefix.len], prefix);
2394 run_count = 1;
2395 }
2396 }
2397 if (current_valid) {
2398 measure.distinct_values += 1;
2399 measure.max_equal = @max(measure.max_equal, run_count);
2400 }
2401 return measure;
2402 }
2403
2404 fn indexPrefixKey(target: *[page.size]u8, columns: []const row.Column, bytes: []const u8, prefix_count: usize) Error![]const u8 {
2405 var values: [relation_mod.max_index_fields]row.Value = undefined;
2406 var scratch: [page.size]u8 = undefined;
2407 const decoded = try key.decodeIndex(&values, &scratch, bytes);
2408 if (prefix_count == 0 or prefix_count > decoded.values.len) return error.CatalogCorrupt;
2409 return try key.encodeIndexPrefix(target, decoded.values[0..prefix_count], columns);
2410 }
2411
2412 fn appendDistributionSamples(
2413 bytes: *std.ArrayList(u8),
2414 allocator: Allocator,
2415 entries: usize,
2416 sample_target: usize,
2417 target_index: *usize,
2418 sample_count: *usize,
2419 sample_key: []const u8,
2420 less_than: usize,
2421 equal_count: usize,
2422 less_distinct: usize,
2423 ) Error!void {
2424 const end = less_than + equal_count;
2425 var sampled = false;
2426 while (target_index.* < sample_target) {
2427 const target = sampleOrdinal(target_index.*, entries, sample_target);
2428 if (target < less_than) {
2429 target_index.* += 1;
2430 continue;
2431 }
2432 if (target >= end) break;
2433 if (!sampled) {
2434 try appendUsizeAsU64(bytes, allocator, less_than);
2435 try appendUsizeAsU64(bytes, allocator, equal_count);
2436 try appendUsizeAsU64(bytes, allocator, less_distinct);
2437 try appendUsizeAsU16(bytes, allocator, sample_key.len);
2438 try bytes.appendSlice(allocator, sample_key);
2439 sample_count.* += 1;
2440 sampled = true;
2441 }
2442 target_index.* += 1;
2443 }
2444 }
2445
2446 fn sampleOrdinal(sample_index: usize, entries: usize, sample_count: usize) usize {
2447 const base = entries / sample_count;
2448 const remainder = entries % sample_count;
2449 const start = sample_index * base + @min(sample_index, remainder);
2450 const width = base + if (sample_index < remainder) @as(usize, 1) else 0;
2451 return @min(entries - 1, start + width / 2);
2452 }
2453
2454 fn decodeIndexDistribution(allocator: Allocator, bytes: []const u8) Error!IndexDistribution {
2455 if (bytes.len == 0) return .{};
2456 if (bytes.len < distribution_header_bytes) return error.CatalogCorrupt;
2457 if (std.mem.readInt(u32, bytes[0..4], .big) != distribution_format) return error.CatalogCorrupt;
2458 const prefix_total = std.mem.readInt(u16, bytes[4..6], .big);
2459 if (prefix_total == 0 or prefix_total > relation_mod.max_index_fields) return error.CatalogCorrupt;
2460
2461 var cursor: usize = distribution_header_bytes;
2462 const prefixes = try allocator.alloc(IndexPrefixDistribution, prefix_total);
2463 for (prefixes) |*prefix| prefix.* = .{};
2464 errdefer freeIndexPrefixDistributions(allocator, prefixes);
2465
2466 var prefix_index: usize = 0;
2467 while (prefix_index < prefix_total) : (prefix_index += 1) {
2468 prefixes[prefix_index] = try decodeIndexPrefixDistribution(allocator, bytes, &cursor);
2469 }
2470 if (cursor != bytes.len) return error.CatalogCorrupt;
2471
2472 const leading = prefixes[0];
2473 return .{
2474 .distinct_values = leading.distinct_values,
2475 .max_equal = leading.max_equal,
2476 .samples = leading.samples,
2477 .sample_keys = leading.sample_keys,
2478 .prefixes = prefixes,
2479 };
2480 }
2481
2482 fn decodeIndexPrefixDistribution(allocator: Allocator, bytes: []const u8, cursor: *usize) Error!IndexPrefixDistribution {
2483 if (distribution_prefix_header_bytes > bytes.len - cursor.*) return error.CatalogCorrupt;
2484 const field_count = std.mem.readInt(u16, bytes[cursor.*..][0..2], .big);
2485 cursor.* += 2;
2486 if (field_count == 0 or field_count > relation_mod.max_index_fields) return error.CatalogCorrupt;
2487 const distinct_values = try readUsizeField(bytes[cursor.*..][0..8]);
2488 cursor.* += 8;
2489 const max_equal = try readUsizeField(bytes[cursor.*..][0..8]);
2490 cursor.* += 8;
2491 const sample_count = std.mem.readInt(u16, bytes[cursor.*..][0..2], .big);
2492 cursor.* += 2;
2493
2494 var scan_cursor = cursor.*;
2495 var key_bytes: usize = 0;
2496 var sample_index: usize = 0;
2497 while (sample_index < sample_count) : (sample_index += 1) {
2498 if (distribution_sample_header_bytes > bytes.len - scan_cursor) return error.CatalogCorrupt;
2499 scan_cursor += 24;
2500 const key_len = std.mem.readInt(u16, bytes[scan_cursor..][0..2], .big);
2501 scan_cursor += 2;
2502 if (key_len > bytes.len - scan_cursor) return error.CatalogCorrupt;
2503 key_bytes += key_len;
2504 scan_cursor += key_len;
2505 }
2506
2507 const samples: []IndexSample = if (sample_count == 0)
2508 &.{}
2509 else
2510 try allocator.alloc(IndexSample, sample_count);
2511 errdefer if (sample_count != 0) allocator.free(samples);
2512 const keys: []u8 = if (key_bytes == 0)
2513 &.{}
2514 else
2515 try allocator.alloc(u8, key_bytes);
2516 errdefer if (key_bytes != 0) allocator.free(keys);
2517
2518 var key_cursor: usize = 0;
2519 sample_index = 0;
2520 while (sample_index < sample_count) : (sample_index += 1) {
2521 const less_than = try readUsizeField(bytes[cursor.*..][0..8]);
2522 cursor.* += 8;
2523 const equal_count = try readUsizeField(bytes[cursor.*..][0..8]);
2524 cursor.* += 8;
2525 const less_distinct = try readUsizeField(bytes[cursor.*..][0..8]);
2526 cursor.* += 8;
2527 const key_len = std.mem.readInt(u16, bytes[cursor.*..][0..2], .big);
2528 cursor.* += 2;
2529 @memcpy(keys[key_cursor..][0..key_len], bytes[cursor.*..][0..key_len]);
2530 samples[sample_index] = .{
2531 .key = keys[key_cursor..][0..key_len],
2532 .less_than = less_than,
2533 .equal_count = equal_count,
2534 .less_distinct = less_distinct,
2535 };
2536 key_cursor += key_len;
2537 cursor.* += key_len;
2538 }
2539
2540 return .{
2541 .field_count = field_count,
2542 .distinct_values = distinct_values,
2543 .max_equal = max_equal,
2544 .samples = samples,
2545 .sample_keys = keys,
2546 };
2547 }
2548
2549 fn appendUsizeAsU64(bytes: *std.ArrayList(u8), allocator: Allocator, value: usize) Error!void {
2550 if (value > std.math.maxInt(u64)) return error.CatalogCorrupt;
2551 try appendInt(u64, bytes, allocator, @intCast(value));
2552 }
2553
2554 fn appendUsizeAsU16(bytes: *std.ArrayList(u8), allocator: Allocator, value: usize) Error!void {
2555 var buffer: [2]u8 = undefined;
2556 try writeUsizeAsU16(&buffer, value);
2557 try bytes.appendSlice(allocator, &buffer);
2558 }
2559
2560 fn writeUsizeAsU16(target: []u8, value: usize) Error!void {
2561 if (target.len < 2) return error.CatalogCorrupt;
2562 if (value > std.math.maxInt(u16)) return error.CatalogCorrupt;
2563 std.mem.writeInt(u16, target[0..2], @intCast(value), .big);
2564 }
2565
2566 fn appendInt(comptime T: type, bytes: *std.ArrayList(u8), allocator: Allocator, value: T) std.mem.Allocator.Error!void {
2567 var buffer: [@sizeOf(T)]u8 = undefined;
2568 std.mem.writeInt(T, &buffer, value, .big);
2569 try bytes.appendSlice(allocator, &buffer);
2570 }
2571
2572 fn readUsizeField(bytes: []const u8) Error!usize {
2573 if (bytes.len < 8) return error.CatalogCorrupt;
2574 const value = std.mem.readInt(u64, bytes[0..8], .big);
2575 if (value > std.math.maxInt(usize)) return error.CatalogCorrupt;
2576 return @intCast(value);
2577 }
2578
2579 fn encodeDefinitions(target: *[page.size]u8, definitions: []const ColumnDefinition) Error![]const u8 {
2580 if (definitions.len > max_columns) return error.TooManyColumns;
2581 var cursor: usize = 0;
2582 for (definitions) |definition| {
2583 if (definition.name.len == 0) return error.CatalogCorrupt;
2584 if (definition.name.len > std.math.maxInt(u16)) return error.CatalogCorrupt;
2585 var default_buffer: [page.size]u8 = undefined;
2586 const encoded_default = try row.encode(&default_buffer, &.{definition.default});
2587 if (encoded_default.len > std.math.maxInt(u16)) return error.CatalogCorrupt;
2588 const required = column_name_len_bytes + definition.name.len + 1 + default_len_bytes + encoded_default.len;
2589 if (required > target.len - cursor) return error.CatalogCorrupt;
2590 std.mem.writeInt(u16, target[cursor..][0..column_name_len_bytes], @intCast(definition.name.len), .big);
2591 cursor += column_name_len_bytes;
2592 @memcpy(target[cursor..][0..definition.name.len], definition.name);
2593 cursor += definition.name.len;
2594 target[cursor] = collationByte(definition.column.collation);
2595 cursor += 1;
2596 std.mem.writeInt(u16, target[cursor..][0..default_len_bytes], @intCast(encoded_default.len), .big);
2597 cursor += default_len_bytes;
2598 @memcpy(target[cursor..][0..encoded_default.len], encoded_default);
2599 cursor += encoded_default.len;
2600 }
2601 return target[0..cursor];
2602 }
2603
2604 fn decodeDefinitions(bytes: []const u8, target: *[max_columns]ColumnDefinition, names_target: *[max_column_name_bytes]u8, defaults_target: *[max_column_default_bytes]u8) Error![]ColumnDefinition {
2605 var cursor: usize = 0;
2606 var name_cursor: usize = 0;
2607 var default_cursor: usize = 0;
2608 var count: usize = 0;
2609 while (cursor < bytes.len) {
2610 if (count >= target.len) return error.TooManyColumns;
2611 if (column_name_len_bytes > bytes.len - cursor) return error.CatalogCorrupt;
2612 const name_len = std.mem.readInt(u16, bytes[cursor..][0..column_name_len_bytes], .big);
2613 cursor += column_name_len_bytes;
2614 if (name_len == 0) return error.CatalogCorrupt;
2615 const name_required = @as(usize, name_len) + 1 + default_len_bytes;
2616 if (name_required > bytes.len - cursor) return error.CatalogCorrupt;
2617 if (name_len > names_target.len - name_cursor) return error.CatalogCorrupt;
2618 @memcpy(names_target[name_cursor..][0..name_len], bytes[cursor..][0..name_len]);
2619 const name = names_target[name_cursor..][0..name_len];
2620 cursor += name_len;
2621 name_cursor += name_len;
2622 const collation = try collationFromByte(bytes[cursor]);
2623 cursor += 1;
2624 const default_len = std.mem.readInt(u16, bytes[cursor..][0..default_len_bytes], .big);
2625 cursor += default_len_bytes;
2626 if (default_len > bytes.len - cursor) return error.CatalogCorrupt;
2627 if (default_len > defaults_target.len - default_cursor) return error.CatalogCorrupt;
2628 @memcpy(defaults_target[default_cursor..][0..default_len], bytes[cursor..][0..default_len]);
2629 const default_bytes = defaults_target[default_cursor..][0..default_len];
2630 const default_value = try defaultValue(default_bytes);
2631 cursor += default_len;
2632 default_cursor += default_len;
2633 for (target[0..count]) |previous| {
2634 if (std.ascii.eqlIgnoreCase(previous.name, name)) return error.CatalogCorrupt;
2635 }
2636 target[count] = .{
2637 .name = name,
2638 .column = .{ .collation = collation },
2639 .default = default_value,
2640 };
2641 count += 1;
2642 }
2643 return target[0..count];
2644 }
2645
2646 const DefinitionCopy = struct {
2647 names: usize,
2648 defaults: usize,
2649 };
2650
2651 fn copyDefinitions(target: *[max_columns]ColumnDefinition, names_target: *[max_column_name_bytes]u8, defaults_target: *[max_column_default_bytes]u8, definitions: []const ColumnDefinition) Error!DefinitionCopy {
2652 if (definitions.len > target.len) return error.TooManyColumns;
2653 var name_cursor: usize = 0;
2654 var default_cursor: usize = 0;
2655 for (definitions, 0..) |definition, offset| {
2656 if (definition.name.len > names_target.len - name_cursor) return error.CatalogCorrupt;
2657 @memcpy(names_target[name_cursor..][0..definition.name.len], definition.name);
2658 var default_buffer: [page.size]u8 = undefined;
2659 const encoded_default = try row.encode(&default_buffer, &.{definition.default});
2660 if (encoded_default.len > defaults_target.len - default_cursor) return error.CatalogCorrupt;
2661 @memcpy(defaults_target[default_cursor..][0..encoded_default.len], encoded_default);
2662 const default_value = try defaultValue(defaults_target[default_cursor..][0..encoded_default.len]);
2663 target[offset] = .{
2664 .name = names_target[name_cursor..][0..definition.name.len],
2665 .column = definition.column,
2666 .default = default_value,
2667 };
2668 name_cursor += definition.name.len;
2669 default_cursor += encoded_default.len;
2670 }
2671 return .{
2672 .names = name_cursor,
2673 .defaults = default_cursor,
2674 };
2675 }
2676
2677 fn copyOpenedDefinitions(target: []ColumnDefinition, names_target: []u8, defaults_target: []u8, definitions: []const ColumnDefinition) void {
2678 var name_cursor: usize = 0;
2679 var default_cursor: usize = 0;
2680 for (definitions, 0..) |definition, offset| {
2681 @memcpy(names_target[name_cursor..][0..definition.name.len], definition.name);
2682 var default_buffer: [page.size]u8 = undefined;
2683 const encoded_default = row.encode(&default_buffer, &.{definition.default}) catch unreachable;
2684 @memcpy(defaults_target[default_cursor..][0..encoded_default.len], encoded_default);
2685 const default_value = defaultValue(defaults_target[default_cursor..][0..encoded_default.len]) catch unreachable;
2686 target[offset] = .{
2687 .name = names_target[name_cursor..][0..definition.name.len],
2688 .column = definition.column,
2689 .default = default_value,
2690 };
2691 name_cursor += definition.name.len;
2692 default_cursor += encoded_default.len;
2693 }
2694 }
2695
2696 fn defaultValue(bytes: []const u8) Error!row.Value {
2697 const view = try row.View.init(bytes);
2698 if (view.columnCount() != 1) return error.CatalogCorrupt;
2699 return try view.column(0);
2700 }
2701
2702 fn encodeFields(target: *[relation_mod.max_index_fields * field_bytes]u8, fields: []const usize) Error![]const u8 {
2703 if (fields.len > relation_mod.max_index_fields) return error.TooManyIndexFields;
2704 for (fields, 0..) |field, offset| {
2705 if (field > std.math.maxInt(u32)) return error.CatalogCorrupt;
2706 std.mem.writeInt(u32, target[offset * field_bytes ..][0..field_bytes], @intCast(field), .big);
2707 }
2708 return target[0 .. fields.len * field_bytes];
2709 }
2710
2711 fn decodeFields(bytes: []const u8, target: *[relation_mod.max_index_fields]usize) Error![]usize {
2712 if (bytes.len % field_bytes != 0) return error.CatalogCorrupt;
2713 const count = bytes.len / field_bytes;
2714 if (count > target.len) return error.TooManyIndexFields;
2715 var index: usize = 0;
2716 while (index < count) : (index += 1) {
2717 target[index] = std.mem.readInt(u32, bytes[index * field_bytes ..][0..field_bytes], .big);
2718 }
2719 return target[0..count];
2720 }
2721
2722 fn encodeCollations(target: *[relation_mod.max_index_fields]u8, field_count: usize, columns: []const row.Column) Error![]const u8 {
2723 if (field_count > target.len) return error.TooManyIndexFields;
2724 if (columns.len > field_count) return error.CatalogCorrupt;
2725 var index: usize = 0;
2726 while (index < field_count) : (index += 1) {
2727 const column = if (index < columns.len) columns[index] else row.Column{};
2728 target[index] = collationByte(column.collation);
2729 }
2730 return target[0..field_count];
2731 }
2732
2733 fn decodeCollations(bytes: []const u8, field_count: usize, target: *[relation_mod.max_index_fields]row.Column) Error![]row.Column {
2734 if (bytes.len != field_count) return error.CatalogCorrupt;
2735 if (field_count > target.len) return error.TooManyIndexFields;
2736 var index: usize = 0;
2737 while (index < field_count) : (index += 1) {
2738 target[index] = .{ .collation = try collationFromByte(bytes[index]) };
2739 }
2740 return target[0..field_count];
2741 }
2742
2743 fn collationByte(collation: row.Collation) u8 {
2744 return switch (collation) {
2745 .binary => 0,
2746 .nocase => 1,
2747 .rtrim => 2,
2748 };
2749 }
2750
2751 fn collationFromByte(byte: u8) Error!row.Collation {
2752 return switch (byte) {
2753 0 => .binary,
2754 1 => .nocase,
2755 2 => .rtrim,
2756 else => error.CatalogCorrupt,
2757 };
2758 }
2759
2760 fn freeAnalyzeIndexes(allocator: Allocator, indexes: []const AnalyzeIndex) void {
2761 for (indexes) |index_object| {
2762 allocator.free(index_object.name);
2763 if (index_object.distribution_blob.len != 0) allocator.free(index_object.distribution_blob);
2764 }
2765 }
2766
2767 fn freeIndexStats(allocator: Allocator, indexes: []const IndexStats) void {
2768 for (indexes) |index_stats| {
2769 allocator.free(index_stats.name);
2770 freeIndexDistribution(allocator, index_stats.distribution);
2771 }
2772 }
2773
2774 fn freeIndexDistribution(allocator: Allocator, distribution: IndexDistribution) void {
2775 if (distribution.prefixes.len != 0) {
2776 freeIndexPrefixDistributions(allocator, distribution.prefixes);
2777 return;
2778 }
2779 if (distribution.samples.len != 0) allocator.free(distribution.samples);
2780 if (distribution.sample_keys.len != 0) allocator.free(distribution.sample_keys);
2781 }
2782
2783 fn freeIndexPrefixDistributions(allocator: Allocator, prefixes: []IndexPrefixDistribution) void {
2784 for (prefixes) |prefix| {
2785 if (prefix.samples.len != 0) allocator.free(prefix.samples);
2786 if (prefix.sample_keys.len != 0) allocator.free(prefix.sample_keys);
2787 }
2788 allocator.free(prefixes);
2789 }
2790
2791 fn containsIndexStats(indexes: []const IndexStats, name: []const u8) bool {
2792 for (indexes) |index_stats| {
2793 if (std.mem.eql(u8, index_stats.name, name)) return true;
2794 }
2795 return false;
2796 }
2797
2798 fn addRoot(roots: *[space_mod.max_roots]space_mod.RootSpec, root_count: *usize, spec: space_mod.RootSpec) Error!void {
2799 if (spec.root_page == 0) return error.InvalidPageId;
2800 var index: usize = 0;
2801 while (index < root_count.*) : (index += 1) {
2802 if (roots[index].root_page == spec.root_page) return error.InvalidPageId;
2803 }
2804 if (root_count.* >= roots.len) return error.TooManyRoots;
2805 roots[root_count.*] = spec;
2806 root_count.* += 1;
2807 }
2808
2809 fn allocateCatalogRoot(write: *tree.Write, roots: *[space_mod.max_roots]space_mod.RootSpec, root_count: *usize) Error!space_mod.RootSpec {
2810 const root_page = try write.allocateRoot();
2811 const identity_page = try write.allocateRoot();
2812 const spec = space_mod.RootSpec{ .root_page = root_page, .identity_page = identity_page };
2813 try addRoot(roots, root_count, spec);
2814 return spec;
2815 }
2816
2817 test "catalog reader exposes one snapshot through query-only methods" {
2818 var tmp = std.testing.tmpDir(.{});
2819 defer tmp.cleanup();
2820
2821 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
2822 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
2823 .header = testingHeader(),
2824 });
2825 defer database.deinit();
2826 try database.reserve(.{ .wal_frames = 160 });
2827
2828 var catalog = try Catalog.open(&database, .{});
2829 const indexes = [_]IndexDefinition{.{
2830 .name = "items_value",
2831 .fields = &.{0},
2832 }};
2833 const created = try catalog.createRelation(std.testing.allocator, .{
2834 .name = "items",
2835 .indexes = &indexes,
2836 }, .{ .durability = .buffered });
2837 {
2838 var writable = try catalog.openRelation(std.testing.allocator, "items");
2839 defer writable.deinit();
2840 _ = try writable.relation.put(
2841 std.testing.allocator,
2842 7,
2843 &.{.{ .integer = 42 }},
2844 .{ .durability = .buffered },
2845 );
2846 }
2847
2848 var read = try database.beginRead();
2849 defer read.deinit();
2850 const reader = try Reader.open(read.snapshot(), .{});
2851 try std.testing.expectEqual(created.schema, try reader.schemaState(std.testing.allocator));
2852 var names = try reader.relationNames(std.testing.allocator);
2853 defer names.deinit();
2854 try std.testing.expectEqual(@as(usize, 1), names.names.len);
2855 try std.testing.expectEqualSlices(u8, "items", names.names[0]);
2856 var opened = try reader.openRelation(std.testing.allocator, "items");
2857 defer opened.deinit();
2858 const bytes = (try opened.relation.get(std.testing.allocator, 7)).?;
2859 defer std.testing.allocator.free(bytes);
2860 const view = try row.View.init(bytes);
2861 try std.testing.expectEqual(@as(i64, 42), (try view.column(0)).integer);
2862 var lookup: index_mod.Scan = undefined;
2863 try opened.relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 42 }});
2864 defer lookup.deinit();
2865 try std.testing.expectEqual(@as(i64, 7), (try lookup.next()).?.rowid);
2866 try std.testing.expect(try lookup.next() == null);
2867 try std.testing.expect((try reader.relationStats(std.testing.allocator, "items")) == null);
2868 try std.testing.expect(!@hasDecl(Reader, "createRelation"));
2869 try std.testing.expect(!@hasDecl(Reader, "analyzeRelation"));
2870 try std.testing.expect(!@hasDecl(Reader, "dropRelation"));
2871 }
2872
2873 test "catalog reads the schema, handle and stats of one relation in one pass" {
2874 var tmp = std.testing.tmpDir(.{});
2875 defer tmp.cleanup();
2876
2877 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
2878 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
2879 .header = testingHeader(),
2880 });
2881 defer database.deinit();
2882 try database.reserve(.{ .wal_frames = 640 });
2883
2884 var catalog = try Catalog.open(&database, .{});
2885 const buffered: file.CommitOptions = .{ .durability = .buffered };
2886 _ = try catalog.createRelation(std.testing.allocator, .{
2887 .name = "items",
2888 .columns = &.{.{ .name = "value" }},
2889 }, buffered);
2890 _ = try catalog.createRelation(std.testing.allocator, .{
2891 .name = "other",
2892 .indexes = &.{.{ .name = "other_value", .fields = &.{0} }},
2893 }, buffered);
2894 _ = try catalog.createIndex(std.testing.allocator, "items", .{
2895 .name = "items_value",
2896 .fields = &.{0},
2897 }, buffered);
2898 {
2899 var writable = try catalog.openRelation(std.testing.allocator, "items");
2900 defer writable.deinit();
2901 _ = try writable.relation.put(std.testing.allocator, 1, &.{.{ .integer = 3 }}, buffered);
2902 _ = try writable.relation.put(std.testing.allocator, 2, &.{.{ .integer = 5 }}, buffered);
2903 }
2904 _ = try catalog.analyzeRelation(std.testing.allocator, "items", buffered);
2905
2906 var items = try catalog.readRelation(std.testing.allocator, "items");
2907 defer items.deinit();
2908 try expectRelationState(&catalog, "items", &items);
2909 try std.testing.expectEqual(@as(usize, 1), items.handle.index_definitions.len);
2910 try std.testing.expectEqual(@as(usize, 2), items.relationStats().?.table.entries);
2911
2912 var other = try catalog.readRelation(std.testing.allocator, "other");
2913 defer other.deinit();
2914 try expectRelationState(&catalog, "other", &other);
2915 try std.testing.expectEqualStrings("other_value", other.handle.index_definitions[0].name);
2916 try std.testing.expect(other.relationStats() == null);
2917 try std.testing.expectError(
2918 error.RelationNotFound,
2919 catalog.readRelation(std.testing.allocator, "missing"),
2920 );
2921
2922 var read = try database.beginRead();
2923 defer read.deinit();
2924 const reader = try Reader.open(read.snapshot(), .{});
2925 var read_items = try reader.readRelation(std.testing.allocator, "items");
2926 defer read_items.deinit();
2927 try expectRelationState(&reader, "items", &read_items);
2928 try std.testing.expectError(
2929 error.RelationNotFound,
2930 reader.readRelation(std.testing.allocator, "missing"),
2931 );
2932 }
2933
2934 /// Checks that `state` holds what the separate readers of `catalog` return
2935 /// for `name`.
2936 fn expectRelationState(catalog: anytype, name: []const u8, state: anytype) !void {
2937 try std.testing.expectEqual(try catalog.schemaState(std.testing.allocator), state.schema);
2938 var handle = try catalog.openRelation(std.testing.allocator, name);
2939 defer handle.deinit();
2940 try std.testing.expectEqual(
2941 handle.relation.table.rows.root_page,
2942 state.handle.relation.table.rows.root_page,
2943 );
2944 try std.testing.expectEqual(handle.definitions.len, state.handle.definitions.len);
2945 try std.testing.expectEqual(handle.specs.len, state.handle.specs.len);
2946 for (handle.specs, state.handle.specs) |expected, actual| {
2947 try std.testing.expectEqual(expected.root_page, actual.root_page);
2948 try std.testing.expectEqualSlices(usize, expected.fields, actual.fields);
2949 }
2950 try std.testing.expectEqual(
2951 handle.index_definitions.len,
2952 state.handle.index_definitions.len,
2953 );
2954 for (handle.index_definitions, state.handle.index_definitions) |expected, actual| {
2955 try std.testing.expectEqualStrings(expected.name, actual.name);
2956 }
2957
2958 var stats = try catalog.relationStats(std.testing.allocator, name);
2959 defer if (stats) |*relation_stats| relation_stats.deinit();
2960 const actual_stats = state.relationStats() orelse {
2961 try std.testing.expect(stats == null);
2962 return;
2963 };
2964 const expected_stats = stats orelse return error.TestUnexpectedResult;
2965 try std.testing.expectEqual(expected_stats.table_root_page, actual_stats.table_root_page);
2966 try std.testing.expectEqual(expected_stats.table, actual_stats.table);
2967 try std.testing.expectEqual(expected_stats.indexes.len, actual_stats.indexes.len);
2968 for (expected_stats.indexes, actual_stats.indexes) |expected, actual| {
2969 try std.testing.expectEqualStrings(expected.name, actual.name);
2970 try std.testing.expectEqual(expected.root_page, actual.root_page);
2971 try std.testing.expectEqual(expected.summary, actual.summary);
2972 try std.testing.expectEqual(
2973 expected.distribution.distinct_values,
2974 actual.distribution.distinct_values,
2975 );
2976 }
2977 }
2978
2979 test "catalog rejects stats that disagree with their relation and rows without a schema" {
2980 const damages = [_]CatalogDamage{
2981 .moved_table_stats,
2982 .orphaned_index_stats,
2983 .missing_schema_row,
2984 };
2985 for (damages) |damage| {
2986 var tmp = std.testing.tmpDir(.{});
2987 defer tmp.cleanup();
2988
2989 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
2990 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
2991 .header = testingHeader(),
2992 });
2993 defer database.deinit();
2994 try database.reserve(.{ .wal_frames = 640 });
2995
2996 var catalog = try Catalog.open(&database, .{});
2997 const buffered: file.CommitOptions = .{ .durability = .buffered };
2998 _ = try catalog.createRelation(std.testing.allocator, .{
2999 .name = "items",
3000 .indexes = &.{.{ .name = "items_value", .fields = &.{0} }},
3001 }, buffered);
3002 {
3003 var writable = try catalog.openRelation(std.testing.allocator, "items");
3004 defer writable.deinit();
3005 _ = try writable.relation.put(
3006 std.testing.allocator,
3007 1,
3008 &.{.{ .integer = 3 }},
3009 buffered,
3010 );
3011 }
3012 _ = try catalog.analyzeRelation(std.testing.allocator, "items", buffered);
3013 try damageCatalog(&database, damage);
3014
3015 try std.testing.expectError(
3016 error.CatalogCorrupt,
3017 catalog.readRelation(std.testing.allocator, "items"),
3018 );
3019 switch (damage) {
3020 .moved_table_stats, .orphaned_index_stats => try std.testing.expectError(
3021 error.CatalogCorrupt,
3022 catalog.relationStats(std.testing.allocator, "items"),
3023 ),
3024 .missing_schema_row => try std.testing.expectError(
3025 error.CatalogCorrupt,
3026 catalog.schemaState(std.testing.allocator),
3027 ),
3028 }
3029 }
3030 }
3031
3032 const CatalogDamage = enum {
3033 /// The table stats row names a root other than the relation's.
3034 moved_table_stats,
3035 /// Index stats rows remain without the table stats row.
3036 orphaned_index_stats,
3037 /// Object rows remain without the schema row.
3038 missing_schema_row,
3039 };
3040
3041 /// Rewrites the catalog rows of an analyzed relation so that `damage` holds.
3042 fn damageCatalog(database: *file.Database, damage: CatalogDamage) !void {
3043 const space = try space_mod.Space.open(database, .{
3044 .meta_page = default_meta_page,
3045 .roots = &.{.{ .root_page = default_root_page }},
3046 });
3047 var schema = try space.rowidTable(default_root_page);
3048 var write = try space.beginWrite();
3049 defer write.deinit();
3050 switch (damage) {
3051 .missing_schema_row => try schema.deleteIn(&write, schema_rowid),
3052 .moved_table_stats, .orphaned_index_stats => {
3053 var scan: table.Scan = undefined;
3054 try schema.scan(&scan, std.testing.allocator, null, null);
3055 defer scan.deinit();
3056 while (try scan.next()) |entry| {
3057 var scratch = CatalogScratch{};
3058 const table_stats = switch (try catalogEntry(entry.rowid, entry.bytes, &scratch)) {
3059 .stats => |found| found,
3060 .schema, .object => continue,
3061 };
3062 if (!std.mem.eql(u8, table_stats.name, table_stats.table_name)) continue;
3063 if (damage == .orphaned_index_stats) {
3064 try schema.deleteIn(&write, entry.rowid);
3065 continue;
3066 }
3067 var summary_buffer: [summary_blob_bytes]u8 = undefined;
3068 try putCatalogRow(
3069 &schema,
3070 &write,
3071 entry.rowid,
3072 stats_kind,
3073 table_stats.name,
3074 table_stats.table_name,
3075 table_stats.root_page + 1,
3076 0,
3077 try encodeSummary(&summary_buffer, table_stats.summary),
3078 "",
3079 );
3080 }
3081 },
3082 }
3083 _ = try write.commit(.{ .durability = .buffered });
3084 }
3085
3086 test "catalog persists relation metadata and reopens without caller specs" {
3087 var tmp = std.testing.tmpDir(.{});
3088 defer tmp.cleanup();
3089
3090 {
3091 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3092 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3093 .header = testingHeader(),
3094 });
3095 defer database.deinit();
3096 try database.reserve(.{ .wal_frames = 220 });
3097
3098 var catalog = try Catalog.open(&database, .{});
3099 try std.testing.expectEqual(Schema{}, try catalog.schemaState(std.testing.allocator));
3100 const indexes = [_]IndexDefinition{.{
3101 .name = "items_by_name",
3102 .fields = &.{1},
3103 .columns = &.{.{ .collation = .nocase }},
3104 }};
3105 const columns = [_]ColumnDefinition{
3106 .{ .name = "id" },
3107 .{ .name = "name", .column = .{ .collation = .nocase }, .default = .{ .text = "unknown" } },
3108 };
3109 const commit = try catalog.createRelation(std.testing.allocator, .{
3110 .name = "items",
3111 .columns = &columns,
3112 .indexes = &indexes,
3113 }, .{ .durability = .buffered });
3114 try std.testing.expectEqual(@as(u64, 1), commit.schema.version);
3115 try std.testing.expectEqual(commit.schema, try catalog.schemaState(std.testing.allocator));
3116 var handle = try catalog.openRelation(std.testing.allocator, "items");
3117 defer handle.deinit();
3118 try std.testing.expectEqual(@as(u32, 3), handle.relation.table.rows.root_page);
3119 try std.testing.expectEqual(@as(usize, 2), handle.definitions.len);
3120 try std.testing.expectEqualStrings("id", handle.definitions[0].name);
3121 try std.testing.expectEqualStrings("name", handle.definitions[1].name);
3122 try std.testing.expectEqual(row.Collation.nocase, handle.definitions[1].column.collation);
3123 try std.testing.expectEqualStrings("unknown", handle.definitions[1].default.text);
3124 try std.testing.expectEqual(@as(usize, 1), handle.index_definitions.len);
3125 try std.testing.expectEqualStrings("items_by_name", handle.index_definitions[0].name);
3126 try std.testing.expectEqual(@as(usize, 1), handle.index_definitions[0].fields.len);
3127 try std.testing.expectEqual(@as(usize, 1), handle.index_definitions[0].fields[0]);
3128 try std.testing.expectEqual(row.Collation.nocase, handle.index_definitions[0].columns[0].collation);
3129 _ = try handle.relation.put(std.testing.allocator, 8, &.{ .{ .integer = 8 }, .{ .text = "Alpha" } }, .{ .durability = .buffered });
3130 try database.syncWal();
3131 }
3132
3133 var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3134 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3135 .header = recoveredHeader(),
3136 });
3137 defer reopened.deinit();
3138
3139 var catalog = try Catalog.open(&reopened, .{});
3140 try std.testing.expectEqual(@as(u64, 1), (try catalog.schemaState(std.testing.allocator)).version);
3141 var handle = try catalog.openRelation(std.testing.allocator, "items");
3142 defer handle.deinit();
3143 try std.testing.expectEqual(@as(usize, 2), handle.definitions.len);
3144 try std.testing.expectEqualStrings("id", handle.definitions[0].name);
3145 try std.testing.expectEqualStrings("name", handle.definitions[1].name);
3146 try std.testing.expectEqual(row.Collation.nocase, handle.definitions[1].column.collation);
3147 try std.testing.expectEqualStrings("unknown", handle.definitions[1].default.text);
3148 try std.testing.expectEqual(@as(usize, 1), handle.index_definitions.len);
3149 try std.testing.expectEqualStrings("items_by_name", handle.index_definitions[0].name);
3150
3151 var lookup: index_mod.Scan = undefined;
3152 try handle.relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .text = "alpha" }});
3153 defer lookup.deinit();
3154 const entry = (try lookup.next()).?;
3155 try std.testing.expectEqual(@as(i64, 8), entry.rowid);
3156 try std.testing.expect(try lookup.next() == null);
3157
3158 const bytes = (try handle.relation.get(std.testing.allocator, 8)).?;
3159 defer std.testing.allocator.free(bytes);
3160 const view = try row.View.init(bytes);
3161 try std.testing.expectEqual(@as(i64, 8), (try view.column(0)).integer);
3162 try std.testing.expectEqualStrings("Alpha", (try view.column(1)).text);
3163 }
3164
3165 test "catalog analyzes relation stats and reopens them without schema bump" {
3166 var tmp = std.testing.tmpDir(.{});
3167 defer tmp.cleanup();
3168
3169 {
3170 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3171 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3172 .header = testingHeader(),
3173 });
3174 defer database.deinit();
3175 try database.reserve(.{ .wal_frames = 420 });
3176
3177 var catalog = try Catalog.open(&database, .{});
3178 const indexes = [_]IndexDefinition{.{
3179 .name = "items_value",
3180 .fields = &.{0},
3181 }};
3182 const created = try catalog.createRelation(std.testing.allocator, .{
3183 .name = "items",
3184 .indexes = &indexes,
3185 }, .{ .durability = .buffered });
3186 try std.testing.expect((try catalog.relationStats(std.testing.allocator, "items")) == null);
3187
3188 var handle = try catalog.openRelation(std.testing.allocator, "items");
3189 defer handle.deinit();
3190 _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .integer = 3 }, .{ .text = "three" } }, .{ .durability = .buffered });
3191 _ = try handle.relation.put(std.testing.allocator, 2, &.{ .{ .integer = 5 }, .{ .text = "five" } }, .{ .durability = .buffered });
3192 _ = try handle.relation.put(std.testing.allocator, 3, &.{ .{ .integer = 5 }, .{ .text = "cinco" } }, .{ .durability = .buffered });
3193
3194 const analyzed = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered });
3195 try std.testing.expectEqual(created.schema, analyzed.schema);
3196 try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator));
3197
3198 var stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
3199 defer stats.deinit();
3200 try std.testing.expectEqual(handle.relation.table.rows.root_page, stats.table_root_page);
3201 try std.testing.expectEqual(@as(usize, 3), stats.table.entries);
3202 try std.testing.expectEqual(@as(usize, 3), stats.table.inline_records);
3203 const index_stats = stats.index("items_value").?;
3204 try std.testing.expectEqual(@as(usize, 3), index_stats.summary.entries);
3205 try std.testing.expect(index_stats.summary.key_bytes > stats.table.key_bytes);
3206 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.distinct_values);
3207 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.max_equal);
3208 try std.testing.expect(index_stats.distribution.samples.len > 0);
3209 try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes.len);
3210 try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes[0].field_count);
3211 try database.syncWal();
3212 }
3213
3214 var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3215 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3216 .header = recoveredHeader(),
3217 });
3218 defer reopened.deinit();
3219
3220 var catalog = try Catalog.open(&reopened, .{});
3221 try std.testing.expectEqual(@as(u64, 1), (try catalog.schemaState(std.testing.allocator)).version);
3222 var stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
3223 defer stats.deinit();
3224 try std.testing.expectEqual(@as(usize, 3), stats.table.entries);
3225 try std.testing.expectEqual(@as(usize, 1), stats.indexes.len);
3226 const index_stats = stats.index("items_value").?;
3227 try std.testing.expectEqual(@as(usize, 3), index_stats.summary.entries);
3228 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.distinct_values);
3229 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.max_equal);
3230 try std.testing.expect(index_stats.distribution.samples.len > 0);
3231 try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes.len);
3232 }
3233
3234 test "catalog clears relation stats without schema bump" {
3235 var tmp = std.testing.tmpDir(.{});
3236 defer tmp.cleanup();
3237
3238 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3239 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3240 .header = testingHeader(),
3241 });
3242 defer database.deinit();
3243 try database.reserve(.{ .wal_frames = 420 });
3244
3245 var catalog = try Catalog.open(&database, .{});
3246 const indexes = [_]IndexDefinition{.{
3247 .name = "items_value",
3248 .fields = &.{0},
3249 }};
3250 const created = try catalog.createRelation(std.testing.allocator, .{
3251 .name = "items",
3252 .indexes = &indexes,
3253 }, .{ .durability = .buffered });
3254
3255 var handle = try catalog.openRelation(std.testing.allocator, "items");
3256 defer handle.deinit();
3257 _ = try handle.relation.put(std.testing.allocator, 1, &.{.{ .integer = 3 }}, .{ .durability = .buffered });
3258 _ = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered });
3259
3260 var stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
3261 stats.deinit();
3262
3263 const cleared = (try catalog.clearRelationStats(std.testing.allocator, "items", .{ .durability = .buffered })).?;
3264 try std.testing.expectEqual(created.schema, cleared.schema);
3265 try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator));
3266 try std.testing.expect((try catalog.relationStats(std.testing.allocator, "items")) == null);
3267 try std.testing.expect((try catalog.clearRelationStats(std.testing.allocator, "items", .{ .durability = .buffered })) == null);
3268 try std.testing.expectError(error.RelationNotFound, catalog.clearRelationStats(std.testing.allocator, "missing", .{ .durability = .buffered }));
3269 }
3270
3271 test "catalog drops relation index and stats metadata" {
3272 var tmp = std.testing.tmpDir(.{});
3273 defer tmp.cleanup();
3274
3275 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3276 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3277 .header = testingHeader(),
3278 });
3279 defer database.deinit();
3280 try database.reserve(.{ .wal_frames = 420 });
3281
3282 var catalog = try Catalog.open(&database, .{});
3283 const indexes = [_]IndexDefinition{.{
3284 .name = "items_value",
3285 .fields = &.{0},
3286 }};
3287 const created = try catalog.createRelation(std.testing.allocator, .{
3288 .name = "items",
3289 .indexes = &indexes,
3290 }, .{ .durability = .buffered });
3291 try std.testing.expectEqual(@as(u64, 1), created.schema.version);
3292
3293 var handle = try catalog.openRelation(std.testing.allocator, "items");
3294 defer handle.deinit();
3295 _ = try handle.relation.put(std.testing.allocator, 1, &.{.{ .integer = 3 }}, .{ .durability = .buffered });
3296 _ = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered });
3297
3298 var stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
3299 stats.deinit();
3300
3301 const dropped = try catalog.dropRelation(std.testing.allocator, "items", .{ .durability = .buffered });
3302 try std.testing.expectEqual(@as(u64, 2), dropped.schema.version);
3303 try std.testing.expectEqual(dropped.schema, try catalog.schemaState(std.testing.allocator));
3304 try std.testing.expectError(error.RelationNotFound, catalog.openRelation(std.testing.allocator, "items"));
3305 try std.testing.expectError(error.RelationNotFound, catalog.relationStats(std.testing.allocator, "items"));
3306
3307 var names = try catalog.relationNames(std.testing.allocator);
3308 defer names.deinit();
3309 try std.testing.expectEqual(@as(usize, 0), names.names.len);
3310
3311 const recreated = try catalog.createRelation(std.testing.allocator, .{
3312 .name = "items",
3313 .indexes = &indexes,
3314 }, .{ .durability = .buffered });
3315 try std.testing.expectEqual(@as(u64, 3), recreated.schema.version);
3316 try std.testing.expectError(error.RelationNotFound, catalog.dropRelation(std.testing.allocator, "missing", .{ .durability = .buffered }));
3317 }
3318
3319 test "catalog analyzes composite index prefix distributions" {
3320 var tmp = std.testing.tmpDir(.{});
3321 defer tmp.cleanup();
3322
3323 {
3324 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3325 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3326 .header = testingHeader(),
3327 });
3328 defer database.deinit();
3329 try database.reserve(.{ .wal_frames = 520 });
3330
3331 var catalog = try Catalog.open(&database, .{});
3332 const columns = [_]ColumnDefinition{
3333 .{ .name = "region" },
3334 .{ .name = "payload" },
3335 .{ .name = "name" },
3336 };
3337 const indexes = [_]IndexDefinition{.{
3338 .name = "items_region_payload",
3339 .fields = &.{ 0, 1 },
3340 }};
3341 _ = try catalog.createRelation(std.testing.allocator, .{
3342 .name = "items",
3343 .columns = &columns,
3344 .indexes = &indexes,
3345 }, .{ .durability = .buffered });
3346
3347 var handle = try catalog.openRelation(std.testing.allocator, "items");
3348 defer handle.deinit();
3349 _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .integer = 0 }, .{ .integer = 1 }, .{ .text = "a" } }, .{ .durability = .buffered });
3350 _ = try handle.relation.put(std.testing.allocator, 2, &.{ .{ .integer = 0 }, .{ .integer = 1 }, .{ .text = "b" } }, .{ .durability = .buffered });
3351 _ = try handle.relation.put(std.testing.allocator, 3, &.{ .{ .integer = 0 }, .{ .integer = 2 }, .{ .text = "c" } }, .{ .durability = .buffered });
3352 _ = try handle.relation.put(std.testing.allocator, 4, &.{ .{ .integer = 1 }, .{ .integer = 1 }, .{ .text = "d" } }, .{ .durability = .buffered });
3353 _ = try handle.relation.put(std.testing.allocator, 5, &.{ .{ .integer = 1 }, .{ .integer = 2 }, .{ .text = "e" } }, .{ .durability = .buffered });
3354 _ = try handle.relation.put(std.testing.allocator, 6, &.{ .{ .integer = 1 }, .{ .integer = 2 }, .{ .text = "f" } }, .{ .durability = .buffered });
3355 _ = try handle.relation.put(std.testing.allocator, 7, &.{ .{ .integer = 1 }, .{ .integer = 3 }, .{ .text = "g" } }, .{ .durability = .buffered });
3356 _ = try handle.relation.put(std.testing.allocator, 8, &.{ .{ .integer = 2 }, .{ .integer = 5 }, .{ .text = "h" } }, .{ .durability = .buffered });
3357 _ = try handle.relation.put(std.testing.allocator, 9, &.{ .{ .integer = 2 }, .{ .integer = 5 }, .{ .text = "i" } }, .{ .durability = .buffered });
3358
3359 _ = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered });
3360
3361 var stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
3362 defer stats.deinit();
3363 const index_stats = stats.index("items_region_payload").?;
3364 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes.len);
3365 try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes[0].field_count);
3366 try std.testing.expectEqual(@as(usize, 3), index_stats.distribution.prefixes[0].distinct_values);
3367 try std.testing.expectEqual(@as(usize, 4), index_stats.distribution.prefixes[0].max_equal);
3368 try std.testing.expect(index_stats.distribution.prefixes[0].samples.len > 0);
3369 for (index_stats.distribution.prefixes[0].samples, 0..) |sample, offset| {
3370 try std.testing.expectEqual(offset, sample.less_distinct);
3371 }
3372 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes[1].field_count);
3373 try std.testing.expectEqual(@as(usize, 6), index_stats.distribution.prefixes[1].distinct_values);
3374 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes[1].max_equal);
3375 try std.testing.expect(index_stats.distribution.prefixes[1].samples.len > 0);
3376 for (index_stats.distribution.prefixes[1].samples, 0..) |sample, offset| {
3377 try std.testing.expectEqual(offset, sample.less_distinct);
3378 }
3379 try std.testing.expectEqual(index_stats.distribution.prefixes[0].distinct_values, index_stats.distribution.distinct_values);
3380 try std.testing.expectEqual(index_stats.distribution.prefixes[0].max_equal, index_stats.distribution.max_equal);
3381 try database.syncWal();
3382 }
3383
3384 var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3385 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3386 .header = recoveredHeader(),
3387 });
3388 defer reopened.deinit();
3389
3390 var catalog = try Catalog.open(&reopened, .{});
3391 var stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
3392 defer stats.deinit();
3393 const index_stats = stats.index("items_region_payload").?;
3394 try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes.len);
3395 try std.testing.expectEqual(@as(usize, 3), index_stats.distribution.prefixes[0].distinct_values);
3396 try std.testing.expectEqual(@as(usize, 6), index_stats.distribution.prefixes[1].distinct_values);
3397 for (index_stats.distribution.prefixes[1].samples, 0..) |sample, offset| {
3398 try std.testing.expectEqual(offset, sample.less_distinct);
3399 }
3400 }
3401
3402 test "catalog rejects duplicate object names" {
3403 var tmp = std.testing.tmpDir(.{});
3404 defer tmp.cleanup();
3405
3406 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3407 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3408 .header = testingHeader(),
3409 });
3410 defer database.deinit();
3411 try database.reserve(.{ .wal_frames = 96 });
3412
3413 var catalog = try Catalog.open(&database, .{});
3414 _ = try catalog.createRelation(std.testing.allocator, .{
3415 .name = "items",
3416 }, .{ .durability = .buffered });
3417 try std.testing.expectError(error.ObjectExists, catalog.createRelation(std.testing.allocator, .{
3418 .name = "items",
3419 }, .{ .durability = .buffered }));
3420 try std.testing.expectError(error.RelationNotFound, catalog.openRelation(std.testing.allocator, "missing"));
3421 }
3422
3423 test "catalog allocates distinct roots for multiple relations" {
3424 var tmp = std.testing.tmpDir(.{});
3425 defer tmp.cleanup();
3426
3427 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3428 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3429 .header = testingHeader(),
3430 });
3431 defer database.deinit();
3432 try database.reserve(.{ .wal_frames = 160 });
3433
3434 var catalog = try Catalog.open(&database, .{});
3435 const indexes = [_]IndexDefinition{.{
3436 .name = "items_value",
3437 .fields = &.{0},
3438 }};
3439 const first = try catalog.createRelation(std.testing.allocator, .{
3440 .name = "items",
3441 .indexes = &indexes,
3442 }, .{ .durability = .buffered });
3443 try std.testing.expectEqual(@as(u64, 1), first.schema.version);
3444 const second = try catalog.createRelation(std.testing.allocator, .{
3445 .name = "users",
3446 }, .{ .durability = .buffered });
3447 try std.testing.expectEqual(@as(u64, 2), second.schema.version);
3448 try std.testing.expectEqual(@as(u64, 2), (try catalog.schemaState(std.testing.allocator)).version);
3449
3450 var items = try catalog.openRelation(std.testing.allocator, "items");
3451 defer items.deinit();
3452 var users = try catalog.openRelation(std.testing.allocator, "users");
3453 defer users.deinit();
3454
3455 try std.testing.expect(items.relation.table.rows.root_page > default_root_page);
3456 try std.testing.expect(users.relation.table.rows.root_page > items.relation.table.rows.root_page);
3457
3458 _ = try items.relation.put(std.testing.allocator, 1, &.{.{ .integer = 7 }}, .{ .durability = .buffered });
3459 _ = try users.relation.put(std.testing.allocator, 1, &.{.{ .text = "Ada" }}, .{ .durability = .buffered });
3460
3461 var lookup: index_mod.Scan = undefined;
3462 try items.relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 7 }});
3463 defer lookup.deinit();
3464 try std.testing.expectEqual(@as(i64, 1), (try lookup.next()).?.rowid);
3465 try std.testing.expect(try lookup.next() == null);
3466
3467 const bytes = (try users.relation.get(std.testing.allocator, 1)).?;
3468 defer std.testing.allocator.free(bytes);
3469 const view = try row.View.init(bytes);
3470 try std.testing.expectEqualStrings("Ada", (try view.column(0)).text);
3471 }
3472
3473 test "catalog rejects duplicate names inside one relation definition" {
3474 var tmp = std.testing.tmpDir(.{});
3475 defer tmp.cleanup();
3476
3477 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3478 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3479 .header = testingHeader(),
3480 });
3481 defer database.deinit();
3482
3483 var catalog = try Catalog.open(&database, .{});
3484 const table_named_index = [_]IndexDefinition{.{
3485 .name = "items",
3486 .fields = &.{0},
3487 }};
3488 try std.testing.expectError(error.ObjectExists, catalog.createRelation(std.testing.allocator, .{
3489 .name = "items",
3490 .indexes = &table_named_index,
3491 }, .{ .durability = .buffered }));
3492
3493 const duplicate_indexes = [_]IndexDefinition{
3494 .{
3495 .name = "items_value",
3496 .fields = &.{0},
3497 },
3498 .{
3499 .name = "items_value",
3500 .fields = &.{1},
3501 },
3502 };
3503 try std.testing.expectError(error.ObjectExists, catalog.createRelation(std.testing.allocator, .{
3504 .name = "items",
3505 .indexes = &duplicate_indexes,
3506 }, .{ .durability = .buffered }));
3507
3508 const duplicate_columns = [_]ColumnDefinition{
3509 .{ .name = "name" },
3510 .{ .name = "Name" },
3511 };
3512 try std.testing.expectError(error.CatalogCorrupt, catalog.createRelation(std.testing.allocator, .{
3513 .name = "items",
3514 .columns = &duplicate_columns,
3515 }, .{ .durability = .buffered }));
3516 }
3517
3518 test "catalog diagnoses pre identity object rows as unsupported format" {
3519 var tmp = std.testing.tmpDir(.{});
3520 defer tmp.cleanup();
3521
3522 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3523 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3524 .header = testingHeader(),
3525 });
3526 defer database.deinit();
3527 try database.reserve(.{ .wal_frames = 96 });
3528
3529 try forgeCatalog(&database, .{ .format = format_version, .version = 1 }, &.{.{ .rowid = first_object_rowid, .values = &.{
3530 .{ .integer = relation_kind },
3531 .{ .text = "items" },
3532 .{ .text = "items" },
3533 .{ .integer = 3 },
3534 .{ .blob = "" },
3535 .{ .blob = "" },
3536 } }});
3537
3538 var catalog = try Catalog.open(&database, .{});
3539 try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));
3540 try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.openRelation(std.testing.allocator, "items"));
3541 }
3542
3543 test "catalog rejects newer schema format before object rows decode" {
3544 var tmp = std.testing.tmpDir(.{});
3545 defer tmp.cleanup();
3546
3547 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3548 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3549 .header = testingHeader(),
3550 });
3551 defer database.deinit();
3552 try database.reserve(.{ .wal_frames = 96 });
3553
3554 try forgeCatalog(&database, .{ .format = format_version + 1, .version = 1 }, &.{.{ .rowid = first_object_rowid, .values = &.{
3555 .{ .integer = 99 },
3556 .{ .text = "items" },
3557 .{ .text = "items" },
3558 .{ .integer = 3 },
3559 .{ .blob = "" },
3560 .{ .blob = "" },
3561 .{ .integer = 0 },
3562 } }});
3563
3564 var catalog = try Catalog.open(&database, .{});
3565 try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));
3566 }
3567
3568 test "catalog diagnoses pre identity stats rows as unsupported format" {
3569 var tmp = std.testing.tmpDir(.{});
3570 defer tmp.cleanup();
3571
3572 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3573 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3574 .header = testingHeader(),
3575 });
3576 defer database.deinit();
3577 try database.reserve(.{ .wal_frames = 96 });
3578
3579 try forgeCatalog(&database, .{ .format = format_version, .version = 1 }, &.{.{ .rowid = first_object_rowid, .values = &.{
3580 .{ .integer = stats_kind },
3581 .{ .text = "items" },
3582 .{ .text = "items" },
3583 .{ .integer = 3 },
3584 .{ .blob = "" },
3585 .{ .blob = "" },
3586 } }});
3587
3588 var catalog = try Catalog.open(&database, .{});
3589 try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));
3590 }
3591
3592 test "catalog diagnoses schema row arity drift as unsupported format" {
3593 var tmp = std.testing.tmpDir(.{});
3594 defer tmp.cleanup();
3595
3596 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
3597 .paths = .{ .database = "catalog.db", .wal = "catalog.wal" },
3598 .header = testingHeader(),
3599 });
3600 defer database.deinit();
3601 try database.reserve(.{ .wal_frames = 96 });
3602
3603 const schema_blob = @as([schema_blob_bytes]u8, @splat(0));
3604 try forgeCatalog(&database, null, &.{.{ .rowid = schema_rowid, .values = &.{
3605 .{ .integer = schema_kind },
3606 .{ .text = schema_name },
3607 .{ .text = "" },
3608 .{ .integer = 0 },
3609 .{ .blob = &schema_blob },
3610 .{ .blob = "" },
3611 .{ .integer = 0 },
3612 } }});
3613
3614 var catalog = try Catalog.open(&database, .{});
3615 try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));
3616 }
3617
3618 const RawCatalogRow = struct {
3619 rowid: i64,
3620 values: []const row.Value,
3621 };
3622
3623 fn forgeCatalog(database: *file.Database, catalog_schema: ?Schema, rows: []const RawCatalogRow) !void {
3624 const space = try space_mod.Space.open(database, .{
3625 .meta_page = default_meta_page,
3626 .roots = &.{.{ .root_page = default_root_page }},
3627 });
3628 var schema = try space.rowidTable(default_root_page);
3629 var write = try space.beginWrite();
3630 defer write.deinit();
3631 if (catalog_schema) |value| try putSchemaRow(&schema, &write, value);
3632 for (rows) |raw| try schema.putIn(&write, raw.rowid, raw.values);
3633 _ = try write.commit(.{ .durability = .buffered });
3634 }
3635
3636 fn testingHeader() wal.Header {
3637 return .{
3638 .sequence = 1101,
3639 .salt = .{ .first = 0x9191_c3c3, .second = 0x6363_d4d4 },
3640 };
3641 }
3642
3643 fn recoveredHeader() wal.Header {
3644 return .{
3645 .sequence = 1102,
3646 .salt = .{ .first = 0xaaaa_5555, .second = 0xbbbb_6666 },
3647 };
3648 }