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