Skip to documentation
SLOP

tiny.sql.catalog

Reference tiny.sql catalog

Defined in tiny.sql.

API (38)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallsNo direct callersTableScandeinitTableScannextRowIdTabledeleteIncatalog.MaterializationclearRelationStats
Static calls · unresolved targets: 1 · external targets: 2.
Called byCallsNo direct callersprivate sourcelib.sql.src.catalogbumpSchemaVersionprivate sourcelib.sql.src.catalogputSchemaRowTreeWritecommitcatalog.Materializationcommit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callersprivate sourcelib.sql.src.catalog.MaterializationallocateMaterializedRelationprivate sourcelib.sql.src.catalog.MaterializationopenMaterializedRelationprivate sourcelib.sql.src.catalog.MaterializationprepareRelationCreationprivate sourcelib.sql.src.catalog.MaterializationwriteRelationCatalogcatalog.MaterializationcreateRelation
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callersTreeWritedeinitcatalog.Materializationdeinit
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallsNo direct callersprivate sourcelib.sql.src.catalog.MaterializationrelationDroppedprivate sourcelib.sql.src.catalog.MaterializationremoveRootTableScandeinitTableScannextRowIdTabledeleteIncatalog.MaterializationdropRelation
Static calls · unresolved targets: 3 · external targets: 4.
Called byCallsNo direct callersprivate sourcelib.sql.src.catalogencodeSummaryprivate sourcelib.sql.src.catalogputCatalogRowprivate sourcelib.sql.src.catalogsameLogicalSummaryprivate sourcelib.sql.src.catalogvalidatePreparedStatscatalog.MaterializationrefreshRelationStats
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsNo direct callersCatalogRelationStatsdeinitcatalog.PreparedRelationStatsdeinit
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsCatalogcreateRelationCatalogprepareRelationStatsprivate sourcelib.sql.src.catalog.MaterializationprepareRelationCreationrowencodedSizecatalogvalidateDefinition
Static calls · unresolved targets: 0 · external targets: 0.

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

Definitions22
Public names22
Members25
Version26.7.0
Revisiondaab053ee433