Skip to main content

prax_query/
builder.rs

1//! Optimized builder patterns for query construction.
2//!
3//! This module provides memory-efficient builder types that minimize allocations:
4//! - `SmallVec` for small collections (partition columns, order by, etc.)
5//! - `Cow<'static, str>` for identifiers that are often static
6//! - `SmolStr` for inline small strings (< 24 bytes stored inline)
7//! - Reusable builders that can be reset and reused
8//!
9//! # Performance Characteristics
10//!
11//! | Type | Stack Size | Inline Capacity | Heap Allocation |
12//! |------|------------|-----------------|-----------------|
13//! | `SmallVec<[T; 8]>` | 64+ bytes | 8 elements | > 8 elements |
14//! | `SmolStr` | 24 bytes | 22 chars | > 22 chars |
15//! | `Cow<'static, str>` | 24 bytes | N/A | Only if owned |
16//! | `Identifier` | 24 bytes | 22 chars | > 22 chars |
17//!
18//! # Example
19//!
20//! ```rust
21//! use prax_query::builder::{Identifier, ColumnList, ReusableBuilder};
22//!
23//! // Identifier that stores small strings inline
24//! let col = Identifier::new("user_id"); // No heap allocation
25//! let long_col = Identifier::new("very_long_column_name_here"); // May heap allocate
26//!
27//! // Column list optimized for typical use (1-8 columns)
28//! let mut cols = ColumnList::new();
29//! cols.push(Identifier::new("id"));
30//! cols.push(Identifier::new("name"));
31//! cols.push(Identifier::new("email")); // Still on stack!
32//!
33//! // Reusable builder pattern
34//! let mut builder = ReusableBuilder::new();
35//! builder.push("SELECT * FROM users");
36//! let sql1 = builder.build();
37//! builder.reset(); // Reuse the allocation
38//! builder.push("SELECT * FROM posts");
39//! let sql2 = builder.build();
40//! ```
41
42use smallvec::SmallVec;
43use smol_str::SmolStr;
44use std::borrow::Cow;
45use std::fmt;
46
47// ==============================================================================
48// Identifier Type (Inline Small Strings)
49// ==============================================================================
50
51/// An identifier (column name, table name, alias) optimized for small strings.
52///
53/// Uses `SmolStr` internally which stores strings up to 22 bytes inline,
54/// avoiding heap allocation for typical identifier names.
55///
56/// # Examples
57///
58/// ```rust
59/// use prax_query::builder::Identifier;
60///
61/// let id = Identifier::new("user_id");
62/// assert_eq!(id.as_str(), "user_id");
63///
64/// // From static str (zero-copy)
65/// let id: Identifier = "email".into();
66/// ```
67#[derive(Clone, PartialEq, Eq, Hash, Default)]
68pub struct Identifier(SmolStr);
69
70impl Identifier {
71    /// Create a new identifier from any string-like type.
72    #[inline]
73    pub fn new(s: impl AsRef<str>) -> Self {
74        Self(SmolStr::new(s.as_ref()))
75    }
76
77    /// Create from a static string (zero allocation).
78    #[inline]
79    pub const fn from_static(s: &'static str) -> Self {
80        Self(SmolStr::new_static(s))
81    }
82
83    /// Get the identifier as a string slice.
84    #[inline]
85    pub fn as_str(&self) -> &str {
86        self.0.as_str()
87    }
88
89    /// Check if the string is stored inline (no heap allocation).
90    #[inline]
91    pub fn is_inline(&self) -> bool {
92        !self.0.is_heap_allocated()
93    }
94
95    /// Get the length of the identifier.
96    #[inline]
97    pub fn len(&self) -> usize {
98        self.0.len()
99    }
100
101    /// Check if the identifier is empty.
102    #[inline]
103    pub fn is_empty(&self) -> bool {
104        self.0.is_empty()
105    }
106}
107
108impl fmt::Debug for Identifier {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        write!(f, "Identifier({:?})", self.0.as_str())
111    }
112}
113
114impl fmt::Display for Identifier {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.0)
117    }
118}
119
120impl From<&str> for Identifier {
121    #[inline]
122    fn from(s: &str) -> Self {
123        Self::new(s)
124    }
125}
126
127impl From<String> for Identifier {
128    #[inline]
129    fn from(s: String) -> Self {
130        Self(SmolStr::new(&s))
131    }
132}
133
134impl From<&String> for Identifier {
135    #[inline]
136    fn from(s: &String) -> Self {
137        Self(SmolStr::new(s))
138    }
139}
140
141impl From<Cow<'_, str>> for Identifier {
142    #[inline]
143    fn from(s: Cow<'_, str>) -> Self {
144        Self(SmolStr::new(&s))
145    }
146}
147
148impl AsRef<str> for Identifier {
149    #[inline]
150    fn as_ref(&self) -> &str {
151        self.as_str()
152    }
153}
154
155// ==============================================================================
156// Cow Identifier (Copy-on-Write)
157// ==============================================================================
158
159/// A copy-on-write identifier that borrows static strings without allocation.
160///
161/// Use this when identifiers are often string literals but occasionally
162/// need to be dynamically generated.
163///
164/// # Examples
165///
166/// ```rust
167/// use prax_query::builder::CowIdentifier;
168///
169/// // Static string - zero allocation
170/// let id = CowIdentifier::borrowed("user_id");
171///
172/// // Dynamic string - allocates if not static
173/// let dynamic_name = format!("col_{}", 1);
174/// let id = CowIdentifier::owned(dynamic_name);
175/// ```
176#[derive(Clone, PartialEq, Eq, Hash)]
177pub struct CowIdentifier<'a>(Cow<'a, str>);
178
179impl<'a> CowIdentifier<'a> {
180    /// Create from a borrowed static string (zero allocation).
181    #[inline]
182    pub const fn borrowed(s: &'a str) -> Self {
183        Self(Cow::Borrowed(s))
184    }
185
186    /// Create from an owned string.
187    #[inline]
188    pub fn owned(s: String) -> Self {
189        Self(Cow::Owned(s))
190    }
191
192    /// Create from any string-like type.
193    #[inline]
194    pub fn new(s: impl Into<Cow<'a, str>>) -> Self {
195        Self(s.into())
196    }
197
198    /// Get as string slice.
199    #[inline]
200    pub fn as_str(&self) -> &str {
201        &self.0
202    }
203
204    /// Check if this is a borrowed (non-allocating) reference.
205    #[inline]
206    pub fn is_borrowed(&self) -> bool {
207        matches!(self.0, Cow::Borrowed(_))
208    }
209
210    /// Convert to owned, cloning if necessary.
211    #[inline]
212    pub fn into_owned(self) -> String {
213        self.0.into_owned()
214    }
215}
216
217impl<'a> fmt::Debug for CowIdentifier<'a> {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        write!(
220            f,
221            "CowIdentifier({:?}, borrowed={})",
222            self.0.as_ref(),
223            self.is_borrowed()
224        )
225    }
226}
227
228impl<'a> fmt::Display for CowIdentifier<'a> {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "{}", self.0)
231    }
232}
233
234impl<'a> From<&'a str> for CowIdentifier<'a> {
235    #[inline]
236    fn from(s: &'a str) -> Self {
237        Self::borrowed(s)
238    }
239}
240
241impl From<String> for CowIdentifier<'static> {
242    #[inline]
243    fn from(s: String) -> Self {
244        Self::owned(s)
245    }
246}
247
248impl<'a> AsRef<str> for CowIdentifier<'a> {
249    #[inline]
250    fn as_ref(&self) -> &str {
251        self.as_str()
252    }
253}
254
255impl<'a> Default for CowIdentifier<'a> {
256    fn default() -> Self {
257        Self::borrowed("")
258    }
259}
260
261// ==============================================================================
262// SmallVec-based Collections
263// ==============================================================================
264
265/// A list of columns optimized for typical use cases (1-8 columns).
266///
267/// Uses `SmallVec` to store up to 8 identifiers on the stack,
268/// only heap-allocating for larger lists.
269pub type ColumnList = SmallVec<[Identifier; 8]>;
270
271/// A list of column names as strings, optimized for 1-8 columns.
272pub type ColumnNameList = SmallVec<[String; 8]>;
273
274/// A list of column names as Cow strings for zero-copy static columns.
275pub type CowColumnList<'a> = SmallVec<[Cow<'a, str>; 8]>;
276
277/// A list of sort orders, optimized for 1-4 ORDER BY columns.
278pub type OrderByList = SmallVec<[(Identifier, crate::types::SortOrder); 4]>;
279
280/// A list of partition columns, optimized for 1-4 PARTITION BY columns.
281pub type PartitionByList = SmallVec<[Identifier; 4]>;
282
283/// A list of expressions, optimized for 1-8 items.
284pub type ExprList = SmallVec<[String; 8]>;
285
286/// A list of values, optimized for 1-16 items (e.g., IN clauses).
287pub type ValueList<T> = SmallVec<[T; 16]>;
288
289// ==============================================================================
290// Reusable Builder
291// ==============================================================================
292
293/// A reusable string builder that can be reset and reused.
294///
295/// This is useful for building multiple queries in a loop without
296/// reallocating the buffer each time.
297///
298/// # Example
299///
300/// ```rust
301/// use prax_query::builder::ReusableBuilder;
302///
303/// let mut builder = ReusableBuilder::with_capacity(256);
304///
305/// for i in 0..10 {
306///     builder.push("SELECT * FROM users WHERE id = ");
307///     builder.push(&i.to_string());
308///     let sql = builder.take(); // Take ownership without reallocating
309///     // Use sql...
310///     builder.reset(); // Clear for next iteration
311/// }
312/// ```
313#[derive(Debug, Clone)]
314pub struct ReusableBuilder {
315    buffer: String,
316    /// Track the initial capacity for efficient reset
317    initial_capacity: usize,
318}
319
320impl ReusableBuilder {
321    /// Create a new builder with default capacity.
322    #[inline]
323    pub fn new() -> Self {
324        Self {
325            buffer: String::new(),
326            initial_capacity: 0,
327        }
328    }
329
330    /// Create with pre-allocated capacity.
331    #[inline]
332    pub fn with_capacity(capacity: usize) -> Self {
333        Self {
334            buffer: String::with_capacity(capacity),
335            initial_capacity: capacity,
336        }
337    }
338
339    /// Push a string slice.
340    #[inline]
341    pub fn push(&mut self, s: &str) -> &mut Self {
342        self.buffer.push_str(s);
343        self
344    }
345
346    /// Push a single character.
347    #[inline]
348    pub fn push_char(&mut self, c: char) -> &mut Self {
349        self.buffer.push(c);
350        self
351    }
352
353    /// Push formatted content.
354    #[inline]
355    pub fn push_fmt(&mut self, args: fmt::Arguments<'_>) -> &mut Self {
356        use std::fmt::Write;
357        let _ = self.buffer.write_fmt(args);
358        self
359    }
360
361    /// Push a space character.
362    #[inline]
363    pub fn space(&mut self) -> &mut Self {
364        self.buffer.push(' ');
365        self
366    }
367
368    /// Push a comma and space.
369    #[inline]
370    pub fn comma(&mut self) -> &mut Self {
371        self.buffer.push_str(", ");
372        self
373    }
374
375    /// Get the current content as a slice.
376    #[inline]
377    pub fn as_str(&self) -> &str {
378        &self.buffer
379    }
380
381    /// Get the current length.
382    #[inline]
383    pub fn len(&self) -> usize {
384        self.buffer.len()
385    }
386
387    /// Check if empty.
388    #[inline]
389    pub fn is_empty(&self) -> bool {
390        self.buffer.is_empty()
391    }
392
393    /// Build and return a clone of the content.
394    #[inline]
395    pub fn build(&self) -> String {
396        self.buffer.clone()
397    }
398
399    /// Take ownership of the buffer, leaving an empty string.
400    #[inline]
401    pub fn take(&mut self) -> String {
402        std::mem::take(&mut self.buffer)
403    }
404
405    /// Reset the builder for reuse, keeping capacity.
406    #[inline]
407    pub fn reset(&mut self) {
408        self.buffer.clear();
409    }
410
411    /// Reset and shrink to initial capacity if grown significantly.
412    #[inline]
413    pub fn reset_shrink(&mut self) {
414        self.buffer.clear();
415        if self.buffer.capacity() > self.initial_capacity * 2 {
416            self.buffer.shrink_to(self.initial_capacity);
417        }
418    }
419
420    /// Reserve additional capacity.
421    #[inline]
422    pub fn reserve(&mut self, additional: usize) {
423        self.buffer.reserve(additional);
424    }
425
426    /// Get the current capacity.
427    #[inline]
428    pub fn capacity(&self) -> usize {
429        self.buffer.capacity()
430    }
431}
432
433impl Default for ReusableBuilder {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439impl fmt::Display for ReusableBuilder {
440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441        write!(f, "{}", self.buffer)
442    }
443}
444
445impl From<ReusableBuilder> for String {
446    fn from(builder: ReusableBuilder) -> String {
447        builder.buffer
448    }
449}
450
451// ==============================================================================
452// Builder Pool (for high-throughput scenarios)
453// ==============================================================================
454
455/// A pool of reusable builders for high-throughput scenarios.
456///
457/// This is useful when building many queries concurrently, as it
458/// allows reusing allocated buffers across requests.
459///
460/// # Example
461///
462/// ```rust
463/// use prax_query::builder::BuilderPool;
464///
465/// let pool = BuilderPool::new(16, 256); // 16 builders, 256 byte capacity each
466///
467/// // Get a builder from the pool
468/// let mut builder = pool.get();
469/// builder.push("SELECT * FROM users");
470/// let sql = builder.take();
471/// pool.put(builder); // Return to pool for reuse
472/// ```
473pub struct BuilderPool {
474    builders: parking_lot::Mutex<Vec<ReusableBuilder>>,
475    capacity: usize,
476}
477
478impl BuilderPool {
479    /// Create a new pool with the specified size and builder capacity.
480    pub fn new(pool_size: usize, builder_capacity: usize) -> Self {
481        let builders: Vec<_> = (0..pool_size)
482            .map(|_| ReusableBuilder::with_capacity(builder_capacity))
483            .collect();
484        Self {
485            builders: parking_lot::Mutex::new(builders),
486            capacity: builder_capacity,
487        }
488    }
489
490    /// Get a builder from the pool, or create a new one if empty.
491    #[inline]
492    pub fn get(&self) -> ReusableBuilder {
493        self.builders
494            .lock()
495            .pop()
496            .unwrap_or_else(|| ReusableBuilder::with_capacity(self.capacity))
497    }
498
499    /// Return a builder to the pool for reuse.
500    #[inline]
501    pub fn put(&self, mut builder: ReusableBuilder) {
502        builder.reset_shrink();
503        self.builders.lock().push(builder);
504    }
505
506    /// Get the current pool size.
507    pub fn len(&self) -> usize {
508        self.builders.lock().len()
509    }
510
511    /// Check if the pool is empty.
512    pub fn is_empty(&self) -> bool {
513        self.builders.lock().is_empty()
514    }
515}
516
517// ==============================================================================
518// Optimized Window Spec Builder
519// ==============================================================================
520
521/// An optimized window specification using SmallVec for partition/order columns.
522///
523/// This is a more memory-efficient version of `WindowSpec` that uses
524/// stack-allocated small vectors for typical use cases.
525#[derive(Debug, Clone, Default)]
526pub struct OptimizedWindowSpec {
527    /// Partition columns (typically 1-4).
528    pub partition_by: PartitionByList,
529    /// Order by columns with direction (typically 1-4).
530    pub order_by: SmallVec<[(Identifier, crate::types::SortOrder); 4]>,
531    /// Frame type.
532    pub frame: Option<WindowFrame>,
533    /// Reference to a named window.
534    pub window_ref: Option<Identifier>,
535}
536
537/// Window frame specification.
538#[derive(Debug, Clone)]
539pub struct WindowFrame {
540    /// Frame type (ROWS, RANGE, GROUPS).
541    pub frame_type: FrameType,
542    /// Start bound.
543    pub start: FrameBound,
544    /// End bound (None = CURRENT ROW).
545    pub end: Option<FrameBound>,
546}
547
548/// Frame type for window functions.
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum FrameType {
551    Rows,
552    Range,
553    Groups,
554}
555
556/// Frame bound specification.
557#[derive(Debug, Clone, PartialEq, Eq)]
558pub enum FrameBound {
559    UnboundedPreceding,
560    Preceding(u32),
561    CurrentRow,
562    Following(u32),
563    UnboundedFollowing,
564}
565
566impl OptimizedWindowSpec {
567    /// Create a new empty window spec.
568    #[inline]
569    pub fn new() -> Self {
570        Self::default()
571    }
572
573    /// Add partition by columns.
574    #[inline]
575    pub fn partition_by<I, S>(mut self, columns: I) -> Self
576    where
577        I: IntoIterator<Item = S>,
578        S: Into<Identifier>,
579    {
580        self.partition_by
581            .extend(columns.into_iter().map(Into::into));
582        self
583    }
584
585    /// Add a single partition column.
586    #[inline]
587    pub fn partition_by_col(mut self, column: impl Into<Identifier>) -> Self {
588        self.partition_by.push(column.into());
589        self
590    }
591
592    /// Add order by column with sort direction.
593    #[inline]
594    pub fn order_by(
595        mut self,
596        column: impl Into<Identifier>,
597        order: crate::types::SortOrder,
598    ) -> Self {
599        self.order_by.push((column.into(), order));
600        self
601    }
602
603    /// Set frame to ROWS BETWEEN ... AND ...
604    #[inline]
605    pub fn rows(mut self, start: FrameBound, end: Option<FrameBound>) -> Self {
606        self.frame = Some(WindowFrame {
607            frame_type: FrameType::Rows,
608            start,
609            end,
610        });
611        self
612    }
613
614    /// Set frame to ROWS UNBOUNDED PRECEDING.
615    #[inline]
616    pub fn rows_unbounded_preceding(self) -> Self {
617        self.rows(FrameBound::UnboundedPreceding, Some(FrameBound::CurrentRow))
618    }
619
620    /// Set a reference to a named window.
621    #[inline]
622    pub fn window_ref(mut self, name: impl Into<Identifier>) -> Self {
623        self.window_ref = Some(name.into());
624        self
625    }
626
627    /// Generate SQL for the OVER clause.
628    pub fn to_sql(&self, _db_type: crate::sql::DatabaseType) -> String {
629        let mut parts: SmallVec<[String; 4]> = SmallVec::new();
630
631        // Window reference
632        if let Some(ref name) = self.window_ref {
633            return format!("OVER {}", name);
634        }
635
636        // PARTITION BY
637        if !self.partition_by.is_empty() {
638            let cols: Vec<_> = self.partition_by.iter().map(|c| c.as_str()).collect();
639            parts.push(format!("PARTITION BY {}", cols.join(", ")));
640        }
641
642        // ORDER BY
643        if !self.order_by.is_empty() {
644            let cols: Vec<_> = self
645                .order_by
646                .iter()
647                .map(|(col, order)| {
648                    format!(
649                        "{} {}",
650                        col.as_str(),
651                        match order {
652                            crate::types::SortOrder::Asc => "ASC",
653                            crate::types::SortOrder::Desc => "DESC",
654                        }
655                    )
656                })
657                .collect();
658            parts.push(format!("ORDER BY {}", cols.join(", ")));
659        }
660
661        // Frame clause
662        if let Some(ref frame) = self.frame {
663            let frame_type = match frame.frame_type {
664                FrameType::Rows => "ROWS",
665                FrameType::Range => "RANGE",
666                FrameType::Groups => "GROUPS",
667            };
668
669            let start = frame_bound_to_sql(&frame.start);
670
671            if let Some(ref end) = frame.end {
672                let end_sql = frame_bound_to_sql(end);
673                parts.push(format!("{} BETWEEN {} AND {}", frame_type, start, end_sql));
674            } else {
675                parts.push(format!("{} {}", frame_type, start));
676            }
677        }
678
679        if parts.is_empty() {
680            "OVER ()".to_string()
681        } else {
682            format!("OVER ({})", parts.join(" "))
683        }
684    }
685}
686
687fn frame_bound_to_sql(bound: &FrameBound) -> &'static str {
688    match bound {
689        FrameBound::UnboundedPreceding => "UNBOUNDED PRECEDING",
690        FrameBound::Preceding(_) => "PRECEDING", // Would need dynamic
691        FrameBound::CurrentRow => "CURRENT ROW",
692        FrameBound::Following(_) => "FOLLOWING", // Would need dynamic
693        FrameBound::UnboundedFollowing => "UNBOUNDED FOLLOWING",
694    }
695}
696
697// ==============================================================================
698// Tests
699// ==============================================================================
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    #[test]
706    fn test_identifier_inline() {
707        let id = Identifier::new("user_id");
708        assert_eq!(id.as_str(), "user_id");
709        assert!(id.is_inline()); // Should be stored inline (< 22 chars)
710    }
711
712    #[test]
713    fn test_identifier_from_static() {
714        let id = Identifier::from_static("email");
715        assert_eq!(id.as_str(), "email");
716    }
717
718    #[test]
719    fn test_cow_identifier_borrowed() {
720        let id = CowIdentifier::borrowed("user_id");
721        assert!(id.is_borrowed());
722        assert_eq!(id.as_str(), "user_id");
723    }
724
725    #[test]
726    fn test_cow_identifier_owned() {
727        let id = CowIdentifier::owned("dynamic".to_string());
728        assert!(!id.is_borrowed());
729        assert_eq!(id.as_str(), "dynamic");
730    }
731
732    #[test]
733    fn test_column_list_stack_allocation() {
734        let mut cols: ColumnList = SmallVec::new();
735        cols.push(Identifier::new("id"));
736        cols.push(Identifier::new("name"));
737        cols.push(Identifier::new("email"));
738        cols.push(Identifier::new("created_at"));
739
740        // Should not have spilled to heap (< 8 items)
741        assert!(!cols.spilled());
742        assert_eq!(cols.len(), 4);
743    }
744
745    #[test]
746    fn test_column_list_heap_spillover() {
747        let mut cols: ColumnList = SmallVec::new();
748        for i in 0..10 {
749            cols.push(Identifier::new(format!("col_{}", i)));
750        }
751
752        // Should have spilled to heap (> 8 items)
753        assert!(cols.spilled());
754        assert_eq!(cols.len(), 10);
755    }
756
757    #[test]
758    fn test_reusable_builder() {
759        let mut builder = ReusableBuilder::with_capacity(64);
760
761        builder.push("SELECT * FROM users");
762        assert_eq!(builder.as_str(), "SELECT * FROM users");
763
764        builder.reset();
765        assert!(builder.is_empty());
766        assert!(builder.capacity() >= 64); // Capacity preserved
767
768        builder.push("SELECT * FROM posts");
769        assert_eq!(builder.as_str(), "SELECT * FROM posts");
770    }
771
772    #[test]
773    fn test_reusable_builder_take() {
774        let mut builder = ReusableBuilder::new();
775        builder.push("test");
776
777        let taken = builder.take();
778        assert_eq!(taken, "test");
779        assert!(builder.is_empty());
780    }
781
782    #[test]
783    fn test_builder_pool() {
784        let pool = BuilderPool::new(4, 128);
785
786        // Get all builders
787        let b1 = pool.get();
788        let b2 = pool.get();
789        let _b3 = pool.get();
790        let _b4 = pool.get();
791
792        // Pool should be empty
793        assert!(pool.is_empty());
794
795        // Return some
796        pool.put(b1);
797        pool.put(b2);
798
799        assert_eq!(pool.len(), 2);
800    }
801
802    #[test]
803    fn test_optimized_window_spec() {
804        use crate::types::SortOrder;
805
806        let spec = OptimizedWindowSpec::new()
807            .partition_by(["dept", "team"])
808            .order_by("salary", SortOrder::Desc)
809            .rows_unbounded_preceding();
810
811        let sql = spec.to_sql(crate::sql::DatabaseType::PostgreSQL);
812        assert!(sql.contains("PARTITION BY"));
813        assert!(sql.contains("ORDER BY"));
814        assert!(sql.contains("ROWS"));
815    }
816}