Skip to main content

hawdb_optimizer_predicate/predicate/
construction.rs

1// Copyright 2026 Nowledge
2// Licensed under the Apache License, Version 2.0.
3
4//! Pre-allocation envelopes for the shared metadata parser. These assumptions
5//! follow the pinned Rust Vec growth and serde_json 1.0.154 slice deserializer:
6//! Strings copy decoded bytes, the sequence Vec grows geometrically, and one
7//! reusable byte Vec decodes escaped strings. Recheck when either pin changes.
8
9use super::{SearchPredicateParseError, IN_SUFFIX, NOT_IN_SUFFIX};
10
11#[derive(Debug, PartialEq, Eq)]
12pub enum SearchPredicateConstructionError<E> {
13    Parse(SearchPredicateParseError),
14    Admission(E),
15    SizeOverflow,
16}
17
18impl<E> From<SearchPredicateParseError> for SearchPredicateConstructionError<E> {
19    fn from(error: SearchPredicateParseError) -> Self {
20        Self::Parse(error)
21    }
22}
23
24// A scalar-set node holds at most eleven scalar values and twelve child
25// pointers. Per input value this also covers internal nodes and split overlap.
26// This is a B-tree capacity allowance, not a measured allocator size.
27const SET_ENTRY_BYTES: usize = 1024;
28const MAX_REASON_BYTES: usize = 64;
29// Boxed Serde syntax/type errors and their bounded numeric/type descriptions.
30// Non-array top-level input is rejected without Serde diagnostic formatting.
31const JSON_ERROR_BYTES: usize = 1024;
32
33pub(super) fn filter_capacity(key: &str, value: &str) -> Option<usize> {
34    let diagnostic = key
35        .len()
36        .checked_add(MAX_REASON_BYTES + 2)?
37        .checked_mul(3)?;
38    // One canonical field, the parse-error key, and Display growth/overlap.
39    let common = key
40        .len()
41        .max(16)
42        .checked_add(key.len())?
43        .checked_add(diagnostic)?;
44    if !key.ends_with(IN_SUFFIX) && !key.ends_with(NOT_IN_SUFFIX) {
45        // Owned scalar plus a normalized bool while its source is still live.
46        return common.checked_add(value.len())?.checked_add(5);
47    }
48    let strings = StringCapacity::scan(value)?;
49    // Serde's Vec<String> can have capacity < 2*n (minimum four). Include
50    // the old buffer during replacement, hence three rather than two.
51    let vector = strings
52        .count
53        .max(4)
54        .checked_mul(3)?
55        .checked_mul(size_of::<String>())?;
56    // Unicode decoding may reserve four bytes for its final code point. The
57    // scratch Vec minimum is eight bytes; geometric replacement overlaps its
58    // predecessor. Source whitespace never contributes to this capacity.
59    let scratch = strings.largest.checked_add(4)?.max(8).checked_mul(3)?;
60    let sets = strings.count.checked_mul(SET_ENTRY_BYTES)?;
61    let bools = strings.count.checked_mul(5)?;
62    common
63        .checked_add(vector)?
64        .checked_add(scratch)?
65        .checked_add(sets)?
66        .checked_add(strings.total)?
67        .checked_add(bools)?
68        .checked_add(JSON_ERROR_BYTES)
69}
70
71#[derive(Default)]
72struct StringCapacity {
73    count: usize,
74    total: usize,
75    largest: usize,
76}
77
78impl StringCapacity {
79    // This is only a byte-capacity scan, not another JSON grammar. Serde remains
80    // authoritative for escapes, surrogate pairs, syntax and element types.
81    // In malformed input, count unfinished and nested strings conservatively.
82    fn scan(input: &str) -> Option<Self> {
83        let mut result = Self::default();
84        let mut start = None;
85        let mut escaped = false;
86        for (offset, byte) in input.bytes().enumerate() {
87            if let Some(begin) = start {
88                if escaped {
89                    escaped = false;
90                } else if byte == b'\\' {
91                    escaped = true;
92                } else if byte == b'"' {
93                    result.include(offset - begin)?;
94                    start = None;
95                }
96            } else if byte == b'"' {
97                result.count = result.count.checked_add(1)?;
98                start = Some(offset + 1);
99            }
100        }
101        if let Some(begin) = start {
102            result.include(input.len() - begin)?;
103        }
104        Some(result)
105    }
106
107    fn include(&mut self, bytes: usize) -> Option<()> {
108        self.total = self.total.checked_add(bytes)?;
109        self.largest = self.largest.max(bytes);
110        Some(())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::{SearchPredicate, SearchPredicateOp, SearchPredicateSet};
118    use std::collections::BTreeMap;
119
120    #[test]
121    fn metadata_admission_refusal_precedes_invalid_json_and_normalization() {
122        let filters = BTreeMap::from([("kind__in".into(), "not json".into())]);
123        let mut calls = 0;
124        let refused = SearchPredicateSet::from_metadata_filters_with_admission(&filters, |_| {
125            calls += 1;
126            if calls == 2 {
127                Err("shared budget")
128            } else {
129                Ok(())
130            }
131        });
132        assert_eq!(calls, 2);
133        assert_eq!(
134            refused,
135            Err(SearchPredicateConstructionError::Admission("shared budget"))
136        );
137        let parsed =
138            SearchPredicateSet::from_metadata_filters_with_admission(&filters, |_| Ok::<_, ()>(()));
139        assert!(
140            matches!(parsed, Err(SearchPredicateConstructionError::Parse(error)) if error.reason() == "expected JSON string array")
141        );
142    }
143
144    #[test]
145    fn metadata_capacity_counts_empty_values_and_excludes_json_whitespace() {
146        let plain = filter_capacity("labels__in", r#"["value"]"#).unwrap();
147        let spaced = filter_capacity(
148            "labels__in",
149            &format!("[{}\"value\"{}]", " ".repeat(4096), "\t".repeat(4096)),
150        )
151        .unwrap();
152        assert_eq!(plain, spaced);
153        let duplicate =
154            filter_capacity("labels__in", &format!("[{}]", vec![r#""""#; 100].join(","))).unwrap();
155        assert!(duplicate >= 100 * SET_ENTRY_BYTES);
156        let scanned = StringCapacity::scan(r#"["a\"b","\\","\uD83D\uDE80"]"#).unwrap();
157        assert_eq!(scanned.count, 3);
158        assert_eq!(scanned.largest, 12);
159        assert_eq!(scanned.total, 18);
160    }
161
162    #[test]
163    fn enum_normalization_reuses_owned_storage_and_preserves_unicode_trimming() {
164        for value in ["\u{2003}Memory\u{2003}", " \t\u{2003}", "Mixed CASE", ""] {
165            let owned = value.to_owned();
166            let pointer = owned.as_ptr();
167            let expected = value.trim().to_ascii_lowercase();
168            let predicate = SearchPredicate::eq("kind", owned);
169            let SearchPredicateOp::Eq(super::super::SearchScalarValue::Enum(actual)) =
170                predicate.op()
171            else {
172                panic!("kind must be an enum");
173            };
174            assert_eq!(actual, &expected);
175            assert_eq!(actual.as_ptr(), pointer);
176        }
177    }
178}