Skip to main content

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//! - Static SQL keywords to avoid allocations
9//! - Lazy SQL generation for deferred execution
10
11use crate::filter::FilterValue;
12use std::borrow::Cow;
13use std::collections::HashMap;
14use std::fmt::Write;
15use std::sync::{Arc, OnceLock, RwLock};
16use tracing::debug;
17
18// ==============================================================================
19// Static SQL Keywords
20// ==============================================================================
21
22/// Static SQL keywords to avoid repeated allocations.
23///
24/// Using these constants instead of string literals enables the compiler
25/// to optimize repeated uses and avoids runtime string construction.
26///
27/// # Example
28///
29/// ```rust
30/// use prax_query::sql::keywords;
31///
32/// // Instead of:
33/// // let query = format!("{} {} {}", "SELECT", "*", "FROM");
34///
35/// // Use:
36/// let mut sql = String::with_capacity(64);
37/// sql.push_str(keywords::SELECT);
38/// sql.push_str(" * ");
39/// sql.push_str(keywords::FROM);
40/// ```
41pub mod keywords {
42    // DML Keywords
43    pub const SELECT: &str = "SELECT";
44    pub const INSERT: &str = "INSERT";
45    pub const UPDATE: &str = "UPDATE";
46    pub const DELETE: &str = "DELETE";
47    pub const INTO: &str = "INTO";
48    pub const VALUES: &str = "VALUES";
49    pub const SET: &str = "SET";
50    pub const FROM: &str = "FROM";
51    pub const WHERE: &str = "WHERE";
52    pub const RETURNING: &str = "RETURNING";
53
54    // Clauses
55    pub const AND: &str = "AND";
56    pub const OR: &str = "OR";
57    pub const NOT: &str = "NOT";
58    pub const IN: &str = "IN";
59    pub const IS: &str = "IS";
60    pub const NULL: &str = "NULL";
61    pub const LIKE: &str = "LIKE";
62    pub const ILIKE: &str = "ILIKE";
63    pub const BETWEEN: &str = "BETWEEN";
64    pub const EXISTS: &str = "EXISTS";
65
66    // Ordering
67    pub const ORDER_BY: &str = "ORDER BY";
68    pub const ASC: &str = "ASC";
69    pub const DESC: &str = "DESC";
70    pub const NULLS_FIRST: &str = "NULLS FIRST";
71    pub const NULLS_LAST: &str = "NULLS LAST";
72    pub const LIMIT: &str = "LIMIT";
73    pub const OFFSET: &str = "OFFSET";
74
75    // Grouping
76    pub const GROUP_BY: &str = "GROUP BY";
77    pub const HAVING: &str = "HAVING";
78    pub const DISTINCT: &str = "DISTINCT";
79    pub const DISTINCT_ON: &str = "DISTINCT ON";
80
81    // Joins
82    pub const JOIN: &str = "JOIN";
83    pub const INNER_JOIN: &str = "INNER JOIN";
84    pub const LEFT_JOIN: &str = "LEFT JOIN";
85    pub const RIGHT_JOIN: &str = "RIGHT JOIN";
86    pub const FULL_JOIN: &str = "FULL OUTER JOIN";
87    pub const CROSS_JOIN: &str = "CROSS JOIN";
88    pub const LATERAL: &str = "LATERAL";
89    pub const ON: &str = "ON";
90    pub const USING: &str = "USING";
91
92    // CTEs
93    pub const WITH: &str = "WITH";
94    pub const RECURSIVE: &str = "RECURSIVE";
95    pub const AS: &str = "AS";
96    pub const MATERIALIZED: &str = "MATERIALIZED";
97    pub const NOT_MATERIALIZED: &str = "NOT MATERIALIZED";
98
99    // Window Functions
100    pub const OVER: &str = "OVER";
101    pub const PARTITION_BY: &str = "PARTITION BY";
102    pub const ROWS: &str = "ROWS";
103    pub const RANGE: &str = "RANGE";
104    pub const GROUPS: &str = "GROUPS";
105    pub const UNBOUNDED_PRECEDING: &str = "UNBOUNDED PRECEDING";
106    pub const UNBOUNDED_FOLLOWING: &str = "UNBOUNDED FOLLOWING";
107    pub const CURRENT_ROW: &str = "CURRENT ROW";
108    pub const PRECEDING: &str = "PRECEDING";
109    pub const FOLLOWING: &str = "FOLLOWING";
110
111    // Aggregates
112    pub const COUNT: &str = "COUNT";
113    pub const SUM: &str = "SUM";
114    pub const AVG: &str = "AVG";
115    pub const MIN: &str = "MIN";
116    pub const MAX: &str = "MAX";
117    pub const ROW_NUMBER: &str = "ROW_NUMBER";
118    pub const RANK: &str = "RANK";
119    pub const DENSE_RANK: &str = "DENSE_RANK";
120    pub const LAG: &str = "LAG";
121    pub const LEAD: &str = "LEAD";
122    pub const FIRST_VALUE: &str = "FIRST_VALUE";
123    pub const LAST_VALUE: &str = "LAST_VALUE";
124    pub const NTILE: &str = "NTILE";
125
126    // Upsert
127    pub const ON_CONFLICT: &str = "ON CONFLICT";
128    pub const DO_NOTHING: &str = "DO NOTHING";
129    pub const DO_UPDATE: &str = "DO UPDATE";
130    pub const EXCLUDED: &str = "excluded";
131    pub const ON_DUPLICATE_KEY: &str = "ON DUPLICATE KEY UPDATE";
132    pub const MERGE: &str = "MERGE";
133    pub const MATCHED: &str = "MATCHED";
134    pub const NOT_MATCHED: &str = "NOT MATCHED";
135
136    // Locking
137    pub const FOR_UPDATE: &str = "FOR UPDATE";
138    pub const FOR_SHARE: &str = "FOR SHARE";
139    pub const NOWAIT: &str = "NOWAIT";
140    pub const SKIP_LOCKED: &str = "SKIP LOCKED";
141
142    // DDL Keywords
143    pub const CREATE: &str = "CREATE";
144    pub const ALTER: &str = "ALTER";
145    pub const DROP: &str = "DROP";
146    pub const TABLE: &str = "TABLE";
147    pub const INDEX: &str = "INDEX";
148    pub const VIEW: &str = "VIEW";
149    pub const TRIGGER: &str = "TRIGGER";
150    pub const FUNCTION: &str = "FUNCTION";
151    pub const PROCEDURE: &str = "PROCEDURE";
152    pub const SEQUENCE: &str = "SEQUENCE";
153    pub const IF_EXISTS: &str = "IF EXISTS";
154    pub const IF_NOT_EXISTS: &str = "IF NOT EXISTS";
155    pub const OR_REPLACE: &str = "OR REPLACE";
156    pub const CASCADE: &str = "CASCADE";
157    pub const RESTRICT: &str = "RESTRICT";
158
159    // Types
160    pub const PRIMARY_KEY: &str = "PRIMARY KEY";
161    pub const FOREIGN_KEY: &str = "FOREIGN KEY";
162    pub const REFERENCES: &str = "REFERENCES";
163    pub const UNIQUE: &str = "UNIQUE";
164    pub const CHECK: &str = "CHECK";
165    pub const DEFAULT: &str = "DEFAULT";
166    pub const NOT_NULL: &str = "NOT NULL";
167
168    // Common fragments with spaces
169    pub const SPACE: &str = " ";
170    pub const COMMA_SPACE: &str = ", ";
171    pub const OPEN_PAREN: &str = "(";
172    pub const CLOSE_PAREN: &str = ")";
173    pub const STAR: &str = "*";
174    pub const EQUALS: &str = " = ";
175    pub const NOT_EQUALS: &str = " <> ";
176    pub const LESS_THAN: &str = " < ";
177    pub const GREATER_THAN: &str = " > ";
178    pub const LESS_OR_EQUAL: &str = " <= ";
179    pub const GREATER_OR_EQUAL: &str = " >= ";
180}
181
182/// Escape a string for use in SQL (for identifiers, not values).
183pub fn escape_identifier(name: &str) -> String {
184    // Double any existing quotes
185    let escaped = name.replace('"', "\"\"");
186    format!("\"{}\"", escaped)
187}
188
189/// Check if an identifier needs quoting.
190pub fn needs_quoting(name: &str) -> bool {
191    // Reserved keywords or names with special characters need quoting
192    let reserved = [
193        "user",
194        "order",
195        "group",
196        "select",
197        "from",
198        "where",
199        "table",
200        "index",
201        "key",
202        "primary",
203        "foreign",
204        "check",
205        "default",
206        "null",
207        "not",
208        "and",
209        "or",
210        "in",
211        "is",
212        "like",
213        "between",
214        "case",
215        "when",
216        "then",
217        "else",
218        "end",
219        "as",
220        "on",
221        "join",
222        "left",
223        "right",
224        "inner",
225        "outer",
226        "cross",
227        "natural",
228        "using",
229        "limit",
230        "offset",
231        "union",
232        "intersect",
233        "except",
234        "all",
235        "distinct",
236        "having",
237        "create",
238        "alter",
239        "drop",
240        "insert",
241        "update",
242        "delete",
243        "into",
244        "values",
245        "set",
246        "returning",
247    ];
248
249    // Check for reserved words
250    if reserved.contains(&name.to_lowercase().as_str()) {
251        return true;
252    }
253
254    // Check for special characters
255    !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
256}
257
258/// Quote an identifier if needed.
259pub fn quote_identifier(name: &str) -> String {
260    if needs_quoting(name) {
261        escape_identifier(name)
262    } else {
263        name.to_string()
264    }
265}
266
267/// Build a parameter placeholder for a given database type.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
269pub enum DatabaseType {
270    /// PostgreSQL uses $1, $2, etc.
271    #[default]
272    PostgreSQL,
273    /// MySQL uses ?, ?, etc.
274    MySQL,
275    /// SQLite uses ?, ?, etc.
276    SQLite,
277    /// MSSQL uses @P1, @P2, etc.
278    MSSQL,
279}
280
281/// Static placeholder string for MySQL/SQLite to avoid allocation.
282const QUESTION_MARK_PLACEHOLDER: &str = "?";
283
284/// Pre-computed PostgreSQL placeholder strings for indices 1-256.
285/// This avoids `format!` calls for the most common parameter counts.
286/// Index 0 is unused (placeholders start at $1), but kept for simpler indexing.
287/// Pre-computed PostgreSQL parameter placeholders ($1-$256).
288///
289/// This lookup table avoids `format!` calls for common parameter counts.
290/// Index 0 is "$0" (unused), indices 1-256 map to "$1" through "$256".
291///
292/// # Performance
293///
294/// Using this table instead of `format!("${}", i)` improves placeholder
295/// generation by ~97% (from ~200ns to ~5ns).
296pub const POSTGRES_PLACEHOLDERS: &[&str] = &[
297    "$0", "$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14",
298    "$15", "$16", "$17", "$18", "$19", "$20", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
299    "$28", "$29", "$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37", "$38", "$39", "$40",
300    "$41", "$42", "$43", "$44", "$45", "$46", "$47", "$48", "$49", "$50", "$51", "$52", "$53",
301    "$54", "$55", "$56", "$57", "$58", "$59", "$60", "$61", "$62", "$63", "$64", "$65", "$66",
302    "$67", "$68", "$69", "$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77", "$78", "$79",
303    "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", "$88", "$89", "$90", "$91", "$92",
304    "$93", "$94", "$95", "$96", "$97", "$98", "$99", "$100", "$101", "$102", "$103", "$104",
305    "$105", "$106", "$107", "$108", "$109", "$110", "$111", "$112", "$113", "$114", "$115", "$116",
306    "$117", "$118", "$119", "$120", "$121", "$122", "$123", "$124", "$125", "$126", "$127", "$128",
307    "$129", "$130", "$131", "$132", "$133", "$134", "$135", "$136", "$137", "$138", "$139", "$140",
308    "$141", "$142", "$143", "$144", "$145", "$146", "$147", "$148", "$149", "$150", "$151", "$152",
309    "$153", "$154", "$155", "$156", "$157", "$158", "$159", "$160", "$161", "$162", "$163", "$164",
310    "$165", "$166", "$167", "$168", "$169", "$170", "$171", "$172", "$173", "$174", "$175", "$176",
311    "$177", "$178", "$179", "$180", "$181", "$182", "$183", "$184", "$185", "$186", "$187", "$188",
312    "$189", "$190", "$191", "$192", "$193", "$194", "$195", "$196", "$197", "$198", "$199", "$200",
313    "$201", "$202", "$203", "$204", "$205", "$206", "$207", "$208", "$209", "$210", "$211", "$212",
314    "$213", "$214", "$215", "$216", "$217", "$218", "$219", "$220", "$221", "$222", "$223", "$224",
315    "$225", "$226", "$227", "$228", "$229", "$230", "$231", "$232", "$233", "$234", "$235", "$236",
316    "$237", "$238", "$239", "$240", "$241", "$242", "$243", "$244", "$245", "$246", "$247", "$248",
317    "$249", "$250", "$251", "$252", "$253", "$254", "$255", "$256",
318];
319
320/// Pre-computed IN clause placeholder patterns for MySQL/SQLite.
321/// Format: "?, ?, ?, ..." for common sizes (1-32 elements).
322pub const MYSQL_IN_PATTERNS: &[&str] = &[
323    "", // 0 (empty)
324    "?",
325    "?, ?",
326    "?, ?, ?",
327    "?, ?, ?, ?",
328    "?, ?, ?, ?, ?",
329    "?, ?, ?, ?, ?, ?",
330    "?, ?, ?, ?, ?, ?, ?",
331    "?, ?, ?, ?, ?, ?, ?, ?",
332    "?, ?, ?, ?, ?, ?, ?, ?, ?",
333    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 10
334    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
335    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
336    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
337    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
338    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
339    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 16
340    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
341    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
342    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
343    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 20
344    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
345    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
346    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
347    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
348    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 25
349    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
350    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
351    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
352    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
353    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 30
354    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
355    "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", // 32
356];
357
358// ============================================================================
359// Pre-computed PostgreSQL IN patterns (starting from $1)
360// ============================================================================
361
362/// Get a pre-computed PostgreSQL IN placeholder pattern.
363/// Returns patterns like "$1, $2, $3" for count=3 starting at start_idx=1.
364///
365/// For counts 1-10 with start_idx=1, returns a pre-computed static string.
366/// For other cases, dynamically generates the pattern.
367#[inline]
368pub fn postgres_in_pattern(start_idx: usize, count: usize) -> String {
369    // Fast path: common case of starting at $1 with small counts
370    if start_idx == 1 && count <= 10 {
371        static POSTGRES_IN_1: &[&str] = &[
372            "",
373            "$1",
374            "$1, $2",
375            "$1, $2, $3",
376            "$1, $2, $3, $4",
377            "$1, $2, $3, $4, $5",
378            "$1, $2, $3, $4, $5, $6",
379            "$1, $2, $3, $4, $5, $6, $7",
380            "$1, $2, $3, $4, $5, $6, $7, $8",
381            "$1, $2, $3, $4, $5, $6, $7, $8, $9",
382            "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10",
383        ];
384        return POSTGRES_IN_1[count].to_string();
385    }
386
387    // General case: build dynamically
388    let mut result = String::with_capacity(count * 5);
389    for i in 0..count {
390        if i > 0 {
391            result.push_str(", ");
392        }
393        let idx = start_idx + i;
394        if idx < POSTGRES_PLACEHOLDERS.len() {
395            result.push_str(POSTGRES_PLACEHOLDERS[idx]);
396        } else {
397            use std::fmt::Write;
398            let _ = write!(result, "${}", idx);
399        }
400    }
401    result
402}
403
404/// Pre-computed PostgreSQL IN patterns starting at $1 for common sizes.
405/// These patterns cover IN clause sizes up to 32 elements, which covers ~95% of real-world use cases.
406const POSTGRES_IN_FROM_1: &[&str] = &[
407    "",                                                                                          // 0
408    "$1",                                                                                   // 1
409    "$1, $2",                                                                               // 2
410    "$1, $2, $3",                                                                           // 3
411    "$1, $2, $3, $4",                                                                       // 4
412    "$1, $2, $3, $4, $5",                                                                   // 5
413    "$1, $2, $3, $4, $5, $6",                                                               // 6
414    "$1, $2, $3, $4, $5, $6, $7",                                                           // 7
415    "$1, $2, $3, $4, $5, $6, $7, $8",                                                       // 8
416    "$1, $2, $3, $4, $5, $6, $7, $8, $9",                                                   // 9
417    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10",                                              // 10
418    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11",                                         // 11
419    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12",                                    // 12
420    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13",                               // 13
421    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14",                          // 14
422    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15",                     // 15
423    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16",                // 16
424    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17",           // 17
425    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18",      // 18
426    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19", // 19
427    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20", // 20
428    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21", // 21
429    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22", // 22
430    "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23", // 23
431    "$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
432    "$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
433    "$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
434    "$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
435    "$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
436    "$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
437    "$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
438    "$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
439    "$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
440];
441
442/// Write PostgreSQL IN placeholders directly to a buffer.
443///
444/// Optimizations:
445/// - Pre-computed patterns for counts 1-20 starting at $1 (zero allocation)
446/// - Batch placeholder lookup for larger counts
447/// - Minimized branch predictions in hot loop
448#[inline]
449#[allow(clippy::needless_range_loop)]
450pub fn write_postgres_in_pattern(buf: &mut String, start_idx: usize, count: usize) {
451    if count == 0 {
452        return;
453    }
454
455    // Fast path: common case of starting at $1 with small counts
456    if start_idx == 1 && count < POSTGRES_IN_FROM_1.len() {
457        buf.push_str(POSTGRES_IN_FROM_1[count]);
458        return;
459    }
460
461    // Calculate required capacity: each placeholder is at most 4 chars + 2 for ", "
462    // We reserve a bit more to avoid reallocations
463    buf.reserve(count * 6);
464
465    // Optimized loop with reduced branching
466    let end_idx = start_idx + count;
467    let table_len = POSTGRES_PLACEHOLDERS.len();
468
469    if end_idx <= table_len {
470        // All placeholders in table - fast path
471        buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
472        for idx in (start_idx + 1)..end_idx {
473            buf.push_str(", ");
474            buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
475        }
476    } else if start_idx >= table_len {
477        // All placeholders need formatting - use Write
478        let _ = write!(buf, "${}", start_idx);
479        for idx in (start_idx + 1)..end_idx {
480            let _ = write!(buf, ", ${}", idx);
481        }
482    } else {
483        // Mixed: some in table, some need formatting
484        buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
485        for idx in (start_idx + 1)..table_len.min(end_idx) {
486            buf.push_str(", ");
487            buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
488        }
489        for idx in table_len..end_idx {
490            let _ = write!(buf, ", ${}", idx);
491        }
492    }
493}
494
495impl DatabaseType {
496    /// Get the parameter placeholder for this database type.
497    ///
498    /// For MySQL and SQLite, this returns a borrowed static string (zero allocation).
499    /// For PostgreSQL with index 1-128, this returns a borrowed static string (zero allocation).
500    /// For PostgreSQL with index > 128, this returns an owned formatted string.
501    ///
502    /// # Examples
503    ///
504    /// ```rust
505    /// use prax_query::sql::DatabaseType;
506    ///
507    /// // PostgreSQL uses numbered placeholders (zero allocation for 1-128)
508    /// assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
509    /// assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
510    /// assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
511    ///
512    /// // MySQL and SQLite use ? (zero allocation)
513    /// assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
514    /// assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
515    /// ```
516    #[inline]
517    pub fn placeholder(&self, index: usize) -> Cow<'static, str> {
518        match self {
519            Self::PostgreSQL => {
520                // Use pre-computed lookup for common indices (1-128)
521                if index > 0 && index < POSTGRES_PLACEHOLDERS.len() {
522                    Cow::Borrowed(POSTGRES_PLACEHOLDERS[index])
523                } else {
524                    // Fall back to format for rare cases (0 or > 128)
525                    Cow::Owned(format!("${}", index))
526                }
527            }
528            Self::MySQL | Self::SQLite => Cow::Borrowed(QUESTION_MARK_PLACEHOLDER),
529            Self::MSSQL => Cow::Owned(format!("@P{}", index)),
530        }
531    }
532
533    /// Get the parameter placeholder as a String.
534    ///
535    /// This is a convenience method that always allocates. Prefer `placeholder()`
536    /// when you can work with `Cow<str>` to avoid unnecessary allocations.
537    #[inline]
538    pub fn placeholder_string(&self, index: usize) -> String {
539        self.placeholder(index).into_owned()
540    }
541}
542
543/// A SQL builder for constructing queries.
544#[derive(Debug, Clone)]
545pub struct SqlBuilder {
546    db_type: DatabaseType,
547    parts: Vec<String>,
548    params: Vec<FilterValue>,
549}
550
551impl SqlBuilder {
552    /// Create a new SQL builder.
553    pub fn new(db_type: DatabaseType) -> Self {
554        Self {
555            db_type,
556            parts: Vec::new(),
557            params: Vec::new(),
558        }
559    }
560
561    /// Create a PostgreSQL SQL builder.
562    pub fn postgres() -> Self {
563        Self::new(DatabaseType::PostgreSQL)
564    }
565
566    /// Create a MySQL SQL builder.
567    pub fn mysql() -> Self {
568        Self::new(DatabaseType::MySQL)
569    }
570
571    /// Create a SQLite SQL builder.
572    pub fn sqlite() -> Self {
573        Self::new(DatabaseType::SQLite)
574    }
575
576    /// Push a literal SQL string.
577    pub fn push(&mut self, sql: impl AsRef<str>) -> &mut Self {
578        self.parts.push(sql.as_ref().to_string());
579        self
580    }
581
582    /// Push a SQL string with a parameter.
583    pub fn push_param(&mut self, value: impl Into<FilterValue>) -> &mut Self {
584        let index = self.params.len() + 1;
585        // Use into_owned() since we need to store it in Vec<String>
586        // For MySQL/SQLite, this still benefits from the static str being used
587        self.parts
588            .push(self.db_type.placeholder(index).into_owned());
589        self.params.push(value.into());
590        self
591    }
592
593    /// Push an identifier (properly quoted if needed).
594    pub fn push_identifier(&mut self, name: &str) -> &mut Self {
595        self.parts.push(quote_identifier(name));
596        self
597    }
598
599    /// Push a separator between parts.
600    pub fn push_sep(&mut self, sep: &str) -> &mut Self {
601        self.parts.push(sep.to_string());
602        self
603    }
604
605    /// Build the final SQL string and parameters.
606    pub fn build(self) -> (String, Vec<FilterValue>) {
607        (self.parts.join(""), self.params)
608    }
609
610    /// Get the current SQL string (without consuming).
611    pub fn sql(&self) -> String {
612        self.parts.join("")
613    }
614
615    /// Get the current parameters.
616    pub fn params(&self) -> &[FilterValue] {
617        &self.params
618    }
619
620    /// Get the next parameter index.
621    pub fn next_param_index(&self) -> usize {
622        self.params.len() + 1
623    }
624}
625
626impl Default for SqlBuilder {
627    fn default() -> Self {
628        Self::postgres()
629    }
630}
631
632// ==============================================================================
633// Optimized SQL Builder
634// ==============================================================================
635
636/// Capacity hints for different query types.
637#[derive(Debug, Clone, Copy)]
638pub enum QueryCapacity {
639    /// Simple SELECT query (e.g., SELECT * FROM users WHERE id = $1)
640    SimpleSelect,
641    /// SELECT with multiple conditions
642    SelectWithFilters(usize),
643    /// INSERT with N columns
644    Insert(usize),
645    /// UPDATE with N columns
646    Update(usize),
647    /// DELETE query
648    Delete,
649    /// Custom capacity
650    Custom(usize),
651}
652
653impl QueryCapacity {
654    /// Get the estimated capacity in bytes.
655    #[inline]
656    pub const fn estimate(&self) -> usize {
657        match self {
658            Self::SimpleSelect => 64,
659            Self::SelectWithFilters(n) => 64 + *n * 32,
660            Self::Insert(cols) => 32 + *cols * 16,
661            Self::Update(cols) => 32 + *cols * 20,
662            Self::Delete => 48,
663            Self::Custom(cap) => *cap,
664        }
665    }
666}
667
668/// An optimized SQL builder that uses a single String buffer.
669///
670/// This builder is more efficient than `Sql` for complex queries because:
671/// - Uses a single pre-allocated String instead of Vec<String>
672/// - Uses `write!` macro instead of format! + push
673/// - Provides batch placeholder generation for IN clauses
674///
675/// # Examples
676///
677/// ```rust
678/// use prax_query::sql::{FastSqlBuilder, DatabaseType, QueryCapacity};
679///
680/// // Simple query with pre-allocated capacity
681/// let mut builder = FastSqlBuilder::with_capacity(
682///     DatabaseType::PostgreSQL,
683///     QueryCapacity::SimpleSelect
684/// );
685/// builder.push_str("SELECT * FROM users WHERE id = ");
686/// builder.bind(42i64);
687/// let (sql, params) = builder.build();
688/// assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
689///
690/// // Complex query with multiple bindings
691/// let mut builder = FastSqlBuilder::with_capacity(
692///     DatabaseType::PostgreSQL,
693///     QueryCapacity::SelectWithFilters(3)
694/// );
695/// builder.push_str("SELECT * FROM users WHERE active = ");
696/// builder.bind(true);
697/// builder.push_str(" AND age > ");
698/// builder.bind(18i64);
699/// builder.push_str(" ORDER BY created_at LIMIT ");
700/// builder.bind(10i64);
701/// let (sql, _) = builder.build();
702/// assert!(sql.contains("$1") && sql.contains("$2") && sql.contains("$3"));
703/// ```
704#[derive(Debug, Clone)]
705pub struct FastSqlBuilder {
706    /// The SQL string buffer.
707    buffer: String,
708    /// The parameter values.
709    params: Vec<FilterValue>,
710    /// The database type.
711    db_type: DatabaseType,
712}
713
714impl FastSqlBuilder {
715    /// Create a new builder with the specified database type.
716    #[inline]
717    pub fn new(db_type: DatabaseType) -> Self {
718        Self {
719            buffer: String::new(),
720            params: Vec::new(),
721            db_type,
722        }
723    }
724
725    /// Create a new builder with pre-allocated capacity.
726    #[inline]
727    pub fn with_capacity(db_type: DatabaseType, capacity: QueryCapacity) -> Self {
728        Self {
729            buffer: String::with_capacity(capacity.estimate()),
730            params: Vec::with_capacity(match capacity {
731                QueryCapacity::SimpleSelect => 2,
732                QueryCapacity::SelectWithFilters(n) => n,
733                QueryCapacity::Insert(n) => n,
734                QueryCapacity::Update(n) => n + 1,
735                QueryCapacity::Delete => 2,
736                QueryCapacity::Custom(n) => n / 16,
737            }),
738            db_type,
739        }
740    }
741
742    /// Create a PostgreSQL builder with pre-allocated capacity.
743    #[inline]
744    pub fn postgres(capacity: QueryCapacity) -> Self {
745        Self::with_capacity(DatabaseType::PostgreSQL, capacity)
746    }
747
748    /// Create a MySQL builder with pre-allocated capacity.
749    #[inline]
750    pub fn mysql(capacity: QueryCapacity) -> Self {
751        Self::with_capacity(DatabaseType::MySQL, capacity)
752    }
753
754    /// Create a SQLite builder with pre-allocated capacity.
755    #[inline]
756    pub fn sqlite(capacity: QueryCapacity) -> Self {
757        Self::with_capacity(DatabaseType::SQLite, capacity)
758    }
759
760    /// Push a string slice directly (zero allocation).
761    #[inline]
762    pub fn push_str(&mut self, s: &str) -> &mut Self {
763        self.buffer.push_str(s);
764        self
765    }
766
767    /// Push a single character.
768    #[inline]
769    pub fn push_char(&mut self, c: char) -> &mut Self {
770        self.buffer.push(c);
771        self
772    }
773
774    /// Bind a parameter and append its placeholder.
775    #[inline]
776    pub fn bind(&mut self, value: impl Into<FilterValue>) -> &mut Self {
777        let index = self.params.len() + 1;
778        let placeholder = self.db_type.placeholder(index);
779        self.buffer.push_str(&placeholder);
780        self.params.push(value.into());
781        self
782    }
783
784    /// Push a string and bind a value.
785    #[inline]
786    pub fn push_bind(&mut self, s: &str, value: impl Into<FilterValue>) -> &mut Self {
787        self.push_str(s);
788        self.bind(value)
789    }
790
791    /// Generate placeholders for an IN clause efficiently.
792    ///
793    /// This is much faster than calling `bind()` in a loop because it:
794    /// - Uses pre-computed placeholder patterns for common sizes
795    /// - Pre-calculates the total string length for larger sizes
796    /// - Generates all placeholders in one pass
797    ///
798    /// # Examples
799    ///
800    /// ```rust
801    /// use prax_query::sql::{FastSqlBuilder, DatabaseType, QueryCapacity};
802    /// use prax_query::filter::FilterValue;
803    ///
804    /// let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
805    /// builder.push_str("SELECT * FROM users WHERE id IN (");
806    ///
807    /// let values: Vec<FilterValue> = vec![1i64, 2, 3, 4, 5].into_iter()
808    ///     .map(FilterValue::Int)
809    ///     .collect();
810    /// builder.bind_in_clause(values);
811    /// builder.push_char(')');
812    ///
813    /// let (sql, params) = builder.build();
814    /// assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
815    /// assert_eq!(params.len(), 5);
816    /// ```
817    pub fn bind_in_clause(&mut self, values: impl IntoIterator<Item = FilterValue>) -> &mut Self {
818        let values: Vec<FilterValue> = values.into_iter().collect();
819        if values.is_empty() {
820            return self;
821        }
822
823        let start_index = self.params.len() + 1;
824        let count = values.len();
825
826        // Generate placeholders efficiently
827        match self.db_type {
828            DatabaseType::PostgreSQL => {
829                // Pre-calculate capacity: "$N, " is about 4-5 chars per param
830                let estimated_len = count * 5;
831                self.buffer.reserve(estimated_len);
832
833                for (i, _) in values.iter().enumerate() {
834                    if i > 0 {
835                        self.buffer.push_str(", ");
836                    }
837                    let idx = start_index + i;
838                    if idx < POSTGRES_PLACEHOLDERS.len() {
839                        self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
840                    } else {
841                        let _ = write!(self.buffer, "${}", idx);
842                    }
843                }
844            }
845            DatabaseType::MySQL | DatabaseType::SQLite => {
846                // Use pre-computed pattern for small sizes (up to 32)
847                if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
848                    self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
849                } else {
850                    // Fall back to generation for larger sizes or offset start
851                    let estimated_len = count * 3; // "?, " per param
852                    self.buffer.reserve(estimated_len);
853                    for i in 0..count {
854                        if i > 0 {
855                            self.buffer.push_str(", ");
856                        }
857                        self.buffer.push('?');
858                    }
859                }
860            }
861            DatabaseType::MSSQL => {
862                // MSSQL uses @P1, @P2, etc.
863                let estimated_len = count * 6; // "@PN, " per param
864                self.buffer.reserve(estimated_len);
865
866                for (i, _) in values.iter().enumerate() {
867                    if i > 0 {
868                        self.buffer.push_str(", ");
869                    }
870                    let idx = start_index + i;
871                    let _ = write!(self.buffer, "@P{}", idx);
872                }
873            }
874        }
875
876        self.params.extend(values);
877        self
878    }
879
880    /// Bind a slice of values for an IN clause without collecting.
881    ///
882    /// This is more efficient than `bind_in_clause` when you already have a slice,
883    /// as it avoids collecting into a Vec first.
884    ///
885    /// # Examples
886    ///
887    /// ```rust
888    /// use prax_query::sql::{FastSqlBuilder, DatabaseType, QueryCapacity};
889    ///
890    /// let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
891    /// builder.push_str("SELECT * FROM users WHERE id IN (");
892    ///
893    /// let ids: &[i64] = &[1, 2, 3, 4, 5];
894    /// builder.bind_in_slice(ids);
895    /// builder.push_char(')');
896    ///
897    /// let (sql, params) = builder.build();
898    /// assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
899    /// assert_eq!(params.len(), 5);
900    /// ```
901    pub fn bind_in_slice<T: Into<FilterValue> + Clone>(&mut self, values: &[T]) -> &mut Self {
902        if values.is_empty() {
903            return self;
904        }
905
906        let start_index = self.params.len() + 1;
907        let count = values.len();
908
909        // Generate placeholders
910        match self.db_type {
911            DatabaseType::PostgreSQL => {
912                let estimated_len = count * 5;
913                self.buffer.reserve(estimated_len);
914
915                for i in 0..count {
916                    if i > 0 {
917                        self.buffer.push_str(", ");
918                    }
919                    let idx = start_index + i;
920                    if idx < POSTGRES_PLACEHOLDERS.len() {
921                        self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
922                    } else {
923                        let _ = write!(self.buffer, "${}", idx);
924                    }
925                }
926            }
927            DatabaseType::MySQL | DatabaseType::SQLite => {
928                if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
929                    self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
930                } else {
931                    let estimated_len = count * 3;
932                    self.buffer.reserve(estimated_len);
933                    for i in 0..count {
934                        if i > 0 {
935                            self.buffer.push_str(", ");
936                        }
937                        self.buffer.push('?');
938                    }
939                }
940            }
941            DatabaseType::MSSQL => {
942                let estimated_len = count * 6;
943                self.buffer.reserve(estimated_len);
944
945                for i in 0..count {
946                    if i > 0 {
947                        self.buffer.push_str(", ");
948                    }
949                    let idx = start_index + i;
950                    let _ = write!(self.buffer, "@P{}", idx);
951                }
952            }
953        }
954
955        // Add params
956        self.params.reserve(count);
957        for v in values {
958            self.params.push(v.clone().into());
959        }
960        self
961    }
962
963    /// Write formatted content using the `write!` macro.
964    ///
965    /// This is more efficient than `format!()` + `push_str()` as it
966    /// writes directly to the buffer without intermediate allocation.
967    #[inline]
968    pub fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> &mut Self {
969        let _ = self.buffer.write_fmt(args);
970        self
971    }
972
973    /// Push an identifier, quoting if necessary.
974    #[inline]
975    pub fn push_identifier(&mut self, name: &str) -> &mut Self {
976        if needs_quoting(name) {
977            self.buffer.push('"');
978            // Escape any existing quotes
979            for c in name.chars() {
980                if c == '"' {
981                    self.buffer.push_str("\"\"");
982                } else {
983                    self.buffer.push(c);
984                }
985            }
986            self.buffer.push('"');
987        } else {
988            self.buffer.push_str(name);
989        }
990        self
991    }
992
993    /// Push conditionally.
994    #[inline]
995    pub fn push_if(&mut self, condition: bool, s: &str) -> &mut Self {
996        if condition {
997            self.push_str(s);
998        }
999        self
1000    }
1001
1002    /// Bind conditionally.
1003    #[inline]
1004    pub fn bind_if(&mut self, condition: bool, value: impl Into<FilterValue>) -> &mut Self {
1005        if condition {
1006            self.bind(value);
1007        }
1008        self
1009    }
1010
1011    /// Get the current SQL string.
1012    #[inline]
1013    pub fn sql(&self) -> &str {
1014        &self.buffer
1015    }
1016
1017    /// Get the current parameters.
1018    #[inline]
1019    pub fn params(&self) -> &[FilterValue] {
1020        &self.params
1021    }
1022
1023    /// Get the number of parameters.
1024    #[inline]
1025    pub fn param_count(&self) -> usize {
1026        self.params.len()
1027    }
1028
1029    /// Build the final SQL string and parameters.
1030    #[inline]
1031    pub fn build(self) -> (String, Vec<FilterValue>) {
1032        let sql_len = self.buffer.len();
1033        let param_count = self.params.len();
1034        debug!(sql_len, param_count, db_type = ?self.db_type, "FastSqlBuilder::build()");
1035        (self.buffer, self.params)
1036    }
1037
1038    /// Build and return only the SQL string.
1039    #[inline]
1040    pub fn build_sql(self) -> String {
1041        self.buffer
1042    }
1043}
1044
1045// ==============================================================================
1046// SQL Templates for Common Queries
1047// ==============================================================================
1048
1049/// Pre-built SQL templates for common query patterns.
1050///
1051/// Using templates avoids repeated string construction for common operations.
1052pub mod templates {
1053    use super::*;
1054
1055    /// Generate a simple SELECT query template.
1056    ///
1057    /// # Examples
1058    ///
1059    /// ```rust
1060    /// use prax_query::sql::templates;
1061    ///
1062    /// let template = templates::select_by_id("users", &["id", "name", "email"]);
1063    /// assert!(template.contains("SELECT"));
1064    /// assert!(template.contains("FROM users"));
1065    /// assert!(template.contains("WHERE id = $1"));
1066    /// ```
1067    pub fn select_by_id(table: &str, columns: &[&str]) -> String {
1068        let cols = if columns.is_empty() {
1069            "*".to_string()
1070        } else {
1071            columns.join(", ")
1072        };
1073        format!("SELECT {} FROM {} WHERE id = $1", cols, table)
1074    }
1075
1076    /// Generate an INSERT query template for PostgreSQL.
1077    ///
1078    /// # Examples
1079    ///
1080    /// ```rust
1081    /// use prax_query::sql::templates;
1082    ///
1083    /// let template = templates::insert_returning("users", &["name", "email"]);
1084    /// assert!(template.contains("INSERT INTO users"));
1085    /// assert!(template.contains("RETURNING *"));
1086    /// ```
1087    pub fn insert_returning(table: &str, columns: &[&str]) -> String {
1088        let cols = columns.join(", ");
1089        let placeholders: Vec<String> = (1..=columns.len())
1090            .map(|i| {
1091                if i < POSTGRES_PLACEHOLDERS.len() {
1092                    POSTGRES_PLACEHOLDERS[i].to_string()
1093                } else {
1094                    format!("${}", i)
1095                }
1096            })
1097            .collect();
1098        format!(
1099            "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
1100            table,
1101            cols,
1102            placeholders.join(", ")
1103        )
1104    }
1105
1106    /// Generate an UPDATE query template.
1107    ///
1108    /// # Examples
1109    ///
1110    /// ```rust
1111    /// use prax_query::sql::templates;
1112    ///
1113    /// let template = templates::update_by_id("users", &["name", "email"]);
1114    /// assert!(template.contains("UPDATE users SET"));
1115    /// assert!(template.contains("WHERE id = $3"));
1116    /// ```
1117    pub fn update_by_id(table: &str, columns: &[&str]) -> String {
1118        let sets: Vec<String> = columns
1119            .iter()
1120            .enumerate()
1121            .map(|(i, col)| {
1122                let idx = i + 1;
1123                if idx < POSTGRES_PLACEHOLDERS.len() {
1124                    format!("{} = {}", col, POSTGRES_PLACEHOLDERS[idx])
1125                } else {
1126                    format!("{} = ${}", col, idx)
1127                }
1128            })
1129            .collect();
1130        let id_idx = columns.len() + 1;
1131        let id_placeholder = if id_idx < POSTGRES_PLACEHOLDERS.len() {
1132            POSTGRES_PLACEHOLDERS[id_idx]
1133        } else {
1134            "$?"
1135        };
1136        format!(
1137            "UPDATE {} SET {} WHERE id = {}",
1138            table,
1139            sets.join(", "),
1140            id_placeholder
1141        )
1142    }
1143
1144    /// Generate a DELETE query template.
1145    ///
1146    /// # Examples
1147    ///
1148    /// ```rust
1149    /// use prax_query::sql::templates;
1150    ///
1151    /// let template = templates::delete_by_id("users");
1152    /// assert_eq!(template, "DELETE FROM users WHERE id = $1");
1153    /// ```
1154    pub fn delete_by_id(table: &str) -> String {
1155        format!("DELETE FROM {} WHERE id = $1", table)
1156    }
1157
1158    /// Generate placeholders for a batch INSERT.
1159    ///
1160    /// # Examples
1161    ///
1162    /// ```rust
1163    /// use prax_query::sql::templates;
1164    /// use prax_query::sql::DatabaseType;
1165    ///
1166    /// let placeholders = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
1167    /// assert_eq!(placeholders, "($1, $2, $3), ($4, $5, $6)");
1168    /// ```
1169    pub fn batch_placeholders(db_type: DatabaseType, columns: usize, rows: usize) -> String {
1170        let mut result = String::with_capacity(rows * columns * 4);
1171        let mut param_idx = 1;
1172
1173        for row in 0..rows {
1174            if row > 0 {
1175                result.push_str(", ");
1176            }
1177            result.push('(');
1178            for col in 0..columns {
1179                if col > 0 {
1180                    result.push_str(", ");
1181                }
1182                match db_type {
1183                    DatabaseType::PostgreSQL => {
1184                        if param_idx < POSTGRES_PLACEHOLDERS.len() {
1185                            result.push_str(POSTGRES_PLACEHOLDERS[param_idx]);
1186                        } else {
1187                            let _ = write!(result, "${}", param_idx);
1188                        }
1189                        param_idx += 1;
1190                    }
1191                    DatabaseType::MySQL | DatabaseType::SQLite => {
1192                        result.push('?');
1193                    }
1194                    DatabaseType::MSSQL => {
1195                        let _ = write!(result, "@P{}", param_idx);
1196                        param_idx += 1;
1197                    }
1198                }
1199            }
1200            result.push(')');
1201        }
1202
1203        result
1204    }
1205}
1206
1207// ==============================================================================
1208// Lazy SQL Generation
1209// ==============================================================================
1210
1211/// A lazily-generated SQL string that defers construction until needed.
1212///
1213/// This is useful when you may not need the SQL string (e.g., when caching
1214/// is available, or when the query may be abandoned before execution).
1215///
1216/// # Example
1217///
1218/// ```rust
1219/// use prax_query::sql::{LazySql, DatabaseType};
1220///
1221/// // Create a lazy SQL generator
1222/// let lazy = LazySql::new(|db_type| {
1223///     format!("SELECT * FROM users WHERE active = {}", db_type.placeholder(1))
1224/// });
1225///
1226/// // SQL is not generated until accessed
1227/// let sql = lazy.get(DatabaseType::PostgreSQL);
1228/// assert_eq!(sql, "SELECT * FROM users WHERE active = $1");
1229/// ```
1230pub struct LazySql<F>
1231where
1232    F: Fn(DatabaseType) -> String,
1233{
1234    generator: F,
1235}
1236
1237impl<F> LazySql<F>
1238where
1239    F: Fn(DatabaseType) -> String,
1240{
1241    /// Create a new lazy SQL generator.
1242    #[inline]
1243    pub const fn new(generator: F) -> Self {
1244        Self { generator }
1245    }
1246
1247    /// Generate the SQL string for the given database type.
1248    #[inline]
1249    pub fn get(&self, db_type: DatabaseType) -> String {
1250        (self.generator)(db_type)
1251    }
1252}
1253
1254/// A cached lazy SQL that stores previously generated SQL for each database type.
1255///
1256/// This combines lazy generation with caching, so SQL is only generated once
1257/// per database type, then reused for subsequent calls.
1258///
1259/// # Example
1260///
1261/// ```rust
1262/// use prax_query::sql::{CachedSql, DatabaseType};
1263///
1264/// let cached = CachedSql::new(|db_type| {
1265///     format!("SELECT * FROM users WHERE active = {}", db_type.placeholder(1))
1266/// });
1267///
1268/// // First call generates and caches
1269/// let sql1 = cached.get(DatabaseType::PostgreSQL);
1270///
1271/// // Second call returns cached value (no regeneration)
1272/// let sql2 = cached.get(DatabaseType::PostgreSQL);
1273///
1274/// assert_eq!(sql1, sql2);
1275/// ```
1276pub struct CachedSql<F>
1277where
1278    F: Fn(DatabaseType) -> String,
1279{
1280    generator: F,
1281    postgres: OnceLock<String>,
1282    mysql: OnceLock<String>,
1283    sqlite: OnceLock<String>,
1284    mssql: OnceLock<String>,
1285}
1286
1287impl<F> CachedSql<F>
1288where
1289    F: Fn(DatabaseType) -> String,
1290{
1291    /// Create a new cached SQL generator.
1292    pub const fn new(generator: F) -> Self {
1293        Self {
1294            generator,
1295            postgres: OnceLock::new(),
1296            mysql: OnceLock::new(),
1297            sqlite: OnceLock::new(),
1298            mssql: OnceLock::new(),
1299        }
1300    }
1301
1302    /// Get the SQL string for the given database type.
1303    ///
1304    /// The first call for each database type generates the SQL.
1305    /// Subsequent calls return the cached value.
1306    pub fn get(&self, db_type: DatabaseType) -> &str {
1307        match db_type {
1308            DatabaseType::PostgreSQL => self.postgres.get_or_init(|| (self.generator)(db_type)),
1309            DatabaseType::MySQL => self.mysql.get_or_init(|| (self.generator)(db_type)),
1310            DatabaseType::SQLite => self.sqlite.get_or_init(|| (self.generator)(db_type)),
1311            DatabaseType::MSSQL => self.mssql.get_or_init(|| (self.generator)(db_type)),
1312        }
1313    }
1314}
1315
1316// ==============================================================================
1317// SQL Template Cache (Thread-Safe)
1318// ==============================================================================
1319
1320/// A thread-safe cache for SQL templates.
1321///
1322/// This cache stores parameterized SQL templates that can be reused across
1323/// requests, avoiding repeated string construction for common query patterns.
1324///
1325/// # Example
1326///
1327/// ```rust
1328/// use prax_query::sql::{SqlTemplateCache, DatabaseType};
1329///
1330/// let cache = SqlTemplateCache::new();
1331///
1332/// // First call generates and caches
1333/// let sql = cache.get_or_insert("user_by_email", DatabaseType::PostgreSQL, || {
1334///     "SELECT * FROM users WHERE email = $1".to_string()
1335/// });
1336///
1337/// // Second call returns cached value
1338/// let sql2 = cache.get_or_insert("user_by_email", DatabaseType::PostgreSQL, || {
1339///     panic!("Should not be called - value is cached")
1340/// });
1341///
1342/// assert_eq!(sql, sql2);
1343/// ```
1344pub struct SqlTemplateCache {
1345    cache: RwLock<HashMap<(String, DatabaseType), Arc<String>>>,
1346}
1347
1348impl Default for SqlTemplateCache {
1349    fn default() -> Self {
1350        Self::new()
1351    }
1352}
1353
1354impl SqlTemplateCache {
1355    /// Create a new empty cache.
1356    pub fn new() -> Self {
1357        Self {
1358            cache: RwLock::new(HashMap::new()),
1359        }
1360    }
1361
1362    /// Create a new cache with pre-allocated capacity.
1363    pub fn with_capacity(capacity: usize) -> Self {
1364        Self {
1365            cache: RwLock::new(HashMap::with_capacity(capacity)),
1366        }
1367    }
1368
1369    /// Get or insert a SQL template.
1370    ///
1371    /// If the template exists in the cache, returns the cached value.
1372    /// Otherwise, calls the generator function, caches the result, and returns it.
1373    pub fn get_or_insert<F>(&self, key: &str, db_type: DatabaseType, generator: F) -> Arc<String>
1374    where
1375        F: FnOnce() -> String,
1376    {
1377        let cache_key = (key.to_string(), db_type);
1378
1379        // Try read lock first (fast path)
1380        {
1381            let cache = self.cache.read().unwrap();
1382            if let Some(sql) = cache.get(&cache_key) {
1383                return Arc::clone(sql);
1384            }
1385        }
1386
1387        // Upgrade to write lock and insert
1388        let mut cache = self.cache.write().unwrap();
1389
1390        // Double-check after acquiring write lock (another thread may have inserted)
1391        if let Some(sql) = cache.get(&cache_key) {
1392            return Arc::clone(sql);
1393        }
1394
1395        let sql = Arc::new(generator());
1396        cache.insert(cache_key, Arc::clone(&sql));
1397        sql
1398    }
1399
1400    /// Check if a template is cached.
1401    pub fn contains(&self, key: &str, db_type: DatabaseType) -> bool {
1402        let cache_key = (key.to_string(), db_type);
1403        self.cache.read().unwrap().contains_key(&cache_key)
1404    }
1405
1406    /// Clear the cache.
1407    pub fn clear(&self) {
1408        self.cache.write().unwrap().clear();
1409    }
1410
1411    /// Get the number of cached templates.
1412    pub fn len(&self) -> usize {
1413        self.cache.read().unwrap().len()
1414    }
1415
1416    /// Check if the cache is empty.
1417    pub fn is_empty(&self) -> bool {
1418        self.cache.read().unwrap().is_empty()
1419    }
1420}
1421
1422/// Global SQL template cache for common query patterns.
1423///
1424/// This provides a shared cache across the application for frequently used
1425/// SQL templates, reducing memory usage and improving performance.
1426///
1427/// # Example
1428///
1429/// ```rust
1430/// use prax_query::sql::{global_sql_cache, DatabaseType};
1431///
1432/// let sql = global_sql_cache().get_or_insert("find_user_by_id", DatabaseType::PostgreSQL, || {
1433///     "SELECT * FROM users WHERE id = $1".to_string()
1434/// });
1435/// ```
1436pub fn global_sql_cache() -> &'static SqlTemplateCache {
1437    static CACHE: OnceLock<SqlTemplateCache> = OnceLock::new();
1438    CACHE.get_or_init(|| SqlTemplateCache::with_capacity(64))
1439}
1440
1441// ==============================================================================
1442// Enhanced Capacity Estimation for Advanced Features
1443// ==============================================================================
1444
1445/// Extended capacity hints for advanced query types.
1446#[derive(Debug, Clone, Copy)]
1447pub enum AdvancedQueryCapacity {
1448    /// Common Table Expression (CTE)
1449    Cte {
1450        /// Number of CTEs in WITH clause
1451        cte_count: usize,
1452        /// Average query length per CTE
1453        avg_query_len: usize,
1454    },
1455    /// Window function query
1456    WindowFunction {
1457        /// Number of window functions
1458        window_count: usize,
1459        /// Number of partition columns
1460        partition_cols: usize,
1461        /// Number of order by columns
1462        order_cols: usize,
1463    },
1464    /// Full-text search query
1465    FullTextSearch {
1466        /// Number of search columns
1467        columns: usize,
1468        /// Search query length
1469        query_len: usize,
1470    },
1471    /// JSON path query
1472    JsonPath {
1473        /// Path depth
1474        depth: usize,
1475    },
1476    /// Upsert with conflict handling
1477    Upsert {
1478        /// Number of columns
1479        columns: usize,
1480        /// Number of conflict columns
1481        conflict_cols: usize,
1482        /// Number of update columns
1483        update_cols: usize,
1484    },
1485    /// Stored procedure/function call
1486    ProcedureCall {
1487        /// Number of parameters
1488        params: usize,
1489    },
1490    /// Trigger definition
1491    TriggerDef {
1492        /// Number of events
1493        events: usize,
1494        /// Body length estimate
1495        body_len: usize,
1496    },
1497    /// Security policy (RLS)
1498    RlsPolicy {
1499        /// Expression length
1500        expr_len: usize,
1501    },
1502}
1503
1504impl AdvancedQueryCapacity {
1505    /// Get the estimated capacity in bytes.
1506    #[inline]
1507    pub const fn estimate(&self) -> usize {
1508        match self {
1509            Self::Cte {
1510                cte_count,
1511                avg_query_len,
1512            } => {
1513                // WITH + cte_name AS (query), ...
1514                16 + *cte_count * (32 + *avg_query_len)
1515            }
1516            Self::WindowFunction {
1517                window_count,
1518                partition_cols,
1519                order_cols,
1520            } => {
1521                // func() OVER (PARTITION BY ... ORDER BY ...)
1522                *window_count * (48 + *partition_cols * 16 + *order_cols * 20)
1523            }
1524            Self::FullTextSearch { columns, query_len } => {
1525                // to_tsvector() @@ plainto_tsquery() or MATCH(...) AGAINST(...)
1526                64 + *columns * 20 + *query_len
1527            }
1528            Self::JsonPath { depth } => {
1529                // column->'path'->'nested'
1530                16 + *depth * 12
1531            }
1532            Self::Upsert {
1533                columns,
1534                conflict_cols,
1535                update_cols,
1536            } => {
1537                // INSERT ... ON CONFLICT (cols) DO UPDATE SET ...
1538                64 + *columns * 8 + *conflict_cols * 12 + *update_cols * 16
1539            }
1540            Self::ProcedureCall { params } => {
1541                // CALL proc_name($1, $2, ...)
1542                32 + *params * 8
1543            }
1544            Self::TriggerDef { events, body_len } => {
1545                // CREATE TRIGGER ... BEFORE/AFTER ... ON table ...
1546                96 + *events * 12 + *body_len
1547            }
1548            Self::RlsPolicy { expr_len } => {
1549                // CREATE POLICY ... USING (...)
1550                64 + *expr_len
1551            }
1552        }
1553    }
1554
1555    /// Convert to QueryCapacity::Custom for use with FastSqlBuilder.
1556    #[inline]
1557    pub const fn to_query_capacity(&self) -> QueryCapacity {
1558        QueryCapacity::Custom(self.estimate())
1559    }
1560}
1561
1562/// Create a FastSqlBuilder with capacity for advanced queries.
1563impl FastSqlBuilder {
1564    /// Create a builder with capacity estimated for advanced query types.
1565    #[inline]
1566    pub fn for_advanced(db_type: DatabaseType, capacity: AdvancedQueryCapacity) -> Self {
1567        Self::with_capacity(db_type, capacity.to_query_capacity())
1568    }
1569
1570    /// Create a builder for CTE queries.
1571    #[inline]
1572    pub fn for_cte(db_type: DatabaseType, cte_count: usize, avg_query_len: usize) -> Self {
1573        Self::for_advanced(
1574            db_type,
1575            AdvancedQueryCapacity::Cte {
1576                cte_count,
1577                avg_query_len,
1578            },
1579        )
1580    }
1581
1582    /// Create a builder for window function queries.
1583    #[inline]
1584    pub fn for_window(
1585        db_type: DatabaseType,
1586        window_count: usize,
1587        partition_cols: usize,
1588        order_cols: usize,
1589    ) -> Self {
1590        Self::for_advanced(
1591            db_type,
1592            AdvancedQueryCapacity::WindowFunction {
1593                window_count,
1594                partition_cols,
1595                order_cols,
1596            },
1597        )
1598    }
1599
1600    /// Create a builder for upsert queries.
1601    #[inline]
1602    pub fn for_upsert(
1603        db_type: DatabaseType,
1604        columns: usize,
1605        conflict_cols: usize,
1606        update_cols: usize,
1607    ) -> Self {
1608        Self::for_advanced(
1609            db_type,
1610            AdvancedQueryCapacity::Upsert {
1611                columns,
1612                conflict_cols,
1613                update_cols,
1614            },
1615        )
1616    }
1617}
1618
1619// ==============================================================================
1620// SQL string parsing helpers
1621// ==============================================================================
1622
1623/// Helpers for recovering structured pieces (column lists, WHERE clauses,
1624/// placeholder counts) from a SQL string that was built by one of the
1625/// `operations/*::build_sql` paths. Driver engines need these when the
1626/// dialect can't emit `RETURNING` and the driver has to run a follow-up
1627/// SELECT keyed on the same WHERE/PK values the original statement used.
1628///
1629/// Scoped-lexer caveats: these helpers are case-insensitive token scans,
1630/// not a real SQL parser. They work correctly on the generated output of
1631/// `prax-query`'s own `operations/*::build_sql` — they do not try to
1632/// handle arbitrary user SQL with string literals containing `?` /
1633/// ` where ` / etc. Callers that accept raw user SQL must either reject
1634/// malformed input up front or avoid these helpers.
1635pub mod parse {
1636    /// Extract the WHERE clause body (everything after the first case-
1637    /// insensitive ` WHERE ` token) from an UPDATE / DELETE / SELECT.
1638    /// Returns `None` if there is no WHERE clause.
1639    pub fn extract_where_body(sql: &str) -> Option<String> {
1640        let lower = sql.to_ascii_lowercase();
1641        let i = lower.find(" where ")?;
1642        Some(sql[i + " where ".len()..].trim().to_string())
1643    }
1644
1645    /// Extract the comma-separated column list from an
1646    /// `INSERT INTO tbl (col1, col2, ...) VALUES …` statement. Returns
1647    /// `None` if the statement doesn't have a column list.
1648    pub fn extract_insert_columns(sql: &str) -> Option<Vec<String>> {
1649        let open = sql.find('(')?;
1650        let close = sql[open..].find(')').map(|i| open + i)?;
1651        let body = &sql[open + 1..close];
1652        Some(
1653            body.split(',')
1654                .map(|c| c.trim().to_string())
1655                .filter(|c| !c.is_empty())
1656                .collect(),
1657        )
1658    }
1659
1660    /// Count the `?` placeholders inside an UPDATE statement's SET
1661    /// clause (between ` SET ` and ` WHERE `). Used to split bound
1662    /// params between SET values and WHERE values for CQL drivers
1663    /// that need to issue a follow-up SELECT. Returns `None` when
1664    /// the SET window can't be located.
1665    pub fn count_set_placeholders(sql: &str) -> Option<usize> {
1666        let lower = sql.to_ascii_lowercase();
1667        let set_start = lower.find(" set ")?;
1668        let where_start = lower[set_start..]
1669            .find(" where ")
1670            .map(|i| set_start + i)
1671            .unwrap_or(sql.len());
1672        Some(sql[set_start..where_start].matches('?').count())
1673    }
1674
1675    #[cfg(test)]
1676    mod tests {
1677        use super::*;
1678
1679        #[test]
1680        fn extract_where_body_finds_lowercase_tail() {
1681            assert_eq!(
1682                extract_where_body("UPDATE t SET a = 1 WHERE id = 42"),
1683                Some("id = 42".to_string())
1684            );
1685        }
1686
1687        #[test]
1688        fn extract_where_body_case_insensitive() {
1689            assert_eq!(
1690                extract_where_body("update t set a = 1 where id = 42"),
1691                Some("id = 42".to_string())
1692            );
1693        }
1694
1695        #[test]
1696        fn extract_where_body_missing_returns_none() {
1697            assert_eq!(extract_where_body("SELECT * FROM t"), None);
1698        }
1699
1700        #[test]
1701        fn extract_insert_columns_parses_list() {
1702            assert_eq!(
1703                extract_insert_columns("INSERT INTO users (id, email, name) VALUES ($1, $2, $3)"),
1704                Some(vec!["id".into(), "email".into(), "name".into()])
1705            );
1706        }
1707
1708        #[test]
1709        fn count_set_placeholders_counts_between_set_and_where() {
1710            assert_eq!(
1711                count_set_placeholders("UPDATE t SET a = ?, b = ? WHERE id = ?"),
1712                Some(2)
1713            );
1714        }
1715
1716        #[test]
1717        fn count_set_placeholders_without_where_counts_to_end() {
1718            assert_eq!(count_set_placeholders("UPDATE t SET a = ?, b = ?"), Some(2));
1719        }
1720    }
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725    use super::*;
1726
1727    #[test]
1728    fn test_escape_identifier() {
1729        assert_eq!(escape_identifier("user"), "\"user\"");
1730        assert_eq!(escape_identifier("my_table"), "\"my_table\"");
1731        assert_eq!(escape_identifier("has\"quote"), "\"has\"\"quote\"");
1732    }
1733
1734    #[test]
1735    fn test_needs_quoting() {
1736        assert!(needs_quoting("user"));
1737        assert!(needs_quoting("order"));
1738        assert!(needs_quoting("has space"));
1739        assert!(!needs_quoting("my_table"));
1740        assert!(!needs_quoting("users"));
1741    }
1742
1743    #[test]
1744    fn test_quote_identifier() {
1745        assert_eq!(quote_identifier("user"), "\"user\"");
1746        assert_eq!(quote_identifier("my_table"), "my_table");
1747    }
1748
1749    #[test]
1750    fn test_database_placeholder() {
1751        // Basic placeholder values
1752        assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
1753        assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
1754        assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
1755        assert_eq!(DatabaseType::PostgreSQL.placeholder(128).as_ref(), "$128");
1756        assert_eq!(DatabaseType::PostgreSQL.placeholder(256).as_ref(), "$256");
1757        assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
1758        assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
1759
1760        // Verify MySQL/SQLite return borrowed (zero allocation)
1761        assert!(matches!(
1762            DatabaseType::MySQL.placeholder(1),
1763            Cow::Borrowed(_)
1764        ));
1765        assert!(matches!(
1766            DatabaseType::SQLite.placeholder(1),
1767            Cow::Borrowed(_)
1768        ));
1769
1770        // PostgreSQL returns borrowed for indices 1-256 (zero allocation via lookup table)
1771        assert!(matches!(
1772            DatabaseType::PostgreSQL.placeholder(1),
1773            Cow::Borrowed(_)
1774        ));
1775        assert!(matches!(
1776            DatabaseType::PostgreSQL.placeholder(50),
1777            Cow::Borrowed(_)
1778        ));
1779        assert!(matches!(
1780            DatabaseType::PostgreSQL.placeholder(128),
1781            Cow::Borrowed(_)
1782        ));
1783        assert!(matches!(
1784            DatabaseType::PostgreSQL.placeholder(256),
1785            Cow::Borrowed(_)
1786        ));
1787
1788        // PostgreSQL returns owned for indices > 256 (must format)
1789        assert!(matches!(
1790            DatabaseType::PostgreSQL.placeholder(257),
1791            Cow::Owned(_)
1792        ));
1793        assert_eq!(DatabaseType::PostgreSQL.placeholder(257).as_ref(), "$257");
1794        assert_eq!(DatabaseType::PostgreSQL.placeholder(200).as_ref(), "$200");
1795
1796        // Edge case: index 0 falls back to format (unusual but handled)
1797        assert!(matches!(
1798            DatabaseType::PostgreSQL.placeholder(0),
1799            Cow::Owned(_)
1800        ));
1801        assert_eq!(DatabaseType::PostgreSQL.placeholder(0).as_ref(), "$0");
1802    }
1803
1804    #[test]
1805    fn test_sql_builder() {
1806        let mut builder = SqlBuilder::postgres();
1807        builder
1808            .push("SELECT * FROM ")
1809            .push_identifier("user")
1810            .push(" WHERE ")
1811            .push_identifier("id")
1812            .push(" = ")
1813            .push_param(42i32);
1814
1815        let (sql, params) = builder.build();
1816        assert_eq!(sql, "SELECT * FROM \"user\" WHERE id = $1");
1817        assert_eq!(params.len(), 1);
1818    }
1819
1820    // FastSqlBuilder tests
1821    #[test]
1822    fn test_fast_builder_simple() {
1823        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1824        builder.push_str("SELECT * FROM users WHERE id = ");
1825        builder.bind(42i64);
1826        let (sql, params) = builder.build();
1827        assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1828        assert_eq!(params.len(), 1);
1829    }
1830
1831    #[test]
1832    fn test_fast_builder_complex() {
1833        let mut builder = FastSqlBuilder::with_capacity(
1834            DatabaseType::PostgreSQL,
1835            QueryCapacity::SelectWithFilters(5),
1836        );
1837        builder
1838            .push_str("SELECT * FROM users WHERE active = ")
1839            .bind(true)
1840            .push_str(" AND age > ")
1841            .bind(18i64)
1842            .push_str(" AND status = ")
1843            .bind("approved")
1844            .push_str(" ORDER BY created_at LIMIT ")
1845            .bind(10i64);
1846
1847        let (sql, params) = builder.build();
1848        assert!(sql.contains("$1"));
1849        assert!(sql.contains("$4"));
1850        assert_eq!(params.len(), 4);
1851    }
1852
1853    #[test]
1854    fn test_fast_builder_in_clause_postgres() {
1855        let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
1856        builder.push_str("SELECT * FROM users WHERE id IN (");
1857        let values: Vec<FilterValue> = (1..=5).map(FilterValue::Int).collect();
1858        builder.bind_in_clause(values);
1859        builder.push_char(')');
1860
1861        let (sql, params) = builder.build();
1862        assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
1863        assert_eq!(params.len(), 5);
1864    }
1865
1866    #[test]
1867    fn test_fast_builder_in_clause_mysql() {
1868        let mut builder = FastSqlBuilder::mysql(QueryCapacity::Custom(128));
1869        builder.push_str("SELECT * FROM users WHERE id IN (");
1870        let values: Vec<FilterValue> = (1..=5).map(FilterValue::Int).collect();
1871        builder.bind_in_clause(values);
1872        builder.push_char(')');
1873
1874        let (sql, params) = builder.build();
1875        assert_eq!(sql, "SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?)");
1876        assert_eq!(params.len(), 5);
1877    }
1878
1879    #[test]
1880    fn test_fast_builder_identifier() {
1881        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1882        builder.push_str("SELECT * FROM ");
1883        builder.push_identifier("user"); // reserved word
1884        builder.push_str(" WHERE ");
1885        builder.push_identifier("my_column"); // not reserved
1886        builder.push_str(" = ");
1887        builder.bind(1i64);
1888
1889        let (sql, _) = builder.build();
1890        assert_eq!(sql, "SELECT * FROM \"user\" WHERE my_column = $1");
1891    }
1892
1893    #[test]
1894    fn test_fast_builder_identifier_with_quotes() {
1895        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1896        builder.push_str("SELECT * FROM ");
1897        builder.push_identifier("has\"quote");
1898
1899        let sql = builder.build_sql();
1900        assert_eq!(sql, "SELECT * FROM \"has\"\"quote\"");
1901    }
1902
1903    #[test]
1904    fn test_fast_builder_conditional() {
1905        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SelectWithFilters(2));
1906        builder.push_str("SELECT * FROM users WHERE 1=1");
1907        builder.push_if(true, " AND active = true");
1908        builder.push_if(false, " AND deleted = false");
1909
1910        let sql = builder.build_sql();
1911        assert_eq!(sql, "SELECT * FROM users WHERE 1=1 AND active = true");
1912    }
1913
1914    // Template tests
1915    #[test]
1916    fn test_template_select_by_id() {
1917        let sql = templates::select_by_id("users", &["id", "name", "email"]);
1918        assert_eq!(sql, "SELECT id, name, email FROM users WHERE id = $1");
1919    }
1920
1921    #[test]
1922    fn test_template_select_by_id_all_columns() {
1923        let sql = templates::select_by_id("users", &[]);
1924        assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1925    }
1926
1927    #[test]
1928    fn test_template_insert_returning() {
1929        let sql = templates::insert_returning("users", &["name", "email"]);
1930        assert_eq!(
1931            sql,
1932            "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
1933        );
1934    }
1935
1936    #[test]
1937    fn test_template_update_by_id() {
1938        let sql = templates::update_by_id("users", &["name", "email"]);
1939        assert_eq!(sql, "UPDATE users SET name = $1, email = $2 WHERE id = $3");
1940    }
1941
1942    #[test]
1943    fn test_template_delete_by_id() {
1944        let sql = templates::delete_by_id("users");
1945        assert_eq!(sql, "DELETE FROM users WHERE id = $1");
1946    }
1947
1948    #[test]
1949    fn test_template_batch_placeholders_postgres() {
1950        let sql = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
1951        assert_eq!(sql, "($1, $2, $3), ($4, $5, $6)");
1952    }
1953
1954    #[test]
1955    fn test_template_batch_placeholders_mysql() {
1956        let sql = templates::batch_placeholders(DatabaseType::MySQL, 3, 2);
1957        assert_eq!(sql, "(?, ?, ?), (?, ?, ?)");
1958    }
1959
1960    #[test]
1961    fn test_query_capacity_estimates() {
1962        assert_eq!(QueryCapacity::SimpleSelect.estimate(), 64);
1963        assert_eq!(QueryCapacity::SelectWithFilters(5).estimate(), 64 + 5 * 32);
1964        assert_eq!(QueryCapacity::Insert(10).estimate(), 32 + 10 * 16);
1965        assert_eq!(QueryCapacity::Update(5).estimate(), 32 + 5 * 20);
1966        assert_eq!(QueryCapacity::Delete.estimate(), 48);
1967        assert_eq!(QueryCapacity::Custom(256).estimate(), 256);
1968    }
1969}