1use 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
18pub mod keywords {
42 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 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 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 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 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 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 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 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 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 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 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 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 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
184pub fn escape_identifier(name: &str) -> String {
186 let escaped = name.replace('"', "\"\"");
188 format!("\"{}\"", escaped)
189}
190
191pub fn needs_quoting(name: &str) -> bool {
193 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 if reserved.contains(&name.to_lowercase().as_str()) {
253 return true;
254 }
255
256 !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
258}
259
260pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
271pub enum DatabaseType {
272 #[default]
274 PostgreSQL,
275 MySQL,
277 SQLite,
279 MSSQL,
281}
282
283const QUESTION_MARK_PLACEHOLDER: &str = "?";
285
286pub 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
322pub const MYSQL_IN_PATTERNS: &[&str] = &[
325 "", "?",
327 "?, ?",
328 "?, ?, ?",
329 "?, ?, ?, ?",
330 "?, ?, ?, ?, ?",
331 "?, ?, ?, ?, ?, ?",
332 "?, ?, ?, ?, ?, ?, ?",
333 "?, ?, ?, ?, ?, ?, ?, ?",
334 "?, ?, ?, ?, ?, ?, ?, ?, ?",
335 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
337 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
338 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
339 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
340 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
341 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
343 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
344 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
345 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
347 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
348 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
349 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
350 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
352 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
353 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
354 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
355 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
357 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", ];
359
360#[inline]
370pub fn postgres_in_pattern(start_idx: usize, count: usize) -> String {
371 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 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
406const POSTGRES_IN_FROM_1: &[&str] = &[
409 "", "$1", "$1, $2", "$1, $2, $3", "$1, $2, $3, $4", "$1, $2, $3, $4, $5", "$1, $2, $3, $4, $5, $6", "$1, $2, $3, $4, $5, $6, $7", "$1, $2, $3, $4, $5, $6, $7, $8", "$1, $2, $3, $4, $5, $6, $7, $8, $9", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23", "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24", "$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", "$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", "$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", "$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", "$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", "$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", "$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", "$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", ];
443
444#[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 if start_idx == 1 && count < POSTGRES_IN_FROM_1.len() {
459 buf.push_str(POSTGRES_IN_FROM_1[count]);
460 return;
461 }
462
463 buf.reserve(count * 6);
466
467 let end_idx = start_idx + count;
469 let table_len = POSTGRES_PLACEHOLDERS.len();
470
471 if end_idx <= table_len {
472 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 let _ = write!(buf, "${}", start_idx);
481 for idx in (start_idx + 1)..end_idx {
482 let _ = write!(buf, ", ${}", idx);
483 }
484 } else {
485 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 #[inline]
519 pub fn placeholder(&self, index: usize) -> Cow<'static, str> {
520 match self {
521 Self::PostgreSQL => {
522 if index > 0 && index < POSTGRES_PLACEHOLDERS.len() {
524 Cow::Borrowed(POSTGRES_PLACEHOLDERS[index])
525 } else {
526 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 #[inline]
540 pub fn placeholder_string(&self, index: usize) -> String {
541 self.placeholder(index).into_owned()
542 }
543}
544
545#[derive(Debug, Clone)]
547pub struct SqlBuilder {
548 db_type: DatabaseType,
549 parts: Vec<String>,
550 params: Vec<FilterValue>,
551}
552
553impl SqlBuilder {
554 pub fn new(db_type: DatabaseType) -> Self {
556 Self {
557 db_type,
558 parts: Vec::new(),
559 params: Vec::new(),
560 }
561 }
562
563 pub fn postgres() -> Self {
565 Self::new(DatabaseType::PostgreSQL)
566 }
567
568 pub fn mysql() -> Self {
570 Self::new(DatabaseType::MySQL)
571 }
572
573 pub fn sqlite() -> Self {
575 Self::new(DatabaseType::SQLite)
576 }
577
578 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 pub fn push_param(&mut self, value: impl Into<FilterValue>) -> &mut Self {
586 let index = self.params.len() + 1;
587 self.parts
590 .push(self.db_type.placeholder(index).into_owned());
591 self.params.push(value.into());
592 self
593 }
594
595 pub fn push_identifier(&mut self, name: &str) -> &mut Self {
597 self.parts.push(quote_identifier(name));
598 self
599 }
600
601 pub fn push_sep(&mut self, sep: &str) -> &mut Self {
603 self.parts.push(sep.to_string());
604 self
605 }
606
607 pub fn build(self) -> (String, Vec<FilterValue>) {
609 (self.parts.join(""), self.params)
610 }
611
612 pub fn sql(&self) -> String {
614 self.parts.join("")
615 }
616
617 pub fn params(&self) -> &[FilterValue] {
619 &self.params
620 }
621
622 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#[derive(Debug, Clone, Copy)]
640pub enum QueryCapacity {
641 SimpleSelect,
643 SelectWithFilters(usize),
645 Insert(usize),
647 Update(usize),
649 Delete,
651 Custom(usize),
653}
654
655impl QueryCapacity {
656 #[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#[derive(Debug, Clone)]
707pub struct FastSqlBuilder {
708 buffer: String,
710 params: Vec<FilterValue>,
712 db_type: DatabaseType,
714}
715
716impl FastSqlBuilder {
717 #[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 #[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 #[inline]
746 pub fn postgres(capacity: QueryCapacity) -> Self {
747 Self::with_capacity(DatabaseType::PostgreSQL, capacity)
748 }
749
750 #[inline]
752 pub fn mysql(capacity: QueryCapacity) -> Self {
753 Self::with_capacity(DatabaseType::MySQL, capacity)
754 }
755
756 #[inline]
758 pub fn sqlite(capacity: QueryCapacity) -> Self {
759 Self::with_capacity(DatabaseType::SQLite, capacity)
760 }
761
762 #[inline]
764 pub fn push_str(&mut self, s: &str) -> &mut Self {
765 self.buffer.push_str(s);
766 self
767 }
768
769 #[inline]
771 pub fn push_char(&mut self, c: char) -> &mut Self {
772 self.buffer.push(c);
773 self
774 }
775
776 #[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 #[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 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 match self.db_type {
830 DatabaseType::PostgreSQL => {
831 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 if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
850 self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
851 } else {
852 let estimated_len = count * 3; 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 let estimated_len = count * 6; 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 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 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 self.params.reserve(count);
959 for v in values {
960 self.params.push(v.clone().into());
961 }
962 self
963 }
964
965 #[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 #[inline]
977 pub fn push_identifier(&mut self, name: &str) -> &mut Self {
978 if needs_quoting(name) {
979 self.buffer.push('"');
980 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 #[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 #[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 #[inline]
1015 pub fn sql(&self) -> &str {
1016 &self.buffer
1017 }
1018
1019 #[inline]
1021 pub fn params(&self) -> &[FilterValue] {
1022 &self.params
1023 }
1024
1025 #[inline]
1027 pub fn param_count(&self) -> usize {
1028 self.params.len()
1029 }
1030
1031 #[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 #[inline]
1042 pub fn build_sql(self) -> String {
1043 self.buffer
1044 }
1045}
1046
1047pub mod templates {
1055 use super::*;
1056
1057 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 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 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 pub fn delete_by_id(table: &str) -> String {
1157 format!("DELETE FROM {} WHERE id = $1", table)
1158 }
1159
1160 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
1209pub 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 #[inline]
1245 pub const fn new(generator: F) -> Self {
1246 Self { generator }
1247 }
1248
1249 #[inline]
1251 pub fn get(&self, db_type: DatabaseType) -> String {
1252 (self.generator)(db_type)
1253 }
1254}
1255
1256pub 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 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 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
1318pub 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 pub fn new() -> Self {
1359 Self {
1360 cache: RwLock::new(HashMap::new()),
1361 }
1362 }
1363
1364 pub fn with_capacity(capacity: usize) -> Self {
1366 Self {
1367 cache: RwLock::new(HashMap::with_capacity(capacity)),
1368 }
1369 }
1370
1371 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 {
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 let mut cache = self.cache.write().unwrap();
1391
1392 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 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 pub fn clear(&self) {
1410 self.cache.write().unwrap().clear();
1411 }
1412
1413 pub fn len(&self) -> usize {
1415 self.cache.read().unwrap().len()
1416 }
1417
1418 pub fn is_empty(&self) -> bool {
1420 self.cache.read().unwrap().is_empty()
1421 }
1422}
1423
1424pub 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#[derive(Debug, Clone, Copy)]
1449pub enum AdvancedQueryCapacity {
1450 Cte {
1452 cte_count: usize,
1454 avg_query_len: usize,
1456 },
1457 WindowFunction {
1459 window_count: usize,
1461 partition_cols: usize,
1463 order_cols: usize,
1465 },
1466 FullTextSearch {
1468 columns: usize,
1470 query_len: usize,
1472 },
1473 JsonPath {
1475 depth: usize,
1477 },
1478 Upsert {
1480 columns: usize,
1482 conflict_cols: usize,
1484 update_cols: usize,
1486 },
1487 ProcedureCall {
1489 params: usize,
1491 },
1492 TriggerDef {
1494 events: usize,
1496 body_len: usize,
1498 },
1499 RlsPolicy {
1501 expr_len: usize,
1503 },
1504}
1505
1506impl AdvancedQueryCapacity {
1507 #[inline]
1509 pub const fn estimate(&self) -> usize {
1510 match self {
1511 Self::Cte {
1512 cte_count,
1513 avg_query_len,
1514 } => {
1515 16 + *cte_count * (32 + *avg_query_len)
1517 }
1518 Self::WindowFunction {
1519 window_count,
1520 partition_cols,
1521 order_cols,
1522 } => {
1523 *window_count * (48 + *partition_cols * 16 + *order_cols * 20)
1525 }
1526 Self::FullTextSearch { columns, query_len } => {
1527 64 + *columns * 20 + *query_len
1529 }
1530 Self::JsonPath { depth } => {
1531 16 + *depth * 12
1533 }
1534 Self::Upsert {
1535 columns,
1536 conflict_cols,
1537 update_cols,
1538 } => {
1539 64 + *columns * 8 + *conflict_cols * 12 + *update_cols * 16
1541 }
1542 Self::ProcedureCall { params } => {
1543 32 + *params * 8
1545 }
1546 Self::TriggerDef { events, body_len } => {
1547 96 + *events * 12 + *body_len
1549 }
1550 Self::RlsPolicy { expr_len } => {
1551 64 + *expr_len
1553 }
1554 }
1555 }
1556
1557 #[inline]
1559 pub const fn to_query_capacity(&self) -> QueryCapacity {
1560 QueryCapacity::Custom(self.estimate())
1561 }
1562}
1563
1564impl FastSqlBuilder {
1566 #[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 #[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 #[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 #[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 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 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 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 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 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 #[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"); builder.push_str(" WHERE ");
1783 builder.push_identifier("my_column"); 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 #[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}