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