1use crate::ast::*;
2use crate::error::SqlError;
3
4#[derive(Debug, Clone, serde::Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct QueryResult {
37 pub query: String,
39 pub params: Vec<serde_json::Value>,
41 pub tables: Vec<String>,
43}
44
45pub struct QueryBuilder {
64 pub sql: String,
66 pub params: Vec<serde_json::Value>,
68 pub param_index: usize,
70 pub tables: Vec<String>,
72 #[cfg(any(feature = "postgres", feature = "wasm"))]
74 pub schema_cache: Option<std::sync::Arc<crate::schema_cache::SchemaCache>>,
75 pub current_schema: String,
77}
78
79impl Default for QueryBuilder {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl QueryBuilder {
86 pub fn new() -> Self {
88 Self {
89 sql: String::new(),
90 params: Vec::new(),
91 param_index: 0,
92 tables: Vec::new(),
93 #[cfg(any(feature = "postgres", feature = "wasm"))]
94 schema_cache: None,
95 current_schema: "public".to_string(),
96 }
97 }
98
99 #[cfg(any(feature = "postgres", feature = "wasm"))]
101 pub fn with_schema_cache(
102 mut self,
103 cache: std::sync::Arc<crate::schema_cache::SchemaCache>,
104 ) -> Self {
105 self.schema_cache = Some(cache);
106 self
107 }
108
109 pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
111 self.current_schema = schema.into();
112 self
113 }
114
115 pub fn build_select(
133 &mut self,
134 table: &str,
135 params: &ParsedParams,
136 ) -> Result<QueryResult, SqlError> {
137 self.tables.push(table.to_string());
138
139 if let Some(select) = ¶ms.select {
140 self.build_select_clause(select)?;
141 } else {
142 self.sql.push_str("SELECT *");
143 }
144
145 self.build_from_clause(table)?;
146
147 if !params.filters.is_empty() {
148 self.build_where_clause(¶ms.filters)?;
149 }
150
151 if !params.order.is_empty() {
152 self.build_order_clause(¶ms.order)?;
153 }
154
155 self.build_limit_offset(params.limit, params.offset)?;
156
157 Ok(QueryResult {
158 query: self.sql.clone(),
159 params: self.params.clone(),
160 tables: self.tables.clone(),
161 })
162 }
163
164 pub(crate) fn build_select_clause(&mut self, items: &[SelectItem]) -> Result<(), SqlError> {
165 if items.is_empty() {
166 return Err(SqlError::NoSelectItems);
167 }
168
169 let columns: Vec<String> = items
170 .iter()
171 .map(|item| self.select_item_to_sql(item))
172 .collect::<Result<Vec<_>, _>>()?;
173 self.sql.push_str("SELECT ");
174 self.sql.push_str(&columns.join(", "));
175 Ok(())
176 }
177
178 fn select_item_to_sql(&self, item: &SelectItem) -> Result<String, SqlError> {
179 match item.item_type {
180 ItemType::Field => {
181 if item.name == "*" {
182 Ok("*".to_string())
183 } else {
184 let field_sql = self.field_to_sql(&Field::new(&item.name));
185 if let Some(alias) = &item.alias {
186 Ok(format!("{} AS {}", field_sql, self.quote_identifier(alias)))
187 } else {
188 Ok(field_sql)
189 }
190 }
191 }
192 ItemType::Relation | ItemType::Spread => self.build_relation_sql(item),
193 }
194 }
195
196 fn build_relation_sql(&self, item: &SelectItem) -> Result<String, SqlError> {
197 #[cfg(any(feature = "postgres", feature = "wasm"))]
198 if let Some(cache) = &self.schema_cache {
199 let rel_table = &item.name;
200 let current_table = self.tables.last().ok_or(SqlError::NoTableContext)?;
201
202 return match cache.find_relationship(&self.current_schema, current_table, rel_table) {
203 Some(rel) => self.build_relation_with_fk(item, &rel),
204 None => Err(SqlError::RelationNotFound {
205 from_table: current_table.clone(),
206 to_table: rel_table.clone(),
207 }),
208 };
209 }
210
211 self.build_relation_placeholder(item)
214 }
215
216 #[cfg(any(feature = "postgres", feature = "wasm"))]
217 fn build_relation_with_fk(
218 &self,
219 item: &SelectItem,
220 rel: &crate::schema_cache::Relationship,
221 ) -> Result<String, SqlError> {
222 use crate::schema_cache::RelationType;
223
224 let rel_table = &item.name;
225 let current_table = self.tables.last().unwrap();
226
227 let column_list = if let Some(children) = &item.children {
229 children
230 .iter()
231 .filter(|c| c.item_type == ItemType::Field)
232 .map(|c| {
233 if c.name == "*" {
234 "*".to_string()
235 } else {
236 self.quote_identifier(&c.name)
237 }
238 })
239 .collect::<Vec<_>>()
240 .join(", ")
241 } else {
242 "*".to_string()
243 };
244
245 match rel.relation_type {
246 RelationType::ManyToOne => {
247 Ok(format!(
250 "COALESCE((SELECT row_to_json({}_1) FROM (SELECT {} FROM {} {} WHERE {}) {}_1), 'null'::json) AS {}",
251 rel_table,
252 column_list,
253 self.quote_identifier(rel_table),
254 rel_table,
255 rel.foreign_key.join_condition(current_table, rel_table),
256 rel_table,
257 self.quote_identifier(rel_table)
258 ))
259 }
260 RelationType::OneToMany => {
261 Ok(format!(
264 "COALESCE((SELECT json_agg({}_1) FROM (SELECT {} FROM {} {} WHERE {}) {}_1), '[]'::json) AS {}",
265 rel_table,
266 column_list,
267 self.quote_identifier(rel_table),
268 rel_table,
269 rel.foreign_key.join_condition(rel_table, current_table),
270 rel_table,
271 self.quote_identifier(rel_table)
272 ))
273 }
274 RelationType::ManyToMany { ref junction_table } => {
275 Err(SqlError::ManyToManyNotYetSupported {
277 junction_table: junction_table.clone(),
278 })
279 }
280 }
281 }
282
283 fn build_relation_placeholder(&self, item: &SelectItem) -> Result<String, SqlError> {
284 let rel_alias = &item.name;
285
286 if let Some(children) = &item.children {
287 let child_columns: Vec<String> = children
288 .iter()
289 .filter(|c| c.item_type == ItemType::Field)
290 .map(|c| {
291 if c.name == "*" {
292 format!("{}.*", rel_alias)
293 } else {
294 format!("{}.{}", rel_alias, self.quote_identifier(&c.name))
295 }
296 })
297 .collect();
298
299 if child_columns.is_empty() {
300 Ok(format!(
301 "(SELECT json_agg(row_to_json({}.*)) AS {} FROM {})",
302 rel_alias, rel_alias, rel_alias
303 ))
304 } else {
305 Ok(format!(
306 "(SELECT json_agg(row_to_json({rel_alias}_sub)) FROM (SELECT {columns} FROM {rel_alias}) {rel_alias}_sub) AS {rel_alias}",
307 rel_alias = rel_alias,
308 columns = child_columns.join(", "),
309 ))
310 }
311 } else {
312 Ok(format!(
313 "(SELECT json_agg(row_to_json({}.*)) AS {} FROM {})",
314 rel_alias, rel_alias, rel_alias
315 ))
316 }
317 }
318
319 fn build_from_clause(&mut self, table: &str) -> Result<(), SqlError> {
320 if table.is_empty() {
321 return Err(SqlError::EmptyTableName);
322 }
323
324 self.sql.push_str(" FROM ");
325 self.sql.push_str(&self.quote_identifier(table));
326 Ok(())
327 }
328
329 pub fn build_where_clause(&mut self, filters: &[LogicCondition]) -> Result<(), SqlError> {
330 self.sql.push_str(" WHERE ");
331
332 let clauses: Result<Vec<String>, SqlError> = filters
333 .iter()
334 .map(|filter| self.build_filter(filter))
335 .collect();
336
337 match clauses {
338 Ok(clauses) => {
339 self.sql.push_str(&clauses.join(" AND "));
340 Ok(())
341 }
342 Err(e) => Err(e),
343 }
344 }
345
346 pub(crate) fn build_filter(&mut self, condition: &LogicCondition) -> Result<String, SqlError> {
347 match condition {
348 LogicCondition::Filter(filter) => self.build_single_filter(filter),
349 LogicCondition::Logic(tree) => self.build_logic_tree(tree),
350 }
351 }
352
353 fn build_single_filter(&mut self, filter: &Filter) -> Result<String, SqlError> {
354 let field_sql = self.field_to_sql(&filter.field);
355 let (clause, _) = self.operator_to_sql(&field_sql, filter)?;
356 Ok(clause)
357 }
358
359 fn build_logic_tree(&mut self, tree: &LogicTree) -> Result<String, SqlError> {
360 let joiner = if tree.operator == LogicOperator::And {
361 " AND "
362 } else {
363 " OR "
364 };
365
366 let conditions: Result<Vec<String>, SqlError> = tree
367 .conditions
368 .iter()
369 .map(|c| self.build_filter(c))
370 .collect();
371
372 let conditions_sql = conditions?.join(joiner);
373
374 if tree.negated {
375 Ok(format!("NOT ({})", conditions_sql))
376 } else {
377 Ok(format!("({})", conditions_sql))
378 }
379 }
380
381 pub(crate) fn build_order_clause(&mut self, order_terms: &[OrderTerm]) -> Result<(), SqlError> {
382 let clauses: Result<Vec<String>, SqlError> = order_terms
383 .iter()
384 .map(|term| self.order_term_to_sql(term))
385 .collect();
386
387 match clauses {
388 Ok(clauses) => {
389 self.sql.push_str(" ORDER BY ");
390 self.sql.push_str(&clauses.join(", "));
391 Ok(())
392 }
393 Err(e) => Err(e),
394 }
395 }
396
397 fn order_term_to_sql(&self, term: &OrderTerm) -> Result<String, SqlError> {
398 let field_sql = self.field_to_sql(&term.field);
399 let dir_sql = if term.direction == Direction::Desc {
400 " DESC"
401 } else {
402 " ASC"
403 };
404
405 let nulls_sql = match term.nulls {
406 Some(Nulls::First) => " NULLS FIRST",
407 Some(Nulls::Last) => " NULLS LAST",
408 None => "",
409 };
410
411 Ok(format!("{}{}{}", field_sql, dir_sql, nulls_sql))
412 }
413
414 pub(crate) fn build_limit_offset(
415 &mut self,
416 limit: Option<u64>,
417 offset: Option<u64>,
418 ) -> Result<(), SqlError> {
419 match (limit, offset) {
420 (Some(lim), Some(off)) => {
421 let lim_ref = self.add_param(serde_json::Value::Number(lim.into()));
422 let off_ref = self.add_param(serde_json::Value::Number(off.into()));
423 self.sql
424 .push_str(&format!(" LIMIT {} OFFSET {}", lim_ref, off_ref));
425 }
426 (Some(lim), None) => {
427 let lim_ref = self.add_param(serde_json::Value::Number(lim.into()));
428 self.sql.push_str(&format!(" LIMIT {}", lim_ref));
429 }
430 (None, Some(off)) => {
431 let off_ref = self.add_param(serde_json::Value::Number(off.into()));
432 self.sql.push_str(&format!(" OFFSET {}", off_ref));
433 }
434 (None, None) => {}
435 }
436
437 Ok(())
438 }
439
440 fn operator_to_sql(
441 &mut self,
442 field: &str,
443 filter: &Filter,
444 ) -> Result<(String, usize), SqlError> {
445 let (op_sql, value) = match (&filter.operator, &filter.quantifier, &filter.value) {
446 (FilterOperator::Eq, Some(Quantifier::Any), FilterValue::List(ref _vals)) => {
448 let param_ref = self.add_param(filter.value.to_json());
449 (format!("{} = ANY({})", field, param_ref), param_ref.len())
450 }
451 (FilterOperator::Eq, Some(Quantifier::All), FilterValue::List(ref _vals)) => {
452 let param_ref = self.add_param(filter.value.to_json());
453 (format!("{} = ALL({})", field, param_ref), param_ref.len())
454 }
455 (FilterOperator::Eq, _, FilterValue::Single(ref _val)) => {
456 let param_ref = self.add_param(filter.value.to_json());
457 let op_sql = if filter.negated { "<>" } else { "=" };
458 (format!("{} {} {}", field, op_sql, param_ref), 1)
459 }
460
461 (FilterOperator::Neq, _, FilterValue::Single(_)) => {
463 let param_ref = self.add_param(filter.value.to_json());
464 let op_sql = if filter.negated { "=" } else { "<>" };
465 (format!("{} {} {}", field, op_sql, param_ref), 1)
466 }
467
468 (FilterOperator::Gt, _, FilterValue::Single(_)) => {
470 let param_ref = self.add_param(filter.value.to_json());
471 let op_sql = if filter.negated { "<=" } else { ">" };
472 (format!("{} {} {}", field, op_sql, param_ref), 1)
473 }
474 (FilterOperator::Gte, _, FilterValue::Single(_)) => {
475 let param_ref = self.add_param(filter.value.to_json());
476 let op_sql = if filter.negated { "<" } else { ">=" };
477 (format!("{} {} {}", field, op_sql, param_ref), 1)
478 }
479 (FilterOperator::Lt, _, FilterValue::Single(_)) => {
480 let param_ref = self.add_param(filter.value.to_json());
481 let op_sql = if filter.negated { ">=" } else { "<" };
482 (format!("{} {} {}", field, op_sql, param_ref), 1)
483 }
484 (FilterOperator::Lte, _, FilterValue::Single(_)) => {
485 let param_ref = self.add_param(filter.value.to_json());
486 let op_sql = if filter.negated { ">" } else { "<=" };
487 (format!("{} {} {}", field, op_sql, param_ref), 1)
488 }
489
490 (FilterOperator::In, _, FilterValue::List(_)) => {
492 let param_ref = self.add_param(filter.value.to_json());
493 let not_prefix = if filter.negated { "NOT " } else { "" };
494 (format!("{}{} = ANY({})", field, not_prefix, param_ref), 1)
495 }
496
497 (FilterOperator::Is, _, FilterValue::Single(ref val)) => {
499 let clause = self.build_is_clause(field, val, filter.negated)?;
500 (clause, 0)
501 }
502
503 (
505 FilterOperator::Like | FilterOperator::Ilike,
506 Some(Quantifier::Any),
507 FilterValue::List(_),
508 ) => {
509 let param_ref = self.add_param(filter.value.to_json());
510 let not_prefix = if filter.negated { "NOT " } else { "" };
511 let op_str = if filter.operator == FilterOperator::Like {
512 "LIKE"
513 } else {
514 "ILIKE"
515 };
516 (
517 format!("{}{} {} ANY({})", field, not_prefix, op_str, param_ref),
518 1,
519 )
520 }
521 (FilterOperator::Like | FilterOperator::Ilike, _, FilterValue::Single(_)) => {
522 let param_ref = self.add_param(filter.value.to_json());
523 let not_prefix = if filter.negated { "NOT " } else { "" };
524 let op_str = if filter.operator == FilterOperator::Like {
525 "LIKE"
526 } else {
527 "ILIKE"
528 };
529 (
530 format!("{}{} {} {}", field, not_prefix, op_str, param_ref),
531 1,
532 )
533 }
534
535 (FilterOperator::Match, _, FilterValue::Single(_)) => {
537 let param_ref = self.add_param(filter.value.to_json());
538 let op_sql = if filter.negated { "!~" } else { "~" };
539 (format!("{} {} {}", field, op_sql, param_ref), 1)
540 }
541 (FilterOperator::Imatch, _, FilterValue::Single(_)) => {
542 let param_ref = self.add_param(filter.value.to_json());
543 let op_sql = if filter.negated { "!~*" } else { "~*" };
544 (format!("{} {} {}", field, op_sql, param_ref), 1)
545 }
546
547 (
549 FilterOperator::Fts
550 | FilterOperator::Plfts
551 | FilterOperator::Phfts
552 | FilterOperator::Wfts,
553 _,
554 FilterValue::Single(_),
555 ) => {
556 let param_ref = self.add_param(filter.value.to_json());
557 let lang = filter.language.as_deref().unwrap_or("english");
558 let ts_fn = match filter.operator {
559 FilterOperator::Fts | FilterOperator::Plfts => "plainto_tsquery",
560 FilterOperator::Phfts => "phraseto_tsquery",
561 FilterOperator::Wfts => "websearch_to_tsquery",
562 _ => unreachable!(),
563 };
564 let not_prefix = if filter.negated { "NOT " } else { "" };
565 (
566 format!(
567 "{}to_tsvector('{}', {}) @@ {}('{}', {})",
568 not_prefix, lang, field, ts_fn, lang, param_ref
569 ),
570 1,
571 )
572 }
573
574 (FilterOperator::Cs, _, FilterValue::Single(_)) => {
576 let param_ref = self.add_param(filter.value.to_json());
577 let op_sql = if filter.negated { "NOT @>" } else { "@>" };
578 (format!("{} {} {}", field, op_sql, param_ref), 1)
579 }
580 (FilterOperator::Cd, _, FilterValue::Single(_)) => {
581 let param_ref = self.add_param(filter.value.to_json());
582 let op_sql = if filter.negated { "NOT <@" } else { "<@" };
583 (format!("{} {} {}", field, op_sql, param_ref), 1)
584 }
585 (FilterOperator::Ov, _, FilterValue::List(_)) => {
586 let param_ref = self.add_param(filter.value.to_json());
587 let not_prefix = if filter.negated { "NOT " } else { "" };
588 (format!("{}{} && {}", field, not_prefix, param_ref), 1)
589 }
590
591 (FilterOperator::Sl, _, FilterValue::Single(_)) => {
593 let param_ref = self.add_param(filter.value.to_json());
594 let op_sql = if filter.negated { "NOT <<" } else { "<<" };
595 (format!("{} {} {}", field, op_sql, param_ref), 1)
596 }
597 (FilterOperator::Sr, _, FilterValue::Single(_)) => {
598 let param_ref = self.add_param(filter.value.to_json());
599 let op_sql = if filter.negated { "NOT >>" } else { ">>" };
600 (format!("{} {} {}", field, op_sql, param_ref), 1)
601 }
602 (FilterOperator::Nxl, _, FilterValue::Single(_)) => {
603 let param_ref = self.add_param(filter.value.to_json());
604 let op_sql = if filter.negated { "NOT &<" } else { "&<" };
605 (format!("{} {} {}", field, op_sql, param_ref), 1)
606 }
607 (FilterOperator::Nxr, _, FilterValue::Single(_)) => {
608 let param_ref = self.add_param(filter.value.to_json());
609 let op_sql = if filter.negated { "NOT &>" } else { "&>" };
610 (format!("{} {} {}", field, op_sql, param_ref), 1)
611 }
612 (FilterOperator::Adj, _, FilterValue::Single(_)) => {
613 let param_ref = self.add_param(filter.value.to_json());
614 let op_sql = if filter.negated { "NOT -|-" } else { "-|-" };
615 (format!("{} {} {}", field, op_sql, param_ref), 1)
616 }
617
618 _ => {
620 return Err(SqlError::InvalidParameter(format!(
621 "unsupported operator/value combination: {:?} with {:?}",
622 filter.operator, filter.value
623 )));
624 }
625 };
626
627 Ok((op_sql, value))
628 }
629
630 fn build_is_clause(&self, field: &str, value: &str, negated: bool) -> Result<String, SqlError> {
631 match (value.to_lowercase().as_str(), negated) {
632 ("null", false) => Ok(format!("{} IS NULL", field)),
633 ("null", true) => Ok(format!("{} IS NOT NULL", field)),
634 ("not_null", false) => Ok(format!("{} IS NOT NULL", field)),
635 ("not_null", true) => Ok(format!("{} IS NULL", field)),
636 ("true", false) => Ok(format!("{} IS TRUE", field)),
637 ("true", true) => Ok(format!("{} IS NOT TRUE", field)),
638 ("false", false) => Ok(format!("{} IS FALSE", field)),
639 ("false", true) => Ok(format!("{} IS NOT FALSE", field)),
640 ("unknown", false) => Ok(format!("{} IS UNKNOWN", field)),
641 ("unknown", true) => Ok(format!("{} IS NOT UNKNOWN", field)),
642 _ => Err(SqlError::InvalidParameter(format!(
643 "invalid IS value: {}",
644 value
645 ))),
646 }
647 }
648
649 fn field_to_sql(&self, field: &Field) -> String {
650 let base = self.quote_identifier(&field.name);
651
652 match (&field.json_path[..], &field.cast) {
653 ([], None) => base,
654 ([], Some(cast)) => format!("{}::{}", base, cast),
655 (json_path, cast_opt) => {
656 let json_path_sql: Vec<String> = json_path
657 .iter()
658 .map(|op| match op {
659 JsonOp::Arrow(key) => format!("->'{}'", key),
660 JsonOp::DoubleArrow(key) => format!("->>'{}'", key),
661 JsonOp::ArrayIndex(idx) => format!("->{}", idx),
662 })
663 .collect();
664
665 let json_path_str = json_path_sql.join("");
666
667 if let Some(cast) = cast_opt {
668 format!("({}{})::{}", base, json_path_str, cast)
669 } else {
670 format!("{}{}", base, json_path_str)
671 }
672 }
673 }
674 }
675
676 pub(crate) fn add_param(&mut self, value: serde_json::Value) -> String {
677 let idx = self.param_index + 1;
678 self.param_index = idx;
679 self.params.push(value);
680 format!("${}", idx)
681 }
682
683 fn quote_identifier(&self, name: &str) -> String {
684 let escaped = name.replace('"', "\"\"");
685 format!("\"{}\"", escaped)
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn test_query_builder_new() {
695 let builder = QueryBuilder::new();
696 assert!(builder.sql.is_empty());
697 assert!(builder.params.is_empty());
698 assert_eq!(builder.param_index, 0);
699 }
700
701 #[test]
702 fn test_add_param() {
703 let mut builder = QueryBuilder::new();
704 let param_ref = builder.add_param(serde_json::Value::String("test".to_string()));
705 assert_eq!(param_ref, "$1");
706 assert_eq!(builder.params.len(), 1);
707 assert_eq!(builder.param_index, 1);
708 }
709
710 #[test]
711 fn test_quote_identifier() {
712 let builder = QueryBuilder::new();
713 assert_eq!(builder.quote_identifier("id"), "\"id\"");
714 assert_eq!(builder.quote_identifier("user\"id"), "\"user\"\"id\"");
715 }
716
717 #[test]
718 fn test_field_to_sql_simple() {
719 let builder = QueryBuilder::new();
720 let field = Field::new("id");
721 assert_eq!(builder.field_to_sql(&field), "\"id\"");
722 }
723
724 #[test]
725 fn test_field_to_sql_with_json_path() {
726 let builder = QueryBuilder::new();
727 let field = Field::new("data").with_json_path(vec![JsonOp::Arrow("key".to_string())]);
728 let sql = builder.field_to_sql(&field);
729 assert!(sql.contains("\"data\"->'key'"));
730 }
731
732 #[test]
733 fn test_field_to_sql_with_cast() {
734 let builder = QueryBuilder::new();
735 let field = Field::new("price").with_cast("numeric");
736 assert_eq!(builder.field_to_sql(&field), "\"price\"::numeric");
737 }
738
739 #[test]
740 fn test_build_is_clause() {
741 let builder = QueryBuilder::new();
742
743 assert_eq!(
744 builder.build_is_clause("\"id\"", "null", false).unwrap(),
745 "\"id\" IS NULL"
746 );
747
748 assert_eq!(
749 builder.build_is_clause("\"id\"", "null", true).unwrap(),
750 "\"id\" IS NOT NULL"
751 );
752 }
753
754 #[test]
755 fn test_operator_to_sql_comparison() {
756 let mut builder = QueryBuilder::new();
757
758 let filter = Filter::new(
760 Field::new("age"),
761 FilterOperator::Gt,
762 FilterValue::Single("18".to_string()),
763 );
764 let (sql, _) = builder.operator_to_sql("\"age\"", &filter).unwrap();
765 assert_eq!(sql, "\"age\" > $1");
766 assert_eq!(
767 builder.params[0],
768 serde_json::Value::String("18".to_string())
769 );
770
771 builder = QueryBuilder::new();
773 let filter = Filter::new(
774 Field::new("age"),
775 FilterOperator::Gte,
776 FilterValue::Single("18".to_string()),
777 );
778 let (sql, _) = builder.operator_to_sql("\"age\"", &filter).unwrap();
779 assert_eq!(sql, "\"age\" >= $1");
780
781 builder = QueryBuilder::new();
783 let filter = Filter::new(
784 Field::new("age"),
785 FilterOperator::Lt,
786 FilterValue::Single("65".to_string()),
787 );
788 let (sql, _) = builder.operator_to_sql("\"age\"", &filter).unwrap();
789 assert_eq!(sql, "\"age\" < $1");
790
791 builder = QueryBuilder::new();
793 let filter = Filter::new(
794 Field::new("age"),
795 FilterOperator::Lte,
796 FilterValue::Single("65".to_string()),
797 );
798 let (sql, _) = builder.operator_to_sql("\"age\"", &filter).unwrap();
799 assert_eq!(sql, "\"age\" <= $1");
800
801 builder = QueryBuilder::new();
803 let filter = Filter::new(
804 Field::new("status"),
805 FilterOperator::Neq,
806 FilterValue::Single("active".to_string()),
807 );
808 let (sql, _) = builder.operator_to_sql("\"status\"", &filter).unwrap();
809 assert_eq!(sql, "\"status\" <> $1");
810 }
811
812 #[test]
813 fn test_operator_to_sql_in_operator() {
814 let mut builder = QueryBuilder::new();
815 let filter = Filter::new(
816 Field::new("status"),
817 FilterOperator::In,
818 FilterValue::List(vec!["active".to_string(), "pending".to_string()]),
819 );
820 let (sql, _) = builder.operator_to_sql("\"status\"", &filter).unwrap();
821 assert_eq!(sql, "\"status\" = ANY($1)");
822 assert!(matches!(builder.params[0], serde_json::Value::Array(_)));
823 }
824
825 #[test]
826 fn test_operator_to_sql_pattern_matching() {
827 let mut builder = QueryBuilder::new();
828
829 let filter = Filter::new(
831 Field::new("name"),
832 FilterOperator::Match,
833 FilterValue::Single("^John".to_string()),
834 );
835 let (sql, _) = builder.operator_to_sql("\"name\"", &filter).unwrap();
836 assert_eq!(sql, "\"name\" ~ $1");
837
838 builder = QueryBuilder::new();
840 let filter = Filter::new(
841 Field::new("name"),
842 FilterOperator::Imatch,
843 FilterValue::Single("^john".to_string()),
844 );
845 let (sql, _) = builder.operator_to_sql("\"name\"", &filter).unwrap();
846 assert_eq!(sql, "\"name\" ~* $1");
847 }
848
849 #[test]
850 fn test_operator_to_sql_fts() {
851 let mut builder = QueryBuilder::new();
852
853 let filter = Filter::new(
855 Field::new("content"),
856 FilterOperator::Fts,
857 FilterValue::Single("search".to_string()),
858 );
859 let (sql, _) = builder.operator_to_sql("\"content\"", &filter).unwrap();
860 assert_eq!(
861 sql,
862 "to_tsvector('english', \"content\") @@ plainto_tsquery('english', $1)"
863 );
864
865 builder = QueryBuilder::new();
867 let mut filter = Filter::new(
868 Field::new("content"),
869 FilterOperator::Fts,
870 FilterValue::Single("search".to_string()),
871 );
872 filter.language = Some("french".to_string());
873 let (sql, _) = builder.operator_to_sql("\"content\"", &filter).unwrap();
874 assert_eq!(
875 sql,
876 "to_tsvector('french', \"content\") @@ plainto_tsquery('french', $1)"
877 );
878
879 builder = QueryBuilder::new();
881 let filter = Filter::new(
882 Field::new("content"),
883 FilterOperator::Phfts,
884 FilterValue::Single("search phrase".to_string()),
885 );
886 let (sql, _) = builder.operator_to_sql("\"content\"", &filter).unwrap();
887 assert_eq!(
888 sql,
889 "to_tsvector('english', \"content\") @@ phraseto_tsquery('english', $1)"
890 );
891
892 builder = QueryBuilder::new();
894 let filter = Filter::new(
895 Field::new("content"),
896 FilterOperator::Wfts,
897 FilterValue::Single("search query".to_string()),
898 );
899 let (sql, _) = builder.operator_to_sql("\"content\"", &filter).unwrap();
900 assert_eq!(
901 sql,
902 "to_tsvector('english', \"content\") @@ websearch_to_tsquery('english', $1)"
903 );
904 }
905
906 #[test]
907 fn test_operator_to_sql_array_operators() {
908 let mut builder = QueryBuilder::new();
909
910 let filter = Filter::new(
912 Field::new("tags"),
913 FilterOperator::Cs,
914 FilterValue::Single("{rust}".to_string()),
915 );
916 let (sql, _) = builder.operator_to_sql("\"tags\"", &filter).unwrap();
917 assert_eq!(sql, "\"tags\" @> $1");
918
919 builder = QueryBuilder::new();
921 let filter = Filter::new(
922 Field::new("tags"),
923 FilterOperator::Cd,
924 FilterValue::Single("{rust,elixir}".to_string()),
925 );
926 let (sql, _) = builder.operator_to_sql("\"tags\"", &filter).unwrap();
927 assert_eq!(sql, "\"tags\" <@ $1");
928
929 builder = QueryBuilder::new();
931 let filter = Filter::new(
932 Field::new("tags"),
933 FilterOperator::Ov,
934 FilterValue::List(vec!["rust".to_string(), "elixir".to_string()]),
935 );
936 let (sql, _) = builder.operator_to_sql("\"tags\"", &filter).unwrap();
937 assert_eq!(sql, "\"tags\" && $1");
938 }
939
940 #[test]
941 fn test_operator_to_sql_range_operators() {
942 let mut builder = QueryBuilder::new();
943
944 let filter = Filter::new(
946 Field::new("range"),
947 FilterOperator::Sl,
948 FilterValue::Single("[1,10)".to_string()),
949 );
950 let (sql, _) = builder.operator_to_sql("\"range\"", &filter).unwrap();
951 assert_eq!(sql, "\"range\" << $1");
952
953 builder = QueryBuilder::new();
955 let filter = Filter::new(
956 Field::new("range"),
957 FilterOperator::Sr,
958 FilterValue::Single("[1,10)".to_string()),
959 );
960 let (sql, _) = builder.operator_to_sql("\"range\"", &filter).unwrap();
961 assert_eq!(sql, "\"range\" >> $1");
962
963 builder = QueryBuilder::new();
965 let filter = Filter::new(
966 Field::new("range"),
967 FilterOperator::Nxl,
968 FilterValue::Single("[1,10)".to_string()),
969 );
970 let (sql, _) = builder.operator_to_sql("\"range\"", &filter).unwrap();
971 assert_eq!(sql, "\"range\" &< $1");
972
973 builder = QueryBuilder::new();
975 let filter = Filter::new(
976 Field::new("range"),
977 FilterOperator::Nxr,
978 FilterValue::Single("[1,10)".to_string()),
979 );
980 let (sql, _) = builder.operator_to_sql("\"range\"", &filter).unwrap();
981 assert_eq!(sql, "\"range\" &> $1");
982
983 builder = QueryBuilder::new();
985 let filter = Filter::new(
986 Field::new("range"),
987 FilterOperator::Adj,
988 FilterValue::Single("[1,10)".to_string()),
989 );
990 let (sql, _) = builder.operator_to_sql("\"range\"", &filter).unwrap();
991 assert_eq!(sql, "\"range\" -|- $1");
992 }
993
994 #[test]
995 fn test_operator_to_sql_negated() {
996 let mut builder = QueryBuilder::new();
997
998 let mut filter = Filter::new(
1000 Field::new("status"),
1001 FilterOperator::Eq,
1002 FilterValue::Single("active".to_string()),
1003 );
1004 filter.negated = true;
1005 let (sql, _) = builder.operator_to_sql("\"status\"", &filter).unwrap();
1006 assert_eq!(sql, "\"status\" <> $1");
1007
1008 builder = QueryBuilder::new();
1010 let mut filter = Filter::new(
1011 Field::new("age"),
1012 FilterOperator::Gt,
1013 FilterValue::Single("18".to_string()),
1014 );
1015 filter.negated = true;
1016 let (sql, _) = builder.operator_to_sql("\"age\"", &filter).unwrap();
1017 assert_eq!(sql, "\"age\" <= $1");
1018
1019 builder = QueryBuilder::new();
1021 let mut filter = Filter::new(
1022 Field::new("content"),
1023 FilterOperator::Fts,
1024 FilterValue::Single("search".to_string()),
1025 );
1026 filter.negated = true;
1027 let (sql, _) = builder.operator_to_sql("\"content\"", &filter).unwrap();
1028 assert_eq!(
1029 sql,
1030 "NOT to_tsvector('english', \"content\") @@ plainto_tsquery('english', $1)"
1031 );
1032 }
1033
1034 #[test]
1035 fn test_operator_to_sql_with_quantifiers() {
1036 let mut builder = QueryBuilder::new();
1037
1038 let mut filter = Filter::new(
1040 Field::new("status"),
1041 FilterOperator::Eq,
1042 FilterValue::List(vec!["active".to_string(), "pending".to_string()]),
1043 );
1044 filter.quantifier = Some(Quantifier::Any);
1045 let (sql, _) = builder.operator_to_sql("\"status\"", &filter).unwrap();
1046 assert_eq!(sql, "\"status\" = ANY($1)");
1047
1048 builder = QueryBuilder::new();
1050 let mut filter = Filter::new(
1051 Field::new("status"),
1052 FilterOperator::Eq,
1053 FilterValue::List(vec!["active".to_string()]),
1054 );
1055 filter.quantifier = Some(Quantifier::All);
1056 let (sql, _) = builder.operator_to_sql("\"status\"", &filter).unwrap();
1057 assert_eq!(sql, "\"status\" = ALL($1)");
1058 }
1059}