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