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";
44 pub const INSERT: &str = "INSERT";
45 pub const UPDATE: &str = "UPDATE";
46 pub const DELETE: &str = "DELETE";
47 pub const INTO: &str = "INTO";
48 pub const VALUES: &str = "VALUES";
49 pub const SET: &str = "SET";
50 pub const FROM: &str = "FROM";
51 pub const WHERE: &str = "WHERE";
52 pub const RETURNING: &str = "RETURNING";
53
54 pub const AND: &str = "AND";
56 pub const OR: &str = "OR";
57 pub const NOT: &str = "NOT";
58 pub const IN: &str = "IN";
59 pub const IS: &str = "IS";
60 pub const NULL: &str = "NULL";
61 pub const LIKE: &str = "LIKE";
62 pub const ILIKE: &str = "ILIKE";
63 pub const BETWEEN: &str = "BETWEEN";
64 pub const EXISTS: &str = "EXISTS";
65
66 pub const ORDER_BY: &str = "ORDER BY";
68 pub const ASC: &str = "ASC";
69 pub const DESC: &str = "DESC";
70 pub const NULLS_FIRST: &str = "NULLS FIRST";
71 pub const NULLS_LAST: &str = "NULLS LAST";
72 pub const LIMIT: &str = "LIMIT";
73 pub const OFFSET: &str = "OFFSET";
74
75 pub const GROUP_BY: &str = "GROUP BY";
77 pub const HAVING: &str = "HAVING";
78 pub const DISTINCT: &str = "DISTINCT";
79 pub const DISTINCT_ON: &str = "DISTINCT ON";
80
81 pub const JOIN: &str = "JOIN";
83 pub const INNER_JOIN: &str = "INNER JOIN";
84 pub const LEFT_JOIN: &str = "LEFT JOIN";
85 pub const RIGHT_JOIN: &str = "RIGHT JOIN";
86 pub const FULL_JOIN: &str = "FULL OUTER JOIN";
87 pub const CROSS_JOIN: &str = "CROSS JOIN";
88 pub const LATERAL: &str = "LATERAL";
89 pub const ON: &str = "ON";
90 pub const USING: &str = "USING";
91
92 pub const WITH: &str = "WITH";
94 pub const RECURSIVE: &str = "RECURSIVE";
95 pub const AS: &str = "AS";
96 pub const MATERIALIZED: &str = "MATERIALIZED";
97 pub const NOT_MATERIALIZED: &str = "NOT MATERIALIZED";
98
99 pub const OVER: &str = "OVER";
101 pub const PARTITION_BY: &str = "PARTITION BY";
102 pub const ROWS: &str = "ROWS";
103 pub const RANGE: &str = "RANGE";
104 pub const GROUPS: &str = "GROUPS";
105 pub const UNBOUNDED_PRECEDING: &str = "UNBOUNDED PRECEDING";
106 pub const UNBOUNDED_FOLLOWING: &str = "UNBOUNDED FOLLOWING";
107 pub const CURRENT_ROW: &str = "CURRENT ROW";
108 pub const PRECEDING: &str = "PRECEDING";
109 pub const FOLLOWING: &str = "FOLLOWING";
110
111 pub const COUNT: &str = "COUNT";
113 pub const SUM: &str = "SUM";
114 pub const AVG: &str = "AVG";
115 pub const MIN: &str = "MIN";
116 pub const MAX: &str = "MAX";
117 pub const ROW_NUMBER: &str = "ROW_NUMBER";
118 pub const RANK: &str = "RANK";
119 pub const DENSE_RANK: &str = "DENSE_RANK";
120 pub const LAG: &str = "LAG";
121 pub const LEAD: &str = "LEAD";
122 pub const FIRST_VALUE: &str = "FIRST_VALUE";
123 pub const LAST_VALUE: &str = "LAST_VALUE";
124 pub const NTILE: &str = "NTILE";
125
126 pub const ON_CONFLICT: &str = "ON CONFLICT";
128 pub const DO_NOTHING: &str = "DO NOTHING";
129 pub const DO_UPDATE: &str = "DO UPDATE";
130 pub const EXCLUDED: &str = "excluded";
131 pub const ON_DUPLICATE_KEY: &str = "ON DUPLICATE KEY UPDATE";
132 pub const MERGE: &str = "MERGE";
133 pub const MATCHED: &str = "MATCHED";
134 pub const NOT_MATCHED: &str = "NOT MATCHED";
135
136 pub const FOR_UPDATE: &str = "FOR UPDATE";
138 pub const FOR_SHARE: &str = "FOR SHARE";
139 pub const NOWAIT: &str = "NOWAIT";
140 pub const SKIP_LOCKED: &str = "SKIP LOCKED";
141
142 pub const CREATE: &str = "CREATE";
144 pub const ALTER: &str = "ALTER";
145 pub const DROP: &str = "DROP";
146 pub const TABLE: &str = "TABLE";
147 pub const INDEX: &str = "INDEX";
148 pub const VIEW: &str = "VIEW";
149 pub const TRIGGER: &str = "TRIGGER";
150 pub const FUNCTION: &str = "FUNCTION";
151 pub const PROCEDURE: &str = "PROCEDURE";
152 pub const SEQUENCE: &str = "SEQUENCE";
153 pub const IF_EXISTS: &str = "IF EXISTS";
154 pub const IF_NOT_EXISTS: &str = "IF NOT EXISTS";
155 pub const OR_REPLACE: &str = "OR REPLACE";
156 pub const CASCADE: &str = "CASCADE";
157 pub const RESTRICT: &str = "RESTRICT";
158
159 pub const PRIMARY_KEY: &str = "PRIMARY KEY";
161 pub const FOREIGN_KEY: &str = "FOREIGN KEY";
162 pub const REFERENCES: &str = "REFERENCES";
163 pub const UNIQUE: &str = "UNIQUE";
164 pub const CHECK: &str = "CHECK";
165 pub const DEFAULT: &str = "DEFAULT";
166 pub const NOT_NULL: &str = "NOT NULL";
167
168 pub const SPACE: &str = " ";
170 pub const COMMA_SPACE: &str = ", ";
171 pub const OPEN_PAREN: &str = "(";
172 pub const CLOSE_PAREN: &str = ")";
173 pub const STAR: &str = "*";
174 pub const EQUALS: &str = " = ";
175 pub const NOT_EQUALS: &str = " <> ";
176 pub const LESS_THAN: &str = " < ";
177 pub const GREATER_THAN: &str = " > ";
178 pub const LESS_OR_EQUAL: &str = " <= ";
179 pub const GREATER_OR_EQUAL: &str = " >= ";
180}
181
182pub fn escape_identifier(name: &str) -> String {
184 let escaped = name.replace('"', "\"\"");
186 format!("\"{}\"", escaped)
187}
188
189pub fn is_valid_sql_identifier(name: &str) -> bool {
196 let mut chars = name.chars();
197 match chars.next() {
198 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
199 _ => return false,
200 }
201 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
202}
203
204pub fn escape_literal(value: &str) -> String {
210 value.replace('\'', "''")
211}
212
213pub fn needs_quoting(name: &str) -> bool {
215 let reserved = [
217 "user",
218 "order",
219 "group",
220 "select",
221 "from",
222 "where",
223 "table",
224 "index",
225 "key",
226 "primary",
227 "foreign",
228 "check",
229 "default",
230 "null",
231 "not",
232 "and",
233 "or",
234 "in",
235 "is",
236 "like",
237 "between",
238 "case",
239 "when",
240 "then",
241 "else",
242 "end",
243 "as",
244 "on",
245 "join",
246 "left",
247 "right",
248 "inner",
249 "outer",
250 "cross",
251 "natural",
252 "using",
253 "limit",
254 "offset",
255 "union",
256 "intersect",
257 "except",
258 "all",
259 "distinct",
260 "having",
261 "create",
262 "alter",
263 "drop",
264 "insert",
265 "update",
266 "delete",
267 "into",
268 "values",
269 "set",
270 "returning",
271 ];
272
273 if reserved.contains(&name.to_lowercase().as_str()) {
275 return true;
276 }
277
278 !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
280}
281
282pub fn quote_identifier(name: &str) -> String {
284 if needs_quoting(name) {
285 escape_identifier(name)
286 } else {
287 name.to_string()
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
293pub enum DatabaseType {
294 #[default]
296 PostgreSQL,
297 MySQL,
299 SQLite,
301 MSSQL,
303}
304
305const QUESTION_MARK_PLACEHOLDER: &str = "?";
307
308pub const POSTGRES_PLACEHOLDERS: &[&str] = &[
321 "$0", "$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14",
322 "$15", "$16", "$17", "$18", "$19", "$20", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
323 "$28", "$29", "$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37", "$38", "$39", "$40",
324 "$41", "$42", "$43", "$44", "$45", "$46", "$47", "$48", "$49", "$50", "$51", "$52", "$53",
325 "$54", "$55", "$56", "$57", "$58", "$59", "$60", "$61", "$62", "$63", "$64", "$65", "$66",
326 "$67", "$68", "$69", "$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77", "$78", "$79",
327 "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", "$88", "$89", "$90", "$91", "$92",
328 "$93", "$94", "$95", "$96", "$97", "$98", "$99", "$100", "$101", "$102", "$103", "$104",
329 "$105", "$106", "$107", "$108", "$109", "$110", "$111", "$112", "$113", "$114", "$115", "$116",
330 "$117", "$118", "$119", "$120", "$121", "$122", "$123", "$124", "$125", "$126", "$127", "$128",
331 "$129", "$130", "$131", "$132", "$133", "$134", "$135", "$136", "$137", "$138", "$139", "$140",
332 "$141", "$142", "$143", "$144", "$145", "$146", "$147", "$148", "$149", "$150", "$151", "$152",
333 "$153", "$154", "$155", "$156", "$157", "$158", "$159", "$160", "$161", "$162", "$163", "$164",
334 "$165", "$166", "$167", "$168", "$169", "$170", "$171", "$172", "$173", "$174", "$175", "$176",
335 "$177", "$178", "$179", "$180", "$181", "$182", "$183", "$184", "$185", "$186", "$187", "$188",
336 "$189", "$190", "$191", "$192", "$193", "$194", "$195", "$196", "$197", "$198", "$199", "$200",
337 "$201", "$202", "$203", "$204", "$205", "$206", "$207", "$208", "$209", "$210", "$211", "$212",
338 "$213", "$214", "$215", "$216", "$217", "$218", "$219", "$220", "$221", "$222", "$223", "$224",
339 "$225", "$226", "$227", "$228", "$229", "$230", "$231", "$232", "$233", "$234", "$235", "$236",
340 "$237", "$238", "$239", "$240", "$241", "$242", "$243", "$244", "$245", "$246", "$247", "$248",
341 "$249", "$250", "$251", "$252", "$253", "$254", "$255", "$256",
342];
343
344pub const MYSQL_IN_PATTERNS: &[&str] = &[
347 "", "?",
349 "?, ?",
350 "?, ?, ?",
351 "?, ?, ?, ?",
352 "?, ?, ?, ?, ?",
353 "?, ?, ?, ?, ?, ?",
354 "?, ?, ?, ?, ?, ?, ?",
355 "?, ?, ?, ?, ?, ?, ?, ?",
356 "?, ?, ?, ?, ?, ?, ?, ?, ?",
357 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
359 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
360 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
361 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
362 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
363 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
365 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
366 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
367 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
369 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
370 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
371 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
372 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
374 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
375 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
376 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
377 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
379 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", ];
381
382#[inline]
397pub fn postgres_in_pattern(start_idx: usize, count: usize) -> String {
398 if start_idx == 1 && count <= 10 {
400 return POSTGRES_IN_FROM_1[count].to_string();
401 }
402
403 let mut result = String::with_capacity(count * 5);
405 for i in 0..count {
406 if i > 0 {
407 result.push_str(", ");
408 }
409 let idx = start_idx + i;
410 if idx < POSTGRES_PLACEHOLDERS.len() {
411 result.push_str(POSTGRES_PLACEHOLDERS[idx]);
412 } else {
413 use std::fmt::Write;
414 let _ = write!(result, "${}", idx);
415 }
416 }
417 result
418}
419
420const POSTGRES_IN_FROM_1: &[&str] = &[
423 "", "$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", ];
457
458#[inline]
465#[allow(clippy::needless_range_loop)]
466pub fn write_postgres_in_pattern(buf: &mut String, start_idx: usize, count: usize) {
467 if count == 0 {
468 return;
469 }
470
471 if start_idx == 1 && count < POSTGRES_IN_FROM_1.len() {
473 buf.push_str(POSTGRES_IN_FROM_1[count]);
474 return;
475 }
476
477 buf.reserve(count * 6);
480
481 let end_idx = start_idx + count;
483 let table_len = POSTGRES_PLACEHOLDERS.len();
484
485 if end_idx <= table_len {
486 buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
488 for idx in (start_idx + 1)..end_idx {
489 buf.push_str(", ");
490 buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
491 }
492 } else if start_idx >= table_len {
493 let _ = write!(buf, "${}", start_idx);
495 for idx in (start_idx + 1)..end_idx {
496 let _ = write!(buf, ", ${}", idx);
497 }
498 } else {
499 buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
501 for idx in (start_idx + 1)..table_len.min(end_idx) {
502 buf.push_str(", ");
503 buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
504 }
505 for idx in table_len..end_idx {
506 let _ = write!(buf, ", ${}", idx);
507 }
508 }
509}
510
511impl DatabaseType {
512 #[inline]
533 pub fn placeholder(&self, index: usize) -> Cow<'static, str> {
534 match self {
535 Self::PostgreSQL => {
536 if index > 0 && index < POSTGRES_PLACEHOLDERS.len() {
538 Cow::Borrowed(POSTGRES_PLACEHOLDERS[index])
539 } else {
540 Cow::Owned(format!("${}", index))
542 }
543 }
544 Self::MySQL | Self::SQLite => Cow::Borrowed(QUESTION_MARK_PLACEHOLDER),
545 Self::MSSQL => Cow::Owned(format!("@P{}", index)),
546 }
547 }
548
549 #[inline]
554 pub fn placeholder_string(&self, index: usize) -> String {
555 self.placeholder(index).into_owned()
556 }
557}
558
559#[derive(Debug, Clone)]
561pub struct SqlBuilder {
562 db_type: DatabaseType,
563 parts: Vec<String>,
564 params: Vec<FilterValue>,
565}
566
567impl SqlBuilder {
568 pub fn new(db_type: DatabaseType) -> Self {
570 Self {
571 db_type,
572 parts: Vec::new(),
573 params: Vec::new(),
574 }
575 }
576
577 pub fn postgres() -> Self {
579 Self::new(DatabaseType::PostgreSQL)
580 }
581
582 pub fn mysql() -> Self {
584 Self::new(DatabaseType::MySQL)
585 }
586
587 pub fn sqlite() -> Self {
589 Self::new(DatabaseType::SQLite)
590 }
591
592 pub fn push(&mut self, sql: impl AsRef<str>) -> &mut Self {
594 self.parts.push(sql.as_ref().to_string());
595 self
596 }
597
598 pub fn push_param(&mut self, value: impl Into<FilterValue>) -> &mut Self {
600 let index = self.params.len() + 1;
601 self.parts
604 .push(self.db_type.placeholder(index).into_owned());
605 self.params.push(value.into());
606 self
607 }
608
609 pub fn push_identifier(&mut self, name: &str) -> &mut Self {
611 self.parts.push(quote_identifier(name));
612 self
613 }
614
615 pub fn push_sep(&mut self, sep: &str) -> &mut Self {
617 self.parts.push(sep.to_string());
618 self
619 }
620
621 pub fn build(self) -> (String, Vec<FilterValue>) {
623 (self.parts.join(""), self.params)
624 }
625
626 pub fn sql(&self) -> String {
628 self.parts.join("")
629 }
630
631 pub fn params(&self) -> &[FilterValue] {
633 &self.params
634 }
635
636 pub fn next_param_index(&self) -> usize {
638 self.params.len() + 1
639 }
640}
641
642impl Default for SqlBuilder {
643 fn default() -> Self {
644 Self::postgres()
645 }
646}
647
648#[derive(Debug, Clone, Copy)]
654pub enum QueryCapacity {
655 SimpleSelect,
657 SelectWithFilters(usize),
659 Insert(usize),
661 Update(usize),
663 Delete,
665 Custom(usize),
667}
668
669impl QueryCapacity {
670 #[inline]
672 pub const fn estimate(&self) -> usize {
673 match self {
674 Self::SimpleSelect => 64,
675 Self::SelectWithFilters(n) => 64 + *n * 32,
676 Self::Insert(cols) => 32 + *cols * 16,
677 Self::Update(cols) => 32 + *cols * 20,
678 Self::Delete => 48,
679 Self::Custom(cap) => *cap,
680 }
681 }
682}
683
684#[derive(Debug, Clone)]
721pub struct FastSqlBuilder {
722 buffer: String,
724 params: Vec<FilterValue>,
726 db_type: DatabaseType,
728}
729
730impl FastSqlBuilder {
731 #[inline]
733 pub fn new(db_type: DatabaseType) -> Self {
734 Self {
735 buffer: String::new(),
736 params: Vec::new(),
737 db_type,
738 }
739 }
740
741 #[inline]
743 pub fn with_capacity(db_type: DatabaseType, capacity: QueryCapacity) -> Self {
744 Self {
745 buffer: String::with_capacity(capacity.estimate()),
746 params: Vec::with_capacity(match capacity {
747 QueryCapacity::SimpleSelect => 2,
748 QueryCapacity::SelectWithFilters(n) => n,
749 QueryCapacity::Insert(n) => n,
750 QueryCapacity::Update(n) => n + 1,
751 QueryCapacity::Delete => 2,
752 QueryCapacity::Custom(n) => n / 16,
753 }),
754 db_type,
755 }
756 }
757
758 #[inline]
760 pub fn postgres(capacity: QueryCapacity) -> Self {
761 Self::with_capacity(DatabaseType::PostgreSQL, capacity)
762 }
763
764 #[inline]
766 pub fn mysql(capacity: QueryCapacity) -> Self {
767 Self::with_capacity(DatabaseType::MySQL, capacity)
768 }
769
770 #[inline]
772 pub fn sqlite(capacity: QueryCapacity) -> Self {
773 Self::with_capacity(DatabaseType::SQLite, capacity)
774 }
775
776 #[inline]
778 pub fn push_str(&mut self, s: &str) -> &mut Self {
779 self.buffer.push_str(s);
780 self
781 }
782
783 #[inline]
785 pub fn push_char(&mut self, c: char) -> &mut Self {
786 self.buffer.push(c);
787 self
788 }
789
790 #[inline]
792 pub fn bind(&mut self, value: impl Into<FilterValue>) -> &mut Self {
793 let index = self.params.len() + 1;
794 let placeholder = self.db_type.placeholder(index);
795 self.buffer.push_str(&placeholder);
796 self.params.push(value.into());
797 self
798 }
799
800 #[inline]
802 pub fn push_bind(&mut self, s: &str, value: impl Into<FilterValue>) -> &mut Self {
803 self.push_str(s);
804 self.bind(value)
805 }
806
807 pub fn bind_in_clause(&mut self, values: impl IntoIterator<Item = FilterValue>) -> &mut Self {
834 let values: Vec<FilterValue> = values.into_iter().collect();
835 if values.is_empty() {
836 return self;
837 }
838
839 let start_index = self.params.len() + 1;
840 let count = values.len();
841
842 match self.db_type {
844 DatabaseType::PostgreSQL => {
845 let estimated_len = count * 5;
847 self.buffer.reserve(estimated_len);
848
849 for (i, _) in values.iter().enumerate() {
850 if i > 0 {
851 self.buffer.push_str(", ");
852 }
853 let idx = start_index + i;
854 if idx < POSTGRES_PLACEHOLDERS.len() {
855 self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
856 } else {
857 let _ = write!(self.buffer, "${}", idx);
858 }
859 }
860 }
861 DatabaseType::MySQL | DatabaseType::SQLite => {
862 if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
864 self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
865 } else {
866 let estimated_len = count * 3; self.buffer.reserve(estimated_len);
869 for i in 0..count {
870 if i > 0 {
871 self.buffer.push_str(", ");
872 }
873 self.buffer.push('?');
874 }
875 }
876 }
877 DatabaseType::MSSQL => {
878 let estimated_len = count * 6; self.buffer.reserve(estimated_len);
881
882 for (i, _) in values.iter().enumerate() {
883 if i > 0 {
884 self.buffer.push_str(", ");
885 }
886 let idx = start_index + i;
887 let _ = write!(self.buffer, "@P{}", idx);
888 }
889 }
890 }
891
892 self.params.extend(values);
893 self
894 }
895
896 pub fn bind_in_slice<T: Into<FilterValue> + Clone>(&mut self, values: &[T]) -> &mut Self {
918 if values.is_empty() {
919 return self;
920 }
921
922 let start_index = self.params.len() + 1;
923 let count = values.len();
924
925 match self.db_type {
927 DatabaseType::PostgreSQL => {
928 let estimated_len = count * 5;
929 self.buffer.reserve(estimated_len);
930
931 for i in 0..count {
932 if i > 0 {
933 self.buffer.push_str(", ");
934 }
935 let idx = start_index + i;
936 if idx < POSTGRES_PLACEHOLDERS.len() {
937 self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
938 } else {
939 let _ = write!(self.buffer, "${}", idx);
940 }
941 }
942 }
943 DatabaseType::MySQL | DatabaseType::SQLite => {
944 if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
945 self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
946 } else {
947 let estimated_len = count * 3;
948 self.buffer.reserve(estimated_len);
949 for i in 0..count {
950 if i > 0 {
951 self.buffer.push_str(", ");
952 }
953 self.buffer.push('?');
954 }
955 }
956 }
957 DatabaseType::MSSQL => {
958 let estimated_len = count * 6;
959 self.buffer.reserve(estimated_len);
960
961 for i in 0..count {
962 if i > 0 {
963 self.buffer.push_str(", ");
964 }
965 let idx = start_index + i;
966 let _ = write!(self.buffer, "@P{}", idx);
967 }
968 }
969 }
970
971 self.params.reserve(count);
973 for v in values {
974 self.params.push(v.clone().into());
975 }
976 self
977 }
978
979 #[inline]
984 pub fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> &mut Self {
985 let _ = self.buffer.write_fmt(args);
986 self
987 }
988
989 #[inline]
991 pub fn push_identifier(&mut self, name: &str) -> &mut Self {
992 if needs_quoting(name) {
993 self.buffer.push('"');
994 for c in name.chars() {
996 if c == '"' {
997 self.buffer.push_str("\"\"");
998 } else {
999 self.buffer.push(c);
1000 }
1001 }
1002 self.buffer.push('"');
1003 } else {
1004 self.buffer.push_str(name);
1005 }
1006 self
1007 }
1008
1009 #[inline]
1011 pub fn push_if(&mut self, condition: bool, s: &str) -> &mut Self {
1012 if condition {
1013 self.push_str(s);
1014 }
1015 self
1016 }
1017
1018 #[inline]
1020 pub fn bind_if(&mut self, condition: bool, value: impl Into<FilterValue>) -> &mut Self {
1021 if condition {
1022 self.bind(value);
1023 }
1024 self
1025 }
1026
1027 #[inline]
1029 pub fn sql(&self) -> &str {
1030 &self.buffer
1031 }
1032
1033 #[inline]
1035 pub fn params(&self) -> &[FilterValue] {
1036 &self.params
1037 }
1038
1039 #[inline]
1041 pub fn param_count(&self) -> usize {
1042 self.params.len()
1043 }
1044
1045 #[inline]
1047 pub fn build(self) -> (String, Vec<FilterValue>) {
1048 let sql_len = self.buffer.len();
1049 let param_count = self.params.len();
1050 debug!(sql_len, param_count, db_type = ?self.db_type, "FastSqlBuilder::build()");
1051 (self.buffer, self.params)
1052 }
1053
1054 #[inline]
1056 pub fn build_sql(self) -> String {
1057 self.buffer
1058 }
1059}
1060
1061pub mod templates {
1069 use super::*;
1070
1071 pub fn select_by_id(table: &str, columns: &[&str]) -> String {
1084 let cols = if columns.is_empty() {
1085 "*".to_string()
1086 } else {
1087 columns.join(", ")
1088 };
1089 format!("SELECT {} FROM {} WHERE id = $1", cols, table)
1090 }
1091
1092 pub fn insert_returning(table: &str, columns: &[&str]) -> String {
1104 let cols = columns.join(", ");
1105 let placeholders: Vec<String> = (1..=columns.len())
1106 .map(|i| {
1107 if i < POSTGRES_PLACEHOLDERS.len() {
1108 POSTGRES_PLACEHOLDERS[i].to_string()
1109 } else {
1110 format!("${}", i)
1111 }
1112 })
1113 .collect();
1114 format!(
1115 "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
1116 table,
1117 cols,
1118 placeholders.join(", ")
1119 )
1120 }
1121
1122 pub fn update_by_id(table: &str, columns: &[&str]) -> String {
1134 let sets: Vec<String> = columns
1135 .iter()
1136 .enumerate()
1137 .map(|(i, col)| {
1138 let idx = i + 1;
1139 if idx < POSTGRES_PLACEHOLDERS.len() {
1140 format!("{} = {}", col, POSTGRES_PLACEHOLDERS[idx])
1141 } else {
1142 format!("{} = ${}", col, idx)
1143 }
1144 })
1145 .collect();
1146 let id_idx = columns.len() + 1;
1147 let id_placeholder: Cow<'_, str> = if id_idx < POSTGRES_PLACEHOLDERS.len() {
1148 Cow::Borrowed(POSTGRES_PLACEHOLDERS[id_idx])
1149 } else {
1150 Cow::Owned(format!("${}", id_idx))
1151 };
1152 format!(
1153 "UPDATE {} SET {} WHERE id = {}",
1154 table,
1155 sets.join(", "),
1156 id_placeholder
1157 )
1158 }
1159
1160 pub fn delete_by_id(table: &str) -> String {
1171 format!("DELETE FROM {} WHERE id = $1", table)
1172 }
1173
1174 pub fn batch_placeholders(db_type: DatabaseType, columns: usize, rows: usize) -> String {
1186 let mut result = String::with_capacity(rows * columns * 4);
1187 let mut param_idx = 1;
1188
1189 for row in 0..rows {
1190 if row > 0 {
1191 result.push_str(", ");
1192 }
1193 result.push('(');
1194 for col in 0..columns {
1195 if col > 0 {
1196 result.push_str(", ");
1197 }
1198 match db_type {
1199 DatabaseType::PostgreSQL => {
1200 if param_idx < POSTGRES_PLACEHOLDERS.len() {
1201 result.push_str(POSTGRES_PLACEHOLDERS[param_idx]);
1202 } else {
1203 let _ = write!(result, "${}", param_idx);
1204 }
1205 param_idx += 1;
1206 }
1207 DatabaseType::MySQL | DatabaseType::SQLite => {
1208 result.push('?');
1209 }
1210 DatabaseType::MSSQL => {
1211 let _ = write!(result, "@P{}", param_idx);
1212 param_idx += 1;
1213 }
1214 }
1215 }
1216 result.push(')');
1217 }
1218
1219 result
1220 }
1221}
1222
1223pub struct LazySql<F>
1247where
1248 F: Fn(DatabaseType) -> String,
1249{
1250 generator: F,
1251}
1252
1253impl<F> LazySql<F>
1254where
1255 F: Fn(DatabaseType) -> String,
1256{
1257 #[inline]
1259 pub const fn new(generator: F) -> Self {
1260 Self { generator }
1261 }
1262
1263 #[inline]
1265 pub fn get(&self, db_type: DatabaseType) -> String {
1266 (self.generator)(db_type)
1267 }
1268}
1269
1270pub struct CachedSql<F>
1293where
1294 F: Fn(DatabaseType) -> String,
1295{
1296 generator: F,
1297 postgres: OnceLock<String>,
1298 mysql: OnceLock<String>,
1299 sqlite: OnceLock<String>,
1300 mssql: OnceLock<String>,
1301}
1302
1303impl<F> CachedSql<F>
1304where
1305 F: Fn(DatabaseType) -> String,
1306{
1307 pub const fn new(generator: F) -> Self {
1309 Self {
1310 generator,
1311 postgres: OnceLock::new(),
1312 mysql: OnceLock::new(),
1313 sqlite: OnceLock::new(),
1314 mssql: OnceLock::new(),
1315 }
1316 }
1317
1318 pub fn get(&self, db_type: DatabaseType) -> &str {
1323 match db_type {
1324 DatabaseType::PostgreSQL => self.postgres.get_or_init(|| (self.generator)(db_type)),
1325 DatabaseType::MySQL => self.mysql.get_or_init(|| (self.generator)(db_type)),
1326 DatabaseType::SQLite => self.sqlite.get_or_init(|| (self.generator)(db_type)),
1327 DatabaseType::MSSQL => self.mssql.get_or_init(|| (self.generator)(db_type)),
1328 }
1329 }
1330}
1331
1332pub struct SqlTemplateCache {
1361 cache: RwLock<HashMap<(String, DatabaseType), Arc<String>>>,
1362}
1363
1364impl Default for SqlTemplateCache {
1365 fn default() -> Self {
1366 Self::new()
1367 }
1368}
1369
1370impl SqlTemplateCache {
1371 pub fn new() -> Self {
1373 Self {
1374 cache: RwLock::new(HashMap::new()),
1375 }
1376 }
1377
1378 pub fn with_capacity(capacity: usize) -> Self {
1380 Self {
1381 cache: RwLock::new(HashMap::with_capacity(capacity)),
1382 }
1383 }
1384
1385 pub fn get_or_insert<F>(&self, key: &str, db_type: DatabaseType, generator: F) -> Arc<String>
1390 where
1391 F: FnOnce() -> String,
1392 {
1393 let cache_key = (key.to_string(), db_type);
1394
1395 {
1397 let cache = self.cache.read().unwrap();
1398 if let Some(sql) = cache.get(&cache_key) {
1399 return Arc::clone(sql);
1400 }
1401 }
1402
1403 let mut cache = self.cache.write().unwrap();
1405
1406 if let Some(sql) = cache.get(&cache_key) {
1408 return Arc::clone(sql);
1409 }
1410
1411 let sql = Arc::new(generator());
1412 cache.insert(cache_key, Arc::clone(&sql));
1413 sql
1414 }
1415
1416 pub fn contains(&self, key: &str, db_type: DatabaseType) -> bool {
1418 let cache_key = (key.to_string(), db_type);
1419 self.cache.read().unwrap().contains_key(&cache_key)
1420 }
1421
1422 pub fn clear(&self) {
1424 self.cache.write().unwrap().clear();
1425 }
1426
1427 pub fn len(&self) -> usize {
1429 self.cache.read().unwrap().len()
1430 }
1431
1432 pub fn is_empty(&self) -> bool {
1434 self.cache.read().unwrap().is_empty()
1435 }
1436}
1437
1438pub fn global_sql_cache() -> &'static SqlTemplateCache {
1453 static CACHE: OnceLock<SqlTemplateCache> = OnceLock::new();
1454 CACHE.get_or_init(|| SqlTemplateCache::with_capacity(64))
1455}
1456
1457#[derive(Debug, Clone, Copy)]
1463pub enum AdvancedQueryCapacity {
1464 Cte {
1466 cte_count: usize,
1468 avg_query_len: usize,
1470 },
1471 WindowFunction {
1473 window_count: usize,
1475 partition_cols: usize,
1477 order_cols: usize,
1479 },
1480 FullTextSearch {
1482 columns: usize,
1484 query_len: usize,
1486 },
1487 JsonPath {
1489 depth: usize,
1491 },
1492 Upsert {
1494 columns: usize,
1496 conflict_cols: usize,
1498 update_cols: usize,
1500 },
1501 ProcedureCall {
1503 params: usize,
1505 },
1506 TriggerDef {
1508 events: usize,
1510 body_len: usize,
1512 },
1513 RlsPolicy {
1515 expr_len: usize,
1517 },
1518}
1519
1520impl AdvancedQueryCapacity {
1521 #[inline]
1523 pub const fn estimate(&self) -> usize {
1524 match self {
1525 Self::Cte {
1526 cte_count,
1527 avg_query_len,
1528 } => {
1529 16 + *cte_count * (32 + *avg_query_len)
1531 }
1532 Self::WindowFunction {
1533 window_count,
1534 partition_cols,
1535 order_cols,
1536 } => {
1537 *window_count * (48 + *partition_cols * 16 + *order_cols * 20)
1539 }
1540 Self::FullTextSearch { columns, query_len } => {
1541 64 + *columns * 20 + *query_len
1543 }
1544 Self::JsonPath { depth } => {
1545 16 + *depth * 12
1547 }
1548 Self::Upsert {
1549 columns,
1550 conflict_cols,
1551 update_cols,
1552 } => {
1553 64 + *columns * 8 + *conflict_cols * 12 + *update_cols * 16
1555 }
1556 Self::ProcedureCall { params } => {
1557 32 + *params * 8
1559 }
1560 Self::TriggerDef { events, body_len } => {
1561 96 + *events * 12 + *body_len
1563 }
1564 Self::RlsPolicy { expr_len } => {
1565 64 + *expr_len
1567 }
1568 }
1569 }
1570
1571 #[inline]
1573 pub const fn to_query_capacity(&self) -> QueryCapacity {
1574 QueryCapacity::Custom(self.estimate())
1575 }
1576}
1577
1578impl FastSqlBuilder {
1580 #[inline]
1582 pub fn for_advanced(db_type: DatabaseType, capacity: AdvancedQueryCapacity) -> Self {
1583 Self::with_capacity(db_type, capacity.to_query_capacity())
1584 }
1585
1586 #[inline]
1588 pub fn for_cte(db_type: DatabaseType, cte_count: usize, avg_query_len: usize) -> Self {
1589 Self::for_advanced(
1590 db_type,
1591 AdvancedQueryCapacity::Cte {
1592 cte_count,
1593 avg_query_len,
1594 },
1595 )
1596 }
1597
1598 #[inline]
1600 pub fn for_window(
1601 db_type: DatabaseType,
1602 window_count: usize,
1603 partition_cols: usize,
1604 order_cols: usize,
1605 ) -> Self {
1606 Self::for_advanced(
1607 db_type,
1608 AdvancedQueryCapacity::WindowFunction {
1609 window_count,
1610 partition_cols,
1611 order_cols,
1612 },
1613 )
1614 }
1615
1616 #[inline]
1618 pub fn for_upsert(
1619 db_type: DatabaseType,
1620 columns: usize,
1621 conflict_cols: usize,
1622 update_cols: usize,
1623 ) -> Self {
1624 Self::for_advanced(
1625 db_type,
1626 AdvancedQueryCapacity::Upsert {
1627 columns,
1628 conflict_cols,
1629 update_cols,
1630 },
1631 )
1632 }
1633}
1634
1635pub mod parse {
1652 pub fn extract_where_body(sql: &str) -> Option<String> {
1656 let lower = sql.to_ascii_lowercase();
1657 let i = lower.find(" where ")?;
1658 Some(sql[i + " where ".len()..].trim().to_string())
1659 }
1660
1661 pub fn extract_insert_columns(sql: &str) -> Option<Vec<String>> {
1665 let open = sql.find('(')?;
1666 let close = sql[open..].find(')').map(|i| open + i)?;
1667 let body = &sql[open + 1..close];
1668 Some(
1669 body.split(',')
1670 .map(|c| c.trim().to_string())
1671 .filter(|c| !c.is_empty())
1672 .collect(),
1673 )
1674 }
1675
1676 pub fn count_set_placeholders(sql: &str) -> Option<usize> {
1682 let lower = sql.to_ascii_lowercase();
1683 let set_start = lower.find(" set ")?;
1684 let where_start = lower[set_start..]
1685 .find(" where ")
1686 .map(|i| set_start + i)
1687 .unwrap_or(sql.len());
1688 Some(sql[set_start..where_start].matches('?').count())
1689 }
1690
1691 #[cfg(test)]
1692 mod tests {
1693 use super::*;
1694
1695 #[test]
1696 fn extract_where_body_finds_lowercase_tail() {
1697 assert_eq!(
1698 extract_where_body("UPDATE t SET a = 1 WHERE id = 42"),
1699 Some("id = 42".to_string())
1700 );
1701 }
1702
1703 #[test]
1704 fn extract_where_body_case_insensitive() {
1705 assert_eq!(
1706 extract_where_body("update t set a = 1 where id = 42"),
1707 Some("id = 42".to_string())
1708 );
1709 }
1710
1711 #[test]
1712 fn extract_where_body_missing_returns_none() {
1713 assert_eq!(extract_where_body("SELECT * FROM t"), None);
1714 }
1715
1716 #[test]
1717 fn extract_insert_columns_parses_list() {
1718 assert_eq!(
1719 extract_insert_columns("INSERT INTO users (id, email, name) VALUES ($1, $2, $3)"),
1720 Some(vec!["id".into(), "email".into(), "name".into()])
1721 );
1722 }
1723
1724 #[test]
1725 fn count_set_placeholders_counts_between_set_and_where() {
1726 assert_eq!(
1727 count_set_placeholders("UPDATE t SET a = ?, b = ? WHERE id = ?"),
1728 Some(2)
1729 );
1730 }
1731
1732 #[test]
1733 fn count_set_placeholders_without_where_counts_to_end() {
1734 assert_eq!(count_set_placeholders("UPDATE t SET a = ?, b = ?"), Some(2));
1735 }
1736 }
1737}
1738
1739#[cfg(test)]
1740mod tests {
1741 use super::*;
1742
1743 #[test]
1744 fn test_escape_identifier() {
1745 assert_eq!(escape_identifier("user"), "\"user\"");
1746 assert_eq!(escape_identifier("my_table"), "\"my_table\"");
1747 assert_eq!(escape_identifier("has\"quote"), "\"has\"\"quote\"");
1748 }
1749
1750 #[test]
1751 fn test_is_valid_sql_identifier() {
1752 for valid in ["users", "_private", "ABC", "a", "sp_1", "a_b_C_1_2_3"] {
1753 assert!(is_valid_sql_identifier(valid), "expected valid: {valid:?}");
1754 }
1755 for invalid in [
1756 "",
1757 "1abc",
1758 "has space",
1759 "dash-name",
1760 "semi;colon",
1761 "quote'name",
1762 "dot.name",
1763 "\"quoted\"",
1764 ] {
1765 assert!(
1766 !is_valid_sql_identifier(invalid),
1767 "expected invalid: {invalid:?}"
1768 );
1769 }
1770 }
1771
1772 #[test]
1773 fn test_escape_literal() {
1774 assert_eq!(escape_literal("plain"), "plain");
1776 assert_eq!(escape_literal("o'brien"), "o''brien");
1777 assert_eq!(escape_literal("''"), "''''");
1778 }
1779
1780 #[test]
1781 fn test_needs_quoting() {
1782 assert!(needs_quoting("user"));
1783 assert!(needs_quoting("order"));
1784 assert!(needs_quoting("has space"));
1785 assert!(!needs_quoting("my_table"));
1786 assert!(!needs_quoting("users"));
1787 }
1788
1789 #[test]
1790 fn test_quote_identifier() {
1791 assert_eq!(quote_identifier("user"), "\"user\"");
1792 assert_eq!(quote_identifier("my_table"), "my_table");
1793 }
1794
1795 #[test]
1796 fn test_database_placeholder() {
1797 assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
1799 assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
1800 assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
1801 assert_eq!(DatabaseType::PostgreSQL.placeholder(128).as_ref(), "$128");
1802 assert_eq!(DatabaseType::PostgreSQL.placeholder(256).as_ref(), "$256");
1803 assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
1804 assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
1805
1806 assert!(matches!(
1808 DatabaseType::MySQL.placeholder(1),
1809 Cow::Borrowed(_)
1810 ));
1811 assert!(matches!(
1812 DatabaseType::SQLite.placeholder(1),
1813 Cow::Borrowed(_)
1814 ));
1815
1816 assert!(matches!(
1818 DatabaseType::PostgreSQL.placeholder(1),
1819 Cow::Borrowed(_)
1820 ));
1821 assert!(matches!(
1822 DatabaseType::PostgreSQL.placeholder(50),
1823 Cow::Borrowed(_)
1824 ));
1825 assert!(matches!(
1826 DatabaseType::PostgreSQL.placeholder(128),
1827 Cow::Borrowed(_)
1828 ));
1829 assert!(matches!(
1830 DatabaseType::PostgreSQL.placeholder(256),
1831 Cow::Borrowed(_)
1832 ));
1833
1834 assert!(matches!(
1836 DatabaseType::PostgreSQL.placeholder(257),
1837 Cow::Owned(_)
1838 ));
1839 assert_eq!(DatabaseType::PostgreSQL.placeholder(257).as_ref(), "$257");
1840 assert_eq!(DatabaseType::PostgreSQL.placeholder(200).as_ref(), "$200");
1841
1842 assert!(matches!(
1844 DatabaseType::PostgreSQL.placeholder(0),
1845 Cow::Owned(_)
1846 ));
1847 assert_eq!(DatabaseType::PostgreSQL.placeholder(0).as_ref(), "$0");
1848 }
1849
1850 #[test]
1851 fn test_sql_builder() {
1852 let mut builder = SqlBuilder::postgres();
1853 builder
1854 .push("SELECT * FROM ")
1855 .push_identifier("user")
1856 .push(" WHERE ")
1857 .push_identifier("id")
1858 .push(" = ")
1859 .push_param(42i32);
1860
1861 let (sql, params) = builder.build();
1862 assert_eq!(sql, "SELECT * FROM \"user\" WHERE id = $1");
1863 assert_eq!(params.len(), 1);
1864 }
1865
1866 #[test]
1868 fn test_fast_builder_simple() {
1869 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1870 builder.push_str("SELECT * FROM users WHERE id = ");
1871 builder.bind(42i64);
1872 let (sql, params) = builder.build();
1873 assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1874 assert_eq!(params.len(), 1);
1875 }
1876
1877 #[test]
1878 fn test_fast_builder_complex() {
1879 let mut builder = FastSqlBuilder::with_capacity(
1880 DatabaseType::PostgreSQL,
1881 QueryCapacity::SelectWithFilters(5),
1882 );
1883 builder
1884 .push_str("SELECT * FROM users WHERE active = ")
1885 .bind(true)
1886 .push_str(" AND age > ")
1887 .bind(18i64)
1888 .push_str(" AND status = ")
1889 .bind("approved")
1890 .push_str(" ORDER BY created_at LIMIT ")
1891 .bind(10i64);
1892
1893 let (sql, params) = builder.build();
1894 assert!(sql.contains("$1"));
1895 assert!(sql.contains("$4"));
1896 assert_eq!(params.len(), 4);
1897 }
1898
1899 #[test]
1900 fn test_fast_builder_in_clause_postgres() {
1901 let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
1902 builder.push_str("SELECT * FROM users WHERE id IN (");
1903 let values: Vec<FilterValue> = (1..=5).map(FilterValue::Int).collect();
1904 builder.bind_in_clause(values);
1905 builder.push_char(')');
1906
1907 let (sql, params) = builder.build();
1908 assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
1909 assert_eq!(params.len(), 5);
1910 }
1911
1912 #[test]
1913 fn test_fast_builder_in_clause_mysql() {
1914 let mut builder = FastSqlBuilder::mysql(QueryCapacity::Custom(128));
1915 builder.push_str("SELECT * FROM users WHERE id IN (");
1916 let values: Vec<FilterValue> = (1..=5).map(FilterValue::Int).collect();
1917 builder.bind_in_clause(values);
1918 builder.push_char(')');
1919
1920 let (sql, params) = builder.build();
1921 assert_eq!(sql, "SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?)");
1922 assert_eq!(params.len(), 5);
1923 }
1924
1925 #[test]
1926 fn test_fast_builder_identifier() {
1927 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1928 builder.push_str("SELECT * FROM ");
1929 builder.push_identifier("user"); builder.push_str(" WHERE ");
1931 builder.push_identifier("my_column"); builder.push_str(" = ");
1933 builder.bind(1i64);
1934
1935 let (sql, _) = builder.build();
1936 assert_eq!(sql, "SELECT * FROM \"user\" WHERE my_column = $1");
1937 }
1938
1939 #[test]
1940 fn test_fast_builder_identifier_with_quotes() {
1941 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1942 builder.push_str("SELECT * FROM ");
1943 builder.push_identifier("has\"quote");
1944
1945 let sql = builder.build_sql();
1946 assert_eq!(sql, "SELECT * FROM \"has\"\"quote\"");
1947 }
1948
1949 #[test]
1950 fn test_fast_builder_conditional() {
1951 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SelectWithFilters(2));
1952 builder.push_str("SELECT * FROM users WHERE 1=1");
1953 builder.push_if(true, " AND active = true");
1954 builder.push_if(false, " AND deleted = false");
1955
1956 let sql = builder.build_sql();
1957 assert_eq!(sql, "SELECT * FROM users WHERE 1=1 AND active = true");
1958 }
1959
1960 #[test]
1962 fn test_template_select_by_id() {
1963 let sql = templates::select_by_id("users", &["id", "name", "email"]);
1964 assert_eq!(sql, "SELECT id, name, email FROM users WHERE id = $1");
1965 }
1966
1967 #[test]
1968 fn test_template_select_by_id_all_columns() {
1969 let sql = templates::select_by_id("users", &[]);
1970 assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1971 }
1972
1973 #[test]
1974 fn test_template_insert_returning() {
1975 let sql = templates::insert_returning("users", &["name", "email"]);
1976 assert_eq!(
1977 sql,
1978 "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
1979 );
1980 }
1981
1982 #[test]
1983 fn test_template_update_by_id() {
1984 let sql = templates::update_by_id("users", &["name", "email"]);
1985 assert_eq!(sql, "UPDATE users SET name = $1, email = $2 WHERE id = $3");
1986 }
1987
1988 #[test]
1989 fn test_template_update_by_id_wide_table() {
1990 let cols: Vec<String> = (0..256).map(|i| format!("c{}", i)).collect();
1993 let col_refs: Vec<&str> = cols.iter().map(String::as_str).collect();
1994 let sql = templates::update_by_id("wide_table", &col_refs);
1995 assert!(sql.contains("WHERE id = $257"));
1996 assert!(!sql.contains("$?"));
1997 }
1998
1999 #[test]
2000 fn test_template_delete_by_id() {
2001 let sql = templates::delete_by_id("users");
2002 assert_eq!(sql, "DELETE FROM users WHERE id = $1");
2003 }
2004
2005 #[test]
2006 fn test_template_batch_placeholders_postgres() {
2007 let sql = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
2008 assert_eq!(sql, "($1, $2, $3), ($4, $5, $6)");
2009 }
2010
2011 #[test]
2012 fn test_template_batch_placeholders_mysql() {
2013 let sql = templates::batch_placeholders(DatabaseType::MySQL, 3, 2);
2014 assert_eq!(sql, "(?, ?, ?), (?, ?, ?)");
2015 }
2016
2017 #[test]
2018 fn test_query_capacity_estimates() {
2019 assert_eq!(QueryCapacity::SimpleSelect.estimate(), 64);
2020 assert_eq!(QueryCapacity::SelectWithFilters(5).estimate(), 64 + 5 * 32);
2021 assert_eq!(QueryCapacity::Insert(10).estimate(), 32 + 10 * 16);
2022 assert_eq!(QueryCapacity::Update(5).estimate(), 32 + 5 * 20);
2023 assert_eq!(QueryCapacity::Delete.estimate(), 48);
2024 assert_eq!(QueryCapacity::Custom(256).estimate(), 256);
2025 }
2026}