1use std::collections::HashMap;
6use std::sync::Arc;
7
8use super::{to_snake_case, GraphQLSchema, RelationType};
9
10#[derive(Debug, Clone)]
12pub struct SqlQuery {
13 pub sql: String,
15 pub params: Vec<serde_json::Value>,
17 pub table: Option<String>,
19 pub is_count: bool,
21}
22
23impl SqlQuery {
24 pub fn new(sql: impl Into<String>) -> Self {
26 Self {
27 sql: sql.into(),
28 params: Vec::new(),
29 table: None,
30 is_count: false,
31 }
32 }
33
34 pub fn param(mut self, value: serde_json::Value) -> Self {
36 self.params.push(value);
37 self
38 }
39
40 pub fn from_table(mut self, table: impl Into<String>) -> Self {
42 self.table = Some(table.into());
43 self
44 }
45
46 pub fn count(mut self) -> Self {
48 self.is_count = true;
49 self
50 }
51
52 pub fn placeholder(index: usize) -> String {
54 format!("${}", index + 1)
55 }
56}
57
58#[derive(Debug, Clone)]
60pub enum QueryPlan {
61 Single {
63 selection: Selection,
65 filters: Vec<Filter>,
67 limit: Option<u32>,
69 offset: Option<u32>,
71 },
72 Relationship {
74 parent: Selection,
76 child: Selection,
78 relation_type: RelationType,
80 join_column: String,
82 parent_column: String,
84 },
85 Batch {
87 queries: Vec<QueryPlan>,
89 },
90 Multiple {
92 plans: Vec<QueryPlan>,
94 },
95}
96
97#[derive(Debug, Clone)]
99pub struct Selection {
100 pub table_name: String,
102 pub fields: Vec<String>,
104 pub relationships: Vec<(String, QueryPlan)>,
106}
107
108impl Selection {
109 pub fn new(table_name: impl Into<String>) -> Self {
111 Self {
112 table_name: table_name.into(),
113 fields: Vec::new(),
114 relationships: Vec::new(),
115 }
116 }
117
118 pub fn field(mut self, name: impl Into<String>) -> Self {
120 self.fields.push(name.into());
121 self
122 }
123
124 pub fn fields(mut self, names: Vec<String>) -> Self {
126 self.fields.extend(names);
127 self
128 }
129
130 pub fn relationship(mut self, name: impl Into<String>, plan: QueryPlan) -> Self {
132 self.relationships.push((name.into(), plan));
133 self
134 }
135
136 pub fn table_name(&self) -> &str {
138 &self.table_name
139 }
140
141 pub fn primary_key(&self) -> &str {
143 "id"
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct Filter {
150 pub field: String,
152 pub operator: FilterOperator,
154 pub value: serde_json::Value,
156}
157
158impl Filter {
159 pub fn new(
161 field: impl Into<String>,
162 operator: FilterOperator,
163 value: serde_json::Value,
164 ) -> Self {
165 Self {
166 field: field.into(),
167 operator,
168 value,
169 }
170 }
171
172 pub fn eq(field: impl Into<String>, value: serde_json::Value) -> Self {
174 Self::new(field, FilterOperator::Eq, value)
175 }
176
177 pub fn ne(field: impl Into<String>, value: serde_json::Value) -> Self {
179 Self::new(field, FilterOperator::Ne, value)
180 }
181
182 pub fn gt(field: impl Into<String>, value: serde_json::Value) -> Self {
184 Self::new(field, FilterOperator::Gt, value)
185 }
186
187 pub fn in_values(field: impl Into<String>, values: Vec<serde_json::Value>) -> Self {
189 Self::new(field, FilterOperator::In, serde_json::Value::Array(values))
190 }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum FilterOperator {
196 Eq,
198 Ne,
200 Gt,
202 Gte,
204 Lt,
206 Lte,
208 Contains,
210 StartsWith,
212 EndsWith,
214 In,
216 NotIn,
218 IsNull,
220 IsNotNull,
222}
223
224impl FilterOperator {
225 pub fn to_sql(&self) -> &'static str {
227 match self {
228 FilterOperator::Eq => "=",
229 FilterOperator::Ne => "<>",
230 FilterOperator::Gt => ">",
231 FilterOperator::Gte => ">=",
232 FilterOperator::Lt => "<",
233 FilterOperator::Lte => "<=",
234 FilterOperator::Contains => "LIKE",
235 FilterOperator::StartsWith => "LIKE",
236 FilterOperator::EndsWith => "LIKE",
237 FilterOperator::In => "IN",
238 FilterOperator::NotIn => "NOT IN",
239 FilterOperator::IsNull => "IS NULL",
240 FilterOperator::IsNotNull => "IS NOT NULL",
241 }
242 }
243
244 pub fn needs_value(&self) -> bool {
246 !matches!(self, FilterOperator::IsNull | FilterOperator::IsNotNull)
247 }
248
249 pub fn is_like(&self) -> bool {
251 matches!(
252 self,
253 FilterOperator::Contains | FilterOperator::StartsWith | FilterOperator::EndsWith
254 )
255 }
256}
257
258#[derive(Debug)]
260pub struct SqlGenerator {
261 #[allow(dead_code)]
263 schema: Arc<GraphQLSchema>,
264 quote_char: char,
266 param_style: ParamStyle,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum ParamStyle {
273 Positional,
275 Named,
277 QuestionMark,
279}
280
281impl SqlGenerator {
282 pub fn new(schema: Arc<GraphQLSchema>) -> Self {
284 Self {
285 schema,
286 quote_char: '"',
287 param_style: ParamStyle::Positional,
288 }
289 }
290
291 pub fn with_quote_char(mut self, char: char) -> Self {
293 self.quote_char = char;
294 self
295 }
296
297 pub fn with_param_style(mut self, style: ParamStyle) -> Self {
299 self.param_style = style;
300 self
301 }
302
303 pub fn generate(&self, plan: &QueryPlan) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
305 match plan {
306 QueryPlan::Single {
307 selection,
308 filters,
309 limit,
310 offset,
311 } => Ok(vec![
312 self.generate_single(selection, filters, *limit, *offset)?
313 ]),
314 QueryPlan::Relationship {
315 parent,
316 child,
317 relation_type,
318 join_column,
319 parent_column,
320 } => self.generate_relationship(
321 parent,
322 child,
323 *relation_type,
324 join_column,
325 parent_column,
326 ),
327 QueryPlan::Batch { queries } => self.generate_batch(queries),
328 QueryPlan::Multiple { plans } => {
329 let mut results = Vec::new();
330 for p in plans {
331 results.extend(self.generate(p)?);
332 }
333 Ok(results)
334 }
335 }
336 }
337
338 fn generate_single(
340 &self,
341 selection: &Selection,
342 filters: &[Filter],
343 limit: Option<u32>,
344 offset: Option<u32>,
345 ) -> Result<SqlQuery, SqlGeneratorError> {
346 let mut params = Vec::new();
347 let mut param_index = 0;
348
349 let columns = if selection.fields.is_empty() {
351 "*".to_string()
352 } else {
353 selection
354 .fields
355 .iter()
356 .map(|f| self.quote_identifier(&to_snake_case(f)))
357 .collect::<Vec<_>>()
358 .join(", ")
359 };
360
361 let table = self.quote_identifier(&selection.table_name);
363
364 let where_clause = if filters.is_empty() {
366 String::new()
367 } else {
368 let conditions: Vec<String> = filters
369 .iter()
370 .map(|f| {
371 let col = self.quote_identifier(&to_snake_case(&f.field));
372 if f.operator.needs_value() {
373 param_index += 1;
374 params.push(self.prepare_value(&f.operator, &f.value));
375 format!(
376 "{} {} {}",
377 col,
378 f.operator.to_sql(),
379 self.placeholder(param_index - 1)
380 )
381 } else {
382 format!("{} {}", col, f.operator.to_sql())
383 }
384 })
385 .collect();
386 format!(" WHERE {}", conditions.join(" AND "))
387 };
388
389 let mut limit_offset = String::new();
391 if let Some(l) = limit {
392 limit_offset.push_str(&format!(" LIMIT {}", l));
393 }
394 if let Some(o) = offset {
395 limit_offset.push_str(&format!(" OFFSET {}", o));
396 }
397
398 let sql = format!(
399 "SELECT {} FROM {}{}{}",
400 columns, table, where_clause, limit_offset
401 );
402
403 Ok(SqlQuery {
404 sql,
405 params,
406 table: Some(selection.table_name.clone()),
407 is_count: false,
408 })
409 }
410
411 fn generate_relationship(
413 &self,
414 parent: &Selection,
415 child: &Selection,
416 relation_type: RelationType,
417 join_column: &str,
418 parent_column: &str,
419 ) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
420 match relation_type {
421 RelationType::OneToOne | RelationType::ManyToOne => {
422 Ok(vec![self.generate_with_join(
424 parent,
425 child,
426 join_column,
427 parent_column,
428 )?])
429 }
430 RelationType::OneToMany | RelationType::ManyToMany => {
431 Ok(vec![self.generate_with_lateral(
433 parent,
434 child,
435 join_column,
436 parent_column,
437 )?])
438 }
439 }
440 }
441
442 fn generate_with_join(
444 &self,
445 parent: &Selection,
446 child: &Selection,
447 join_column: &str,
448 parent_column: &str,
449 ) -> Result<SqlQuery, SqlGeneratorError> {
450 let parent_alias = "p";
451 let child_alias = "c";
452
453 let parent_cols: Vec<String> = if parent.fields.is_empty() {
454 vec![format!("{}.*", parent_alias)]
455 } else {
456 parent
457 .fields
458 .iter()
459 .map(|f| {
460 format!(
461 "{}.{}",
462 parent_alias,
463 self.quote_identifier(&to_snake_case(f))
464 )
465 })
466 .collect()
467 };
468
469 let child_cols: Vec<String> = if child.fields.is_empty() {
470 vec![format!("{}.*", child_alias)]
471 } else {
472 child
473 .fields
474 .iter()
475 .map(|f| {
476 format!(
477 "{}.{}",
478 child_alias,
479 self.quote_identifier(&to_snake_case(f))
480 )
481 })
482 .collect()
483 };
484
485 let all_cols = [parent_cols, child_cols].concat();
486
487 let sql = format!(
488 "SELECT {} FROM {} {} LEFT JOIN {} {} ON {}.{} = {}.{}",
489 all_cols.join(", "),
490 self.quote_identifier(&parent.table_name),
491 parent_alias,
492 self.quote_identifier(&child.table_name),
493 child_alias,
494 child_alias,
495 self.quote_identifier(&to_snake_case(join_column)),
496 parent_alias,
497 self.quote_identifier(&to_snake_case(parent_column))
498 );
499
500 Ok(SqlQuery::new(sql).from_table(&parent.table_name))
501 }
502
503 fn generate_with_lateral(
505 &self,
506 parent: &Selection,
507 child: &Selection,
508 join_column: &str,
509 parent_column: &str,
510 ) -> Result<SqlQuery, SqlGeneratorError> {
511 let parent_alias = "p";
512 let child_alias = "c";
513
514 let parent_cols: Vec<String> = if parent.fields.is_empty() {
515 vec![format!("{}.*", parent_alias)]
516 } else {
517 parent
518 .fields
519 .iter()
520 .map(|f| {
521 format!(
522 "{}.{}",
523 parent_alias,
524 self.quote_identifier(&to_snake_case(f))
525 )
526 })
527 .collect()
528 };
529
530 let child_cols: Vec<String> = if child.fields.is_empty() {
531 vec!["*".to_string()]
532 } else {
533 child
534 .fields
535 .iter()
536 .map(|f| self.quote_identifier(&to_snake_case(f)))
537 .collect()
538 };
539
540 let sql = format!(
541 "SELECT {}, LATERAL (
542 SELECT json_agg(sub.*) FROM (
543 SELECT {} FROM {} {} WHERE {}.{} = {}.{}
544 ) sub
545 ) AS {}
546 FROM {} {}",
547 parent_cols.join(", "),
548 child_cols.join(", "),
549 self.quote_identifier(&child.table_name),
550 child_alias,
551 child_alias,
552 self.quote_identifier(&to_snake_case(join_column)),
553 parent_alias,
554 self.quote_identifier(&to_snake_case(parent_column)),
555 self.quote_identifier(&to_snake_case(&child.table_name)),
556 self.quote_identifier(&parent.table_name),
557 parent_alias
558 );
559
560 Ok(SqlQuery::new(sql).from_table(&parent.table_name))
561 }
562
563 fn generate_batch(&self, queries: &[QueryPlan]) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
565 let mut results = Vec::new();
568 for query in queries {
569 results.extend(self.generate(query)?);
570 }
571 Ok(results)
572 }
573
574 fn quote_identifier(&self, name: &str) -> String {
576 format!("{}{}{}", self.quote_char, name, self.quote_char)
577 }
578
579 fn placeholder(&self, index: usize) -> String {
581 match self.param_style {
582 ParamStyle::Positional => format!("${}", index + 1),
583 ParamStyle::Named => format!(":p{}", index),
584 ParamStyle::QuestionMark => "?".to_string(),
585 }
586 }
587
588 fn prepare_value(
590 &self,
591 operator: &FilterOperator,
592 value: &serde_json::Value,
593 ) -> serde_json::Value {
594 match operator {
595 FilterOperator::Contains => {
596 if let serde_json::Value::String(s) = value {
597 serde_json::Value::String(format!("%{}%", s))
598 } else {
599 value.clone()
600 }
601 }
602 FilterOperator::StartsWith => {
603 if let serde_json::Value::String(s) = value {
604 serde_json::Value::String(format!("{}%", s))
605 } else {
606 value.clone()
607 }
608 }
609 FilterOperator::EndsWith => {
610 if let serde_json::Value::String(s) = value {
611 serde_json::Value::String(format!("%{}", s))
612 } else {
613 value.clone()
614 }
615 }
616 _ => value.clone(),
617 }
618 }
619
620 pub fn generate_count(
622 &self,
623 table: &str,
624 filters: &[Filter],
625 ) -> Result<SqlQuery, SqlGeneratorError> {
626 let mut params = Vec::new();
627 let mut param_index = 0;
628
629 let where_clause = if filters.is_empty() {
630 String::new()
631 } else {
632 let conditions: Vec<String> = filters
633 .iter()
634 .map(|f| {
635 let col = self.quote_identifier(&to_snake_case(&f.field));
636 if f.operator.needs_value() {
637 param_index += 1;
638 params.push(self.prepare_value(&f.operator, &f.value));
639 format!(
640 "{} {} {}",
641 col,
642 f.operator.to_sql(),
643 self.placeholder(param_index - 1)
644 )
645 } else {
646 format!("{} {}", col, f.operator.to_sql())
647 }
648 })
649 .collect();
650 format!(" WHERE {}", conditions.join(" AND "))
651 };
652
653 let sql = format!(
654 "SELECT COUNT(*) FROM {}{}",
655 self.quote_identifier(table),
656 where_clause
657 );
658
659 Ok(SqlQuery {
660 sql,
661 params,
662 table: Some(table.to_string()),
663 is_count: true,
664 })
665 }
666
667 pub fn generate_insert(
669 &self,
670 table: &str,
671 values: &HashMap<String, serde_json::Value>,
672 ) -> Result<SqlQuery, SqlGeneratorError> {
673 if values.is_empty() {
674 return Err(SqlGeneratorError::EmptyValues);
675 }
676
677 let columns: Vec<String> = values
678 .keys()
679 .map(|k| self.quote_identifier(&to_snake_case(k)))
680 .collect();
681
682 let placeholders: Vec<String> = (0..values.len()).map(|i| self.placeholder(i)).collect();
683
684 let params: Vec<serde_json::Value> = values.values().cloned().collect();
685
686 let sql = format!(
687 "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
688 self.quote_identifier(table),
689 columns.join(", "),
690 placeholders.join(", ")
691 );
692
693 Ok(SqlQuery {
694 sql,
695 params,
696 table: Some(table.to_string()),
697 is_count: false,
698 })
699 }
700
701 pub fn generate_update(
703 &self,
704 table: &str,
705 id: &serde_json::Value,
706 values: &HashMap<String, serde_json::Value>,
707 ) -> Result<SqlQuery, SqlGeneratorError> {
708 if values.is_empty() {
709 return Err(SqlGeneratorError::EmptyValues);
710 }
711
712 let set_clauses: Vec<String> = values
713 .keys()
714 .enumerate()
715 .map(|(i, k)| {
716 format!(
717 "{} = {}",
718 self.quote_identifier(&to_snake_case(k)),
719 self.placeholder(i)
720 )
721 })
722 .collect();
723
724 let mut params: Vec<serde_json::Value> = values.values().cloned().collect();
725 params.push(id.clone());
726
727 let id_placeholder = self.placeholder(params.len() - 1);
728
729 let sql = format!(
730 "UPDATE {} SET {} WHERE {} = {} RETURNING *",
731 self.quote_identifier(table),
732 set_clauses.join(", "),
733 self.quote_identifier("id"),
734 id_placeholder
735 );
736
737 Ok(SqlQuery {
738 sql,
739 params,
740 table: Some(table.to_string()),
741 is_count: false,
742 })
743 }
744
745 pub fn generate_delete(
747 &self,
748 table: &str,
749 id: &serde_json::Value,
750 ) -> Result<SqlQuery, SqlGeneratorError> {
751 let sql = format!(
752 "DELETE FROM {} WHERE {} = {} RETURNING {}",
753 self.quote_identifier(table),
754 self.quote_identifier("id"),
755 self.placeholder(0),
756 self.quote_identifier("id")
757 );
758
759 Ok(SqlQuery {
760 sql,
761 params: vec![id.clone()],
762 table: Some(table.to_string()),
763 is_count: false,
764 })
765 }
766}
767
768#[derive(Debug, Clone)]
770pub enum SqlGeneratorError {
771 EmptyValues,
773 InvalidFilter(String),
775 UnknownTable(String),
777 InvalidRelationship(String),
779}
780
781impl std::fmt::Display for SqlGeneratorError {
782 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
783 match self {
784 SqlGeneratorError::EmptyValues => write!(f, "No values provided"),
785 SqlGeneratorError::InvalidFilter(msg) => write!(f, "Invalid filter: {}", msg),
786 SqlGeneratorError::UnknownTable(table) => write!(f, "Unknown table: {}", table),
787 SqlGeneratorError::InvalidRelationship(msg) => {
788 write!(f, "Invalid relationship: {}", msg)
789 }
790 }
791 }
792}
793
794impl std::error::Error for SqlGeneratorError {}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use crate::graphql::introspector::GraphQLSchema;
800
801 fn create_generator() -> SqlGenerator {
802 let schema = Arc::new(GraphQLSchema::new());
803 SqlGenerator::new(schema)
804 }
805
806 #[test]
807 fn test_generate_simple_select() {
808 let generator = create_generator();
809 let selection = Selection::new("users").field("id").field("name");
810
811 let plan = QueryPlan::Single {
812 selection,
813 filters: vec![],
814 limit: None,
815 offset: None,
816 };
817
818 let queries = generator.generate(&plan).unwrap();
819 assert_eq!(queries.len(), 1);
820 assert!(queries[0].sql.contains("SELECT"));
821 assert!(queries[0].sql.contains("\"users\""));
822 }
823
824 #[test]
825 fn test_generate_with_filters() {
826 let generator = create_generator();
827 let selection = Selection::new("users");
828
829 let plan = QueryPlan::Single {
830 selection,
831 filters: vec![Filter::eq("id", serde_json::json!("123"))],
832 limit: None,
833 offset: None,
834 };
835
836 let queries = generator.generate(&plan).unwrap();
837 assert_eq!(queries.len(), 1);
838 assert!(queries[0].sql.contains("WHERE"));
839 assert!(queries[0].sql.contains("$1"));
840 assert_eq!(queries[0].params.len(), 1);
841 }
842
843 #[test]
844 fn test_generate_with_limit_offset() {
845 let generator = create_generator();
846 let selection = Selection::new("users");
847
848 let plan = QueryPlan::Single {
849 selection,
850 filters: vec![],
851 limit: Some(10),
852 offset: Some(20),
853 };
854
855 let queries = generator.generate(&plan).unwrap();
856 assert!(queries[0].sql.contains("LIMIT 10"));
857 assert!(queries[0].sql.contains("OFFSET 20"));
858 }
859
860 #[test]
861 fn test_generate_join() {
862 let generator = create_generator();
863 let parent = Selection::new("users").field("id").field("name");
864 let child = Selection::new("profiles").field("bio");
865
866 let plan = QueryPlan::Relationship {
867 parent,
868 child,
869 relation_type: RelationType::OneToOne,
870 join_column: "user_id".to_string(),
871 parent_column: "id".to_string(),
872 };
873
874 let queries = generator.generate(&plan).unwrap();
875 assert_eq!(queries.len(), 1);
876 assert!(queries[0].sql.contains("LEFT JOIN"));
877 }
878
879 #[test]
880 fn test_generate_lateral() {
881 let generator = create_generator();
882 let parent = Selection::new("users").field("id");
883 let child = Selection::new("posts").field("title");
884
885 let plan = QueryPlan::Relationship {
886 parent,
887 child,
888 relation_type: RelationType::OneToMany,
889 join_column: "user_id".to_string(),
890 parent_column: "id".to_string(),
891 };
892
893 let queries = generator.generate(&plan).unwrap();
894 assert_eq!(queries.len(), 1);
895 assert!(queries[0].sql.contains("LATERAL"));
896 assert!(queries[0].sql.contains("json_agg"));
897 }
898
899 #[test]
900 fn test_generate_count() {
901 let generator = create_generator();
902 let query = generator.generate_count("users", &[]).unwrap();
903
904 assert!(query.sql.contains("COUNT(*)"));
905 assert!(query.is_count);
906 }
907
908 #[test]
909 fn test_generate_insert() {
910 let generator = create_generator();
911 let mut values = HashMap::new();
912 values.insert("name".to_string(), serde_json::json!("John"));
913 values.insert("email".to_string(), serde_json::json!("john@example.com"));
914
915 let query = generator.generate_insert("users", &values).unwrap();
916
917 assert!(query.sql.contains("INSERT INTO"));
918 assert!(query.sql.contains("RETURNING"));
919 assert_eq!(query.params.len(), 2);
920 }
921
922 #[test]
923 fn test_generate_update() {
924 let generator = create_generator();
925 let mut values = HashMap::new();
926 values.insert("name".to_string(), serde_json::json!("Jane"));
927
928 let query = generator
929 .generate_update("users", &serde_json::json!("123"), &values)
930 .unwrap();
931
932 assert!(query.sql.contains("UPDATE"));
933 assert!(query.sql.contains("SET"));
934 assert!(query.sql.contains("WHERE"));
935 assert!(query.sql.contains("RETURNING"));
936 }
937
938 #[test]
939 fn test_generate_delete() {
940 let generator = create_generator();
941 let query = generator
942 .generate_delete("users", &serde_json::json!("123"))
943 .unwrap();
944
945 assert!(query.sql.contains("DELETE FROM"));
946 assert!(query.sql.contains("WHERE"));
947 assert!(query.sql.contains("RETURNING"));
948 }
949
950 #[test]
951 fn test_filter_operators() {
952 assert_eq!(FilterOperator::Eq.to_sql(), "=");
953 assert_eq!(FilterOperator::Ne.to_sql(), "<>");
954 assert_eq!(FilterOperator::Gt.to_sql(), ">");
955 assert_eq!(FilterOperator::Contains.to_sql(), "LIKE");
956 assert_eq!(FilterOperator::In.to_sql(), "IN");
957 assert_eq!(FilterOperator::IsNull.to_sql(), "IS NULL");
958 }
959
960 #[test]
961 fn test_like_pattern_preparation() {
962 let generator = create_generator();
963
964 let contains =
965 generator.prepare_value(&FilterOperator::Contains, &serde_json::json!("test"));
966 assert_eq!(contains, serde_json::json!("%test%"));
967
968 let starts =
969 generator.prepare_value(&FilterOperator::StartsWith, &serde_json::json!("test"));
970 assert_eq!(starts, serde_json::json!("test%"));
971
972 let ends = generator.prepare_value(&FilterOperator::EndsWith, &serde_json::json!("test"));
973 assert_eq!(ends, serde_json::json!("%test"));
974 }
975
976 #[test]
977 fn test_empty_insert_error() {
978 let generator = create_generator();
979 let result = generator.generate_insert("users", &HashMap::new());
980 assert!(matches!(result, Err(SqlGeneratorError::EmptyValues)));
981 }
982
983 #[test]
984 fn test_param_styles() {
985 let schema = Arc::new(GraphQLSchema::new());
986
987 let positional = SqlGenerator::new(schema.clone());
988 assert_eq!(positional.placeholder(0), "$1");
989 assert_eq!(positional.placeholder(2), "$3");
990
991 let question = SqlGenerator::new(schema.clone()).with_param_style(ParamStyle::QuestionMark);
992 assert_eq!(question.placeholder(0), "?");
993
994 let named = SqlGenerator::new(schema).with_param_style(ParamStyle::Named);
995 assert_eq!(named.placeholder(0), ":p0");
996 }
997}