1use super::ascii_equal_lower;
2use crate::ast::{CollectionConfig, OptimizationThreads, Value};
3use crate::error::QqlError;
4use alloc::string::String;
5
6pub fn config_value<'a>(config: &'a [(String, Value)], key: &str) -> Option<&'a Value> {
7 for (k, v) in config {
8 if ascii_equal_lower(k, key) {
9 return Some(v);
10 }
11 }
12 None
13}
14
15pub fn config_has_key(config: &[(String, Value)], key: &str) -> bool {
16 config_value(config, key).is_some()
17}
18
19pub fn config_bool(config: &[(String, Value)], key: &str) -> Option<bool> {
20 match config_value(config, key)? {
21 Value::Bool(b) => Some(*b),
22 _ => None,
23 }
24}
25
26use crate::error::Span;
27
28fn validation_err(
29 message: impl Into<alloc::borrow::Cow<'static, str>>,
30 position: usize,
31) -> QqlError {
32 QqlError::validation(
33 "QQL-VALIDATION-CONFIG",
34 message,
35 Some(Span::point(position)),
36 )
37}
38
39pub fn config_positive_u64(
40 config: &[(String, Value)],
41 key: &str,
42 pos: usize,
43) -> Result<Option<u64>, QqlError> {
44 match config_value(config, key) {
45 None => Ok(None),
46 Some(Value::Int(n)) if *n > 0 => Ok(Some(*n as u64)),
47 Some(Value::Float(n)) if *n > 0.0 && *n == (*n as u64) as f64 => Ok(Some(*n as u64)),
48 _ => Err(validation_err(
49 alloc::format!("{} must be a positive integer", key),
50 pos,
51 )),
52 }
53}
54
55pub fn config_non_negative_u64(
56 config: &[(String, Value)],
57 key: &str,
58 pos: usize,
59) -> Result<Option<u64>, QqlError> {
60 match config_value(config, key) {
61 None => Ok(None),
62 Some(Value::Int(n)) if *n >= 0 => Ok(Some(*n as u64)),
63 Some(Value::Float(n)) if *n >= 0.0 && *n == (*n as u64) as f64 => Ok(Some(*n as u64)),
64 _ => Err(validation_err(
65 alloc::format!("{} must be a non-negative integer", key),
66 pos,
67 )),
68 }
69}
70
71pub fn config_float_range(
72 config: &[(String, Value)],
73 key: &str,
74 min: f64,
75 max: f64,
76) -> Option<f64> {
77 match config_value(config, key)? {
78 Value::Int(n) => {
79 let f = *n as f64;
80 if (min..=max).contains(&f) {
81 Some(f)
82 } else {
83 None
84 }
85 }
86 Value::Float(f) => {
87 if (min..=max).contains(f) {
88 Some(*f)
89 } else {
90 None
91 }
92 }
93 _ => None,
94 }
95}
96
97pub fn config_max_optimization_threads(
98 config: &[(String, Value)],
99 key: &str,
100) -> Option<OptimizationThreads> {
101 match config_value(config, key)? {
102 Value::Int(n) if *n > 0 => Some(OptimizationThreads {
103 auto_: false,
104 value: *n as u64,
105 }),
106 Value::Str(s) if ascii_equal_lower(s, "auto") => Some(OptimizationThreads {
107 auto_: true,
108 value: 0,
109 }),
110 _ => None,
111 }
112}
113
114pub fn is_integer_val(value: &Value) -> bool {
115 match value {
116 Value::Int(_) => true,
117 Value::Float(f) => *f >= 0.0 && *f == (*f as u64) as f64,
118 _ => false,
119 }
120}
121
122pub fn validate_hnsw_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
123 let lower = key.to_ascii_lowercase();
124 match lower.as_str() {
125 "m" | "ef_construct" | "full_scan_threshold" | "max_indexing_threads" | "payload_m" => {
126 if !is_integer_val(value) {
127 return Err(validation_err(
128 alloc::format!("{} must be an integer", key),
129 pos,
130 ));
131 }
132 }
133 "on_disk" | "inline_storage" if !matches!(value, Value::Bool(_)) => {
134 return Err(validation_err(
135 alloc::format!("{} must be true or false", key),
136 pos,
137 ));
138 }
139 _ => {}
140 }
141 Ok(())
142}
143
144pub fn validate_vectors_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
145 if ascii_equal_lower(key, "on_disk") && !matches!(value, Value::Bool(_)) {
146 return Err(validation_err(
147 alloc::format!("{} must be true or false", key),
148 pos,
149 ));
150 }
151 Ok(())
152}
153
154pub fn validate_optimizers_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
155 let lower = key.to_ascii_lowercase();
156 match lower.as_str() {
157 "deleted_threshold" => {
158 if !matches!(value, Value::Int(_) | Value::Float(_)) {
159 return Err(validation_err(
160 alloc::format!("{} must be a number", key),
161 pos,
162 ));
163 }
164 }
165 "vacuum_min_vector_number"
166 | "default_segment_number"
167 | "max_segment_size"
168 | "memmap_threshold"
169 | "indexing_threshold"
170 | "flush_interval_sec" => {
171 if !is_integer_val(value) {
172 return Err(validation_err(
173 alloc::format!("{} must be an integer", key),
174 pos,
175 ));
176 }
177 }
178 "max_optimization_threads" => {
179 if !is_integer_val(value) && !matches!(value, Value::Str(_)) {
180 return Err(validation_err(
181 alloc::format!("{} must be a positive integer or 'auto'", key),
182 pos,
183 ));
184 }
185 }
186 "prevent_unoptimized" if !matches!(value, Value::Bool(_)) => {
187 return Err(validation_err(
188 alloc::format!("{} must be true or false", key),
189 pos,
190 ));
191 }
192 _ => {}
193 }
194 Ok(())
195}
196
197pub fn validate_params_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
198 let lower = key.to_ascii_lowercase();
199 match lower.as_str() {
200 "replication_factor"
201 | "write_consistency_factor"
202 | "read_fan_out_factor"
203 | "read_fan_out_delay_ms"
204 | "shard_number" => {
205 if !matches!(value, Value::Int(_)) {
206 return Err(validation_err(
207 alloc::format!("{} must be an integer", key),
208 pos,
209 ));
210 }
211 }
212 "on_disk_payload" if !matches!(value, Value::Bool(_)) => {
213 return Err(validation_err(
214 alloc::format!("{} must be true or false", key),
215 pos,
216 ));
217 }
218 "sharding_method" => match value {
219 Value::Str(s) if s.eq_ignore_ascii_case("auto") || s.eq_ignore_ascii_case("custom") => {
220 }
221 Value::Str(_) => {
222 return Err(validation_err(
223 "sharding_method must be 'auto' or 'custom'",
224 pos,
225 ));
226 }
227 _ => {
228 return Err(validation_err(
229 "sharding_method must be a string ('auto' or 'custom')",
230 pos,
231 ));
232 }
233 },
234 "shard_keys" => match value {
235 Value::List(items) if items.is_empty() => {
236 return Err(validation_err(
237 "shard_keys must be a non-empty list of strings",
238 pos,
239 ));
240 }
241 Value::List(items) => {
242 for item in items {
243 if !matches!(item, Value::Str(_)) {
244 return Err(validation_err(
245 "shard_keys entries must all be strings",
246 pos,
247 ));
248 }
249 }
250 }
251 _ => {
252 return Err(validation_err("shard_keys must be a list of strings", pos));
253 }
254 },
255 _ => {}
256 }
257 Ok(())
258}
259
260pub fn merge_collection_config(
261 current: &mut CollectionConfig,
262 new: CollectionConfig,
263 pos: usize,
264) -> Result<(), QqlError> {
265 if new.vectors.is_some() {
266 if current.vectors.is_some() {
267 return Err(QqlError::syntax("VECTOR clause may only appear once", pos));
268 }
269 current.vectors = new.vectors;
270 }
271 if new.hnsw.is_some() {
272 if current.hnsw.is_some() {
273 return Err(QqlError::syntax("HNSW clause may only appear once", pos));
274 }
275 current.hnsw = new.hnsw;
276 }
277 if new.optimizers.is_some() {
278 if current.optimizers.is_some() {
279 return Err(QqlError::syntax(
280 "OPTIMIZERS clause may only appear once",
281 pos,
282 ));
283 }
284 current.optimizers = new.optimizers;
285 }
286 if new.params.is_some() {
287 if current.params.is_some() {
288 return Err(QqlError::syntax("PARAMS clause may only appear once", pos));
289 }
290 current.params = new.params;
291 }
292 if new.quantization.is_some() {
293 if current.quantization.is_some() {
294 return Err(QqlError::syntax(
295 "QUANTIZATION clause may only appear once",
296 pos,
297 ));
298 }
299 current.quantization = new.quantization;
300 }
301 if new.quantization_update.is_some() {
302 if current.quantization_update.is_some() {
303 return Err(QqlError::syntax(
304 "QUANTIZATION clause may only appear once",
305 pos,
306 ));
307 }
308 current.quantization_update = new.quantization_update;
309 }
310 Ok(())
311}
312
313pub fn check_deleted_threshold(value: &Value, pos: usize) -> Result<(), QqlError> {
314 match value {
315 Value::Int(n) => {
316 let f = *n as f64;
317 if !(0.0..=1.0).contains(&f) {
318 return Err(QqlError::syntax(
319 "deleted_threshold must be between 0.0 and 1.0",
320 pos,
321 ));
322 }
323 }
324 Value::Float(f) if !(0.0..=1.0).contains(f) => {
325 return Err(QqlError::syntax(
326 "deleted_threshold must be between 0.0 and 1.0",
327 pos,
328 ));
329 }
330 _ => {}
331 }
332 Ok(())
333}
334
335pub fn validate_index_options(options: &[(String, Value)], pos: usize) -> Result<(), QqlError> {
336 for (k, v) in options {
337 let lower = k.to_ascii_lowercase();
338 match lower.as_str() {
339 "is_tenant" | "on_disk" | "enable_hnsw" | "lowercase" | "ascii_folding"
340 | "phrase_matching" | "lookup" | "range" | "is_principal" => {
341 if !matches!(v, Value::Bool(_)) {
342 return Err(QqlError::syntax(
343 alloc::format!("{} must be true or false", k),
344 pos,
345 ));
346 }
347 }
348 "min_token_len" | "max_token_len" => {
349 if !matches!(v, Value::Int(n) if *n >= 0) {
350 return Err(QqlError::syntax(
351 alloc::format!("{} must be a non-negative integer", k),
352 pos,
353 ));
354 }
355 }
356 "tokenizer" | "stemmer" => {
357 if !matches!(v, Value::Str(_)) {
358 return Err(QqlError::syntax(
359 alloc::format!("{} must be a string", k),
360 pos,
361 ));
362 }
363 }
364 "stopwords" => match v {
365 Value::List(items) => {
366 for item in items {
367 if !matches!(item, Value::Str(_)) {
368 return Err(QqlError::syntax(
369 alloc::format!("{} must be a list of strings", k),
370 pos,
371 ));
372 }
373 }
374 }
375 _ => {
376 return Err(QqlError::syntax(
377 alloc::format!("{} must be a list of strings", k),
378 pos,
379 ));
380 }
381 },
382 _ => {
383 return Err(QqlError::syntax(
384 alloc::format!("unknown index option: {}", k),
385 pos,
386 ));
387 }
388 }
389 }
390 Ok(())
391}