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.push(PathSegmentRef::Field(Cow::Borrowed(name)));
159        self
160    }
161
162    /// Add a field access segment (owned).
163    #[inline]
164    pub fn field_owned(mut self, name: String) -> Self {
165        self.segments.push(PathSegmentRef::Field(Cow::Owned(name)));
166        self
167    }
168
169    /// Add an array index segment.
170    #[inline]
171    pub fn index(mut self, idx: i64) -> Self {
172        self.segments.push(PathSegmentRef::Index(idx));
173        self
174    }
175
176    /// Add an array wildcard segment.
177    #[inline]
178    pub fn all(mut self) -> Self {
179        self.segments.push(PathSegmentRef::Wildcard);
180        self
181    }
182
183    /// Return the value as text instead of JSON.
184    #[inline]
185    pub fn text(mut self) -> Self {
186        self.as_text = true;
187        self
188    }
189
190    /// Check if this path uses only borrowed data (no allocations).
191    pub fn is_zero_copy(&self) -> bool {
192        matches!(self.column, Cow::Borrowed(_))
193            && self.segments.iter().all(|s| match s {
194                PathSegmentRef::Field(cow) => matches!(cow, Cow::Borrowed(_)),
195                _ => true,
196            })
197    }
198
199    /// Get the number of segments.
200    #[inline]
201    pub fn depth(&self) -> usize {
202        self.segments.len()
203    }
204
205    /// Generate SQL for this path.
206    pub fn to_sql(&self, db_type: DatabaseType) -> String {
207        match db_type {
208            DatabaseType::PostgreSQL => self.to_postgres_sql(),
209            DatabaseType::MySQL => self.to_mysql_sql(),
210            DatabaseType::SQLite => self.to_sqlite_sql(),
211            DatabaseType::MSSQL => self.to_mssql_sql(),
212        }
213    }
214
215    fn to_postgres_sql(&self) -> String {
216        let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
217        sql.push_str(&self.column);
218
219        let last_idx = self.segments.len().saturating_sub(1);
220        for (i, segment) in self.segments.iter().enumerate() {
221            match segment {
222                PathSegmentRef::Field(name) => {
223                    if self.as_text && i == last_idx {
224                        sql.push_str(" ->> '");
225                    } else {
226                        sql.push_str(" -> '");
227                    }
228                    sql.push_str(name);
229                    sql.push('\'');
230                }
231                PathSegmentRef::Index(idx) => {
232                    if self.as_text && i == last_idx {
233                        sql.push_str(" ->> ");
234                    } else {
235                        sql.push_str(" -> ");
236                    }
237                    sql.push_str(&idx.to_string());
238                }
239                PathSegmentRef::Wildcard => {
240                    sql.push_str(" -> '*'");
241                }
242                PathSegmentRef::RecursiveDescent => {
243                    // PostgreSQL doesn't have native recursive descent
244                    sql.push_str(" #> '{}'");
245                }
246            }
247        }
248
249        sql
250    }
251
252    fn to_mysql_sql(&self) -> String {
253        let mut sql = String::with_capacity(self.column.len() + self.segments.len() * 16);
254        sql.push_str(&self.column);
255
256        let last_idx = self.segments.len().saturating_sub(1);
257        for (i, segment) in self.segments.iter().enumerate() {
258            match segment {
259                PathSegmentRef::Field(name) => {
260                    if self.as_text && i == last_idx {
261                        sql.push_str(" ->> '$.");
262                    } else {
263                        sql.push_str(" -> '$.");
264                    }
265                    sql.push_str(name);
266                    sql.push('\'');
267                }
268                PathSegmentRef::Index(idx) => {
269                    sql.push_str(" -> '$[");
270                    sql.push_str(&idx.to_string());
271                    sql.push_str("]'");
272                }
273                PathSegmentRef::Wildcard => {
274                    sql.push_str(" -> '$[*]'");
275                }
276                PathSegmentRef::RecursiveDescent => {
277                    sql.push_str(" -> '$**'");
278                }
279            }
280        }
281
282        sql
283    }
284
285    fn to_sqlite_sql(&self) -> String {
286        // SQLite uses json_extract function
287        let mut path = String::from("$");
288        for segment in &self.segments {
289            match segment {
290                PathSegmentRef::Field(name) => {
291                    path.push('.');
292                    path.push_str(name);
293                }
294                PathSegmentRef::Index(idx) => {
295                    path.push('[');
296                    path.push_str(&idx.to_string());
297                    path.push(']');
298                }
299                PathSegmentRef::Wildcard => {
300                    path.push_str("[*]");
301                }
302                PathSegmentRef::RecursiveDescent => {
303                    path.push_str("..");
304                }
305            }
306        }
307
308        format!("json_extract({}, '{}')", self.column, path)
309    }
310
311    fn to_mssql_sql(&self) -> String {
312        // MSSQL uses JSON_VALUE or JSON_QUERY
313        let mut path = String::from("$");
314        for segment in &self.segments {
315            match segment {
316                PathSegmentRef::Field(name) => {
317                    path.push('.');
318                    path.push_str(name);
319                }
320                PathSegmentRef::Index(idx) => {
321                    path.push('[');
322                    path.push_str(&idx.to_string());
323                    path.push(']');
324                }
325                PathSegmentRef::Wildcard | PathSegmentRef::RecursiveDescent => {
326                    // MSSQL doesn't support wildcards directly
327                    path.push_str("[0]");
328                }
329            }
330        }
331
332        if self.as_text {
333            format!("JSON_VALUE({}, '{}')", self.column, path)
334        } else {
335            format!("JSON_QUERY({}, '{}')", self.column, path)
336        }
337    }
338
339    /// Convert to owned JsonPath.
340    pub fn to_owned(&self) -> crate::json::JsonPath {
341        crate::json::JsonPath {
342            column: self.column.to_string(),
343            segments: self
344                .segments
345                .iter()
346                .map(|s| match s {
347                    PathSegmentRef::Field(cow) => crate::json::PathSegment::Field(cow.to_string()),
348                    PathSegmentRef::Index(i) => crate::json::PathSegment::Index(*i),
349                    PathSegmentRef::Wildcard => crate::json::PathSegment::Wildcard,
350                    PathSegmentRef::RecursiveDescent => crate::json::PathSegment::RecursiveDescent,
351                })
352                .collect(),
353            as_text: self.as_text,
354        }
355    }
356}
357
358impl<'a> From<&'a crate::json::JsonPath> for JsonPathRef<'a> {
359    fn from(path: &'a crate::json::JsonPath) -> Self {
360        Self {
361            column: Cow::Borrowed(&path.column),
362            segments: path
363                .segments
364                .iter()
365                .map(|s| match s {
366                    crate::json::PathSegment::Field(name) => {
367                        PathSegmentRef::Field(Cow::Borrowed(name))
368                    }
369                    crate::json::PathSegment::Index(i) => PathSegmentRef::Index(*i),
370                    crate::json::PathSegment::Wildcard => PathSegmentRef::Wildcard,
371                    crate::json::PathSegment::RecursiveDescent => PathSegmentRef::RecursiveDescent,
372                })
373                .collect(),
374            as_text: path.as_text,
375        }
376    }
377}
378
379// ==============================================================================
380// Zero-Copy Window Spec
381// ==============================================================================
382
383/// A zero-copy window specification using borrowed column references.
384///
385/// This is more efficient than `WindowSpec` when working with string literals.
386///
387/// # Example
388///
389/// ```rust
390/// use prax_query::zero_copy::WindowSpecRef;
391/// use prax_query::sql::DatabaseType;
392///
393/// // All column names are borrowed - no allocations
394/// let spec = WindowSpecRef::new()
395///     .partition_by(&["dept", "team"])
396///     .order_by_asc("salary")
397///     .rows_unbounded_preceding();
398///
399/// let sql = spec.to_sql(DatabaseType::PostgreSQL);
400/// ```
401#[derive(Debug, Clone, Default)]
402pub struct WindowSpecRef<'a> {
403    /// Partition by columns (borrowed slice or owned vec).
404    pub partition_by: SmallVec<[Cow<'a, str>; 4]>,
405    /// Order by columns with direction.
406    pub order_by: SmallVec<[(Cow<'a, str>, SortOrder); 4]>,
407    /// Frame clause.
408    pub frame: Option<FrameRef<'a>>,
409    /// Reference to named window.
410    pub window_ref: Option<Cow<'a, str>>,
411}
412
413/// A frame clause for window specifications.
414#[derive(Debug, Clone)]
415pub struct FrameRef<'a> {
416    /// Frame type (ROWS, RANGE, GROUPS).
417    pub frame_type: FrameTypeRef,
418    /// Start bound.
419    pub start: FrameBoundRef<'a>,
420    /// End bound.
421    pub end: Option<FrameBoundRef<'a>>,
422}
423
424/// Frame type for window functions.
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426pub enum FrameTypeRef {
427    Rows,
428    Range,
429    Groups,
430}
431
432/// Frame bound specification.
433#[derive(Debug, Clone, PartialEq)]
434pub enum FrameBoundRef<'a> {
435    UnboundedPreceding,
436    Preceding(u32),
437    CurrentRow,
438    Following(u32),
439    UnboundedFollowing,
440    /// Expression-based bound (may be borrowed or owned).
441    Expr(Cow<'a, str>),
442}
443
444impl<'a> WindowSpecRef<'a> {
445    /// Create a new empty window spec.
446    #[inline]
447    pub fn new() -> Self {
448        Self::default()
449    }
450
451    /// Add partition by columns from a slice (zero-copy).
452    #[inline]
453    pub fn partition_by(mut self, columns: &[&'a str]) -> Self {
454        self.partition_by
455            .extend(columns.iter().map(|&s| Cow::Borrowed(s)));
456        self
457    }
458
459    /// Add partition by columns (owned).
460    #[inline]
461    pub fn partition_by_owned<I, S>(mut self, columns: I) -> Self
462    where
463        I: IntoIterator<Item = S>,
464        S: Into<String>,
465    {
466        self.partition_by
467            .extend(columns.into_iter().map(|s| Cow::Owned(s.into())));
468        self
469    }
470
471    /// Add a single partition column (borrowed).
472    #[inline]
473    pub fn partition_by_col(mut self, column: &'a str) -> Self {
474        self.partition_by.push(Cow::Borrowed(column));
475        self
476    }
477
478    /// Add order by column ascending (borrowed).
479    #[inline]
480    pub fn order_by_asc(mut self, column: &'a str) -> Self {
481        self.order_by.push((Cow::Borrowed(column), SortOrder::Asc));
482        self
483    }
484
485    /// Add order by column descending (borrowed).
486    #[inline]
487    pub fn order_by_desc(mut self, column: &'a str) -> Self {
488        self.order_by
489            .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.iter().all(|c| matches!(c, Cow::Borrowed(_)))
548            && self
549                .order_by
550                .iter()
551                .all(|(c, _)| matches!(c, Cow::Borrowed(_)))
552            && self
553                .window_ref
554                .as_ref()
555                .map(|w| matches!(w, Cow::Borrowed(_)))
556                .unwrap_or(true)
557    }
558
559    /// Generate SQL for the OVER clause.
560    pub fn to_sql(&self, _db_type: DatabaseType) -> String {
561        // Window reference
562        if let Some(ref name) = self.window_ref {
563            return format!("OVER {}", name);
564        }
565
566        let mut parts: SmallVec<[String; 4]> = SmallVec::new();
567
568        // PARTITION BY
569        if !self.partition_by.is_empty() {
570            let cols: Vec<&str> = self.partition_by.iter().map(|c| c.as_ref()).collect();
571            parts.push(format!("PARTITION BY {}", cols.join(", ")));
572        }
573
574        // ORDER BY
575        if !self.order_by.is_empty() {
576            let cols: Vec<String> = self
577                .order_by
578                .iter()
579                .map(|(col, order)| {
580                    format!(
581                        "{} {}",
582                        col,
583                        match order {
584                            SortOrder::Asc => "ASC",
585                            SortOrder::Desc => "DESC",
586                        }
587                    )
588                })
589                .collect();
590            parts.push(format!("ORDER BY {}", cols.join(", ")));
591        }
592
593        // Frame
594        if let Some(ref frame) = self.frame {
595            let frame_type = match frame.frame_type {
596                FrameTypeRef::Rows => "ROWS",
597                FrameTypeRef::Range => "RANGE",
598                FrameTypeRef::Groups => "GROUPS",
599            };
600
601            let start = frame_bound_to_sql(&frame.start);
602
603            if let Some(ref end) = frame.end {
604                let end_sql = frame_bound_to_sql(end);
605                parts.push(format!("{} BETWEEN {} AND {}", frame_type, start, end_sql));
606            } else {
607                parts.push(format!("{} {}", frame_type, start));
608            }
609        }
610
611        if parts.is_empty() {
612            "OVER ()".to_string()
613        } else {
614            format!("OVER ({})", parts.join(" "))
615        }
616    }
617}
618
619fn frame_bound_to_sql(bound: &FrameBoundRef<'_>) -> String {
620    match bound {
621        FrameBoundRef::UnboundedPreceding => "UNBOUNDED PRECEDING".to_string(),
622        FrameBoundRef::Preceding(n) => format!("{} PRECEDING", n),
623        FrameBoundRef::CurrentRow => "CURRENT ROW".to_string(),
624        FrameBoundRef::Following(n) => format!("{} FOLLOWING", n),
625        FrameBoundRef::UnboundedFollowing => "UNBOUNDED FOLLOWING".to_string(),
626        FrameBoundRef::Expr(expr) => expr.to_string(),
627    }
628}
629
630// ==============================================================================
631// Zero-Copy CTE
632// ==============================================================================
633
634/// A zero-copy CTE definition that accepts column slices.
635///
636/// This is more efficient than `Cte` when working with static column lists.
637///
638/// # Example
639///
640/// ```rust
641/// use prax_query::zero_copy::CteRef;
642/// use prax_query::sql::DatabaseType;
643///
644/// // Column names are borrowed from a static slice
645/// let cte = CteRef::new("active_users")
646///     .columns(&["id", "name", "email"])
647///     .query("SELECT id, name, email FROM users WHERE active = true");
648///
649/// let sql = cte.to_sql(DatabaseType::PostgreSQL);
650/// ```
651#[derive(Debug, Clone, Default)]
652pub struct CteRef<'a> {
653    /// CTE name.
654    pub name: Cow<'a, str>,
655    /// Column aliases (borrowed from slice).
656    pub columns: SmallVec<[Cow<'a, str>; 8]>,
657    /// The defining query.
658    pub query: Cow<'a, str>,
659    /// Whether this is recursive.
660    pub recursive: bool,
661    /// Materialization hint (PostgreSQL).
662    pub materialized: Option<bool>,
663}
664
665impl<'a> CteRef<'a> {
666    /// Create a new CTE with a borrowed name.
667    #[inline]
668    pub fn new(name: &'a str) -> Self {
669        Self {
670            name: Cow::Borrowed(name),
671            columns: SmallVec::new(),
672            query: Cow::Borrowed(""),
673            recursive: false,
674            materialized: None,
675        }
676    }
677
678    /// Create a new CTE with an owned name.
679    #[inline]
680    pub fn owned(name: String) -> Self {
681        Self {
682            name: Cow::Owned(name),
683            columns: SmallVec::new(),
684            query: Cow::Borrowed(""),
685            recursive: false,
686            materialized: None,
687        }
688    }
689
690    /// Set column aliases from a slice (zero-copy).
691    #[inline]
692    pub fn columns(mut self, cols: &[&'a str]) -> Self {
693        self.columns.clear();
694        self.columns.extend(cols.iter().map(|&s| Cow::Borrowed(s)));
695        self
696    }
697
698    /// Set column aliases (owned).
699    #[inline]
700    pub fn columns_owned<I, S>(mut self, cols: I) -> Self
701    where
702        I: IntoIterator<Item = S>,
703        S: Into<String>,
704    {
705        self.columns.clear();
706        self.columns
707            .extend(cols.into_iter().map(|s| Cow::Owned(s.into())));
708        self
709    }
710
711    /// Add a single column (borrowed).
712    #[inline]
713    pub fn column(mut self, col: &'a str) -> Self {
714        self.columns.push(Cow::Borrowed(col));
715        self
716    }
717
718    /// Set the defining query (borrowed).
719    #[inline]
720    pub fn query(mut self, q: &'a str) -> Self {
721        self.query = Cow::Borrowed(q);
722        self
723    }
724
725    /// Set the defining query (owned).
726    #[inline]
727    pub fn query_owned(mut self, q: String) -> Self {
728        self.query = Cow::Owned(q);
729        self
730    }
731
732    /// Mark as recursive CTE.
733    #[inline]
734    pub fn recursive(mut self) -> Self {
735        self.recursive = true;
736        self
737    }
738
739    /// Set materialization hint (PostgreSQL).
740    #[inline]
741    pub fn materialized(mut self, mat: bool) -> Self {
742        self.materialized = Some(mat);
743        self
744    }
745
746    /// Check if this CTE uses only borrowed data.
747    pub fn is_zero_copy(&self) -> bool {
748        matches!(self.name, Cow::Borrowed(_))
749            && matches!(self.query, Cow::Borrowed(_))
750            && self.columns.iter().all(|c| matches!(c, Cow::Borrowed(_)))
751    }
752
753    /// Generate the CTE definition SQL.
754    pub fn to_sql(&self, db_type: DatabaseType) -> String {
755        let mut sql = String::with_capacity(64 + self.query.len());
756
757        sql.push_str(&self.name);
758
759        // Column list
760        if !self.columns.is_empty() {
761            sql.push_str(" (");
762            let cols: Vec<&str> = self.columns.iter().map(|c| c.as_ref()).collect();
763            sql.push_str(&cols.join(", "));
764            sql.push(')');
765        }
766
767        sql.push_str(" AS ");
768
769        // Materialization hint (PostgreSQL only)
770        if matches!(db_type, DatabaseType::PostgreSQL) {
771            if let Some(mat) = self.materialized {
772                if mat {
773                    sql.push_str("MATERIALIZED ");
774                } else {
775                    sql.push_str("NOT MATERIALIZED ");
776                }
777            }
778        }
779
780        sql.push('(');
781        sql.push_str(&self.query);
782        sql.push(')');
783
784        sql
785    }
786
787    /// Convert to owned Cte.
788    pub fn to_owned_cte(&self) -> crate::cte::Cte {
789        crate::cte::Cte {
790            name: self.name.to_string(),
791            columns: self.columns.iter().map(|c| c.to_string()).collect(),
792            query: self.query.to_string(),
793            recursive: self.recursive,
794            materialized: self.materialized.map(|m| {
795                if m {
796                    crate::cte::Materialized::Yes
797                } else {
798                    crate::cte::Materialized::No
799                }
800            }),
801            search: None,
802            cycle: None,
803        }
804    }
805}
806
807// ==============================================================================
808// Zero-Copy WITH Clause Builder
809// ==============================================================================
810
811/// A builder for WITH clauses using zero-copy CTEs.
812///
813/// # Example
814///
815/// ```rust
816/// use prax_query::zero_copy::{CteRef, WithClauseRef};
817/// use prax_query::sql::DatabaseType;
818///
819/// let with = WithClauseRef::new()
820///     .cte(CteRef::new("active_users")
821///         .columns(&["id", "name"])
822///         .query("SELECT id, name FROM users WHERE active = true"))
823///     .cte(CteRef::new("recent_orders")
824///         .columns(&["user_id", "total"])
825///         .query("SELECT user_id, SUM(amount) FROM orders GROUP BY user_id"));
826///
827/// let sql = with.build_select(&["*"], "active_users", DatabaseType::PostgreSQL);
828/// ```
829#[derive(Debug, Clone, Default)]
830pub struct WithClauseRef<'a> {
831    /// CTEs in this WITH clause.
832    pub ctes: SmallVec<[CteRef<'a>; 4]>,
833    /// Whether this is a recursive WITH.
834    pub recursive: bool,
835}
836
837impl<'a> WithClauseRef<'a> {
838    /// Create a new empty WITH clause.
839    #[inline]
840    pub fn new() -> Self {
841        Self::default()
842    }
843
844    /// Add a CTE.
845    #[inline]
846    pub fn cte(mut self, cte: CteRef<'a>) -> Self {
847        if cte.recursive {
848            self.recursive = true;
849        }
850        self.ctes.push(cte);
851        self
852    }
853
854    /// Mark as recursive.
855    #[inline]
856    pub fn recursive(mut self) -> Self {
857        self.recursive = true;
858        self
859    }
860
861    /// Build the WITH clause SQL.
862    pub fn to_sql(&self, db_type: DatabaseType) -> String {
863        if self.ctes.is_empty() {
864            return String::new();
865        }
866
867        let mut sql = String::with_capacity(256);
868
869        sql.push_str("WITH ");
870        if self.recursive {
871            sql.push_str("RECURSIVE ");
872        }
873
874        let cte_sqls: Vec<String> = self.ctes.iter().map(|c| c.to_sql(db_type)).collect();
875        sql.push_str(&cte_sqls.join(", "));
876
877        sql
878    }
879
880    /// Build a complete SELECT query with this WITH clause.
881    pub fn build_select(
882        &self,
883        columns: &[&str],
884        from: &str,
885        db_type: DatabaseType,
886    ) -> String {
887        let with_sql = self.to_sql(db_type);
888        let cols = if columns.is_empty() || columns == ["*"] {
889            "*".to_string()
890        } else {
891            columns.join(", ")
892        };
893
894        if with_sql.is_empty() {
895            format!("SELECT {} FROM {}", cols, from)
896        } else {
897            format!("{} SELECT {} FROM {}", with_sql, cols, from)
898        }
899    }
900}
901
902// ==============================================================================
903// Tests
904// ==============================================================================
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909
910    #[test]
911    fn test_json_path_ref_zero_copy() {
912        let path = JsonPathRef::new("data").field("user").field("name");
913
914        assert!(path.is_zero_copy());
915        assert_eq!(path.depth(), 2);
916
917        let sql = path.to_sql(DatabaseType::PostgreSQL);
918        assert!(sql.contains("data"));
919        assert!(sql.contains("user"));
920        assert!(sql.contains("name"));
921    }
922
923    #[test]
924    fn test_json_path_ref_with_index() {
925        let path = JsonPathRef::new("items").field("products").index(0).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}
1029
1030
1031