tiny.smg.storage.sync
Defined in storage.
API (16)
Actions
Public operations.
ChangeSet.deinitGraphPlan.Capacity.deriveGraphPlan.activateGraphPlan.deinitGraphPlan.initbeginRefreshfinishRefreshrefreshIncompletereplaceGraphreplaceGraphObserved
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
Source
Source: tools/smg/src/storage/root.zig:13
zig
pub const sync = @import("sync.zig");Source: tools/smg/src/storage/sync.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const sql = @import("sql");const sys = @import("sys");const smg = @import("../root.zig");const storage = smg.storage;const database = storage.database;const edits = database.edits;const read = database.read;const rows = storage.rows;const graph_mod = smg.graph;const model = smg.model;const text = smg.text;const PlannedPut = struct { rowid: i64, index: u32,};const GraphIndex = u32;const PlannedEdits = struct { node_puts: []const PlannedPut, node_deletes: []const i64, edge_puts: []const PlannedPut, edge_deletes: []const i64,};pub const ChangeSet = struct { node_puts: []rows.NodePut, node_deletes: []i64, edge_puts: usize, edge_deletes: usize, pub fn deinit(self: *ChangeSet, allocator: std.mem.Allocator) void { freeSlice(rows.NodePut, allocator, self.node_puts); freeSlice(i64, allocator, self.node_deletes); self.* = undefined; }};pub const GraphPlan = struct { pub const Limits = struct { desired_nodes: usize, stored_nodes: usize, desired_edges: usize, stored_edges: usize, batch_size: usize, }; pub const Capacity = struct { node_order: usize, node_puts: usize, node_deletes: usize, edge_order: usize, edge_key_ptrs: usize, edge_puts: usize, edge_deletes: usize, seen_node_bytes: usize, seen_edge_bytes: usize, batch_node_puts: usize, batch_edge_puts: usize, batch_size: usize, bytes: usize, pub fn derive(limits: Limits) error{ CapacityOverflow, InvalidEditBatchSize }!Capacity { if (limits.batch_size == 0) return error.InvalidEditBatchSize; if (limits.desired_nodes > std.math.maxInt(u32)) return error.CapacityOverflow; if (limits.desired_edges > std.math.maxInt(u32)) return error.CapacityOverflow; return .{ .node_order = limits.desired_nodes, .node_puts = limits.desired_nodes, .node_deletes = limits.stored_nodes, .edge_order = limits.desired_edges, .edge_key_ptrs = limits.desired_edges, .edge_puts = limits.desired_edges, .edge_deletes = limits.stored_edges, .seen_node_bytes = bitmapBytes(limits.desired_nodes), .seen_edge_bytes = bitmapBytes(limits.desired_edges), .batch_node_puts = @min(limits.desired_nodes, limits.batch_size), .batch_edge_puts = @min(limits.desired_edges, limits.batch_size), .batch_size = limits.batch_size, .bytes = try graphPlanBytes(limits), }; } }; pub const InitError = std.mem.Allocator.Error || error{ CapacityOverflow, InvalidEditBatchSize }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "smg.graph_diff_plan", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "compact_graph_relative_node_and_edge_order_indexes_29865def4529", .lifetime = .steady, .detail = "compact graph-relative node and edge order indexes bounded by desired row counts", }, .{ .id = "borrowed_combined_edge_key_pointers_indexed_by_the_34559d226c70", .lifetime = .steady, .detail = "borrowed combined-edge-key pointers indexed by the retained graph", }, .{ .id = "compact_node_and_edge_put_descriptors_bounded_by_de_139e5f58f713", .lifetime = .steady, .detail = "compact node and edge put descriptors bounded by desired row counts", }, .{ .id = "node_and_edge_delete_plans_bounded_by_stored_row_counts", .lifetime = .steady, .detail = "node and edge delete plans bounded by stored row counts", }, .{ .id = "packed_desired_row_seen_bitmaps", .lifetime = .steady, .detail = "packed desired-row seen bitmaps", }, .{ .id = "full_width_database_edits_bounded_by_one_commit_batch", .lifetime = .steady, .detail = "full-width database edits bounded by one commit batch", }, }, .excluded = &.{ "retained graph models, indexes, edge keys, suffixes, and adjacency lists", "SQL catalog, relation scans, database pages, and bounded row decode scratch", "database write staging, pager, WAL, history, and search-index storage", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "desired_nodes", "desired_nodes"), alloc_phase.capacity.bindInput(Limits, "desired_edges", "desired_edges"), alloc_phase.capacity.bindInput(Limits, "stored_nodes", "stored_nodes"), alloc_phase.capacity.bindInput(Limits, "stored_edges", "stored_edges"), alloc_phase.capacity.bindInput(Limits, "batch_size", "batch_size"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(GraphIndex, "graphindex"), alloc_phase.capacity.bindType(PlannedPut, "plannedput"), alloc_phase.capacity.bindType(i64, "i64"), alloc_phase.capacity.bindType([*]const u8, "edge_key_pointer"), alloc_phase.capacity.bindType(rows.NodePut, "node_put"), alloc_phase.capacity.bindType(rows.EdgePut, "edge_put"), }, .nodes = &.{ .{ .input = 0 }, .{ .input = 1 }, .{ .input = 2 }, .{ .input = 3 }, .{ .constant = 1 }, .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 1 } } }, .{ .add = .{ .left = 5, .right = 6 } }, .{ .product = .{ .left = 0, .right = 7 } }, .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 2 } } }, .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 3 } } }, .{ .scale = .{ .node = 4, .coefficient = .{ .size_of_concrete_type = 1 } } }, .{ .add = .{ .left = 10, .right = 11 } }, .{ .add = .{ .left = 13, .right = 12 } }, .{ .product = .{ .left = 1, .right = 14 } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 2 } } }, .{ .constant = 8 }, .{ .ceiling_division = .{ .left = 0, .right = 17 } }, .{ .ceiling_division = .{ .left = 1, .right = 17 } }, .{ .input = 4 }, .{ .conditional = .{ .predicate = .{ .comparison = .less_than, .left = 0, .right = 20 }, .when_true = 0, .when_false = 20 } }, .{ .scale = .{ .node = 21, .coefficient = .{ .size_of_concrete_type = 4 } } }, .{ .conditional = .{ .predicate = .{ .comparison = .less_than, .left = 1, .right = 20 }, .when_true = 1, .when_false = 20 } }, .{ .scale = .{ .node = 23, .coefficient = .{ .size_of_concrete_type = 5 } } }, .{ .add = .{ .left = 8, .right = 9 } }, .{ .add = .{ .left = 25, .right = 15 } }, .{ .add = .{ .left = 26, .right = 16 } }, .{ .add = .{ .left = 27, .right = 18 } }, .{ .add = .{ .left = 28, .right = 19 } }, .{ .add = .{ .left = 29, .right = 22 } }, .{ .add = .{ .left = 30, .right = 24 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 31, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "checked byte arithmetic and exact acquisitions reject overflow or OOM before the plan activates", }, .risks = .{ .transitive = .{ .status = .open, .detail = "streaming row decode and SQL scans remain outside the fixed plan and lack a transitive allocation-closure certificate", }, .foreign = .{ .status = .excluded, .detail = "filesystem and durable database effects occur only after plan construction and are outside plan-owned storage", }, }, .obligations = &.{ .{ .key = "smg_graph_diff_plan_capacity_capacity_model", .role = .capacity_model }, .{ .key = "smg_graph_diff_plan_capacity_overload", .role = .overload }, .{ .key = "smg_graph_diff_plan_oom", .role = .overload }, .{ .key = "smg_graph_diff_plan_sealed", .role = .custom }, .{ .key = "smg_graph_diff_plan_streaming_transitive_risk", .role = .transitive_risk }, .{ .key = "smg_graph_diff_plan_streaming_foreign_risk", .role = .foreign_risk }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; phase: alloc_phase.capacity.Phase, capacity: Capacity, node_order: []GraphIndex, node_puts: []PlannedPut, node_deletes: []i64, edge_order: []GraphIndex, edge_key_ptrs: [][*]const u8, edge_puts: []PlannedPut, edge_deletes: []i64, seen_nodes: []u8, seen_edges: []u8, batch_node_puts: []rows.NodePut, batch_edge_puts: []rows.EdgePut, node_put_count: usize = 0, node_delete_count: usize = 0, edge_put_count: usize = 0, edge_delete_count: usize = 0, pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!GraphPlan { const capacity = try Capacity.derive(limits); const node_order = try allocSlice(GraphIndex, allocator, capacity.node_order); errdefer freeSlice(GraphIndex, allocator, node_order); const node_puts = try allocSlice(PlannedPut, allocator, capacity.node_puts); errdefer freeSlice(PlannedPut, allocator, node_puts); const node_deletes = try allocSlice(i64, allocator, capacity.node_deletes); errdefer freeSlice(i64, allocator, node_deletes); const edge_order = try allocSlice(GraphIndex, allocator, capacity.edge_order); errdefer freeSlice(GraphIndex, allocator, edge_order); const edge_key_ptrs = try allocSlice([*]const u8, allocator, capacity.edge_key_ptrs); errdefer freeSlice([*]const u8, allocator, edge_key_ptrs); const edge_puts = try allocSlice(PlannedPut, allocator, capacity.edge_puts); errdefer freeSlice(PlannedPut, allocator, edge_puts); const edge_deletes = try allocSlice(i64, allocator, capacity.edge_deletes); errdefer freeSlice(i64, allocator, edge_deletes); const seen_nodes = try allocSlice(u8, allocator, capacity.seen_node_bytes); errdefer freeSlice(u8, allocator, seen_nodes); const seen_edges = try allocSlice(u8, allocator, capacity.seen_edge_bytes); errdefer freeSlice(u8, allocator, seen_edges); const batch_node_puts = try allocSlice(rows.NodePut, allocator, capacity.batch_node_puts); errdefer freeSlice(rows.NodePut, allocator, batch_node_puts); const batch_edge_puts = try allocSlice(rows.EdgePut, allocator, capacity.batch_edge_puts); return .{ .phase = .initialization, .capacity = capacity, .node_order = node_order, .node_puts = node_puts, .node_deletes = node_deletes, .edge_order = edge_order, .edge_key_ptrs = edge_key_ptrs, .edge_puts = edge_puts, .edge_deletes = edge_deletes, .seen_nodes = seen_nodes, .seen_edges = seen_edges, .batch_node_puts = batch_node_puts, .batch_edge_puts = batch_edge_puts, }; } fn order(self: *GraphPlan, graph: graph_mod.Graph) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.node_order.len == graph.nodes.items.len); std.debug.assert(self.edge_order.len == graph.edges.items.len); std.debug.assert(self.edge_key_ptrs.len == graph.edges.items.len); for (self.node_order, 0..) |*entry, index| entry.* = @intCast(index); for (self.edge_order, 0..) |*entry, index| entry.* = @intCast(index); var edge_iterator = graph.edge_index.iterator(); var edge_key_count: usize = 0; while (edge_iterator.next()) |entry| : (edge_key_count += 1) { self.edge_key_ptrs[entry.value_ptr.*] = entry.key_ptr.*.ptr; } std.debug.assert(edge_key_count == self.edge_key_ptrs.len); std.mem.sort(GraphIndex, self.node_order, graph.nodes.items, nodeIndexLess); std.mem.sort(GraphIndex, self.edge_order, EdgeSort{ .edges = graph.edges.items, .key_ptrs = self.edge_key_ptrs, .encoded_order = encodedEdgeOrderMatchesFields(graph.edges.items), }, edgeIndexLess); } pub fn activate(self: *GraphPlan) void { std.debug.assert(self.phase == .initialization); @memset(self.seen_nodes, 0); @memset(self.seen_edges, 0); self.phase = .steady; } pub fn deinit(self: *GraphPlan, allocator: std.mem.Allocator) void { std.debug.assert(self.phase != .teardown); self.phase = .teardown; freeSlice(GraphIndex, allocator, self.node_order); freeSlice(PlannedPut, allocator, self.node_puts); freeSlice(i64, allocator, self.node_deletes); freeSlice(GraphIndex, allocator, self.edge_order); freeSlice([*]const u8, allocator, self.edge_key_ptrs); freeSlice(PlannedPut, allocator, self.edge_puts); freeSlice(i64, allocator, self.edge_deletes); freeSlice(u8, allocator, self.seen_nodes); freeSlice(u8, allocator, self.seen_edges); freeSlice(rows.NodePut, allocator, self.batch_node_puts); freeSlice(rows.EdgePut, allocator, self.batch_edge_puts); self.* = undefined; } fn markNode(self: *GraphPlan, index: usize) bool { std.debug.assert(self.phase == .steady); std.debug.assert(index < self.node_order.len); return markBit(self.seen_nodes, index); } fn markEdge(self: *GraphPlan, index: usize) bool { std.debug.assert(self.phase == .steady); std.debug.assert(index < self.edge_order.len); return markBit(self.seen_edges, index); } fn nodeSeen(self: *const GraphPlan, index: usize) bool { std.debug.assert(self.phase == .steady); std.debug.assert(index < self.node_order.len); return bitSet(self.seen_nodes, index); } fn edgeSeen(self: *const GraphPlan, index: usize) bool { std.debug.assert(self.phase == .steady); std.debug.assert(index < self.edge_order.len); return bitSet(self.seen_edges, index); } fn putNode(self: *GraphPlan, rowid: i64, index: usize) void { std.debug.assert(self.phase == .steady); std.debug.assert(self.node_put_count < self.node_puts.len); std.debug.assert(index < self.node_order.len); self.node_puts[self.node_put_count] = .{ .rowid = rowid, .index = @intCast(index) }; self.node_put_count += 1; } fn deleteNode(self: *GraphPlan, rowid: i64) void { std.debug.assert(self.phase == .steady); std.debug.assert(self.node_delete_count < self.node_deletes.len); self.node_deletes[self.node_delete_count] = rowid; self.node_delete_count += 1; } fn putEdge(self: *GraphPlan, rowid: i64, index: usize) void { std.debug.assert(self.phase == .steady); std.debug.assert(self.edge_put_count < self.edge_puts.len); std.debug.assert(index < self.edge_order.len); self.edge_puts[self.edge_put_count] = .{ .rowid = rowid, .index = @intCast(index) }; self.edge_put_count += 1; } fn deleteEdge(self: *GraphPlan, rowid: i64) void { std.debug.assert(self.phase == .steady); std.debug.assert(self.edge_delete_count < self.edge_deletes.len); self.edge_deletes[self.edge_delete_count] = rowid; self.edge_delete_count += 1; } fn edits(self: *const GraphPlan) PlannedEdits { std.debug.assert(self.phase == .steady); return .{ .node_puts = self.node_puts[0..self.node_put_count], .node_deletes = self.node_deletes[0..self.node_delete_count], .edge_puts = self.edge_puts[0..self.edge_put_count], .edge_deletes = self.edge_deletes[0..self.edge_delete_count], }; } fn materializeNodePuts(self: *GraphPlan, graph: graph_mod.Graph, planned: []const PlannedPut) []const rows.NodePut { std.debug.assert(self.phase == .steady); std.debug.assert(planned.len <= self.batch_node_puts.len); for (planned, 0..) |put, index| { self.batch_node_puts[index] = .{ .rowid = put.rowid, .node = graph.nodes.items[put.index], }; } return self.batch_node_puts[0..planned.len]; } fn materializeEdgePuts(self: *GraphPlan, graph: graph_mod.Graph, planned: []const PlannedPut) []const rows.EdgePut { std.debug.assert(self.phase == .steady); std.debug.assert(planned.len <= self.batch_edge_puts.len); for (planned, 0..) |put, index| { self.batch_edge_puts[index] = .{ .rowid = put.rowid, .edge = graph.edges.items[put.index], }; } return self.batch_edge_puts[0..planned.len]; }};comptime { alloc_phase.capacity.requireAllocatorExactOwnerShape(GraphPlan);}pub fn beginRefresh(allocator: std.mem.Allocator, root: []const u8) !void { const path = try refreshPath(allocator, root); defer allocator.free(path); try text.writeFile(path, "graph refresh in progress\n");}pub fn finishRefresh(allocator: std.mem.Allocator, root: []const u8) !void { const path = try refreshPath(allocator, root); defer allocator.free(path); sys.fs.deleteFile(path) catch |err| switch (err) { error.FileNotFound => {}, else => return err, };}pub fn refreshIncomplete(allocator: std.mem.Allocator, root: []const u8) !bool { const path = try refreshPath(allocator, root); defer allocator.free(path); return sys.fs.exists(path);}fn refreshPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 { return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name, storage.paths.refresh_file_name });}pub fn replaceGraph( allocator: std.mem.Allocator, store_database: *database.Database, graph: graph_mod.Graph,) !void { var changes = try replaceGraphObserved(allocator, store_database, graph); changes.deinit(allocator);}pub fn replaceGraphObserved( allocator: std.mem.Allocator, store_database: *database.Database, graph: graph_mod.Graph,) !ChangeSet { const stored = try read.graphCounts(store_database); var plan = try GraphPlan.init(allocator, .{ .desired_nodes = graph.nodes.items.len, .stored_nodes = stored.nodes, .desired_edges = graph.edges.items.len, .stored_edges = stored.edges, .batch_size = store_database.limits.edit_batch_size, }); defer plan.deinit(allocator); plan.order(graph); plan.activate(); try fillNodeDiff(allocator, store_database, graph, &plan); try fillEdgeDiff(allocator, store_database, graph, &plan); var changes = try changeSet(allocator, graph, plan.edits()); errdefer changes.deinit(allocator); _ = try applyBatches(store_database, graph, &plan); return changes;}fn changeSet( allocator: std.mem.Allocator, graph: graph_mod.Graph, planned: PlannedEdits,) !ChangeSet { const node_puts = try allocSlice(rows.NodePut, allocator, planned.node_puts.len); errdefer freeSlice(rows.NodePut, allocator, node_puts); for (planned.node_puts, 0..) |put, index| { node_puts[index] = .{ .rowid = put.rowid, .node = graph.nodes.items[put.index], }; } const node_deletes = try allocSlice(i64, allocator, planned.node_deletes.len); @memcpy(node_deletes, planned.node_deletes); return .{ .node_puts = node_puts, .node_deletes = node_deletes, .edge_puts = planned.edge_puts.len, .edge_deletes = planned.edge_deletes.len, };}fn applyBatches( store_database: *database.Database, graph: graph_mod.Graph, plan: *GraphPlan,) !usize { const planned = plan.edits(); var node_put_offset: usize = 0; var node_delete_offset: usize = 0; var edge_put_offset: usize = 0; var edge_delete_offset: usize = 0; var batches: usize = 0; while (node_put_offset < planned.node_puts.len or node_delete_offset < planned.node_deletes.len or edge_put_offset < planned.edge_puts.len or edge_delete_offset < planned.edge_deletes.len) { var remaining = plan.capacity.batch_size; const node_put_end = @min(node_put_offset + remaining, planned.node_puts.len); remaining -= node_put_end - node_put_offset; const edge_put_end = @min(edge_put_offset + remaining, planned.edge_puts.len); remaining -= edge_put_end - edge_put_offset; const edge_delete_end = @min(edge_delete_offset + remaining, planned.edge_deletes.len); remaining -= edge_delete_end - edge_delete_offset; const node_delete_end = @min(node_delete_offset + remaining, planned.node_deletes.len); const node_puts = plan.materializeNodePuts(graph, planned.node_puts[node_put_offset..node_put_end]); const edge_puts = plan.materializeEdgePuts(graph, planned.edge_puts[edge_put_offset..edge_put_end]); try edits.apply(store_database, .{ .node_puts = node_puts, .node_deletes = planned.node_deletes[node_delete_offset..node_delete_end], .edge_puts = edge_puts, .edge_deletes = planned.edge_deletes[edge_delete_offset..edge_delete_end], }); try database.commitIfDirty(store_database); node_put_offset = node_put_end; node_delete_offset = node_delete_end; edge_put_offset = edge_put_end; edge_delete_offset = edge_delete_end; batches += 1; } return batches;}fn fillNodeDiff( allocator: std.mem.Allocator, store_database: *database.Database, graph: graph_mod.Graph, plan: *GraphPlan,) !void { var handle = store_database.connection.catalog.openRelation( store_database.allocator, rows.nodes_relation, ) catch |err| switch (err) { error.RelationNotFound => return fillNewNodes(0, plan), else => return err, }; defer handle.deinit(); var scan: sql.TableScan = undefined; try handle.relation.scan(&scan, store_database.allocator, null, null); defer scan.deinit(); var row_arena = std.heap.ArenaAllocator.init(allocator); defer row_arena.deinit(); var max_rowid: i64 = 0; while (try scan.next()) |entry| { max_rowid = @max(max_rowid, entry.rowid); const name = try rows.nodeNameView(entry.bytes); const desired_index = graph.node_index.get(name) orelse { plan.deleteNode(entry.rowid); continue; }; if (!plan.markNode(desired_index)) { plan.deleteNode(entry.rowid); continue; } _ = row_arena.reset(.retain_capacity); const loaded = try rows.decodeNodeRow( row_arena.allocator(), entry.rowid, entry.bytes, ); if (!model.nodeEquals(loaded.node, graph.nodes.items[desired_index])) { plan.putNode(entry.rowid, desired_index); } } try fillNewNodes(max_rowid, plan);}fn fillEdgeDiff( allocator: std.mem.Allocator, store_database: *database.Database, graph: graph_mod.Graph, plan: *GraphPlan,) !void { const key_buffer = try allocSlice(u8, allocator, maxEdgeKeyLen(graph.edges.items)); defer freeSlice(u8, allocator, key_buffer); var handle = store_database.connection.catalog.openRelation( store_database.allocator, rows.edges_relation, ) catch |err| switch (err) { error.RelationNotFound => return fillNewEdges(0, plan), else => return err, }; defer handle.deinit(); var scan: sql.TableScan = undefined; try handle.relation.scan(&scan, store_database.allocator, null, null); defer scan.deinit(); var row_arena = std.heap.ArenaAllocator.init(allocator); defer row_arena.deinit(); var max_rowid: i64 = 0; while (try scan.next()) |entry| { max_rowid = @max(max_rowid, entry.rowid); const desired_index = try desiredEdgeIndex(graph, entry.bytes, key_buffer); if (desired_index == null or !plan.markEdge(desired_index.?)) { plan.deleteEdge(entry.rowid); continue; } _ = row_arena.reset(.retain_capacity); const loaded = try rows.decodeEdgeRow( row_arena.allocator(), entry.rowid, entry.bytes, ); if (!model.edgeEquals(loaded.edge, graph.edges.items[desired_index.?])) { plan.putEdge(entry.rowid, desired_index.?); } } try fillNewEdges(max_rowid, plan);}fn fillNewNodes( max_rowid: i64, plan: *GraphPlan,) !void { var next_rowid: ?i64 = std.math.add(i64, max_rowid, 1) catch null; for (plan.node_order) |order| { const index: usize = order; if (plan.nodeSeen(index)) continue; plan.putNode(next_rowid orelse return error.RowIdOverflow, index); next_rowid = std.math.add(i64, next_rowid.?, 1) catch null; }}fn fillNewEdges( max_rowid: i64, plan: *GraphPlan,) !void { var next_rowid: ?i64 = std.math.add(i64, max_rowid, 1) catch null; for (plan.edge_order) |order| { const index: usize = order; if (plan.edgeSeen(index)) continue; plan.putEdge(next_rowid orelse return error.RowIdOverflow, index); next_rowid = std.math.add(i64, next_rowid.?, 1) catch null; }}fn desiredEdgeIndex( graph: graph_mod.Graph, bytes: []const u8, key_buffer: []u8,) rows.Error!?usize { const source = try rows.edgeSourceView(bytes); const rel = try rows.edgeRelView(bytes); const target = try rows.edgeTargetView(bytes); const key_len = graph_mod.edgeKeyLen(source, rel, target); if (key_len > key_buffer.len) return null; const key = graph_mod.edgeKeyInto(key_buffer[0..key_len], source, rel, target); return graph.edge_index.get(key);}fn maxEdgeKeyLen(edges: []const model.Edge) usize { var maximum: usize = 0; for (edges) |edge| { maximum = @max( maximum, graph_mod.edgeKeyLen(edge.source, edge.rel, edge.target), ); } return maximum;}fn nodeIndexLess(nodes: []const model.Node, left: GraphIndex, right: GraphIndex) bool { const order = std.mem.order(u8, nodes[left].name, nodes[right].name); if (order != .eq) return order == .lt; return left < right;}const EdgeSort = struct { edges: []const model.Edge, key_ptrs: []const [*]const u8, encoded_order: bool,};fn edgeIndexLess(context: EdgeSort, left: GraphIndex, right: GraphIndex) bool { const left_edge = context.edges[left]; const right_edge = context.edges[right]; if (!context.encoded_order) return edgeFieldIndexLess(left_edge, right_edge, left, right); const left_key = context.key_ptrs[left][0..graph_mod.edgeKeyLen(left_edge.source, left_edge.rel, left_edge.target)]; const right_key = context.key_ptrs[right][0..graph_mod.edgeKeyLen(right_edge.source, right_edge.rel, right_edge.target)]; const order = std.mem.order(u8, left_key, right_key); if (order != .eq) return order == .lt; return left < right;}fn edgeFieldIndexLess(left_edge: model.Edge, right_edge: model.Edge, left: GraphIndex, right: GraphIndex) bool { const source = std.mem.order(u8, left_edge.source, right_edge.source); if (source != .eq) return source == .lt; const rel = std.mem.order(u8, left_edge.rel, right_edge.rel); if (rel != .eq) return rel == .lt; const target = std.mem.order(u8, left_edge.target, right_edge.target); if (target != .eq) return target == .lt; return left < right;}fn encodedEdgeOrderMatchesFields(edges: []const model.Edge) bool { for (edges) |edge| { for (edge.source) |byte| if (byte <= graph_mod.edge_key_separator) return false; for (edge.rel) |byte| if (byte <= graph_mod.edge_key_separator) return false; } return true;}fn bitmapBytes(count: usize) usize { return count / 8 + @intFromBool(count % 8 != 0);}fn markBit(bitmap: []u8, index: usize) bool { const byte_index = index / 8; const mask = bitMask(index); if (bitmap[byte_index] & mask != 0) return false; bitmap[byte_index] |= mask; return true;}fn bitSet(bitmap: []const u8, index: usize) bool { return bitmap[index / 8] & bitMask(index) != 0;}fn bitMask(index: usize) u8 { return @as(u8, 1) << @intCast(index % 8);}fn graphPlanBytes(limits: GraphPlan.Limits) error{CapacityOverflow}!usize { if (limits.desired_nodes > std.math.maxInt(u32)) return error.CapacityOverflow; if (limits.desired_edges > std.math.maxInt(u32)) return error.CapacityOverflow; var total: usize = 0; total = try addPlanBytes(total, limits.desired_nodes, @sizeOf(GraphIndex)); total = try addPlanBytes(total, limits.desired_nodes, @sizeOf(PlannedPut)); total = try addPlanBytes(total, limits.stored_nodes, @sizeOf(i64)); total = try addPlanBytes(total, limits.desired_edges, @sizeOf(GraphIndex)); total = try addPlanBytes(total, limits.desired_edges, @sizeOf([*]const u8)); total = try addPlanBytes(total, limits.desired_edges, @sizeOf(PlannedPut)); total = try addPlanBytes(total, limits.stored_edges, @sizeOf(i64)); total = try addPlanBytes(total, bitmapBytes(limits.desired_nodes), @sizeOf(u8)); total = try addPlanBytes(total, bitmapBytes(limits.desired_edges), @sizeOf(u8)); total = try addPlanBytes(total, @min(limits.desired_nodes, limits.batch_size), @sizeOf(rows.NodePut)); return try addPlanBytes(total, @min(limits.desired_edges, limits.batch_size), @sizeOf(rows.EdgePut));}fn addPlanBytes(total: usize, count: usize, size: usize) error{CapacityOverflow}!usize { const bytes = std.math.mul(usize, count, size) catch return error.CapacityOverflow; return std.math.add(usize, total, bytes) catch error.CapacityOverflow;}fn allocSlice( comptime T: type, allocator: std.mem.Allocator, count: usize,) std.mem.Allocator.Error![]T { if (count == 0) return @constCast((&[_]T{})[0..]); return try allocator.alloc(T, count);}fn freeSlice(comptime T: type, allocator: std.mem.Allocator, values: []T) void { if (values.len != 0) allocator.free(values);}test "graph edits commit in bounded batches" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var opened = try database.open(std.testing.allocator, tmp.dir, smg.default_limits.storage); defer opened.deinit(); var graph = graph_mod.init(std.testing.allocator); defer graph_mod.deinit(&graph); try graph_mod.addNode(&graph, .{ .name = "a", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "b", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "c", .type = model.NodeType.module }); var plan = try GraphPlan.init(std.testing.allocator, .{ .desired_nodes = 3, .stored_nodes = 0, .desired_edges = 0, .stored_edges = 0, .batch_size = 2, }); defer plan.deinit(std.testing.allocator); plan.order(graph); plan.activate(); plan.putNode(1, 0); plan.putNode(2, 1); plan.putNode(3, 2); try std.testing.expectEqual(@as(usize, 2), try applyBatches(&opened, graph, &plan)); const loaded = try read.databaseNodes(&opened, std.testing.allocator, std.testing.allocator); defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, loaded); try std.testing.expectEqual(@as(usize, 3), loaded.len); try std.testing.expectError(error.InvalidEditBatchSize, GraphPlan.Capacity.derive(.{ .desired_nodes = 0, .stored_nodes = 0, .desired_edges = 0, .stored_edges = 0, .batch_size = 0, }));}fn modelGraphPlanBytes(limits: GraphPlan.Limits) ?usize { if (limits.batch_size == 0) return null; if (limits.desired_nodes > std.math.maxInt(u32)) return null; if (limits.desired_edges > std.math.maxInt(u32)) return null; const counts = [_]usize{ limits.desired_nodes, limits.desired_nodes, limits.stored_nodes, limits.desired_edges, limits.desired_edges, limits.desired_edges, limits.stored_edges, limits.desired_nodes / 8 + @intFromBool(limits.desired_nodes % 8 != 0), limits.desired_edges / 8 + @intFromBool(limits.desired_edges % 8 != 0), @min(limits.desired_nodes, limits.batch_size), @min(limits.desired_edges, limits.batch_size), }; const sizes = [_]usize{ @sizeOf(GraphIndex), @sizeOf(PlannedPut), @sizeOf(i64), @sizeOf(GraphIndex), @sizeOf([*]const u8), @sizeOf(PlannedPut), @sizeOf(i64), @sizeOf(u8), @sizeOf(u8), @sizeOf(rows.NodePut), @sizeOf(rows.EdgePut), }; var total: usize = 0; for (counts, sizes) |count, size| { if (count > std.math.maxInt(usize) / size) return null; const bytes = count * size; if (total > std.math.maxInt(usize) - bytes) return null; total += bytes; } return total;}test "graph diff plan capacity matches an independent typed storage model" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(GraphPlan, "smg_graph_diff_plan_capacity_capacity_model"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(GraphPlan, "smg_graph_diff_plan_capacity_overload"), null, null, null, null, null, null, ); } for (0..64) |count| { const limits = GraphPlan.Limits{ .desired_nodes = count, .stored_nodes = count + 1, .desired_edges = count * 2, .stored_edges = count * 3, .batch_size = 7, }; const capacity = try GraphPlan.Capacity.derive(limits); try std.testing.expectEqual(modelGraphPlanBytes(limits).?, capacity.bytes); } try std.testing.expect(@sizeOf(PlannedPut) < @sizeOf(rows.NodePut)); try std.testing.expect(@sizeOf(PlannedPut) < @sizeOf(rows.EdgePut)); try std.testing.expect(@sizeOf(GraphIndex) < @sizeOf(model.Node)); try std.testing.expect(@sizeOf(GraphIndex) < @sizeOf(model.Edge)); const overflow = GraphPlan.Limits{ .desired_nodes = std.math.maxInt(usize), .stored_nodes = 0, .desired_edges = 0, .stored_edges = 0, .batch_size = 1, }; try std.testing.expect(modelGraphPlanBytes(overflow) == null); try std.testing.expectError( error.CapacityOverflow, GraphPlan.Capacity.derive(overflow), );}test "graph diff plan packs seen bits across byte boundaries" { var bitmap = [_]u8{ 0, 0 }; try std.testing.expect(markBit(&bitmap, 0)); try std.testing.expect(markBit(&bitmap, 8)); try std.testing.expect(!markBit(&bitmap, 0)); try std.testing.expect(bitSet(&bitmap, 0)); try std.testing.expect(!bitSet(&bitmap, 7)); try std.testing.expect(bitSet(&bitmap, 8));}test "graph edge index ordering preserves field tuple semantics" { const edges = [_]model.Edge{ .{ .source = "a", .rel = model.RelType.calls, .target = "z" }, .{ .source = "a\n", .rel = model.RelType.calls, .target = "z" }, }; var first_key: [graph_mod.edgeKeyLen("a", model.RelType.calls, "z")]u8 = undefined; var second_key: [graph_mod.edgeKeyLen("a\n", model.RelType.calls, "z")]u8 = undefined; const first = graph_mod.edgeKeyInto(&first_key, edges[0].source, edges[0].rel, edges[0].target); const second = graph_mod.edgeKeyInto(&second_key, edges[1].source, edges[1].rel, edges[1].target); const key_ptrs = [_][*]const u8{ first.ptr, second.ptr }; const context = EdgeSort{ .edges = &edges, .key_ptrs = &key_ptrs, .encoded_order = encodedEdgeOrderMatchesFields(&edges), }; try std.testing.expect(!context.encoded_order); try std.testing.expect(edgeIndexLess(context, 0, 1));}fn checkGraphPlanInitAllocationFailures(allocator: std.mem.Allocator) !void { var plan = try GraphPlan.init(allocator, .{ .desired_nodes = 3, .stored_nodes = 4, .desired_edges = 5, .stored_edges = 6, .batch_size = 2, }); plan.deinit(allocator);}test "graph diff plan initialization cleans allocation failure and retries" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(GraphPlan, "smg_graph_diff_plan_oom"), null, null, null, null, null, null, ); } try std.testing.checkAllAllocationFailures( std.testing.allocator, checkGraphPlanInitAllocationFailures, .{}, ); var graph = graph_mod.init(std.testing.allocator); defer graph_mod.deinit(&graph); try graph_mod.addNode(&graph, .{ .name = "a", .type = model.NodeType.module }); try graph_mod.addEdge(&graph, .{ .source = "a", .target = "a", .rel = model.RelType.calls }); var plan = try GraphPlan.init(std.testing.allocator, .{ .desired_nodes = 1, .stored_nodes = 1, .desired_edges = 1, .stored_edges = 1, .batch_size = 1, }); defer plan.deinit(std.testing.allocator); plan.order(graph); plan.activate(); try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, plan.phase);}test "graph diff plan stays sealed while filling bounded storage" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(GraphPlan, "smg_graph_diff_plan_sealed"), null, null, null, null, null, null, ); } var graph = graph_mod.init(std.testing.allocator); defer graph_mod.deinit(&graph); try graph_mod.addNode(&graph, .{ .name = "b", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "a", .type = model.NodeType.module }); try graph_mod.addEdge(&graph, .{ .source = "b", .target = "a", .rel = model.RelType.calls }); try graph_mod.addEdge(&graph, .{ .source = "a", .target = "b", .rel = model.RelType.contains }); var phase_allocator = try alloc_phase.SealedPhaseAllocator.init( std.testing.allocator, ); var plan = try GraphPlan.init(phase_allocator.initializationAllocator(), .{ .desired_nodes = 2, .stored_nodes = 2, .desired_edges = 2, .stored_edges = 2, .batch_size = 1, }); defer { if (phase_allocator.phase() == .initialization) { phase_allocator.abortInitialization(); } if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown(); plan.deinit(phase_allocator.teardownAllocator()); phase_allocator.deinit(); } const node_puts_pointer = plan.node_puts.ptr; const edge_puts_pointer = plan.edge_puts.ptr; const batch_node_puts_pointer = plan.batch_node_puts.ptr; const batch_edge_puts_pointer = plan.batch_edge_puts.ptr; phase_allocator.seal(); plan.order(graph); plan.activate(); try std.testing.expectEqual(@as(u32, 1), plan.node_order[0]); try std.testing.expectEqual(@as(u32, 0), plan.node_order[1]); try std.testing.expectEqual(@as(u32, 1), plan.edge_order[0]); try std.testing.expectEqual(@as(u32, 0), plan.edge_order[1]); _ = plan.markNode(0); _ = plan.markEdge(1); plan.putNode(1, 0); plan.deleteNode(2); plan.putEdge(3, 1); plan.deleteEdge(4); const graph_edits = plan.edits(); try std.testing.expectEqual(@as(usize, 1), graph_edits.node_puts.len); try std.testing.expectEqual(@as(usize, 1), graph_edits.edge_puts.len); try std.testing.expectEqualStrings("b", plan.materializeNodePuts(graph, graph_edits.node_puts)[0].node.name); try std.testing.expectEqualStrings(model.RelType.contains, plan.materializeEdgePuts(graph, graph_edits.edge_puts)[0].edge.rel); try std.testing.expectEqual(node_puts_pointer, plan.node_puts.ptr); try std.testing.expectEqual(edge_puts_pointer, plan.edge_puts.ptr); try std.testing.expectEqual(batch_node_puts_pointer, plan.batch_node_puts.ptr); try std.testing.expectEqual(batch_edge_puts_pointer, plan.batch_edge_puts.ptr); try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());}test "graph replacement streams stored rows and preserves stable rowids" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(GraphPlan, "smg_graph_diff_plan_streaming_transitive_risk"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(GraphPlan, "smg_graph_diff_plan_streaming_foreign_risk"), null, null, null, null, null, null, ); } var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var opened = try database.open(std.testing.allocator, tmp.dir, smg.default_limits.storage); defer opened.deinit(); try edits.apply(&opened, .{ .node_puts = &.{ .{ .rowid = 5, .node = .{ .name = "a", .type = "module", .line = 1 } }, .{ .rowid = 8, .node = .{ .name = "a", .type = "module", .line = 2 } }, .{ .rowid = 9, .node = .{ .name = "orphan", .type = "module" } }, }, .edge_puts = &.{ .{ .rowid = 3, .edge = .{ .source = "a", .target = "b", .rel = "contains" } }, .{ .rowid = 4, .edge = .{ .source = "a", .target = "b", .rel = "contains" } }, .{ .rowid = 7, .edge = .{ .source = "orphan", .target = "a", .rel = "calls" } }, }, }); _ = try database.commit(&opened); var graph = graph_mod.init(std.testing.allocator); defer graph_mod.deinit(&graph); try graph_mod.addNode(&graph, .{ .name = "b", .type = "module" }); try graph_mod.addNode(&graph, .{ .name = "a", .type = "module", .line = 42 }); try graph_mod.addEdge(&graph, .{ .source = "b", .target = "a", .rel = "calls" }); try graph_mod.addEdge(&graph, .{ .source = "a", .target = "b", .rel = "contains" }); try replaceGraph(std.testing.allocator, &opened, graph); const loaded_nodes = try read.databaseNodes(&opened, std.testing.allocator, std.testing.allocator); defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, loaded_nodes); const loaded_edges = try read.databaseEdges(&opened, std.testing.allocator, std.testing.allocator); defer rows.freeEdgeRowSlice(std.testing.allocator, std.testing.allocator, loaded_edges); try std.testing.expectEqual(@as(usize, 2), loaded_nodes.len); try std.testing.expectEqual(@as(i64, 5), loaded_nodes[0].rowid); try std.testing.expectEqual(@as(?i64, 42), loaded_nodes[0].node.line); try std.testing.expectEqual(@as(i64, 10), loaded_nodes[1].rowid); try std.testing.expectEqual(@as(usize, 2), loaded_edges.len); try std.testing.expectEqual(@as(i64, 3), loaded_edges[0].rowid); try std.testing.expectEqual(@as(i64, 8), loaded_edges[1].rowid);}Complete call list for storage.sync.replaceGraphObserved
10 direct calls.
tiny.smg.storage.database.read.graphCounts[function] attools/smg/src/storage/database/read.zig:11tiny.smg.storage.GraphDiffPlan.activate[method] attools/smg/src/storage/sync.zig:313tiny.smg.storage.GraphDiffPlan.deinit[method] attools/smg/src/storage/sync.zig:320tools.smg.src.storage.sync.GraphPlan.edits[method] — private; no exact target attools/smg/src/storage/sync.zig:391in nearest public ownertiny.smg.storage.synctiny.smg.storage.GraphDiffPlan.init[function] attools/smg/src/storage/sync.zig:252tools.smg.src.storage.sync.GraphPlan.order[method] — private; no exact target attools/smg/src/storage/sync.zig:292in nearest public ownertiny.smg.storage.synctools.smg.src.storage.sync.applyBatches[function] — private; no exact target attools/smg/src/storage/sync.zig:511in nearest public ownertiny.smg.storage.synctools.smg.src.storage.sync.changeSet[function] — private; no exact target attools/smg/src/storage/sync.zig:488in nearest public ownertiny.smg.storage.synctools.smg.src.storage.sync.fillEdgeDiff[function] — private; no exact target attools/smg/src/storage/sync.zig:593in nearest public ownertiny.smg.storage.synctools.smg.src.storage.sync.fillNodeDiff[function] — private; no exact target attools/smg/src/storage/sync.zig:549in nearest public ownertiny.smg.storage.sync
Audit
| Definitions | 17 |
|---|---|
| Public names | 26 |
| Members | 39 |
| Version | 26.7.0 |
| Revision | daab053ee433 |