Skip to main content

supabase_client_query/
sql.rs

1use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
2use serde_json::Value as JsonValue;
3use uuid::Uuid;
4
5/// Type-erased SQL parameter for dynamic query building.
6#[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
28/// Trait for converting Rust types into `SqlParam`.
29pub trait IntoSqlParam {
30    fn into_sql_param(self) -> SqlParam;
31}
32
33// Implementations for all common types
34
35impl 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/// Store for collecting parameters during query building.
159#[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    /// Push a parameter and return its 1-based index (for `$N` placeholders).
170    pub fn push(&mut self, param: SqlParam) -> usize {
171        self.params.push(param);
172        self.params.len()
173    }
174
175    /// Push a value that implements IntoSqlParam.
176    pub fn push_value(&mut self, value: impl IntoSqlParam) -> usize {
177        self.push(value.into_sql_param())
178    }
179
180    /// Get a parameter by 0-based index.
181    pub fn get(&self, index: usize) -> Option<&SqlParam> {
182        self.params.get(index)
183    }
184
185    /// Get all parameters.
186    pub fn params(&self) -> &[SqlParam] {
187        &self.params
188    }
189
190    /// Consume and return all parameters.
191    pub fn into_params(self) -> Vec<SqlParam> {
192        self.params
193    }
194
195    /// Number of parameters stored.
196    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// --- Filter types ---
206
207/// A single filter condition in a WHERE clause.
208#[derive(Debug, Clone)]
209pub enum FilterCondition {
210    /// column op $N (e.g. "name" = $1)
211    Comparison {
212        column: String,
213        operator: FilterOperator,
214        param_index: usize,
215    },
216    /// column IS NULL / IS NOT NULL / IS TRUE / IS FALSE
217    Is {
218        column: String,
219        value: IsValue,
220    },
221    /// column IN ($1, $2, ...)
222    In {
223        column: String,
224        param_indices: Vec<usize>,
225    },
226    /// column LIKE/ILIKE $N
227    Pattern {
228        column: String,
229        operator: PatternOperator,
230        param_index: usize,
231    },
232    /// Full-text search: column @@ to_tsquery(config, $N)
233    TextSearch {
234        column: String,
235        query_param_index: usize,
236        config: Option<String>,
237        search_type: TextSearchType,
238    },
239    /// Array/range operators (e.g. @>, <@, &&)
240    ArrayRange {
241        column: String,
242        operator: ArrayRangeOperator,
243        param_index: usize,
244    },
245    /// NOT (condition)
246    Not(Box<FilterCondition>),
247    /// (condition OR condition OR ...)
248    Or(Vec<FilterCondition>),
249    /// (condition AND condition AND ...) - used inside or_filter
250    And(Vec<FilterCondition>),
251    /// Raw SQL fragment (escape hatch)
252    Raw(String),
253    /// Match multiple column=value conditions (AND)
254    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 => "&>",   // in PostGIS/range context
353            Self::RangeLt => "<<",
354            Self::RangeLte => "&<",
355            Self::RangeAdjacent => "-|-",
356        }
357    }
358}
359
360// --- Order / Modifier types ---
361
362#[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/// Count mode for responses.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum CountOption {
402    /// No count requested.
403    None,
404    /// Exact count via COUNT(*).
405    Exact,
406    /// Planned count from query planner (fast, approximate).
407    Planned,
408    /// Estimated count using statistics (fast, approximate).
409    Estimated,
410}
411
412// --- SQL Parts ---
413
414/// The type of SQL operation.
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum SqlOperation {
417    Select,
418    Insert,
419    Update,
420    Delete,
421    Upsert,
422}
423
424/// Collects all the components of a SQL query being built.
425#[derive(Debug, Clone)]
426pub struct SqlParts {
427    pub operation: SqlOperation,
428    pub schema: String,
429    pub table: String,
430    /// Columns to select (None = *)
431    pub select_columns: Option<String>,
432    /// Filter conditions (WHERE)
433    pub filters: Vec<FilterCondition>,
434    /// ORDER BY clauses
435    pub orders: Vec<OrderClause>,
436    /// LIMIT
437    pub limit: Option<i64>,
438    /// OFFSET (from range)
439    pub offset: Option<i64>,
440    /// Whether to return a single row (enforced at execution)
441    pub single: bool,
442    /// Whether to return zero or one row
443    pub maybe_single: bool,
444    /// Count option
445    pub count: CountOption,
446    /// Insert/Update column-value pairs: Vec<(column, param_index)>
447    pub set_clauses: Vec<(String, usize)>,
448    /// For insert_many/upsert_many: Vec of rows, each is Vec<(column, param_index)>
449    pub many_rows: Vec<Vec<(String, usize)>>,
450    /// RETURNING columns (None = don't return, Some("*") = all)
451    pub returning: Option<String>,
452    /// ON CONFLICT columns (for upsert)
453    pub conflict_columns: Vec<String>,
454    /// ON CONFLICT constraint name (alternative to columns)
455    pub conflict_constraint: Option<String>,
456    /// When true, upsert generates ON CONFLICT DO NOTHING instead of DO UPDATE
457    pub ignore_duplicates: bool,
458    /// Schema override for per-query schema qualification
459    pub schema_override: Option<String>,
460    /// EXPLAIN options (only for SELECT)
461    pub explain: Option<ExplainOptions>,
462    /// Head mode: SELECT count(*) only, no rows
463    pub head: bool,
464}
465
466/// Options for the EXPLAIN modifier.
467#[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/// Output format for EXPLAIN.
485#[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    /// Get the fully-qualified table name, using schema_override if set.
531    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
537/// Validate that a column name is safe (no SQL injection).
538pub 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
552/// Validate a table or schema name.
553pub 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    // ---- IntoSqlParam conversions ----
575
576    #[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    // ---- ParamStore ----
747
748    #[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    // ---- FilterOperator ----
810
811    #[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    // ---- PatternOperator ----
822
823    #[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    // ---- IsValue ----
830
831    #[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    // ---- TextSearchType ----
840
841    #[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    // ---- ArrayRangeOperator ----
849
850    #[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    // ---- OrderDirection ----
863
864    #[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    // ---- NullsPosition ----
871
872    #[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    // ---- ExplainFormat ----
879
880    #[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    // ---- validate_column_name ----
889
890    #[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    // ---- validate_identifier ----
918
919    #[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    // ---- SqlParts ----
938
939    #[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    // ---- ExplainOptions ----
978
979    #[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    // ---- CountOption ----
988
989    #[test]
990    fn test_count_option_construction() {
991        let _ = CountOption::None;
992        let _ = CountOption::Exact;
993        let _ = CountOption::Planned;
994        let _ = CountOption::Estimated;
995        // Verify equality
996        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}