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