1use super::{Field, Value, WhereOperator};
20use crate::Result;
21use std::collections::HashMap;
22
23fn escape_like_literal(s: &str) -> String {
29 s.replace('\\', "\\\\")
31 .replace('%', "\\%")
32 .replace('_', "\\_")
33}
34
35const fn infer_type_cast(value: &Value) -> &'static str {
39 match value {
40 Value::String(_) => "::text",
41 Value::Number(_) => "::numeric", Value::Bool(_) => "::boolean",
43 Value::Null => "", Value::Array(_) => "", Value::FloatArray(_) => "", Value::RawSql(_) => "", }
48}
49
50pub fn generate_where_operator_sql(
79 operator: &WhereOperator,
80 param_index: &mut usize,
81 params: &mut HashMap<usize, Value>,
82) -> Result<String> {
83 operator.validate().map_err(crate::Error::InvalidSchema)?;
84
85 match operator {
86 WhereOperator::Eq(field, value) => {
90 let field_sql = field.to_sql();
91 if value.is_null() {
92 Ok(format!("{} IS NULL", field_sql))
93 } else {
94 let param_num = *param_index + 1;
95 *param_index += 1;
96 params.insert(param_num, value.clone());
97 let cast = match field {
99 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
100 Field::DirectColumn(_) => "", };
102 Ok(format!("{}{} = ${}", field_sql, cast, param_num))
103 }
104 }
105
106 WhereOperator::Neq(field, value) => {
107 let field_sql = field.to_sql();
108 if value.is_null() {
109 Ok(format!("{} IS NOT NULL", field_sql))
110 } else {
111 let param_num = *param_index + 1;
112 *param_index += 1;
113 params.insert(param_num, value.clone());
114 let cast = match field {
115 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
116 Field::DirectColumn(_) => "",
117 };
118 Ok(format!("{}{} != ${}", field_sql, cast, param_num))
119 }
120 }
121
122 WhereOperator::Gt(field, value) => {
123 let field_sql = field.to_sql();
124 let param_num = *param_index + 1;
125 *param_index += 1;
126 params.insert(param_num, value.clone());
127 let cast = match field {
128 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
129 Field::DirectColumn(_) => "",
130 };
131 Ok(format!("{}{} > ${}", field_sql, cast, param_num))
132 }
133
134 WhereOperator::Gte(field, value) => {
135 let field_sql = field.to_sql();
136 let param_num = *param_index + 1;
137 *param_index += 1;
138 params.insert(param_num, value.clone());
139 let cast = match field {
140 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
141 Field::DirectColumn(_) => "",
142 };
143 Ok(format!("{}{} >= ${}", field_sql, cast, param_num))
144 }
145
146 WhereOperator::Lt(field, value) => {
147 let field_sql = field.to_sql();
148 let param_num = *param_index + 1;
149 *param_index += 1;
150 params.insert(param_num, value.clone());
151 let cast = match field {
152 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
153 Field::DirectColumn(_) => "",
154 };
155 Ok(format!("{}{} < ${}", field_sql, cast, param_num))
156 }
157
158 WhereOperator::Lte(field, value) => {
159 let field_sql = field.to_sql();
160 let param_num = *param_index + 1;
161 *param_index += 1;
162 params.insert(param_num, value.clone());
163 let cast = match field {
164 Field::JsonbField(_) | Field::JsonbPath(_) => infer_type_cast(value),
165 Field::DirectColumn(_) => "",
166 };
167 Ok(format!("{}{} <= ${}", field_sql, cast, param_num))
168 }
169
170 WhereOperator::In(field, values) => {
172 if values.is_empty() {
174 return Ok("FALSE".to_string());
175 }
176 let field_sql = field.to_sql();
177 let placeholders: Vec<String> = values
178 .iter()
179 .map(|v| {
180 let param_num = *param_index + 1;
181 *param_index += 1;
182 params.insert(param_num, v.clone());
183 format!("${}", param_num)
184 })
185 .collect();
186 Ok(format!("{} IN ({})", field_sql, placeholders.join(", ")))
187 }
188
189 WhereOperator::Nin(field, values) => {
190 if values.is_empty() {
192 return Ok("TRUE".to_string());
193 }
194 let field_sql = field.to_sql();
195 let placeholders: Vec<String> = values
196 .iter()
197 .map(|v| {
198 let param_num = *param_index + 1;
199 *param_index += 1;
200 params.insert(param_num, v.clone());
201 format!("${}", param_num)
202 })
203 .collect();
204 Ok(format!(
205 "{} NOT IN ({})",
206 field_sql,
207 placeholders.join(", ")
208 ))
209 }
210
211 WhereOperator::Contains(field, substring) => {
212 let field_sql = field.to_sql();
213 let param_num = *param_index + 1;
214 *param_index += 1;
215 params.insert(param_num, Value::String(escape_like_literal(substring)));
216 Ok(format!(
217 "{} LIKE '%' || ${}::text || '%'",
218 field_sql, param_num
219 ))
220 }
221
222 WhereOperator::ArrayContains(field, value) => {
223 let field_sql = field.to_sql();
224 let param_num = *param_index + 1;
225 *param_index += 1;
226 params.insert(param_num, value.clone());
227 Ok(format!("{} @> ARRAY[${}]", field_sql, param_num))
228 }
229
230 WhereOperator::ArrayContainedBy(field, value) => {
231 let field_sql = field.to_sql();
232 let param_num = *param_index + 1;
233 *param_index += 1;
234 params.insert(param_num, value.clone());
235 Ok(format!("{} <@ ARRAY[${}]", field_sql, param_num))
236 }
237
238 WhereOperator::ArrayOverlaps(field, values) => {
239 let field_sql = field.to_sql();
240 let placeholders: Vec<String> = values
241 .iter()
242 .map(|v| {
243 let param_num = *param_index + 1;
244 *param_index += 1;
245 params.insert(param_num, v.clone());
246 format!("${}", param_num)
247 })
248 .collect();
249 Ok(format!(
250 "{} && ARRAY[{}]",
251 field_sql,
252 placeholders.join(", ")
253 ))
254 }
255
256 WhereOperator::LenEq(field, len) => {
258 let field_sql = field.to_sql();
259 Ok(format!("array_length({}, 1) = {}", field_sql, len))
260 }
261
262 WhereOperator::LenGt(field, len) => {
263 let field_sql = field.to_sql();
264 Ok(format!("array_length({}, 1) > {}", field_sql, len))
265 }
266
267 WhereOperator::LenGte(field, len) => {
268 let field_sql = field.to_sql();
269 Ok(format!("array_length({}, 1) >= {}", field_sql, len))
270 }
271
272 WhereOperator::LenLt(field, len) => {
273 let field_sql = field.to_sql();
274 Ok(format!("array_length({}, 1) < {}", field_sql, len))
275 }
276
277 WhereOperator::LenLte(field, len) => {
278 let field_sql = field.to_sql();
279 Ok(format!("array_length({}, 1) <= {}", field_sql, len))
280 }
281
282 WhereOperator::Icontains(field, substring) => {
284 let field_sql = field.to_sql();
285 let param_num = *param_index + 1;
286 *param_index += 1;
287 params.insert(param_num, Value::String(escape_like_literal(substring)));
288 Ok(format!(
289 "{} ILIKE '%' || ${}::text || '%'",
290 field_sql, param_num
291 ))
292 }
293
294 WhereOperator::Startswith(field, prefix) => {
295 let field_sql = field.to_sql();
296 let param_num = *param_index + 1;
297 *param_index += 1;
298 params.insert(
299 param_num,
300 Value::String(format!("{}%", escape_like_literal(prefix))),
301 );
302 Ok(format!("{} LIKE ${}", field_sql, param_num))
303 }
304
305 WhereOperator::Istartswith(field, prefix) => {
306 let field_sql = field.to_sql();
307 let param_num = *param_index + 1;
308 *param_index += 1;
309 params.insert(
310 param_num,
311 Value::String(format!("{}%", escape_like_literal(prefix))),
312 );
313 Ok(format!("{} ILIKE ${}", field_sql, param_num))
314 }
315
316 WhereOperator::Endswith(field, suffix) => {
317 let field_sql = field.to_sql();
318 let param_num = *param_index + 1;
319 *param_index += 1;
320 params.insert(
321 param_num,
322 Value::String(format!("%{}", escape_like_literal(suffix))),
323 );
324 Ok(format!("{} LIKE ${}", field_sql, param_num))
325 }
326
327 WhereOperator::Iendswith(field, suffix) => {
328 let field_sql = field.to_sql();
329 let param_num = *param_index + 1;
330 *param_index += 1;
331 params.insert(
332 param_num,
333 Value::String(format!("%{}", escape_like_literal(suffix))),
334 );
335 Ok(format!("{} ILIKE ${}", field_sql, param_num))
336 }
337
338 WhereOperator::Like(field, pattern) => {
339 let field_sql = field.to_sql();
340 let param_num = *param_index + 1;
341 *param_index += 1;
342 params.insert(param_num, Value::String(pattern.clone()));
343 Ok(format!("{} LIKE ${}", field_sql, param_num))
344 }
345
346 WhereOperator::Ilike(field, pattern) => {
347 let field_sql = field.to_sql();
348 let param_num = *param_index + 1;
349 *param_index += 1;
350 params.insert(param_num, Value::String(pattern.clone()));
351 Ok(format!("{} ILIKE ${}", field_sql, param_num))
352 }
353
354 WhereOperator::IsNull(field, is_null) => {
356 let field_sql = field.to_sql();
357 if *is_null {
358 Ok(format!("{} IS NULL", field_sql))
359 } else {
360 Ok(format!("{} IS NOT NULL", field_sql))
361 }
362 }
363
364 WhereOperator::L2Distance {
366 field,
367 vector,
368 threshold,
369 } => {
370 let field_sql = field.to_sql();
371 let param_num = *param_index + 1;
372 *param_index += 1;
373 params.insert(param_num, Value::FloatArray(vector.clone()));
374 Ok(format!(
375 "l2_distance({}::vector, ${}::vector) < {}",
376 field_sql, param_num, threshold
377 ))
378 }
379
380 WhereOperator::CosineDistance {
381 field,
382 vector,
383 threshold,
384 } => {
385 let field_sql = field.to_sql();
386 let param_num = *param_index + 1;
387 *param_index += 1;
388 params.insert(param_num, Value::FloatArray(vector.clone()));
389 Ok(format!(
390 "cosine_distance({}::vector, ${}::vector) < {}",
391 field_sql, param_num, threshold
392 ))
393 }
394
395 WhereOperator::InnerProduct {
396 field,
397 vector,
398 threshold,
399 } => {
400 let field_sql = field.to_sql();
401 let param_num = *param_index + 1;
402 *param_index += 1;
403 params.insert(param_num, Value::FloatArray(vector.clone()));
404 Ok(format!(
405 "inner_product({}::vector, ${}::vector) > {}",
406 field_sql, param_num, threshold
407 ))
408 }
409
410 WhereOperator::L1Distance {
411 field,
412 vector,
413 threshold,
414 } => {
415 let field_sql = field.to_sql();
416 let param_num = *param_index + 1;
417 *param_index += 1;
418 params.insert(param_num, Value::FloatArray(vector.clone()));
419 Ok(format!(
420 "l1_distance({}::vector, ${}::vector) < {}",
421 field_sql, param_num, threshold
422 ))
423 }
424
425 WhereOperator::HammingDistance {
426 field,
427 vector,
428 threshold,
429 } => {
430 let field_sql = field.to_sql();
431 let param_num = *param_index + 1;
432 *param_index += 1;
433 params.insert(param_num, Value::FloatArray(vector.clone()));
434 Ok(format!(
435 "hamming_distance({}::bit, ${}::bit) < {}",
436 field_sql, param_num, threshold
437 ))
438 }
439
440 WhereOperator::JaccardDistance {
441 field,
442 set,
443 threshold,
444 } => {
445 let field_sql = field.to_sql();
446 let param_num = *param_index + 1;
447 *param_index += 1;
448 let value_array: Vec<Value> = set.iter().map(|s| Value::String(s.clone())).collect();
449 params.insert(param_num, Value::Array(value_array));
450 Ok(format!(
451 "jaccard_distance({}::text[], ${}::text[]) < {}",
452 field_sql, param_num, threshold
453 ))
454 }
455
456 WhereOperator::Matches {
458 field,
459 query,
460 language,
461 } => {
462 let field_sql = field.to_sql();
463 let param_num = *param_index + 1;
464 *param_index += 1;
465 params.insert(param_num, Value::String(query.clone()));
466 let lang = language.as_deref().unwrap_or("english");
467 Ok(format!(
468 "{} @@ plainto_tsquery('{}', ${})",
469 field_sql, lang, param_num
470 ))
471 }
472
473 WhereOperator::PlainQuery { field, query } => {
474 let field_sql = field.to_sql();
475 let param_num = *param_index + 1;
476 *param_index += 1;
477 params.insert(param_num, Value::String(query.clone()));
478 Ok(format!(
479 "{} @@ plainto_tsquery(${})::tsvector",
480 field_sql, param_num
481 ))
482 }
483
484 WhereOperator::PhraseQuery {
485 field,
486 query,
487 language,
488 } => {
489 let field_sql = field.to_sql();
490 let param_num = *param_index + 1;
491 *param_index += 1;
492 params.insert(param_num, Value::String(query.clone()));
493 let lang = language.as_deref().unwrap_or("english");
494 Ok(format!(
495 "{} @@ phraseto_tsquery('{}', ${})",
496 field_sql, lang, param_num
497 ))
498 }
499
500 WhereOperator::WebsearchQuery {
501 field,
502 query,
503 language,
504 } => {
505 let field_sql = field.to_sql();
506 let param_num = *param_index + 1;
507 *param_index += 1;
508 params.insert(param_num, Value::String(query.clone()));
509 let lang = language.as_deref().unwrap_or("english");
510 Ok(format!(
511 "{} @@ websearch_to_tsquery('{}', ${})",
512 field_sql, lang, param_num
513 ))
514 }
515
516 WhereOperator::IsIPv4(field) => {
518 let field_sql = field.to_sql();
519 Ok(format!("family({}::inet) = 4", field_sql))
520 }
521
522 WhereOperator::IsIPv6(field) => {
523 let field_sql = field.to_sql();
524 Ok(format!("family({}::inet) = 6", field_sql))
525 }
526
527 WhereOperator::IsPrivate(field) => {
528 let field_sql = field.to_sql();
529 Ok(format!(
531 "({}::inet << '10.0.0.0/8'::inet OR {}::inet << '172.16.0.0/12'::inet OR {}::inet << '192.168.0.0/16'::inet OR {}::inet << '169.254.0.0/16'::inet)",
532 field_sql, field_sql, field_sql, field_sql
533 ))
534 }
535
536 WhereOperator::IsPublic(field) => {
537 let field_sql = field.to_sql();
538 Ok(format!(
540 "NOT ({}::inet << '10.0.0.0/8'::inet OR {}::inet << '172.16.0.0/12'::inet OR {}::inet << '192.168.0.0/16'::inet OR {}::inet << '169.254.0.0/16'::inet)",
541 field_sql, field_sql, field_sql, field_sql
542 ))
543 }
544
545 WhereOperator::IsLoopback(field) => {
546 let field_sql = field.to_sql();
547 Ok(format!(
548 "(family({}::inet) = 4 AND {}::inet << '127.0.0.0/8'::inet) OR (family({}::inet) = 6 AND {}::inet << '::1/128'::inet)",
549 field_sql, field_sql, field_sql, field_sql
550 ))
551 }
552
553 WhereOperator::InSubnet { field, subnet } => {
554 let field_sql = field.to_sql();
555 let param_num = *param_index + 1;
556 *param_index += 1;
557 params.insert(param_num, Value::String(subnet.clone()));
558 Ok(format!("{}::inet << ${}::inet", field_sql, param_num))
559 }
560
561 WhereOperator::ContainsSubnet { field, subnet } => {
562 let field_sql = field.to_sql();
563 let param_num = *param_index + 1;
564 *param_index += 1;
565 params.insert(param_num, Value::String(subnet.clone()));
566 Ok(format!("{}::inet >> ${}::inet", field_sql, param_num))
567 }
568
569 WhereOperator::ContainsIP { field, ip } => {
570 let field_sql = field.to_sql();
571 let param_num = *param_index + 1;
572 *param_index += 1;
573 params.insert(param_num, Value::String(ip.clone()));
574 Ok(format!("{}::inet >> ${}::inet", field_sql, param_num))
575 }
576
577 WhereOperator::IPRangeOverlap { field, range } => {
578 let field_sql = field.to_sql();
579 let param_num = *param_index + 1;
580 *param_index += 1;
581 params.insert(param_num, Value::String(range.clone()));
582 Ok(format!("{}::inet && ${}::inet", field_sql, param_num))
583 }
584
585 WhereOperator::StrictlyContains(field, value) => {
587 let field_sql = field.to_sql();
588 let param_num = *param_index + 1;
589 *param_index += 1;
590 params.insert(param_num, value.clone());
591 Ok(format!("{}::jsonb @> ${}::jsonb", field_sql, param_num))
592 }
593
594 WhereOperator::AncestorOf { field, path } => {
596 let field_sql = field.to_sql();
597 let param_num = *param_index + 1;
598 *param_index += 1;
599 params.insert(param_num, Value::String(path.clone()));
600 Ok(format!("{}::ltree @> ${}::ltree", field_sql, param_num))
601 }
602
603 WhereOperator::DescendantOf { field, path } => {
604 let field_sql = field.to_sql();
605 let param_num = *param_index + 1;
606 *param_index += 1;
607 params.insert(param_num, Value::String(path.clone()));
608 Ok(format!("{}::ltree <@ ${}::ltree", field_sql, param_num))
609 }
610
611 WhereOperator::MatchesLquery { field, pattern } => {
612 let field_sql = field.to_sql();
613 let param_num = *param_index + 1;
614 *param_index += 1;
615 params.insert(param_num, Value::String(pattern.clone()));
616 Ok(format!("{}::ltree ~ ${}::lquery", field_sql, param_num))
617 }
618
619 WhereOperator::MatchesLtxtquery { field, query } => {
620 let field_sql = field.to_sql();
621 let param_num = *param_index + 1;
622 *param_index += 1;
623 params.insert(param_num, Value::String(query.clone()));
624 Ok(format!("{}::ltree @ ${}::ltxtquery", field_sql, param_num))
625 }
626
627 WhereOperator::MatchesAnyLquery { field, patterns } => {
628 let field_sql = field.to_sql();
629 let placeholders: Vec<String> = patterns
630 .iter()
631 .map(|p| {
632 let param_num = *param_index + 1;
633 *param_index += 1;
634 params.insert(param_num, Value::String(p.clone()));
635 format!("${}::lquery", param_num)
636 })
637 .collect();
638 Ok(format!(
639 "{}::ltree ? ARRAY[{}]",
640 field_sql,
641 placeholders.join(", ")
642 ))
643 }
644
645 WhereOperator::DepthEq { field, depth } => {
647 let field_sql = field.to_sql();
648 Ok(format!("nlevel({}::ltree) = {}", field_sql, depth))
649 }
650
651 WhereOperator::DepthNeq { field, depth } => {
652 let field_sql = field.to_sql();
653 Ok(format!("nlevel({}::ltree) != {}", field_sql, depth))
654 }
655
656 WhereOperator::DepthGt { field, depth } => {
657 let field_sql = field.to_sql();
658 Ok(format!("nlevel({}::ltree) > {}", field_sql, depth))
659 }
660
661 WhereOperator::DepthGte { field, depth } => {
662 let field_sql = field.to_sql();
663 Ok(format!("nlevel({}::ltree) >= {}", field_sql, depth))
664 }
665
666 WhereOperator::DepthLt { field, depth } => {
667 let field_sql = field.to_sql();
668 Ok(format!("nlevel({}::ltree) < {}", field_sql, depth))
669 }
670
671 WhereOperator::DepthLte { field, depth } => {
672 let field_sql = field.to_sql();
673 Ok(format!("nlevel({}::ltree) <= {}", field_sql, depth))
674 }
675
676 WhereOperator::Lca { field, paths } => {
678 let field_sql = field.to_sql();
679 let placeholders: Vec<String> = paths
680 .iter()
681 .map(|p| {
682 let param_num = *param_index + 1;
683 *param_index += 1;
684 params.insert(param_num, Value::String(p.clone()));
685 format!("${}::ltree", param_num)
686 })
687 .collect();
688 Ok(format!(
689 "{}::ltree = lca(ARRAY[{}])",
690 field_sql,
691 placeholders.join(", ")
692 ))
693 }
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 #![allow(clippy::unwrap_used)] use super::*;
701
702 #[test]
703 fn test_eq_operator_jsonb_string() {
704 let mut param_index = 0;
705 let mut params = HashMap::new();
706 let op = WhereOperator::Eq(
707 Field::JsonbField("name".to_string()),
708 Value::String("John".to_string()),
709 );
710 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
711 assert_eq!(sql, "(data->'name')::text = $1");
713 assert_eq!(param_index, 1);
714 }
715
716 #[test]
717 fn test_eq_operator_direct_column() {
718 let mut param_index = 0;
719 let mut params = HashMap::new();
720 let op = WhereOperator::Eq(
721 Field::DirectColumn("status".to_string()),
722 Value::String("active".to_string()),
723 );
724 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
725 assert_eq!(sql, "status = $1");
727 assert_eq!(param_index, 1);
728 }
729
730 #[test]
731 fn test_len_eq_operator() {
732 let mut param_index = 0;
733 let mut params = HashMap::new();
734 let op = WhereOperator::LenEq(Field::JsonbField("tags".to_string()), 5);
735 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
736 assert_eq!(sql, "array_length((data->'tags'), 1) = 5");
737 assert_eq!(param_index, 0); }
739
740 #[test]
741 fn test_is_ipv4_operator() {
742 let mut param_index = 0;
743 let mut params = HashMap::new();
744 let op = WhereOperator::IsIPv4(Field::JsonbField("ip".to_string()));
745 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
746 assert_eq!(sql, "family((data->'ip')::inet) = 4");
747 }
748
749 #[test]
750 fn test_l2_distance_operator() {
751 let mut param_index = 0;
752 let mut params = HashMap::new();
753 let op = WhereOperator::L2Distance {
754 field: Field::JsonbField("embedding".to_string()),
755 vector: vec![0.1, 0.2, 0.3],
756 threshold: 0.5,
757 };
758 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
759 assert_eq!(
760 sql,
761 "l2_distance((data->'embedding')::vector, $1::vector) < 0.5"
762 );
763 assert_eq!(param_index, 1);
764 }
765
766 #[test]
767 fn test_in_operator() {
768 let mut param_index = 0;
769 let mut params = HashMap::new();
770 let op = WhereOperator::In(
771 Field::JsonbField("status".to_string()),
772 vec![
773 Value::String("active".to_string()),
774 Value::String("pending".to_string()),
775 ],
776 );
777 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
778 assert_eq!(sql, "(data->'status') IN ($1, $2)");
779 assert_eq!(param_index, 2);
780 }
781
782 #[test]
783 fn test_in_empty_list_returns_false() {
784 let mut param_index = 0;
785 let mut params = HashMap::new();
786 let op = WhereOperator::In(Field::DirectColumn("status".to_string()), vec![]);
787 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
788 assert_eq!(sql, "FALSE");
789 assert_eq!(param_index, 0, "no parameters consumed for empty IN");
790 }
791
792 #[test]
793 fn test_nin_empty_list_returns_true() {
794 let mut param_index = 0;
795 let mut params = HashMap::new();
796 let op = WhereOperator::Nin(Field::DirectColumn("status".to_string()), vec![]);
797 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
798 assert_eq!(sql, "TRUE");
799 assert_eq!(param_index, 0, "no parameters consumed for empty NOT IN");
800 }
801
802 fn value_as_str(v: &Value) -> &str {
804 match v {
805 Value::String(s) => s.as_str(),
806 other => panic!("expected Value::String, got {other:?}"),
807 }
808 }
809
810 #[test]
811 fn test_contains_escapes_percent() {
812 let mut param_index = 0;
813 let mut params = HashMap::new();
814 let op =
815 WhereOperator::Contains(Field::DirectColumn("note".to_string()), "50%".to_string());
816 generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
817 assert_eq!(value_as_str(¶ms[&1]), "50\\%");
818 }
819
820 #[test]
821 fn test_contains_escapes_underscore() {
822 let mut param_index = 0;
823 let mut params = HashMap::new();
824 let op =
825 WhereOperator::Contains(Field::DirectColumn("code".to_string()), "A_B".to_string());
826 generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
827 assert_eq!(value_as_str(¶ms[&1]), "A\\_B");
828 }
829
830 #[test]
831 fn test_startswith_escapes_wildcard_in_prefix() {
832 let mut param_index = 0;
833 let mut params = HashMap::new();
834 let op =
835 WhereOperator::Startswith(Field::DirectColumn("name".to_string()), "C%D".to_string());
836 generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
837 assert_eq!(value_as_str(¶ms[&1]), "C\\%D%");
839 }
840
841 #[test]
842 fn test_endswith_escapes_wildcard_in_suffix() {
843 let mut param_index = 0;
844 let mut params = HashMap::new();
845 let op = WhereOperator::Endswith(
846 Field::DirectColumn("name".to_string()),
847 "_suffix".to_string(),
848 );
849 generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
850 assert_eq!(value_as_str(¶ms[&1]), "%\\_suffix");
852 }
853
854 #[test]
855 fn test_escape_like_literal_backslash() {
856 assert_eq!(escape_like_literal("a\\b"), "a\\\\b");
857 assert_eq!(escape_like_literal("a%b"), "a\\%b");
858 assert_eq!(escape_like_literal("a_b"), "a\\_b");
859 assert_eq!(escape_like_literal("100%_\\n"), "100\\%\\_\\\\n");
861 }
862
863 #[test]
866 fn test_ltree_ancestor_of() {
867 let mut param_index = 0;
868 let mut params = HashMap::new();
869 let op = WhereOperator::AncestorOf {
870 field: Field::DirectColumn("path".to_string()),
871 path: "Top.Sciences.Astronomy".to_string(),
872 };
873 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
874 assert_eq!(sql, "path::ltree @> $1::ltree");
875 assert_eq!(param_index, 1);
876 }
877
878 #[test]
879 fn test_ltree_descendant_of() {
880 let mut param_index = 0;
881 let mut params = HashMap::new();
882 let op = WhereOperator::DescendantOf {
883 field: Field::DirectColumn("path".to_string()),
884 path: "Top.Sciences".to_string(),
885 };
886 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
887 assert_eq!(sql, "path::ltree <@ $1::ltree");
888 assert_eq!(param_index, 1);
889 }
890
891 #[test]
892 fn test_ltree_matches_lquery() {
893 let mut param_index = 0;
894 let mut params = HashMap::new();
895 let op = WhereOperator::MatchesLquery {
896 field: Field::DirectColumn("path".to_string()),
897 pattern: "Top.*.Ast*".to_string(),
898 };
899 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
900 assert_eq!(sql, "path::ltree ~ $1::lquery");
901 assert_eq!(param_index, 1);
902 }
903
904 #[test]
905 fn test_ltree_matches_ltxtquery() {
906 let mut param_index = 0;
907 let mut params = HashMap::new();
908 let op = WhereOperator::MatchesLtxtquery {
909 field: Field::DirectColumn("path".to_string()),
910 query: "Science & !Deprecated".to_string(),
911 };
912 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
913 assert_eq!(sql, "path::ltree @ $1::ltxtquery");
914 assert_eq!(param_index, 1);
915 }
916
917 #[test]
918 fn test_ltree_matches_any_lquery() {
919 let mut param_index = 0;
920 let mut params = HashMap::new();
921 let op = WhereOperator::MatchesAnyLquery {
922 field: Field::DirectColumn("path".to_string()),
923 patterns: vec!["Top.*".to_string(), "Other.*".to_string()],
924 };
925 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
926 assert_eq!(sql, "path::ltree ? ARRAY[$1::lquery, $2::lquery]");
927 assert_eq!(param_index, 2);
928 }
929
930 #[test]
931 fn test_ltree_depth_eq() {
932 let mut param_index = 0;
933 let mut params = HashMap::new();
934 let op = WhereOperator::DepthEq {
935 field: Field::DirectColumn("path".to_string()),
936 depth: 3,
937 };
938 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
939 assert_eq!(sql, "nlevel(path::ltree) = 3");
940 assert_eq!(param_index, 0); }
942
943 #[test]
944 fn test_ltree_depth_gt() {
945 let mut param_index = 0;
946 let mut params = HashMap::new();
947 let op = WhereOperator::DepthGt {
948 field: Field::DirectColumn("path".to_string()),
949 depth: 2,
950 };
951 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
952 assert_eq!(sql, "nlevel(path::ltree) > 2");
953 assert_eq!(param_index, 0);
954 }
955
956 #[test]
957 fn test_ltree_depth_lte() {
958 let mut param_index = 0;
959 let mut params = HashMap::new();
960 let op = WhereOperator::DepthLte {
961 field: Field::DirectColumn("path".to_string()),
962 depth: 5,
963 };
964 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
965 assert_eq!(sql, "nlevel(path::ltree) <= 5");
966 assert_eq!(param_index, 0);
967 }
968
969 #[test]
970 fn test_ltree_lca() {
971 let mut param_index = 0;
972 let mut params = HashMap::new();
973 let op = WhereOperator::Lca {
974 field: Field::DirectColumn("path".to_string()),
975 paths: vec![
976 "Org.Engineering.Backend".to_string(),
977 "Org.Engineering.Frontend".to_string(),
978 ],
979 };
980 let sql = generate_where_operator_sql(&op, &mut param_index, &mut params).unwrap();
981 assert_eq!(sql, "path::ltree = lca(ARRAY[$1::ltree, $2::ltree])");
982 assert_eq!(param_index, 2);
983 }
984}