1use std::collections::HashMap;
6use std::sync::Arc;
7
8use super::{GraphQLSchema, RelationType, to_snake_case};
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(field: impl Into<String>, operator: FilterOperator, value: serde_json::Value) -> Self {
161 Self {
162 field: field.into(),
163 operator,
164 value,
165 }
166 }
167
168 pub fn eq(field: impl Into<String>, value: serde_json::Value) -> Self {
170 Self::new(field, FilterOperator::Eq, value)
171 }
172
173 pub fn ne(field: impl Into<String>, value: serde_json::Value) -> Self {
175 Self::new(field, FilterOperator::Ne, value)
176 }
177
178 pub fn gt(field: impl Into<String>, value: serde_json::Value) -> Self {
180 Self::new(field, FilterOperator::Gt, value)
181 }
182
183 pub fn in_values(field: impl Into<String>, values: Vec<serde_json::Value>) -> Self {
185 Self::new(field, FilterOperator::In, serde_json::Value::Array(values))
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum FilterOperator {
192 Eq,
194 Ne,
196 Gt,
198 Gte,
200 Lt,
202 Lte,
204 Contains,
206 StartsWith,
208 EndsWith,
210 In,
212 NotIn,
214 IsNull,
216 IsNotNull,
218}
219
220impl FilterOperator {
221 pub fn to_sql(&self) -> &'static str {
223 match self {
224 FilterOperator::Eq => "=",
225 FilterOperator::Ne => "<>",
226 FilterOperator::Gt => ">",
227 FilterOperator::Gte => ">=",
228 FilterOperator::Lt => "<",
229 FilterOperator::Lte => "<=",
230 FilterOperator::Contains => "LIKE",
231 FilterOperator::StartsWith => "LIKE",
232 FilterOperator::EndsWith => "LIKE",
233 FilterOperator::In => "IN",
234 FilterOperator::NotIn => "NOT IN",
235 FilterOperator::IsNull => "IS NULL",
236 FilterOperator::IsNotNull => "IS NOT NULL",
237 }
238 }
239
240 pub fn needs_value(&self) -> bool {
242 !matches!(self, FilterOperator::IsNull | FilterOperator::IsNotNull)
243 }
244
245 pub fn is_like(&self) -> bool {
247 matches!(
248 self,
249 FilterOperator::Contains | FilterOperator::StartsWith | FilterOperator::EndsWith
250 )
251 }
252}
253
254#[derive(Debug)]
256pub struct SqlGenerator {
257 schema: Arc<GraphQLSchema>,
259 quote_char: char,
261 param_style: ParamStyle,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum ParamStyle {
268 Positional,
270 Named,
272 QuestionMark,
274}
275
276impl SqlGenerator {
277 pub fn new(schema: Arc<GraphQLSchema>) -> Self {
279 Self {
280 schema,
281 quote_char: '"',
282 param_style: ParamStyle::Positional,
283 }
284 }
285
286 pub fn with_quote_char(mut self, char: char) -> Self {
288 self.quote_char = char;
289 self
290 }
291
292 pub fn with_param_style(mut self, style: ParamStyle) -> Self {
294 self.param_style = style;
295 self
296 }
297
298 pub fn generate(&self, plan: &QueryPlan) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
300 match plan {
301 QueryPlan::Single { selection, filters, limit, offset } => {
302 Ok(vec![self.generate_single(selection, filters, *limit, *offset)?])
303 }
304 QueryPlan::Relationship { parent, child, relation_type, join_column, parent_column } => {
305 self.generate_relationship(parent, child, *relation_type, join_column, parent_column)
306 }
307 QueryPlan::Batch { queries } => {
308 self.generate_batch(queries)
309 }
310 QueryPlan::Multiple { plans } => {
311 let mut results = Vec::new();
312 for p in plans {
313 results.extend(self.generate(p)?);
314 }
315 Ok(results)
316 }
317 }
318 }
319
320 fn generate_single(
322 &self,
323 selection: &Selection,
324 filters: &[Filter],
325 limit: Option<u32>,
326 offset: Option<u32>,
327 ) -> Result<SqlQuery, SqlGeneratorError> {
328 let mut params = Vec::new();
329 let mut param_index = 0;
330
331 let columns = if selection.fields.is_empty() {
333 "*".to_string()
334 } else {
335 selection.fields.iter()
336 .map(|f| self.quote_identifier(&to_snake_case(f)))
337 .collect::<Vec<_>>()
338 .join(", ")
339 };
340
341 let table = self.quote_identifier(&selection.table_name);
343
344 let where_clause = if filters.is_empty() {
346 String::new()
347 } else {
348 let conditions: Vec<String> = filters.iter()
349 .map(|f| {
350 let col = self.quote_identifier(&to_snake_case(&f.field));
351 if f.operator.needs_value() {
352 param_index += 1;
353 params.push(self.prepare_value(&f.operator, &f.value));
354 format!("{} {} {}", col, f.operator.to_sql(), self.placeholder(param_index - 1))
355 } else {
356 format!("{} {}", col, f.operator.to_sql())
357 }
358 })
359 .collect();
360 format!(" WHERE {}", conditions.join(" AND "))
361 };
362
363 let mut limit_offset = String::new();
365 if let Some(l) = limit {
366 limit_offset.push_str(&format!(" LIMIT {}", l));
367 }
368 if let Some(o) = offset {
369 limit_offset.push_str(&format!(" OFFSET {}", o));
370 }
371
372 let sql = format!(
373 "SELECT {} FROM {}{}{}",
374 columns,
375 table,
376 where_clause,
377 limit_offset
378 );
379
380 Ok(SqlQuery {
381 sql,
382 params,
383 table: Some(selection.table_name.clone()),
384 is_count: false,
385 })
386 }
387
388 fn generate_relationship(
390 &self,
391 parent: &Selection,
392 child: &Selection,
393 relation_type: RelationType,
394 join_column: &str,
395 parent_column: &str,
396 ) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
397 match relation_type {
398 RelationType::OneToOne | RelationType::ManyToOne => {
399 Ok(vec![self.generate_with_join(parent, child, join_column, parent_column)?])
401 }
402 RelationType::OneToMany | RelationType::ManyToMany => {
403 Ok(vec![self.generate_with_lateral(parent, child, join_column, parent_column)?])
405 }
406 }
407 }
408
409 fn generate_with_join(
411 &self,
412 parent: &Selection,
413 child: &Selection,
414 join_column: &str,
415 parent_column: &str,
416 ) -> Result<SqlQuery, SqlGeneratorError> {
417 let parent_alias = "p";
418 let child_alias = "c";
419
420 let parent_cols: Vec<String> = if parent.fields.is_empty() {
421 vec![format!("{}.*", parent_alias)]
422 } else {
423 parent.fields.iter()
424 .map(|f| format!("{}.{}", parent_alias, self.quote_identifier(&to_snake_case(f))))
425 .collect()
426 };
427
428 let child_cols: Vec<String> = if child.fields.is_empty() {
429 vec![format!("{}.*", child_alias)]
430 } else {
431 child.fields.iter()
432 .map(|f| format!("{}.{}", child_alias, self.quote_identifier(&to_snake_case(f))))
433 .collect()
434 };
435
436 let all_cols = [parent_cols, child_cols].concat();
437
438 let sql = format!(
439 "SELECT {} FROM {} {} LEFT JOIN {} {} ON {}.{} = {}.{}",
440 all_cols.join(", "),
441 self.quote_identifier(&parent.table_name),
442 parent_alias,
443 self.quote_identifier(&child.table_name),
444 child_alias,
445 child_alias,
446 self.quote_identifier(&to_snake_case(join_column)),
447 parent_alias,
448 self.quote_identifier(&to_snake_case(parent_column))
449 );
450
451 Ok(SqlQuery::new(sql).from_table(&parent.table_name))
452 }
453
454 fn generate_with_lateral(
456 &self,
457 parent: &Selection,
458 child: &Selection,
459 join_column: &str,
460 parent_column: &str,
461 ) -> Result<SqlQuery, SqlGeneratorError> {
462 let parent_alias = "p";
463 let child_alias = "c";
464
465 let parent_cols: Vec<String> = if parent.fields.is_empty() {
466 vec![format!("{}.*", parent_alias)]
467 } else {
468 parent.fields.iter()
469 .map(|f| format!("{}.{}", parent_alias, self.quote_identifier(&to_snake_case(f))))
470 .collect()
471 };
472
473 let child_cols: Vec<String> = if child.fields.is_empty() {
474 vec!["*".to_string()]
475 } else {
476 child.fields.iter()
477 .map(|f| self.quote_identifier(&to_snake_case(f)))
478 .collect()
479 };
480
481 let sql = format!(
482 "SELECT {}, LATERAL (
483 SELECT json_agg(sub.*) FROM (
484 SELECT {} FROM {} {} WHERE {}.{} = {}.{}
485 ) sub
486 ) AS {}
487 FROM {} {}",
488 parent_cols.join(", "),
489 child_cols.join(", "),
490 self.quote_identifier(&child.table_name),
491 child_alias,
492 child_alias,
493 self.quote_identifier(&to_snake_case(join_column)),
494 parent_alias,
495 self.quote_identifier(&to_snake_case(parent_column)),
496 self.quote_identifier(&to_snake_case(&child.table_name)),
497 self.quote_identifier(&parent.table_name),
498 parent_alias
499 );
500
501 Ok(SqlQuery::new(sql).from_table(&parent.table_name))
502 }
503
504 fn generate_batch(&self, queries: &[QueryPlan]) -> Result<Vec<SqlQuery>, SqlGeneratorError> {
506 let mut results = Vec::new();
509 for query in queries {
510 results.extend(self.generate(query)?);
511 }
512 Ok(results)
513 }
514
515 fn quote_identifier(&self, name: &str) -> String {
517 format!("{}{}{}", self.quote_char, name, self.quote_char)
518 }
519
520 fn placeholder(&self, index: usize) -> String {
522 match self.param_style {
523 ParamStyle::Positional => format!("${}", index + 1),
524 ParamStyle::Named => format!(":p{}", index),
525 ParamStyle::QuestionMark => "?".to_string(),
526 }
527 }
528
529 fn prepare_value(&self, operator: &FilterOperator, value: &serde_json::Value) -> serde_json::Value {
531 match operator {
532 FilterOperator::Contains => {
533 if let serde_json::Value::String(s) = value {
534 serde_json::Value::String(format!("%{}%", s))
535 } else {
536 value.clone()
537 }
538 }
539 FilterOperator::StartsWith => {
540 if let serde_json::Value::String(s) = value {
541 serde_json::Value::String(format!("{}%", s))
542 } else {
543 value.clone()
544 }
545 }
546 FilterOperator::EndsWith => {
547 if let serde_json::Value::String(s) = value {
548 serde_json::Value::String(format!("%{}", s))
549 } else {
550 value.clone()
551 }
552 }
553 _ => value.clone(),
554 }
555 }
556
557 pub fn generate_count(&self, table: &str, filters: &[Filter]) -> Result<SqlQuery, SqlGeneratorError> {
559 let mut params = Vec::new();
560 let mut param_index = 0;
561
562 let where_clause = if filters.is_empty() {
563 String::new()
564 } else {
565 let conditions: Vec<String> = filters.iter()
566 .map(|f| {
567 let col = self.quote_identifier(&to_snake_case(&f.field));
568 if f.operator.needs_value() {
569 param_index += 1;
570 params.push(self.prepare_value(&f.operator, &f.value));
571 format!("{} {} {}", col, f.operator.to_sql(), self.placeholder(param_index - 1))
572 } else {
573 format!("{} {}", col, f.operator.to_sql())
574 }
575 })
576 .collect();
577 format!(" WHERE {}", conditions.join(" AND "))
578 };
579
580 let sql = format!(
581 "SELECT COUNT(*) FROM {}{}",
582 self.quote_identifier(table),
583 where_clause
584 );
585
586 Ok(SqlQuery {
587 sql,
588 params,
589 table: Some(table.to_string()),
590 is_count: true,
591 })
592 }
593
594 pub fn generate_insert(
596 &self,
597 table: &str,
598 values: &HashMap<String, serde_json::Value>,
599 ) -> Result<SqlQuery, SqlGeneratorError> {
600 if values.is_empty() {
601 return Err(SqlGeneratorError::EmptyValues);
602 }
603
604 let columns: Vec<String> = values.keys()
605 .map(|k| self.quote_identifier(&to_snake_case(k)))
606 .collect();
607
608 let placeholders: Vec<String> = (0..values.len())
609 .map(|i| self.placeholder(i))
610 .collect();
611
612 let params: Vec<serde_json::Value> = values.values().cloned().collect();
613
614 let sql = format!(
615 "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
616 self.quote_identifier(table),
617 columns.join(", "),
618 placeholders.join(", ")
619 );
620
621 Ok(SqlQuery {
622 sql,
623 params,
624 table: Some(table.to_string()),
625 is_count: false,
626 })
627 }
628
629 pub fn generate_update(
631 &self,
632 table: &str,
633 id: &serde_json::Value,
634 values: &HashMap<String, serde_json::Value>,
635 ) -> Result<SqlQuery, SqlGeneratorError> {
636 if values.is_empty() {
637 return Err(SqlGeneratorError::EmptyValues);
638 }
639
640 let set_clauses: Vec<String> = values.keys()
641 .enumerate()
642 .map(|(i, k)| format!("{} = {}", self.quote_identifier(&to_snake_case(k)), self.placeholder(i)))
643 .collect();
644
645 let mut params: Vec<serde_json::Value> = values.values().cloned().collect();
646 params.push(id.clone());
647
648 let id_placeholder = self.placeholder(params.len() - 1);
649
650 let sql = format!(
651 "UPDATE {} SET {} WHERE {} = {} RETURNING *",
652 self.quote_identifier(table),
653 set_clauses.join(", "),
654 self.quote_identifier("id"),
655 id_placeholder
656 );
657
658 Ok(SqlQuery {
659 sql,
660 params,
661 table: Some(table.to_string()),
662 is_count: false,
663 })
664 }
665
666 pub fn generate_delete(&self, table: &str, id: &serde_json::Value) -> Result<SqlQuery, SqlGeneratorError> {
668 let sql = format!(
669 "DELETE FROM {} WHERE {} = {} RETURNING {}",
670 self.quote_identifier(table),
671 self.quote_identifier("id"),
672 self.placeholder(0),
673 self.quote_identifier("id")
674 );
675
676 Ok(SqlQuery {
677 sql,
678 params: vec![id.clone()],
679 table: Some(table.to_string()),
680 is_count: false,
681 })
682 }
683}
684
685#[derive(Debug, Clone)]
687pub enum SqlGeneratorError {
688 EmptyValues,
690 InvalidFilter(String),
692 UnknownTable(String),
694 InvalidRelationship(String),
696}
697
698impl std::fmt::Display for SqlGeneratorError {
699 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
700 match self {
701 SqlGeneratorError::EmptyValues => write!(f, "No values provided"),
702 SqlGeneratorError::InvalidFilter(msg) => write!(f, "Invalid filter: {}", msg),
703 SqlGeneratorError::UnknownTable(table) => write!(f, "Unknown table: {}", table),
704 SqlGeneratorError::InvalidRelationship(msg) => write!(f, "Invalid relationship: {}", msg),
705 }
706 }
707}
708
709impl std::error::Error for SqlGeneratorError {}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use crate::graphql::introspector::GraphQLSchema;
715
716 fn create_generator() -> SqlGenerator {
717 let schema = Arc::new(GraphQLSchema::new());
718 SqlGenerator::new(schema)
719 }
720
721 #[test]
722 fn test_generate_simple_select() {
723 let generator = create_generator();
724 let selection = Selection::new("users")
725 .field("id")
726 .field("name");
727
728 let plan = QueryPlan::Single {
729 selection,
730 filters: vec![],
731 limit: None,
732 offset: None,
733 };
734
735 let queries = generator.generate(&plan).unwrap();
736 assert_eq!(queries.len(), 1);
737 assert!(queries[0].sql.contains("SELECT"));
738 assert!(queries[0].sql.contains("\"users\""));
739 }
740
741 #[test]
742 fn test_generate_with_filters() {
743 let generator = create_generator();
744 let selection = Selection::new("users");
745
746 let plan = QueryPlan::Single {
747 selection,
748 filters: vec![
749 Filter::eq("id", serde_json::json!("123")),
750 ],
751 limit: None,
752 offset: None,
753 };
754
755 let queries = generator.generate(&plan).unwrap();
756 assert_eq!(queries.len(), 1);
757 assert!(queries[0].sql.contains("WHERE"));
758 assert!(queries[0].sql.contains("$1"));
759 assert_eq!(queries[0].params.len(), 1);
760 }
761
762 #[test]
763 fn test_generate_with_limit_offset() {
764 let generator = create_generator();
765 let selection = Selection::new("users");
766
767 let plan = QueryPlan::Single {
768 selection,
769 filters: vec![],
770 limit: Some(10),
771 offset: Some(20),
772 };
773
774 let queries = generator.generate(&plan).unwrap();
775 assert!(queries[0].sql.contains("LIMIT 10"));
776 assert!(queries[0].sql.contains("OFFSET 20"));
777 }
778
779 #[test]
780 fn test_generate_join() {
781 let generator = create_generator();
782 let parent = Selection::new("users").field("id").field("name");
783 let child = Selection::new("profiles").field("bio");
784
785 let plan = QueryPlan::Relationship {
786 parent,
787 child,
788 relation_type: RelationType::OneToOne,
789 join_column: "user_id".to_string(),
790 parent_column: "id".to_string(),
791 };
792
793 let queries = generator.generate(&plan).unwrap();
794 assert_eq!(queries.len(), 1);
795 assert!(queries[0].sql.contains("LEFT JOIN"));
796 }
797
798 #[test]
799 fn test_generate_lateral() {
800 let generator = create_generator();
801 let parent = Selection::new("users").field("id");
802 let child = Selection::new("posts").field("title");
803
804 let plan = QueryPlan::Relationship {
805 parent,
806 child,
807 relation_type: RelationType::OneToMany,
808 join_column: "user_id".to_string(),
809 parent_column: "id".to_string(),
810 };
811
812 let queries = generator.generate(&plan).unwrap();
813 assert_eq!(queries.len(), 1);
814 assert!(queries[0].sql.contains("LATERAL"));
815 assert!(queries[0].sql.contains("json_agg"));
816 }
817
818 #[test]
819 fn test_generate_count() {
820 let generator = create_generator();
821 let query = generator.generate_count("users", &[]).unwrap();
822
823 assert!(query.sql.contains("COUNT(*)"));
824 assert!(query.is_count);
825 }
826
827 #[test]
828 fn test_generate_insert() {
829 let generator = create_generator();
830 let mut values = HashMap::new();
831 values.insert("name".to_string(), serde_json::json!("John"));
832 values.insert("email".to_string(), serde_json::json!("john@example.com"));
833
834 let query = generator.generate_insert("users", &values).unwrap();
835
836 assert!(query.sql.contains("INSERT INTO"));
837 assert!(query.sql.contains("RETURNING"));
838 assert_eq!(query.params.len(), 2);
839 }
840
841 #[test]
842 fn test_generate_update() {
843 let generator = create_generator();
844 let mut values = HashMap::new();
845 values.insert("name".to_string(), serde_json::json!("Jane"));
846
847 let query = generator.generate_update("users", &serde_json::json!("123"), &values).unwrap();
848
849 assert!(query.sql.contains("UPDATE"));
850 assert!(query.sql.contains("SET"));
851 assert!(query.sql.contains("WHERE"));
852 assert!(query.sql.contains("RETURNING"));
853 }
854
855 #[test]
856 fn test_generate_delete() {
857 let generator = create_generator();
858 let query = generator.generate_delete("users", &serde_json::json!("123")).unwrap();
859
860 assert!(query.sql.contains("DELETE FROM"));
861 assert!(query.sql.contains("WHERE"));
862 assert!(query.sql.contains("RETURNING"));
863 }
864
865 #[test]
866 fn test_filter_operators() {
867 assert_eq!(FilterOperator::Eq.to_sql(), "=");
868 assert_eq!(FilterOperator::Ne.to_sql(), "<>");
869 assert_eq!(FilterOperator::Gt.to_sql(), ">");
870 assert_eq!(FilterOperator::Contains.to_sql(), "LIKE");
871 assert_eq!(FilterOperator::In.to_sql(), "IN");
872 assert_eq!(FilterOperator::IsNull.to_sql(), "IS NULL");
873 }
874
875 #[test]
876 fn test_like_pattern_preparation() {
877 let generator = create_generator();
878
879 let contains = generator.prepare_value(
880 &FilterOperator::Contains,
881 &serde_json::json!("test")
882 );
883 assert_eq!(contains, serde_json::json!("%test%"));
884
885 let starts = generator.prepare_value(
886 &FilterOperator::StartsWith,
887 &serde_json::json!("test")
888 );
889 assert_eq!(starts, serde_json::json!("test%"));
890
891 let ends = generator.prepare_value(
892 &FilterOperator::EndsWith,
893 &serde_json::json!("test")
894 );
895 assert_eq!(ends, serde_json::json!("%test"));
896 }
897
898 #[test]
899 fn test_empty_insert_error() {
900 let generator = create_generator();
901 let result = generator.generate_insert("users", &HashMap::new());
902 assert!(matches!(result, Err(SqlGeneratorError::EmptyValues)));
903 }
904
905 #[test]
906 fn test_param_styles() {
907 let schema = Arc::new(GraphQLSchema::new());
908
909 let positional = SqlGenerator::new(schema.clone());
910 assert_eq!(positional.placeholder(0), "$1");
911 assert_eq!(positional.placeholder(2), "$3");
912
913 let question = SqlGenerator::new(schema.clone()).with_param_style(ParamStyle::QuestionMark);
914 assert_eq!(question.placeholder(0), "?");
915
916 let named = SqlGenerator::new(schema).with_param_style(ParamStyle::Named);
917 assert_eq!(named.placeholder(0), ":p0");
918 }
919}