tiny.smg.storage.database.lifecycle
Defined in storage.database.
API (12)
Actions
Public operations.
Database.deinitReader.deinitcheckpointIfWalLargecommitcommitIfDirtyopenopenReaderreaderSchemaReady
Types and contracts
Public types and contracts.
Source
Source: tools/smg/src/storage/database/lifecycle.zig
zig
const std = @import("std");const sql = @import("sql");const storage = @import("../root.zig");const limits_mod = @import("../../limits/root.zig");const paths = storage.paths;const rows = storage.rows;pub const Error = rows.Error || sql.lifecycle.Error;const branch_name = "main";const checkpoint_wal_bytes: usize = 4 * 1024 * 1024;const fs_io = std.Options.debug_io;const database_header = sql.wal.Header{ .sequence = 0x534d_4744, .salt = .{ .first = 0x534d_4744, .second = 0x4752_5048 },};pub const Database = struct { allocator: std.mem.Allocator, workspace: *sql.LifecycleWorkspace, limits: limits_mod.Storage, file: *sql.FileDatabase, history: sql.LazyHistory, connection: sql.Connection, recovery: sql.LifecycleRecovery, pub fn deinit(self: *Database) void { const allocator = self.allocator; const workspace = self.workspace; sql.lifecycle.close(self.file, &self.history, &self.connection); workspace.deallocate(allocator); allocator.destroy(workspace); self.* = undefined; }};pub const Reader = struct { allocator: std.mem.Allocator, workspace: *sql.LifecycleWorkspace, file: *sql.FileReadOnlyDatabase, catalog: sql.CatalogReader, head: sql.Hash, root: sql.Hash, pub fn deinit(self: *Reader) void { const allocator = self.allocator; const workspace = self.workspace; self.file.deinit(); self.allocator.destroy(self.file); workspace.deallocate(allocator); allocator.destroy(workspace); self.* = undefined; }};pub const ReaderOpenResult = union(enum) { ready: Reader, repair_required: sql.LifecycleReadRepairReason,};pub fn open( allocator: std.mem.Allocator, dir: std.Io.Dir, limits: limits_mod.Storage,) Error!Database { const workspace = try allocator.create(sql.LifecycleWorkspace); errdefer allocator.destroy(workspace); workspace.* = try sql.LifecycleWorkspace.allocate(allocator, .{ .header = database_header, .max_wal_bytes = limits.wal_recovery_limit_bytes, .path_storage = sql.file.PathStorage.Limits.forDirect(.{ .database = paths.database_file_name, .wal = paths.wal_file_name, }), }); errdefer workspace.deallocate(allocator); var lifecycle = try sql.lifecycle.open(allocator, workspace, dir, .{ .paths = .{ .database = paths.database_file_name, .wal = paths.wal_file_name }, .history_path = paths.history_file_name, .branch = branch_name, .header = database_header, .max_wal_bytes = limits.wal_recovery_limit_bytes, }); errdefer lifecycle.deinit(); var database = Database{ .allocator = lifecycle.allocator, .workspace = workspace, .limits = limits, .file = lifecycle.file, .history = lifecycle.history, .connection = lifecycle.connection, .recovery = lifecycle.recovery, }; try ensureSchema(&database); return database;}pub fn openReader( allocator: std.mem.Allocator, dir: std.Io.Dir, limits: limits_mod.Storage,) Error!ReaderOpenResult { const workspace = try allocator.create(sql.LifecycleWorkspace); errdefer allocator.destroy(workspace); workspace.* = try sql.LifecycleWorkspace.allocate(allocator, .{ .header = database_header, .max_wal_bytes = limits.wal_recovery_limit_bytes, .path_storage = sql.file.PathStorage.Limits.forDirect(.{ .database = paths.database_file_name, .wal = paths.wal_file_name, }), }); errdefer workspace.deallocate(allocator); const opened = switch (try sql.lifecycle.openReadOnly(allocator, workspace, dir, .{ .paths = .{ .database = paths.database_file_name, .wal = paths.wal_file_name }, .history_path = paths.history_file_name, .branch = branch_name, .header = database_header, .max_wal_bytes = limits.wal_recovery_limit_bytes, })) { .ready => |ready| ready, .repair_required => |reason| { workspace.deallocate(allocator); allocator.destroy(workspace); return .{ .repair_required = reason }; }, }; return .{ .ready = .{ .allocator = opened.allocator, .workspace = workspace, .file = opened.file, .catalog = opened.catalog, .head = opened.head, .root = opened.root, } };}pub fn readerSchemaReady(reader: *Reader) Error!bool { return try relationHasIndex(&reader.catalog, reader.allocator, rows.nodes_relation, rows.node_name_index) and try relationHasIndex(&reader.catalog, reader.allocator, rows.edges_relation, rows.edge_source_index) and try relationHasIndex(&reader.catalog, reader.allocator, rows.edges_relation, rows.edge_target_index) and try relationHasIndex(&reader.catalog, reader.allocator, rows.concept_documents_relation, rows.concept_name_index);}pub fn commitIfDirty(opened: *Database) Error!void { if (!(try opened.connection.checkout()).working.dirty()) return; _ = try commit(opened); try checkpointIfWalLarge(opened);}pub fn commit(opened: *Database) Error!sql.Hash { return try sql.lifecycle.commit(&opened.connection, &opened.history, opened.file);}pub fn checkpointIfWalLarge(opened: *Database) Error!void { try sql.lifecycle.checkpointIfWalLarge(opened.file, database_header, checkpoint_wal_bytes);}fn checkpoint(opened: *Database) Error!void { try sql.lifecycle.checkpoint(opened.file, database_header);}fn ensureSchema(opened: *Database) Error!void { const statements = [_]struct { name: []const u8, source: []const u8 }{ .{ .name = rows.nodes_relation, .source = "CREATE TABLE " ++ rows.nodes_relation ++ " (" ++ rows.node_columns ++ ")" }, .{ .name = rows.edges_relation, .source = "CREATE TABLE " ++ rows.edges_relation ++ " (" ++ rows.edge_columns ++ ")" }, .{ .name = rows.concept_documents_relation, .source = "CREATE TABLE " ++ rows.concept_documents_relation ++ " (" ++ rows.concept_document_columns ++ ")" }, }; var names = try opened.connection.catalog.relationNames(opened.allocator); defer names.deinit(); var created = false; for (statements) |statement| { var exists = false; for (names.names) |name| { if (std.mem.eql(u8, name, statement.name)) { exists = true; break; } } if (exists) continue; var result = try opened.connection.execute(opened.allocator, statement.source, .{ .durability = .buffered }); result.deinit(opened.allocator); created = true; } if (created) { _ = try commit(opened); try checkpoint(opened); } const indexes = [_]struct { relation: []const u8, name: []const u8, columns: []const u8 }{ .{ .relation = rows.nodes_relation, .name = rows.node_name_index, .columns = "name" }, .{ .relation = rows.edges_relation, .name = rows.edge_source_index, .columns = "source" }, .{ .relation = rows.edges_relation, .name = rows.edge_target_index, .columns = "target" }, .{ .relation = rows.concept_documents_relation, .name = rows.concept_name_index, .columns = "name" }, }; for (indexes) |index| { if (try relationHasIndex(&opened.connection.catalog, opened.allocator, index.relation, index.name)) continue; const source = try std.fmt.allocPrint(opened.allocator, "CREATE INDEX {s} ON {s} ({s})", .{ index.name, index.relation, index.columns }); defer opened.allocator.free(source); var result = try opened.connection.execute(opened.allocator, source, .{ .durability = .buffered }); result.deinit(opened.allocator); _ = try commit(opened); try checkpoint(opened); }}fn relationHasIndex(catalog: anytype, allocator: std.mem.Allocator, relation: []const u8, index: []const u8) Error!bool { var handle = catalog.openRelation(allocator, relation) catch |err| switch (err) { error.RelationNotFound => return false, else => return err, }; defer handle.deinit(); for (handle.index_definitions) |definition| { if (std.mem.eql(u8, definition.name, index)) return true; } return false;}test "smg database round-trips nodes and edges with history" { const database = @import("root.zig"); const edits = database.edits; const read = database.read; const model = @import("../../root.zig").model; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const metadata = [_]model.Pair{ .{ .key = "source", .value = "scan" }, .{ .key = "metrics", .value = "{\"cyclomatic_complexity\":2}", .json = true }, }; const node = model.Node{ .name = "app.main", .type = model.NodeType.function, .file = "src/app.zig", .line = 3, .end_line = 9, .docstring = "doc body", .metadata = &metadata, }; const edge = model.Edge{ .source = "app", .target = "app.main", .rel = model.RelType.contains, }; var first_head: sql.Hash = undefined; { var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); try edits.apply(&db, .{ .node_puts = &.{.{ .rowid = 1, .node = node }}, .edge_puts = &.{.{ .rowid = 1, .edge = edge }}, }); try std.testing.expect((try db.connection.checkout()).working.dirty()); first_head = try commit(&db); try std.testing.expect(!(try db.connection.checkout()).working.dirty()); } var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); try std.testing.expect(!(try db.connection.checkout()).working.dirty()); try std.testing.expect(sql.version.same(first_head, (try db.connection.checkout()).head)); { var node_handle = try db.connection.catalog.openRelation(std.testing.allocator, rows.nodes_relation); defer node_handle.deinit(); try std.testing.expectEqual(@as(usize, 1), node_handle.index_definitions.len); try std.testing.expectEqualStrings(rows.node_name_index, node_handle.index_definitions[0].name); } { var edge_handle = try db.connection.catalog.openRelation(std.testing.allocator, rows.edges_relation); defer edge_handle.deinit(); try std.testing.expectEqual(@as(usize, 2), edge_handle.index_definitions.len); try std.testing.expectEqualStrings(rows.edge_source_index, edge_handle.index_definitions[0].name); try std.testing.expectEqualStrings(rows.edge_target_index, edge_handle.index_definitions[1].name); } const nodes = try read.databaseNodes(&db, std.testing.allocator, std.testing.allocator); defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, nodes); try std.testing.expectEqual(@as(usize, 1), nodes.len); try std.testing.expectEqual(@as(i64, 1), nodes[0].rowid); try std.testing.expectEqualStrings("app.main", nodes[0].node.name); try std.testing.expectEqual(@as(?i64, 3), nodes[0].node.line); try std.testing.expectEqual(@as(?i64, 9), nodes[0].node.end_line); try std.testing.expectEqualStrings("doc body", nodes[0].node.docstring.?); try std.testing.expectEqualStrings("scan", model.pairValue(nodes[0].node.metadata, "source").?); try std.testing.expectEqualStrings("{\"cyclomatic_complexity\":2}", model.pairValue(nodes[0].node.metadata, "metrics").?); try std.testing.expect(nodes[0].node.metadata[1].json); const edges = try read.databaseEdges(&db, std.testing.allocator, std.testing.allocator); defer rows.freeEdgeRowSlice(std.testing.allocator, std.testing.allocator, edges); try std.testing.expectEqual(@as(usize, 1), edges.len); try std.testing.expectEqualStrings("app", edges[0].edge.source); try std.testing.expectEqualStrings("app.main", edges[0].edge.target); try std.testing.expectEqual(@as(usize, 0), edges[0].edge.metadata.len);}test "smg database updates and deletes survive checkpointed reopen" { const database = @import("root.zig"); const edits = database.edits; const read = database.read; const model = @import("../../root.zig").model; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const kept = model.Node{ .name = "app.kept", .type = model.NodeType.function }; const dropped = model.Node{ .name = "app.dropped", .type = model.NodeType.function }; { var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); try edits.apply(&db, .{ .node_puts = &.{ .{ .rowid = 1, .node = kept }, .{ .rowid = 2, .node = dropped } }, }); _ = try commit(&db); var renamed = kept; renamed.line = 42; try edits.apply(&db, .{ .node_puts = &.{.{ .rowid = 1, .node = renamed }}, .node_deletes = &.{2}, }); _ = try commit(&db); try checkpoint(&db); } var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); const nodes = try read.databaseNodes(&db, std.testing.allocator, std.testing.allocator); defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, nodes); try std.testing.expectEqual(@as(usize, 1), nodes.len); try std.testing.expectEqualStrings("app.kept", nodes[0].node.name); try std.testing.expectEqual(@as(?i64, 42), nodes[0].node.line);}test "smg database rebuilds unreadable history from live state" { const database = @import("root.zig"); const edits = database.edits; const read = database.read; const model = @import("../../root.zig").model; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const node = model.Node{ .name = "app.rebuild", .type = model.NodeType.function }; { var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); try edits.apply(&db, .{ .node_puts = &.{.{ .rowid = 1, .node = node }} }); _ = try commit(&db); try checkpoint(&db); } { var file = try tmp.dir.createFile(fs_io, paths.history_file_name, .{ .read = true, .truncate = false }); var stale_version: [4]u8 = undefined; std.mem.writeInt(u32, stale_version[0..], 1, .big); try file.writePositionalAll(fs_io, stale_version[0..], 4); file.close(fs_io); } var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); try std.testing.expect(!(try db.connection.checkout()).working.dirty()); const nodes = try read.databaseNodes(&db, std.testing.allocator, std.testing.allocator); defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, nodes); try std.testing.expectEqual(@as(usize, 1), nodes.len); try std.testing.expectEqualStrings("app.rebuild", nodes[0].node.name); const next = model.Node{ .name = "app.next", .type = model.NodeType.function }; try edits.apply(&db, .{ .node_puts = &.{.{ .rowid = 2, .node = next }} }); _ = try commit(&db);}test "smg database tolerates empty graph edits" { const database = @import("root.zig"); const edits = database.edits; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var db = try open(std.testing.allocator, tmp.dir, testingLimits()); defer db.deinit(); try edits.apply(&db, .{}); try std.testing.expect(!(try db.connection.checkout()).working.dirty());}test "smg database installs query indexes on existing unindexed relations" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var raw = try sql.lifecycle.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = paths.database_file_name, .wal = paths.wal_file_name }, .history_path = paths.history_file_name, .branch = branch_name, .header = database_header, .max_wal_bytes = testingLimits().wal_recovery_limit_bytes, }); defer raw.deinit(); var nodes_result = try raw.connection.execute(std.testing.allocator, "CREATE TABLE nodes (name, type, file, line, end_line, docstring, metadata)", .{ .durability = .buffered }); nodes_result.deinit(std.testing.allocator); var edges_result = try raw.connection.execute(std.testing.allocator, "CREATE TABLE edges (source, rel, target, metadata)", .{ .durability = .buffered }); edges_result.deinit(std.testing.allocator); _ = try sql.lifecycle.commit(&raw.connection, &raw.history, raw.file); } var upgraded = try open(std.testing.allocator, tmp.dir, testingLimits()); defer upgraded.deinit(); try std.testing.expect(try relationHasIndex(&upgraded.connection.catalog, std.testing.allocator, rows.nodes_relation, rows.node_name_index)); try std.testing.expect(try relationHasIndex(&upgraded.connection.catalog, std.testing.allocator, rows.edges_relation, rows.edge_source_index)); try std.testing.expect(try relationHasIndex(&upgraded.connection.catalog, std.testing.allocator, rows.edges_relation, rows.edge_target_index));}fn testingLimits() limits_mod.Storage { return @import("../../root.zig").default_limits.storage;}Source: tools/smg/src/storage/database/root.zig:2
zig
pub const lifecycle = @import("lifecycle.zig");Audit
| Definitions | 13 |
|---|---|
| Public names | 25 |
| Members | 15 |
| Version | 26.7.0 |
| Revision | daab053ee433 |