tiny.sql.catalog
Defined in tiny.sql.
API (38)
Actions
Public operations.
Materialization.clearRelationStatsMaterialization.commitMaterialization.createRelationMaterialization.deinitMaterialization.dropRelationMaterialization.refreshRelationStatsMaterialization.treeWritePreparedRelationStats.deinitvalidateDefinition
Types and contracts
Public types and contracts.
CatalogColumnDefinitionCommitErrorIndexDefinitionIndexDistributionIndexPrefixDistributionIndexSampleIndexStatsMaterializationMaterializedRelationOptionsPreparedRelationStatsReadRelationHandleReadRelationState: The schema state, read handle and statistics of one relation.ReaderRelationDefinitionRelationHandleRelationNamesRelationState: The schema state, handle and statistics of one relation.RelationStatsSchema
Values and defaults
Public values and defaults.
default_meta_pagedefault_root_pageformat_versionmax_column_default_bytesmax_column_name_bytesmax_columnsmax_index_distribution_samples
Source
Source: lib/sql/src/catalog.zig
zig
const std = @import("std");const simd = @import("simd");const file = @import("file.zig");const index_mod = @import("index.zig");const key = @import("key.zig");const page = @import("page.zig");const relation_mod = @import("relation.zig");const row = @import("row.zig");const space_mod = @import("space.zig");const table = @import("table.zig");const trace = @import("trace.zig");const tree = @import("tree.zig");const wal = @import("wal.zig");const Bytes = simd.ScalableTag(u8);const Allocator = std.mem.Allocator;const CatalogError = error{ CatalogCorrupt, ObjectExists, RelationNotFound, SchemaVersionOverflow, TooManyColumns, UnsupportedCatalogFormat,};pub const Error = relation_mod.Error || table.Error || CatalogError;pub const default_meta_page: u32 = 1;pub const default_root_page: u32 = 2;pub const format_version: u32 = 1;const schema_kind: i64 = 0;const relation_kind: i64 = 1;const index_kind: i64 = 2;const stats_kind: i64 = 3;const schema_rowid: i64 = 0;const first_object_rowid: i64 = 1;const schema_row_columns: usize = 6;const catalog_row_columns: usize = 7;const schema_blob_bytes = 12;const summary_blob_bytes = 80;const distribution_header_bytes = 6;const distribution_prefix_header_bytes = 20;const distribution_sample_header_bytes = 26;const field_bytes = 4;const column_name_len_bytes = 2;const default_len_bytes = 2;const schema_name = "schema";const distribution_format: u32 = 3;pub const max_columns: usize = 64;pub const max_column_name_bytes: usize = page.size;pub const max_column_default_bytes: usize = page.size;pub const max_index_distribution_samples: usize = 32;pub const Options = struct { meta_page: u32 = default_meta_page, root_page: u32 = default_root_page,};pub const Schema = struct { format: u32 = format_version, version: u64 = 0,};pub const Commit = struct { storage: file.Commit, schema: Schema,};pub const IndexDefinition = struct { name: []const u8, fields: []const usize, columns: []const row.Column = &.{},};pub const ColumnDefinition = struct { name: []const u8, column: row.Column = .{}, default: row.Value = .nil,};pub const RelationDefinition = struct { name: []const u8, columns: []const ColumnDefinition = &.{}, indexes: []const IndexDefinition = &.{},};fn RelationHandleType(comptime Relation: type) type { return struct { allocator: Allocator, relation: Relation, specs: []relation_mod.IndexSpec, index_definitions: []IndexDefinition, fields: []usize, columns: []row.Column, definitions: []ColumnDefinition, index_names: []u8, names: []u8, defaults: []u8, pub fn deinit(self: *@This()) void { self.allocator.free(self.defaults); self.allocator.free(self.names); self.allocator.free(self.index_names); self.allocator.free(self.definitions); self.allocator.free(self.columns); self.allocator.free(self.fields); self.allocator.free(self.index_definitions); self.allocator.free(self.specs); self.* = undefined; } };}pub const RelationHandle = RelationHandleType(relation_mod.Relation);pub const ReadRelationHandle = RelationHandleType(relation_mod.Reader);pub const IndexStats = struct { name: []u8, root_page: u32, summary: tree.Summary, distribution: IndexDistribution = .{},};pub const IndexDistribution = struct { distinct_values: usize = 0, max_equal: usize = 0, samples: []IndexSample = &.{}, sample_keys: []u8 = &.{}, prefixes: []IndexPrefixDistribution = &.{},};pub const IndexPrefixDistribution = struct { field_count: usize = 0, distinct_values: usize = 0, max_equal: usize = 0, samples: []IndexSample = &.{}, sample_keys: []u8 = &.{},};pub const IndexSample = struct { key: []const u8, less_than: usize, equal_count: usize, less_distinct: usize,};pub const RelationStats = struct { allocator: Allocator, table_root_page: u32, table: tree.Summary, indexes: []IndexStats, pub fn deinit(self: *RelationStats) void { for (self.indexes) |*index_stats| { self.allocator.free(index_stats.name); freeIndexDistribution(self.allocator, index_stats.distribution); } self.allocator.free(self.indexes); self.* = undefined; } pub fn index(self: *const RelationStats, name: []const u8) ?*const IndexStats { for (self.indexes) |*index_stats| { if (std.mem.eql(u8, index_stats.name, name)) return index_stats; } return null; }};pub const PreparedRelationStats = struct { allocator: Allocator, stats: RelationStats, distribution_blobs: [][]u8, pub fn deinit(self: *PreparedRelationStats) void { for (self.distribution_blobs) |bytes| { if (bytes.len != 0) self.allocator.free(bytes); } self.allocator.free(self.distribution_blobs); self.stats.deinit(); self.* = undefined; }};const PreparedIndexStats = struct { stats: IndexStats, distribution_blob: []u8,};pub const MaterializedRelation = struct { relation: relation_mod.Relation, table_root: space_mod.RootSpec, index_roots: [relation_mod.max_indexes]space_mod.RootSpec = undefined, index_count: usize = 0, table_stats_rowid: ?i64 = null, index_stats_rowids: [relation_mod.max_indexes]i64 = undefined,};pub const Materialization = struct { allocator: Allocator, catalog: *const Catalog, roots: [space_mod.max_roots]space_mod.RootSpec, root_count: usize, schema: table.Table, write: tree.Write, current_schema: Schema, next_rowid: i64, schema_dirty: bool = false, dropped_relations: std.ArrayList([]u8) = .empty, created_objects: std.ArrayList([]u8) = .empty, pub fn deinit(self: *Materialization) void { for (self.created_objects.items) |name| self.allocator.free(name); self.created_objects.deinit(self.allocator); for (self.dropped_relations.items) |name| self.allocator.free(name); self.dropped_relations.deinit(self.allocator); self.write.deinit(); self.* = undefined; } pub fn treeWrite(self: *Materialization) *tree.Write { return &self.write; } pub fn dropRelation(self: *Materialization, name: []const u8) Error!void { if (self.relationDropped(name)) return error.RelationNotFound; const owned_name = try self.allocator.dupe(u8, name); errdefer self.allocator.free(owned_name); try self.dropped_relations.ensureUnusedCapacity(self.allocator, 1); var found = false; var state = CatalogState{}; var scan: table.Scan = undefined; try self.catalog.schema.scan(&scan, self.allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| { if (std.mem.eql(u8, object.table_name, name)) { if (object.kind == .relation) found = true; try self.schema.deleteIn(&self.write, entry.rowid); self.removeRoot(object.root_page); } }, .stats => |stats| { if (std.mem.eql(u8, stats.table_name, name)) { try self.schema.deleteIn(&self.write, entry.rowid); } }, } } _ = try state.schemaOrDefault(); if (!found) return error.RelationNotFound; self.dropped_relations.appendAssumeCapacity(owned_name); self.schema_dirty = true; } pub fn clearRelationStats(self: *Materialization, name: []const u8) Error!void { var found = false; var state = CatalogState{}; var scan: table.Scan = undefined; try self.catalog.schema.scan(&scan, self.allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| { if (object.kind == .relation and std.mem.eql(u8, object.name, name)) { found = true; } }, .stats => |stats| { if (std.mem.eql(u8, stats.table_name, name)) { try self.schema.deleteIn(&self.write, entry.rowid); } }, } } _ = try state.schemaOrDefault(); if (!found) return error.RelationNotFound; } pub fn createRelation( self: *Materialization, definition: RelationDefinition, prepared_stats: ?*const PreparedRelationStats, ) Error!MaterializedRelation { try self.prepareRelationCreation(definition, prepared_stats); var materialized = try self.allocateMaterializedRelation(definition); try self.writeRelationCatalog(definition, &materialized, prepared_stats); materialized.relation = try self.openMaterializedRelation( definition, &materialized, ); self.schema_dirty = true; return materialized; } fn prepareRelationCreation( self: *Materialization, definition: RelationDefinition, prepared_stats: ?*const PreparedRelationStats, ) Error!void { try validateDefinition(definition); if (self.root_count + 1 + definition.indexes.len > self.roots.len) { return error.TooManyRoots; } const stats_rows = if (prepared_stats == null) @as(usize, 0) else 1 + definition.indexes.len; const catalog_rows = 1 + definition.indexes.len + stats_rows; _ = std.math.add( i64, self.next_rowid, @intCast(catalog_rows), ) catch return error.CatalogCorrupt; try self.ensureObjectNameAvailable(definition.name); for (definition.indexes) |index_definition| { try self.ensureObjectNameAvailable(index_definition.name); } if (prepared_stats) |prepared| try validatePreparedStats(definition, prepared); try self.retainCreatedObject(definition.name); for (definition.indexes) |index_definition| { try self.retainCreatedObject(index_definition.name); } } fn allocateMaterializedRelation( self: *Materialization, definition: RelationDefinition, ) Error!MaterializedRelation { const table_root = try allocateCatalogRoot(&self.write, &self.roots, &self.root_count); var index_roots: [relation_mod.max_indexes]space_mod.RootSpec = undefined; for (definition.indexes, 0..) |_, index_offset| { index_roots[index_offset] = try allocateCatalogRoot( &self.write, &self.roots, &self.root_count, ); } return .{ .relation = undefined, .table_root = table_root, .index_roots = index_roots, .index_count = definition.indexes.len, }; } fn writeRelationCatalog( self: *Materialization, definition: RelationDefinition, materialized: *MaterializedRelation, prepared_stats: ?*const PreparedRelationStats, ) Error!void { var definitions_buffer: [page.size]u8 = undefined; const definitions = try encodeDefinitions(&definitions_buffer, definition.columns); try putCatalogRow( &self.schema, &self.write, self.takeRowid(), relation_kind, definition.name, definition.name, materialized.table_root.root_page, materialized.table_root.identity_page, definitions, "", ); try self.writeIndexCatalog(definition, materialized); if (prepared_stats) |prepared| { try self.writePreparedStatsCatalog(definition, materialized, prepared); } } fn writeIndexCatalog( self: *Materialization, definition: RelationDefinition, materialized: *const MaterializedRelation, ) Error!void { for ( definition.indexes, materialized.index_roots[0..materialized.index_count], ) |index_definition, index_root| { var fields_buffer: [relation_mod.max_index_fields * field_bytes]u8 = undefined; var collations_buffer: [relation_mod.max_index_fields]u8 = undefined; const fields = try encodeFields(&fields_buffer, index_definition.fields); const collations = try encodeCollations( &collations_buffer, index_definition.fields.len, index_definition.columns, ); try putCatalogRow( &self.schema, &self.write, self.takeRowid(), index_kind, index_definition.name, definition.name, index_root.root_page, index_root.identity_page, fields, collations, ); } } fn writePreparedStatsCatalog( self: *Materialization, definition: RelationDefinition, materialized: *MaterializedRelation, prepared: *const PreparedRelationStats, ) Error!void { var summary_buffer: [summary_blob_bytes]u8 = undefined; materialized.table_stats_rowid = self.takeRowid(); try putCatalogRow( &self.schema, &self.write, materialized.table_stats_rowid.?, stats_kind, definition.name, definition.name, materialized.table_root.root_page, 0, try encodeSummary(&summary_buffer, prepared.stats.table), "", ); for ( definition.indexes, materialized.index_roots[0..materialized.index_count], prepared.stats.indexes, prepared.distribution_blobs, 0.., ) |index_definition, index_root, index_stats, distribution_blob, index_offset| { materialized.index_stats_rowids[index_offset] = self.takeRowid(); try putCatalogRow( &self.schema, &self.write, materialized.index_stats_rowids[index_offset], stats_kind, index_definition.name, definition.name, index_root.root_page, 0, try encodeSummary(&summary_buffer, index_stats.summary), distribution_blob, ); } } fn openMaterializedRelation( self: *Materialization, definition: RelationDefinition, materialized: *const MaterializedRelation, ) Error!relation_mod.Relation { const relation_space = try space_mod.Space.open(self.catalog.database, .{ .meta_page = self.catalog.meta_page, .roots = self.roots[0..self.root_count], .reserved_page_max = self.write.reserved_page_max, }); var specs: [relation_mod.max_indexes]relation_mod.IndexSpec = undefined; for ( definition.indexes, materialized.index_roots[0..materialized.index_count], specs[0..definition.indexes.len], ) |index_definition, index_root, *spec| { spec.* = .{ .root_page = index_root.root_page, .fields = index_definition.fields, .columns = index_definition.columns, }; } return try relation_mod.Relation.open(&relation_space, .{ .table_root = materialized.table_root.root_page, .indexes = specs[0..definition.indexes.len], }); } pub fn refreshRelationStats( self: *Materialization, definition: RelationDefinition, materialized: *const MaterializedRelation, prepared: *const PreparedRelationStats, ) Error!void { try validatePreparedStats(definition, prepared); if (materialized.index_count != definition.indexes.len) return error.CatalogCorrupt; const table_rowid = materialized.table_stats_rowid orelse return error.CatalogCorrupt; const table_summary = try materialized.relation.table.summarizeIn(&self.write); if (!sameLogicalSummary(table_summary, prepared.stats.table)) return error.CatalogCorrupt; var summary_buffer: [summary_blob_bytes]u8 = undefined; try putCatalogRow( &self.schema, &self.write, table_rowid, stats_kind, definition.name, definition.name, materialized.table_root.root_page, 0, try encodeSummary(&summary_buffer, table_summary), "", ); for ( definition.indexes, materialized.index_roots[0..materialized.index_count], prepared.stats.indexes, prepared.distribution_blobs, 0.., ) |index_definition, index_root, index_stats, distribution_blob, index_offset| { var relation_index = try materialized.relation.space.index( index_root.root_page, index_definition.columns, ); const index_summary = try relation_index.summarizeIn(&self.write); if (!sameLogicalSummary(index_summary, index_stats.summary)) { return error.CatalogCorrupt; } try putCatalogRow( &self.schema, &self.write, materialized.index_stats_rowids[index_offset], stats_kind, index_definition.name, definition.name, index_root.root_page, 0, try encodeSummary(&summary_buffer, index_summary), distribution_blob, ); } } pub fn commit(self: *Materialization, options: file.CommitOptions) Error!Commit { const next_schema = if (self.schema_dirty) try bumpSchemaVersion(self.current_schema) else self.current_schema; if (self.schema_dirty) try putSchemaRow(&self.schema, &self.write, next_schema); return .{ .storage = try self.write.commit(options), .schema = next_schema, }; } fn relationDropped(self: *const Materialization, name: []const u8) bool { for (self.dropped_relations.items) |dropped| { if (std.mem.eql(u8, dropped, name)) return true; } return false; } fn objectCreated(self: *const Materialization, name: []const u8) bool { for (self.created_objects.items) |created| { if (std.mem.eql(u8, created, name)) return true; } return false; } fn ensureObjectNameAvailable(self: *const Materialization, name: []const u8) Error!void { if (self.objectCreated(name)) return error.ObjectExists; var state = CatalogState{}; var scan: table.Scan = undefined; try self.catalog.schema.scan(&scan, self.allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema, .stats => {}, .object => |object| { if (std.mem.eql(u8, object.name, name) and !self.relationDropped(object.table_name)) { return error.ObjectExists; } }, } } _ = try state.schemaOrDefault(); } fn retainCreatedObject(self: *Materialization, name: []const u8) Error!void { const owned_name = try self.allocator.dupe(u8, name); errdefer self.allocator.free(owned_name); try self.created_objects.append(self.allocator, owned_name); } fn removeRoot(self: *Materialization, root_page: u32) void { var index: usize = 0; while (index < self.root_count) : (index += 1) { if (self.roots[index].root_page != root_page) continue; self.root_count -= 1; self.roots[index] = self.roots[self.root_count]; return; } unreachable; } fn takeRowid(self: *Materialization) i64 { const rowid = self.next_rowid; self.next_rowid += 1; return rowid; }};pub const RelationNames = struct { allocator: Allocator, names: [][]u8, pub fn deinit(self: *RelationNames) void { for (self.names) |name| self.allocator.free(name); self.allocator.free(self.names); self.* = undefined; }};pub const Reader = struct { snapshot: file.Snapshot, meta_page: u32, root_page: u32, schema: table.Reader, pub fn open(snapshot: file.Snapshot, options: Options) Error!Reader { try validateOptions(options); return .{ .snapshot = snapshot, .meta_page = options.meta_page, .root_page = options.root_page, .schema = try table.Reader.open(snapshot, .{ .tree = catalogTreeOptions(options), }), }; } pub fn schemaState(self: *const Reader, allocator: Allocator) Error!Schema { return try readSchemaState(&self.schema, allocator); } pub fn openRelation( self: *const Reader, allocator: Allocator, name: []const u8, ) Error!ReadRelationHandle { return try openRelationFrom( ReadRelationHandle, space_mod.Reader, relation_mod.Reader, self.snapshot, &self.schema, self.meta_page, self.root_page, allocator, name, ); } pub fn relationNames(self: *const Reader, allocator: Allocator) Error!RelationNames { return try readRelationNames(&self.schema, allocator); } pub fn relationStats( self: *const Reader, allocator: Allocator, name: []const u8, ) Error!?RelationStats { return try readRelationStats(&self.schema, allocator, name); } /// Returns what `schemaState`, `openRelation` and `relationStats` return /// for `name`, read in one catalog pass. pub fn readRelation( self: *const Reader, allocator: Allocator, name: []const u8, ) Error!ReadRelationState { return try readRelationFrom( ReadRelationHandle, space_mod.Reader, relation_mod.Reader, self.snapshot, &self.schema, self.meta_page, self.root_page, allocator, name, ); }};pub const Catalog = struct { database: *file.Database, meta_page: u32, root_page: u32, schema: table.Table, pub fn open(database: *file.Database, options: Options) Error!Catalog { try validateOptions(options); return .{ .database = database, .meta_page = options.meta_page, .root_page = options.root_page, .schema = try table.Table.open(database, .{ .tree = catalogTreeOptions(options), }), }; } pub fn schemaState(self: *const Catalog, allocator: Allocator) Error!Schema { return try readSchemaState(&self.schema, allocator); } pub fn beginMaterialization(self: *const Catalog, allocator: Allocator) Error!Materialization { var roots: [space_mod.max_roots]space_mod.RootSpec = undefined; var root_count: usize = 0; try addRoot(&roots, &root_count, .{ .root_page = self.root_page }); var state = CatalogState{}; var scan: table.Scan = undefined; try self.schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema, .stats => {}, .object => |object| try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page, }), } } const current_schema = try state.schemaOrDefault(); if (state.max_rowid == std.math.maxInt(i64)) return error.CatalogCorrupt; const space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var write = try space.beginWrite(); errdefer write.deinit(); return .{ .allocator = allocator, .catalog = self, .roots = roots, .root_count = root_count, .schema = try space.rowidTable(self.root_page), .write = write, .current_schema = current_schema, .next_rowid = @max(state.max_rowid + 1, first_object_rowid), }; } pub fn prepareRelationStats( _: *const Catalog, allocator: Allocator, definition: RelationDefinition, puts: []const relation_mod.Edit.Put, table_summary: tree.Summary, index_summaries: []const tree.Summary, ) Error!PreparedRelationStats { try validateDefinition(definition); if (definition.indexes.len != index_summaries.len) return error.CatalogCorrupt; var previous_rowid: ?i64 = null; for (puts) |put| { _ = try row.View.init(put.bytes); if (previous_rowid) |previous| { if (put.rowid <= previous) return error.CatalogCorrupt; } previous_rowid = put.rowid; } const indexes = try allocator.alloc(IndexStats, definition.indexes.len); var index_count: usize = 0; errdefer { for (indexes[0..index_count]) |*index_stats| { allocator.free(index_stats.name); freeIndexDistribution(allocator, index_stats.distribution); } allocator.free(indexes); } const distribution_blobs = try allocator.alloc([]u8, definition.indexes.len); var blob_count: usize = 0; errdefer { for (distribution_blobs[0..blob_count]) |bytes| { if (bytes.len != 0) allocator.free(bytes); } allocator.free(distribution_blobs); } for ( definition.indexes, index_summaries, indexes, distribution_blobs, 0.., ) |index_definition, summary, *index_stats, *distribution_blob, index_offset| { const prepared = try prepareIndexStats( allocator, index_definition, puts, summary, index_offset, ); index_stats.* = prepared.stats; distribution_blob.* = prepared.distribution_blob; index_count += 1; blob_count += 1; } return .{ .allocator = allocator, .stats = .{ .allocator = allocator, .table_root_page = 0, .table = table_summary, .indexes = indexes, }, .distribution_blobs = distribution_blobs, }; } pub fn createRelation(self: *const Catalog, allocator: Allocator, definition: RelationDefinition, options: file.CommitOptions) Error!Commit { const phase = trace.scope("catalog.create_relation"); defer phase.end(); try validateDefinition(definition); var roots: [space_mod.max_roots]space_mod.RootSpec = undefined; var root_count: usize = 0; try addRoot(&roots, &root_count, .{ .root_page = self.root_page }); var state = CatalogState{}; { var scan: table.Scan = undefined; try self.schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| { if (std.mem.eql(u8, object.name, definition.name)) return error.ObjectExists; for (definition.indexes) |index_definition| { if (std.mem.eql(u8, object.name, index_definition.name)) return error.ObjectExists; } try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); }, .stats => {}, } } } const next_schema = try bumpSchemaVersion(try state.schemaOrDefault()); const space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var schema = try space.rowidTable(self.root_page); var write = try space.beginWrite(); defer write.deinit(); const table_root = try allocateCatalogRoot(&write, &roots, &root_count); var index_roots: [relation_mod.max_indexes]space_mod.RootSpec = undefined; for (definition.indexes, 0..) |_, index_offset| { index_roots[index_offset] = try allocateCatalogRoot(&write, &roots, &root_count); } try putSchemaRow(&schema, &write, next_schema); var definitions_buffer: [page.size]u8 = undefined; const definitions = try encodeDefinitions(&definitions_buffer, definition.columns); var next_rowid = @max(state.max_rowid + 1, first_object_rowid); try putCatalogRow(&schema, &write, next_rowid, relation_kind, definition.name, definition.name, table_root.root_page, table_root.identity_page, definitions, ""); next_rowid += 1; for (definition.indexes, 0..) |index_definition, index_offset| { var fields_buffer: [relation_mod.max_index_fields * field_bytes]u8 = undefined; var collations_buffer: [relation_mod.max_index_fields]u8 = undefined; const fields = try encodeFields(&fields_buffer, index_definition.fields); const collations = try encodeCollations(&collations_buffer, index_definition.fields.len, index_definition.columns); try putCatalogRow( &schema, &write, next_rowid, index_kind, index_definition.name, definition.name, index_roots[index_offset].root_page, index_roots[index_offset].identity_page, fields, collations, ); next_rowid += 1; } return .{ .storage = try write.commit(options), .schema = next_schema, }; } pub fn createIndex(self: *const Catalog, allocator: Allocator, table_name: []const u8, definition: IndexDefinition, options: file.CommitOptions) Error!Commit { const phase = trace.scope("catalog.create_index"); defer phase.end(); if (definition.name.len == 0) return error.CatalogCorrupt; if (definition.fields.len == 0 or definition.fields.len > relation_mod.max_index_fields) return error.TooManyIndexFields; if (definition.columns.len > definition.fields.len) return error.CatalogCorrupt; var roots: [space_mod.max_roots]space_mod.RootSpec = undefined; var root_count: usize = 0; try addRoot(&roots, &root_count, .{ .root_page = self.root_page }); var table_root: ?u32 = null; var table_definitions: [max_columns]ColumnDefinition = undefined; var table_names: [max_column_name_bytes]u8 = undefined; var table_defaults: [max_column_default_bytes]u8 = undefined; var table_definition_count: usize = 0; var table_index_count: usize = 0; var old_stats: std.ArrayList(i64) = .empty; defer old_stats.deinit(allocator); var state = CatalogState{}; var scan: table.Scan = undefined; try self.schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| switch (object.kind) { .relation => { if (std.mem.eql(u8, object.name, definition.name)) return error.ObjectExists; try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.name, table_name)) { if (table_root != null) return error.CatalogCorrupt; table_root = object.root_page; table_definition_count = object.definitions.len; _ = try copyDefinitions(&table_definitions, &table_names, &table_defaults, object.definitions); } }, .index => { if (std.mem.eql(u8, object.name, definition.name)) return error.ObjectExists; try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.table_name, table_name)) table_index_count += 1; }, .schema => unreachable, .stats => unreachable, }, .stats => |stats| { if (std.mem.eql(u8, stats.table_name, table_name)) try old_stats.append(allocator, entry.rowid); }, } } const next_schema = try bumpSchemaVersion(try state.schemaOrDefault()); const root_page = table_root orelse return error.RelationNotFound; if (table_index_count >= relation_mod.max_indexes) return error.TooManyIndexes; for (definition.fields) |field| { if (field >= table_definition_count) return error.ColumnOutOfBounds; } const space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var table_rows = try space.rowidTable(root_page); var schema = try space.rowidTable(self.root_page); var write = try space.beginWrite(); defer write.deinit(); const index_root = try allocateCatalogRoot(&write, &roots, &root_count); const index_space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var relation_index = try index_space.index(index_root.root_page, definition.columns); var table_scan: table.Scan = undefined; try table_rows.scan(&table_scan, allocator, null, null); defer table_scan.deinit(); while (try table_scan.next()) |entry| { const view = try entry.view(); var projected: [relation_mod.max_index_fields]row.Value = undefined; try relation_index.putIn(&write, entry.rowid, try view.project(definition.fields, &projected)); } for (old_stats.items) |rowid| try schema.deleteIn(&write, rowid); try putSchemaRow(&schema, &write, next_schema); var fields_buffer: [relation_mod.max_index_fields * field_bytes]u8 = undefined; var collations_buffer: [relation_mod.max_index_fields]u8 = undefined; const fields = try encodeFields(&fields_buffer, definition.fields); const collations = try encodeCollations(&collations_buffer, definition.fields.len, definition.columns); try putCatalogRow( &schema, &write, @max(state.max_rowid + 1, first_object_rowid), index_kind, definition.name, table_name, index_root.root_page, index_root.identity_page, fields, collations, ); return .{ .storage = try write.commit(options), .schema = next_schema, }; } pub fn dropRelation(self: *const Catalog, allocator: Allocator, name: []const u8, options: file.CommitOptions) Error!Commit { const phase = trace.scope("catalog.drop_relation"); defer phase.end(); var roots: [space_mod.max_roots]space_mod.RootSpec = undefined; var root_count: usize = 0; try addRoot(&roots, &root_count, .{ .root_page = self.root_page }); var rowids: std.ArrayList(i64) = .empty; defer rowids.deinit(allocator); var found_relation = false; var state = CatalogState{}; var scan: table.Scan = undefined; try self.schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| switch (object.kind) { .relation => { try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.name, name)) { if (found_relation) return error.CatalogCorrupt; found_relation = true; try rowids.append(allocator, entry.rowid); } }, .index => { try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.table_name, name)) try rowids.append(allocator, entry.rowid); }, .schema => unreachable, .stats => unreachable, }, .stats => |stats| { if (std.mem.eql(u8, stats.table_name, name)) try rowids.append(allocator, entry.rowid); }, } } const next_schema = try bumpSchemaVersion(try state.schemaOrDefault()); if (!found_relation) return error.RelationNotFound; const space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var schema = try space.rowidTable(self.root_page); var write = try space.beginWrite(); defer write.deinit(); for (rowids.items) |rowid| try schema.deleteIn(&write, rowid); try putSchemaRow(&schema, &write, next_schema); return .{ .storage = try write.commit(options), .schema = next_schema, }; } pub fn openRelation( self: *const Catalog, allocator: Allocator, name: []const u8, ) Error!RelationHandle { return try openRelationFrom( RelationHandle, space_mod.Space, relation_mod.Relation, self.database, &self.schema, self.meta_page, self.root_page, allocator, name, ); } pub fn relationNames(self: *const Catalog, allocator: Allocator) Error!RelationNames { return try readRelationNames(&self.schema, allocator); } pub fn analyzeRelation(self: *const Catalog, allocator: Allocator, name: []const u8, options: file.CommitOptions) Error!Commit { const phase = trace.scope("catalog.analyze_relation"); defer phase.end(); var roots: [space_mod.max_roots]space_mod.RootSpec = undefined; var root_count: usize = 0; try addRoot(&roots, &root_count, .{ .root_page = self.root_page }); var table_root: ?u32 = null; var indexes: std.ArrayList(AnalyzeIndex) = .empty; defer indexes.deinit(allocator); defer freeAnalyzeIndexes(allocator, indexes.items); var old_stats: std.ArrayList(i64) = .empty; defer old_stats.deinit(allocator); var state = CatalogState{}; var scan: table.Scan = undefined; try self.schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| switch (object.kind) { .relation => { try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.name, name)) table_root = object.root_page; }, .index => { try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.table_name, name)) { if (indexes.items.len >= relation_mod.max_indexes) return error.TooManyIndexes; const owned_name = try allocator.dupe(u8, object.name); var analyzed_index = AnalyzeIndex{ .name = owned_name, .root_page = object.root_page, .field_count = object.fields.len, }; @memcpy(analyzed_index.fields[0..object.fields.len], object.fields); @memcpy(analyzed_index.columns[0..object.columns.len], object.columns); indexes.append(allocator, analyzed_index) catch |err| { allocator.free(owned_name); return err; }; } }, .schema => unreachable, .stats => unreachable, }, .stats => |stats| { if (std.mem.eql(u8, stats.table_name, name)) try old_stats.append(allocator, entry.rowid); }, } } const current_schema = try state.schemaOrDefault(); const root_page = table_root orelse return error.RelationNotFound; const space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var schema = try space.rowidTable(self.root_page); var relation_table = try space.rowidTable(root_page); const table_summary = try relation_table.summarize(); var index_summaries: [relation_mod.max_indexes]tree.Summary = undefined; for (indexes.items, 0..) |index_object, index_offset| { var relation_index = try space.index(index_object.root_page, index_object.indexColumns()); index_summaries[index_offset] = try relation_index.summarize(); indexes.items[index_offset].distribution_blob = try analyzeIndexDistribution(allocator, &relation_index); } var write = try space.beginWrite(); defer write.deinit(); for (old_stats.items) |rowid| try schema.deleteIn(&write, rowid); var summary_buffer: [summary_blob_bytes]u8 = undefined; var next_rowid = @max(state.max_rowid + 1, first_object_rowid); try putCatalogRow( &schema, &write, next_rowid, stats_kind, name, name, root_page, 0, try encodeSummary(&summary_buffer, table_summary), "", ); next_rowid += 1; for (indexes.items, 0..) |index_object, index_offset| { try putCatalogRow( &schema, &write, next_rowid, stats_kind, index_object.name, name, index_object.root_page, 0, try encodeSummary(&summary_buffer, index_summaries[index_offset]), index_object.distribution_blob, ); next_rowid += 1; } return .{ .storage = try write.commit(options), .schema = current_schema, }; } pub fn clearRelationStats(self: *const Catalog, allocator: Allocator, name: []const u8, options: file.CommitOptions) Error!?Commit { const phase = trace.scope("catalog.clear_relation_stats"); defer phase.end(); var roots: [space_mod.max_roots]space_mod.RootSpec = undefined; var root_count: usize = 0; try addRoot(&roots, &root_count, .{ .root_page = self.root_page }); var old_stats: std.ArrayList(i64) = .empty; defer old_stats.deinit(allocator); var found_relation = false; var state = CatalogState{}; var scan: table.Scan = undefined; try self.schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| switch (object.kind) { .relation => { try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }); if (std.mem.eql(u8, object.name, name)) { if (found_relation) return error.CatalogCorrupt; found_relation = true; } }, .index => try addRoot(&roots, &root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page }), .schema => unreachable, .stats => unreachable, }, .stats => |stats| { if (std.mem.eql(u8, stats.table_name, name)) try old_stats.append(allocator, entry.rowid); }, } } const current_schema = try state.schemaOrDefault(); if (!found_relation) return error.RelationNotFound; if (old_stats.items.len == 0) return null; const space = try space_mod.Space.open(self.database, .{ .meta_page = self.meta_page, .roots = roots[0..root_count], }); var schema = try space.rowidTable(self.root_page); var write = try space.beginWrite(); defer write.deinit(); for (old_stats.items) |rowid| try schema.deleteIn(&write, rowid); return .{ .storage = try write.commit(options), .schema = current_schema, }; } pub fn relationStats(self: *const Catalog, allocator: Allocator, name: []const u8) Error!?RelationStats { return try readRelationStats(&self.schema, allocator, name); } /// Returns what `schemaState`, `openRelation` and `relationStats` return /// for `name`, read in one catalog pass over one snapshot. pub fn readRelation( self: *const Catalog, allocator: Allocator, name: []const u8, ) Error!RelationState { return try readRelationFrom( RelationHandle, space_mod.Space, relation_mod.Relation, self.database, &self.schema, self.meta_page, self.root_page, allocator, name, ); }};fn RelationStateType(comptime Handle: type) type { return struct { /// The schema state of the catalog pass that read the relation. schema: Schema, handle: Handle, stats: ?RelationStats, pub fn relationStats(self: *const @This()) ?*const RelationStats { if (self.stats) |*stats| return stats; return null; } pub fn deinit(self: *@This()) void { if (self.stats) |*stats| stats.deinit(); self.handle.deinit(); self.* = undefined; } };}/// The schema state, handle and statistics of one relation.pub const RelationState = RelationStateType(RelationHandle);/// The schema state, read handle and statistics of one relation.pub const ReadRelationState = RelationStateType(ReadRelationHandle);fn validateOptions(options: Options) Error!void { if (options.meta_page == 0) return error.InvalidPageId; if (options.root_page == 0) return error.InvalidPageId; if (options.meta_page == options.root_page) return error.InvalidPageId;}fn catalogTreeOptions(options: Options) tree.Options { return .{ .meta_page = options.meta_page, .root_page = options.root_page, .reserved_page_max = options.root_page, };}fn readSchemaState(schema: anytype, allocator: Allocator) Error!Schema { const phase = trace.scope("catalog.schema_state"); defer phase.end(); return try scanCatalog(schema, allocator, SchemaRows{});}/// Reads every catalog row once and returns the schema state. Each relation/// object, index object and statistics row goes to `rows`, which copies what/// it keeps before the pass reads the next row.fn scanCatalog(schema: anytype, allocator: Allocator, rows: anytype) Error!Schema { var state = CatalogState{}; var scan: table.Scan = undefined; try schema.scan(&scan, allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; switch (try state.record(entry, &scratch)) { .schema => {}, .object => |object| try rows.visitObject(object), .stats => |stats| try rows.visitStats(stats), } } return try state.schemaOrDefault();}/// Keeps no rows, for a pass that only reads the schema state.const SchemaRows = struct { fn visitObject(_: SchemaRows, _: CatalogObject) Error!void {} fn visitStats(_: SchemaRows, _: CatalogStats) Error!void {}};const RelationMetadata = struct { allocator: Allocator, specs: []relation_mod.IndexSpec, index_definitions: []IndexDefinition, fields: []usize, columns: []row.Column, definitions: []ColumnDefinition, index_names: []u8, names: []u8, defaults: []u8, fn deinit(self: *RelationMetadata) void { self.allocator.free(self.defaults); self.allocator.free(self.names); self.allocator.free(self.index_names); self.allocator.free(self.definitions); self.allocator.free(self.columns); self.allocator.free(self.fields); self.allocator.free(self.index_definitions); self.allocator.free(self.specs); self.* = undefined; }};const RelationOpenPlan = struct { name: []const u8, roots: [space_mod.max_roots]space_mod.RootSpec = undefined, root_count: usize = 0, table_root: ?u32 = null, index_roots: [relation_mod.max_indexes]u32 = undefined, index_name_stack: [relation_mod.max_indexes][max_column_name_bytes]u8 = undefined, index_name_lengths: [relation_mod.max_indexes]usize = undefined, fields_stack: [relation_mod.max_indexes][relation_mod.max_index_fields]usize = undefined, columns_stack: [relation_mod.max_indexes][relation_mod.max_index_fields]row.Column = undefined, field_counts: [relation_mod.max_indexes]usize = undefined, definition_stack: [max_columns]ColumnDefinition = undefined, name_stack: [max_column_name_bytes]u8 = undefined, default_stack: [max_column_default_bytes]u8 = undefined, definition_count: usize = 0, name_count: usize = 0, default_count: usize = 0, index_count: usize = 0, total_index_name_bytes: usize = 0, total_fields: usize = 0, fn init(root_page: u32, name: []const u8) Error!RelationOpenPlan { var plan = RelationOpenPlan{ .name = name }; try addRoot(&plan.roots, &plan.root_count, .{ .root_page = root_page }); return plan; } fn visitObject(self: *RelationOpenPlan, object: CatalogObject) Error!void { switch (object.kind) { .relation => if (std.mem.eql(u8, object.name, self.name)) { self.table_root = object.root_page; self.definition_count = object.definitions.len; const copied = try copyDefinitions( &self.definition_stack, &self.name_stack, &self.default_stack, object.definitions, ); self.name_count = copied.names; self.default_count = copied.defaults; try self.addObjectRoot(object); }, .index => if (std.mem.eql(u8, object.table_name, self.name)) { try self.recordIndex(object); }, .schema, .stats => unreachable, } } fn visitStats(_: *RelationOpenPlan, _: CatalogStats) Error!void {} fn recordIndex(self: *RelationOpenPlan, object: CatalogObject) Error!void { if (self.index_count >= relation_mod.max_indexes) return error.TooManyIndexes; const offset = self.index_count; if (object.name.len > self.index_name_stack[offset].len) return error.CatalogCorrupt; self.index_roots[offset] = object.root_page; @memcpy(self.index_name_stack[offset][0..object.name.len], object.name); self.index_name_lengths[offset] = object.name.len; self.field_counts[offset] = object.fields.len; @memcpy(self.fields_stack[offset][0..object.fields.len], object.fields); @memcpy(self.columns_stack[offset][0..object.columns.len], object.columns); self.total_index_name_bytes += object.name.len; self.total_fields += object.fields.len; try self.addObjectRoot(object); self.index_count += 1; } fn addObjectRoot(self: *RelationOpenPlan, object: CatalogObject) Error!void { try addRoot(&self.roots, &self.root_count, .{ .root_page = object.root_page, .identity_page = object.identity_page, }); } fn allocate(self: *const RelationOpenPlan, allocator: Allocator) Error!RelationMetadata { const specs = try allocator.alloc(relation_mod.IndexSpec, self.index_count); errdefer allocator.free(specs); const indexes = try allocator.alloc(IndexDefinition, self.index_count); errdefer allocator.free(indexes); const fields = try allocator.alloc(usize, self.total_fields); errdefer allocator.free(fields); const columns = try allocator.alloc(row.Column, self.total_fields); errdefer allocator.free(columns); const definitions = try allocator.alloc(ColumnDefinition, self.definition_count); errdefer allocator.free(definitions); const index_names = try allocator.alloc(u8, self.total_index_name_bytes); errdefer allocator.free(index_names); const names = try allocator.alloc(u8, self.name_count); errdefer allocator.free(names); const defaults = try allocator.alloc(u8, self.default_count); errdefer allocator.free(defaults); self.copyIndexes(specs, indexes, fields, columns, index_names); copyOpenedDefinitions( definitions, names, defaults, self.definition_stack[0..self.definition_count], ); return .{ .allocator = allocator, .specs = specs, .index_definitions = indexes, .fields = fields, .columns = columns, .definitions = definitions, .index_names = index_names, .names = names, .defaults = defaults, }; } fn copyIndexes( self: *const RelationOpenPlan, specs: []relation_mod.IndexSpec, definitions: []IndexDefinition, fields: []usize, columns: []row.Column, names: []u8, ) void { var field_offset: usize = 0; var name_offset: usize = 0; var offset: usize = 0; while (offset < self.index_count) : (offset += 1) { const count = self.field_counts[offset]; @memcpy(fields[field_offset..][0..count], self.fields_stack[offset][0..count]); @memcpy(columns[field_offset..][0..count], self.columns_stack[offset][0..count]); const name = names[name_offset..][0..self.index_name_lengths[offset]]; @memcpy(name, self.index_name_stack[offset][0..name.len]); specs[offset] = .{ .root_page = self.index_roots[offset], .fields = fields[field_offset..][0..count], .columns = columns[field_offset..][0..count], }; definitions[offset] = .{ .name = name, .fields = fields[field_offset..][0..count], .columns = columns[field_offset..][0..count], }; field_offset += count; name_offset += name.len; } }};fn openRelationFrom( comptime Handle: type, comptime Space: type, comptime Relation: type, source: anytype, schema: anytype, meta_page: u32, catalog_root: u32, allocator: Allocator, name: []const u8,) Error!Handle { const phase = trace.scope("catalog.open_relation"); defer phase.end(); var plan = try RelationOpenPlan.init(catalog_root, name); _ = try scanCatalog(schema, allocator, &plan); return try openPlannedRelation(Handle, Space, Relation, source, meta_page, &plan, allocator);}fn readRelationFrom( comptime Handle: type, comptime Space: type, comptime Relation: type, source: anytype, schema: anytype, meta_page: u32, catalog_root: u32, allocator: Allocator, name: []const u8,) Error!RelationStateType(Handle) { const phase = trace.scope("catalog.read_relation"); defer phase.end(); var plan = try RelationOpenPlan.init(catalog_root, name); var stats_rows = RelationStatsRows{ .allocator = allocator, .name = name }; defer stats_rows.deinit(); const schema_state = try scanCatalog(schema, allocator, RelationRows{ .plan = &plan, .stats = &stats_rows, }); var handle = try openPlannedRelation( Handle, Space, Relation, source, meta_page, &plan, allocator, ); errdefer handle.deinit(); return .{ .schema = schema_state, .handle = handle, .stats = try stats_rows.finish() };}fn openPlannedRelation( comptime Handle: type, comptime Space: type, comptime Relation: type, source: anytype, meta_page: u32, plan: *const RelationOpenPlan, allocator: Allocator,) Error!Handle { const root_page = plan.table_root orelse return error.RelationNotFound; var metadata = try plan.allocate(allocator); errdefer metadata.deinit(); const space = try Space.open(source, .{ .meta_page = meta_page, .roots = plan.roots[0..plan.root_count], }); const opened = try Relation.open(&space, .{ .table_root = root_page, .indexes = metadata.specs, }); return .{ .allocator = allocator, .relation = opened, .specs = metadata.specs, .index_definitions = metadata.index_definitions, .fields = metadata.fields, .columns = metadata.columns, .definitions = metadata.definitions, .index_names = metadata.index_names, .names = metadata.names, .defaults = metadata.defaults, };}fn readRelationNames(schema: anytype, allocator: Allocator) Error!RelationNames { const phase = trace.scope("catalog.relation_names"); defer phase.end(); var rows = RelationNameRows{ .allocator = allocator }; errdefer rows.deinit(); _ = try scanCatalog(schema, allocator, &rows); return .{ .allocator = allocator, .names = try rows.names.toOwnedSlice(allocator), };}/// Collects the name of every relation during a catalog pass.const RelationNameRows = struct { allocator: Allocator, names: std.ArrayList([]u8) = .empty, fn deinit(self: *RelationNameRows) void { for (self.names.items) |name| self.allocator.free(name); self.names.deinit(self.allocator); self.* = undefined; } fn visitObject(self: *RelationNameRows, object: CatalogObject) Error!void { switch (object.kind) { .relation => { const owned = try self.allocator.dupe(u8, object.name); self.names.append(self.allocator, owned) catch |err| { self.allocator.free(owned); return err; }; }, .index => {}, .schema, .stats => unreachable, } } fn visitStats(_: *RelationNameRows, _: CatalogStats) Error!void {}};fn readRelationStats( schema: anytype, allocator: Allocator, name: []const u8,) Error!?RelationStats { const phase = trace.scope("catalog.relation_stats"); defer phase.end(); var rows = RelationStatsRows{ .allocator = allocator, .name = name }; defer rows.deinit(); _ = try scanCatalog(schema, allocator, &rows); return try rows.finish();}/// Collects the statistics rows of one relation during a catalog pass.const RelationStatsRows = struct { allocator: Allocator, name: []const u8, table_root: ?u32 = null, table_stats_root: ?u32 = null, table_summary: ?tree.Summary = null, matching_stats_seen: bool = false, index_stats: std.ArrayList(IndexStats) = .empty, fn deinit(self: *RelationStatsRows) void { freeIndexStats(self.allocator, self.index_stats.items); self.index_stats.deinit(self.allocator); self.* = undefined; } fn visitObject(self: *RelationStatsRows, object: CatalogObject) Error!void { switch (object.kind) { .relation => if (std.mem.eql(u8, object.name, self.name)) { self.table_root = object.root_page; }, .index => {}, .schema, .stats => unreachable, } } fn visitStats(self: *RelationStatsRows, stats: CatalogStats) Error!void { if (!std.mem.eql(u8, stats.table_name, self.name)) return; self.matching_stats_seen = true; if (std.mem.eql(u8, stats.name, self.name)) { if (self.table_summary != null) return error.CatalogCorrupt; if (stats.distribution_blob.len != 0) return error.CatalogCorrupt; self.table_summary = stats.summary; self.table_stats_root = stats.root_page; } else try appendIndexStats(self.allocator, &self.index_stats, stats); } /// Returns the statistics of the relation once the pass is over. The /// result owns the index statistics the rows collected. fn finish(self: *RelationStatsRows) Error!?RelationStats { const root_page = self.table_root orelse return error.RelationNotFound; if (self.table_summary == null) { if (self.matching_stats_seen) return error.CatalogCorrupt; return null; } if (self.table_stats_root.? != root_page) return error.CatalogCorrupt; return .{ .allocator = self.allocator, .table_root_page = root_page, .table = self.table_summary.?, .indexes = try self.index_stats.toOwnedSlice(self.allocator), }; }};/// Hands each catalog row to the open plan and the statistics of one/// relation, so one pass reads both.const RelationRows = struct { plan: *RelationOpenPlan, stats: *RelationStatsRows, fn visitObject(self: RelationRows, object: CatalogObject) Error!void { try self.plan.visitObject(object); try self.stats.visitObject(object); } fn visitStats(self: RelationRows, stats: CatalogStats) Error!void { try self.stats.visitStats(stats); }};fn appendIndexStats( allocator: Allocator, index_stats: *std.ArrayList(IndexStats), stats: CatalogStats,) Error!void { if (containsIndexStats(index_stats.items, stats.name)) return error.CatalogCorrupt; const owned_name = try allocator.dupe(u8, stats.name); errdefer allocator.free(owned_name); const distribution = if (stats.distribution_blob.len == 0) IndexDistribution{} else try decodeIndexDistribution(allocator, stats.distribution_blob); errdefer freeIndexDistribution(allocator, distribution); try index_stats.append(allocator, .{ .name = owned_name, .root_page = stats.root_page, .summary = stats.summary, .distribution = distribution, });}const CatalogKind = enum { schema, relation, index, stats,};const CatalogEntry = union(enum) { schema: Schema, object: CatalogObject, stats: CatalogStats,};const CatalogObject = struct { kind: CatalogKind, name: []const u8, table_name: []const u8, root_page: u32, identity_page: u32, fields: []const usize, columns: []const row.Column, definitions: []const ColumnDefinition,};const CatalogStats = struct { name: []const u8, table_name: []const u8, root_page: u32, summary: tree.Summary, distribution_blob: []const u8,};const AnalyzeIndex = struct { name: []u8, root_page: u32, fields: [relation_mod.max_index_fields]usize = undefined, columns: [relation_mod.max_index_fields]row.Column = undefined, field_count: usize = 0, distribution_blob: []u8 = &.{}, fn indexColumns(self: *const AnalyzeIndex) []const row.Column { return self.columns[0..self.field_count]; }};const CatalogScratch = struct { fields: [relation_mod.max_index_fields]usize = undefined, columns: [relation_mod.max_index_fields]row.Column = undefined, definitions: [max_columns]ColumnDefinition = undefined, names: [max_column_name_bytes]u8 = undefined, defaults: [max_column_default_bytes]u8 = undefined,};const CatalogState = struct { schema: ?Schema = null, data_seen: bool = false, max_rowid: i64 = 0, fn record(self: *CatalogState, entry: table.Entry, scratch: *CatalogScratch) Error!CatalogEntry { const parsed = try catalogEntry(entry.rowid, entry.bytes, scratch); switch (parsed) { .schema => |schema| { if (self.schema != null) return error.CatalogCorrupt; if (schema.format != format_version) return error.UnsupportedCatalogFormat; self.schema = schema; }, .object => { self.data_seen = true; self.max_rowid = @max(self.max_rowid, entry.rowid); }, .stats => { self.data_seen = true; self.max_rowid = @max(self.max_rowid, entry.rowid); }, } return parsed; } fn schemaOrDefault(self: CatalogState) Error!Schema { const schema = self.schema orelse { if (self.data_seen) return error.CatalogCorrupt; return .{}; }; return schema; }};pub fn validateDefinition(definition: RelationDefinition) Error!void { if (definition.name.len == 0) return error.CatalogCorrupt; if (definition.columns.len > max_columns) return error.TooManyColumns; for (definition.columns, 0..) |column, column_offset| { if (column.name.len == 0) return error.CatalogCorrupt; if (column.name.len > std.math.maxInt(u16)) return error.CatalogCorrupt; if ((try row.encodedSize(&.{column.default})) > std.math.maxInt(u16)) return error.CatalogCorrupt; for (definition.columns[0..column_offset]) |previous_column| { if (std.ascii.eqlIgnoreCase(column.name, previous_column.name)) return error.CatalogCorrupt; } } if (definition.indexes.len > relation_mod.max_indexes) return error.TooManyIndexes; for (definition.indexes, 0..) |index_definition, index_offset| { if (index_definition.name.len == 0) return error.CatalogCorrupt; if (std.mem.eql(u8, index_definition.name, definition.name)) return error.ObjectExists; for (definition.indexes[0..index_offset]) |previous_index| { if (std.mem.eql(u8, index_definition.name, previous_index.name)) return error.ObjectExists; } if (index_definition.fields.len > relation_mod.max_index_fields) return error.TooManyIndexFields; if (index_definition.columns.len > index_definition.fields.len) return error.CatalogCorrupt; if (definition.columns.len > 0) { for (index_definition.fields) |field| { if (field >= definition.columns.len) return error.ColumnOutOfBounds; } } }}fn validatePreparedStats( definition: RelationDefinition, prepared: *const PreparedRelationStats,) Error!void { if (prepared.stats.indexes.len != definition.indexes.len) return error.CatalogCorrupt; if (prepared.distribution_blobs.len != definition.indexes.len) return error.CatalogCorrupt; for (definition.indexes, prepared.stats.indexes) |index_definition, index_stats| { if (!std.mem.eql(u8, index_definition.name, index_stats.name)) return error.CatalogCorrupt; }}fn sameLogicalSummary(left: tree.Summary, right: tree.Summary) bool { return left.entries == right.entries and left.key_bytes == right.key_bytes and left.value_bytes == right.value_bytes;}fn bumpSchemaVersion(schema: Schema) Error!Schema { if (schema.version == std.math.maxInt(u64)) return error.SchemaVersionOverflow; return .{ .format = schema.format, .version = schema.version + 1, };}fn putSchemaRow(schema: *table.Table, write: *tree.Write, catalog_schema: Schema) Error!void { var schema_buffer: [schema_blob_bytes]u8 = undefined; try schema.putIn(write, schema_rowid, &.{ .{ .integer = schema_kind }, .{ .text = schema_name }, .{ .text = "" }, .{ .integer = 0 }, .{ .blob = encodeSchema(&schema_buffer, catalog_schema) }, .{ .blob = "" }, });}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 { try schema.putIn(write, rowid, &.{ .{ .integer = kind }, .{ .text = name }, .{ .text = table_name }, .{ .integer = rootAsInteger(root_page) }, .{ .blob = fields }, .{ .blob = collations }, .{ .integer = identityAsInteger(identity_page) }, });}fn catalogEntry(rowid: i64, bytes: []const u8, scratch: *CatalogScratch) Error!CatalogEntry { const view = try row.View.init(bytes); const kind = try catalogKind(try view.column(0)); return switch (kind) { .schema => .{ .schema = try catalogSchema(rowid, view) }, .relation, .index => .{ .object = try catalogObject(rowid, kind, view, scratch) }, .stats => .{ .stats = try catalogStats(rowid, view) }, };}fn catalogSchema(rowid: i64, view: row.View) Error!Schema { var cursor = view.cursor(); if (view.columnCount() != schema_row_columns) return error.UnsupportedCatalogFormat; if (rowid != schema_rowid) return error.CatalogCorrupt; if (!std.mem.eql(u8, try textValue(try cursor.column(1)), schema_name)) return error.CatalogCorrupt; if ((try textValue(try cursor.column(2))).len != 0) return error.CatalogCorrupt; if (try integerValue(try cursor.column(3)) != 0) return error.CatalogCorrupt; const schema_blob = try blobValue(try cursor.column(4)); if ((try blobValue(try cursor.column(5))).len != 0) return error.CatalogCorrupt; return try decodeSchema(schema_blob);}fn catalogObject(rowid: i64, kind: CatalogKind, view: row.View, scratch: *CatalogScratch) Error!CatalogObject { var cursor = view.cursor(); if (view.columnCount() != catalog_row_columns) return error.UnsupportedCatalogFormat; if (rowid < first_object_rowid) return error.CatalogCorrupt; const name = try textValue(try cursor.column(1)); const table_name = try textValue(try cursor.column(2)); const root_page = try rootValue(try cursor.column(3)); const first_blob = try blobValue(try cursor.column(4)); const second_blob = try blobValue(try cursor.column(5)); const identity_page = try rootValue(try cursor.column(6)); const fields = if (kind == .index) try decodeFields(first_blob, &scratch.fields) else scratch.fields[0..0]; const columns = if (kind == .index) try decodeCollations(second_blob, fields.len, &scratch.columns) else scratch.columns[0..0]; const definitions = if (kind == .relation) try decodeDefinitions(first_blob, &scratch.definitions, &scratch.names, &scratch.defaults) else scratch.definitions[0..0]; if (kind == .relation and second_blob.len != 0) return error.CatalogCorrupt; return .{ .kind = kind, .name = name, .table_name = table_name, .root_page = root_page, .identity_page = identity_page, .fields = fields, .columns = columns, .definitions = definitions, };}fn catalogStats(rowid: i64, view: row.View) Error!CatalogStats { var cursor = view.cursor(); if (view.columnCount() != catalog_row_columns) return error.UnsupportedCatalogFormat; if (rowid < first_object_rowid) return error.CatalogCorrupt; const name = try textValue(try cursor.column(1)); const table_name = try textValue(try cursor.column(2)); const root_page = try rootValue(try cursor.column(3)); const summary = try decodeSummary(try blobValue(try cursor.column(4))); const distribution_blob = try blobValue(try cursor.column(5)); if (name.len == 0 or table_name.len == 0) return error.CatalogCorrupt; return .{ .name = name, .table_name = table_name, .root_page = root_page, .summary = summary, .distribution_blob = distribution_blob, };}fn catalogKind(value: row.Value) Error!CatalogKind { return switch (try integerValue(value)) { schema_kind => .schema, relation_kind => .relation, index_kind => .index, stats_kind => .stats, else => error.CatalogCorrupt, };}fn integerValue(value: row.Value) Error!i64 { return switch (value) { .integer => |integer| integer, else => error.CatalogCorrupt, };}fn textValue(value: row.Value) Error![]const u8 { return switch (value) { .text => |text| text, else => error.CatalogCorrupt, };}fn blobValue(value: row.Value) Error![]const u8 { return switch (value) { .blob => |blob| blob, else => error.CatalogCorrupt, };}fn rootValue(value: row.Value) Error!u32 { const integer = try integerValue(value); if (integer <= 0) return error.InvalidPageId; if (integer > std.math.maxInt(u32)) return error.InvalidPageId; return @intCast(integer);}fn rootAsInteger(root_page: u32) i64 { return @intCast(root_page);}fn identityAsInteger(identity_page: u32) i64 { return @intCast(identity_page);}fn encodeSchema(target: *[schema_blob_bytes]u8, schema: Schema) []const u8 { std.mem.writeInt(u32, target[0..4], schema.format, .big); std.mem.writeInt(u64, target[4..12], schema.version, .big); return target[0..];}fn decodeSchema(bytes: []const u8) Error!Schema { if (bytes.len != schema_blob_bytes) return error.CatalogCorrupt; return .{ .format = std.mem.readInt(u32, bytes[0..4], .big), .version = std.mem.readInt(u64, bytes[4..12], .big), };}fn encodeSummary(target: *[summary_blob_bytes]u8, summary: tree.Summary) Error![]const u8 { try writeSummaryField(target, 0, summary.branch_pages); try writeSummaryField(target, 1, summary.leaf_pages); try writeSummaryField(target, 2, summary.overflow_pages); try writeSummaryField(target, 3, summary.entries); try writeSummaryField(target, 4, summary.inline_records); try writeSummaryField(target, 5, summary.overflow_records); try writeSummaryField(target, 6, summary.max_depth); try writeSummaryField(target, 7, summary.key_bytes); try writeSummaryField(target, 8, summary.record_bytes); try writeSummaryField(target, 9, summary.value_bytes); return target[0..];}fn decodeSummary(bytes: []const u8) Error!tree.Summary { if (bytes.len != summary_blob_bytes) return error.CatalogCorrupt; return .{ .branch_pages = try readSummaryField(bytes, 0), .leaf_pages = try readSummaryField(bytes, 1), .overflow_pages = try readSummaryField(bytes, 2), .entries = try readSummaryField(bytes, 3), .inline_records = try readSummaryField(bytes, 4), .overflow_records = try readSummaryField(bytes, 5), .max_depth = try readSummaryField(bytes, 6), .key_bytes = try readSummaryField(bytes, 7), .record_bytes = try readSummaryField(bytes, 8), .value_bytes = try readSummaryField(bytes, 9), };}fn writeSummaryField(target: *[summary_blob_bytes]u8, field: usize, value: usize) Error!void { if (value > std.math.maxInt(u64)) return error.CatalogCorrupt; std.mem.writeInt(u64, target[field * 8 ..][0..8], @intCast(value), .big);}fn readSummaryField(bytes: []const u8, field: usize) Error!usize { const value = std.mem.readInt(u64, bytes[field * 8 ..][0..8], .big); if (value > std.math.maxInt(usize)) return error.CatalogCorrupt; return @intCast(value);}const DistributionMeasure = struct { entries: usize = 0, distinct_values: usize = 0, max_equal: usize = 0,};fn analyzeIndexDistribution(allocator: Allocator, relation_index: *const index_mod.Index) Error![]u8 { var source = LiveDistributionSource{ .index = relation_index }; return try analyzeDistribution(allocator, &source);}const LiveDistributionSource = struct { index: *const index_mod.Index, const Scan = LiveDistributionScan; fn columns(self: *const LiveDistributionSource) []const row.Column { return self.index.columns; } fn scan( self: *const LiveDistributionSource, target: *LiveDistributionScan, allocator: Allocator, ) Error!void { try self.index.scan(&target.scan_value, allocator, null, null); }};const LiveDistributionScan = struct { scan_value: index_mod.Scan, fn deinit(self: *LiveDistributionScan) void { self.scan_value.deinit(); } fn next(self: *LiveDistributionScan) Error!?[]const u8 { const entry = (try self.scan_value.next()) orelse return null; return entry.key; }};const PreparedDistributionKeys = struct { allocator: Allocator, bytes: []u8, keys: [][]const u8, fn deinit(self: *PreparedDistributionKeys) void { self.allocator.free(self.keys); if (self.bytes.len != 0) self.allocator.free(self.bytes); self.* = undefined; }};const PreparedDistributionSource = struct { columns_value: []const row.Column, keys: []const []const u8, const Scan = PreparedDistributionScan; fn columns(self: *const PreparedDistributionSource) []const row.Column { return self.columns_value; } fn scan( self: *const PreparedDistributionSource, target: *PreparedDistributionScan, allocator: Allocator, ) Error!void { _ = allocator; target.* = .{ .keys = self.keys }; }};const PreparedDistributionScan = struct { keys: []const []const u8, offset: usize = 0, fn deinit(self: *PreparedDistributionScan) void { self.* = undefined; } fn next(self: *PreparedDistributionScan) Error!?[]const u8 { if (self.offset == self.keys.len) return null; const bytes = self.keys[self.offset]; self.offset += 1; return bytes; }};fn prepareIndexStats( allocator: Allocator, definition: IndexDefinition, puts: []const relation_mod.Edit.Put, summary: tree.Summary, index_offset: usize,) Error!PreparedIndexStats { std.debug.assert(index_offset < relation_mod.max_indexes); var sorted_keys = try prepareDistributionKeys(allocator, definition, puts); defer sorted_keys.deinit(); var source = PreparedDistributionSource{ .columns_value = definition.columns, .keys = sorted_keys.keys, }; const distribution_blob = try analyzeDistribution(allocator, &source); errdefer if (distribution_blob.len != 0) allocator.free(distribution_blob); const distribution = if (distribution_blob.len == 0) IndexDistribution{} else try decodeIndexDistribution(allocator, distribution_blob); errdefer freeIndexDistribution(allocator, distribution); return .{ .stats = .{ .name = try allocator.dupe(u8, definition.name), .root_page = @intCast(index_offset + 1), .summary = summary, .distribution = distribution, }, .distribution_blob = distribution_blob, };}fn prepareDistributionKeys( allocator: Allocator, definition: IndexDefinition, puts: []const relation_mod.Edit.Put,) Error!PreparedDistributionKeys { var total_bytes: usize = 0; for (puts) |put| { const view = try row.View.init(put.bytes); var projected: [relation_mod.max_index_fields]row.Value = undefined; const values = try view.project(definition.fields, projected[0..]); var buffer: [page.size]u8 = undefined; const encoded = key.encodeIndex( &buffer, values, definition.columns, put.rowid, ) catch |err| switch (err) { error.OutputTooSmall => return error.KeyTooLarge, else => return err, }; total_bytes = std.math.add(usize, total_bytes, encoded.len) catch return error.KeyTooLarge; } const bytes = if (total_bytes == 0) @as([]u8, &.{}) else try allocator.alloc(u8, total_bytes); errdefer if (bytes.len != 0) allocator.free(bytes); const keys = try allocator.alloc([]const u8, puts.len); errdefer allocator.free(keys); var cursor: usize = 0; for (puts, keys) |put, *encoded_key| { const view = try row.View.init(put.bytes); var projected: [relation_mod.max_index_fields]row.Value = undefined; const values = try view.project(definition.fields, projected[0..]); encoded_key.* = key.encodeIndex( bytes[cursor..], values, definition.columns, put.rowid, ) catch |err| switch (err) { error.OutputTooSmall => return error.KeyTooLarge, else => return err, }; cursor += encoded_key.*.len; } std.debug.assert(cursor == bytes.len); std.mem.sort([]const u8, keys, {}, encodedKeyLessThan); return .{ .allocator = allocator, .bytes = bytes, .keys = keys, };}fn encodedKeyLessThan(_: void, left: []const u8, right: []const u8) bool { return simd.order(Bytes, left, right) == .lt;}fn analyzeDistribution(allocator: Allocator, source: anytype) Error![]u8 { const prefix_total = source.columns().len; if (prefix_total == 0) return try allocator.alloc(u8, 0); const leading_measure = try measureIndexDistribution(allocator, source, 1); if (leading_measure.entries == 0) return try allocator.alloc(u8, 0); var encoded: std.ArrayList(u8) = .empty; errdefer encoded.deinit(allocator); try appendInt(u32, &encoded, allocator, distribution_format); try appendUsizeAsU16(&encoded, allocator, prefix_total); try appendPrefixDistribution(&encoded, allocator, source, 1, leading_measure); var prefix_count: usize = 2; while (prefix_count <= prefix_total) : (prefix_count += 1) { const measure = try measureIndexDistribution(allocator, source, prefix_count); try appendPrefixDistribution(&encoded, allocator, source, prefix_count, measure); } return try encoded.toOwnedSlice(allocator);}fn appendPrefixDistribution( encoded: *std.ArrayList(u8), allocator: Allocator, source: anytype, prefix_count: usize, measure: DistributionMeasure,) Error!void { if (measure.entries == 0) return error.CatalogCorrupt; var samples: std.ArrayList(u8) = .empty; defer samples.deinit(allocator); const sample_target = @min(max_index_distribution_samples, measure.entries); var target_index: usize = 0; var sample_count: usize = 0; var scan: @TypeOf(source.*).Scan = undefined; try source.scan(&scan, allocator); defer scan.deinit(); var current_key: [page.size]u8 = undefined; var current_len: usize = 0; var current_valid = false; var run_start: usize = 0; var run_count: usize = 0; var ordinal: usize = 0; var distinct_ordinal: usize = 0; while (try scan.next()) |bytes| { var prefix_buffer: [page.size]u8 = undefined; const prefix = try indexPrefixKey(&prefix_buffer, source.columns(), bytes, prefix_count); if (!current_valid) { current_valid = true; current_len = prefix.len; @memcpy(current_key[0..prefix.len], prefix); run_start = ordinal; run_count = 1; } else if (std.mem.eql(u8, current_key[0..current_len], prefix)) { run_count += 1; } else { try appendDistributionSamples(&samples, allocator, measure.entries, sample_target, &target_index, &sample_count, current_key[0..current_len], run_start, run_count, distinct_ordinal); distinct_ordinal += 1; current_len = prefix.len; @memcpy(current_key[0..prefix.len], prefix); run_start = ordinal; run_count = 1; } ordinal += 1; } 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); try appendUsizeAsU16(encoded, allocator, prefix_count); try appendUsizeAsU64(encoded, allocator, measure.distinct_values); try appendUsizeAsU64(encoded, allocator, measure.max_equal); try appendUsizeAsU16(encoded, allocator, sample_count); try encoded.appendSlice(allocator, samples.items);}fn measureIndexDistribution( allocator: Allocator, source: anytype, prefix_count: usize,) Error!DistributionMeasure { var measure = DistributionMeasure{}; var scan: @TypeOf(source.*).Scan = undefined; try source.scan(&scan, allocator); defer scan.deinit(); var current_key: [page.size]u8 = undefined; var current_len: usize = 0; var current_valid = false; var run_count: usize = 0; while (try scan.next()) |bytes| { var prefix_buffer: [page.size]u8 = undefined; const prefix = try indexPrefixKey(&prefix_buffer, source.columns(), bytes, prefix_count); measure.entries += 1; if (!current_valid) { current_valid = true; current_len = prefix.len; @memcpy(current_key[0..prefix.len], prefix); run_count = 1; } else if (std.mem.eql(u8, current_key[0..current_len], prefix)) { run_count += 1; } else { measure.distinct_values += 1; measure.max_equal = @max(measure.max_equal, run_count); current_len = prefix.len; @memcpy(current_key[0..prefix.len], prefix); run_count = 1; } } if (current_valid) { measure.distinct_values += 1; measure.max_equal = @max(measure.max_equal, run_count); } return measure;}fn indexPrefixKey(target: *[page.size]u8, columns: []const row.Column, bytes: []const u8, prefix_count: usize) Error![]const u8 { var values: [relation_mod.max_index_fields]row.Value = undefined; var scratch: [page.size]u8 = undefined; const decoded = try key.decodeIndex(&values, &scratch, bytes); if (prefix_count == 0 or prefix_count > decoded.values.len) return error.CatalogCorrupt; return try key.encodeIndexPrefix(target, decoded.values[0..prefix_count], columns);}fn appendDistributionSamples( bytes: *std.ArrayList(u8), allocator: Allocator, entries: usize, sample_target: usize, target_index: *usize, sample_count: *usize, sample_key: []const u8, less_than: usize, equal_count: usize, less_distinct: usize,) Error!void { const end = less_than + equal_count; var sampled = false; while (target_index.* < sample_target) { const target = sampleOrdinal(target_index.*, entries, sample_target); if (target < less_than) { target_index.* += 1; continue; } if (target >= end) break; if (!sampled) { try appendUsizeAsU64(bytes, allocator, less_than); try appendUsizeAsU64(bytes, allocator, equal_count); try appendUsizeAsU64(bytes, allocator, less_distinct); try appendUsizeAsU16(bytes, allocator, sample_key.len); try bytes.appendSlice(allocator, sample_key); sample_count.* += 1; sampled = true; } target_index.* += 1; }}fn sampleOrdinal(sample_index: usize, entries: usize, sample_count: usize) usize { const base = entries / sample_count; const remainder = entries % sample_count; const start = sample_index * base + @min(sample_index, remainder); const width = base + if (sample_index < remainder) @as(usize, 1) else 0; return @min(entries - 1, start + width / 2);}fn decodeIndexDistribution(allocator: Allocator, bytes: []const u8) Error!IndexDistribution { if (bytes.len == 0) return .{}; if (bytes.len < distribution_header_bytes) return error.CatalogCorrupt; if (std.mem.readInt(u32, bytes[0..4], .big) != distribution_format) return error.CatalogCorrupt; const prefix_total = std.mem.readInt(u16, bytes[4..6], .big); if (prefix_total == 0 or prefix_total > relation_mod.max_index_fields) return error.CatalogCorrupt; var cursor: usize = distribution_header_bytes; const prefixes = try allocator.alloc(IndexPrefixDistribution, prefix_total); for (prefixes) |*prefix| prefix.* = .{}; errdefer freeIndexPrefixDistributions(allocator, prefixes); var prefix_index: usize = 0; while (prefix_index < prefix_total) : (prefix_index += 1) { prefixes[prefix_index] = try decodeIndexPrefixDistribution(allocator, bytes, &cursor); } if (cursor != bytes.len) return error.CatalogCorrupt; const leading = prefixes[0]; return .{ .distinct_values = leading.distinct_values, .max_equal = leading.max_equal, .samples = leading.samples, .sample_keys = leading.sample_keys, .prefixes = prefixes, };}fn decodeIndexPrefixDistribution(allocator: Allocator, bytes: []const u8, cursor: *usize) Error!IndexPrefixDistribution { if (distribution_prefix_header_bytes > bytes.len - cursor.*) return error.CatalogCorrupt; const field_count = std.mem.readInt(u16, bytes[cursor.*..][0..2], .big); cursor.* += 2; if (field_count == 0 or field_count > relation_mod.max_index_fields) return error.CatalogCorrupt; const distinct_values = try readUsizeField(bytes[cursor.*..][0..8]); cursor.* += 8; const max_equal = try readUsizeField(bytes[cursor.*..][0..8]); cursor.* += 8; const sample_count = std.mem.readInt(u16, bytes[cursor.*..][0..2], .big); cursor.* += 2; var scan_cursor = cursor.*; var key_bytes: usize = 0; var sample_index: usize = 0; while (sample_index < sample_count) : (sample_index += 1) { if (distribution_sample_header_bytes > bytes.len - scan_cursor) return error.CatalogCorrupt; scan_cursor += 24; const key_len = std.mem.readInt(u16, bytes[scan_cursor..][0..2], .big); scan_cursor += 2; if (key_len > bytes.len - scan_cursor) return error.CatalogCorrupt; key_bytes += key_len; scan_cursor += key_len; } const samples: []IndexSample = if (sample_count == 0) &.{} else try allocator.alloc(IndexSample, sample_count); errdefer if (sample_count != 0) allocator.free(samples); const keys: []u8 = if (key_bytes == 0) &.{} else try allocator.alloc(u8, key_bytes); errdefer if (key_bytes != 0) allocator.free(keys); var key_cursor: usize = 0; sample_index = 0; while (sample_index < sample_count) : (sample_index += 1) { const less_than = try readUsizeField(bytes[cursor.*..][0..8]); cursor.* += 8; const equal_count = try readUsizeField(bytes[cursor.*..][0..8]); cursor.* += 8; const less_distinct = try readUsizeField(bytes[cursor.*..][0..8]); cursor.* += 8; const key_len = std.mem.readInt(u16, bytes[cursor.*..][0..2], .big); cursor.* += 2; @memcpy(keys[key_cursor..][0..key_len], bytes[cursor.*..][0..key_len]); samples[sample_index] = .{ .key = keys[key_cursor..][0..key_len], .less_than = less_than, .equal_count = equal_count, .less_distinct = less_distinct, }; key_cursor += key_len; cursor.* += key_len; } return .{ .field_count = field_count, .distinct_values = distinct_values, .max_equal = max_equal, .samples = samples, .sample_keys = keys, };}fn appendUsizeAsU64(bytes: *std.ArrayList(u8), allocator: Allocator, value: usize) Error!void { if (value > std.math.maxInt(u64)) return error.CatalogCorrupt; try appendInt(u64, bytes, allocator, @intCast(value));}fn appendUsizeAsU16(bytes: *std.ArrayList(u8), allocator: Allocator, value: usize) Error!void { var buffer: [2]u8 = undefined; try writeUsizeAsU16(&buffer, value); try bytes.appendSlice(allocator, &buffer);}fn writeUsizeAsU16(target: []u8, value: usize) Error!void { if (target.len < 2) return error.CatalogCorrupt; if (value > std.math.maxInt(u16)) return error.CatalogCorrupt; std.mem.writeInt(u16, target[0..2], @intCast(value), .big);}fn appendInt(comptime T: type, bytes: *std.ArrayList(u8), allocator: Allocator, value: T) std.mem.Allocator.Error!void { var buffer: [@sizeOf(T)]u8 = undefined; std.mem.writeInt(T, &buffer, value, .big); try bytes.appendSlice(allocator, &buffer);}fn readUsizeField(bytes: []const u8) Error!usize { if (bytes.len < 8) return error.CatalogCorrupt; const value = std.mem.readInt(u64, bytes[0..8], .big); if (value > std.math.maxInt(usize)) return error.CatalogCorrupt; return @intCast(value);}fn encodeDefinitions(target: *[page.size]u8, definitions: []const ColumnDefinition) Error![]const u8 { if (definitions.len > max_columns) return error.TooManyColumns; var cursor: usize = 0; for (definitions) |definition| { if (definition.name.len == 0) return error.CatalogCorrupt; if (definition.name.len > std.math.maxInt(u16)) return error.CatalogCorrupt; var default_buffer: [page.size]u8 = undefined; const encoded_default = try row.encode(&default_buffer, &.{definition.default}); if (encoded_default.len > std.math.maxInt(u16)) return error.CatalogCorrupt; const required = column_name_len_bytes + definition.name.len + 1 + default_len_bytes + encoded_default.len; if (required > target.len - cursor) return error.CatalogCorrupt; std.mem.writeInt(u16, target[cursor..][0..column_name_len_bytes], @intCast(definition.name.len), .big); cursor += column_name_len_bytes; @memcpy(target[cursor..][0..definition.name.len], definition.name); cursor += definition.name.len; target[cursor] = collationByte(definition.column.collation); cursor += 1; std.mem.writeInt(u16, target[cursor..][0..default_len_bytes], @intCast(encoded_default.len), .big); cursor += default_len_bytes; @memcpy(target[cursor..][0..encoded_default.len], encoded_default); cursor += encoded_default.len; } return target[0..cursor];}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 { var cursor: usize = 0; var name_cursor: usize = 0; var default_cursor: usize = 0; var count: usize = 0; while (cursor < bytes.len) { if (count >= target.len) return error.TooManyColumns; if (column_name_len_bytes > bytes.len - cursor) return error.CatalogCorrupt; const name_len = std.mem.readInt(u16, bytes[cursor..][0..column_name_len_bytes], .big); cursor += column_name_len_bytes; if (name_len == 0) return error.CatalogCorrupt; const name_required = @as(usize, name_len) + 1 + default_len_bytes; if (name_required > bytes.len - cursor) return error.CatalogCorrupt; if (name_len > names_target.len - name_cursor) return error.CatalogCorrupt; @memcpy(names_target[name_cursor..][0..name_len], bytes[cursor..][0..name_len]); const name = names_target[name_cursor..][0..name_len]; cursor += name_len; name_cursor += name_len; const collation = try collationFromByte(bytes[cursor]); cursor += 1; const default_len = std.mem.readInt(u16, bytes[cursor..][0..default_len_bytes], .big); cursor += default_len_bytes; if (default_len > bytes.len - cursor) return error.CatalogCorrupt; if (default_len > defaults_target.len - default_cursor) return error.CatalogCorrupt; @memcpy(defaults_target[default_cursor..][0..default_len], bytes[cursor..][0..default_len]); const default_bytes = defaults_target[default_cursor..][0..default_len]; const default_value = try defaultValue(default_bytes); cursor += default_len; default_cursor += default_len; for (target[0..count]) |previous| { if (std.ascii.eqlIgnoreCase(previous.name, name)) return error.CatalogCorrupt; } target[count] = .{ .name = name, .column = .{ .collation = collation }, .default = default_value, }; count += 1; } return target[0..count];}const DefinitionCopy = struct { names: usize, defaults: usize,};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 { if (definitions.len > target.len) return error.TooManyColumns; var name_cursor: usize = 0; var default_cursor: usize = 0; for (definitions, 0..) |definition, offset| { if (definition.name.len > names_target.len - name_cursor) return error.CatalogCorrupt; @memcpy(names_target[name_cursor..][0..definition.name.len], definition.name); var default_buffer: [page.size]u8 = undefined; const encoded_default = try row.encode(&default_buffer, &.{definition.default}); if (encoded_default.len > defaults_target.len - default_cursor) return error.CatalogCorrupt; @memcpy(defaults_target[default_cursor..][0..encoded_default.len], encoded_default); const default_value = try defaultValue(defaults_target[default_cursor..][0..encoded_default.len]); target[offset] = .{ .name = names_target[name_cursor..][0..definition.name.len], .column = definition.column, .default = default_value, }; name_cursor += definition.name.len; default_cursor += encoded_default.len; } return .{ .names = name_cursor, .defaults = default_cursor, };}fn copyOpenedDefinitions(target: []ColumnDefinition, names_target: []u8, defaults_target: []u8, definitions: []const ColumnDefinition) void { var name_cursor: usize = 0; var default_cursor: usize = 0; for (definitions, 0..) |definition, offset| { @memcpy(names_target[name_cursor..][0..definition.name.len], definition.name); var default_buffer: [page.size]u8 = undefined; const encoded_default = row.encode(&default_buffer, &.{definition.default}) catch unreachable; @memcpy(defaults_target[default_cursor..][0..encoded_default.len], encoded_default); const default_value = defaultValue(defaults_target[default_cursor..][0..encoded_default.len]) catch unreachable; target[offset] = .{ .name = names_target[name_cursor..][0..definition.name.len], .column = definition.column, .default = default_value, }; name_cursor += definition.name.len; default_cursor += encoded_default.len; }}fn defaultValue(bytes: []const u8) Error!row.Value { const view = try row.View.init(bytes); if (view.columnCount() != 1) return error.CatalogCorrupt; return try view.column(0);}fn encodeFields(target: *[relation_mod.max_index_fields * field_bytes]u8, fields: []const usize) Error![]const u8 { if (fields.len > relation_mod.max_index_fields) return error.TooManyIndexFields; for (fields, 0..) |field, offset| { if (field > std.math.maxInt(u32)) return error.CatalogCorrupt; std.mem.writeInt(u32, target[offset * field_bytes ..][0..field_bytes], @intCast(field), .big); } return target[0 .. fields.len * field_bytes];}fn decodeFields(bytes: []const u8, target: *[relation_mod.max_index_fields]usize) Error![]usize { if (bytes.len % field_bytes != 0) return error.CatalogCorrupt; const count = bytes.len / field_bytes; if (count > target.len) return error.TooManyIndexFields; var index: usize = 0; while (index < count) : (index += 1) { target[index] = std.mem.readInt(u32, bytes[index * field_bytes ..][0..field_bytes], .big); } return target[0..count];}fn encodeCollations(target: *[relation_mod.max_index_fields]u8, field_count: usize, columns: []const row.Column) Error![]const u8 { if (field_count > target.len) return error.TooManyIndexFields; if (columns.len > field_count) return error.CatalogCorrupt; var index: usize = 0; while (index < field_count) : (index += 1) { const column = if (index < columns.len) columns[index] else row.Column{}; target[index] = collationByte(column.collation); } return target[0..field_count];}fn decodeCollations(bytes: []const u8, field_count: usize, target: *[relation_mod.max_index_fields]row.Column) Error![]row.Column { if (bytes.len != field_count) return error.CatalogCorrupt; if (field_count > target.len) return error.TooManyIndexFields; var index: usize = 0; while (index < field_count) : (index += 1) { target[index] = .{ .collation = try collationFromByte(bytes[index]) }; } return target[0..field_count];}fn collationByte(collation: row.Collation) u8 { return switch (collation) { .binary => 0, .nocase => 1, .rtrim => 2, };}fn collationFromByte(byte: u8) Error!row.Collation { return switch (byte) { 0 => .binary, 1 => .nocase, 2 => .rtrim, else => error.CatalogCorrupt, };}fn freeAnalyzeIndexes(allocator: Allocator, indexes: []const AnalyzeIndex) void { for (indexes) |index_object| { allocator.free(index_object.name); if (index_object.distribution_blob.len != 0) allocator.free(index_object.distribution_blob); }}fn freeIndexStats(allocator: Allocator, indexes: []const IndexStats) void { for (indexes) |index_stats| { allocator.free(index_stats.name); freeIndexDistribution(allocator, index_stats.distribution); }}fn freeIndexDistribution(allocator: Allocator, distribution: IndexDistribution) void { if (distribution.prefixes.len != 0) { freeIndexPrefixDistributions(allocator, distribution.prefixes); return; } if (distribution.samples.len != 0) allocator.free(distribution.samples); if (distribution.sample_keys.len != 0) allocator.free(distribution.sample_keys);}fn freeIndexPrefixDistributions(allocator: Allocator, prefixes: []IndexPrefixDistribution) void { for (prefixes) |prefix| { if (prefix.samples.len != 0) allocator.free(prefix.samples); if (prefix.sample_keys.len != 0) allocator.free(prefix.sample_keys); } allocator.free(prefixes);}fn containsIndexStats(indexes: []const IndexStats, name: []const u8) bool { for (indexes) |index_stats| { if (std.mem.eql(u8, index_stats.name, name)) return true; } return false;}fn addRoot(roots: *[space_mod.max_roots]space_mod.RootSpec, root_count: *usize, spec: space_mod.RootSpec) Error!void { if (spec.root_page == 0) return error.InvalidPageId; var index: usize = 0; while (index < root_count.*) : (index += 1) { if (roots[index].root_page == spec.root_page) return error.InvalidPageId; } if (root_count.* >= roots.len) return error.TooManyRoots; roots[root_count.*] = spec; root_count.* += 1;}fn allocateCatalogRoot(write: *tree.Write, roots: *[space_mod.max_roots]space_mod.RootSpec, root_count: *usize) Error!space_mod.RootSpec { const root_page = try write.allocateRoot(); const identity_page = try write.allocateRoot(); const spec = space_mod.RootSpec{ .root_page = root_page, .identity_page = identity_page }; try addRoot(roots, root_count, spec); return spec;}test "catalog reader exposes one snapshot through query-only methods" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); var catalog = try Catalog.open(&database, .{}); const indexes = [_]IndexDefinition{.{ .name = "items_value", .fields = &.{0}, }}; const created = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &indexes, }, .{ .durability = .buffered }); { var writable = try catalog.openRelation(std.testing.allocator, "items"); defer writable.deinit(); _ = try writable.relation.put( std.testing.allocator, 7, &.{.{ .integer = 42 }}, .{ .durability = .buffered }, ); } var read = try database.beginRead(); defer read.deinit(); const reader = try Reader.open(read.snapshot(), .{}); try std.testing.expectEqual(created.schema, try reader.schemaState(std.testing.allocator)); var names = try reader.relationNames(std.testing.allocator); defer names.deinit(); try std.testing.expectEqual(@as(usize, 1), names.names.len); try std.testing.expectEqualSlices(u8, "items", names.names[0]); var opened = try reader.openRelation(std.testing.allocator, "items"); defer opened.deinit(); const bytes = (try opened.relation.get(std.testing.allocator, 7)).?; defer std.testing.allocator.free(bytes); const view = try row.View.init(bytes); try std.testing.expectEqual(@as(i64, 42), (try view.column(0)).integer); var lookup: index_mod.Scan = undefined; try opened.relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 42 }}); defer lookup.deinit(); try std.testing.expectEqual(@as(i64, 7), (try lookup.next()).?.rowid); try std.testing.expect(try lookup.next() == null); try std.testing.expect((try reader.relationStats(std.testing.allocator, "items")) == null); try std.testing.expect(!@hasDecl(Reader, "createRelation")); try std.testing.expect(!@hasDecl(Reader, "analyzeRelation")); try std.testing.expect(!@hasDecl(Reader, "dropRelation"));}test "catalog reads the schema, handle and stats of one relation in one pass" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 640 }); var catalog = try Catalog.open(&database, .{}); const buffered: file.CommitOptions = .{ .durability = .buffered }; _ = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .columns = &.{.{ .name = "value" }}, }, buffered); _ = try catalog.createRelation(std.testing.allocator, .{ .name = "other", .indexes = &.{.{ .name = "other_value", .fields = &.{0} }}, }, buffered); _ = try catalog.createIndex(std.testing.allocator, "items", .{ .name = "items_value", .fields = &.{0}, }, buffered); { var writable = try catalog.openRelation(std.testing.allocator, "items"); defer writable.deinit(); _ = try writable.relation.put(std.testing.allocator, 1, &.{.{ .integer = 3 }}, buffered); _ = try writable.relation.put(std.testing.allocator, 2, &.{.{ .integer = 5 }}, buffered); } _ = try catalog.analyzeRelation(std.testing.allocator, "items", buffered); var items = try catalog.readRelation(std.testing.allocator, "items"); defer items.deinit(); try expectRelationState(&catalog, "items", &items); try std.testing.expectEqual(@as(usize, 1), items.handle.index_definitions.len); try std.testing.expectEqual(@as(usize, 2), items.relationStats().?.table.entries); var other = try catalog.readRelation(std.testing.allocator, "other"); defer other.deinit(); try expectRelationState(&catalog, "other", &other); try std.testing.expectEqualStrings("other_value", other.handle.index_definitions[0].name); try std.testing.expect(other.relationStats() == null); try std.testing.expectError( error.RelationNotFound, catalog.readRelation(std.testing.allocator, "missing"), ); var read = try database.beginRead(); defer read.deinit(); const reader = try Reader.open(read.snapshot(), .{}); var read_items = try reader.readRelation(std.testing.allocator, "items"); defer read_items.deinit(); try expectRelationState(&reader, "items", &read_items); try std.testing.expectError( error.RelationNotFound, reader.readRelation(std.testing.allocator, "missing"), );}/// Checks that `state` holds what the separate readers of `catalog` return/// for `name`.fn expectRelationState(catalog: anytype, name: []const u8, state: anytype) !void { try std.testing.expectEqual(try catalog.schemaState(std.testing.allocator), state.schema); var handle = try catalog.openRelation(std.testing.allocator, name); defer handle.deinit(); try std.testing.expectEqual( handle.relation.table.rows.root_page, state.handle.relation.table.rows.root_page, ); try std.testing.expectEqual(handle.definitions.len, state.handle.definitions.len); try std.testing.expectEqual(handle.specs.len, state.handle.specs.len); for (handle.specs, state.handle.specs) |expected, actual| { try std.testing.expectEqual(expected.root_page, actual.root_page); try std.testing.expectEqualSlices(usize, expected.fields, actual.fields); } try std.testing.expectEqual( handle.index_definitions.len, state.handle.index_definitions.len, ); for (handle.index_definitions, state.handle.index_definitions) |expected, actual| { try std.testing.expectEqualStrings(expected.name, actual.name); } var stats = try catalog.relationStats(std.testing.allocator, name); defer if (stats) |*relation_stats| relation_stats.deinit(); const actual_stats = state.relationStats() orelse { try std.testing.expect(stats == null); return; }; const expected_stats = stats orelse return error.TestUnexpectedResult; try std.testing.expectEqual(expected_stats.table_root_page, actual_stats.table_root_page); try std.testing.expectEqual(expected_stats.table, actual_stats.table); try std.testing.expectEqual(expected_stats.indexes.len, actual_stats.indexes.len); for (expected_stats.indexes, actual_stats.indexes) |expected, actual| { try std.testing.expectEqualStrings(expected.name, actual.name); try std.testing.expectEqual(expected.root_page, actual.root_page); try std.testing.expectEqual(expected.summary, actual.summary); try std.testing.expectEqual( expected.distribution.distinct_values, actual.distribution.distinct_values, ); }}test "catalog rejects stats that disagree with their relation and rows without a schema" { const damages = [_]CatalogDamage{ .moved_table_stats, .orphaned_index_stats, .missing_schema_row, }; for (damages) |damage| { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 640 }); var catalog = try Catalog.open(&database, .{}); const buffered: file.CommitOptions = .{ .durability = .buffered }; _ = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &.{.{ .name = "items_value", .fields = &.{0} }}, }, buffered); { var writable = try catalog.openRelation(std.testing.allocator, "items"); defer writable.deinit(); _ = try writable.relation.put( std.testing.allocator, 1, &.{.{ .integer = 3 }}, buffered, ); } _ = try catalog.analyzeRelation(std.testing.allocator, "items", buffered); try damageCatalog(&database, damage); try std.testing.expectError( error.CatalogCorrupt, catalog.readRelation(std.testing.allocator, "items"), ); switch (damage) { .moved_table_stats, .orphaned_index_stats => try std.testing.expectError( error.CatalogCorrupt, catalog.relationStats(std.testing.allocator, "items"), ), .missing_schema_row => try std.testing.expectError( error.CatalogCorrupt, catalog.schemaState(std.testing.allocator), ), } }}const CatalogDamage = enum { /// The table stats row names a root other than the relation's. moved_table_stats, /// Index stats rows remain without the table stats row. orphaned_index_stats, /// Object rows remain without the schema row. missing_schema_row,};/// Rewrites the catalog rows of an analyzed relation so that `damage` holds.fn damageCatalog(database: *file.Database, damage: CatalogDamage) !void { const space = try space_mod.Space.open(database, .{ .meta_page = default_meta_page, .roots = &.{.{ .root_page = default_root_page }}, }); var schema = try space.rowidTable(default_root_page); var write = try space.beginWrite(); defer write.deinit(); switch (damage) { .missing_schema_row => try schema.deleteIn(&write, schema_rowid), .moved_table_stats, .orphaned_index_stats => { var scan: table.Scan = undefined; try schema.scan(&scan, std.testing.allocator, null, null); defer scan.deinit(); while (try scan.next()) |entry| { var scratch = CatalogScratch{}; const table_stats = switch (try catalogEntry(entry.rowid, entry.bytes, &scratch)) { .stats => |found| found, .schema, .object => continue, }; if (!std.mem.eql(u8, table_stats.name, table_stats.table_name)) continue; if (damage == .orphaned_index_stats) { try schema.deleteIn(&write, entry.rowid); continue; } var summary_buffer: [summary_blob_bytes]u8 = undefined; try putCatalogRow( &schema, &write, entry.rowid, stats_kind, table_stats.name, table_stats.table_name, table_stats.root_page + 1, 0, try encodeSummary(&summary_buffer, table_stats.summary), "", ); } }, } _ = try write.commit(.{ .durability = .buffered });}test "catalog persists relation metadata and reopens without caller specs" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 220 }); var catalog = try Catalog.open(&database, .{}); try std.testing.expectEqual(Schema{}, try catalog.schemaState(std.testing.allocator)); const indexes = [_]IndexDefinition{.{ .name = "items_by_name", .fields = &.{1}, .columns = &.{.{ .collation = .nocase }}, }}; const columns = [_]ColumnDefinition{ .{ .name = "id" }, .{ .name = "name", .column = .{ .collation = .nocase }, .default = .{ .text = "unknown" } }, }; const commit = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .columns = &columns, .indexes = &indexes, }, .{ .durability = .buffered }); try std.testing.expectEqual(@as(u64, 1), commit.schema.version); try std.testing.expectEqual(commit.schema, try catalog.schemaState(std.testing.allocator)); var handle = try catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); try std.testing.expectEqual(@as(u32, 3), handle.relation.table.rows.root_page); try std.testing.expectEqual(@as(usize, 2), handle.definitions.len); try std.testing.expectEqualStrings("id", handle.definitions[0].name); try std.testing.expectEqualStrings("name", handle.definitions[1].name); try std.testing.expectEqual(row.Collation.nocase, handle.definitions[1].column.collation); try std.testing.expectEqualStrings("unknown", handle.definitions[1].default.text); try std.testing.expectEqual(@as(usize, 1), handle.index_definitions.len); try std.testing.expectEqualStrings("items_by_name", handle.index_definitions[0].name); try std.testing.expectEqual(@as(usize, 1), handle.index_definitions[0].fields.len); try std.testing.expectEqual(@as(usize, 1), handle.index_definitions[0].fields[0]); try std.testing.expectEqual(row.Collation.nocase, handle.index_definitions[0].columns[0].collation); _ = try handle.relation.put(std.testing.allocator, 8, &.{ .{ .integer = 8 }, .{ .text = "Alpha" } }, .{ .durability = .buffered }); try database.syncWal(); } var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = recoveredHeader(), }); defer reopened.deinit(); var catalog = try Catalog.open(&reopened, .{}); try std.testing.expectEqual(@as(u64, 1), (try catalog.schemaState(std.testing.allocator)).version); var handle = try catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); try std.testing.expectEqual(@as(usize, 2), handle.definitions.len); try std.testing.expectEqualStrings("id", handle.definitions[0].name); try std.testing.expectEqualStrings("name", handle.definitions[1].name); try std.testing.expectEqual(row.Collation.nocase, handle.definitions[1].column.collation); try std.testing.expectEqualStrings("unknown", handle.definitions[1].default.text); try std.testing.expectEqual(@as(usize, 1), handle.index_definitions.len); try std.testing.expectEqualStrings("items_by_name", handle.index_definitions[0].name); var lookup: index_mod.Scan = undefined; try handle.relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .text = "alpha" }}); defer lookup.deinit(); const entry = (try lookup.next()).?; try std.testing.expectEqual(@as(i64, 8), entry.rowid); try std.testing.expect(try lookup.next() == null); const bytes = (try handle.relation.get(std.testing.allocator, 8)).?; defer std.testing.allocator.free(bytes); const view = try row.View.init(bytes); try std.testing.expectEqual(@as(i64, 8), (try view.column(0)).integer); try std.testing.expectEqualStrings("Alpha", (try view.column(1)).text);}test "catalog analyzes relation stats and reopens them without schema bump" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 420 }); var catalog = try Catalog.open(&database, .{}); const indexes = [_]IndexDefinition{.{ .name = "items_value", .fields = &.{0}, }}; const created = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &indexes, }, .{ .durability = .buffered }); try std.testing.expect((try catalog.relationStats(std.testing.allocator, "items")) == null); var handle = try catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .integer = 3 }, .{ .text = "three" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 2, &.{ .{ .integer = 5 }, .{ .text = "five" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 3, &.{ .{ .integer = 5 }, .{ .text = "cinco" } }, .{ .durability = .buffered }); const analyzed = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered }); try std.testing.expectEqual(created.schema, analyzed.schema); try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator)); var stats = (try catalog.relationStats(std.testing.allocator, "items")).?; defer stats.deinit(); try std.testing.expectEqual(handle.relation.table.rows.root_page, stats.table_root_page); try std.testing.expectEqual(@as(usize, 3), stats.table.entries); try std.testing.expectEqual(@as(usize, 3), stats.table.inline_records); const index_stats = stats.index("items_value").?; try std.testing.expectEqual(@as(usize, 3), index_stats.summary.entries); try std.testing.expect(index_stats.summary.key_bytes > stats.table.key_bytes); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.distinct_values); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.max_equal); try std.testing.expect(index_stats.distribution.samples.len > 0); try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes.len); try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes[0].field_count); try database.syncWal(); } var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = recoveredHeader(), }); defer reopened.deinit(); var catalog = try Catalog.open(&reopened, .{}); try std.testing.expectEqual(@as(u64, 1), (try catalog.schemaState(std.testing.allocator)).version); var stats = (try catalog.relationStats(std.testing.allocator, "items")).?; defer stats.deinit(); try std.testing.expectEqual(@as(usize, 3), stats.table.entries); try std.testing.expectEqual(@as(usize, 1), stats.indexes.len); const index_stats = stats.index("items_value").?; try std.testing.expectEqual(@as(usize, 3), index_stats.summary.entries); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.distinct_values); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.max_equal); try std.testing.expect(index_stats.distribution.samples.len > 0); try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes.len);}test "catalog clears relation stats without schema bump" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 420 }); var catalog = try Catalog.open(&database, .{}); const indexes = [_]IndexDefinition{.{ .name = "items_value", .fields = &.{0}, }}; const created = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &indexes, }, .{ .durability = .buffered }); var handle = try catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); _ = try handle.relation.put(std.testing.allocator, 1, &.{.{ .integer = 3 }}, .{ .durability = .buffered }); _ = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered }); var stats = (try catalog.relationStats(std.testing.allocator, "items")).?; stats.deinit(); const cleared = (try catalog.clearRelationStats(std.testing.allocator, "items", .{ .durability = .buffered })).?; try std.testing.expectEqual(created.schema, cleared.schema); try std.testing.expectEqual(created.schema, try catalog.schemaState(std.testing.allocator)); try std.testing.expect((try catalog.relationStats(std.testing.allocator, "items")) == null); try std.testing.expect((try catalog.clearRelationStats(std.testing.allocator, "items", .{ .durability = .buffered })) == null); try std.testing.expectError(error.RelationNotFound, catalog.clearRelationStats(std.testing.allocator, "missing", .{ .durability = .buffered }));}test "catalog drops relation index and stats metadata" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 420 }); var catalog = try Catalog.open(&database, .{}); const indexes = [_]IndexDefinition{.{ .name = "items_value", .fields = &.{0}, }}; const created = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &indexes, }, .{ .durability = .buffered }); try std.testing.expectEqual(@as(u64, 1), created.schema.version); var handle = try catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); _ = try handle.relation.put(std.testing.allocator, 1, &.{.{ .integer = 3 }}, .{ .durability = .buffered }); _ = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered }); var stats = (try catalog.relationStats(std.testing.allocator, "items")).?; stats.deinit(); const dropped = try catalog.dropRelation(std.testing.allocator, "items", .{ .durability = .buffered }); try std.testing.expectEqual(@as(u64, 2), dropped.schema.version); try std.testing.expectEqual(dropped.schema, try catalog.schemaState(std.testing.allocator)); try std.testing.expectError(error.RelationNotFound, catalog.openRelation(std.testing.allocator, "items")); try std.testing.expectError(error.RelationNotFound, catalog.relationStats(std.testing.allocator, "items")); var names = try catalog.relationNames(std.testing.allocator); defer names.deinit(); try std.testing.expectEqual(@as(usize, 0), names.names.len); const recreated = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &indexes, }, .{ .durability = .buffered }); try std.testing.expectEqual(@as(u64, 3), recreated.schema.version); try std.testing.expectError(error.RelationNotFound, catalog.dropRelation(std.testing.allocator, "missing", .{ .durability = .buffered }));}test "catalog analyzes composite index prefix distributions" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 520 }); var catalog = try Catalog.open(&database, .{}); const columns = [_]ColumnDefinition{ .{ .name = "region" }, .{ .name = "payload" }, .{ .name = "name" }, }; const indexes = [_]IndexDefinition{.{ .name = "items_region_payload", .fields = &.{ 0, 1 }, }}; _ = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .columns = &columns, .indexes = &indexes, }, .{ .durability = .buffered }); var handle = try catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .integer = 0 }, .{ .integer = 1 }, .{ .text = "a" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 2, &.{ .{ .integer = 0 }, .{ .integer = 1 }, .{ .text = "b" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 3, &.{ .{ .integer = 0 }, .{ .integer = 2 }, .{ .text = "c" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 4, &.{ .{ .integer = 1 }, .{ .integer = 1 }, .{ .text = "d" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 5, &.{ .{ .integer = 1 }, .{ .integer = 2 }, .{ .text = "e" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 6, &.{ .{ .integer = 1 }, .{ .integer = 2 }, .{ .text = "f" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 7, &.{ .{ .integer = 1 }, .{ .integer = 3 }, .{ .text = "g" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 8, &.{ .{ .integer = 2 }, .{ .integer = 5 }, .{ .text = "h" } }, .{ .durability = .buffered }); _ = try handle.relation.put(std.testing.allocator, 9, &.{ .{ .integer = 2 }, .{ .integer = 5 }, .{ .text = "i" } }, .{ .durability = .buffered }); _ = try catalog.analyzeRelation(std.testing.allocator, "items", .{ .durability = .buffered }); var stats = (try catalog.relationStats(std.testing.allocator, "items")).?; defer stats.deinit(); const index_stats = stats.index("items_region_payload").?; try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes.len); try std.testing.expectEqual(@as(usize, 1), index_stats.distribution.prefixes[0].field_count); try std.testing.expectEqual(@as(usize, 3), index_stats.distribution.prefixes[0].distinct_values); try std.testing.expectEqual(@as(usize, 4), index_stats.distribution.prefixes[0].max_equal); try std.testing.expect(index_stats.distribution.prefixes[0].samples.len > 0); for (index_stats.distribution.prefixes[0].samples, 0..) |sample, offset| { try std.testing.expectEqual(offset, sample.less_distinct); } try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes[1].field_count); try std.testing.expectEqual(@as(usize, 6), index_stats.distribution.prefixes[1].distinct_values); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes[1].max_equal); try std.testing.expect(index_stats.distribution.prefixes[1].samples.len > 0); for (index_stats.distribution.prefixes[1].samples, 0..) |sample, offset| { try std.testing.expectEqual(offset, sample.less_distinct); } try std.testing.expectEqual(index_stats.distribution.prefixes[0].distinct_values, index_stats.distribution.distinct_values); try std.testing.expectEqual(index_stats.distribution.prefixes[0].max_equal, index_stats.distribution.max_equal); try database.syncWal(); } var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = recoveredHeader(), }); defer reopened.deinit(); var catalog = try Catalog.open(&reopened, .{}); var stats = (try catalog.relationStats(std.testing.allocator, "items")).?; defer stats.deinit(); const index_stats = stats.index("items_region_payload").?; try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.prefixes.len); try std.testing.expectEqual(@as(usize, 3), index_stats.distribution.prefixes[0].distinct_values); try std.testing.expectEqual(@as(usize, 6), index_stats.distribution.prefixes[1].distinct_values); for (index_stats.distribution.prefixes[1].samples, 0..) |sample, offset| { try std.testing.expectEqual(offset, sample.less_distinct); }}test "catalog rejects duplicate object names" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); var catalog = try Catalog.open(&database, .{}); _ = try catalog.createRelation(std.testing.allocator, .{ .name = "items", }, .{ .durability = .buffered }); try std.testing.expectError(error.ObjectExists, catalog.createRelation(std.testing.allocator, .{ .name = "items", }, .{ .durability = .buffered })); try std.testing.expectError(error.RelationNotFound, catalog.openRelation(std.testing.allocator, "missing"));}test "catalog allocates distinct roots for multiple relations" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 160 }); var catalog = try Catalog.open(&database, .{}); const indexes = [_]IndexDefinition{.{ .name = "items_value", .fields = &.{0}, }}; const first = try catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &indexes, }, .{ .durability = .buffered }); try std.testing.expectEqual(@as(u64, 1), first.schema.version); const second = try catalog.createRelation(std.testing.allocator, .{ .name = "users", }, .{ .durability = .buffered }); try std.testing.expectEqual(@as(u64, 2), second.schema.version); try std.testing.expectEqual(@as(u64, 2), (try catalog.schemaState(std.testing.allocator)).version); var items = try catalog.openRelation(std.testing.allocator, "items"); defer items.deinit(); var users = try catalog.openRelation(std.testing.allocator, "users"); defer users.deinit(); try std.testing.expect(items.relation.table.rows.root_page > default_root_page); try std.testing.expect(users.relation.table.rows.root_page > items.relation.table.rows.root_page); _ = try items.relation.put(std.testing.allocator, 1, &.{.{ .integer = 7 }}, .{ .durability = .buffered }); _ = try users.relation.put(std.testing.allocator, 1, &.{.{ .text = "Ada" }}, .{ .durability = .buffered }); var lookup: index_mod.Scan = undefined; try items.relation.lookup(&lookup, std.testing.allocator, 0, &.{.{ .integer = 7 }}); defer lookup.deinit(); try std.testing.expectEqual(@as(i64, 1), (try lookup.next()).?.rowid); try std.testing.expect(try lookup.next() == null); const bytes = (try users.relation.get(std.testing.allocator, 1)).?; defer std.testing.allocator.free(bytes); const view = try row.View.init(bytes); try std.testing.expectEqualStrings("Ada", (try view.column(0)).text);}test "catalog rejects duplicate names inside one relation definition" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); var catalog = try Catalog.open(&database, .{}); const table_named_index = [_]IndexDefinition{.{ .name = "items", .fields = &.{0}, }}; try std.testing.expectError(error.ObjectExists, catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &table_named_index, }, .{ .durability = .buffered })); const duplicate_indexes = [_]IndexDefinition{ .{ .name = "items_value", .fields = &.{0}, }, .{ .name = "items_value", .fields = &.{1}, }, }; try std.testing.expectError(error.ObjectExists, catalog.createRelation(std.testing.allocator, .{ .name = "items", .indexes = &duplicate_indexes, }, .{ .durability = .buffered })); const duplicate_columns = [_]ColumnDefinition{ .{ .name = "name" }, .{ .name = "Name" }, }; try std.testing.expectError(error.CatalogCorrupt, catalog.createRelation(std.testing.allocator, .{ .name = "items", .columns = &duplicate_columns, }, .{ .durability = .buffered }));}test "catalog diagnoses pre identity object rows as unsupported format" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); try forgeCatalog(&database, .{ .format = format_version, .version = 1 }, &.{.{ .rowid = first_object_rowid, .values = &.{ .{ .integer = relation_kind }, .{ .text = "items" }, .{ .text = "items" }, .{ .integer = 3 }, .{ .blob = "" }, .{ .blob = "" }, } }}); var catalog = try Catalog.open(&database, .{}); try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator)); try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.openRelation(std.testing.allocator, "items"));}test "catalog rejects newer schema format before object rows decode" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); try forgeCatalog(&database, .{ .format = format_version + 1, .version = 1 }, &.{.{ .rowid = first_object_rowid, .values = &.{ .{ .integer = 99 }, .{ .text = "items" }, .{ .text = "items" }, .{ .integer = 3 }, .{ .blob = "" }, .{ .blob = "" }, .{ .integer = 0 }, } }}); var catalog = try Catalog.open(&database, .{}); try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));}test "catalog diagnoses pre identity stats rows as unsupported format" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); try forgeCatalog(&database, .{ .format = format_version, .version = 1 }, &.{.{ .rowid = first_object_rowid, .values = &.{ .{ .integer = stats_kind }, .{ .text = "items" }, .{ .text = "items" }, .{ .integer = 3 }, .{ .blob = "" }, .{ .blob = "" }, } }}); var catalog = try Catalog.open(&database, .{}); try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));}test "catalog diagnoses schema row arity drift as unsupported format" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "catalog.db", .wal = "catalog.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 96 }); const schema_blob = @as([schema_blob_bytes]u8, @splat(0)); try forgeCatalog(&database, null, &.{.{ .rowid = schema_rowid, .values = &.{ .{ .integer = schema_kind }, .{ .text = schema_name }, .{ .text = "" }, .{ .integer = 0 }, .{ .blob = &schema_blob }, .{ .blob = "" }, .{ .integer = 0 }, } }}); var catalog = try Catalog.open(&database, .{}); try std.testing.expectError(error.UnsupportedCatalogFormat, catalog.schemaState(std.testing.allocator));}const RawCatalogRow = struct { rowid: i64, values: []const row.Value,};fn forgeCatalog(database: *file.Database, catalog_schema: ?Schema, rows: []const RawCatalogRow) !void { const space = try space_mod.Space.open(database, .{ .meta_page = default_meta_page, .roots = &.{.{ .root_page = default_root_page }}, }); var schema = try space.rowidTable(default_root_page); var write = try space.beginWrite(); defer write.deinit(); if (catalog_schema) |value| try putSchemaRow(&schema, &write, value); for (rows) |raw| try schema.putIn(&write, raw.rowid, raw.values); _ = try write.commit(.{ .durability = .buffered });}fn testingHeader() wal.Header { return .{ .sequence = 1101, .salt = .{ .first = 0x9191_c3c3, .second = 0x6363_d4d4 }, };}fn recoveredHeader() wal.Header { return .{ .sequence = 1102, .salt = .{ .first = 0xaaaa_5555, .second = 0xbbbb_6666 }, };}Source: lib/sql/src/root.zig:24
zig
pub const catalog = @import("catalog.zig");Audit
| Definitions | 22 |
|---|---|
| Public names | 22 |
| Members | 25 |
| Version | 26.7.0 |
| Revision | daab053ee433 |