hawdb_optimizer_predicate/predicate/
construction.rs1use 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
24const SET_ENTRY_BYTES: usize = 1024;
28const MAX_REASON_BYTES: usize = 64;
29const 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 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 return common.checked_add(value.len())?.checked_add(5);
47 }
48 let strings = StringCapacity::scan(value)?;
49 let vector = strings
52 .count
53 .max(4)
54 .checked_mul(3)?
55 .checked_mul(size_of::<String>())?;
56 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 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}