1use crate::filter::FilterValue;
10use std::borrow::Cow;
11use std::fmt::Write;
12use tracing::debug;
13
14pub fn escape_identifier(name: &str) -> String {
16 let escaped = name.replace('"', "\"\"");
18 format!("\"{}\"", escaped)
19}
20
21pub fn needs_quoting(name: &str) -> bool {
23 let reserved = [
25 "user",
26 "order",
27 "group",
28 "select",
29 "from",
30 "where",
31 "table",
32 "index",
33 "key",
34 "primary",
35 "foreign",
36 "check",
37 "default",
38 "null",
39 "not",
40 "and",
41 "or",
42 "in",
43 "is",
44 "like",
45 "between",
46 "case",
47 "when",
48 "then",
49 "else",
50 "end",
51 "as",
52 "on",
53 "join",
54 "left",
55 "right",
56 "inner",
57 "outer",
58 "cross",
59 "natural",
60 "using",
61 "limit",
62 "offset",
63 "union",
64 "intersect",
65 "except",
66 "all",
67 "distinct",
68 "having",
69 "create",
70 "alter",
71 "drop",
72 "insert",
73 "update",
74 "delete",
75 "into",
76 "values",
77 "set",
78 "returning",
79 ];
80
81 if reserved.contains(&name.to_lowercase().as_str()) {
83 return true;
84 }
85
86 !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
88}
89
90pub fn quote_identifier(name: &str) -> String {
92 if needs_quoting(name) {
93 escape_identifier(name)
94 } else {
95 name.to_string()
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[derive(Default)]
102pub enum DatabaseType {
103 #[default]
105 PostgreSQL,
106 MySQL,
108 SQLite,
110}
111
112const QUESTION_MARK_PLACEHOLDER: &str = "?";
114
115pub const POSTGRES_PLACEHOLDERS: &[&str] = &[
128 "$0", "$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$10", "$11", "$12", "$13", "$14",
129 "$15", "$16", "$17", "$18", "$19", "$20", "$21", "$22", "$23", "$24", "$25", "$26", "$27",
130 "$28", "$29", "$30", "$31", "$32", "$33", "$34", "$35", "$36", "$37", "$38", "$39", "$40",
131 "$41", "$42", "$43", "$44", "$45", "$46", "$47", "$48", "$49", "$50", "$51", "$52", "$53",
132 "$54", "$55", "$56", "$57", "$58", "$59", "$60", "$61", "$62", "$63", "$64", "$65", "$66",
133 "$67", "$68", "$69", "$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77", "$78", "$79",
134 "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", "$88", "$89", "$90", "$91", "$92",
135 "$93", "$94", "$95", "$96", "$97", "$98", "$99", "$100", "$101", "$102", "$103", "$104",
136 "$105", "$106", "$107", "$108", "$109", "$110", "$111", "$112", "$113", "$114", "$115", "$116",
137 "$117", "$118", "$119", "$120", "$121", "$122", "$123", "$124", "$125", "$126", "$127", "$128",
138 "$129", "$130", "$131", "$132", "$133", "$134", "$135", "$136", "$137", "$138", "$139", "$140",
139 "$141", "$142", "$143", "$144", "$145", "$146", "$147", "$148", "$149", "$150", "$151", "$152",
140 "$153", "$154", "$155", "$156", "$157", "$158", "$159", "$160", "$161", "$162", "$163", "$164",
141 "$165", "$166", "$167", "$168", "$169", "$170", "$171", "$172", "$173", "$174", "$175", "$176",
142 "$177", "$178", "$179", "$180", "$181", "$182", "$183", "$184", "$185", "$186", "$187", "$188",
143 "$189", "$190", "$191", "$192", "$193", "$194", "$195", "$196", "$197", "$198", "$199", "$200",
144 "$201", "$202", "$203", "$204", "$205", "$206", "$207", "$208", "$209", "$210", "$211", "$212",
145 "$213", "$214", "$215", "$216", "$217", "$218", "$219", "$220", "$221", "$222", "$223", "$224",
146 "$225", "$226", "$227", "$228", "$229", "$230", "$231", "$232", "$233", "$234", "$235", "$236",
147 "$237", "$238", "$239", "$240", "$241", "$242", "$243", "$244", "$245", "$246", "$247", "$248",
148 "$249", "$250", "$251", "$252", "$253", "$254", "$255", "$256",
149];
150
151pub const MYSQL_IN_PATTERNS: &[&str] = &[
154 "", "?",
156 "?, ?",
157 "?, ?, ?",
158 "?, ?, ?, ?",
159 "?, ?, ?, ?, ?",
160 "?, ?, ?, ?, ?, ?",
161 "?, ?, ?, ?, ?, ?, ?",
162 "?, ?, ?, ?, ?, ?, ?, ?",
163 "?, ?, ?, ?, ?, ?, ?, ?, ?",
164 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
166 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
167 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
168 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
169 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
170 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
172 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
173 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
174 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
176 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
177 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
178 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
179 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
181 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
182 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
183 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
184 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?",
186 "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?", ];
188
189#[inline]
199pub fn postgres_in_pattern(start_idx: usize, count: usize) -> String {
200 if start_idx == 1 && count <= 10 {
202 static POSTGRES_IN_1: &[&str] = &[
203 "",
204 "$1",
205 "$1, $2",
206 "$1, $2, $3",
207 "$1, $2, $3, $4",
208 "$1, $2, $3, $4, $5",
209 "$1, $2, $3, $4, $5, $6",
210 "$1, $2, $3, $4, $5, $6, $7",
211 "$1, $2, $3, $4, $5, $6, $7, $8",
212 "$1, $2, $3, $4, $5, $6, $7, $8, $9",
213 "$1, $2, $3, $4, $5, $6, $7, $8, $9, $10",
214 ];
215 return POSTGRES_IN_1[count].to_string();
216 }
217
218 let mut result = String::with_capacity(count * 5);
220 for i in 0..count {
221 if i > 0 {
222 result.push_str(", ");
223 }
224 let idx = start_idx + i;
225 if idx < POSTGRES_PLACEHOLDERS.len() {
226 result.push_str(POSTGRES_PLACEHOLDERS[idx]);
227 } else {
228 use std::fmt::Write;
229 let _ = write!(result, "${}", idx);
230 }
231 }
232 result
233}
234
235const POSTGRES_IN_FROM_1: &[&str] = &[
238 "", "$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", ];
272
273#[inline]
280#[allow(clippy::needless_range_loop)]
281pub fn write_postgres_in_pattern(buf: &mut String, start_idx: usize, count: usize) {
282 if count == 0 {
283 return;
284 }
285
286 if start_idx == 1 && count < POSTGRES_IN_FROM_1.len() {
288 buf.push_str(POSTGRES_IN_FROM_1[count]);
289 return;
290 }
291
292 buf.reserve(count * 6);
295
296 let end_idx = start_idx + count;
298 let table_len = POSTGRES_PLACEHOLDERS.len();
299
300 if end_idx <= table_len {
301 buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
303 for idx in (start_idx + 1)..end_idx {
304 buf.push_str(", ");
305 buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
306 }
307 } else if start_idx >= table_len {
308 let _ = write!(buf, "${}", start_idx);
310 for idx in (start_idx + 1)..end_idx {
311 let _ = write!(buf, ", ${}", idx);
312 }
313 } else {
314 buf.push_str(POSTGRES_PLACEHOLDERS[start_idx]);
316 for idx in (start_idx + 1)..table_len.min(end_idx) {
317 buf.push_str(", ");
318 buf.push_str(POSTGRES_PLACEHOLDERS[idx]);
319 }
320 for idx in table_len..end_idx {
321 let _ = write!(buf, ", ${}", idx);
322 }
323 }
324}
325
326impl DatabaseType {
327 #[inline]
348 pub fn placeholder(&self, index: usize) -> Cow<'static, str> {
349 match self {
350 Self::PostgreSQL => {
351 if index > 0 && index < POSTGRES_PLACEHOLDERS.len() {
353 Cow::Borrowed(POSTGRES_PLACEHOLDERS[index])
354 } else {
355 Cow::Owned(format!("${}", index))
357 }
358 }
359 Self::MySQL | Self::SQLite => Cow::Borrowed(QUESTION_MARK_PLACEHOLDER),
360 }
361 }
362
363 #[inline]
368 pub fn placeholder_string(&self, index: usize) -> String {
369 self.placeholder(index).into_owned()
370 }
371}
372
373
374#[derive(Debug, Clone)]
376pub struct SqlBuilder {
377 db_type: DatabaseType,
378 parts: Vec<String>,
379 params: Vec<FilterValue>,
380}
381
382impl SqlBuilder {
383 pub fn new(db_type: DatabaseType) -> Self {
385 Self {
386 db_type,
387 parts: Vec::new(),
388 params: Vec::new(),
389 }
390 }
391
392 pub fn postgres() -> Self {
394 Self::new(DatabaseType::PostgreSQL)
395 }
396
397 pub fn mysql() -> Self {
399 Self::new(DatabaseType::MySQL)
400 }
401
402 pub fn sqlite() -> Self {
404 Self::new(DatabaseType::SQLite)
405 }
406
407 pub fn push(&mut self, sql: impl AsRef<str>) -> &mut Self {
409 self.parts.push(sql.as_ref().to_string());
410 self
411 }
412
413 pub fn push_param(&mut self, value: impl Into<FilterValue>) -> &mut Self {
415 let index = self.params.len() + 1;
416 self.parts
419 .push(self.db_type.placeholder(index).into_owned());
420 self.params.push(value.into());
421 self
422 }
423
424 pub fn push_identifier(&mut self, name: &str) -> &mut Self {
426 self.parts.push(quote_identifier(name));
427 self
428 }
429
430 pub fn push_sep(&mut self, sep: &str) -> &mut Self {
432 self.parts.push(sep.to_string());
433 self
434 }
435
436 pub fn build(self) -> (String, Vec<FilterValue>) {
438 (self.parts.join(""), self.params)
439 }
440
441 pub fn sql(&self) -> String {
443 self.parts.join("")
444 }
445
446 pub fn params(&self) -> &[FilterValue] {
448 &self.params
449 }
450
451 pub fn next_param_index(&self) -> usize {
453 self.params.len() + 1
454 }
455}
456
457impl Default for SqlBuilder {
458 fn default() -> Self {
459 Self::postgres()
460 }
461}
462
463#[derive(Debug, Clone, Copy)]
469pub enum QueryCapacity {
470 SimpleSelect,
472 SelectWithFilters(usize),
474 Insert(usize),
476 Update(usize),
478 Delete,
480 Custom(usize),
482}
483
484impl QueryCapacity {
485 #[inline]
487 pub const fn estimate(&self) -> usize {
488 match self {
489 Self::SimpleSelect => 64,
490 Self::SelectWithFilters(n) => 64 + *n * 32,
491 Self::Insert(cols) => 32 + *cols * 16,
492 Self::Update(cols) => 32 + *cols * 20,
493 Self::Delete => 48,
494 Self::Custom(cap) => *cap,
495 }
496 }
497}
498
499#[derive(Debug, Clone)]
536pub struct FastSqlBuilder {
537 buffer: String,
539 params: Vec<FilterValue>,
541 db_type: DatabaseType,
543}
544
545impl FastSqlBuilder {
546 #[inline]
548 pub fn new(db_type: DatabaseType) -> Self {
549 Self {
550 buffer: String::new(),
551 params: Vec::new(),
552 db_type,
553 }
554 }
555
556 #[inline]
558 pub fn with_capacity(db_type: DatabaseType, capacity: QueryCapacity) -> Self {
559 Self {
560 buffer: String::with_capacity(capacity.estimate()),
561 params: Vec::with_capacity(match capacity {
562 QueryCapacity::SimpleSelect => 2,
563 QueryCapacity::SelectWithFilters(n) => n,
564 QueryCapacity::Insert(n) => n,
565 QueryCapacity::Update(n) => n + 1,
566 QueryCapacity::Delete => 2,
567 QueryCapacity::Custom(n) => n / 16,
568 }),
569 db_type,
570 }
571 }
572
573 #[inline]
575 pub fn postgres(capacity: QueryCapacity) -> Self {
576 Self::with_capacity(DatabaseType::PostgreSQL, capacity)
577 }
578
579 #[inline]
581 pub fn mysql(capacity: QueryCapacity) -> Self {
582 Self::with_capacity(DatabaseType::MySQL, capacity)
583 }
584
585 #[inline]
587 pub fn sqlite(capacity: QueryCapacity) -> Self {
588 Self::with_capacity(DatabaseType::SQLite, capacity)
589 }
590
591 #[inline]
593 pub fn push_str(&mut self, s: &str) -> &mut Self {
594 self.buffer.push_str(s);
595 self
596 }
597
598 #[inline]
600 pub fn push_char(&mut self, c: char) -> &mut Self {
601 self.buffer.push(c);
602 self
603 }
604
605 #[inline]
607 pub fn bind(&mut self, value: impl Into<FilterValue>) -> &mut Self {
608 let index = self.params.len() + 1;
609 let placeholder = self.db_type.placeholder(index);
610 self.buffer.push_str(&placeholder);
611 self.params.push(value.into());
612 self
613 }
614
615 #[inline]
617 pub fn push_bind(&mut self, s: &str, value: impl Into<FilterValue>) -> &mut Self {
618 self.push_str(s);
619 self.bind(value)
620 }
621
622 pub fn bind_in_clause(&mut self, values: impl IntoIterator<Item = FilterValue>) -> &mut Self {
649 let values: Vec<FilterValue> = values.into_iter().collect();
650 if values.is_empty() {
651 return self;
652 }
653
654 let start_index = self.params.len() + 1;
655 let count = values.len();
656
657 match self.db_type {
659 DatabaseType::PostgreSQL => {
660 let estimated_len = count * 5;
662 self.buffer.reserve(estimated_len);
663
664 for (i, _) in values.iter().enumerate() {
665 if i > 0 {
666 self.buffer.push_str(", ");
667 }
668 let idx = start_index + i;
669 if idx < POSTGRES_PLACEHOLDERS.len() {
670 self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
671 } else {
672 let _ = write!(self.buffer, "${}", idx);
673 }
674 }
675 }
676 DatabaseType::MySQL | DatabaseType::SQLite => {
677 if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
679 self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
680 } else {
681 let estimated_len = count * 3; self.buffer.reserve(estimated_len);
684 for i in 0..count {
685 if i > 0 {
686 self.buffer.push_str(", ");
687 }
688 self.buffer.push('?');
689 }
690 }
691 }
692 }
693
694 self.params.extend(values);
695 self
696 }
697
698 pub fn bind_in_slice<T: Into<FilterValue> + Clone>(&mut self, values: &[T]) -> &mut Self {
720 if values.is_empty() {
721 return self;
722 }
723
724 let start_index = self.params.len() + 1;
725 let count = values.len();
726
727 match self.db_type {
729 DatabaseType::PostgreSQL => {
730 let estimated_len = count * 5;
731 self.buffer.reserve(estimated_len);
732
733 for i in 0..count {
734 if i > 0 {
735 self.buffer.push_str(", ");
736 }
737 let idx = start_index + i;
738 if idx < POSTGRES_PLACEHOLDERS.len() {
739 self.buffer.push_str(POSTGRES_PLACEHOLDERS[idx]);
740 } else {
741 let _ = write!(self.buffer, "${}", idx);
742 }
743 }
744 }
745 DatabaseType::MySQL | DatabaseType::SQLite => {
746 if start_index == 1 && count < MYSQL_IN_PATTERNS.len() {
747 self.buffer.push_str(MYSQL_IN_PATTERNS[count]);
748 } else {
749 let estimated_len = count * 3;
750 self.buffer.reserve(estimated_len);
751 for i in 0..count {
752 if i > 0 {
753 self.buffer.push_str(", ");
754 }
755 self.buffer.push('?');
756 }
757 }
758 }
759 }
760
761 self.params.reserve(count);
763 for v in values {
764 self.params.push(v.clone().into());
765 }
766 self
767 }
768
769 #[inline]
774 pub fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> &mut Self {
775 let _ = self.buffer.write_fmt(args);
776 self
777 }
778
779 #[inline]
781 pub fn push_identifier(&mut self, name: &str) -> &mut Self {
782 if needs_quoting(name) {
783 self.buffer.push('"');
784 for c in name.chars() {
786 if c == '"' {
787 self.buffer.push_str("\"\"");
788 } else {
789 self.buffer.push(c);
790 }
791 }
792 self.buffer.push('"');
793 } else {
794 self.buffer.push_str(name);
795 }
796 self
797 }
798
799 #[inline]
801 pub fn push_if(&mut self, condition: bool, s: &str) -> &mut Self {
802 if condition {
803 self.push_str(s);
804 }
805 self
806 }
807
808 #[inline]
810 pub fn bind_if(&mut self, condition: bool, value: impl Into<FilterValue>) -> &mut Self {
811 if condition {
812 self.bind(value);
813 }
814 self
815 }
816
817 #[inline]
819 pub fn sql(&self) -> &str {
820 &self.buffer
821 }
822
823 #[inline]
825 pub fn params(&self) -> &[FilterValue] {
826 &self.params
827 }
828
829 #[inline]
831 pub fn param_count(&self) -> usize {
832 self.params.len()
833 }
834
835 #[inline]
837 pub fn build(self) -> (String, Vec<FilterValue>) {
838 let sql_len = self.buffer.len();
839 let param_count = self.params.len();
840 debug!(sql_len, param_count, db_type = ?self.db_type, "FastSqlBuilder::build()");
841 (self.buffer, self.params)
842 }
843
844 #[inline]
846 pub fn build_sql(self) -> String {
847 self.buffer
848 }
849}
850
851pub mod templates {
859 use super::*;
860
861 pub fn select_by_id(table: &str, columns: &[&str]) -> String {
874 let cols = if columns.is_empty() {
875 "*".to_string()
876 } else {
877 columns.join(", ")
878 };
879 format!("SELECT {} FROM {} WHERE id = $1", cols, table)
880 }
881
882 pub fn insert_returning(table: &str, columns: &[&str]) -> String {
894 let cols = columns.join(", ");
895 let placeholders: Vec<String> = (1..=columns.len())
896 .map(|i| {
897 if i < POSTGRES_PLACEHOLDERS.len() {
898 POSTGRES_PLACEHOLDERS[i].to_string()
899 } else {
900 format!("${}", i)
901 }
902 })
903 .collect();
904 format!(
905 "INSERT INTO {} ({}) VALUES ({}) RETURNING *",
906 table,
907 cols,
908 placeholders.join(", ")
909 )
910 }
911
912 pub fn update_by_id(table: &str, columns: &[&str]) -> String {
924 let sets: Vec<String> = columns
925 .iter()
926 .enumerate()
927 .map(|(i, col)| {
928 let idx = i + 1;
929 if idx < POSTGRES_PLACEHOLDERS.len() {
930 format!("{} = {}", col, POSTGRES_PLACEHOLDERS[idx])
931 } else {
932 format!("{} = ${}", col, idx)
933 }
934 })
935 .collect();
936 let id_idx = columns.len() + 1;
937 let id_placeholder = if id_idx < POSTGRES_PLACEHOLDERS.len() {
938 POSTGRES_PLACEHOLDERS[id_idx]
939 } else {
940 "$?"
941 };
942 format!(
943 "UPDATE {} SET {} WHERE id = {}",
944 table,
945 sets.join(", "),
946 id_placeholder
947 )
948 }
949
950 pub fn delete_by_id(table: &str) -> String {
961 format!("DELETE FROM {} WHERE id = $1", table)
962 }
963
964 pub fn batch_placeholders(db_type: DatabaseType, columns: usize, rows: usize) -> String {
976 let mut result = String::with_capacity(rows * columns * 4);
977 let mut param_idx = 1;
978
979 for row in 0..rows {
980 if row > 0 {
981 result.push_str(", ");
982 }
983 result.push('(');
984 for col in 0..columns {
985 if col > 0 {
986 result.push_str(", ");
987 }
988 match db_type {
989 DatabaseType::PostgreSQL => {
990 if param_idx < POSTGRES_PLACEHOLDERS.len() {
991 result.push_str(POSTGRES_PLACEHOLDERS[param_idx]);
992 } else {
993 let _ = write!(result, "${}", param_idx);
994 }
995 param_idx += 1;
996 }
997 DatabaseType::MySQL | DatabaseType::SQLite => {
998 result.push('?');
999 }
1000 }
1001 }
1002 result.push(')');
1003 }
1004
1005 result
1006 }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 #[test]
1014 fn test_escape_identifier() {
1015 assert_eq!(escape_identifier("user"), "\"user\"");
1016 assert_eq!(escape_identifier("my_table"), "\"my_table\"");
1017 assert_eq!(escape_identifier("has\"quote"), "\"has\"\"quote\"");
1018 }
1019
1020 #[test]
1021 fn test_needs_quoting() {
1022 assert!(needs_quoting("user"));
1023 assert!(needs_quoting("order"));
1024 assert!(needs_quoting("has space"));
1025 assert!(!needs_quoting("my_table"));
1026 assert!(!needs_quoting("users"));
1027 }
1028
1029 #[test]
1030 fn test_quote_identifier() {
1031 assert_eq!(quote_identifier("user"), "\"user\"");
1032 assert_eq!(quote_identifier("my_table"), "my_table");
1033 }
1034
1035 #[test]
1036 fn test_database_placeholder() {
1037 assert_eq!(DatabaseType::PostgreSQL.placeholder(1).as_ref(), "$1");
1039 assert_eq!(DatabaseType::PostgreSQL.placeholder(5).as_ref(), "$5");
1040 assert_eq!(DatabaseType::PostgreSQL.placeholder(100).as_ref(), "$100");
1041 assert_eq!(DatabaseType::PostgreSQL.placeholder(128).as_ref(), "$128");
1042 assert_eq!(DatabaseType::PostgreSQL.placeholder(256).as_ref(), "$256");
1043 assert_eq!(DatabaseType::MySQL.placeholder(1).as_ref(), "?");
1044 assert_eq!(DatabaseType::SQLite.placeholder(1).as_ref(), "?");
1045
1046 assert!(matches!(
1048 DatabaseType::MySQL.placeholder(1),
1049 Cow::Borrowed(_)
1050 ));
1051 assert!(matches!(
1052 DatabaseType::SQLite.placeholder(1),
1053 Cow::Borrowed(_)
1054 ));
1055
1056 assert!(matches!(
1058 DatabaseType::PostgreSQL.placeholder(1),
1059 Cow::Borrowed(_)
1060 ));
1061 assert!(matches!(
1062 DatabaseType::PostgreSQL.placeholder(50),
1063 Cow::Borrowed(_)
1064 ));
1065 assert!(matches!(
1066 DatabaseType::PostgreSQL.placeholder(128),
1067 Cow::Borrowed(_)
1068 ));
1069 assert!(matches!(
1070 DatabaseType::PostgreSQL.placeholder(256),
1071 Cow::Borrowed(_)
1072 ));
1073
1074 assert!(matches!(
1076 DatabaseType::PostgreSQL.placeholder(257),
1077 Cow::Owned(_)
1078 ));
1079 assert_eq!(DatabaseType::PostgreSQL.placeholder(257).as_ref(), "$257");
1080 assert_eq!(DatabaseType::PostgreSQL.placeholder(200).as_ref(), "$200");
1081
1082 assert!(matches!(
1084 DatabaseType::PostgreSQL.placeholder(0),
1085 Cow::Owned(_)
1086 ));
1087 assert_eq!(DatabaseType::PostgreSQL.placeholder(0).as_ref(), "$0");
1088 }
1089
1090 #[test]
1091 fn test_sql_builder() {
1092 let mut builder = SqlBuilder::postgres();
1093 builder
1094 .push("SELECT * FROM ")
1095 .push_identifier("user")
1096 .push(" WHERE ")
1097 .push_identifier("id")
1098 .push(" = ")
1099 .push_param(42i32);
1100
1101 let (sql, params) = builder.build();
1102 assert_eq!(sql, "SELECT * FROM \"user\" WHERE id = $1");
1103 assert_eq!(params.len(), 1);
1104 }
1105
1106 #[test]
1108 fn test_fast_builder_simple() {
1109 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1110 builder.push_str("SELECT * FROM users WHERE id = ");
1111 builder.bind(42i64);
1112 let (sql, params) = builder.build();
1113 assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1114 assert_eq!(params.len(), 1);
1115 }
1116
1117 #[test]
1118 fn test_fast_builder_complex() {
1119 let mut builder = FastSqlBuilder::with_capacity(
1120 DatabaseType::PostgreSQL,
1121 QueryCapacity::SelectWithFilters(5),
1122 );
1123 builder
1124 .push_str("SELECT * FROM users WHERE active = ")
1125 .bind(true)
1126 .push_str(" AND age > ")
1127 .bind(18i64)
1128 .push_str(" AND status = ")
1129 .bind("approved")
1130 .push_str(" ORDER BY created_at LIMIT ")
1131 .bind(10i64);
1132
1133 let (sql, params) = builder.build();
1134 assert!(sql.contains("$1"));
1135 assert!(sql.contains("$4"));
1136 assert_eq!(params.len(), 4);
1137 }
1138
1139 #[test]
1140 fn test_fast_builder_in_clause_postgres() {
1141 let mut builder = FastSqlBuilder::postgres(QueryCapacity::Custom(128));
1142 builder.push_str("SELECT * FROM users WHERE id IN (");
1143 let values: Vec<FilterValue> = (1..=5).map(|i| FilterValue::Int(i)).collect();
1144 builder.bind_in_clause(values);
1145 builder.push_char(')');
1146
1147 let (sql, params) = builder.build();
1148 assert_eq!(sql, "SELECT * FROM users WHERE id IN ($1, $2, $3, $4, $5)");
1149 assert_eq!(params.len(), 5);
1150 }
1151
1152 #[test]
1153 fn test_fast_builder_in_clause_mysql() {
1154 let mut builder = FastSqlBuilder::mysql(QueryCapacity::Custom(128));
1155 builder.push_str("SELECT * FROM users WHERE id IN (");
1156 let values: Vec<FilterValue> = (1..=5).map(|i| FilterValue::Int(i)).collect();
1157 builder.bind_in_clause(values);
1158 builder.push_char(')');
1159
1160 let (sql, params) = builder.build();
1161 assert_eq!(sql, "SELECT * FROM users WHERE id IN (?, ?, ?, ?, ?)");
1162 assert_eq!(params.len(), 5);
1163 }
1164
1165 #[test]
1166 fn test_fast_builder_identifier() {
1167 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1168 builder.push_str("SELECT * FROM ");
1169 builder.push_identifier("user"); builder.push_str(" WHERE ");
1171 builder.push_identifier("my_column"); builder.push_str(" = ");
1173 builder.bind(1i64);
1174
1175 let (sql, _) = builder.build();
1176 assert_eq!(sql, "SELECT * FROM \"user\" WHERE my_column = $1");
1177 }
1178
1179 #[test]
1180 fn test_fast_builder_identifier_with_quotes() {
1181 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SimpleSelect);
1182 builder.push_str("SELECT * FROM ");
1183 builder.push_identifier("has\"quote");
1184
1185 let sql = builder.build_sql();
1186 assert_eq!(sql, "SELECT * FROM \"has\"\"quote\"");
1187 }
1188
1189 #[test]
1190 fn test_fast_builder_conditional() {
1191 let mut builder = FastSqlBuilder::postgres(QueryCapacity::SelectWithFilters(2));
1192 builder.push_str("SELECT * FROM users WHERE 1=1");
1193 builder.push_if(true, " AND active = true");
1194 builder.push_if(false, " AND deleted = false");
1195
1196 let sql = builder.build_sql();
1197 assert_eq!(sql, "SELECT * FROM users WHERE 1=1 AND active = true");
1198 }
1199
1200 #[test]
1202 fn test_template_select_by_id() {
1203 let sql = templates::select_by_id("users", &["id", "name", "email"]);
1204 assert_eq!(sql, "SELECT id, name, email FROM users WHERE id = $1");
1205 }
1206
1207 #[test]
1208 fn test_template_select_by_id_all_columns() {
1209 let sql = templates::select_by_id("users", &[]);
1210 assert_eq!(sql, "SELECT * FROM users WHERE id = $1");
1211 }
1212
1213 #[test]
1214 fn test_template_insert_returning() {
1215 let sql = templates::insert_returning("users", &["name", "email"]);
1216 assert_eq!(
1217 sql,
1218 "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
1219 );
1220 }
1221
1222 #[test]
1223 fn test_template_update_by_id() {
1224 let sql = templates::update_by_id("users", &["name", "email"]);
1225 assert_eq!(sql, "UPDATE users SET name = $1, email = $2 WHERE id = $3");
1226 }
1227
1228 #[test]
1229 fn test_template_delete_by_id() {
1230 let sql = templates::delete_by_id("users");
1231 assert_eq!(sql, "DELETE FROM users WHERE id = $1");
1232 }
1233
1234 #[test]
1235 fn test_template_batch_placeholders_postgres() {
1236 let sql = templates::batch_placeholders(DatabaseType::PostgreSQL, 3, 2);
1237 assert_eq!(sql, "($1, $2, $3), ($4, $5, $6)");
1238 }
1239
1240 #[test]
1241 fn test_template_batch_placeholders_mysql() {
1242 let sql = templates::batch_placeholders(DatabaseType::MySQL, 3, 2);
1243 assert_eq!(sql, "(?, ?, ?), (?, ?, ?)");
1244 }
1245
1246 #[test]
1247 fn test_query_capacity_estimates() {
1248 assert_eq!(QueryCapacity::SimpleSelect.estimate(), 64);
1249 assert_eq!(QueryCapacity::SelectWithFilters(5).estimate(), 64 + 5 * 32);
1250 assert_eq!(QueryCapacity::Insert(10).estimate(), 32 + 10 * 16);
1251 assert_eq!(QueryCapacity::Update(5).estimate(), 32 + 5 * 20);
1252 assert_eq!(QueryCapacity::Delete.estimate(), 48);
1253 assert_eq!(QueryCapacity::Custom(256).estimate(), 256);
1254 }
1255}