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    //! 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 => self.postgres.get_or_init(|| (self.generator)(db_type)),
1311            DatabaseType::MySQL => self.mysql.get_or_init(|| (self.generator)(db_type)),
1312            DatabaseType::SQLite => self.sqlite.get_or_init(|| (self.generator)(db_type)),
1313            DatabaseType::MSSQL => self.mssql.get_or_init(|| (self.generator)(db_type)),
1314        }
1315    }
1316}
1317
1318// ==============================================================================
1319// SQL Template Cache (Thread-Safe)
1320// ==============================================================================
1321
1322/// A thread-safe cache for SQL templates.
1323///
1324/// This cache stores parameterized SQL templates that can be reused across
1325/// requests, avoiding repeated string construction for common query patterns.
1326///
1327/// # Example
1328///
1329/// ```rust
1330/// use prax_query::sql::{SqlTemplateCache, DatabaseType};
1331///
1332/// let cache = SqlTemplateCache::new();
1333///
1334/// // First call generates and caches
1335/// let sql = cache.get_or_insert("user_by_email", DatabaseType::PostgreSQL, || {
1336///     "SELECT * FROM users WHERE email = $1".to_string()
1337/// });
1338///
1339/// // Second call returns cached value
1340/// let sql2 = cache.get_or_insert("user_by_email", DatabaseType::PostgreSQL, || {
1341///     panic!("Should not be called - value is cached")
1342/// });
1343///
1344/// assert_eq!(sql, sql2);
1345/// ```
1346pub struct SqlTemplateCache {
1347    cache: RwLock<HashMap<(String, DatabaseType), Arc<String>>>,
1348}
1349
1350impl Default for SqlTemplateCache {
1351    fn default() -> Self {
1352        Self::new()
1353    }
1354}
1355
1356impl SqlTemplateCache {
1357    /// Create a new empty cache.
1358    pub fn new() -> Self {
1359        Self {
1360            cache: RwLock::new(HashMap::new()),
1361        }
1362    }
1363
1364    /// Create a new cache with pre-allocated capacity.
1365    pub fn with_capacity(capacity: usize) -> Self {
1366        Self {
1367            cache: RwLock::new(HashMap::with_capacity(capacity)),
1368        }
1369    }
1370
1371    /// Get or insert a SQL template.
1372    ///
1373    /// If the template exists in the cache, returns the cached value.
1374    /// Otherwise, calls the generator function, caches the result, and returns it.
1375    pub fn get_or_insert<F>(&self, key: &str, db_type: DatabaseType, generator: F) -> Arc<String>
1376    where
1377        F: FnOnce() -> String,
1378    {
1379        let cache_key = (key.to_string(), db_type);
1380
1381        // Try read lock first (fast path)
1382        {
1383            let cache = self.cache.read().unwrap();
1384            if let Some(sql) = cache.get(&cache_key) {
1385                return Arc::clone(sql);
1386            }
1387        }
1388
1389        // Upgrade to write lock and insert
1390        let mut cache = self.cache.write().unwrap();
1391
1392        // Double-check after acquiring write lock (another thread may have inserted)
1393        if let Some(sql) = cache.get(&cache_key) {
1394            return Arc::clone(sql);
1395        }
1396
1397        let sql = Arc::new(generator());
1398        cache.insert(cache_key, Arc::clone(&sql));
1399        sql
1400    }
1401
1402    /// Check if a template is cached.
1403    pub fn contains(&self, key: &str, db_type: DatabaseType) -> bool {
1404        let cache_key = (key.to_string(), db_type);
1405        self.cache.read().unwrap().contains_key(&cache_key)
1406    }
1407
1408    /// Clear the cache.
1409    pub fn clear(&self) {
1410        self.cache.write().unwrap().clear();
1411    }
1412
1413    /// Get the number of cached templates.
1414    pub fn len(&self) -> usize {
1415        self.cache.read().unwrap().len()
1416    }
1417
1418    /// Check if the cache is empty.
1419    pub fn is_empty(&self) -> bool {
1420        self.cache.read().unwrap().is_empty()
1421    }
1422}
1423
1424/// Global SQL template cache for common query patterns.
1425///
1426/// This provides a shared cache across the application for frequently used
1427/// SQL templates, reducing memory usage and improving performance.
1428///
1429/// # Example
1430///
1431/// ```rust
1432/// use prax_query::sql::{global_sql_cache, DatabaseType};
1433///
1434/// let sql = global_sql_cache().get_or_insert("find_user_by_id", DatabaseType::PostgreSQL, || {
1435///     "SELECT * FROM users WHERE id = $1".to_string()
1436/// });
1437/// ```
1438pub fn global_sql_cache() -> &'static SqlTemplateCache {
1439    static CACHE: OnceLock<SqlTemplateCache> = OnceLock::new();
1440    CACHE.get_or_init(|| SqlTemplateCache::with_capacity(64))
1441}
1442
1443// ==============================================================================
1444// Enhanced Capacity Estimation for Advanced Features
1445// ==============================================================================
1446
1447/// Extended capacity hints for advanced query types.
1448#[derive(Debug, Clone, Copy)]
1449pub enum AdvancedQueryCapacity {
1450    /// Common Table Expression (CTE)
1451    Cte {
1452        /// Number of CTEs in WITH clause
1453        cte_count: usize,
1454        /// Average query length per CTE
1455        avg_query_len: usize,
1456    },
1457    /// Window function query
1458    WindowFunction {
1459        /// Number of window functions
1460        window_count: usize,
1461        /// Number of partition columns
1462        partition_cols: usize,
1463        /// Number of order by columns
1464        order_cols: usize,
1465    },
1466    /// Full-text search query
1467    FullTextSearch {
1468        /// Number of search columns
1469        columns: usize,
1470        /// Search query length
1471        query_len: usize,
1472    },
1473    /// JSON path query
1474    JsonPath {
1475        /// Path depth
1476        depth: usize,
1477    },
1478    /// Upsert with conflict handling
1479    Upsert {
1480        /// Number of columns
1481        columns: usize,
1482        /// Number of conflict columns
1483        conflict_cols: usize,
1484        /// Number of update columns
1485        update_cols: usize,
1486    },
1487    /// Stored procedure/function call
1488    ProcedureCall {
1489        /// Number of parameters
1490        params: usize,
1491    },
1492    /// Trigger definition
1493    TriggerDef {
1494        /// Number of events
1495        events: usize,
1496        /// Body length estimate
1497        body_len: usize,
1498    },
1499    /// Security policy (RLS)
1500    RlsPolicy {
1501        /// Expression length
1502        expr_len: usize,
1503    },
1504}
1505
1506impl AdvancedQueryCapacity {
1507    /// Get the estimated capacity in bytes.
1508    #[inline]
1509    pub const fn estimate(&self) -> usize {
1510        match self {
1511            Self::Cte {
1512                cte_count,
1513                avg_query_len,
1514            } => {
1515                // WITH + cte_name AS (query), ...
1516                16 + *cte_count * (32 + *avg_query_len)
1517            }
1518            Self::WindowFunction {
1519                window_count,
1520                partition_cols,
1521                order_cols,
1522            } => {
1523                // func() OVER (PARTITION BY ... ORDER BY ...)
1524                *window_count * (48 + *partition_cols * 16 + *order_cols * 20)
1525            }
1526            Self::FullTextSearch { columns, query_len } => {
1527                // to_tsvector() @@ plainto_tsquery() or MATCH(...) AGAINST(...)
1528                64 + *columns * 20 + *query_len
1529            }
1530            Self::JsonPath { depth } => {
1531                // column->'path'->'nested'
1532                16 + *depth * 12
1533            }
1534            Self::Upsert {
1535                columns,
1536                conflict_cols,
1537                update_cols,
1538            } => {
1539                // INSERT ... ON CONFLICT (cols) DO UPDATE SET ...
1540                64 + *columns * 8 + *conflict_cols * 12 + *update_cols * 16
1541            }
1542            Self::ProcedureCall { params } => {
1543                // CALL proc_name($1, $2, ...)
1544                32 + *params * 8
1545            }
1546            Self::TriggerDef { events, body_len } => {
1547                // CREATE TRIGGER ... BEFORE/AFTER ... ON table ...
1548                96 + *events * 12 + *body_len
1549            }
1550            Self::RlsPolicy { expr_len } => {
1551                // CREATE POLICY ... USING (...)
1552                64 + *expr_len
1553            }
1554        }
1555    }
1556
1557    /// Convert to QueryCapacity::Custom for use with FastSqlBuilder.
1558    #[inline]
1559    pub const fn to_query_capacity(&self) -> QueryCapacity {
1560        QueryCapacity::Custom(self.estimate())
1561    }
1562}
1563
1564/// Create a FastSqlBuilder with capacity for advanced queries.
1565impl FastSqlBuilder {
1566    /// Create a builder with capacity estimated for advanced query types.
1567    #[inline]
1568    pub fn for_advanced(db_type: DatabaseType, capacity: AdvancedQueryCapacity) -> Self {
1569        Self::with_capacity(db_type, capacity.to_query_capacity())
1570    }
1571
1572    /// Create a builder for CTE queries.
1573    #[inline]
1574    pub fn for_cte(db_type: DatabaseType, cte_count: usize, avg_query_len: usize) -> Self {
1575        Self::for_advanced(
1576            db_type,
1577            AdvancedQueryCapacity::Cte {
1578                cte_count,
1579                avg_query_len,
1580            },
1581        )
1582    }
1583
1584    /// Create a builder for window function queries.
1585    #[inline]
1586    pub fn for_window(
1587        db_type: DatabaseType,
1588        window_count: usize,
1589        partition_cols: usize,
1590        order_cols: usize,
1591    ) -> Self {
1592        Self::for_advanced(
1593            db_type,
1594            AdvancedQueryCapacity::WindowFunction {
1595                window_count,
1596                partition_cols,
1597                order_cols,
1598            },
1599        )
1600    }
1601
1602    /// Create a builder for upsert queries.
1603    #[inline]
1604    pub fn for_upsert(
1605        db_type: DatabaseType,
1606        columns: usize,
1607        conflict_cols: usize,
1608        update_cols: usize,
1609    ) -> Self {
1610        Self::for_advanced(
1611            db_type,
1612            AdvancedQueryCapacity::Upsert {
1613                columns,
1614                conflict_cols,
1615                update_cols,
1616            },
1617        )
1618    }
1619}
1620
1621#[cfg(test)]
1622mod tests {
1623    use super::*;
1624
1625    #[test]
1626    fn test_escape_identifier() {
1627        assert_eq!(escape_identifier("user"), "\"user\"");
1628        assert_eq!(escape_identifier("my_table"), "\"my_table\"");
1629        assert_eq!(escape_identifier("has\"quote"), "\"has\"\"quote\"");
1630    }
1631
1632    #[test]
1633    fn test_needs_quoting() {
1634        assert!(needs_quoting("user"));
1635        assert!(needs_quoting("order"));
1636        assert!(needs_quoting("has space"));
1637        assert!(!needs_quoting("my_table"));
1638        assert!(!needs_quoting("users"));
1639    }
1640
1641    #[test]
1642    fn test_quote_identifier() {
1643        assert_eq!(quote_identifier("user"), "\"user\"");
1644        assert_eq!(quote_identifier("my_table"), "my_table");
1645    }
1646
1647    #[test]
1648    fn test_database_placeholder() {
1649        // Basic placeholder values
1650        assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
1651        assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
1652        assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
1653        assert_eq!(DatabaseType::PostgreSQL.placeholder(128).as_ref(), "$128");
1654        assert_eq!(DatabaseType::PostgreSQL.placeholder(256).as_ref(), "$256");
1655        assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
1656        assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
1657
1658        // Verify MySQL/SQLite return borrowed (zero allocation)
1659        assert!(matches!(
1660            DatabaseType::MySQL.placeholder(1),
1661            Cow::Borrowed(_)
1662        ));
1663        assert!(matches!(
1664            DatabaseType::SQLite.placeholder(1),
1665            Cow::Borrowed(_)
1666        ));
1667
1668        // PostgreSQL returns borrowed for indices 1-256 (zero allocation via lookup table)
1669        assert!(matches!(
1670            DatabaseType::PostgreSQL.placeholder(1),
1671            Cow::Borrowed(_)
1672        ));
1673        assert!(matches!(
1674            DatabaseType::PostgreSQL.placeholder(50),
1675            Cow::Borrowed(_)
1676        ));
1677        assert!(matches!(
1678            DatabaseType::PostgreSQL.placeholder(128),
1679            Cow::Borrowed(_)
1680        ));
1681        assert!(matches!(
1682            DatabaseType::PostgreSQL.placeholder(256),
1683            Cow::Borrowed(_)
1684        ));
1685
1686        // PostgreSQL returns owned for indices > 256 (must format)
1687        assert!(matches!(
1688            DatabaseType::PostgreSQL.placeholder(257),
1689            Cow::Owned(_)
1690        ));
1691        assert_eq!(DatabaseType::PostgreSQL.placeholder(257).as_ref(), "$257");
1692        assert_eq!(DatabaseType::PostgreSQL.placeholder(200).as_ref(), "$200");
1693
1694        // Edge case: index 0 falls back to format (unusual but handled)
1695        assert!(matches!(
1696            DatabaseType::PostgreSQL.placeholder(0),
1697            Cow::Owned(_)
1698        ));
1699        assert_eq!(DatabaseType::PostgreSQL.placeholder(0).as_ref(), "$0");
1700    }
1701
1702    #[test]
1703    fn test_sql_builder() {
1704        let mut builder = SqlBuilder::postgres();
1705        builder
1706            .push("SELECT * FROM ")
1707            .push_identifier("user")
1708            .push(" WHERE ")
1709            .push_identifier("id")
1710            .push(" = ")
1711            .push_param(42i32);
1712
1713        let (sql, params) = builder.build();
1714        assert_eq!(sql, "SELECT * FROM \"user\" WHERE id = $1");
1715        assert_eq!(params.len(), 1);
1716    }
1717
1718    // FastSqlBuilder tests
1719    #[test]
1720    fn test_fast_builder_simple() {
1721        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1722        builder.push_str("SELECT * FROM users WHERE id = ");
1723        builder.bind(42i64);
1724        let (sql, params) = builder.build();
1725        assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1726        assert_eq!(params.len(), 1);
1727    }
1728
1729    #[test]
1730    fn test_fast_builder_complex() {
1731        let mut builder = FastSqlBuilder::with_capacity(
1732            DatabaseType::PostgreSQL,
1733            QueryCapacity::SelectWithFilters(5),
1734        );
1735        builder
1736            .push_str("SELECT * FROM users WHERE active = ")
1737            .bind(true)
1738            .push_str(" AND age > ")
1739            .bind(18i64)
1740            .push_str(" AND status = ")
1741            .bind("approved")
1742            .push_str(" ORDER BY created_at LIMIT ")
1743            .bind(10i64);
1744
1745        let (sql, params) = builder.build();
1746        assert!(sql.contains("$1"));
1747        assert!(sql.contains("$4"));
1748        assert_eq!(params.len(), 4);
1749    }
1750
1751    #[test]
1752    fn test_fast_builder_in_clause_postgres() {
1753        let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
1754        builder.push_str("SELECT * FROM users WHERE id IN (");
1755        let values: Vec<FilterValue> = (1..=5).map(|i| FilterValue::Int(i)).collect();
1756        builder.bind_in_clause(values);
1757        builder.push_char(')');
1758
1759        let (sql, params) = builder.build();
1760        assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
1761        assert_eq!(params.len(), 5);
1762    }
1763
1764    #[test]
1765    fn test_fast_builder_in_clause_mysql() {
1766        let mut builder = FastSqlBuilder::mysql(QueryCapacity::Custom(128));
1767        builder.push_str("SELECT * FROM users WHERE id IN (");
1768        let values: Vec<FilterValue> = (1..=5).map(|i| FilterValue::Int(i)).collect();
1769        builder.bind_in_clause(values);
1770        builder.push_char(')');
1771
1772        let (sql, params) = builder.build();
1773        assert_eq!(sql, "SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?)");
1774        assert_eq!(params.len(), 5);
1775    }
1776
1777    #[test]
1778    fn test_fast_builder_identifier() {
1779        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1780        builder.push_str("SELECT * FROM ");
1781        builder.push_identifier("user"); // reserved word
1782        builder.push_str(" WHERE ");
1783        builder.push_identifier("my_column"); // not reserved
1784        builder.push_str(" = ");
1785        builder.bind(1i64);
1786
1787        let (sql, _) = builder.build();
1788        assert_eq!(sql, "SELECT * FROM \"user\" WHERE my_column = $1");
1789    }
1790
1791    #[test]
1792    fn test_fast_builder_identifier_with_quotes() {
1793        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1794        builder.push_str("SELECT * FROM ");
1795        builder.push_identifier("has\"quote");
1796
1797        let sql = builder.build_sql();
1798        assert_eq!(sql, "SELECT * FROM \"has\"\"quote\"");
1799    }
1800
1801    #[test]
1802    fn test_fast_builder_conditional() {
1803        let mut builder = FastSqlBuilder::postgres(QueryCapacity::SelectWithFilters(2));
1804        builder.push_str("SELECT * FROM users WHERE 1=1");
1805        builder.push_if(true, " AND active = true");
1806        builder.push_if(false, " AND deleted = false");
1807
1808        let sql = builder.build_sql();
1809        assert_eq!(sql, "SELECT * FROM users WHERE 1=1 AND active = true");
1810    }
1811
1812    // Template tests
1813    #[test]
1814    fn test_template_select_by_id() {
1815        let sql = templates::select_by_id("users", &["id", "name", "email"]);
1816        assert_eq!(sql, "SELECT id, name, email FROM users WHERE id = $1");
1817    }
1818
1819    #[test]
1820    fn test_template_select_by_id_all_columns() {
1821        let sql = templates::select_by_id("users", &[]);
1822        assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1823    }
1824
1825    #[test]
1826    fn test_template_insert_returning() {
1827        let sql = templates::insert_returning("users", &["name", "email"]);
1828        assert_eq!(
1829            sql,
1830            "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
1831        );
1832    }
1833
1834    #[test]
1835    fn test_template_update_by_id() {
1836        let sql = templates::update_by_id("users", &["name", "email"]);
1837        assert_eq!(sql, "UPDATE users SET name = $1, email = $2 WHERE id = $3");
1838    }
1839
1840    #[test]
1841    fn test_template_delete_by_id() {
1842        let sql = templates::delete_by_id("users");
1843        assert_eq!(sql, "DELETE FROM users WHERE id = $1");
1844    }
1845
1846    #[test]
1847    fn test_template_batch_placeholders_postgres() {
1848        let sql = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
1849        assert_eq!(sql, "($1, $2, $3), ($4, $5, $6)");
1850    }
1851
1852    #[test]
1853    fn test_template_batch_placeholders_mysql() {
1854        let sql = templates::batch_placeholders(DatabaseType::MySQL, 3, 2);
1855        assert_eq!(sql, "(?, ?, ?), (?, ?, ?)");
1856    }
1857
1858    #[test]
1859    fn test_query_capacity_estimates() {
1860        assert_eq!(QueryCapacity::SimpleSelect.estimate(), 64);
1861        assert_eq!(QueryCapacity::SelectWithFilters(5).estimate(), 64 + 5 * 32);
1862        assert_eq!(QueryCapacity::Insert(10).estimate(), 32 + 10 * 16);
1863        assert_eq!(QueryCapacity::Update(5).estimate(), 32 + 5 * 20);
1864        assert_eq!(QueryCapacity::Delete.estimate(), 48);
1865        assert_eq!(QueryCapacity::Custom(256).estimate(), 256);
1866    }
1867}