Skip to main content

prax_query/
zero_copy.rs

1//! Zero-copy types for performance-critical operations.
2//!
3//! This module provides borrowed/reference-based versions of common types
4//! to avoid unnecessary allocations in hot paths.
5//!
6//! # Types
7//!
8//! | Owned Type | Zero-Copy Type | Use Case |
9//! |------------|----------------|----------|
10//! | `JsonPath` | `JsonPathRef<'a>` | JSON path queries with borrowed strings |
11//! | `WindowSpec` | `WindowSpecRef<'a>` | Window function specs with borrowed columns |
12//! | `Cte` | `CteRef<'a>` | CTE definitions with borrowed column slices |
13//!
14//! # Performance Benefits
15//!
16//! - Avoids `String` allocations when using string literals or borrowed data
17//! - Reduces memory copies in query building hot paths
18//! - Enables zero-copy deserialization patterns
19//!
20//! # Example
21//!
22//! ```rust
23//! use prax_query::zero_copy::{JsonPathRef, PathSegmentRef, WindowSpecRef};
24//! use prax_query::sql::DatabaseType;
25//!
26//! // Zero-copy JSON path - no allocations!
27//! let path = JsonPathRef::new("metadata")
28//!     .field("role")
29//!     .field("permissions");
30//!
31//! let sql = path.to_sql(DatabaseType::PostgreSQL);
32//!
33//! // Zero-copy window spec
34//! let spec = WindowSpecRef::new()
35//!     .partition_by(&["dept", "team"])
36//!     .order_by_asc("salary");
37//!
38//! let sql = spec.to_sql(DatabaseType::PostgreSQL);
39//! ```
40
41use smallvec::SmallVec;
42use std::borrow::Cow;
43
44use crate::sql::DatabaseType;
45use crate::types::SortOrder;
46
47// ==============================================================================
48// Zero-Copy JSON Path
49// ==============================================================================
50
51/// A zero-copy JSON path expression that borrows strings where possible.
52///
53/// This is a more efficient alternative to `JsonPath` when you're working
54/// with string literals or borrowed data and don't need to store the path.
55///
56/// # Example
57///
58/// ```rust
59/// use prax_query::zero_copy::{JsonPathRef, PathSegmentRef};
60/// use prax_query::sql::DatabaseType;
61///
62/// // All string data is borrowed - no allocations
63/// let path = JsonPathRef::new("data")
64///     .field("user")
65///     .field("profile")
66///     .index(0);
67///
68/// // Generate SQL without owning the strings
69/// let sql = path.to_sql(DatabaseType::PostgreSQL);
70/// assert!(sql.contains("data"));
71/// ```
72#[derive(Debug, Clone)]
73pub struct JsonPathRef<'a> {
74    /// The column name containing JSON (borrowed).
75    pub column: Cow<'a, str>,
76    /// Path segments (may contain borrowed or owned strings).
77    pub segments: SmallVec<[PathSegmentRef<'a>; 8]>,
78    /// Whether to return text (::text in PostgreSQL).
79    pub as_text: bool,
80}
81
82/// A segment in a zero-copy JSON path.
83#[derive(Debug, Clone, PartialEq)]
84pub enum PathSegmentRef<'a> {
85    /// Field access with borrowed name.
86    Field(Cow<'a, str>),
87    /// Array index access.
88    Index(i64),
89    /// Array wildcard.
90    Wildcard,
91    /// Recursive descent.
92    RecursiveDescent,
93}
94
95impl<'a> JsonPathRef<'a> {
96    /// Create a new JSON path from a borrowed column name.
97    #[inline]
98    pub fn new(column: &'a str) -> Self {
99        Self {
100            column: Cow::Borrowed(column),
101            segments: SmallVec::new(),
102            as_text: false,
103        }
104    }
105
106    /// Create a new JSON path from an owned column name.
107    #[inline]
108    pub fn owned(column: String) -> Self {
109        Self {
110            column: Cow::Owned(column),
111            segments: SmallVec::new(),
112            as_text: false,
113        }
114    }
115
116    /// Create from a JSONPath string (e.g., "$.user.name").
117    ///
118    /// Note: This may allocate for parsed field names.
119    pub fn from_path(column: &'a str, path: &str) -> Self {
120        let mut json_path = Self::new(column);
121
122        let path = path.trim_start_matches('$').trim_start_matches('.');
123
124        for segment in path.split('.') {
125            if segment.is_empty() {
126                continue;
127            }
128
129            if let Some(bracket_pos) = segment.find('[') {
130                let field_name = &segment[..bracket_pos];
131                if !field_name.is_empty() {
132                    json_path
133                        .segments
134                        .push(PathSegmentRef::Field(Cow::Owned(field_name.to_string())));
135                }
136
137                if let Some(end_pos) = segment.find(']') {
138                    let idx_str = &segment[bracket_pos + 1..end_pos];
139                    if idx_str == "*" {
140                        json_path.segments.push(PathSegmentRef::Wildcard);
141                    } else if let Ok(i) = idx_str.parse::<i64>() {
142                        json_path.segments.push(PathSegmentRef::Index(i));
143                    }
144                }
145            } else {
146                json_path
147                    .segments
148                    .push(PathSegmentRef::Field(Cow::Owned(segment.to_string())));
149            }
150        }
151
152        json_path
153    }
154
155    /// Add a field access segment (borrowed).
156    #[inline]
157    pub fn field(mut self, name: &'a str) -> Self {
158        self.segments
159            .push(PathSegmentRef::Field(Cow::Borrowed(name)));
160        self
161    }
162
163    /// Add a field access segment (owned).
164    #[inline]
165    pub fn field_owned(mut self, name: String) -> Self {
166        self.segments.push(PathSegmentRef::Field(Cow::Owned(name)));
167        self
168    }
169
170    /// Add an array index segment.
171    #[inline]
172    pub fn index(mut self, idx: i64) -> Self {
173        self.segments.push(PathSegmentRef::Index(idx));
174        self
175    }
176
177    /// Add an array wildcard segment.
178    #[inline]
179    pub fn all(mut self) -> Self {
180        self.segments.push(PathSegmentRef::Wildcard);
181        self
182    }
183
184    /// Return the value as text instead of JSON.
185    #[inline]
186    pub fn text(mut self) -> Self {
187        self.as_text = true;
188        self
189    }
190
191    /// Check if this path uses only borrowed data (no allocations).
192    pub fn is_zero_copy(&self) -> bool {
193        matches!(self.column, Cow::Borrowed(_))
194            && self.segments.iter().all(|s| match s {
195                PathSegmentRef::Field(cow) => matches!(cow, Cow::Borrowed(_)),
196                _ => true,
197            })
198    }
199
200    /// Get the number of segments.
201    #[inline]
202    pub fn depth(&self) -> usize {
203        self.segments.len()
204    }
205
206    /// Generate SQL for this path.
207    pub fn to_sql(&self, db_type: DatabaseType) -> String {
208        match db_type {
209            DatabaseType::PostgreSQL => self.to_postgres_sql(),
210            DatabaseType::MySQL => self.to_mysql_sql(),
211            DatabaseType::SQLite => self.to_sqlite_sql(),
212            DatabaseType::MSSQL => self.to_mssql_sql(),
213        }
214    }
215
216    fn to_postgres_sql(&self) -> String {
217        let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
218        sql.push_str(&self.column);
219
220        let last_idx = self.segments.len().saturating_sub(1);
221        for (i, segment) in self.segments.iter().enumerate() {
222            match segment {
223                PathSegmentRef::Field(name) => {
224                    if self.as_text && i == last_idx {
225                        sql.push_str(" ->> '");
226                    } else {
227                        sql.push_str(" -> '");
228                    }
229                    sql.push_str(name);
230                    sql.push('\'');
231                }
232                PathSegmentRef::Index(idx) => {
233                    if self.as_text && i == last_idx {
234                        sql.push_str(" ->> ");
235                    } else {
236                        sql.push_str(" -> ");
237                    }
238                    sql.push_str(&idx.to_string());
239                }
240                PathSegmentRef::Wildcard => {
241                    sql.push_str(" -> '*'");
242                }
243                PathSegmentRef::RecursiveDescent => {
244                    // PostgreSQL doesn't have native recursive descent
245                    sql.push_str(" #> '{}'");
246                }
247            }
248        }
249
250        sql
251    }
252
253    fn to_mysql_sql(&self) -> String {
254        let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
255        sql.push_str(&self.column);
256
257        let last_idx = self.segments.len().saturating_sub(1);
258        for (i, segment) in self.segments.iter().enumerate() {
259            match segment {
260                PathSegmentRef::Field(name) => {
261                    if self.as_text && i == last_idx {
262                        sql.push_str(" ->> '$.");
263                    } else {
264                        sql.push_str(" -> '$.");
265                    }
266                    sql.push_str(name);
267                    sql.push('\'');
268                }
269                PathSegmentRef::Index(idx) => {
270                    sql.push_str(" -> '$[");
271                    sql.push_str(&idx.to_string());
272                    sql.push_str("]'");
273                }
274                PathSegmentRef::Wildcard => {
275                    sql.push_str(" -> '$[*]'");
276                }
277                PathSegmentRef::RecursiveDescent => {
278                    sql.push_str(" -> '$**'");
279                }
280            }
281        }
282
283        sql
284    }
285
286    fn to_sqlite_sql(&self) -> String {
287        // SQLite uses json_extract function
288        let mut path = String::from("$");
289        for segment in &self.segments {
290            match segment {
291                PathSegmentRef::Field(name) => {
292                    path.push('.');
293                    path.push_str(name);
294                }
295                PathSegmentRef::Index(idx) => {
296                    path.push('[');
297                    path.push_str(&idx.to_string());
298                    path.push(']');
299                }
300                PathSegmentRef::Wildcard => {
301                    path.push_str("[*]");
302                }
303                PathSegmentRef::RecursiveDescent => {
304                    path.push_str("..");
305                }
306            }
307        }
308
309        format!("json_extract({}, '{}')", self.column, path)
310    }
311
312    fn to_mssql_sql(&self) -> String {
313        // MSSQL uses JSON_VALUE or JSON_QUERY
314        let mut path = String::from("$");
315        for segment in &self.segments {
316            match segment {
317                PathSegmentRef::Field(name) => {
318                    path.push('.');
319                    path.push_str(name);
320                }
321                PathSegmentRef::Index(idx) => {
322                    path.push('[');
323                    path.push_str(&idx.to_string());
324                    path.push(']');
325                }
326                PathSegmentRef::Wildcard | PathSegmentRef::RecursiveDescent => {
327                    // MSSQL doesn't support wildcards directly
328                    path.push_str("[0]");
329                }
330            }
331        }
332
333        if self.as_text {
334            format!("JSON_VALUE({}, '{}')", self.column, path)
335        } else {
336            format!("JSON_QUERY({}, '{}')", self.column, path)
337        }
338    }
339
340    /// Convert to owned JsonPath.
341    pub fn to_owned(&self) -> crate::json::JsonPath {
342        crate::json::JsonPath {
343            column: self.column.to_string(),
344            segments: self
345                .segments
346                .iter()
347                .map(|s| match s {
348                    PathSegmentRef::Field(cow) => crate::json::PathSegment::Field(cow.to_string()),
349                    PathSegmentRef::Index(i) => crate::json::PathSegment::Index(*i),
350                    PathSegmentRef::Wildcard => crate::json::PathSegment::Wildcard,
351                    PathSegmentRef::RecursiveDescent => crate::json::PathSegment::RecursiveDescent,
352                })
353                .collect(),
354            as_text: self.as_text,
355        }
356    }
357}
358
359impl<'a> From<&'a crate::json::JsonPath> for JsonPathRef<'a> {
360    fn from(path: &'a crate::json::JsonPath) -> Self {
361        Self {
362            column: Cow::Borrowed(&path.column),
363            segments: path
364                .segments
365                .iter()
366                .map(|s| match s {
367                    crate::json::PathSegment::Field(name) => {
368                        PathSegmentRef::Field(Cow::Borrowed(name))
369                    }
370                    crate::json::PathSegment::Index(i) => PathSegmentRef::Index(*i),
371                    crate::json::PathSegment::Wildcard => PathSegmentRef::Wildcard,
372                    crate::json::PathSegment::RecursiveDescent => PathSegmentRef::RecursiveDescent,
373                })
374                .collect(),
375            as_text: path.as_text,
376        }
377    }
378}
379
380// ==============================================================================
381// Zero-Copy Window Spec
382// ==============================================================================
383
384/// A zero-copy window specification using borrowed column references.
385///
386/// This is more efficient than `WindowSpec` when working with string literals.
387///
388/// # Example
389///
390/// ```rust
391/// use prax_query::zero_copy::WindowSpecRef;
392/// use prax_query::sql::DatabaseType;
393///
394/// // All column names are borrowed - no allocations
395/// let spec = WindowSpecRef::new()
396///     .partition_by(&["dept", "team"])
397///     .order_by_asc("salary")
398///     .rows_unbounded_preceding();
399///
400/// let sql = spec.to_sql(DatabaseType::PostgreSQL);
401/// ```
402#[derive(Debug, Clone, Default)]
403pub struct WindowSpecRef<'a> {
404    /// Partition by columns (borrowed slice or owned vec).
405    pub partition_by: SmallVec<[Cow<'a, str>; 4]>,
406    /// Order by columns with direction.
407    pub order_by: SmallVec<[(Cow<'a, str>, SortOrder); 4]>,
408    /// Frame clause.
409    pub frame: Option<FrameRef<'a>>,
410    /// Reference to named window.
411    pub window_ref: Option<Cow<'a, str>>,
412}
413
414/// A frame clause for window specifications.
415#[derive(Debug, Clone)]
416pub struct FrameRef<'a> {
417    /// Frame type (ROWS, RANGE, GROUPS).
418    pub frame_type: FrameTypeRef,
419    /// Start bound.
420    pub start: FrameBoundRef<'a>,
421    /// End bound.
422    pub end: Option<FrameBoundRef<'a>>,
423}
424
425/// Frame type for window functions.
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum FrameTypeRef {
428    Rows,
429    Range,
430    Groups,
431}
432
433/// Frame bound specification.
434#[derive(Debug, Clone, PartialEq)]
435pub enum FrameBoundRef<'a> {
436    UnboundedPreceding,
437    Preceding(u32),
438    CurrentRow,
439    Following(u32),
440    UnboundedFollowing,
441    /// Expression-based bound (may be borrowed or owned).
442    Expr(Cow<'a, str>),
443}
444
445impl<'a> WindowSpecRef<'a> {
446    /// Create a new empty window spec.
447    #[inline]
448    pub fn new() -> Self {
449        Self::default()
450    }
451
452    /// Add partition by columns from a slice (zero-copy).
453    #[inline]
454    pub fn partition_by(mut self, columns: &[&'a str]) -> Self {
455        self.partition_by
456            .extend(columns.iter().map(|&s| Cow::Borrowed(s)));
457        self
458    }
459
460    /// Add partition by columns (owned).
461    #[inline]
462    pub fn partition_by_owned<I, S>(mut self, columns: I) -> Self
463    where
464        I: IntoIterator<Item = S>,
465        S: Into<String>,
466    {
467        self.partition_by
468            .extend(columns.into_iter().map(|s| Cow::Owned(s.into())));
469        self
470    }
471
472    /// Add a single partition column (borrowed).
473    #[inline]
474    pub fn partition_by_col(mut self, column: &'a str) -> Self {
475        self.partition_by.push(Cow::Borrowed(column));
476        self
477    }
478
479    /// Add order by column ascending (borrowed).
480    #[inline]
481    pub fn order_by_asc(mut self, column: &'a str) -> Self {
482        self.order_by.push((Cow::Borrowed(column), SortOrder::Asc));
483        self
484    }
485
486    /// Add order by column descending (borrowed).
487    #[inline]
488    pub fn order_by_desc(mut self, column: &'a str) -> Self {
489        self.order_by.push((Cow::Borrowed(column), SortOrder::Desc));
490        self
491    }
492
493    /// Add order by column with direction (borrowed).
494    #[inline]
495    pub fn order_by(mut self, column: &'a str, order: SortOrder) -> Self {
496        self.order_by.push((Cow::Borrowed(column), order));
497        self
498    }
499
500    /// Add order by column (owned).
501    #[inline]
502    pub fn order_by_owned(mut self, column: String, order: SortOrder) -> Self {
503        self.order_by.push((Cow::Owned(column), order));
504        self
505    }
506
507    /// Set ROWS frame.
508    #[inline]
509    pub fn rows(mut self, start: FrameBoundRef<'a>, end: Option<FrameBoundRef<'a>>) -> Self {
510        self.frame = Some(FrameRef {
511            frame_type: FrameTypeRef::Rows,
512            start,
513            end,
514        });
515        self
516    }
517
518    /// Set RANGE frame.
519    #[inline]
520    pub fn range(mut self, start: FrameBoundRef<'a>, end: Option<FrameBoundRef<'a>>) -> Self {
521        self.frame = Some(FrameRef {
522            frame_type: FrameTypeRef::Range,
523            start,
524            end,
525        });
526        self
527    }
528
529    /// Set ROWS UNBOUNDED PRECEDING frame (common for running totals).
530    #[inline]
531    pub fn rows_unbounded_preceding(self) -> Self {
532        self.rows(
533            FrameBoundRef::UnboundedPreceding,
534            Some(FrameBoundRef::CurrentRow),
535        )
536    }
537
538    /// Set reference to named window.
539    #[inline]
540    pub fn window_name(mut self, name: &'a str) -> Self {
541        self.window_ref = Some(Cow::Borrowed(name));
542        self
543    }
544
545    /// Check if this spec uses only borrowed data.
546    pub fn is_zero_copy(&self) -> bool {
547        self.partition_by
548            .iter()
549            .all(|c| matches!(c, Cow::Borrowed(_)))
550            && self
551                .order_by
552                .iter()
553                .all(|(c, _)| matches!(c, Cow::Borrowed(_)))
554            && self
555                .window_ref
556                .as_ref()
557                .map(|w| matches!(w, Cow::Borrowed(_)))
558                .unwrap_or(true)
559    }
560
561    /// Generate SQL for the OVER clause.
562    pub fn to_sql(&self, _db_type: DatabaseType) -> String {
563        // Window reference
564        if let Some(ref name) = self.window_ref {
565            return format!("OVER {}", name);
566        }
567
568        let mut parts: SmallVec<[String; 4]> = SmallVec::new();
569
570        // PARTITION BY
571        if !self.partition_by.is_empty() {
572            let cols: Vec<&str> = self.partition_by.iter().map(|c| c.as_ref()).collect();
573            parts.push(format!("PARTITION BY {}", cols.join(", ")));
574        }
575
576        // ORDER BY
577        if !self.order_by.is_empty() {
578            let cols: Vec<String> = self
579                .order_by
580                .iter()
581                .map(|(col, order)| {
582                    format!(
583                        "{} {}",
584                        col,
585                        match order {
586                            SortOrder::Asc => "ASC",
587                            SortOrder::Desc => "DESC",
588                        }
589                    )
590                })
591                .collect();
592            parts.push(format!("ORDER BY {}", cols.join(", ")));
593        }
594
595        // Frame
596        if let Some(ref frame) = self.frame {
597            let frame_type = match frame.frame_type {
598                FrameTypeRef::Rows => "ROWS",
599                FrameTypeRef::Range => "RANGE",
600                FrameTypeRef::Groups => "GROUPS",
601            };
602
603            let start = frame_bound_to_sql(&frame.start);
604
605            if let Some(ref end) = frame.end {
606                let end_sql = frame_bound_to_sql(end);
607                parts.push(format!("{} BETWEEN {} AND {}", frame_type, start, end_sql));
608            } else {
609                parts.push(format!("{} {}", frame_type, start));
610            }
611        }
612
613        if parts.is_empty() {
614            "OVER ()".to_string()
615        } else {
616            format!("OVER ({})", parts.join(" "))
617        }
618    }
619}
620
621fn frame_bound_to_sql(bound: &FrameBoundRef<'_>) -> String {
622    match bound {
623        FrameBoundRef::UnboundedPreceding => "UNBOUNDED PRECEDING".to_string(),
624        FrameBoundRef::Preceding(n) => format!("{} PRECEDING", n),
625        FrameBoundRef::CurrentRow => "CURRENT ROW".to_string(),
626        FrameBoundRef::Following(n) => format!("{} FOLLOWING", n),
627        FrameBoundRef::UnboundedFollowing => "UNBOUNDED FOLLOWING".to_string(),
628        FrameBoundRef::Expr(expr) => expr.to_string(),
629    }
630}
631
632// ==============================================================================
633// Zero-Copy CTE
634// ==============================================================================
635
636/// A zero-copy CTE definition that accepts column slices.
637///
638/// This is more efficient than `Cte` when working with static column lists.
639///
640/// # Example
641///
642/// ```rust
643/// use prax_query::zero_copy::CteRef;
644/// use prax_query::sql::DatabaseType;
645///
646/// // Column names are borrowed from a static slice
647/// let cte = CteRef::new("active_users")
648///     .columns(&["id", "name", "email"])
649///     .query("SELECT id, name, email FROM users WHERE active = true");
650///
651/// let sql = cte.to_sql(DatabaseType::PostgreSQL);
652/// ```
653#[derive(Debug, Clone, Default)]
654pub struct CteRef<'a> {
655    /// CTE name.
656    pub name: Cow<'a, str>,
657    /// Column aliases (borrowed from slice).
658    pub columns: SmallVec<[Cow<'a, str>; 8]>,
659    /// The defining query.
660    pub query: Cow<'a, str>,
661    /// Whether this is recursive.
662    pub recursive: bool,
663    /// Materialization hint (PostgreSQL).
664    pub materialized: Option<bool>,
665}
666
667impl<'a> CteRef<'a> {
668    /// Create a new CTE with a borrowed name.
669    #[inline]
670    pub fn new(name: &'a str) -> Self {
671        Self {
672            name: Cow::Borrowed(name),
673            columns: SmallVec::new(),
674            query: Cow::Borrowed(""),
675            recursive: false,
676            materialized: None,
677        }
678    }
679
680    /// Create a new CTE with an owned name.
681    #[inline]
682    pub fn owned(name: String) -> Self {
683        Self {
684            name: Cow::Owned(name),
685            columns: SmallVec::new(),
686            query: Cow::Borrowed(""),
687            recursive: false,
688            materialized: None,
689        }
690    }
691
692    /// Set column aliases from a slice (zero-copy).
693    #[inline]
694    pub fn columns(mut self, cols: &[&'a str]) -> Self {
695        self.columns.clear();
696        self.columns.extend(cols.iter().map(|&s| Cow::Borrowed(s)));
697        self
698    }
699
700    /// Set column aliases (owned).
701    #[inline]
702    pub fn columns_owned<I, S>(mut self, cols: I) -> Self
703    where
704        I: IntoIterator<Item = S>,
705        S: Into<String>,
706    {
707        self.columns.clear();
708        self.columns
709            .extend(cols.into_iter().map(|s| Cow::Owned(s.into())));
710        self
711    }
712
713    /// Add a single column (borrowed).
714    #[inline]
715    pub fn column(mut self, col: &'a str) -> Self {
716        self.columns.push(Cow::Borrowed(col));
717        self
718    }
719
720    /// Set the defining query (borrowed).
721    #[inline]
722    pub fn query(mut self, q: &'a str) -> Self {
723        self.query = Cow::Borrowed(q);
724        self
725    }
726
727    /// Set the defining query (owned).
728    #[inline]
729    pub fn query_owned(mut self, q: String) -> Self {
730        self.query = Cow::Owned(q);
731        self
732    }
733
734    /// Mark as recursive CTE.
735    #[inline]
736    pub fn recursive(mut self) -> Self {
737        self.recursive = true;
738        self
739    }
740
741    /// Set materialization hint (PostgreSQL).
742    #[inline]
743    pub fn materialized(mut self, mat: bool) -> Self {
744        self.materialized = Some(mat);
745        self
746    }
747
748    /// Check if this CTE uses only borrowed data.
749    pub fn is_zero_copy(&self) -> bool {
750        matches!(self.name, Cow::Borrowed(_))
751            && matches!(self.query, Cow::Borrowed(_))
752            && self.columns.iter().all(|c| matches!(c, Cow::Borrowed(_)))
753    }
754
755    /// Generate the CTE definition SQL.
756    pub fn to_sql(&self, db_type: DatabaseType) -> String {
757        let mut sql = String::with_capacity(64 + self.query.len());
758
759        sql.push_str(&self.name);
760
761        // Column list
762        if !self.columns.is_empty() {
763            sql.push_str(" (");
764            let cols: Vec<&str> = self.columns.iter().map(|c| c.as_ref()).collect();
765            sql.push_str(&cols.join(", "));
766            sql.push(')');
767        }
768
769        sql.push_str(" AS ");
770
771        // Materialization hint (PostgreSQL only)
772        if matches!(db_type, DatabaseType::PostgreSQL) {
773            if let Some(mat) = self.materialized {
774                if mat {
775                    sql.push_str("MATERIALIZED ");
776                } else {
777                    sql.push_str("NOT MATERIALIZED ");
778                }
779            }
780        }
781
782        sql.push('(');
783        sql.push_str(&self.query);
784        sql.push(')');
785
786        sql
787    }
788
789    /// Convert to owned Cte.
790    pub fn to_owned_cte(&self) -> crate::cte::Cte {
791        crate::cte::Cte {
792            name: self.name.to_string(),
793            columns: self.columns.iter().map(|c| c.to_string()).collect(),
794            query: self.query.to_string(),
795            recursive: self.recursive,
796            materialized: self.materialized.map(|m| {
797                if m {
798                    crate::cte::Materialized::Yes
799                } else {
800                    crate::cte::Materialized::No
801                }
802            }),
803            search: None,
804            cycle: None,
805        }
806    }
807}
808
809// ==============================================================================
810// Zero-Copy WITH Clause Builder
811// ==============================================================================
812
813/// A builder for WITH clauses using zero-copy CTEs.
814///
815/// # Example
816///
817/// ```rust
818/// use prax_query::zero_copy::{CteRef, WithClauseRef};
819/// use prax_query::sql::DatabaseType;
820///
821/// let with = WithClauseRef::new()
822///     .cte(CteRef::new("active_users")
823///         .columns(&["id", "name"])
824///         .query("SELECT id, name FROM users WHERE active = true"))
825///     .cte(CteRef::new("recent_orders")
826///         .columns(&["user_id", "total"])
827///         .query("SELECT user_id, SUM(amount) FROM orders GROUP BY user_id"));
828///
829/// let sql = with.build_select(&["*"], "active_users", DatabaseType::PostgreSQL);
830/// ```
831#[derive(Debug, Clone, Default)]
832pub struct WithClauseRef<'a> {
833    /// CTEs in this WITH clause.
834    pub ctes: SmallVec<[CteRef<'a>; 4]>,
835    /// Whether this is a recursive WITH.
836    pub recursive: bool,
837}
838
839impl<'a> WithClauseRef<'a> {
840    /// Create a new empty WITH clause.
841    #[inline]
842    pub fn new() -> Self {
843        Self::default()
844    }
845
846    /// Add a CTE.
847    #[inline]
848    pub fn cte(mut self, cte: CteRef<'a>) -> Self {
849        if cte.recursive {
850            self.recursive = true;
851        }
852        self.ctes.push(cte);
853        self
854    }
855
856    /// Mark as recursive.
857    #[inline]
858    pub fn recursive(mut self) -> Self {
859        self.recursive = true;
860        self
861    }
862
863    /// Build the WITH clause SQL.
864    pub fn to_sql(&self, db_type: DatabaseType) -> String {
865        if self.ctes.is_empty() {
866            return String::new();
867        }
868
869        let mut sql = String::with_capacity(256);
870
871        sql.push_str("WITH ");
872        if self.recursive {
873            sql.push_str("RECURSIVE ");
874        }
875
876        let cte_sqls: Vec<String> = self.ctes.iter().map(|c| c.to_sql(db_type)).collect();
877        sql.push_str(&cte_sqls.join(", "));
878
879        sql
880    }
881
882    /// Build a complete SELECT query with this WITH clause.
883    pub fn build_select(&self, columns: &[&str], from: &str, db_type: DatabaseType) -> String {
884        let with_sql = self.to_sql(db_type);
885        let cols = if columns.is_empty() || columns == ["*"] {
886            "*".to_string()
887        } else {
888            columns.join(", ")
889        };
890
891        if with_sql.is_empty() {
892            format!("SELECT {} FROM {}", cols, from)
893        } else {
894            format!("{} SELECT {} FROM {}", with_sql, cols, from)
895        }
896    }
897}
898
899// ==============================================================================
900// Tests
901// ==============================================================================
902
903#[cfg(test)]
904mod tests {
905    use super::*;
906
907    #[test]
908    fn test_json_path_ref_zero_copy() {
909        let path = JsonPathRef::new("data").field("user").field("name");
910
911        assert!(path.is_zero_copy());
912        assert_eq!(path.depth(), 2);
913
914        let sql = path.to_sql(DatabaseType::PostgreSQL);
915        assert!(sql.contains("data"));
916        assert!(sql.contains("user"));
917        assert!(sql.contains("name"));
918    }
919
920    #[test]
921    fn test_json_path_ref_with_index() {
922        let path = JsonPathRef::new("items")
923            .field("products")
924            .index(0)
925            .field("name");
926
927        let sql = path.to_sql(DatabaseType::PostgreSQL);
928        assert!(sql.contains("products"));
929        assert!(sql.contains("0"));
930        assert!(sql.contains("name"));
931    }
932
933    #[test]
934    fn test_json_path_ref_as_text() {
935        let path = JsonPathRef::new("data").field("name").text();
936
937        let pg_sql = path.to_sql(DatabaseType::PostgreSQL);
938        assert!(pg_sql.contains("->>")); // Text extraction
939    }
940
941    #[test]
942    fn test_window_spec_ref_zero_copy() {
943        let spec = WindowSpecRef::new()
944            .partition_by(&["dept", "team"])
945            .order_by_asc("salary");
946
947        assert!(spec.is_zero_copy());
948
949        let sql = spec.to_sql(DatabaseType::PostgreSQL);
950        assert!(sql.contains("PARTITION BY dept, team"));
951        assert!(sql.contains("ORDER BY salary ASC"));
952    }
953
954    #[test]
955    fn test_window_spec_ref_with_frame() {
956        let spec = WindowSpecRef::new()
957            .order_by_asc("date")
958            .rows_unbounded_preceding();
959
960        let sql = spec.to_sql(DatabaseType::PostgreSQL);
961        assert!(sql.contains("ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"));
962    }
963
964    #[test]
965    fn test_cte_ref_zero_copy() {
966        let cte = CteRef::new("active_users")
967            .columns(&["id", "name", "email"])
968            .query("SELECT id, name, email FROM users WHERE active = true");
969
970        assert!(cte.is_zero_copy());
971
972        let sql = cte.to_sql(DatabaseType::PostgreSQL);
973        assert!(sql.contains("active_users"));
974        assert!(sql.contains("id, name, email"));
975        assert!(sql.contains("SELECT id, name, email FROM users"));
976    }
977
978    #[test]
979    fn test_cte_ref_recursive() {
980        let cte = CteRef::new("tree")
981            .columns(&["id", "parent_id", "level"])
982            .query("SELECT id, parent_id, 1 FROM items WHERE parent_id IS NULL")
983            .recursive();
984
985        assert!(cte.recursive);
986        let sql = cte.to_sql(DatabaseType::PostgreSQL);
987        assert!(sql.contains("tree"));
988    }
989
990    #[test]
991    fn test_with_clause_ref() {
992        let with = WithClauseRef::new()
993            .cte(
994                CteRef::new("active_users")
995                    .columns(&["id", "name"])
996                    .query("SELECT id, name FROM users WHERE active = true"),
997            )
998            .cte(
999                CteRef::new("orders")
1000                    .columns(&["user_id", "total"])
1001                    .query("SELECT user_id, SUM(amount) FROM orders GROUP BY user_id"),
1002            );
1003
1004        let sql = with.build_select(&["*"], "active_users", DatabaseType::PostgreSQL);
1005        assert!(sql.contains("WITH"));
1006        assert!(sql.contains("active_users"));
1007        assert!(sql.contains("orders"));
1008        assert!(sql.contains("SELECT * FROM active_users"));
1009    }
1010
1011    #[test]
1012    fn test_json_path_ref_mysql() {
1013        let path = JsonPathRef::new("data").field("user").field("email");
1014
1015        let sql = path.to_sql(DatabaseType::MySQL);
1016        assert!(sql.contains("data"));
1017        assert!(sql.contains("$.user"));
1018    }
1019
1020    #[test]
1021    fn test_json_path_ref_sqlite() {
1022        let path = JsonPathRef::new("config").field("settings").field("theme");
1023
1024        let sql = path.to_sql(DatabaseType::SQLite);
1025        assert!(sql.contains("json_extract"));
1026        assert!(sql.contains("$.settings.theme"));
1027    }
1028}