From 233d8087550dfe794ff4d58ada2e26a96f652011 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:49:28 -0800 Subject: [PATCH 1/2] build: consume Grafeo fork main without a downstream patch --- Cargo.lock | 10 +- Cargo.toml | 18 +- MODULE.bazel | 7 - .../patches/grafeo-core-read-allocation.patch | 473 ------------------ 4 files changed, 15 insertions(+), 493 deletions(-) delete mode 100644 scripts/bazel/patches/grafeo-core-read-allocation.patch diff --git a/Cargo.lock b/Cargo.lock index 12682e8a5f..f4394e5755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2885,7 +2885,7 @@ dependencies = [ [[package]] name = "grafeo-adapters" version = "0.5.43" -source = "git+https://github.com/ScriptedAlchemy/grafeo.git?rev=be4713a5c2ee9df7e4639671c1e1244b7680f103#be4713a5c2ee9df7e4639671c1e1244b7680f103" +source = "git+https://github.com/ScriptedAlchemy/grafeo.git?branch=main#5116be3c7b9392006ef4d229634963e1306a7b6b" dependencies = [ "bincode", "grafeo-common", @@ -2900,7 +2900,7 @@ dependencies = [ [[package]] name = "grafeo-common" version = "0.5.43" -source = "git+https://github.com/ScriptedAlchemy/grafeo.git?rev=be4713a5c2ee9df7e4639671c1e1244b7680f103#be4713a5c2ee9df7e4639671c1e1244b7680f103" +source = "git+https://github.com/ScriptedAlchemy/grafeo.git?branch=main#5116be3c7b9392006ef4d229634963e1306a7b6b" dependencies = [ "arcstr", "bincode", @@ -2920,7 +2920,7 @@ dependencies = [ [[package]] name = "grafeo-core" version = "0.5.43" -source = "git+https://github.com/ScriptedAlchemy/grafeo.git?rev=be4713a5c2ee9df7e4639671c1e1244b7680f103#be4713a5c2ee9df7e4639671c1e1244b7680f103" +source = "git+https://github.com/ScriptedAlchemy/grafeo.git?branch=main#5116be3c7b9392006ef4d229634963e1306a7b6b" dependencies = [ "arc-swap", "arcstr", @@ -2943,7 +2943,7 @@ dependencies = [ [[package]] name = "grafeo-engine" version = "0.5.43" -source = "git+https://github.com/ScriptedAlchemy/grafeo.git?rev=be4713a5c2ee9df7e4639671c1e1244b7680f103#be4713a5c2ee9df7e4639671c1e1244b7680f103" +source = "git+https://github.com/ScriptedAlchemy/grafeo.git?branch=main#5116be3c7b9392006ef4d229634963e1306a7b6b" dependencies = [ "arcstr", "bincode", @@ -2965,7 +2965,7 @@ dependencies = [ [[package]] name = "grafeo-storage" version = "0.5.43" -source = "git+https://github.com/ScriptedAlchemy/grafeo.git?rev=be4713a5c2ee9df7e4639671c1e1244b7680f103#be4713a5c2ee9df7e4639671c1e1244b7680f103" +source = "git+https://github.com/ScriptedAlchemy/grafeo.git?branch=main#5116be3c7b9392006ef4d229634963e1306a7b6b" dependencies = [ "bincode", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index f181a8429d..8d448c3b59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,14 +81,16 @@ asupersync = { git = "https://github.com/Dicklesworthstone/asupersync", rev = "e # and in-place, page-verified sealed sections that sealed generation stores # are built and reopened with. Its `memory_usage()` also counts the catalog, # empty property-index shards and plan-cache tables, which the serving graph -# heap charge reads. The fork's workspace resolves sibling crates -# by path, so all five crates must name the same `rev`; mixed revisions build -# type-incompatible instances. -grafeo-adapters = { git = "https://github.com/ScriptedAlchemy/grafeo.git", rev = "be4713a5c2ee9df7e4639671c1e1244b7680f103", default-features = false, features = ["algos"] } -grafeo-common = { git = "https://github.com/ScriptedAlchemy/grafeo.git", rev = "be4713a5c2ee9df7e4639671c1e1244b7680f103", default-features = false } -grafeo-core = { git = "https://github.com/ScriptedAlchemy/grafeo.git", rev = "be4713a5c2ee9df7e4639671c1e1244b7680f103", default-features = false, features = ["lpg"] } -grafeo-engine = { git = "https://github.com/ScriptedAlchemy/grafeo.git", rev = "be4713a5c2ee9df7e4639671c1e1244b7680f103", default-features = false, features = ["lpg", "gql", "algos", "wal", "grafeo-file"] } -grafeo-storage = { git = "https://github.com/ScriptedAlchemy/grafeo.git", rev = "be4713a5c2ee9df7e4639671c1e1244b7680f103", default-features = false, features = ["wal"] } +# heap charge reads, and reports native read allocation demand before +# materializing rows. These APIs live in the fork so every build uses the +# same implementation. The fork resolves sibling crates by path: keep all +# five dependencies on main together and lock their exact commit in Cargo.lock +# to avoid type-incompatible instances from mixed sources. +grafeo-adapters = { git = "https://github.com/ScriptedAlchemy/grafeo.git", branch = "main", default-features = false, features = ["algos"] } +grafeo-common = { git = "https://github.com/ScriptedAlchemy/grafeo.git", branch = "main", default-features = false } +grafeo-core = { git = "https://github.com/ScriptedAlchemy/grafeo.git", branch = "main", default-features = false, features = ["lpg"] } +grafeo-engine = { git = "https://github.com/ScriptedAlchemy/grafeo.git", branch = "main", default-features = false, features = ["lpg", "gql", "algos", "wal", "grafeo-file"] } +grafeo-storage = { git = "https://github.com/ScriptedAlchemy/grafeo.git", branch = "main", default-features = false, features = ["wal"] } # Pinned once: the MCP git handlers and the root's edit/lifecycle readers must # resolve the same object database and status implementation, so a feature # added for one is never missing for the other. diff --git a/MODULE.bazel b/MODULE.bazel index c4f38a74ae..4cb5ca79a8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -103,13 +103,6 @@ crate.annotation( version = "0.46.0", ) -crate.annotation( - crate = "grafeo-core", - patch_args = ["-p1"], - patches = ["//scripts:bazel/patches/grafeo-core-read-allocation.patch"], - version = "0.5.43", -) - # arborium grammar build scripts read `env!("CARGO_MANIFEST_DIR")`, which bakes # the compiling output base's absolute path into the build-script binary. The # action key does not include that path, so the disk cache served one output diff --git a/scripts/bazel/patches/grafeo-core-read-allocation.patch b/scripts/bazel/patches/grafeo-core-read-allocation.patch deleted file mode 100644 index 1effd1e0ff..0000000000 --- a/scripts/bazel/patches/grafeo-core-read-allocation.patch +++ /dev/null @@ -1,473 +0,0 @@ -diff --git a/src/graph/compact/column.rs b/src/graph/compact/column.rs -index 861a9595..098e900f 100644 ---- a/src/graph/compact/column.rs -+++ b/src/graph/compact/column.rs -@@ -291,6 +291,28 @@ pub enum ColumnCodec { - } - - impl ColumnCodec { -+ /// Reads borrowed codec storage. It never decodes a Value. -+ pub fn allocation_bytes(&self, index: usize) -> usize { -+ use std::mem::size_of; -+ use grafeo_common::memory::heap::arc_slice_bytes; -+ match self { -+ Self::Dict(dict) => dict.get(index).map_or(0, |entry| { -+ // A malformed marker falls back to a literal string. Include -+ // the partial hex vector as well as that fallback allocation. -+ entry.len().saturating_mul(2).saturating_add(2 * size_of::()) -+ }), -+ Self::Int8Vector { dimensions, .. } => { -+ usize::from(*dimensions).saturating_mul(size_of::()) -+ .saturating_add(arc_slice_bytes::(usize::from(*dimensions))) -+ } -+ Self::Float32Vector { dimensions, .. } => { -+ usize::from(*dimensions).saturating_mul(size_of::()) -+ .saturating_add(arc_slice_bytes::(usize::from(*dimensions))) -+ } -+ _ => 0, -+ } -+ } -+ - // ── Phase 3a: Bytes-backed constructors ───────────────────────── - - /// Constructs a [`RawI64`](Self::RawI64) column from a `Vec` -diff --git a/src/graph/compact/graph_store_impl.rs b/src/graph/compact/graph_store_impl.rs -index 341460db..75dfac1b 100644 ---- a/src/graph/compact/graph_store_impl.rs -+++ b/src/graph/compact/graph_store_impl.rs -@@ -19,6 +19,40 @@ use crate::graph::traits::{GraphStore, GraphStoreSearch}; - use crate::statistics::Statistics; - - impl GraphStore for CompactStore { -+ fn node_allocation_bytes(&self, id: NodeId) -> Option { -+ let Some((table, offset)) = self.resolve_node(id) else { return Some(0); }; -+ let table = self.resolve_node_table(table)?; -+ Some(table.allocation_bytes(usize::try_from(offset).ok()?).saturating_add(table.label().len())) -+ } -+ -+ fn edge_allocation_bytes(&self, id: EdgeId) -> Option { -+ let Some((table, offset)) = self.resolve_edge(id) else { return Some(0); }; -+ Some(self.resolve_rel_table(table)?.allocation_bytes(usize::try_from(offset).ok()?)) -+ } -+ -+ fn node_property_allocation_bytes(&self, id: NodeId, key: &PropertyKey) -> Option { -+ let Some((table, offset)) = self.resolve_node(id) else { return Some(0); }; -+ Some(self.resolve_node_table(table)?.property_allocation_bytes(usize::try_from(offset).ok()?, key)) -+ } -+ -+ fn label_allocation_bytes(&self) -> Option { -+ Some(self.table_id_to_label.iter().fold( -+ self.table_id_to_label.len().saturating_mul(std::mem::size_of::()), -+ |bytes, label| bytes.saturating_add(label.len()), -+ )) -+ } -+ -+ fn property_lookup_allocation_bytes(&self, key: &PropertyKey, value: &Value) -> Option { -+ let index = self.property_value_index(key)?; -+ let mut count = 0_usize; -+ for (table, order) in index.iter() { -+ let col = self.resolve_node_table(*table)?.column(key)?; -+ count = count.saturating_add(super::value_order::equality_count(col, order, value)?); -+ } -+ let payload = match value { Value::String(value) => value.len(), Value::Bytes(value) => value.len() * 2, _ => 0 }; -+ Some(count.saturating_mul(8 * std::mem::size_of::()).saturating_add(payload.saturating_mul(3)).saturating_add(128)) -+ } -+ - fn get_node(&self, id: NodeId) -> Option { - let (table_id, offset) = self.resolve_node(id)?; - let nt = self.resolve_node_table(table_id)?; -@@ -29,10 +63,7 @@ impl GraphStore for CompactStore { - - let mut node = Node::new(id); - node.add_label(nt.label()); -- let props = nt.get_all_properties(row); -- for (k, v) in props { -- node.set_property(k, v); -- } -+ nt.for_each_property(row, |key, value| node.set_property(key.clone(), value)); - Some(node) - } - -@@ -48,10 +79,7 @@ impl GraphStore for CompactStore { - let edge_type = rt.edge_type().clone(); - - let mut edge = Edge::new(id, src, dst, edge_type); -- let props = rt.get_all_edge_properties(pos as usize); -- for (k, v) in props { -- edge.set_property(k, v); -- } -+ rt.for_each_property(pos as usize, |key, value| edge.set_property(key.clone(), value)); - Some(edge) - } - -diff --git a/src/graph/compact/layered.rs b/src/graph/compact/layered.rs -index 6eb25cb7..ed189b50 100644 ---- a/src/graph/compact/layered.rs -+++ b/src/graph/compact/layered.rs -@@ -437,6 +437,31 @@ impl LayeredStore { - // ── GraphStore implementation ────────────────────────────────────── - - impl GraphStore for LayeredStore { -+ fn node_allocation_bytes(&self, id: NodeId) -> Option { -+ Some(self.base.load().node_allocation_bytes(id)? -+ .saturating_add(self.overlay.load().node_allocation_bytes(id)?)) -+ } -+ -+ fn edge_allocation_bytes(&self, id: EdgeId) -> Option { -+ Some(self.base.load().edge_allocation_bytes(id)? -+ .saturating_add(self.overlay.load().edge_allocation_bytes(id)?)) -+ } -+ -+ fn node_property_allocation_bytes(&self, id: NodeId, key: &PropertyKey) -> Option { -+ Some(self.base.load().node_property_allocation_bytes(id, key)? -+ .saturating_add(self.overlay.load().node_property_allocation_bytes(id, key)?)) -+ } -+ -+ fn label_allocation_bytes(&self) -> Option { -+ Some(self.base.load().label_allocation_bytes()? -+ .saturating_add(self.overlay.load().label_allocation_bytes()?).saturating_mul(4)) -+ } -+ -+ fn property_lookup_allocation_bytes(&self, key: &PropertyKey, value: &Value) -> Option { -+ Some(self.base.load().property_lookup_allocation_bytes(key, value)? -+ .saturating_add(self.overlay.load().property_lookup_allocation_bytes(key, value)?).saturating_mul(2)) -+ } -+ - fn get_node(&self, id: NodeId) -> Option { - if self.is_node_deleted_from_base(id) { - return None; -diff --git a/src/graph/compact/node_table.rs b/src/graph/compact/node_table.rs -index 95ee80e5..103db9d0 100644 ---- a/src/graph/compact/node_table.rs -+++ b/src/graph/compact/node_table.rs -@@ -135,6 +135,34 @@ impl NodeTable { - /// A `true` row is one the builder padded because the node never had the - /// property; its column body still decodes to a value there, which no - /// read may surface. -+ pub fn allocation_bytes(&self, offset: usize) -> usize { -+ use std::mem::size_of; -+ if offset >= self.len { return 0; } -+ self.columns.iter().fold( -+ self.columns.len().saturating_mul(4 * size_of::<(PropertyKey, Value)>()), -+ |bytes, (key, col)| { -+ if self.is_null(offset, key) { bytes } else { -+ bytes.saturating_add(col.allocation_bytes(offset)).saturating_add(key.as_str().len()) -+ } -+ }, -+ ) -+ } -+ -+ pub fn for_each_property(&self, offset: usize, mut visit: impl FnMut(&PropertyKey, Value)) { -+ for (key, col) in &self.columns { -+ if !self.is_null(offset, key) { -+ if let Some(value) = col.get(offset) { -+ visit(key, value); -+ } -+ } -+ } -+ } -+ -+ pub fn property_allocation_bytes(&self, offset: usize, key: &PropertyKey) -> usize { -+ if self.is_null(offset, key) { return 0; } -+ self.columns.get(key).map_or(0, |col| col.allocation_bytes(offset)) -+ } -+ - #[must_use] - pub fn is_null(&self, offset: usize, key: &PropertyKey) -> bool { - self.null_masks -diff --git a/src/graph/compact/rel_table.rs b/src/graph/compact/rel_table.rs -index f5e5df76..55eb73e3 100644 ---- a/src/graph/compact/rel_table.rs -+++ b/src/graph/compact/rel_table.rs -@@ -182,6 +182,28 @@ impl RelTable { - - /// Returns the property value for a specific edge (by CSR position) and - /// key; `None` when the edge holds no value for it. -+ pub fn allocation_bytes(&self, offset: usize) -> usize { -+ use std::mem::size_of; -+ self.properties.iter().fold( -+ self.properties.len().saturating_mul(4 * size_of::<(PropertyKey, Value)>()), -+ |bytes, (key, col)| { -+ if self.is_null(offset, key) { bytes } else { -+ bytes.saturating_add(col.allocation_bytes(offset)).saturating_add(key.as_str().len()) -+ } -+ }, -+ ) -+ } -+ -+ pub fn for_each_property(&self, offset: usize, mut visit: impl FnMut(&PropertyKey, Value)) { -+ for (key, col) in &self.properties { -+ if !self.is_null(offset, key) { -+ if let Some(value) = col.get(offset) { -+ visit(key, value); -+ } -+ } -+ } -+ } -+ - #[must_use] - pub fn get_edge_property(&self, csr_position: usize, key: &PropertyKey) -> Option { - if self.is_null(csr_position, key) { -diff --git a/src/graph/compact/value_order.rs b/src/graph/compact/value_order.rs -index 230b3a4d..83a3c523 100644 ---- a/src/graph/compact/value_order.rs -+++ b/src/graph/compact/value_order.rs -@@ -137,6 +137,46 @@ pub(crate) fn build_row_order( - Some(RowOrder::Inline(rows)) - } - -+/// The number of rows whose value equals `value`, found by binary search -+/// over `order`, mirroring [`find_eq`] without collecting the rows. -+/// -+/// `None` when the codec and value are not an orderable pair this index -+/// serves, so the caller can decline rather than assume zero. -+pub(crate) fn equality_count(codec: &ColumnCodec, order: &RowOrder, value: &Value) -> Option { -+ let entry; -+ let compare: Box Option + '_> = match (codec, value) { -+ (ColumnCodec::Dict(dict), Value::String(_) | Value::Bytes(_)) => { -+ entry = encode_dict_entry(value); -+ Box::new(|row| { -+ let code = dict.code_at(row)?; -+ Some(dict.entry(code as usize)?.cmp(entry.as_str())) -+ }) -+ } -+ (ColumnCodec::BitPacked(packed), &Value::Int64(target)) => { -+ let target = u64::try_from(target).ok()?; -+ Box::new(move |row| Some(packed.get(row)?.cmp(&target))) -+ } -+ (ColumnCodec::RawI64(store), &Value::Int64(target)) => { -+ Box::new(move |row| Some(store.get(row)?.cmp(&target))) -+ } -+ _ => return None, -+ }; -+ let bound = |inclusive: bool| -> Option { -+ let (mut low, mut high) = (0, order.len()); -+ while low < high { -+ let mid = low + (high - low) / 2; -+ let ordering = order.row(mid).and_then(&compare)?; -+ if ordering == Ordering::Less || (inclusive && ordering == Ordering::Equal) { -+ low = mid + 1; -+ } else { -+ high = mid; -+ } -+ } -+ Some(low) -+ }; -+ Some(bound(true)?.saturating_sub(bound(false)?)) -+} -+ - /// The rows of `codec` whose value equals `value`, in row order, found by - /// binary search over `order`. - pub(crate) fn find_eq(codec: &ColumnCodec, order: &RowOrder, value: &Value) -> Vec { -diff --git a/src/graph/lpg/property.rs b/src/graph/lpg/property.rs -index 415d1498..e22be7fd 100644 ---- a/src/graph/lpg/property.rs -+++ b/src/graph/lpg/property.rs -@@ -302,6 +302,27 @@ impl PropertyStorage { - } - - /// Gets all properties for an entity. -+ #[must_use] -+ /// Borrowed current values, without a map, Value clone, or payload decode. -+ pub fn allocation_bytes(&self, id: Id, key: Option<&PropertyKey>) -> usize { -+ use std::mem::size_of; -+ let columns = self.columns.read(); -+ let mut bytes = 0_usize; -+ for (name, col) in columns.iter() { -+ if key.is_some_and(|key| key != name) { continue; } -+ if let Some(value) = col.borrowed(id) { -+ bytes = bytes.saturating_add(4 * size_of::<(PropertyKey, Value)>()) -+ .saturating_add(name.as_str().len()) -+ .saturating_add(match value { -+ Value::String(value) => value.len(), -+ Value::Bytes(value) => value.len(), -+ _ => 0, -+ }); -+ } -+ } -+ bytes -+ } -+ - #[must_use] - pub fn get_all(&self, id: Id) -> FxHashMap { - let columns = self.columns.read(); -@@ -705,6 +726,16 @@ impl PropertyStorage { - } - } - -+ /// Visits the same visible epoch values as get_all_at. -+ pub fn for_each_at(&self, id: Id, epoch: EpochId, mut visit: impl FnMut(&PropertyKey, Value)) { -+ let columns = self.columns.read(); -+ for (key, col) in columns.iter() { -+ if let Some(value) = col.get_at(id, epoch) { -+ visit(key, value); -+ } -+ } -+ } -+ - /// Removes all PENDING entries from all columns (transaction rollback). - pub fn remove_pending(&self) { - let mut columns = self.columns.write(); -@@ -1020,6 +1051,12 @@ impl PropertyColumn { - /// - /// First checks the hot buffer (uncompressed values), then falls back - /// to the compressed data if present. -+ #[must_use] -+ #[cfg(not(feature = "temporal"))] -+ fn borrowed(&self, id: Id) -> Option<&Value> { -+ self.values.get(&id) -+ } -+ - #[must_use] - pub fn get(&self, id: Id) -> Option { - // First check hot buffer -@@ -1685,6 +1722,12 @@ impl PropertyColumn { - } - - /// Gets the latest value for an entity, filtering out tombstones (Null). -+ #[must_use] -+ #[cfg(feature = "temporal")] -+ fn borrowed(&self, id: Id) -> Option<&Value> { -+ self.values.get(&id).and_then(|log| log.latest()).filter(|value| !value.is_null()) -+ } -+ - #[must_use] - pub fn get(&self, id: Id) -> Option { - self.values -diff --git a/src/graph/lpg/store/edge_ops.rs b/src/graph/lpg/store/edge_ops.rs -index 2aa96b6c..e9a49569 100644 ---- a/src/graph/lpg/store/edge_ops.rs -+++ b/src/graph/lpg/store/edge_ops.rs -@@ -20,7 +20,9 @@ impl LpgStore { - id_to_type.get(record.type_id as usize)?.clone() - }; - let mut edge = Edge::new(id, record.src, record.dst, edge_type); -- edge.properties = self.edge_properties.get_all(id).into_iter().collect(); -+ self.edge_properties.for_each(id, |key, value| { -+ edge.set_property(key.clone(), value); -+ }); - Some(edge) - } - -@@ -76,11 +78,7 @@ impl LpgStore { - id_to_type.get(record.type_id as usize)?.clone() - }; - let mut edge = Edge::new(id, record.src, record.dst, edge_type); -- edge.properties = self -- .edge_properties -- .get_all_at(id, epoch) -- .into_iter() -- .collect(); -+ self.edge_properties.for_each_at(id, epoch, |key, value| edge.set_property(key.clone(), value)); - Some(edge) - } - -diff --git a/src/graph/lpg/store/graph_store_impl.rs b/src/graph/lpg/store/graph_store_impl.rs -index f19a3e9e..8fb6e2e8 100644 ---- a/src/graph/lpg/store/graph_store_impl.rs -+++ b/src/graph/lpg/store/graph_store_impl.rs -@@ -20,6 +20,41 @@ use grafeo_common::utils::hash::FxHashMap; - use std::sync::Arc; - - impl GraphStore for LpgStore { -+ fn node_allocation_bytes(&self, id: NodeId) -> Option { -+ if !self.node_visible(id) { return Some(0); } -+ let labels = self.node_labels.read(); -+ #[cfg(not(feature = "temporal"))] -+ let count = labels.get(&id).map_or(0, |labels| labels.len()); -+ #[cfg(feature = "temporal")] -+ let count = labels.get(&id).and_then(|log| log.latest()).map_or(0, |labels| labels.len()); -+ Some(self.node_properties.allocation_bytes(id, None) -+ .saturating_add(count.saturating_mul(4 * std::mem::size_of::()))) -+ } -+ -+ fn property_lookup_allocation_bytes(&self, key: &PropertyKey, value: &Value) -> Option { -+ let indexes = self.property_indexes.read(); -+ let index = indexes.get(key)?; -+ let value = grafeo_common::types::HashableValue::new(value.clone()); -+ Some(index.get(&value).map_or(0, |nodes| nodes.len()).saturating_mul(std::mem::size_of::())) -+ } -+ -+ fn edge_allocation_bytes(&self, id: EdgeId) -> Option { -+ if self.edge_header(id).is_none() { return Some(0); } -+ Some(self.edge_properties.allocation_bytes(id, None)) -+ } -+ -+ fn node_property_allocation_bytes(&self, id: NodeId, key: &PropertyKey) -> Option { -+ Some(self.node_properties.allocation_bytes(id, Some(key))) -+ } -+ -+ fn label_allocation_bytes(&self) -> Option { -+ let registry = self.label_registry.read(); -+ Some(registry.names().iter().fold( -+ registry.names().len().saturating_mul(std::mem::size_of::()), -+ |bytes, label| bytes.saturating_add(label.len()), -+ )) -+ } -+ - fn get_node(&self, id: NodeId) -> Option { - LpgStore::get_node(self, id) - } -diff --git a/src/graph/lpg/store/node_ops.rs b/src/graph/lpg/store/node_ops.rs -index 3cd1b525..bd3a02d9 100644 ---- a/src/graph/lpg/store/node_ops.rs -+++ b/src/graph/lpg/store/node_ops.rs -@@ -102,7 +102,9 @@ impl LpgStore { - } - } - -- node.properties = self.node_properties.get_all(id).into_iter().collect(); -+ self.node_properties.for_each(id, |key, value| { -+ node.set_property(key.clone(), value); -+ }); - node - } - -@@ -181,11 +183,7 @@ impl LpgStore { - } - } - -- node.properties = self -- .node_properties -- .get_all_at(id, epoch) -- .into_iter() -- .collect(); -+ self.node_properties.for_each_at(id, epoch, |key, value| node.set_property(key.clone(), value)); - node - } - -diff --git a/src/graph/traits.rs b/src/graph/traits.rs -index 4b226ec2..cb2154e6 100644 ---- a/src/graph/traits.rs -+++ b/src/graph/traits.rs -@@ -44,6 +44,30 @@ use std::sync::Arc; - pub trait GraphStore: Send + Sync { - // --- Point lookups --- - -+ /// Allocation demand of materializing a native row, without materializing it. -+ /// None means this store cannot supply allocation metadata. -+ fn node_allocation_bytes(&self, _id: NodeId) -> Option { -+ None -+ } -+ -+ fn edge_allocation_bytes(&self, _id: EdgeId) -> Option { -+ None -+ } -+ -+ /// Allocation demand of one property lookup, including dictionary decoding. -+ fn node_property_allocation_bytes(&self, _id: NodeId, _key: &PropertyKey) -> Option { -+ None -+ } -+ -+ /// Allocation demand of enumerating the label universe. -+ fn label_allocation_bytes(&self) -> Option { -+ None -+ } -+ -+ fn property_lookup_allocation_bytes(&self, _key: &PropertyKey, _value: &Value) -> Option { -+ None -+ } -+ - /// Returns a node by ID (latest visible version at current epoch). - fn get_node(&self, id: NodeId) -> Option; - From 9651a9ff7f03d69bdfd2d1a0b5dfb7c55f8bf070 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:48:17 -0800 Subject: [PATCH 2/2] test(index): use serving readiness in admission fixtures Apply the reviewed fixture repair from fbca068b27cd50c12c84fbdbf7cc81ec4d027246 (#4731) after this branch's private Linux run failed three admission tests at the redundant generation-publication wait. The original fixture blob matches exactly. Retain the stronger fresh serving-seat wait and every admission assertion. No timeout increases, production changes, or validation skips. The independent cold-daemon Git topology failure is not addressed by this repair. The shared repair passed native Linux on #4731; the updated Grafeo branch still needs its own full hosted Bazel validation. No local Rust execution is claimed. --- .../ignored_dependency_admission_tests.rs | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission_tests.rs b/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission_tests.rs index 35ab833191..9135cf4f90 100644 --- a/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/project_reads/ignored_dependency_admission_tests.rs @@ -21,7 +21,6 @@ use tracedecay_tool_catalog::{CapabilityId, UseCaseId}; use crate::code_index_scheduler::{CodeIndexSchedulerRegistryV1, LatestCompleteCodeIndexV1}; use crate::project_reads::project_code_index_ignored_dependency_admission_port; -use tracedecay_runtime_core::path_safety::canonical_existing_identity; const PROJECT_ID: &str = "project.project-open-ignored-dependency"; @@ -70,7 +69,6 @@ export function generationAnchor() { return 1; } .mount_worktree(project_id(), root.path(), store.path().to_path_buf()) .await .expect("mount code-index scheduler"); - wait_for_initial_generation(®istry, root.path()).await; let baseline = latest(®istry, root.path()).await; let generation = baseline.generation(); let snapshot = generation.snapshot(); @@ -242,31 +240,11 @@ fn request_context(scope: ResolvedScope, suffix: &str) -> RequestContext { .expect("request context") } -async fn wait_for_initial_generation(registry: &CodeIndexSchedulerRegistryV1, project_root: &Path) { - if registry.latest_generation_id(project_root).await.is_some() { - return; - } - let canonical_root = canonical_existing_identity(project_root).expect("canonical fixture root"); - let mut publications = registry.subscribe_generation_publications(); - tokio::time::timeout(Duration::from_secs(5), async { - loop { - let event = publications.recv().await.expect("generation publication"); - if event.project_root == canonical_root { - break; - } - } - }) - .await - .expect("initial generation publication"); -} - /// Wait for the complete serving generation, not merely its publication. /// -/// The generation-publication broadcast and `latest_generation_id` both fire -/// on the reconcile pass, ahead of the bind and the serving swap that fill -/// `serving_generation`. Reading the complete arm the instant -/// `wait_for_initial_generation` returns therefore raced the seat and reported -/// a missing generation for a store that had one. +/// Publication and `latest_generation_id` precede the serving swap. Wait +/// for the actual fresh serving state before using imports as admission +/// evidence; no earlier broadcast establishes readiness. async fn latest( registry: &CodeIndexSchedulerRegistryV1, project_root: &Path,