Skip to main content

hawdb_executor/
binding.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Executor-owned row bindings and deterministic memory accounting.
16
17mod graph_slots;
18pub use graph_slots::GraphSlots;
19
20use hawdb_core::{LabelId, Value};
21use hawdb_plan_cypher::SortDirection;
22use hawdb_storage::{NodeRecord, RelRecord};
23use std::cmp::Ordering;
24use std::collections::{BTreeMap, BTreeSet};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Binding {
28    pub values: BTreeMap<String, Value>,
29    pub nodes: GraphSlots<NodeRecord>,
30    pub relationships: GraphSlots<RelRecord>,
31}
32
33impl Binding {
34    pub fn values(values: BTreeMap<String, Value>) -> Self {
35        Self {
36            values,
37            nodes: GraphSlots::new(),
38            relationships: GraphSlots::new(),
39        }
40    }
41
42    pub fn scalar(name: impl Into<String>, value: Value) -> Self {
43        Self::values(BTreeMap::from([(name.into(), value)]))
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct TopNBinding {
49    pub sort_values: Vec<(Value, SortDirection)>,
50    pub ordinal: u64,
51    pub binding: Binding,
52}
53
54impl TopNBinding {
55    pub fn memory_bytes(&self) -> usize {
56        binding_memory_bytes(&self.binding)
57            .saturating_add(sort_values_memory_bytes(&self.sort_values))
58    }
59}
60
61pub(crate) fn sort_values_memory_bytes(values: &Vec<(Value, SortDirection)>) -> usize {
62    let layout = std::mem::size_of::<Vec<(Value, SortDirection)>>().saturating_add(
63        values
64            .capacity()
65            .saturating_mul(std::mem::size_of::<(Value, SortDirection)>()),
66    );
67    values.iter().fold(layout, |total, (value, _)| {
68        total.saturating_add(value_memory_bytes(value).saturating_sub(std::mem::size_of::<Value>()))
69    })
70}
71
72impl Ord for TopNBinding {
73    fn cmp(&self, other: &Self) -> Ordering {
74        for ((left, direction), (right, other_direction)) in
75            self.sort_values.iter().zip(&other.sort_values)
76        {
77            debug_assert_eq!(direction, other_direction);
78            let ordering = match direction {
79                SortDirection::Asc => left.cmp(right),
80                SortDirection::Desc => left.cmp(right).reverse(),
81            };
82            if ordering != Ordering::Equal {
83                return ordering;
84            }
85        }
86        self.ordinal.cmp(&other.ordinal)
87    }
88}
89
90impl PartialOrd for TopNBinding {
91    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
92        Some(self.cmp(other))
93    }
94}
95
96pub fn map_payload_bytes(values: &BTreeMap<String, Value>) -> usize {
97    values.iter().fold(0usize, |total, (name, value)| {
98        total
99            .saturating_add(name.len())
100            .saturating_add(value_payload_bytes(value))
101    })
102}
103
104pub fn map_memory_bytes(values: &BTreeMap<String, Value>) -> usize {
105    map_memory_bytes_with_entries(values.iter().map(|(name, value)| (name.as_str(), value)))
106}
107
108fn map_memory_bytes_with_entries<'a>(values: impl Iterator<Item = (&'a str, &'a Value)>) -> usize {
109    std::mem::size_of::<BTreeMap<String, Value>>().saturating_add(values.fold(
110        0usize,
111        |total, (name, value)| {
112            total
113                .saturating_add(std::mem::size_of::<(String, Value)>() * 3)
114                .saturating_add(name.len())
115                .saturating_add(value_memory_bytes(value))
116        },
117    ))
118}
119
120/// Estimate the selected owned scan input with the same rules as its final
121/// node-only Binding, while all property payloads still belong to storage.
122pub(crate) fn projected_node_binding_memory_bytes(
123    variable: &str,
124    node: &NodeRecord,
125    required_properties: &BTreeSet<String>,
126) -> usize {
127    std::mem::size_of::<Binding>()
128        .saturating_add(variable.len())
129        .saturating_add(std::mem::size_of_val(&node.id))
130        .saturating_add(
131            node.labels
132                .len()
133                .saturating_mul(std::mem::size_of::<LabelId>()),
134        )
135        .saturating_add(map_memory_bytes_with_entries(
136            required_properties.iter().filter_map(|property| {
137                node.properties
138                    .get(property)
139                    .map(|value| (property.as_str(), value))
140            }),
141        ))
142        .saturating_add(graph_slot_memory_bytes(1))
143}
144
145pub fn value_memory_bytes(value: &Value) -> usize {
146    std::mem::size_of::<Value>().saturating_add(match value {
147        Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) => 0,
148        Value::String(value) => value.len(),
149        Value::Binary(value) => value.len(),
150        Value::Vector(value) => value.encoded().len(),
151        Value::Uuid(_) => 16,
152        Value::List(values) => values
153            .iter()
154            .fold(std::mem::size_of::<Vec<Value>>(), |total, value| {
155                total.saturating_add(value_memory_bytes(value))
156            }),
157        Value::Map(values) => map_memory_bytes(values),
158    })
159}
160
161pub fn binding_payload_bytes(binding: &Binding) -> usize {
162    map_payload_bytes(&binding.values).saturating_add(graph_binding_payload_bytes(binding))
163}
164
165fn graph_binding_payload_bytes(binding: &Binding) -> usize {
166    graph_binding_bytes(binding, map_payload_bytes)
167}
168
169fn graph_binding_bytes(
170    binding: &Binding,
171    property_bytes: fn(&BTreeMap<String, Value>) -> usize,
172) -> usize {
173    graph_binding_parts_bytes(
174        binding
175            .nodes
176            .iter()
177            .map(|(name, node)| (name.as_str(), node)),
178        binding
179            .relationships
180            .iter()
181            .map(|(name, row)| (name.as_str(), row)),
182        property_bytes,
183    )
184}
185
186fn graph_binding_parts_bytes<'a>(
187    nodes: impl Iterator<Item = (&'a str, &'a NodeRecord)>,
188    relationships: impl Iterator<Item = (&'a str, &'a RelRecord)>,
189    property_bytes: fn(&BTreeMap<String, Value>) -> usize,
190) -> usize {
191    nodes
192        .fold(0usize, |total, (name, node)| {
193            total
194                .saturating_add(name.len())
195                .saturating_add(std::mem::size_of_val(&node.id))
196                .saturating_add(
197                    node.labels
198                        .len()
199                        .saturating_mul(std::mem::size_of::<LabelId>()),
200                )
201                .saturating_add(property_bytes(&node.properties))
202        })
203        .saturating_add(relationships.fold(0usize, |total, (name, row)| {
204            total
205                .saturating_add(name.len())
206                .saturating_add(std::mem::size_of_val(&row.id))
207                .saturating_add(std::mem::size_of_val(&row.source))
208                .saturating_add(std::mem::size_of_val(&row.target))
209                .saturating_add(std::mem::size_of_val(&row.rel_type))
210                .saturating_add(property_bytes(&row.properties))
211        }))
212}
213
214pub(crate) fn binding_payload_bytes_with_parts<'a>(
215    values: impl Iterator<Item = (&'a str, &'a Value)>,
216    nodes: impl Iterator<Item = (&'a str, &'a NodeRecord)>,
217    relationships: impl Iterator<Item = (&'a str, &'a RelRecord)>,
218) -> usize {
219    values
220        .fold(0usize, |total, (name, value)| {
221            total
222                .saturating_add(name.len())
223                .saturating_add(value_payload_bytes(value))
224        })
225        .saturating_add(graph_binding_parts_bytes(
226            nodes,
227            relationships,
228            map_payload_bytes,
229        ))
230}
231
232pub fn binding_memory_bytes(binding: &Binding) -> usize {
233    binding_memory_bytes_with_values(
234        binding,
235        binding
236            .values
237            .iter()
238            .map(|(name, value)| (name.as_str(), value)),
239    )
240}
241
242/// Estimate an overwritten column from the final borrowed map. Resident
243/// container storage differs from transport payload and must be removed in full.
244pub(crate) fn binding_memory_bytes_replacing_value(
245    binding: &Binding,
246    name: &str,
247    value: &Value,
248) -> usize {
249    binding_memory_bytes_with_values(
250        binding,
251        binding
252            .values
253            .iter()
254            .filter(|(key, _)| key.as_str() != name)
255            .map(|(key, value)| (key.as_str(), value))
256            .chain(std::iter::once((name, value))),
257    )
258}
259
260/// Account a projection's borrowed values with the same retained graph and
261/// entry rules as its final owned Binding, before copying any of those values.
262pub(crate) fn binding_memory_bytes_with_values<'a>(
263    binding: &'a Binding,
264    values: impl Iterator<Item = (&'a str, &'a Value)>,
265) -> usize {
266    binding_memory_bytes_with_parts(
267        values,
268        binding
269            .nodes
270            .iter()
271            .map(|(name, node)| (name.as_str(), node)),
272        binding
273            .relationships
274            .iter()
275            .map(|(name, row)| (name.as_str(), row)),
276    )
277}
278
279pub(crate) fn binding_memory_bytes_with_parts<'a>(
280    values: impl Iterator<Item = (&'a str, &'a Value)>,
281    nodes: impl Iterator<Item = (&'a str, &'a NodeRecord)>,
282    relationships: impl Iterator<Item = (&'a str, &'a RelRecord)>,
283) -> usize {
284    let nodes_count = std::cell::Cell::new(0usize);
285    let relationships_count = std::cell::Cell::new(0usize);
286    let graph_bytes = graph_binding_parts_bytes(
287        nodes.inspect(|_| nodes_count.set(nodes_count.get().saturating_add(1))),
288        relationships
289            .inspect(|_| relationships_count.set(relationships_count.get().saturating_add(1))),
290        map_memory_bytes,
291    );
292    std::mem::size_of::<Binding>()
293        .saturating_add(graph_bytes)
294        // Charge every row as if its graph records were uniquely retained.
295        // Sharing reduces allocator traffic but never weakens admission. Include
296        // slot vectors, Arc headers, String/label layouts and record headers.
297        .saturating_add(graph_slot_memory_bytes(nodes_count.get()))
298        .saturating_add(graph_slot_memory_bytes(relationships_count.get()))
299        .saturating_add(values.fold(0usize, |total, (name, value)| {
300            total
301                .saturating_add(name.len())
302                .saturating_add(match value {
303                    Value::List(_) | Value::Map(_) => {
304                        value_memory_bytes(value).saturating_sub(std::mem::size_of::<Value>())
305                    }
306                    _ => value_payload_bytes(value),
307                })
308                .saturating_add(std::mem::size_of::<usize>() * 6)
309        }))
310}
311
312pub(crate) fn graph_slot_memory_bytes(entries: usize) -> usize {
313    if entries == 0 {
314        return 0;
315    }
316    // A Vec grown one slot at a time has capacity at most max(4, 2*len).
317    std::mem::size_of::<Vec<usize>>()
318        .saturating_add(2 * std::mem::size_of::<usize>())
319        .saturating_add(
320            entries
321                .saturating_mul(2)
322                .max(4)
323                .saturating_mul(2 * std::mem::size_of::<usize>()),
324        )
325        .saturating_add(entries.saturating_mul(10 * std::mem::size_of::<usize>()))
326}
327
328pub fn node_memory_bytes(node: &NodeRecord) -> usize {
329    std::mem::size_of::<NodeRecord>()
330        .saturating_add(
331            node.labels
332                .len()
333                .saturating_mul(std::mem::size_of::<LabelId>() * 3),
334        )
335        .saturating_add(map_memory_bytes(&node.properties))
336}
337
338pub fn relationship_memory_bytes(relationship: &RelRecord) -> usize {
339    std::mem::size_of::<RelRecord>().saturating_add(map_memory_bytes(&relationship.properties))
340}
341
342pub fn value_payload_bytes(value: &Value) -> usize {
343    value_ref_payload_bytes(value.as_ref())
344}
345
346pub fn value_ref_payload_bytes(value: hawdb_core::ValueRef<'_>) -> usize {
347    use hawdb_core::ValueRef;
348    match value {
349        ValueRef::Null => 0,
350        ValueRef::Bool(_) => std::mem::size_of::<bool>(),
351        ValueRef::Int(_) => std::mem::size_of::<i64>(),
352        ValueRef::Float(_) => std::mem::size_of::<f64>(),
353        ValueRef::String(value) => value.len(),
354        ValueRef::Binary(value) => value.len(),
355        ValueRef::Vector(value) => value.encoded().len(),
356        ValueRef::Uuid(_) => 16,
357        ValueRef::List(values) => values.iter().fold(0usize, |total, value| {
358            total.saturating_add(value_payload_bytes(value))
359        }),
360        ValueRef::Map(values) => map_payload_bytes(values),
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use hawdb_core::RelTypeId;
368    use hawdb_storage::{NodeId, RelId};
369    use std::collections::BTreeSet;
370
371    #[test]
372    fn value_only_constructors_leave_graph_bindings_empty() {
373        let values = Binding::values(BTreeMap::from([("left".to_string(), Value::Int(1))]));
374        assert_eq!(values.values["left"], Value::Int(1));
375        assert!(values.nodes.is_empty());
376        assert!(values.relationships.is_empty());
377
378        let scalar = Binding::scalar("right", Value::Bool(true));
379        assert_eq!(scalar.values["right"], Value::Bool(true));
380        assert!(scalar.nodes.is_empty());
381        assert!(scalar.relationships.is_empty());
382    }
383
384    #[test]
385    fn binding_accounting_includes_nested_values_and_graph_records() {
386        let binding = Binding {
387            values: BTreeMap::from([(
388                "nested".to_string(),
389                Value::List(vec![Value::Int(1), Value::String("two".to_string())]),
390            )]),
391            nodes: BTreeMap::from([(
392                "n".to_string(),
393                NodeRecord {
394                    id: NodeId(7),
395                    labels: BTreeSet::from([LabelId(3)]),
396                    properties: BTreeMap::new(),
397                },
398            )])
399            .into(),
400            relationships: BTreeMap::from([(
401                "r".to_string(),
402                RelRecord {
403                    id: RelId(11),
404                    source: NodeId(7),
405                    target: NodeId(8),
406                    rel_type: RelTypeId(4),
407                    properties: BTreeMap::new(),
408                },
409            )])
410            .into(),
411        };
412
413        assert!(binding_payload_bytes(&binding) > "nested".len() + "two".len());
414        assert!(binding_memory_bytes(&binding) > binding_payload_bytes(&binding));
415    }
416
417    #[test]
418    fn projected_node_admission_matches_graph_slot_ownership() {
419        let node = NodeRecord {
420            id: NodeId(3),
421            labels: BTreeSet::from([LabelId(1)]),
422            properties: BTreeMap::from([
423                ("id".into(), Value::String("chosen".into())),
424                ("body".into(), Value::String("x".repeat(65536))),
425            ]),
426        };
427        let required = BTreeSet::from(["id".to_string()]);
428        let bytes = projected_node_binding_memory_bytes("n", &node, &required);
429        let mut projected = node.clone();
430        projected
431            .properties
432            .retain(|name, _| required.contains(name));
433        let binding = Binding {
434            values: BTreeMap::new(),
435            nodes: BTreeMap::from([("n".into(), projected)]).into(),
436            relationships: GraphSlots::new(),
437        };
438        assert_eq!(bytes, binding_memory_bytes(&binding));
439    }
440
441    #[test]
442    fn replacement_admission_matches_final_rows_without_mutating_input() {
443        let column = hawdb_plan_cypher::SCORING_RERANK_SCORE_COLUMN;
444        for previous in [
445            None,
446            Some(Value::Null),
447            Some(Value::Float(-1.0)),
448            Some(Value::List(vec![Value::Map(BTreeMap::from([(
449                "nested".into(),
450                Value::String("x".repeat(1_024)),
451            )]))])),
452        ] {
453            let mut input = Binding::scalar("score", Value::Float(0.5));
454            if let Some(previous) = previous {
455                input.values.insert(column.into(), previous);
456            }
457            let original = input.clone();
458            let estimate =
459                binding_memory_bytes_replacing_value(&input, column, &Value::Float(0.25));
460            assert_eq!(input, original, "admission must not alter the candidate");
461            let mut output = input;
462            output.values.insert(column.into(), Value::Float(0.25));
463            assert_eq!(estimate, binding_memory_bytes(&output));
464        }
465    }
466}