1use std::{
4 collections::{HashMap, HashSet},
5 sync::Arc,
6};
7
8use crate::{
9 db::where_clause::{WhereClause, WhereOperator},
10 error::{FraiseQLError, Result},
11};
12
13pub type IndexedColumnsCache = HashMap<String, HashSet<String>>;
42
43pub struct PostgresWhereGenerator {
102 param_counter: std::cell::Cell<usize>,
111 indexed_columns: Option<Arc<HashSet<String>>>,
115}
116
117impl PostgresWhereGenerator {
118 #[must_use]
120 pub fn new() -> Self {
121 Self {
122 param_counter: std::cell::Cell::new(0),
123 indexed_columns: None,
124 }
125 }
126
127 #[must_use]
148 pub fn with_indexed_columns(indexed_columns: Arc<HashSet<String>>) -> Self {
149 Self {
150 param_counter: std::cell::Cell::new(0),
151 indexed_columns: Some(indexed_columns),
152 }
153 }
154
155 pub fn generate(&self, clause: &WhereClause) -> Result<(String, Vec<serde_json::Value>)> {
169 self.param_counter.set(0);
170 let mut params = Vec::new();
171 let sql = self.generate_clause(clause, &mut params)?;
172 Ok((sql, params))
173 }
174
175 fn generate_clause(
176 &self,
177 clause: &WhereClause,
178 params: &mut Vec<serde_json::Value>,
179 ) -> Result<String> {
180 match clause {
181 WhereClause::Field {
182 path,
183 operator,
184 value,
185 } => self.generate_field(path, operator, value, params),
186 WhereClause::And(clauses) => {
187 if clauses.is_empty() {
188 return Ok("TRUE".to_string());
189 }
190 let parts: Result<Vec<String>> =
191 clauses.iter().map(|c| self.generate_clause(c, params)).collect();
192 Ok(format!("({})", parts?.join(" AND ")))
193 },
194 WhereClause::Or(clauses) => {
195 if clauses.is_empty() {
196 return Ok("FALSE".to_string());
197 }
198 let parts: Result<Vec<String>> =
199 clauses.iter().map(|c| self.generate_clause(c, params)).collect();
200 Ok(format!("({})", parts?.join(" OR ")))
201 },
202 WhereClause::Not(clause) => {
203 let inner = self.generate_clause(clause, params)?;
204 Ok(format!("NOT ({inner})"))
205 },
206 }
207 }
208
209 fn generate_field(
210 &self,
211 path: &[String],
212 operator: &WhereOperator,
213 value: &serde_json::Value,
214 params: &mut Vec<serde_json::Value>,
215 ) -> Result<String> {
216 let field_path = self.build_jsonb_path(path);
218
219 match operator {
221 WhereOperator::Eq => self.generate_comparison(&field_path, "=", value, params),
223 WhereOperator::Neq => self.generate_comparison(&field_path, "!=", value, params),
224 WhereOperator::Gt => self.generate_comparison(&field_path, ">", value, params),
225 WhereOperator::Gte => self.generate_comparison(&field_path, ">=", value, params),
226 WhereOperator::Lt => self.generate_comparison(&field_path, "<", value, params),
227 WhereOperator::Lte => self.generate_comparison(&field_path, "<=", value, params),
228
229 WhereOperator::In => self.generate_in(&field_path, value, params),
231 WhereOperator::Nin => {
232 let in_clause = self.generate_in(&field_path, value, params)?;
233 Ok(format!("NOT ({in_clause})"))
234 },
235
236 WhereOperator::Contains => {
238 self.generate_like(&field_path, "LIKE", value, params, true, true)
239 },
240 WhereOperator::Icontains => {
241 self.generate_like(&field_path, "ILIKE", value, params, true, true)
242 },
243 WhereOperator::Startswith => {
244 self.generate_like(&field_path, "LIKE", value, params, false, true)
245 },
246 WhereOperator::Istartswith => {
247 self.generate_like(&field_path, "ILIKE", value, params, false, true)
248 },
249 WhereOperator::Endswith => {
250 self.generate_like(&field_path, "LIKE", value, params, true, false)
251 },
252 WhereOperator::Iendswith => {
253 self.generate_like(&field_path, "ILIKE", value, params, true, false)
254 },
255 WhereOperator::Like => self.generate_comparison(&field_path, "LIKE", value, params),
256 WhereOperator::Ilike => self.generate_comparison(&field_path, "ILIKE", value, params),
257
258 WhereOperator::IsNull => {
260 let is_null = if value.as_bool().unwrap_or(true) {
261 "IS NULL"
262 } else {
263 "IS NOT NULL"
264 };
265 Ok(format!("{field_path} {is_null}"))
266 },
267
268 WhereOperator::ArrayContains => {
270 self.generate_jsonb_op(&field_path, "@>", value, params)
271 },
272 WhereOperator::ArrayContainedBy => {
273 self.generate_jsonb_op(&field_path, "<@", value, params)
274 },
275 WhereOperator::ArrayOverlaps => {
276 self.generate_jsonb_op(&field_path, "&&", value, params)
277 },
278 WhereOperator::LenEq => self.generate_array_length(&field_path, "=", value, params),
279 WhereOperator::LenGt => self.generate_array_length(&field_path, ">", value, params),
280 WhereOperator::LenLt => self.generate_array_length(&field_path, "<", value, params),
281 WhereOperator::LenGte => self.generate_array_length(&field_path, ">=", value, params),
282 WhereOperator::LenLte => self.generate_array_length(&field_path, "<=", value, params),
283 WhereOperator::LenNeq => self.generate_array_length(&field_path, "!=", value, params),
284
285 WhereOperator::CosineDistance => {
287 self.generate_vector_distance(&field_path, "<=>", value, params)
288 },
289 WhereOperator::L2Distance => {
290 self.generate_vector_distance(&field_path, "<->", value, params)
291 },
292 WhereOperator::L1Distance => {
293 self.generate_vector_distance(&field_path, "<+>", value, params)
294 },
295 WhereOperator::HammingDistance => {
296 self.generate_vector_distance(&field_path, "<~>", value, params)
297 },
298 WhereOperator::InnerProduct => {
299 self.generate_vector_distance(&field_path, "<#>", value, params)
300 },
301 WhereOperator::JaccardDistance => {
302 self.generate_jaccard_distance(&field_path, value, params)
303 },
304
305 WhereOperator::Matches => self.generate_fts(&field_path, "@@", value, params),
307 WhereOperator::PlainQuery => {
308 self.generate_fts_func(&field_path, "plainto_tsquery", value, params)
309 },
310 WhereOperator::PhraseQuery => {
311 self.generate_fts_func(&field_path, "phraseto_tsquery", value, params)
312 },
313 WhereOperator::WebsearchQuery => {
314 self.generate_fts_func(&field_path, "websearch_to_tsquery", value, params)
315 },
316
317 WhereOperator::IsIPv4 => Ok(format!("family({field_path}::inet) = 4")),
319 WhereOperator::IsIPv6 => Ok(format!("family({field_path}::inet) = 6")),
320 WhereOperator::IsPrivate => Ok(format!(
321 "({field_path}::inet << '10.0.0.0/8'::inet OR {field_path}::inet << '172.16.0.0/12'::inet OR {field_path}::inet << '192.168.0.0/16'::inet OR {field_path}::inet << '169.254.0.0/16'::inet)"
322 )),
323 WhereOperator::IsPublic => Ok(format!(
324 "NOT ({field_path}::inet << '10.0.0.0/8'::inet OR {field_path}::inet << '172.16.0.0/12'::inet OR {field_path}::inet << '192.168.0.0/16'::inet OR {field_path}::inet << '169.254.0.0/16'::inet)"
325 )),
326 WhereOperator::IsLoopback => Ok(format!(
327 "(family({field_path}::inet) = 4 AND {field_path}::inet << '127.0.0.0/8'::inet) OR (family({field_path}::inet) = 6 AND {field_path}::inet << '::1/128'::inet)"
328 )),
329 WhereOperator::InSubnet => self.generate_inet_op(&field_path, "<<", value, params),
330 WhereOperator::ContainsSubnet => {
331 self.generate_inet_op(&field_path, ">>", value, params)
332 },
333 WhereOperator::ContainsIP => self.generate_inet_op(&field_path, ">>", value, params),
334 WhereOperator::Overlaps => self.generate_inet_op(&field_path, "&&", value, params),
335
336 WhereOperator::StrictlyContains => {
338 self.generate_jsonb_op(&field_path, "@>", value, params)
339 },
340
341 WhereOperator::AncestorOf => {
343 self.generate_ltree_op(&field_path, "@>", "ltree", value, params)
344 },
345 WhereOperator::DescendantOf => {
346 self.generate_ltree_op(&field_path, "<@", "ltree", value, params)
347 },
348 WhereOperator::MatchesLquery => {
349 self.generate_ltree_op(&field_path, "~", "lquery", value, params)
350 },
351 WhereOperator::MatchesLtxtquery => {
352 self.generate_ltree_op(&field_path, "@", "ltxtquery", value, params)
353 },
354 WhereOperator::MatchesAnyLquery => {
355 self.generate_ltree_array_op(&field_path, value, params)
356 },
357 WhereOperator::DepthEq => self.generate_ltree_depth(&field_path, "=", value, params),
358 WhereOperator::DepthNeq => self.generate_ltree_depth(&field_path, "!=", value, params),
359 WhereOperator::DepthGt => self.generate_ltree_depth(&field_path, ">", value, params),
360 WhereOperator::DepthGte => self.generate_ltree_depth(&field_path, ">=", value, params),
361 WhereOperator::DepthLt => self.generate_ltree_depth(&field_path, "<", value, params),
362 WhereOperator::DepthLte => self.generate_ltree_depth(&field_path, "<=", value, params),
363 WhereOperator::Lca => self.generate_ltree_lca(&field_path, value, params),
364
365 WhereOperator::Extended(op) => {
367 use crate::filters::ExtendedOperatorHandler;
368 self.generate_extended_sql(op, &field_path, params)
369 },
370 }
371 }
372
373 fn build_jsonb_path(&self, path: &[String]) -> String {
374 if let Some(indexed_col) = self.find_indexed_column(path) {
376 return format!("\"{indexed_col}\"");
378 }
379
380 if path.len() == 1 {
382 let escaped = crate::db::path_escape::escape_postgres_jsonb_segment(&path[0]);
383 format!("data->>'{}'", escaped)
384 } else {
385 let escaped_path = crate::db::path_escape::escape_postgres_jsonb_path(path);
386 let mut result = "data".to_string();
387 for (i, segment) in escaped_path.iter().enumerate() {
388 if i < escaped_path.len() - 1 {
389 result.push_str(&format!("->'{}'", segment));
390 } else {
391 result.push_str(&format!("->>'{}' ", segment));
392 }
393 }
394 result.trim_end().to_string()
395 }
396 }
397
398 fn find_indexed_column(&self, path: &[String]) -> Option<String> {
407 let indexed_columns = self.indexed_columns.as_ref()?;
408
409 let human_readable = path.join("__");
411
412 if indexed_columns.contains(&human_readable) {
414 return Some(human_readable);
415 }
416
417 None
423 }
424
425 fn next_param(&self) -> String {
426 let current = self.param_counter.get();
427 self.param_counter.set(current + 1);
428 format!("${}", current + 1)
429 }
430
431 fn generate_comparison(
432 &self,
433 field_path: &str,
434 op: &str,
435 value: &serde_json::Value,
436 params: &mut Vec<serde_json::Value>,
437 ) -> Result<String> {
438 let param = self.next_param();
439 params.push(value.clone());
440
441 if value.is_number()
444 && (op == ">" || op == ">=" || op == "<" || op == "<=" || op == "=" || op == "!=")
445 {
446 Ok(format!("({field_path})::numeric {op} ({param}::text)::numeric"))
447 } else if value.is_boolean() && (op == "=" || op == "!=") {
448 Ok(format!("({field_path})::boolean {op} {param}"))
450 } else {
451 Ok(format!("{field_path} {op} {param}"))
452 }
453 }
454
455 fn generate_in(
456 &self,
457 field_path: &str,
458 value: &serde_json::Value,
459 params: &mut Vec<serde_json::Value>,
460 ) -> Result<String> {
461 let array = value.as_array().ok_or_else(|| {
462 FraiseQLError::validation("IN operator requires array value".to_string())
463 })?;
464
465 if array.is_empty() {
466 return Ok("FALSE".to_string());
467 }
468
469 let placeholders: Vec<String> = array
470 .iter()
471 .map(|v| {
472 let param = self.next_param();
473 params.push(v.clone());
474 param
475 })
476 .collect();
477
478 Ok(format!("{field_path} IN ({})", placeholders.join(", ")))
479 }
480
481 fn generate_like(
482 &self,
483 field_path: &str,
484 op: &str,
485 value: &serde_json::Value,
486 params: &mut Vec<serde_json::Value>,
487 prefix: bool,
488 suffix: bool,
489 ) -> Result<String> {
490 let param = self.next_param();
491 let val_str = value.as_str().ok_or_else(|| {
492 FraiseQLError::validation("LIKE operator requires string value".to_string())
493 })?;
494
495 let pattern = if prefix && suffix {
496 format!("'%' || {param} || '%'")
497 } else if prefix {
498 format!("'%' || {param}")
499 } else if suffix {
500 format!("{param} || '%'")
501 } else {
502 param.clone()
503 };
504
505 params.push(serde_json::Value::String(val_str.to_string()));
506 Ok(format!("{field_path} {op} {pattern}"))
507 }
508
509 fn generate_jsonb_op(
510 &self,
511 field_path: &str,
512 op: &str,
513 value: &serde_json::Value,
514 params: &mut Vec<serde_json::Value>,
515 ) -> Result<String> {
516 let param = self.next_param();
517 params.push(value.clone());
518 Ok(format!("{field_path}::jsonb {op} {param}::jsonb"))
519 }
520
521 fn generate_array_length(
522 &self,
523 field_path: &str,
524 op: &str,
525 value: &serde_json::Value,
526 params: &mut Vec<serde_json::Value>,
527 ) -> Result<String> {
528 let param = self.next_param();
529 params.push(value.clone());
530 Ok(format!("jsonb_array_length({field_path}::jsonb) {op} {param}"))
531 }
532
533 fn generate_vector_distance(
534 &self,
535 field_path: &str,
536 op: &str,
537 value: &serde_json::Value,
538 params: &mut Vec<serde_json::Value>,
539 ) -> Result<String> {
540 let param = self.next_param();
541 params.push(value.clone());
542 Ok(format!("{field_path}::vector {op} {param}::vector"))
543 }
544
545 fn generate_fts(
546 &self,
547 field_path: &str,
548 op: &str,
549 value: &serde_json::Value,
550 params: &mut Vec<serde_json::Value>,
551 ) -> Result<String> {
552 let param = self.next_param();
553 params.push(value.clone());
554 Ok(format!("to_tsvector({field_path}) {op} to_tsquery({param})"))
555 }
556
557 fn generate_fts_func(
558 &self,
559 field_path: &str,
560 func: &str,
561 value: &serde_json::Value,
562 params: &mut Vec<serde_json::Value>,
563 ) -> Result<String> {
564 let param = self.next_param();
565 params.push(value.clone());
566 Ok(format!("to_tsvector({field_path}) @@ {func}({param})"))
567 }
568
569 fn generate_jaccard_distance(
570 &self,
571 field_path: &str,
572 value: &serde_json::Value,
573 params: &mut Vec<serde_json::Value>,
574 ) -> Result<String> {
575 let param = self.next_param();
576 params.push(value.clone());
577 Ok(format!("({field_path})::text[] <%> ({param})::text[]"))
579 }
580
581 fn generate_inet_op(
582 &self,
583 field_path: &str,
584 op: &str,
585 value: &serde_json::Value,
586 params: &mut Vec<serde_json::Value>,
587 ) -> Result<String> {
588 let param = self.next_param();
589 params.push(value.clone());
590 Ok(format!("{field_path}::inet {op} {param}::inet"))
591 }
592
593 fn generate_ltree_op(
594 &self,
595 field_path: &str,
596 op: &str,
597 value_type: &str,
598 value: &serde_json::Value,
599 params: &mut Vec<serde_json::Value>,
600 ) -> Result<String> {
601 let param = self.next_param();
602 params.push(value.clone());
603 Ok(format!("{field_path}::ltree {op} {param}::{value_type}"))
604 }
605
606 fn generate_ltree_array_op(
607 &self,
608 field_path: &str,
609 value: &serde_json::Value,
610 params: &mut Vec<serde_json::Value>,
611 ) -> Result<String> {
612 let array = value.as_array().ok_or_else(|| {
613 FraiseQLError::validation(
614 "matches_any_lquery operator requires array value".to_string(),
615 )
616 })?;
617
618 if array.is_empty() {
619 return Ok("FALSE".to_string());
620 }
621
622 let placeholders: Vec<String> = array
623 .iter()
624 .map(|v| {
625 let param = self.next_param();
626 params.push(v.clone());
627 format!("{param}::lquery")
628 })
629 .collect();
630
631 Ok(format!("{field_path}::ltree ? ARRAY[{}]", placeholders.join(", ")))
632 }
633
634 fn generate_ltree_depth(
635 &self,
636 field_path: &str,
637 op: &str,
638 value: &serde_json::Value,
639 params: &mut Vec<serde_json::Value>,
640 ) -> Result<String> {
641 let param = self.next_param();
642 params.push(value.clone());
643 Ok(format!("nlevel({field_path}::ltree) {op} {param}"))
644 }
645
646 fn generate_ltree_lca(
647 &self,
648 field_path: &str,
649 value: &serde_json::Value,
650 params: &mut Vec<serde_json::Value>,
651 ) -> Result<String> {
652 let array = value.as_array().ok_or_else(|| {
653 FraiseQLError::validation("lca operator requires array value".to_string())
654 })?;
655
656 if array.is_empty() {
657 return Err(FraiseQLError::validation(
658 "lca operator requires at least one path".to_string(),
659 ));
660 }
661
662 let placeholders: Vec<String> = array
663 .iter()
664 .map(|v| {
665 let param = self.next_param();
666 params.push(v.clone());
667 format!("{param}::ltree")
668 })
669 .collect();
670
671 Ok(format!("{field_path}::ltree = lca(ARRAY[{}])", placeholders.join(", ")))
672 }
673}
674
675impl Default for PostgresWhereGenerator {
676 fn default() -> Self {
677 Self::new()
678 }
679}
680
681impl crate::filters::ExtendedOperatorHandler for PostgresWhereGenerator {
682 fn generate_extended_sql(
683 &self,
684 operator: &crate::filters::ExtendedOperator,
685 field_sql: &str,
686 params: &mut Vec<serde_json::Value>,
687 ) -> Result<String> {
688 match operator {
689 crate::filters::ExtendedOperator::EmailDomainEq(domain) => {
691 params.push(serde_json::Value::String(domain.clone()));
692 let param_idx = params.len();
693 Ok(format!("SPLIT_PART({}, '@', 2) = ${}", field_sql, param_idx))
695 },
696
697 crate::filters::ExtendedOperator::EmailDomainIn(domains) => {
698 let placeholders: Vec<String> = domains
699 .iter()
700 .map(|d| {
701 params.push(serde_json::Value::String(d.clone()));
702 format!("${}", params.len())
703 })
704 .collect();
705 Ok(format!("SPLIT_PART({}, '@', 2) IN ({})", field_sql, placeholders.join(", ")))
706 },
707
708 crate::filters::ExtendedOperator::EmailDomainEndswith(suffix) => {
709 params.push(serde_json::Value::String(suffix.clone()));
710 let param_idx = params.len();
711 Ok(format!("SPLIT_PART({}, '@', 2) LIKE '%' || ${}", field_sql, param_idx))
713 },
714
715 crate::filters::ExtendedOperator::EmailLocalPartStartswith(prefix) => {
716 params.push(serde_json::Value::String(prefix.clone()));
717 let param_idx = params.len();
718 Ok(format!("SPLIT_PART({}, '@', 1) LIKE ${} || '%'", field_sql, param_idx))
720 },
721
722 crate::filters::ExtendedOperator::VinWmiEq(wmi) => {
724 params.push(serde_json::Value::String(wmi.clone()));
725 let param_idx = params.len();
726 Ok(format!("SUBSTRING({} FROM 1 FOR 3) = ${}", field_sql, param_idx))
728 },
729
730 crate::filters::ExtendedOperator::IbanCountryEq(country) => {
732 params.push(serde_json::Value::String(country.clone()));
733 let param_idx = params.len();
734 Ok(format!("SUBSTRING({} FROM 1 FOR 2) = ${}", field_sql, param_idx))
736 },
737
738 _ => Err(FraiseQLError::validation(format!(
740 "Extended operator not yet implemented: {}",
741 operator
742 ))),
743 }
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use std::{collections::HashSet, sync::Arc};
750
751 use serde_json::json;
752
753 use super::*;
754
755 #[test]
756 fn test_simple_equality() {
757 let gen = PostgresWhereGenerator::new();
758 let clause = WhereClause::Field {
759 path: vec!["email".to_string()],
760 operator: WhereOperator::Eq,
761 value: json!("test@example.com"),
762 };
763
764 let (sql, params) = gen.generate(&clause).unwrap();
765 assert_eq!(sql, "data->>'email' = $1");
766 assert_eq!(params, vec![json!("test@example.com")]);
767 }
768
769 #[test]
770 fn test_icontains() {
771 let gen = PostgresWhereGenerator::new();
772 let clause = WhereClause::Field {
773 path: vec!["email".to_string()],
774 operator: WhereOperator::Icontains,
775 value: json!("example.com"),
776 };
777
778 let (sql, params) = gen.generate(&clause).unwrap();
779 assert_eq!(sql, "data->>'email' ILIKE '%' || $1 || '%'");
780 assert_eq!(params, vec![json!("example.com")]);
781 }
782
783 #[test]
784 fn test_nested_path() {
785 let gen = PostgresWhereGenerator::new();
786 let clause = WhereClause::Field {
787 path: vec!["address".to_string(), "city".to_string()],
788 operator: WhereOperator::Eq,
789 value: json!("Paris"),
790 };
791
792 let (sql, params) = gen.generate(&clause).unwrap();
793 assert_eq!(sql, "data->'address'->>'city' = $1");
794 assert_eq!(params, vec![json!("Paris")]);
795 }
796
797 #[test]
798 fn test_and_clause() {
799 let gen = PostgresWhereGenerator::new();
800 let clause = WhereClause::And(vec![
801 WhereClause::Field {
802 path: vec!["age".to_string()],
803 operator: WhereOperator::Gte,
804 value: json!(18),
805 },
806 WhereClause::Field {
807 path: vec!["active".to_string()],
808 operator: WhereOperator::Eq,
809 value: json!(true),
810 },
811 ]);
812
813 let (sql, params) = gen.generate(&clause).unwrap();
814 assert_eq!(
816 sql,
817 "((data->>'age')::numeric >= ($1::text)::numeric AND (data->>'active')::boolean = $2)"
818 );
819 assert_eq!(params, vec![json!(18), json!(true)]);
820 }
821
822 #[test]
823 fn test_or_clause() {
824 let gen = PostgresWhereGenerator::new();
825 let clause = WhereClause::Or(vec![
826 WhereClause::Field {
827 path: vec!["role".to_string()],
828 operator: WhereOperator::Eq,
829 value: json!("admin"),
830 },
831 WhereClause::Field {
832 path: vec!["role".to_string()],
833 operator: WhereOperator::Eq,
834 value: json!("moderator"),
835 },
836 ]);
837
838 let (sql, params) = gen.generate(&clause).unwrap();
839 assert_eq!(sql, "(data->>'role' = $1 OR data->>'role' = $2)");
840 assert_eq!(params, vec![json!("admin"), json!("moderator")]);
841 }
842
843 #[test]
844 fn test_not_clause() {
845 let gen = PostgresWhereGenerator::new();
846 let clause = WhereClause::Not(Box::new(WhereClause::Field {
847 path: vec!["deleted".to_string()],
848 operator: WhereOperator::Eq,
849 value: json!(true),
850 }));
851
852 let (sql, params) = gen.generate(&clause).unwrap();
853 assert_eq!(sql, "NOT ((data->>'deleted')::boolean = $1)");
855 assert_eq!(params, vec![json!(true)]);
856 }
857
858 #[test]
859 fn test_in_operator() {
860 let gen = PostgresWhereGenerator::new();
861 let clause = WhereClause::Field {
862 path: vec!["status".to_string()],
863 operator: WhereOperator::In,
864 value: json!(["active", "pending"]),
865 };
866
867 let (sql, params) = gen.generate(&clause).unwrap();
868 assert_eq!(sql, "data->>'status' IN ($1, $2)");
869 assert_eq!(params, vec![json!("active"), json!("pending")]);
870 }
871
872 #[test]
873 fn test_is_null() {
874 let gen = PostgresWhereGenerator::new();
875 let clause = WhereClause::Field {
876 path: vec!["deleted_at".to_string()],
877 operator: WhereOperator::IsNull,
878 value: json!(true),
879 };
880
881 let (sql, _params) = gen.generate(&clause).unwrap();
882 assert_eq!(sql, "data->>'deleted_at' IS NULL");
883 }
884
885 #[test]
886 fn test_array_contains() {
887 let gen = PostgresWhereGenerator::new();
888 let clause = WhereClause::Field {
889 path: vec!["tags".to_string()],
890 operator: WhereOperator::ArrayContains,
891 value: json!(["rust"]),
892 };
893
894 let (sql, params) = gen.generate(&clause).unwrap();
895 assert_eq!(sql, "data->>'tags'::jsonb @> $1::jsonb");
896 assert_eq!(params, vec![json!(["rust"])]);
897 }
898
899 #[test]
902 fn test_ltree_ancestor_of() {
903 let gen = PostgresWhereGenerator::new();
904 let clause = WhereClause::Field {
905 path: vec!["path".to_string()],
906 operator: WhereOperator::AncestorOf,
907 value: json!("Top.Sciences.Astronomy"),
908 };
909
910 let (sql, params) = gen.generate(&clause).unwrap();
911 assert_eq!(sql, "data->>'path'::ltree @> $1::ltree");
912 assert_eq!(params, vec![json!("Top.Sciences.Astronomy")]);
913 }
914
915 #[test]
916 fn test_ltree_descendant_of() {
917 let gen = PostgresWhereGenerator::new();
918 let clause = WhereClause::Field {
919 path: vec!["path".to_string()],
920 operator: WhereOperator::DescendantOf,
921 value: json!("Top.Sciences"),
922 };
923
924 let (sql, params) = gen.generate(&clause).unwrap();
925 assert_eq!(sql, "data->>'path'::ltree <@ $1::ltree");
926 assert_eq!(params, vec![json!("Top.Sciences")]);
927 }
928
929 #[test]
930 fn test_ltree_matches_lquery() {
931 let gen = PostgresWhereGenerator::new();
932 let clause = WhereClause::Field {
933 path: vec!["path".to_string()],
934 operator: WhereOperator::MatchesLquery,
935 value: json!("Top.*.Ast*"),
936 };
937
938 let (sql, params) = gen.generate(&clause).unwrap();
939 assert_eq!(sql, "data->>'path'::ltree ~ $1::lquery");
940 assert_eq!(params, vec![json!("Top.*.Ast*")]);
941 }
942
943 #[test]
944 fn test_ltree_matches_ltxtquery() {
945 let gen = PostgresWhereGenerator::new();
946 let clause = WhereClause::Field {
947 path: vec!["path".to_string()],
948 operator: WhereOperator::MatchesLtxtquery,
949 value: json!("Science & !Deprecated"),
950 };
951
952 let (sql, params) = gen.generate(&clause).unwrap();
953 assert_eq!(sql, "data->>'path'::ltree @ $1::ltxtquery");
954 assert_eq!(params, vec![json!("Science & !Deprecated")]);
955 }
956
957 #[test]
958 fn test_ltree_matches_any_lquery() {
959 let gen = PostgresWhereGenerator::new();
960 let clause = WhereClause::Field {
961 path: vec!["path".to_string()],
962 operator: WhereOperator::MatchesAnyLquery,
963 value: json!(["Top.*", "Other.*"]),
964 };
965
966 let (sql, params) = gen.generate(&clause).unwrap();
967 assert_eq!(sql, "data->>'path'::ltree ? ARRAY[$1::lquery, $2::lquery]");
968 assert_eq!(params, vec![json!("Top.*"), json!("Other.*")]);
969 }
970
971 #[test]
972 fn test_ltree_depth_eq() {
973 let gen = PostgresWhereGenerator::new();
974 let clause = WhereClause::Field {
975 path: vec!["path".to_string()],
976 operator: WhereOperator::DepthEq,
977 value: json!(3),
978 };
979
980 let (sql, params) = gen.generate(&clause).unwrap();
981 assert_eq!(sql, "nlevel(data->>'path'::ltree) = $1");
982 assert_eq!(params, vec![json!(3)]);
983 }
984
985 #[test]
986 fn test_ltree_depth_gt() {
987 let gen = PostgresWhereGenerator::new();
988 let clause = WhereClause::Field {
989 path: vec!["path".to_string()],
990 operator: WhereOperator::DepthGt,
991 value: json!(2),
992 };
993
994 let (sql, params) = gen.generate(&clause).unwrap();
995 assert_eq!(sql, "nlevel(data->>'path'::ltree) > $1");
996 assert_eq!(params, vec![json!(2)]);
997 }
998
999 #[test]
1000 fn test_ltree_lca() {
1001 let gen = PostgresWhereGenerator::new();
1002 let clause = WhereClause::Field {
1003 path: vec!["path".to_string()],
1004 operator: WhereOperator::Lca,
1005 value: json!(["Org.Engineering.Backend", "Org.Engineering.Frontend"]),
1006 };
1007
1008 let (sql, params) = gen.generate(&clause).unwrap();
1009 assert_eq!(sql, "data->>'path'::ltree = lca(ARRAY[$1::ltree, $2::ltree])");
1010 assert_eq!(
1011 params,
1012 vec![
1013 json!("Org.Engineering.Backend"),
1014 json!("Org.Engineering.Frontend")
1015 ]
1016 );
1017 }
1018
1019 #[test]
1022 fn test_indexed_column_simple_path() {
1023 let mut indexed = HashSet::new();
1025 indexed.insert("category__code".to_string());
1026 let gen = PostgresWhereGenerator::with_indexed_columns(Arc::new(indexed));
1027
1028 let clause = WhereClause::Field {
1029 path: vec!["category".to_string(), "code".to_string()],
1030 operator: WhereOperator::Eq,
1031 value: json!("ELEC"),
1032 };
1033
1034 let (sql, params) = gen.generate(&clause).unwrap();
1035 assert_eq!(sql, "\"category__code\" = $1");
1037 assert_eq!(params, vec![json!("ELEC")]);
1038 }
1039
1040 #[test]
1041 fn test_indexed_column_nested_path() {
1042 let mut indexed = HashSet::new();
1044 indexed.insert("items__product__category__code".to_string());
1045 let gen = PostgresWhereGenerator::with_indexed_columns(Arc::new(indexed));
1046
1047 let clause = WhereClause::Field {
1048 path: vec![
1049 "items".to_string(),
1050 "product".to_string(),
1051 "category".to_string(),
1052 "code".to_string(),
1053 ],
1054 operator: WhereOperator::Eq,
1055 value: json!("ELEC"),
1056 };
1057
1058 let (sql, params) = gen.generate(&clause).unwrap();
1059 assert_eq!(sql, "\"items__product__category__code\" = $1");
1061 assert_eq!(params, vec![json!("ELEC")]);
1062 }
1063
1064 #[test]
1065 fn test_indexed_column_fallback_to_jsonb() {
1066 let mut indexed = HashSet::new();
1068 indexed.insert("items__product__category__code".to_string());
1069 let gen = PostgresWhereGenerator::with_indexed_columns(Arc::new(indexed));
1070
1071 let clause = WhereClause::Field {
1072 path: vec![
1073 "items".to_string(),
1074 "product".to_string(),
1075 "name".to_string(),
1076 ],
1077 operator: WhereOperator::Eq,
1078 value: json!("Widget"),
1079 };
1080
1081 let (sql, params) = gen.generate(&clause).unwrap();
1082 assert_eq!(sql, "data->'items'->'product'->>'name' = $1");
1084 assert_eq!(params, vec![json!("Widget")]);
1085 }
1086
1087 #[test]
1088 fn test_indexed_column_with_like_operator() {
1089 let mut indexed = HashSet::new();
1091 indexed.insert("category__name".to_string());
1092 let gen = PostgresWhereGenerator::with_indexed_columns(Arc::new(indexed));
1093
1094 let clause = WhereClause::Field {
1095 path: vec!["category".to_string(), "name".to_string()],
1096 operator: WhereOperator::Icontains,
1097 value: json!("electronics"),
1098 };
1099
1100 let (sql, params) = gen.generate(&clause).unwrap();
1101 assert_eq!(sql, "\"category__name\" ILIKE '%' || $1 || '%'");
1103 assert_eq!(params, vec![json!("electronics")]);
1104 }
1105
1106 #[test]
1107 fn test_indexed_column_with_numeric_comparison() {
1108 let mut indexed = HashSet::new();
1110 indexed.insert("order__total".to_string());
1111 let gen = PostgresWhereGenerator::with_indexed_columns(Arc::new(indexed));
1112
1113 let clause = WhereClause::Field {
1114 path: vec!["order".to_string(), "total".to_string()],
1115 operator: WhereOperator::Gt,
1116 value: json!(100),
1117 };
1118
1119 let (sql, params) = gen.generate(&clause).unwrap();
1120 assert_eq!(sql, "(\"order__total\")::numeric > ($1::text)::numeric");
1122 assert_eq!(params, vec![json!(100)]);
1123 }
1124
1125 #[test]
1126 fn test_indexed_column_empty_cache() {
1127 let indexed = HashSet::new();
1129 let gen = PostgresWhereGenerator::with_indexed_columns(Arc::new(indexed));
1130
1131 let clause = WhereClause::Field {
1132 path: vec!["category".to_string(), "code".to_string()],
1133 operator: WhereOperator::Eq,
1134 value: json!("ELEC"),
1135 };
1136
1137 let (sql, params) = gen.generate(&clause).unwrap();
1138 assert_eq!(sql, "data->'category'->>'code' = $1");
1140 assert_eq!(params, vec![json!("ELEC")]);
1141 }
1142
1143 #[test]
1144 fn test_no_indexed_columns_cache() {
1145 let gen = PostgresWhereGenerator::new();
1147
1148 let clause = WhereClause::Field {
1149 path: vec!["category".to_string(), "code".to_string()],
1150 operator: WhereOperator::Eq,
1151 value: json!("ELEC"),
1152 };
1153
1154 let (sql, params) = gen.generate(&clause).unwrap();
1155 assert_eq!(sql, "data->'category'->>'code' = $1");
1157 assert_eq!(params, vec![json!("ELEC")]);
1158 }
1159}