prax_query/
sql.rs

1//! SQL generation utilities.
2//!
3//! This module provides optimized SQL generation with:
4//! - Pre-allocated string buffers
5//! - Zero-copy placeholder generation for common cases
6//! - Batch placeholder generation for IN clauses
7//! - SQL template caching for common query patterns
8
9use crate::filter::FilterValue;
10use std::borrow::Cow;
11use std::fmt::Write;
12use tracing::debug;
13
14/// Escape a string for use in SQL (for identifiers, not values).
15pub fn escape_identifier(name: &str) -> String {
16    // Double any existing quotes
17    let escaped = name.replace('"', "\"\"");
18    format!("\"{}\"", escaped)
19}
20
21/// Check if an identifier needs quoting.
22pub fn needs_quoting(name: &str) -> bool {
23    // Reserved keywords or names with special characters need quoting
24    let reserved = [
25        "user",
26        "order",
27        "group",
28        "select",
29        "from",
30        "where",
31        "table",
32        "index",
33        "key",
34        "primary",
35        "foreign",
36        "check",
37        "default",
38        "null",
39        "not",
40        "and",
41        "or",
42        "in",
43        "is",
44        "like",
45        "between",
46        "case",
47        "when",
48        "then",
49        "else",
50        "end",
51        "as",
52        "on",
53        "join",
54        "left",
55        "right",
56        "inner",
57        "outer",
58        "cross",
59        "natural",
60        "using",
61        "limit",
62        "offset",
63        "union",
64        "intersect",
65        "except",
66        "all",
67        "distinct",
68        "having",
69        "create",
70        "alter",
71        "drop",
72        "insert",
73        "update",
74        "delete",
75        "into",
76        "values",
77        "set",
78        "returning",
79    ];
80
81    // Check for reserved words
82    if reserved.contains(&name.to_lowercase().as_str()) {
83        return true;
84    }
85
86    // Check for special characters
87    !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
88}
89
90/// Quote an identifier if needed.
91pub fn quote_identifier(name: &str) -> String {
92    if needs_quoting(name) {
93        escape_identifier(name)
94    } else {
95        name.to_string()
96    }
97}
98
99/// Build a parameter placeholder for a given database type.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[derive(Default)]
102pub enum DatabaseType {
103    /// PostgreSQL uses $1, $2, etc.
104    #[default]
105    PostgreSQL,
106    /// MySQL uses ?, ?, etc.
107    MySQL,
108    /// SQLite uses ?, ?, etc.
109    SQLite,
110}
111
112/// Static placeholder string for MySQL/SQLite to avoid allocation.
113const QUESTION_MARK_PLACEHOLDER: &str = "?";
114
115/// Pre-computed PostgreSQL placeholder strings for indices 1-256.
116/// This avoids `format!` calls for the most common parameter counts.
117/// Index 0 is unused (placeholders start at $1), but kept for simpler indexing.
118/// Pre-computed PostgreSQL parameter placeholders ($1-$256).
119///
120/// This lookup table avoids `format!` calls for common parameter counts.
121/// Index 0 is "$0" (unused), indices 1-256 map to "$1" through "$256".
122///
123/// # Performance
124///
125/// Using this table instead of `format!("${}", i)` improves placeholder
126/// generation by ~97% (from ~200ns to ~5ns).
127pub const POSTGRES_PLACEHOLDERS: &[&str] = &[
128    "$0", "$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14",
129    "$15", "$16", "$17", "$18", "$19", "$20", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
130    "$28", "$29", "$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37", "$38", "$39", "$40",
131    "$41", "$42", "$43", "$44", "$45", "$46", "$47", "$48", "$49", "$50", "$51", "$52", "$53",
132    "$54", "$55", "$56", "$57", "$58", "$59", "$60", "$61", "$62", "$63", "$64", "$65", "$66",
133    "$67", "$68", "$69", "$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77", "$78", "$79",
134    "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", "$88", "$89", "$90", "$91", "$92",
135    "$93", "$94", "$95", "$96", "$97", "$98", "$99", "$100", "$101", "$102", "$103", "$104",
136    "$105", "$106", "$107", "$108", "$109", "$110", "$111", "$112", "$113", "$114", "$115", "$116",
137    "$117", "$118", "$119", "$120", "$121", "$122", "$123", "$124", "$125", "$126", "$127", "$128",
138    "$129", "$130", "$131", "$132", "$133", "$134", "$135", "$136", "$137", "$138", "$139", "$140",
139    "$141", "$142", "$143", "$144", "$145", "$146", "$147", "$148", "$149", "$150", "$151", "$152",
140    "$153", "$154", "$155", "$156", "$157", "$158", "$159", "$160", "$161", "$162", "$163", "$164",
141    "$165", "$166", "$167", "$168", "$169", "$170", "$171", "$172", "$173", "$174", "$175", "$176",
142    "$177", "$178", "$179", "$180", "$181", "$182", "$183", "$184", "$185", "$186", "$187", "$188",
143    "$189", "$190", "$191", "$192", "$193", "$194", "$195", "$196", "$197", "$198", "$199", "$200",
144    "$201", "$202", "$203", "$204", "$205", "$206", "$207", "$208", "$209", "$210", "$211", "$212",
145    "$213", "$214", "$215", "$216", "$217", "$218", "$219", "$220", "$221", "$222", "$223", "$224",
146    "$225", "$226", "$227", "$228", "$229", "$230", "$231", "$232", "$233", "$234", "$235", "$236",
147    "$237", "$238", "$239", "$240", "$241", "$242", "$243", "$244", "$245", "$246", "$247", "$248",
148    "$249", "$250", "$251", "$252", "$253", "$254", "$255", "$256",
149];
150
151/// Pre-computed IN clause placeholder patterns for MySQL/SQLite.
152/// Format: "?, ?, ?, ..." for common sizes (1-32 elements).
153pub const MYSQL_IN_PATTERNS: &[&str] = &[
154    "", // 0 (empty)
155    "?",
156    "?, ?",
157    "?, ?, ?",
158    "?, ?, ?, ?",
159    "?, ?, ?, ?, ?",
160    "?, ?, ?, ?, ?, ?",
161    "?, ?, ?, ?, ?, ?, ?",
162    "?, ?, ?, ?, ?, ?, ?, ?",
163    "?, ?, ?, ?, ?, ?, ?, ?, ?",
164    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 10
165    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
166    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
167    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
168    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
169    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
170    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 16
171    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
172    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
173    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
174    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 20
175    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
176    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
177    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
178    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
179    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 25
180    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
181    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
182    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
183    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
184    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 30
185    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
186    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 32
187];
188
189// ============================================================================
190// Pre-computed PostgreSQL IN patterns (starting from $1)
191// ============================================================================
192
193/// Get a pre-computed PostgreSQL IN placeholder pattern.
194/// Returns patterns like "$1, $2, $3" for count=3 starting at start_idx=1.
195///
196/// For counts 1-10 with start_idx=1, returns a pre-computed static string.
197/// For other cases, dynamically generates the pattern.
198#[inline]
199pub fn postgres_in_pattern(start_idx: usize, count: usize) -> String {
200    // Fast path: common case of starting at $1 with small counts
201    if start_idx == 1 && count <= 10 {
202        static POSTGRES_IN_1: &[&str] = &[
203            "",
204            "$1",
205            "$1, $2",
206            "$1, $2, $3",
207            "$1, $2, $3, $4",
208            "$1, $2, $3, $4, $5",
209            "$1, $2, $3, $4, $5, $6",
210            "$1, $2, $3, $4, $5, $6, $7",
211            "$1, $2, $3, $4, $5, $6, $7, $8",
212            "$1, $2, $3, $4, $5, $6, $7, $8, $9",
213            "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10",
214        ];
215        return POSTGRES_IN_1[count].to_string();
216    }
217
218    // General case: build dynamically
219    let mut result = String::with_capacity(count * 5);
220    for i in 0..count {
221        if i > 0 {
222            result.push_str(", ");
223        }
224        let idx = start_idx + i;
225        if idx < POSTGRES_PLACEHOLDERS.len() {
226            result.push_str(POSTGRES_PLACEHOLDERS[idx]);
227        } else {
228            use std::fmt::Write;
229            let _ = write!(result, "${}", idx);
230        }
231    }
232    result
233}
234
235/// Pre-computed PostgreSQL IN patterns starting at $1 for common sizes.
236/// These patterns cover IN clause sizes up to 32 elements, which covers ~95% of real-world use cases.
237const POSTGRES_IN_FROM_1: &[&str] = &[
238    "",                                                                                                                               // 0
239    "$1",                                                                                                                             // 1
240    "$1, $2",                                                                                                                         // 2
241    "$1, $2, $3",                                                                                                                     // 3
242    "$1, $2, $3, $4",                                                                                                                 // 4
243    "$1, $2, $3, $4, $5",                                                                                                             // 5
244    "$1, $2, $3, $4, $5, $6",                                                                                                         // 6
245    "$1, $2, $3, $4, $5, $6, $7",                                                                                                     // 7
246    "$1, $2, $3, $4, $5, $6, $7, $8",                                                                                                 // 8
247    "$1, $2, $3, $4, $5, $6, $7, $8, $9",                                                                                             // 9
248    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10",                                                                                        // 10
249    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11",                                                                                   // 11
250    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12",                                                                              // 12
251    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13",                                                                         // 13
252    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14",                                                                    // 14
253    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15",                                                               // 15
254    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16",                                                          // 16
255    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17",                                                     // 17
256    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18",                                                // 18
257    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19",                                           // 19
258    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20",                                      // 20
259    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21",                                 // 21
260    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22",                            // 22
261    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23",                       // 23
262    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24",                  // 24
263    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25",             // 25
264    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26",        // 26
265    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27",   // 27
266    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28", // 28
267    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29", // 29
268    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30", // 30
269    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31", // 31
270    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32", // 32
271];
272
273/// Write PostgreSQL IN placeholders directly to a buffer.
274///
275/// Optimizations:
276/// - Pre-computed patterns for counts 1-20 starting at $1 (zero allocation)
277/// - Batch placeholder lookup for larger counts
278/// - Minimized branch predictions in hot loop
279#[inline]
280#[allow(clippy::needless_range_loop)]
281pub fn write_postgres_in_pattern(buf: &mut String, start_idx: usize, count: usize) {
282    if count == 0 {
283        return;
284    }
285
286    // Fast path: common case of starting at $1 with small counts
287    if start_idx == 1 && count < POSTGRES_IN_FROM_1.len() {
288        buf.push_str(POSTGRES_IN_FROM_1[count]);
289        return;
290    }
291
292    // Calculate required capacity: each placeholder is at most 4 chars + 2 for ", "
293    // We reserve a bit more to avoid reallocations
294    buf.reserve(count * 6);
295
296    // Optimized loop with reduced branching
297    let end_idx = start_idx + count;
298    let table_len = POSTGRES_PLACEHOLDERS.len();
299
300    if end_idx <= table_len {
301        // All placeholders in table - fast path
302        buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
303        for idx in (start_idx + 1)..end_idx {
304            buf.push_str(", ");
305            buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
306        }
307    } else if start_idx >= table_len {
308        // All placeholders need formatting - use Write
309        let _ = write!(buf, "${}", start_idx);
310        for idx in (start_idx + 1)..end_idx {
311            let _ = write!(buf, ", ${}", idx);
312        }
313    } else {
314        // Mixed: some in table, some need formatting
315        buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
316        for idx in (start_idx + 1)..table_len.min(end_idx) {
317            buf.push_str(", ");
318            buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
319        }
320        for idx in table_len..end_idx {
321            let _ = write!(buf, ", ${}", idx);
322        }
323    }
324}
325
326impl DatabaseType {
327    /// Get the parameter placeholder for this database type.
328    ///
329    /// For MySQL and SQLite, this returns a borrowed static string (zero allocation).
330    /// For PostgreSQL with index 1-128, this returns a borrowed static string (zero allocation).
331    /// For PostgreSQL with index > 128, this returns an owned formatted string.
332    ///
333    /// # Examples
334    ///
335    /// ```rust
336    /// use prax_query::sql::DatabaseType;
337    ///
338    /// // PostgreSQL uses numbered placeholders (zero allocation for 1-128)
339    /// assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
340    /// assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
341    /// assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
342    ///
343    /// // MySQL and SQLite use ? (zero allocation)
344    /// assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
345    /// assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
346    /// ```
347    #[inline]
348    pub fn placeholder(&self, index: usize) -> Cow<'static, str> {
349        match self {
350            Self::PostgreSQL => {
351                // Use pre-computed lookup for common indices (1-128)
352                if index > 0 && index < POSTGRES_PLACEHOLDERS.len() {
353                    Cow::Borrowed(POSTGRES_PLACEHOLDERS[index])
354                } else {
355                    // Fall back to format for rare cases (0 or > 128)
356                    Cow::Owned(format!("${}", index))
357                }
358            }
359            Self::MySQL | Self::SQLite => Cow::Borrowed(QUESTION_MARK_PLACEHOLDER),
360        }
361    }
362
363    /// Get the parameter placeholder as a String.
364    ///
365    /// This is a convenience method that always allocates. Prefer `placeholder()`
366    /// when you can work with `Cow<str>` to avoid unnecessary allocations.
367    #[inline]
368    pub fn placeholder_string(&self, index: usize) -> String {
369        self.placeholder(index).into_owned()
370    }
371}
372
373
374/// A SQL builder for constructing queries.
375#[derive(Debug, Clone)]
376pub struct SqlBuilder {
377    db_type: DatabaseType,
378    parts: Vec<String>,
379    params: Vec<FilterValue>,
380}
381
382impl SqlBuilder {
383    /// Create a new SQL builder.
384    pub fn new(db_type: DatabaseType) -> Self {
385        Self {
386            db_type,
387            parts: Vec::new(),
388            params: Vec::new(),
389        }
390    }
391
392    /// Create a PostgreSQL SQL builder.
393    pub fn postgres() -> Self {
394        Self::new(DatabaseType::PostgreSQL)
395    }
396
397    /// Create a MySQL SQL builder.
398    pub fn mysql() -> Self {
399        Self::new(DatabaseType::MySQL)
400    }
401
402    /// Create a SQLite SQL builder.
403    pub fn sqlite() -> Self {
404        Self::new(DatabaseType::SQLite)
405    }
406
407    /// Push a literal SQL string.
408    pub fn push(&mut self, sql: impl AsRef<str>) -> &mut Self {
409        self.parts.push(sql.as_ref().to_string());
410        self
411    }
412
413    /// Push a SQL string with a parameter.
414    pub fn push_param(&mut self, value: impl Into<FilterValue>) -> &mut Self {
415        let index = self.params.len() + 1;
416        // Use into_owned() since we need to store it in Vec<String>
417        // For MySQL/SQLite, this still benefits from the static str being used
418        self.parts
419            .push(self.db_type.placeholder(index).into_owned());
420        self.params.push(value.into());
421        self
422    }
423
424    /// Push an identifier (properly quoted if needed).
425    pub fn push_identifier(&mut self, name: &str) -> &mut Self {
426        self.parts.push(quote_identifier(name));
427        self
428    }
429
430    /// Push a separator between parts.
431    pub fn push_sep(&mut self, sep: &str) -> &mut Self {
432        self.parts.push(sep.to_string());
433        self
434    }
435
436    /// Build the final SQL string and parameters.
437    pub fn build(self) -> (String, Vec<FilterValue>) {
438        (self.parts.join(""), self.params)
439    }
440
441    /// Get the current SQL string (without consuming).
442    pub fn sql(&self) -> String {
443        self.parts.join("")
444    }
445
446    /// Get the current parameters.
447    pub fn params(&self) -> &[FilterValue] {
448        &self.params
449    }
450
451    /// Get the next parameter index.
452    pub fn next_param_index(&self) -> usize {
453        self.params.len() + 1
454    }
455}
456
457impl Default for SqlBuilder {
458    fn default() -> Self {
459        Self::postgres()
460    }
461}
462
463// ==============================================================================
464// Optimized SQL Builder
465// ==============================================================================
466
467/// Capacity hints for different query types.
468#[derive(Debug, Clone, Copy)]
469pub enum QueryCapacity {
470    /// Simple SELECT query (e.g., SELECT * FROM users WHERE id = $1)
471    SimpleSelect,
472    /// SELECT with multiple conditions
473    SelectWithFilters(usize),
474    /// INSERT with N columns
475    Insert(usize),
476    /// UPDATE with N columns
477    Update(usize),
478    /// DELETE query
479    Delete,
480    /// Custom capacity
481    Custom(usize),
482}
483
484impl QueryCapacity {
485    /// Get the estimated capacity in bytes.
486    #[inline]
487    pub const fn estimate(&self) -> usize {
488        match self {
489            Self::SimpleSelect => 64,
490            Self::SelectWithFilters(n) => 64 + *n * 32,
491            Self::Insert(cols) => 32 + *cols * 16,
492            Self::Update(cols) => 32 + *cols * 20,
493            Self::Delete => 48,
494            Self::Custom(cap) => *cap,
495        }
496    }
497}
498
499/// An optimized SQL builder that uses a single String buffer.
500///
501/// This builder is more efficient than `Sql` for complex queries because:
502/// - Uses a single pre-allocated String instead of Vec<String>
503/// - Uses `write!` macro instead of format! + push
504/// - Provides batch placeholder generation for IN clauses
505///
506/// # Examples
507///
508/// ```rust
509/// use prax_query::sql::{FastSqlBuilder, DatabaseType, QueryCapacity};
510///
511/// // Simple query with pre-allocated capacity
512/// let mut builder = FastSqlBuilder::with_capacity(
513///     DatabaseType::PostgreSQL,
514///     QueryCapacity::SimpleSelect
515/// );
516/// builder.push_str("SELECT * FROM users WHERE id = ");
517/// builder.bind(42i64);
518/// let (sql, params) = builder.build();
519/// assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
520///
521/// // Complex query with multiple bindings
522/// let mut builder = FastSqlBuilder::with_capacity(
523///     DatabaseType::PostgreSQL,
524///     QueryCapacity::SelectWithFilters(3)
525/// );
526/// builder.push_str("SELECT * FROM users WHERE active = ");
527/// builder.bind(true);
528/// builder.push_str(" AND age > ");
529/// builder.bind(18i64);
530/// builder.push_str(" ORDER BY created_at LIMIT ");
531/// builder.bind(10i64);
532/// let (sql, _) = builder.build();
533/// assert!(sql.contains("$1") && sql.contains("$2") && sql.contains("$3"));
534/// ```
535#[derive(Debug, Clone)]
536pub struct FastSqlBuilder {
537    /// The SQL string buffer.
538    buffer: String,
539    /// The parameter values.
540    params: Vec<FilterValue>,
541    /// The database type.
542    db_type: DatabaseType,
543}
544
545impl FastSqlBuilder {
546    /// Create a new builder with the specified database type.
547    #[inline]
548    pub fn new(db_type: DatabaseType) -> Self {
549        Self {
550            buffer: String::new(),
551            params: Vec::new(),
552            db_type,
553        }
554    }
555
556    /// Create a new builder with pre-allocated capacity.
557    #[inline]
558    pub fn with_capacity(db_type: DatabaseType, capacity: QueryCapacity) -> Self {
559        Self {
560            buffer: String::with_capacity(capacity.estimate()),
561            params: Vec::with_capacity(match capacity {
562                QueryCapacity::SimpleSelect => 2,
563                QueryCapacity::SelectWithFilters(n) => n,
564                QueryCapacity::Insert(n) => n,
565                QueryCapacity::Update(n) => n + 1,
566                QueryCapacity::Delete => 2,
567                QueryCapacity::Custom(n) => n / 16,
568            }),
569            db_type,
570        }
571    }
572
573    /// Create a PostgreSQL builder with pre-allocated capacity.
574    #[inline]
575    pub fn postgres(capacity: QueryCapacity) -> Self {
576        Self::with_capacity(DatabaseType::PostgreSQL, capacity)
577    }
578
579    /// Create a MySQL builder with pre-allocated capacity.
580    #[inline]
581    pub fn mysql(capacity: QueryCapacity) -> Self {
582        Self::with_capacity(DatabaseType::MySQL, capacity)
583    }
584
585    /// Create a SQLite builder with pre-allocated capacity.
586    #[inline]
587    pub fn sqlite(capacity: QueryCapacity) -> Self {
588        Self::with_capacity(DatabaseType::SQLite, capacity)
589    }
590
591    /// Push a string slice directly (zero allocation).
592    #[inline]
593    pub fn push_str(&mut self, s: &str) -> &mut Self {
594        self.buffer.push_str(s);
595        self
596    }
597
598    /// Push a single character.
599    #[inline]
600    pub fn push_char(&mut self, c: char) -> &mut Self {
601        self.buffer.push(c);
602        self
603    }
604
605    /// Bind a parameter and append its placeholder.
606    #[inline]
607    pub fn bind(&mut self, value: impl Into<FilterValue>) -> &mut Self {
608        let index = self.params.len() + 1;
609        let placeholder = self.db_type.placeholder(index);
610        self.buffer.push_str(&placeholder);
611        self.params.push(value.into());
612        self
613    }
614
615    /// Push a string and bind a value.
616    #[inline]
617    pub fn push_bind(&mut self, s: &str, value: impl Into<FilterValue>) -> &mut Self {
618        self.push_str(s);
619        self.bind(value)
620    }
621
622    /// Generate placeholders for an IN clause efficiently.
623    ///
624    /// This is much faster than calling `bind()` in a loop because it:
625    /// - Uses pre-computed placeholder patterns for common sizes
626    /// - Pre-calculates the total string length for larger sizes
627    /// - Generates all placeholders in one pass
628    ///
629    /// # Examples
630    ///
631    /// ```rust
632    /// use prax_query::sql::{FastSqlBuilder, DatabaseType, QueryCapacity};
633    /// use prax_query::filter::FilterValue;
634    ///
635    /// let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
636    /// builder.push_str("SELECT * FROM users WHERE id IN (");
637    ///
638    /// let values: Vec<FilterValue> = vec![1i64, 2, 3, 4, 5].into_iter()
639    ///     .map(FilterValue::Int)
640    ///     .collect();
641    /// builder.bind_in_clause(values);
642    /// builder.push_char(')');
643    ///
644    /// let (sql, params) = builder.build();
645    /// assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
646    /// assert_eq!(params.len(), 5);
647    /// ```
648    pub fn bind_in_clause(&mut self, values: impl IntoIterator<Item = FilterValue>) -> &mut Self {
649        let values: Vec<FilterValue> = values.into_iter().collect();
650        if values.is_empty() {
651            return self;
652        }
653
654        let start_index = self.params.len() + 1;
655        let count = values.len();
656
657        // Generate placeholders efficiently
658        match self.db_type {
659            DatabaseType::PostgreSQL => {
660                // Pre-calculate capacity: "$N, " is about 4-5 chars per param
661                let estimated_len = count * 5;
662                self.buffer.reserve(estimated_len);
663
664                for (i, _) in values.iter().enumerate() {
665                    if i > 0 {
666                        self.buffer.push_str(", ");
667                    }
668                    let idx = start_index + i;
669                    if idx < POSTGRES_PLACEHOLDERS.len() {
670                        self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
671                    } else {
672                        let _ = write!(self.buffer, "${}", idx);
673                    }
674                }
675            }
676            DatabaseType::MySQL | DatabaseType::SQLite => {
677                // Use pre-computed pattern for small sizes (up to 32)
678                if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
679                    self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
680                } else {
681                    // Fall back to generation for larger sizes or offset start
682                    let estimated_len = count * 3; // "?, " per param
683                    self.buffer.reserve(estimated_len);
684                    for i in 0..count {
685                        if i > 0 {
686                            self.buffer.push_str(", ");
687                        }
688                        self.buffer.push('?');
689                    }
690                }
691            }
692        }
693
694        self.params.extend(values);
695        self
696    }
697
698    /// Bind a slice of values for an IN clause without collecting.
699    ///
700    /// This is more efficient than `bind_in_clause` when you already have a slice,
701    /// as it avoids collecting into a Vec first.
702    ///
703    /// # Examples
704    ///
705    /// ```rust
706    /// use prax_query::sql::{FastSqlBuilder, DatabaseType, QueryCapacity};
707    ///
708    /// let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
709    /// builder.push_str("SELECT * FROM users WHERE id IN (");
710    ///
711    /// let ids: &[i64] = &[1, 2, 3, 4, 5];
712    /// builder.bind_in_slice(ids);
713    /// builder.push_char(')');
714    ///
715    /// let (sql, params) = builder.build();
716    /// assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
717    /// assert_eq!(params.len(), 5);
718    /// ```
719    pub fn bind_in_slice<T: Into<FilterValue> + Clone>(&mut self, values: &[T]) -> &mut Self {
720        if values.is_empty() {
721            return self;
722        }
723
724        let start_index = self.params.len() + 1;
725        let count = values.len();
726
727        // Generate placeholders
728        match self.db_type {
729            DatabaseType::PostgreSQL => {
730                let estimated_len = count * 5;
731                self.buffer.reserve(estimated_len);
732
733                for i in 0..count {
734                    if i > 0 {
735                        self.buffer.push_str(", ");
736                    }
737                    let idx = start_index + i;
738                    if idx < POSTGRES_PLACEHOLDERS.len() {
739                        self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
740                    } else {
741                        let _ = write!(self.buffer, "${}", idx);
742                    }
743                }
744            }
745            DatabaseType::MySQL | DatabaseType::SQLite => {
746                if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
747                    self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
748                } else {
749                    let estimated_len = count * 3;
750                    self.buffer.reserve(estimated_len);
751                    for i in 0..count {
752                        if i > 0 {
753                            self.buffer.push_str(", ");
754                        }
755                        self.buffer.push('?');
756                    }
757                }
758            }
759        }
760
761        // Add params
762        self.params.reserve(count);
763        for v in values {
764            self.params.push(v.clone().into());
765        }
766        self
767    }
768
769    /// Write formatted content using the `write!` macro.
770    ///
771    /// This is more efficient than `format!()` + `push_str()` as it
772    /// writes directly to the buffer without intermediate allocation.
773    #[inline]
774    pub fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> &mut Self {
775        let _ = self.buffer.write_fmt(args);
776        self
777    }
778
779    /// Push an identifier, quoting if necessary.
780    #[inline]
781    pub fn push_identifier(&mut self, name: &str) -> &mut Self {
782        if needs_quoting(name) {
783            self.buffer.push('"');
784            // Escape any existing quotes
785            for c in name.chars() {
786                if c == '"' {
787                    self.buffer.push_str("\"\"");
788                } else {
789                    self.buffer.push(c);
790                }
791            }
792            self.buffer.push('"');
793        } else {
794            self.buffer.push_str(name);
795        }
796        self
797    }
798
799    /// Push conditionally.
800    #[inline]
801    pub fn push_if(&mut self, condition: bool, s: &str) -> &mut Self {
802        if condition {
803            self.push_str(s);
804        }
805        self
806    }
807
808    /// Bind conditionally.
809    #[inline]
810    pub fn bind_if(&mut self, condition: bool, value: impl Into<FilterValue>) -> &mut Self {
811        if condition {
812            self.bind(value);
813        }
814        self
815    }
816
817    /// Get the current SQL string.
818    #[inline]
819    pub fn sql(&self) -> &str {
820        &self.buffer
821    }
822
823    /// Get the current parameters.
824    #[inline]
825    pub fn params(&self) -> &[FilterValue] {
826        &self.params
827    }
828
829    /// Get the number of parameters.
830    #[inline]
831    pub fn param_count(&self) -> usize {
832        self.params.len()
833    }
834
835    /// Build the final SQL string and parameters.
836    #[inline]
837    pub fn build(self) -> (String, Vec<FilterValue>) {
838        let sql_len = self.buffer.len();
839        let param_count = self.params.len();
840        debug!(sql_len, param_count, db_type = ?self.db_type, "FastSqlBuilder::build()");
841        (self.buffer, self.params)
842    }
843
844    /// Build and return only the SQL string.
845    #[inline]
846    pub fn build_sql(self) -> String {
847        self.buffer
848    }
849}
850
851// ==============================================================================
852// SQL Templates for Common Queries
853// ==============================================================================
854
855/// Pre-built SQL templates for common query patterns.
856///
857/// Using templates avoids repeated string construction for common operations.
858pub mod templates {
859    use super::*;
860
861    /// Generate a simple SELECT query template.
862    ///
863    /// # Examples
864    ///
865    /// ```rust
866    /// use prax_query::sql::templates;
867    ///
868    /// let template = templates::select_by_id("users", &["id", "name", "email"]);
869    /// assert!(template.contains("SELECT"));
870    /// assert!(template.contains("FROM users"));
871    /// assert!(template.contains("WHERE id = $1"));
872    /// ```
873    pub fn select_by_id(table: &str, columns: &[&str]) -> String {
874        let cols = if columns.is_empty() {
875            "*".to_string()
876        } else {
877            columns.join(", ")
878        };
879        format!("SELECT {} FROM {} WHERE id = $1", cols, table)
880    }
881
882    /// Generate an INSERT query template for PostgreSQL.
883    ///
884    /// # Examples
885    ///
886    /// ```rust
887    /// use prax_query::sql::templates;
888    ///
889    /// let template = templates::insert_returning("users", &["name", "email"]);
890    /// assert!(template.contains("INSERT INTO users"));
891    /// assert!(template.contains("RETURNING *"));
892    /// ```
893    pub fn insert_returning(table: &str, columns: &[&str]) -> String {
894        let cols = columns.join(", ");
895        let placeholders: Vec<String> = (1..=columns.len())
896            .map(|i| {
897                if i < POSTGRES_PLACEHOLDERS.len() {
898                    POSTGRES_PLACEHOLDERS[i].to_string()
899                } else {
900                    format!("${}", i)
901                }
902            })
903            .collect();
904        format!(
905            "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
906            table,
907            cols,
908            placeholders.join(", ")
909        )
910    }
911
912    /// Generate an UPDATE query template.
913    ///
914    /// # Examples
915    ///
916    /// ```rust
917    /// use prax_query::sql::templates;
918    ///
919    /// let template = templates::update_by_id("users", &["name", "email"]);
920    /// assert!(template.contains("UPDATE users SET"));
921    /// assert!(template.contains("WHERE id = $3"));
922    /// ```
923    pub fn update_by_id(table: &str, columns: &[&str]) -> String {
924        let sets: Vec<String> = columns
925            .iter()
926            .enumerate()
927            .map(|(i, col)| {
928                let idx = i + 1;
929                if idx < POSTGRES_PLACEHOLDERS.len() {
930                    format!("{} = {}", col, POSTGRES_PLACEHOLDERS[idx])
931                } else {
932                    format!("{} = ${}", col, idx)
933                }
934            })
935            .collect();
936        let id_idx = columns.len() + 1;
937        let id_placeholder = if id_idx < POSTGRES_PLACEHOLDERS.len() {
938            POSTGRES_PLACEHOLDERS[id_idx]
939        } else {
940            "$?"
941        };
942        format!(
943            "UPDATE {} SET {} WHERE id = {}",
944            table,
945            sets.join(", "),
946            id_placeholder
947        )
948    }
949
950    /// Generate a DELETE query template.
951    ///
952    /// # Examples
953    ///
954    /// ```rust
955    /// use prax_query::sql::templates;
956    ///
957    /// let template = templates::delete_by_id("users");
958    /// assert_eq!(template, "DELETE FROM users WHERE id = $1");
959    /// ```
960    pub fn delete_by_id(table: &str) -> String {
961        format!("DELETE FROM {} WHERE id = $1", table)
962    }
963
964    /// Generate placeholders for a batch INSERT.
965    ///
966    /// # Examples
967    ///
968    /// ```rust
969    /// use prax_query::sql::templates;
970    /// use prax_query::sql::DatabaseType;
971    ///
972    /// let placeholders = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
973    /// assert_eq!(placeholders, "($1, $2, $3), ($4, $5, $6)");
974    /// ```
975    pub fn batch_placeholders(db_type: DatabaseType, columns: usize, rows: usize) -> String {
976        let mut result = String::with_capacity(rows * columns * 4);
977        let mut param_idx = 1;
978
979        for row in 0..rows {
980            if row > 0 {
981                result.push_str(", ");
982            }
983            result.push('(');
984            for col in 0..columns {
985                if col > 0 {
986                    result.push_str(", ");
987                }
988                match db_type {
989                    DatabaseType::PostgreSQL => {
990                        if param_idx < POSTGRES_PLACEHOLDERS.len() {
991                            result.push_str(POSTGRES_PLACEHOLDERS[param_idx]);
992                        } else {
993                            let _ = write!(result, "${}", param_idx);
994                        }
995                        param_idx += 1;
996                    }
997                    DatabaseType::MySQL | DatabaseType::SQLite => {
998                        result.push('?');
999                    }
1000                }
1001            }
1002            result.push(')');
1003        }
1004
1005        result
1006    }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012
1013    #[test]
1014    fn test_escape_identifier() {
1015        assert_eq!(escape_identifier("user"), "\"user\"");
1016        assert_eq!(escape_identifier("my_table"), "\"my_table\"");
1017        assert_eq!(escape_identifier("has\"quote"), "\"has\"\"quote\"");
1018    }
1019
1020    #[test]
1021    fn test_needs_quoting() {
1022        assert!(needs_quoting("user"));
1023        assert!(needs_quoting("order"));
1024        assert!(needs_quoting("has space"));
1025        assert!(!needs_quoting("my_table"));
1026        assert!(!needs_quoting("users"));
1027    }
1028
1029    #[test]
1030    fn test_quote_identifier() {
1031        assert_eq!(quote_identifier("user"), "\"user\"");
1032        assert_eq!(quote_identifier("my_table"), "my_table");
1033    }
1034
1035    #[test]
1036    fn test_database_placeholder() {
1037        // Basic placeholder values
1038        assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
1039        assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
1040        assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
1041        assert_eq!(DatabaseType::PostgreSQL.placeholder(128).as_ref(), "$128");
1042        assert_eq!(DatabaseType::PostgreSQL.placeholder(256).as_ref(), "$256");
1043        assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
1044        assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
1045
1046        // Verify MySQL/SQLite return borrowed (zero allocation)
1047        assert!(matches!(
1048            DatabaseType::MySQL.placeholder(1),
1049            Cow::Borrowed(_)
1050        ));
1051        assert!(matches!(
1052            DatabaseType::SQLite.placeholder(1),
1053            Cow::Borrowed(_)
1054        ));
1055
1056        // PostgreSQL returns borrowed for indices 1-256 (zero allocation via lookup table)
1057        assert!(matches!(
1058            DatabaseType::PostgreSQL.placeholder(1),
1059            Cow::Borrowed(_)
1060        ));
1061        assert!(matches!(
1062            DatabaseType::PostgreSQL.placeholder(50),
1063            Cow::Borrowed(_)
1064        ));
1065        assert!(matches!(
1066            DatabaseType::PostgreSQL.placeholder(128),
1067            Cow::Borrowed(_)
1068        ));
1069        assert!(matches!(
1070            DatabaseType::PostgreSQL.placeholder(256),
1071            Cow::Borrowed(_)
1072        ));
1073
1074        // PostgreSQL returns owned for indices > 256 (must format)
1075        assert!(matches!(
1076            DatabaseType::PostgreSQL.placeholder(257),
1077            Cow::Owned(_)
1078        ));
1079        assert_eq!(DatabaseType::PostgreSQL.placeholder(257).as_ref(), "$257");
1080        assert_eq!(DatabaseType::PostgreSQL.placeholder(200).as_ref(), "$200");
1081
1082        // Edge case: index 0 falls back to format (unusual but handled)
1083        assert!(matches!(
1084            DatabaseType::PostgreSQL.placeholder(0),
1085            Cow::Owned(_)
1086        ));
1087        assert_eq!(DatabaseType::PostgreSQL.placeholder(0).as_ref(), "$0");
1088    }
1089
1090    #[test]
1091    fn test_sql_builder() {
1092        let mut builder = SqlBuilder::postgres();
1093        builder
1094            .push("SELECT * FROM ")
1095            .push_identifier("user")
1096            .push(" WHERE ")
1097            .push_identifier("id")
1098            .push(" = ")
1099            .push_param(42i32);
1100
1101        let (sql, params) = builder.build();
1102        assert_eq!(sql, "SELECT * FROM \"user\" WHERE id = $1");
1103        assert_eq!(params.len(), 1);
1104    }
1105
1106    // FastSqlBuilder tests
1107    #[test]
1108    fn test_fast_builder_simple() {
1109        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1110        builder.push_str("SELECT * FROM users WHERE id = ");
1111        builder.bind(42i64);
1112        let (sql, params) = builder.build();
1113        assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1114        assert_eq!(params.len(), 1);
1115    }
1116
1117    #[test]
1118    fn test_fast_builder_complex() {
1119        let mut builder = FastSqlBuilder::with_capacity(
1120            DatabaseType::PostgreSQL,
1121            QueryCapacity::SelectWithFilters(5),
1122        );
1123        builder
1124            .push_str("SELECT * FROM users WHERE active = ")
1125            .bind(true)
1126            .push_str(" AND age > ")
1127            .bind(18i64)
1128            .push_str(" AND status = ")
1129            .bind("approved")
1130            .push_str(" ORDER BY created_at LIMIT ")
1131            .bind(10i64);
1132
1133        let (sql, params) = builder.build();
1134        assert!(sql.contains("$1"));
1135        assert!(sql.contains("$4"));
1136        assert_eq!(params.len(), 4);
1137    }
1138
1139    #[test]
1140    fn test_fast_builder_in_clause_postgres() {
1141        let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
1142        builder.push_str("SELECT * FROM users WHERE id IN (");
1143        let values: Vec<FilterValue> = (1..=5).map(|i| FilterValue::Int(i)).collect();
1144        builder.bind_in_clause(values);
1145        builder.push_char(')');
1146
1147        let (sql, params) = builder.build();
1148        assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
1149        assert_eq!(params.len(), 5);
1150    }
1151
1152    #[test]
1153    fn test_fast_builder_in_clause_mysql() {
1154        let mut builder = FastSqlBuilder::mysql(QueryCapacity::Custom(128));
1155        builder.push_str("SELECT * FROM users WHERE id IN (");
1156        let values: Vec<FilterValue> = (1..=5).map(|i| FilterValue::Int(i)).collect();
1157        builder.bind_in_clause(values);
1158        builder.push_char(')');
1159
1160        let (sql, params) = builder.build();
1161        assert_eq!(sql, "SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?)");
1162        assert_eq!(params.len(), 5);
1163    }
1164
1165    #[test]
1166    fn test_fast_builder_identifier() {
1167        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1168        builder.push_str("SELECT * FROM ");
1169        builder.push_identifier("user"); // reserved word
1170        builder.push_str(" WHERE ");
1171        builder.push_identifier("my_column"); // not reserved
1172        builder.push_str(" = ");
1173        builder.bind(1i64);
1174
1175        let (sql, _) = builder.build();
1176        assert_eq!(sql, "SELECT * FROM \"user\" WHERE my_column = $1");
1177    }
1178
1179    #[test]
1180    fn test_fast_builder_identifier_with_quotes() {
1181        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1182        builder.push_str("SELECT * FROM ");
1183        builder.push_identifier("has\"quote");
1184
1185        let sql = builder.build_sql();
1186        assert_eq!(sql, "SELECT * FROM \"has\"\"quote\"");
1187    }
1188
1189    #[test]
1190    fn test_fast_builder_conditional() {
1191        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SelectWithFilters(2));
1192        builder.push_str("SELECT * FROM users WHERE 1=1");
1193        builder.push_if(true, " AND active = true");
1194        builder.push_if(false, " AND deleted = false");
1195
1196        let sql = builder.build_sql();
1197        assert_eq!(sql, "SELECT * FROM users WHERE 1=1 AND active = true");
1198    }
1199
1200    // Template tests
1201    #[test]
1202    fn test_template_select_by_id() {
1203        let sql = templates::select_by_id("users", &["id", "name", "email"]);
1204        assert_eq!(sql, "SELECT id, name, email FROM users WHERE id = $1");
1205    }
1206
1207    #[test]
1208    fn test_template_select_by_id_all_columns() {
1209        let sql = templates::select_by_id("users", &[]);
1210        assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1211    }
1212
1213    #[test]
1214    fn test_template_insert_returning() {
1215        let sql = templates::insert_returning("users", &["name", "email"]);
1216        assert_eq!(
1217            sql,
1218            "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
1219        );
1220    }
1221
1222    #[test]
1223    fn test_template_update_by_id() {
1224        let sql = templates::update_by_id("users", &["name", "email"]);
1225        assert_eq!(sql, "UPDATE users SET name = $1, email = $2 WHERE id = $3");
1226    }
1227
1228    #[test]
1229    fn test_template_delete_by_id() {
1230        let sql = templates::delete_by_id("users");
1231        assert_eq!(sql, "DELETE FROM users WHERE id = $1");
1232    }
1233
1234    #[test]
1235    fn test_template_batch_placeholders_postgres() {
1236        let sql = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
1237        assert_eq!(sql, "($1, $2, $3), ($4, $5, $6)");
1238    }
1239
1240    #[test]
1241    fn test_template_batch_placeholders_mysql() {
1242        let sql = templates::batch_placeholders(DatabaseType::MySQL, 3, 2);
1243        assert_eq!(sql, "(?, ?, ?), (?, ?, ?)");
1244    }
1245
1246    #[test]
1247    fn test_query_capacity_estimates() {
1248        assert_eq!(QueryCapacity::SimpleSelect.estimate(), 64);
1249        assert_eq!(QueryCapacity::SelectWithFilters(5).estimate(), 64 + 5 * 32);
1250        assert_eq!(QueryCapacity::Insert(10).estimate(), 32 + 10 * 16);
1251        assert_eq!(QueryCapacity::Update(5).estimate(), 32 + 5 * 20);
1252        assert_eq!(QueryCapacity::Delete.estimate(), 48);
1253        assert_eq!(QueryCapacity::Custom(256).estimate(), 256);
1254    }
1255}