1use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
2use serde_json::Value as JsonValue;
3use uuid::Uuid;
4
5#[derive(Debug, Clone)]
7pub enum SqlParam {
8 Null,
9 Bool(bool),
10 I16(i16),
11 I32(i32),
12 I64(i64),
13 F32(f32),
14 F64(f64),
15 Text(String),
16 Uuid(Uuid),
17 Timestamp(NaiveDateTime),
18 TimestampTz(chrono::DateTime<chrono::Utc>),
19 Date(NaiveDate),
20 Time(NaiveTime),
21 Json(JsonValue),
22 ByteArray(Vec<u8>),
23 TextArray(Vec<String>),
24 I32Array(Vec<i32>),
25 I64Array(Vec<i64>),
26}
27
28pub trait IntoSqlParam {
30 fn into_sql_param(self) -> SqlParam;
31}
32
33impl IntoSqlParam for SqlParam {
36 fn into_sql_param(self) -> SqlParam {
37 self
38 }
39}
40
41impl IntoSqlParam for bool {
42 fn into_sql_param(self) -> SqlParam {
43 SqlParam::Bool(self)
44 }
45}
46
47impl IntoSqlParam for i16 {
48 fn into_sql_param(self) -> SqlParam {
49 SqlParam::I16(self)
50 }
51}
52
53impl IntoSqlParam for i32 {
54 fn into_sql_param(self) -> SqlParam {
55 SqlParam::I32(self)
56 }
57}
58
59impl IntoSqlParam for i64 {
60 fn into_sql_param(self) -> SqlParam {
61 SqlParam::I64(self)
62 }
63}
64
65impl IntoSqlParam for f32 {
66 fn into_sql_param(self) -> SqlParam {
67 SqlParam::F32(self)
68 }
69}
70
71impl IntoSqlParam for f64 {
72 fn into_sql_param(self) -> SqlParam {
73 SqlParam::F64(self)
74 }
75}
76
77impl IntoSqlParam for String {
78 fn into_sql_param(self) -> SqlParam {
79 SqlParam::Text(self)
80 }
81}
82
83impl IntoSqlParam for &str {
84 fn into_sql_param(self) -> SqlParam {
85 SqlParam::Text(self.to_string())
86 }
87}
88
89impl IntoSqlParam for Uuid {
90 fn into_sql_param(self) -> SqlParam {
91 SqlParam::Uuid(self)
92 }
93}
94
95impl IntoSqlParam for NaiveDateTime {
96 fn into_sql_param(self) -> SqlParam {
97 SqlParam::Timestamp(self)
98 }
99}
100
101impl IntoSqlParam for chrono::DateTime<chrono::Utc> {
102 fn into_sql_param(self) -> SqlParam {
103 SqlParam::TimestampTz(self)
104 }
105}
106
107impl IntoSqlParam for NaiveDate {
108 fn into_sql_param(self) -> SqlParam {
109 SqlParam::Date(self)
110 }
111}
112
113impl IntoSqlParam for NaiveTime {
114 fn into_sql_param(self) -> SqlParam {
115 SqlParam::Time(self)
116 }
117}
118
119impl IntoSqlParam for JsonValue {
120 fn into_sql_param(self) -> SqlParam {
121 SqlParam::Json(self)
122 }
123}
124
125impl IntoSqlParam for Vec<u8> {
126 fn into_sql_param(self) -> SqlParam {
127 SqlParam::ByteArray(self)
128 }
129}
130
131impl IntoSqlParam for Vec<String> {
132 fn into_sql_param(self) -> SqlParam {
133 SqlParam::TextArray(self)
134 }
135}
136
137impl IntoSqlParam for Vec<i32> {
138 fn into_sql_param(self) -> SqlParam {
139 SqlParam::I32Array(self)
140 }
141}
142
143impl IntoSqlParam for Vec<i64> {
144 fn into_sql_param(self) -> SqlParam {
145 SqlParam::I64Array(self)
146 }
147}
148
149impl<T: IntoSqlParam> IntoSqlParam for Option<T> {
150 fn into_sql_param(self) -> SqlParam {
151 match self {
152 Some(v) => v.into_sql_param(),
153 None => SqlParam::Null,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Default)]
160pub struct ParamStore {
161 params: Vec<SqlParam>,
162}
163
164impl ParamStore {
165 pub fn new() -> Self {
166 Self { params: Vec::new() }
167 }
168
169 pub fn push(&mut self, param: SqlParam) -> usize {
171 self.params.push(param);
172 self.params.len()
173 }
174
175 pub fn push_value(&mut self, value: impl IntoSqlParam) -> usize {
177 self.push(value.into_sql_param())
178 }
179
180 pub fn get(&self, index: usize) -> Option<&SqlParam> {
182 self.params.get(index)
183 }
184
185 pub fn params(&self) -> &[SqlParam] {
187 &self.params
188 }
189
190 pub fn into_params(self) -> Vec<SqlParam> {
192 self.params
193 }
194
195 pub fn len(&self) -> usize {
197 self.params.len()
198 }
199
200 pub fn is_empty(&self) -> bool {
201 self.params.is_empty()
202 }
203}
204
205#[derive(Debug, Clone)]
209pub enum FilterCondition {
210 Comparison {
212 column: String,
213 operator: FilterOperator,
214 param_index: usize,
215 },
216 Is {
218 column: String,
219 value: IsValue,
220 },
221 In {
223 column: String,
224 param_indices: Vec<usize>,
225 },
226 Pattern {
228 column: String,
229 operator: PatternOperator,
230 param_index: usize,
231 },
232 TextSearch {
234 column: String,
235 query_param_index: usize,
236 config: Option<String>,
237 search_type: TextSearchType,
238 },
239 ArrayRange {
241 column: String,
242 operator: ArrayRangeOperator,
243 param_index: usize,
244 },
245 Not(Box<FilterCondition>),
247 Or(Vec<FilterCondition>),
249 And(Vec<FilterCondition>),
251 Raw(String),
253 Match {
255 conditions: Vec<(String, usize)>,
256 },
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum FilterOperator {
261 Eq,
262 Neq,
263 Gt,
264 Gte,
265 Lt,
266 Lte,
267}
268
269impl FilterOperator {
270 pub fn as_sql(&self) -> &'static str {
271 match self {
272 Self::Eq => "=",
273 Self::Neq => "!=",
274 Self::Gt => ">",
275 Self::Gte => ">=",
276 Self::Lt => "<",
277 Self::Lte => "<=",
278 }
279 }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum PatternOperator {
284 Like,
285 ILike,
286}
287
288impl PatternOperator {
289 pub fn as_sql(&self) -> &'static str {
290 match self {
291 Self::Like => "LIKE",
292 Self::ILike => "ILIKE",
293 }
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum IsValue {
299 Null,
300 NotNull,
301 True,
302 False,
303}
304
305impl IsValue {
306 pub fn as_sql(&self) -> &'static str {
307 match self {
308 Self::Null => "IS NULL",
309 Self::NotNull => "IS NOT NULL",
310 Self::True => "IS TRUE",
311 Self::False => "IS FALSE",
312 }
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum TextSearchType {
318 Plain,
319 Phrase,
320 Websearch,
321}
322
323impl TextSearchType {
324 pub fn function_name(&self) -> &'static str {
325 match self {
326 Self::Plain => "plainto_tsquery",
327 Self::Phrase => "phraseto_tsquery",
328 Self::Websearch => "websearch_to_tsquery",
329 }
330 }
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum ArrayRangeOperator {
335 Contains,
336 ContainedBy,
337 Overlaps,
338 RangeGt,
339 RangeGte,
340 RangeLt,
341 RangeLte,
342 RangeAdjacent,
343}
344
345impl ArrayRangeOperator {
346 pub fn as_sql(&self) -> &'static str {
347 match self {
348 Self::Contains => "@>",
349 Self::ContainedBy => "<@",
350 Self::Overlaps => "&&",
351 Self::RangeGt => ">>",
352 Self::RangeGte => "&>", Self::RangeLt => "<<",
354 Self::RangeLte => "&<",
355 Self::RangeAdjacent => "-|-",
356 }
357 }
358}
359
360#[derive(Debug, Clone)]
363pub struct OrderClause {
364 pub column: String,
365 pub direction: OrderDirection,
366 pub nulls: Option<NullsPosition>,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum OrderDirection {
371 Ascending,
372 Descending,
373}
374
375impl OrderDirection {
376 pub fn as_sql(&self) -> &'static str {
377 match self {
378 Self::Ascending => "ASC",
379 Self::Descending => "DESC",
380 }
381 }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum NullsPosition {
386 First,
387 Last,
388}
389
390impl NullsPosition {
391 pub fn as_sql(&self) -> &'static str {
392 match self {
393 Self::First => "NULLS FIRST",
394 Self::Last => "NULLS LAST",
395 }
396 }
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum CountOption {
402 None,
404 Exact,
406 Planned,
408 Estimated,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum SqlOperation {
417 Select,
418 Insert,
419 Update,
420 Delete,
421 Upsert,
422}
423
424#[derive(Debug, Clone)]
426pub struct SqlParts {
427 pub operation: SqlOperation,
428 pub schema: String,
429 pub table: String,
430 pub select_columns: Option<String>,
432 pub filters: Vec<FilterCondition>,
434 pub orders: Vec<OrderClause>,
436 pub limit: Option<i64>,
438 pub offset: Option<i64>,
440 pub single: bool,
442 pub maybe_single: bool,
444 pub count: CountOption,
446 pub set_clauses: Vec<(String, usize)>,
448 pub many_rows: Vec<Vec<(String, usize)>>,
450 pub returning: Option<String>,
452 pub conflict_columns: Vec<String>,
454 pub conflict_constraint: Option<String>,
456 pub ignore_duplicates: bool,
458 pub schema_override: Option<String>,
460 pub explain: Option<ExplainOptions>,
462 pub head: bool,
464}
465
466#[derive(Debug, Clone)]
468pub struct ExplainOptions {
469 pub analyze: bool,
470 pub verbose: bool,
471 pub format: ExplainFormat,
472}
473
474impl Default for ExplainOptions {
475 fn default() -> Self {
476 Self {
477 analyze: true,
478 verbose: false,
479 format: ExplainFormat::Json,
480 }
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub enum ExplainFormat {
487 Text,
488 Json,
489 Xml,
490 Yaml,
491}
492
493impl ExplainFormat {
494 pub fn as_sql(&self) -> &'static str {
495 match self {
496 Self::Text => "TEXT",
497 Self::Json => "JSON",
498 Self::Xml => "XML",
499 Self::Yaml => "YAML",
500 }
501 }
502}
503
504impl SqlParts {
505 pub fn new(operation: SqlOperation, schema: impl Into<String>, table: impl Into<String>) -> Self {
506 Self {
507 operation,
508 schema: schema.into(),
509 table: table.into(),
510 select_columns: None,
511 filters: Vec::new(),
512 orders: Vec::new(),
513 limit: None,
514 offset: None,
515 single: false,
516 maybe_single: false,
517 count: CountOption::None,
518 set_clauses: Vec::new(),
519 many_rows: Vec::new(),
520 returning: None,
521 conflict_columns: Vec::new(),
522 conflict_constraint: None,
523 ignore_duplicates: false,
524 schema_override: None,
525 explain: None,
526 head: false,
527 }
528 }
529
530 pub fn qualified_table(&self) -> String {
532 let schema = self.schema_override.as_deref().unwrap_or(&self.schema);
533 format!("\"{}\".\"{}\"", schema, self.table)
534 }
535}
536
537pub fn validate_column_name(name: &str) -> Result<(), supabase_client_core::SupabaseError> {
539 if name.is_empty() {
540 return Err(supabase_client_core::SupabaseError::query_builder(
541 "Column name cannot be empty",
542 ));
543 }
544 if name.contains('"') || name.contains(';') || name.contains("--") {
545 return Err(supabase_client_core::SupabaseError::query_builder(format!(
546 "Invalid column name: {name:?} (contains prohibited characters)"
547 )));
548 }
549 Ok(())
550}
551
552pub fn validate_identifier(name: &str, kind: &str) -> Result<(), supabase_client_core::SupabaseError> {
554 if name.is_empty() {
555 return Err(supabase_client_core::SupabaseError::query_builder(format!(
556 "{kind} name cannot be empty"
557 )));
558 }
559 if name.contains('"') || name.contains(';') || name.contains("--") {
560 return Err(supabase_client_core::SupabaseError::query_builder(format!(
561 "Invalid {kind} name: {name:?} (contains prohibited characters)"
562 )));
563 }
564 Ok(())
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use chrono::{NaiveDate, NaiveTime, Utc};
571 use serde_json::json;
572 use uuid::Uuid;
573
574 #[test]
577 fn test_bool_into_sql_param() {
578 let param = true.into_sql_param();
579 assert!(matches!(param, SqlParam::Bool(true)));
580 let param = false.into_sql_param();
581 assert!(matches!(param, SqlParam::Bool(false)));
582 }
583
584 #[test]
585 fn test_i16_into_sql_param() {
586 let param = 42i16.into_sql_param();
587 assert!(matches!(param, SqlParam::I16(42)));
588 }
589
590 #[test]
591 fn test_i32_into_sql_param() {
592 let param = 100i32.into_sql_param();
593 assert!(matches!(param, SqlParam::I32(100)));
594 }
595
596 #[test]
597 fn test_i64_into_sql_param() {
598 let param = 999_999_999_999i64.into_sql_param();
599 assert!(matches!(param, SqlParam::I64(999_999_999_999)));
600 }
601
602 #[test]
603 fn test_f32_into_sql_param() {
604 let param = 3.14f32.into_sql_param();
605 match param {
606 SqlParam::F32(v) => assert!((v - 3.14).abs() < 0.001),
607 _ => panic!("expected F32"),
608 }
609 }
610
611 #[test]
612 fn test_f64_into_sql_param() {
613 let param = 2.71828f64.into_sql_param();
614 match param {
615 SqlParam::F64(v) => assert!((v - 2.71828).abs() < 0.00001),
616 _ => panic!("expected F64"),
617 }
618 }
619
620 #[test]
621 fn test_string_into_sql_param() {
622 let param = String::from("hello").into_sql_param();
623 match param {
624 SqlParam::Text(s) => assert_eq!(s, "hello"),
625 _ => panic!("expected Text"),
626 }
627 }
628
629 #[test]
630 fn test_str_into_sql_param() {
631 let param = "world".into_sql_param();
632 match param {
633 SqlParam::Text(s) => assert_eq!(s, "world"),
634 _ => panic!("expected Text"),
635 }
636 }
637
638 #[test]
639 fn test_uuid_into_sql_param() {
640 let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
641 let param = uuid.into_sql_param();
642 match param {
643 SqlParam::Uuid(u) => assert_eq!(u.to_string(), "550e8400-e29b-41d4-a716-446655440000"),
644 _ => panic!("expected Uuid"),
645 }
646 }
647
648 #[test]
649 fn test_naive_datetime_into_sql_param() {
650 let dt = NaiveDate::from_ymd_opt(2024, 1, 15)
651 .unwrap()
652 .and_hms_opt(10, 30, 0)
653 .unwrap();
654 let param = dt.into_sql_param();
655 assert!(matches!(param, SqlParam::Timestamp(_)));
656 }
657
658 #[test]
659 fn test_datetime_utc_into_sql_param() {
660 let dt = Utc::now();
661 let param = dt.into_sql_param();
662 assert!(matches!(param, SqlParam::TimestampTz(_)));
663 }
664
665 #[test]
666 fn test_naive_date_into_sql_param() {
667 let d = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
668 let param = d.into_sql_param();
669 assert!(matches!(param, SqlParam::Date(_)));
670 }
671
672 #[test]
673 fn test_naive_time_into_sql_param() {
674 let t = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
675 let param = t.into_sql_param();
676 assert!(matches!(param, SqlParam::Time(_)));
677 }
678
679 #[test]
680 fn test_json_value_into_sql_param() {
681 let val = json!({"key": "value"});
682 let param = val.into_sql_param();
683 assert!(matches!(param, SqlParam::Json(_)));
684 }
685
686 #[test]
687 fn test_vec_u8_into_sql_param() {
688 let bytes = vec![1u8, 2, 3];
689 let param = bytes.into_sql_param();
690 match param {
691 SqlParam::ByteArray(b) => assert_eq!(b, vec![1, 2, 3]),
692 _ => panic!("expected ByteArray"),
693 }
694 }
695
696 #[test]
697 fn test_vec_string_into_sql_param() {
698 let strs = vec!["a".to_string(), "b".to_string()];
699 let param = strs.into_sql_param();
700 match param {
701 SqlParam::TextArray(a) => assert_eq!(a, vec!["a", "b"]),
702 _ => panic!("expected TextArray"),
703 }
704 }
705
706 #[test]
707 fn test_vec_i32_into_sql_param() {
708 let nums = vec![1i32, 2, 3];
709 let param = nums.into_sql_param();
710 match param {
711 SqlParam::I32Array(a) => assert_eq!(a, vec![1, 2, 3]),
712 _ => panic!("expected I32Array"),
713 }
714 }
715
716 #[test]
717 fn test_vec_i64_into_sql_param() {
718 let nums = vec![10i64, 20, 30];
719 let param = nums.into_sql_param();
720 match param {
721 SqlParam::I64Array(a) => assert_eq!(a, vec![10, 20, 30]),
722 _ => panic!("expected I64Array"),
723 }
724 }
725
726 #[test]
727 fn test_option_some_into_sql_param() {
728 let param = Some(42i32).into_sql_param();
729 assert!(matches!(param, SqlParam::I32(42)));
730 }
731
732 #[test]
733 fn test_option_none_into_sql_param() {
734 let param: Option<i32> = None;
735 let param = param.into_sql_param();
736 assert!(matches!(param, SqlParam::Null));
737 }
738
739 #[test]
740 fn test_sql_param_passthrough() {
741 let original = SqlParam::Bool(true);
742 let param = original.into_sql_param();
743 assert!(matches!(param, SqlParam::Bool(true)));
744 }
745
746 #[test]
749 fn test_param_store_new() {
750 let store = ParamStore::new();
751 assert!(store.is_empty());
752 assert_eq!(store.len(), 0);
753 }
754
755 #[test]
756 fn test_param_store_push_returns_1_based_index() {
757 let mut store = ParamStore::new();
758 let idx1 = store.push(SqlParam::I32(1));
759 assert_eq!(idx1, 1);
760 let idx2 = store.push(SqlParam::I32(2));
761 assert_eq!(idx2, 2);
762 let idx3 = store.push(SqlParam::I32(3));
763 assert_eq!(idx3, 3);
764 }
765
766 #[test]
767 fn test_param_store_push_value() {
768 let mut store = ParamStore::new();
769 let idx = store.push_value(42i32);
770 assert_eq!(idx, 1);
771 assert!(matches!(store.get(0), Some(SqlParam::I32(42))));
772 }
773
774 #[test]
775 fn test_param_store_get() {
776 let mut store = ParamStore::new();
777 store.push(SqlParam::Text("hello".to_string()));
778 assert!(store.get(0).is_some());
779 assert!(store.get(1).is_none());
780 }
781
782 #[test]
783 fn test_param_store_params() {
784 let mut store = ParamStore::new();
785 store.push(SqlParam::Bool(true));
786 store.push(SqlParam::I32(42));
787 assert_eq!(store.params().len(), 2);
788 }
789
790 #[test]
791 fn test_param_store_into_params() {
792 let mut store = ParamStore::new();
793 store.push(SqlParam::Bool(true));
794 let params = store.into_params();
795 assert_eq!(params.len(), 1);
796 assert!(matches!(params[0], SqlParam::Bool(true)));
797 }
798
799 #[test]
800 fn test_param_store_len_and_is_empty() {
801 let mut store = ParamStore::new();
802 assert!(store.is_empty());
803 assert_eq!(store.len(), 0);
804 store.push(SqlParam::Null);
805 assert!(!store.is_empty());
806 assert_eq!(store.len(), 1);
807 }
808
809 #[test]
812 fn test_filter_operator_as_sql() {
813 assert_eq!(FilterOperator::Eq.as_sql(), "=");
814 assert_eq!(FilterOperator::Neq.as_sql(), "!=");
815 assert_eq!(FilterOperator::Gt.as_sql(), ">");
816 assert_eq!(FilterOperator::Gte.as_sql(), ">=");
817 assert_eq!(FilterOperator::Lt.as_sql(), "<");
818 assert_eq!(FilterOperator::Lte.as_sql(), "<=");
819 }
820
821 #[test]
824 fn test_pattern_operator_as_sql() {
825 assert_eq!(PatternOperator::Like.as_sql(), "LIKE");
826 assert_eq!(PatternOperator::ILike.as_sql(), "ILIKE");
827 }
828
829 #[test]
832 fn test_is_value_as_sql() {
833 assert_eq!(IsValue::Null.as_sql(), "IS NULL");
834 assert_eq!(IsValue::NotNull.as_sql(), "IS NOT NULL");
835 assert_eq!(IsValue::True.as_sql(), "IS TRUE");
836 assert_eq!(IsValue::False.as_sql(), "IS FALSE");
837 }
838
839 #[test]
842 fn test_text_search_type_function_name() {
843 assert_eq!(TextSearchType::Plain.function_name(), "plainto_tsquery");
844 assert_eq!(TextSearchType::Phrase.function_name(), "phraseto_tsquery");
845 assert_eq!(TextSearchType::Websearch.function_name(), "websearch_to_tsquery");
846 }
847
848 #[test]
851 fn test_array_range_operator_as_sql() {
852 assert_eq!(ArrayRangeOperator::Contains.as_sql(), "@>");
853 assert_eq!(ArrayRangeOperator::ContainedBy.as_sql(), "<@");
854 assert_eq!(ArrayRangeOperator::Overlaps.as_sql(), "&&");
855 assert_eq!(ArrayRangeOperator::RangeGt.as_sql(), ">>");
856 assert_eq!(ArrayRangeOperator::RangeGte.as_sql(), "&>");
857 assert_eq!(ArrayRangeOperator::RangeLt.as_sql(), "<<");
858 assert_eq!(ArrayRangeOperator::RangeLte.as_sql(), "&<");
859 assert_eq!(ArrayRangeOperator::RangeAdjacent.as_sql(), "-|-");
860 }
861
862 #[test]
865 fn test_order_direction_as_sql() {
866 assert_eq!(OrderDirection::Ascending.as_sql(), "ASC");
867 assert_eq!(OrderDirection::Descending.as_sql(), "DESC");
868 }
869
870 #[test]
873 fn test_nulls_position_as_sql() {
874 assert_eq!(NullsPosition::First.as_sql(), "NULLS FIRST");
875 assert_eq!(NullsPosition::Last.as_sql(), "NULLS LAST");
876 }
877
878 #[test]
881 fn test_explain_format_as_sql() {
882 assert_eq!(ExplainFormat::Text.as_sql(), "TEXT");
883 assert_eq!(ExplainFormat::Json.as_sql(), "JSON");
884 assert_eq!(ExplainFormat::Xml.as_sql(), "XML");
885 assert_eq!(ExplainFormat::Yaml.as_sql(), "YAML");
886 }
887
888 #[test]
891 fn test_validate_column_name_valid() {
892 assert!(validate_column_name("name").is_ok());
893 assert!(validate_column_name("user_id").is_ok());
894 assert!(validate_column_name("CamelCase").is_ok());
895 }
896
897 #[test]
898 fn test_validate_column_name_empty() {
899 assert!(validate_column_name("").is_err());
900 }
901
902 #[test]
903 fn test_validate_column_name_with_quotes() {
904 assert!(validate_column_name("col\"name").is_err());
905 }
906
907 #[test]
908 fn test_validate_column_name_with_semicolons() {
909 assert!(validate_column_name("col;DROP TABLE").is_err());
910 }
911
912 #[test]
913 fn test_validate_column_name_with_comment() {
914 assert!(validate_column_name("col--comment").is_err());
915 }
916
917 #[test]
920 fn test_validate_identifier_valid() {
921 assert!(validate_identifier("my_table", "table").is_ok());
922 assert!(validate_identifier("public", "schema").is_ok());
923 }
924
925 #[test]
926 fn test_validate_identifier_empty() {
927 assert!(validate_identifier("", "table").is_err());
928 }
929
930 #[test]
931 fn test_validate_identifier_prohibited_chars() {
932 assert!(validate_identifier("bad\"name", "table").is_err());
933 assert!(validate_identifier("bad;name", "table").is_err());
934 assert!(validate_identifier("bad--name", "table").is_err());
935 }
936
937 #[test]
940 fn test_sql_parts_new_defaults() {
941 let parts = SqlParts::new(SqlOperation::Select, "public", "users");
942 assert_eq!(parts.operation, SqlOperation::Select);
943 assert_eq!(parts.schema, "public");
944 assert_eq!(parts.table, "users");
945 assert!(parts.select_columns.is_none());
946 assert!(parts.filters.is_empty());
947 assert!(parts.orders.is_empty());
948 assert!(parts.limit.is_none());
949 assert!(parts.offset.is_none());
950 assert!(!parts.single);
951 assert!(!parts.maybe_single);
952 assert_eq!(parts.count, CountOption::None);
953 assert!(parts.set_clauses.is_empty());
954 assert!(parts.many_rows.is_empty());
955 assert!(parts.returning.is_none());
956 assert!(parts.conflict_columns.is_empty());
957 assert!(parts.conflict_constraint.is_none());
958 assert!(!parts.ignore_duplicates);
959 assert!(parts.schema_override.is_none());
960 assert!(parts.explain.is_none());
961 assert!(!parts.head);
962 }
963
964 #[test]
965 fn test_sql_parts_qualified_table_no_override() {
966 let parts = SqlParts::new(SqlOperation::Select, "public", "users");
967 assert_eq!(parts.qualified_table(), "\"public\".\"users\"");
968 }
969
970 #[test]
971 fn test_sql_parts_qualified_table_with_override() {
972 let mut parts = SqlParts::new(SqlOperation::Select, "public", "users");
973 parts.schema_override = Some("custom_schema".to_string());
974 assert_eq!(parts.qualified_table(), "\"custom_schema\".\"users\"");
975 }
976
977 #[test]
980 fn test_explain_options_default() {
981 let opts = ExplainOptions::default();
982 assert!(opts.analyze);
983 assert!(!opts.verbose);
984 assert_eq!(opts.format, ExplainFormat::Json);
985 }
986
987 #[test]
990 fn test_count_option_construction() {
991 let _ = CountOption::None;
992 let _ = CountOption::Exact;
993 let _ = CountOption::Planned;
994 let _ = CountOption::Estimated;
995 assert_eq!(CountOption::None, CountOption::None);
997 assert_eq!(CountOption::Exact, CountOption::Exact);
998 assert_eq!(CountOption::Planned, CountOption::Planned);
999 assert_eq!(CountOption::Estimated, CountOption::Estimated);
1000 assert_ne!(CountOption::None, CountOption::Exact);
1001 }
1002}