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