1use ankql::ast::{ComparisonOperator, Expr, Literal, OrderByItem, OrderDirection, Predicate, Selection};
2use ankurah_core::{error::RetrievalError, EntityId};
3use thiserror::Error;
4use tokio_postgres::types::ToSql;
5
6#[derive(Debug, Error, Clone)]
7pub enum SqlGenerationError {
8 #[error("Placeholder found in predicate - placeholders should be replaced before predicate processing")]
9 PlaceholderFound,
10 #[error("Unsupported expression type: {0}")]
11 UnsupportedExpression(&'static str),
12 #[error("Unsupported operator: {0}")]
13 UnsupportedOperator(&'static str),
14 #[error("SqlBuilder requires both fields and table_name to be set for complete SELECT generation, or neither for WHERE-only mode")]
15 IncompleteConfiguration,
16}
17
18#[derive(Debug, Clone)]
25pub struct SplitPredicate {
26 pub sql_predicate: Predicate,
28 pub remaining_predicate: Predicate,
30}
31
32impl SplitPredicate {
33 pub fn needs_post_filter(&self) -> bool { !matches!(self.remaining_predicate, Predicate::True) }
35}
36
37pub fn split_predicate_for_postgres(predicate: &Predicate) -> SplitPredicate {
48 let (sql_pred, remaining_pred) = split_predicate_recursive(predicate);
53
54 SplitPredicate { sql_predicate: sql_pred, remaining_predicate: remaining_pred }
55}
56
57fn split_predicate_recursive(predicate: &Predicate) -> (Predicate, Predicate) {
59 match predicate {
60 Predicate::Comparison { left, operator: _, right } => {
62 if can_pushdown_comparison(left, right) {
63 (predicate.clone(), Predicate::True)
64 } else {
65 (Predicate::True, predicate.clone())
67 }
68 }
69
70 Predicate::And(left, right) => {
72 let (left_sql, left_remaining) = split_predicate_recursive(left);
73 let (right_sql, right_remaining) = split_predicate_recursive(right);
74
75 let sql_pred = match (&left_sql, &right_sql) {
76 (Predicate::True, Predicate::True) => Predicate::True,
77 (Predicate::True, _) => right_sql,
78 (_, Predicate::True) => left_sql,
79 _ => Predicate::And(Box::new(left_sql), Box::new(right_sql)),
80 };
81
82 let remaining_pred = match (&left_remaining, &right_remaining) {
83 (Predicate::True, Predicate::True) => Predicate::True,
84 (Predicate::True, _) => right_remaining,
85 (_, Predicate::True) => left_remaining,
86 _ => Predicate::And(Box::new(left_remaining), Box::new(right_remaining)),
87 };
88
89 (sql_pred, remaining_pred)
90 }
91
92 Predicate::Or(left, right) => {
95 let (left_sql, left_remaining) = split_predicate_recursive(left);
96 let (right_sql, right_remaining) = split_predicate_recursive(right);
97
98 if matches!(left_remaining, Predicate::True) && matches!(right_remaining, Predicate::True) {
100 (predicate.clone(), Predicate::True)
101 } else {
102 let sql_pred = match (&left_sql, &right_sql) {
105 (Predicate::True, Predicate::True) => Predicate::True,
106 (Predicate::True, _) => right_sql,
107 (_, Predicate::True) => left_sql,
108 _ => Predicate::Or(Box::new(left_sql), Box::new(right_sql)),
109 };
110 (sql_pred, predicate.clone())
111 }
112 }
113
114 Predicate::Not(inner) => {
116 let (inner_sql, inner_remaining) = split_predicate_recursive(inner);
117 if matches!(inner_remaining, Predicate::True) {
118 (Predicate::Not(Box::new(inner_sql)), Predicate::True)
119 } else {
120 (Predicate::True, predicate.clone())
122 }
123 }
124
125 Predicate::IsNull(expr) => {
127 if can_pushdown_expr(expr) {
128 (predicate.clone(), Predicate::True)
129 } else {
130 (Predicate::True, predicate.clone())
131 }
132 }
133
134 Predicate::True => (Predicate::True, Predicate::True),
135 Predicate::False => (Predicate::False, Predicate::True),
136 Predicate::Placeholder => (Predicate::True, predicate.clone()), }
138}
139
140fn can_pushdown_comparison(left: &Expr, right: &Expr) -> bool { can_pushdown_expr(left) && can_pushdown_expr(right) }
142
143fn can_pushdown_expr(expr: &Expr) -> bool {
165 match expr {
166 Expr::Literal(_) => true,
167 Expr::Path(path) => {
168 !path.steps.is_empty()
176 }
177 Expr::ExprList(exprs) => exprs.iter().all(can_pushdown_expr),
178 Expr::Predicate(_) => false, Expr::InfixExpr { .. } => false, Expr::Placeholder => false, }
182}
183
184impl From<SqlGenerationError> for RetrievalError {
185 fn from(err: SqlGenerationError) -> Self { RetrievalError::StorageError(Box::new(err)) }
186}
187
188pub enum SqlExpr {
189 Sql(String),
190 Argument(Box<dyn ToSql + Send + Sync>),
191}
192
193pub struct SqlBuilder {
194 expressions: Vec<SqlExpr>,
195 fields: Vec<String>,
196 table_name: Option<String>,
197}
198
199impl Default for SqlBuilder {
200 fn default() -> Self { Self::new() }
201}
202
203impl SqlBuilder {
204 pub fn new() -> Self { Self { expressions: Vec::new(), fields: Vec::new(), table_name: None } }
205
206 pub fn with_fields<T: Into<String>>(fields: Vec<T>) -> Self {
207 Self { expressions: Vec::new(), fields: fields.into_iter().map(|f| f.into()).collect(), table_name: None }
208 }
209
210 pub fn table_name(&mut self, name: impl Into<String>) -> &mut Self {
211 self.table_name = Some(name.into());
212 self
213 }
214
215 pub fn push(&mut self, expr: SqlExpr) { self.expressions.push(expr); }
216
217 pub fn arg(&mut self, arg: impl ToSql + Send + Sync + 'static) {
218 self.push(SqlExpr::Argument(Box::new(arg) as Box<dyn ToSql + Send + Sync>));
219 }
220
221 pub fn sql(&mut self, s: impl AsRef<str>) { self.push(SqlExpr::Sql(s.as_ref().to_owned())); }
222
223 pub fn build(self) -> Result<(String, Vec<Box<dyn ToSql + Send + Sync>>), SqlGenerationError> {
224 let mut counter = 1;
225 let mut where_clause = String::new();
226 let mut args = Vec::new();
227
228 for expr in self.expressions {
230 match expr {
231 SqlExpr::Argument(arg) => {
232 where_clause += &format!("${}", counter);
233 args.push(arg);
234 counter += 1;
235 }
236 SqlExpr::Sql(s) => {
237 where_clause += &s;
238 }
239 }
240 }
241
242 if self.fields.is_empty() || self.table_name.is_none() {
244 return Err(SqlGenerationError::IncompleteConfiguration);
245 }
246
247 let fields_clause = self.fields.iter().map(|field| format!(r#""{}""#, field.replace('"', "\"\""))).collect::<Vec<_>>().join(", ");
248 let table = self.table_name.unwrap();
249 let sql = format!(r#"SELECT {} FROM "{}" WHERE {}"#, fields_clause, table.replace('"', "\"\""), where_clause);
250
251 Ok((sql, args))
252 }
253
254 pub fn build_where_clause(self) -> (String, Vec<Box<dyn ToSql + Send + Sync>>) {
255 let mut counter = 1;
256 let mut where_clause = String::new();
257 let mut args = Vec::new();
258
259 for expr in self.expressions {
261 match expr {
262 SqlExpr::Argument(arg) => {
263 where_clause += &format!("${}", counter);
264 args.push(arg);
265 counter += 1;
266 }
267 SqlExpr::Sql(s) => {
268 where_clause += &s;
269 }
270 }
271 }
272
273 (where_clause, args)
274 }
275
276 pub fn expr(&mut self, expr: &Expr) -> Result<(), SqlGenerationError> {
278 match expr {
279 Expr::Placeholder => return Err(SqlGenerationError::PlaceholderFound),
280 Expr::Literal(lit) => match lit {
281 Literal::String(s) => self.arg(s.to_owned()),
282 Literal::I64(int) => self.arg(*int),
283 Literal::F64(float) => self.arg(*float),
284 Literal::Bool(bool) => self.arg(*bool),
285 Literal::I16(i) => self.arg(*i),
286 Literal::I32(i) => self.arg(*i),
287 Literal::EntityId(ulid) => self.arg(EntityId::from_ulid(*ulid).to_base64()),
288 Literal::Object(bytes) => self.arg(bytes.clone()),
289 Literal::Binary(bytes) => self.arg(bytes.clone()),
290 Literal::Json(json) => self.arg(json.clone()),
291 },
292 Expr::Path(path) => {
293 if path.is_simple() {
294 let escaped = path.first().replace('"', "\"\"");
296 self.sql(format!(r#""{}""#, escaped));
297 } else {
298 let first = path.first().replace('"', "\"\"");
302 self.sql(format!(r#""{}""#, first));
303
304 for step in path.steps.iter().skip(1) {
305 let escaped = step.replace('\'', "''");
306 self.sql(format!("->'{}'", escaped));
308 }
309 }
310 }
311 Expr::ExprList(exprs) => {
312 self.sql("(");
313 for (i, expr) in exprs.iter().enumerate() {
314 if i > 0 {
315 self.sql(", ");
316 }
317 match expr {
318 Expr::Placeholder => return Err(SqlGenerationError::PlaceholderFound),
319 Expr::Literal(lit) => match lit {
320 Literal::String(s) => self.arg(s.to_owned()),
321 Literal::I64(int) => self.arg(*int),
322 Literal::F64(float) => self.arg(*float),
323 Literal::Bool(bool) => self.arg(*bool),
324 Literal::I16(i) => self.arg(*i),
325 Literal::I32(i) => self.arg(*i),
326 Literal::EntityId(ulid) => self.arg(EntityId::from_ulid(*ulid).to_base64()),
327 Literal::Object(bytes) => self.arg(bytes.clone()),
328 Literal::Binary(bytes) => self.arg(bytes.clone()),
329 Literal::Json(json) => self.arg(json.clone()),
330 },
331 _ => {
332 return Err(SqlGenerationError::UnsupportedExpression(
333 "Only literal expressions and placeholders are supported in IN lists",
334 ))
335 }
336 }
337 }
338 self.sql(")");
339 }
340 _ => return Err(SqlGenerationError::UnsupportedExpression("Only literal, identifier, and list expressions are supported")),
341 }
342 Ok(())
343 }
344
345 pub fn expr_as_jsonb(&mut self, expr: &Expr) -> Result<(), SqlGenerationError> {
349 match expr {
350 Expr::Literal(lit) => {
351 match lit {
354 Literal::String(s) => {
355 let json_escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
358 let sql_escaped = format!("\"{}\"", json_escaped).replace('\'', "''");
359 self.sql(format!("'{}'::jsonb", sql_escaped));
360 }
361 Literal::I64(n) => self.sql(format!("'{}'::jsonb", n)),
362 Literal::F64(n) => self.sql(format!("'{}'::jsonb", n)),
363 Literal::Bool(b) => self.sql(format!("'{}'::jsonb", b)),
364 Literal::I16(n) => self.sql(format!("'{}'::jsonb", n)),
365 Literal::I32(n) => self.sql(format!("'{}'::jsonb", n)),
366 Literal::EntityId(_) | Literal::Object(_) | Literal::Binary(_) => {
368 self.expr(expr)?;
370 }
371 Literal::Json(json) => self.sql(format!("'{}'::jsonb", json)),
373 }
374 Ok(())
375 }
376 _ => self.expr(expr),
378 }
379 }
380
381 pub fn comparison_op(&mut self, op: &ComparisonOperator) -> Result<(), SqlGenerationError> {
382 self.sql(comparison_op_to_sql(op)?);
383 Ok(())
384 }
385
386 pub fn predicate(&mut self, predicate: &Predicate) -> Result<(), SqlGenerationError> {
387 match predicate {
388 Predicate::Comparison { left, operator, right } => {
389 let left_is_jsonb = matches!(left.as_ref(), Expr::Path(p) if !p.is_simple());
391 let right_is_jsonb = matches!(right.as_ref(), Expr::Path(p) if !p.is_simple());
392
393 self.expr(left)?;
394 self.sql(" ");
395 self.comparison_op(operator)?;
396 self.sql(" ");
397
398 if left_is_jsonb && matches!(right.as_ref(), Expr::Literal(_)) {
399 self.expr_as_jsonb(right)?;
401 } else if right_is_jsonb && matches!(left.as_ref(), Expr::Literal(_)) {
402 self.expr_as_jsonb(right)?;
404 } else {
405 self.expr(right)?;
406 }
407 }
408 Predicate::And(left, right) => {
409 self.predicate(left)?;
410 self.sql(" AND ");
411 self.predicate(right)?;
412 }
413 Predicate::Or(left, right) => {
414 self.sql("(");
415 self.predicate(left)?;
416 self.sql(" OR ");
417 self.predicate(right)?;
418 self.sql(")");
419 }
420 Predicate::Not(pred) => {
421 self.sql("NOT (");
422 self.predicate(pred)?;
423 self.sql(")");
424 }
425 Predicate::IsNull(expr) => {
426 self.expr(expr)?;
427 self.sql(" IS NULL");
428 }
429 Predicate::True => {
430 self.sql("TRUE");
431 }
432 Predicate::False => {
433 self.sql("FALSE");
434 }
435 Predicate::Placeholder => {
436 return Err(SqlGenerationError::PlaceholderFound);
437 }
438 }
439 Ok(())
440 }
441
442 pub fn selection(&mut self, selection: &Selection) -> Result<(), SqlGenerationError> {
443 self.predicate(&selection.predicate)?;
445
446 if let Some(order_by_items) = &selection.order_by {
448 self.sql(" ORDER BY ");
449 for (i, order_by) in order_by_items.iter().enumerate() {
450 if i > 0 {
451 self.sql(", ");
452 }
453 self.order_by_item(order_by)?;
454 }
455 }
456
457 if let Some(limit) = selection.limit {
459 self.sql(" LIMIT ");
460 self.arg(limit as i64); }
462
463 Ok(())
464 }
465
466 pub fn order_by_item(&mut self, order_by: &OrderByItem) -> Result<(), SqlGenerationError> {
467 for (i, step) in order_by.path.steps.iter().enumerate() {
469 if i > 0 {
470 self.sql(".");
471 }
472 let escaped_step = step.replace('"', "\"\"");
474 self.sql(format!(r#""{}""#, escaped_step));
475 }
476
477 match order_by.direction {
479 OrderDirection::Asc => self.sql(" ASC"),
480 OrderDirection::Desc => self.sql(" DESC"),
481 }
482
483 Ok(())
484 }
485}
486
487fn comparison_op_to_sql(op: &ComparisonOperator) -> Result<&'static str, SqlGenerationError> {
488 Ok(match op {
489 ComparisonOperator::Equal => "=",
490 ComparisonOperator::NotEqual => "<>",
491 ComparisonOperator::GreaterThan => ">",
492 ComparisonOperator::GreaterThanOrEqual => ">=",
493 ComparisonOperator::LessThan => "<",
494 ComparisonOperator::LessThanOrEqual => "<=",
495 ComparisonOperator::In => "IN",
496 ComparisonOperator::Between => return Err(SqlGenerationError::UnsupportedOperator("BETWEEN operator is not yet supported")),
497 })
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use ankql::parser::parse_selection;
504 use anyhow::Result;
505
506 fn assert_args<'a, 'b>(args: &Vec<Box<dyn ToSql + Send + Sync>>, expected: &Vec<Box<dyn ToSql + Send + Sync>>) {
507 assert_eq!(format!("{:?}", args), format!("{:?}", expected));
509 }
510
511 #[test]
512 fn test_simple_equality() -> Result<()> {
513 let selection = parse_selection("name = 'Alice'").unwrap();
514 let mut sql = SqlBuilder::new();
515 sql.selection(&selection)?;
516
517 let (sql_string, args) = sql.build_where_clause();
518 assert_eq!(sql_string, r#""name" = $1"#);
519 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new("Alice")];
520 assert_args(&args, &expected);
521 Ok(())
522 }
523
524 #[test]
525 fn test_and_condition() -> Result<()> {
526 let selection = parse_selection("name = 'Alice' AND age = 30").unwrap();
527 let mut sql = SqlBuilder::with_fields(vec!["id", "name", "age"]);
528 sql.table_name("users");
529 sql.selection(&selection)?;
530 let (sql_string, args) = sql.build()?;
531
532 assert_eq!(sql_string, r#"SELECT "id", "name", "age" FROM "users" WHERE "name" = $1 AND "age" = $2"#);
533 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new("Alice"), Box::new(30)];
534 assert_args(&args, &expected);
535 Ok(())
536 }
537
538 #[test]
539 fn test_complex_condition() -> Result<()> {
540 let selection = parse_selection("(name = 'Alice' OR name = 'Charlie') AND age >= 30 AND age <= 40").unwrap();
541
542 let mut sql = SqlBuilder::with_fields(vec!["id", "name", "age"]);
543 sql.table_name("users");
544 sql.selection(&selection)?;
545 let (sql_string, args) = sql.build()?;
546
547 assert_eq!(
548 sql_string,
549 r#"SELECT "id", "name", "age" FROM "users" WHERE ("name" = $1 OR "name" = $2) AND "age" >= $3 AND "age" <= $4"#
550 );
551 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new("Alice"), Box::new("Charlie"), Box::new(30), Box::new(40)];
552 assert_args(&args, &expected);
553 Ok(())
554 }
555
556 #[test]
557 fn test_including_collection_identifier() -> Result<()> {
558 let selection = parse_selection("person.name = 'Alice'").unwrap();
563
564 let mut sql = SqlBuilder::with_fields(vec!["id", "name"]);
565 sql.table_name("people");
566 sql.selection(&selection)?;
567 let (sql_string, args) = sql.build()?;
568
569 assert_eq!(sql_string, r#"SELECT "id", "name" FROM "people" WHERE "person"->'name' = '"Alice"'::jsonb"#);
571 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![];
573 assert_args(&args, &expected);
574 Ok(())
575 }
576
577 #[test]
578 fn test_false_predicate() -> Result<()> {
579 let mut sql = SqlBuilder::with_fields(vec!["id"]);
580 sql.table_name("test");
581 sql.predicate(&Predicate::False)?;
582 let (sql_string, args) = sql.build()?;
583
584 assert_eq!(sql_string, r#"SELECT "id" FROM "test" WHERE FALSE"#);
585 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![];
586 assert_args(&args, &expected);
587 Ok(())
588 }
589
590 #[test]
591 fn test_in_operator() -> Result<()> {
592 let selection = parse_selection("name IN ('Alice', 'Bob', 'Charlie')").unwrap();
593 let mut sql = SqlBuilder::with_fields(vec!["id", "name"]);
594 sql.table_name("users");
595 sql.selection(&selection)?;
596 let (sql_string, args) = sql.build()?;
597
598 assert_eq!(sql_string, r#"SELECT "id", "name" FROM "users" WHERE "name" IN ($1, $2, $3)"#);
599 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new("Alice"), Box::new("Bob"), Box::new("Charlie")];
600 assert_args(&args, &expected);
601 Ok(())
602 }
603
604 #[test]
605 fn test_placeholder_error() {
606 let mut sql = SqlBuilder::with_fields(vec!["id"]);
607 sql.table_name("test");
608 let err = sql.predicate(&Predicate::Placeholder).expect_err("Expected an error");
609 assert!(matches!(err, SqlGenerationError::PlaceholderFound));
610 }
611
612 #[test]
613 fn test_selection_with_order_by() -> Result<()> {
614 use ankql::ast::{OrderByItem, OrderDirection, PathExpr, Selection};
615
616 let base_selection = ankql::parser::parse_selection("name = 'Alice'").unwrap();
617 let selection = Selection {
618 predicate: base_selection.predicate,
619 order_by: Some(vec![OrderByItem { path: PathExpr::simple("created_at"), direction: OrderDirection::Desc }]),
620 limit: None,
621 };
622
623 let mut sql = SqlBuilder::with_fields(vec!["id", "name", "created_at"]);
624 sql.table_name("users");
625 sql.selection(&selection)?;
626 let (sql_string, args) = sql.build()?;
627
628 assert_eq!(sql_string, r#"SELECT "id", "name", "created_at" FROM "users" WHERE "name" = $1 ORDER BY "created_at" DESC"#);
629 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new("Alice")];
630 assert_args(&args, &expected);
631 Ok(())
632 }
633
634 #[test]
635 fn test_selection_with_limit() -> Result<()> {
636 let base_selection = ankql::parser::parse_selection("age > 18").unwrap();
637 let selection = Selection { predicate: base_selection.predicate, order_by: None, limit: Some(10) };
638
639 let mut sql = SqlBuilder::with_fields(vec!["id", "name", "age"]);
640 sql.table_name("users");
641 sql.selection(&selection)?;
642 let (sql_string, args) = sql.build()?;
643
644 assert_eq!(sql_string, r#"SELECT "id", "name", "age" FROM "users" WHERE "age" > $1 LIMIT $2"#);
645 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new(18i64), Box::new(10i64)];
646 assert_args(&args, &expected);
647 Ok(())
648 }
649
650 #[test]
651 fn test_selection_with_order_by_and_limit() -> Result<()> {
652 use ankql::ast::{OrderByItem, OrderDirection, PathExpr, Selection};
653
654 let base_selection = ankql::parser::parse_selection("status = 'active'").unwrap();
655 let selection = Selection {
656 predicate: base_selection.predicate,
657 order_by: Some(vec![
658 OrderByItem { path: PathExpr::simple("priority"), direction: OrderDirection::Desc },
659 OrderByItem { path: PathExpr::simple("created_at"), direction: OrderDirection::Asc },
660 ]),
661 limit: Some(5),
662 };
663
664 let mut sql = SqlBuilder::with_fields(vec!["id", "status", "priority", "created_at"]);
665 sql.table_name("tasks");
666 sql.selection(&selection)?;
667 let (sql_string, args) = sql.build()?;
668
669 assert_eq!(
670 sql_string,
671 r#"SELECT "id", "status", "priority", "created_at" FROM "tasks" WHERE "status" = $1 ORDER BY "priority" DESC, "created_at" ASC LIMIT $2"#
672 );
673 let expected: Vec<Box<dyn ToSql + Send + Sync>> = vec![Box::new("active"), Box::new(5i64)];
674 assert_args(&args, &expected);
675 Ok(())
676 }
677
678 mod jsonb_sql_tests {
688 use super::*;
689 use ankql::ast::PathExpr;
690
691 #[test]
692 fn test_two_step_json_path() -> Result<()> {
693 let selection = parse_selection("licensing.territory = 'US'").unwrap();
695 let mut sql = SqlBuilder::new();
696 sql.selection(&selection)?;
697 let (sql_string, _) = sql.build_where_clause();
698
699 assert_eq!(sql_string, r#""licensing"->'territory' = '"US"'::jsonb"#);
701 Ok(())
702 }
703
704 #[test]
705 fn test_three_step_json_path() -> Result<()> {
706 let selection = parse_selection("licensing.rights.holder = 'Label'").unwrap();
708 let mut sql = SqlBuilder::new();
709 sql.selection(&selection)?;
710 let (sql_string, _) = sql.build_where_clause();
711
712 assert_eq!(sql_string, r#""licensing"->'rights'->'holder' = '"Label"'::jsonb"#);
713 Ok(())
714 }
715
716 #[test]
717 fn test_four_step_json_path() -> Result<()> {
718 let selection = parse_selection("a.b.c.d = 'value'").unwrap();
720 let mut sql = SqlBuilder::new();
721 sql.selection(&selection)?;
722 let (sql_string, _) = sql.build_where_clause();
723
724 assert_eq!(sql_string, r#""a"->'b'->'c'->'d' = '"value"'::jsonb"#);
725 Ok(())
726 }
727
728 #[test]
729 fn test_json_path_with_numeric_comparison() -> Result<()> {
730 let selection = parse_selection("data.count > 10").unwrap();
735 let mut sql = SqlBuilder::new();
736 sql.selection(&selection)?;
737 let (sql_string, _) = sql.build_where_clause();
738
739 assert_eq!(sql_string, r#""data"->'count' > '10'::jsonb"#);
740 Ok(())
741 }
742
743 #[test]
744 fn test_mixed_simple_and_json_paths() -> Result<()> {
745 let selection = parse_selection("name = 'test' AND data.status = 'active'").unwrap();
748 let mut sql = SqlBuilder::new();
749 sql.selection(&selection)?;
750 let (sql_string, _) = sql.build_where_clause();
751
752 assert_eq!(sql_string, r#""name" = $1 AND "data"->'status' = '"active"'::jsonb"#);
753 Ok(())
754 }
755
756 #[test]
757 fn test_json_path_escaping() -> Result<()> {
758 let mut sql = SqlBuilder::new();
761 let path = PathExpr { steps: vec!["data".to_string(), "it's".to_string()] };
762 sql.expr(&Expr::Path(path))?;
763 let (sql_string, _) = sql.build_where_clause();
764
765 assert_eq!(sql_string, r#""data"->'it''s'"#);
767 Ok(())
768 }
769
770 #[test]
771 fn test_json_path_with_boolean() -> Result<()> {
772 let selection = parse_selection("data.active = true").unwrap();
773 let mut sql = SqlBuilder::new();
774 sql.selection(&selection)?;
775 let (sql_string, _) = sql.build_where_clause();
776
777 assert_eq!(sql_string, r#""data"->'active' = 'true'::jsonb"#);
778 Ok(())
779 }
780
781 #[test]
782 fn test_json_path_with_float() -> Result<()> {
783 let selection = parse_selection("data.score >= 95").unwrap();
785 let mut sql = SqlBuilder::new();
786 sql.selection(&selection)?;
787 let (sql_string, _) = sql.build_where_clause();
788
789 assert_eq!(sql_string, r#""data"->'score' >= '95'::jsonb"#);
790 Ok(())
791 }
792 }
793
794 mod predicate_split_tests {
799 use super::*;
800
801 #[test]
802 fn test_simple_predicate_fully_pushable() {
803 let selection = parse_selection("name = 'Alice'").unwrap();
804 let split = split_predicate_for_postgres(&selection.predicate);
805
806 assert!(!split.needs_post_filter());
808 assert!(matches!(split.remaining_predicate, Predicate::True));
809 }
810
811 #[test]
812 fn test_json_path_predicate_pushable() {
813 let selection = parse_selection("licensing.territory = 'US'").unwrap();
818 let split = split_predicate_for_postgres(&selection.predicate);
819
820 assert!(!split.needs_post_filter());
822 }
823
824 #[test]
825 fn test_and_with_all_pushable() {
826 let selection = parse_selection("name = 'test' AND licensing.status = 'active'").unwrap();
827 let split = split_predicate_for_postgres(&selection.predicate);
828
829 assert!(!split.needs_post_filter());
831 }
832
833 #[test]
834 fn test_or_with_all_pushable() {
835 let selection = parse_selection("name = 'a' OR name = 'b'").unwrap();
836 let split = split_predicate_for_postgres(&selection.predicate);
837
838 assert!(!split.needs_post_filter());
840 }
841
842 #[test]
843 fn test_complex_nested_predicate() {
844 let selection = parse_selection("(name = 'test' OR data.type = 'special') AND status = 'active'").unwrap();
845 let split = split_predicate_for_postgres(&selection.predicate);
846
847 assert!(!split.needs_post_filter());
849 }
850
851 #[test]
852 fn test_not_predicate_pushable() {
853 let selection = parse_selection("NOT (status = 'deleted')").unwrap();
854 let split = split_predicate_for_postgres(&selection.predicate);
855
856 assert!(!split.needs_post_filter());
857 }
858
859 #[test]
860 fn test_is_null_pushable() {
861 let selection = parse_selection("name IS NULL").unwrap();
862 let split = split_predicate_for_postgres(&selection.predicate);
863
864 assert!(!split.needs_post_filter());
865 }
866
867 }
877}