drizzle_core/
sql.rs

1use compact_str::{CompactString, ToCompactString};
2use smallvec::{SmallVec, smallvec};
3use std::{borrow::Cow, collections::HashMap, fmt::Display};
4
5use crate::{
6    OwnedParam, Param, ParamBind, Placeholder, PlaceholderStyle,
7    traits::{SQLColumnInfo, SQLParam, SQLTableInfo},
8};
9
10/// A SQL chunk represents a part of an SQL statement.
11#[derive(Clone)]
12pub enum SQLChunk<'a, V: SQLParam + 'a> {
13    Text(Cow<'a, CompactString>),
14    Param(Param<'a, V>),
15    SQL(Box<SQL<'a, V>>),
16    /// A table reference that can render itself with proper schema/alias handling
17    Table(&'static dyn SQLTableInfo),
18    /// A column reference that can render itself with proper table qualification
19    Column(&'static dyn SQLColumnInfo),
20    /// An alias wrapping any SQL chunk: "chunk AS alias"
21    Alias {
22        chunk: Box<SQLChunk<'a, V>>,
23        alias: CompactString,
24    },
25    /// A subquery wrapped in parentheses: "(SELECT ...)"
26    Subquery(Box<SQL<'a, V>>),
27}
28
29pub enum OwnedSQLChunk<V: SQLParam> {
30    Text(CompactString),
31    Param(OwnedParam<V>),
32    SQL(Box<OwnedSQL<V>>),
33    Table(&'static dyn SQLTableInfo),
34    Column(&'static dyn SQLColumnInfo),
35    Alias {
36        chunk: Box<OwnedSQLChunk<V>>,
37        alias: CompactString,
38    },
39    Subquery(Box<OwnedSQL<V>>),
40}
41
42impl<'a, V: SQLParam> From<SQLChunk<'a, V>> for OwnedSQLChunk<V> {
43    fn from(value: SQLChunk<'a, V>) -> Self {
44        match value {
45            SQLChunk::Text(cow) => Self::Text(cow.into_owned()),
46            SQLChunk::Param(param) => Self::Param(param.into()),
47            SQLChunk::SQL(sql) => Self::SQL(Box::new((*sql).into())),
48            SQLChunk::Table(sqltable_info) => Self::Table(sqltable_info),
49            SQLChunk::Column(sqlcolumn_info) => Self::Column(sqlcolumn_info),
50            SQLChunk::Alias { chunk, alias } => Self::Alias {
51                chunk: Box::new((*chunk).into()),
52                alias,
53            },
54            SQLChunk::Subquery(sql) => Self::Subquery(Box::new((*sql).into())),
55        }
56    }
57}
58
59impl<'a, V: SQLParam + 'a> SQLChunk<'a, V> {
60    /// Creates a text chunk from a borrowed string - zero allocation
61    pub const fn text(text: &'static str) -> Self {
62        Self::Text(Cow::Owned(CompactString::const_new(text)))
63    }
64
65    /// Creates a parameter chunk with borrowed value and placeholder
66    pub const fn param(value: &'a V, placeholder: Placeholder) -> Self {
67        Self::Param(Param {
68            value: Some(Cow::Borrowed(value)),
69            placeholder,
70        })
71    }
72
73    /// Creates a nested SQL chunk
74    pub fn sql(sql: SQL<'a, V>) -> Self {
75        Self::SQL(Box::new(sql))
76    }
77
78    /// Creates a table chunk
79    pub const fn table(table: &'static dyn SQLTableInfo) -> Self {
80        Self::Table(table)
81    }
82
83    /// Creates a column chunk
84    pub const fn column(column: &'static dyn SQLColumnInfo) -> Self {
85        Self::Column(column)
86    }
87
88    /// Creates an alias chunk wrapping any SQLChunk
89    pub fn alias(chunk: SQLChunk<'a, V>, alias: impl Into<CompactString>) -> Self {
90        Self::Alias {
91            chunk: Box::new(chunk),
92            alias: alias.into(),
93        }
94    }
95
96    /// Creates a subquery chunk
97    pub fn subquery(sql: SQL<'a, V>) -> Self {
98        Self::Subquery(Box::new(sql))
99    }
100
101    /// Write chunk to buffer (zero-allocation internal method)
102    pub(crate) fn write_to_buffer(&self, buf: &mut CompactString) {
103        match self {
104            SQLChunk::Text(text) => buf.push_str(text),
105            SQLChunk::Param(Param { placeholder, .. }) => buf.push_str(&placeholder.to_string()),
106            SQLChunk::SQL(sql) => buf.push_str(&sql.sql()),
107            SQLChunk::Table(table) => {
108                buf.push('"');
109                buf.push_str(table.name());
110                buf.push('"');
111            }
112            SQLChunk::Column(column) => {
113                buf.push('"');
114                buf.push_str(column.table().name());
115                buf.push_str(r#"".""#);
116                buf.push_str(column.name());
117                buf.push('"');
118            }
119            SQLChunk::Alias { chunk, alias } => {
120                chunk.write_to_buffer(buf);
121                buf.push_str(" AS ");
122                buf.push_str(alias);
123            }
124            SQLChunk::Subquery(sql) => {
125                buf.push('(');
126                buf.push_str(&sql.sql());
127                buf.push(')');
128            }
129        }
130    }
131}
132
133impl<'a, V: SQLParam + std::fmt::Debug> std::fmt::Debug for SQLChunk<'a, V> {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            SQLChunk::Text(text) => f.debug_tuple("Text").field(text).finish(),
137            SQLChunk::Param(param) => f.debug_tuple("Param").field(param).finish(),
138            SQLChunk::SQL(_) => f.debug_tuple("SQL").field(&"<nested>").finish(),
139            SQLChunk::Table(table) => f.debug_tuple("Table").field(&table.name()).finish(),
140            SQLChunk::Column(column) => f
141                .debug_tuple("Column")
142                .field(&format!("{}.{}", column.table().name(), column.name()))
143                .finish(),
144            SQLChunk::Alias { alias, .. } => f
145                .debug_struct("Alias")
146                .field("alias", alias)
147                .field("chunk", &"<nested>")
148                .finish(),
149            SQLChunk::Subquery(_) => f.debug_tuple("Subquery").field(&"<nested>").finish(),
150        }
151    }
152}
153
154/// A SQL statement or fragment with parameters.
155///
156/// This type is used to build SQL statements with proper parameter handling.
157/// It keeps track of both the SQL text and the parameters to be bound.
158#[derive(Debug, Clone)]
159pub struct SQL<'a, V: SQLParam> {
160    /// The chunks that make up this SQL statement or fragment.
161    pub chunks: SmallVec<[SQLChunk<'a, V>; 3]>,
162}
163
164pub struct OwnedSQL<V: SQLParam> {
165    pub chunks: SmallVec<[OwnedSQLChunk<V>; 3]>,
166}
167
168impl<'a, V: SQLParam> From<SQL<'a, V>> for OwnedSQL<V> {
169    fn from(value: SQL<'a, V>) -> Self {
170        Self {
171            chunks: value.chunks.iter().map(|v| v.clone().into()).collect(),
172        }
173    }
174}
175
176impl<'a, V: SQLParam> SQL<'a, V> {
177    /// Const placeholder instances for zero-copy usage
178    const POSITIONAL_PLACEHOLDER: Placeholder = Placeholder::positional();
179
180    pub const fn new<'b>(chunks: [SQLChunk<'b, V>; 3]) -> SQL<'b, V> {
181        SQL {
182            chunks: SmallVec::from_const(chunks),
183        }
184    }
185
186    /// Creates a new empty SQL fragment.
187    pub const fn empty<'b>() -> SQL<'b, V> {
188        SQL {
189            chunks: SmallVec::new_const(),
190        }
191    }
192
193    pub fn into_owned(&self) -> OwnedSQL<V> {
194        OwnedSQL::from(self.clone())
195    }
196
197    /// Helper to create const SQL
198    pub const fn text(text: &'static str) -> Self {
199        // Create a SmallVec with the single chunk and pad with empty chunks
200        let chunks = SmallVec::from_const([
201            SQLChunk::Text(Cow::Owned(CompactString::const_new(text))),
202            SQLChunk::Text(Cow::Owned(CompactString::const_new(""))), // These will be ignored in sql() generation
203            SQLChunk::Text(Cow::Owned(CompactString::const_new(""))),
204        ]);
205        Self { chunks }
206    }
207
208    /// Creates a wildcard SELECT fragment: "*"
209    pub const fn wildcard() -> Self {
210        Self::text("*")
211    }
212
213    /// Creates a NULL SQL fragment
214    pub const fn null() -> Self {
215        Self::text("NULL")
216    }
217
218    /// Creates a TRUE SQL fragment
219    pub const fn r#true() -> Self {
220        Self::text("TRUE")
221    }
222
223    /// Creates a FALSE SQL fragment
224    pub const fn r#false() -> Self {
225        Self::text("FALSE")
226    }
227
228    /// Creates a new SQL fragment from a raw string.
229    ///
230    /// The string is treated as literal SQL text, not a parameter.
231    pub fn raw<T: AsRef<str>>(sql: T) -> Self {
232        let sql = Cow::Owned(sql.as_ref().to_compact_string());
233
234        Self {
235            chunks: smallvec![SQLChunk::Text(sql)],
236        }
237    }
238
239    /// Creates a new SQL fragment representing a parameter.
240    ///
241    /// A default positional placeholder ('?') is used, and the provided value
242    /// is stored for later binding. Accepts both owned and borrowed values.
243    pub fn parameter(param: impl Into<Cow<'a, V>>) -> Self {
244        Self {
245            chunks: smallvec![SQLChunk::Param(Param {
246                value: Some(param.into()),
247                placeholder: Self::POSITIONAL_PLACEHOLDER,
248            })],
249        }
250    }
251
252    /// Creates a new SQL fragment representing a table.
253    pub fn table(table: &'static dyn SQLTableInfo) -> SQL<'a, V> {
254        SQL {
255            chunks: smallvec![SQLChunk::table(table)],
256        }
257    }
258
259    pub fn column(column: &'static dyn SQLColumnInfo) -> SQL<'a, V> {
260        SQL {
261            chunks: smallvec![SQLChunk::column(column)],
262        }
263    }
264
265    /// Creates a named placeholder without a value - for use in query building.
266    /// Similar to drizzle-orm's sql.placeholder('name').
267    /// The value will be bound later during execution.
268    pub fn placeholder(name: &'static str) -> Self
269    where
270        V: Default,
271    {
272        Self {
273            chunks: smallvec![SQLChunk::Param(Param {
274                value: None,
275                placeholder: Placeholder::colon(name),
276            })],
277        }
278    }
279
280    /// Creates a named placeholder with a specific style.
281    pub fn placeholder_with_style(name: &'static str, style: PlaceholderStyle) -> Self
282    where
283        V: Default,
284    {
285        Self {
286            chunks: smallvec![SQLChunk::Param(Param {
287                value: None, // Temporary default value
288                placeholder: Placeholder::with_style(name, style),
289            })],
290        }
291    }
292
293    /// Creates SQL from an existing Placeholder struct.
294    pub fn from_placeholder(placeholder: Placeholder) -> Self
295    where
296        V: Default,
297    {
298        Self {
299            chunks: smallvec![SQLChunk::Param(Param::from_placeholder(placeholder))],
300        }
301    }
302
303    /// Appends a raw string to this SQL fragment.
304    ///
305    /// The string is treated as literal SQL text, not a parameter.
306    pub fn append_raw(mut self, sql: impl AsRef<str>) -> Self {
307        let text_chunk = SQLChunk::Text(Cow::Owned(sql.as_ref().to_compact_string()));
308        self.chunks.push(text_chunk);
309        self
310    }
311
312    /// Appends another SQL fragment to this one.
313    ///
314    /// Both the SQL text and parameters are merged.
315    pub fn append(mut self, other: impl Into<SQL<'a, V>>) -> Self {
316        let other_sql = other.into();
317        self.chunks.extend(other_sql.chunks);
318        self
319    }
320
321    /// Pre-allocates capacity for additional chunks.
322    pub fn with_capacity(mut self, additional: usize) -> Self {
323        self.chunks.reserve(additional);
324        self
325    }
326
327    /// Joins multiple SQL fragments with a separator.
328    ///
329    /// The separator is inserted between each fragment, but not before the first or after the last.
330    pub fn join<T>(sqls: T, separator: &'static str) -> SQL<'a, V>
331    where
332        T: IntoIterator,
333        T::Item: crate::ToSQL<'a, V>,
334    {
335        let sqls: Vec<_> = sqls.into_iter().map(|sql| sql.to_sql()).collect();
336
337        if sqls.is_empty() {
338            return SQL::empty();
339        }
340
341        if sqls.len() == 1 {
342            return sqls.into_iter().next().unwrap();
343        }
344
345        // Pre-calculate capacity: sum of all chunks + separators
346        let total_chunks =
347            sqls.iter().map(|sql| sql.chunks.len()).sum::<usize>() + (sqls.len() - 1);
348        let mut chunks = SmallVec::with_capacity(total_chunks);
349
350        let separator_chunk = SQLChunk::Text(Cow::Owned(CompactString::const_new(separator)));
351
352        for (i, sql) in sqls.into_iter().enumerate() {
353            if i > 0 {
354                chunks.push(separator_chunk.clone());
355            }
356            chunks.extend(sql.chunks);
357        }
358
359        SQL { chunks }
360    }
361
362    /// Collects parameter references from a single chunk
363    fn collect_chunk_params<'b>(chunk: &'b SQLChunk<'a, V>, params_vec: &mut Vec<&'b V>) {
364        match chunk {
365            SQLChunk::Param(Param {
366                value: Some(value), ..
367            }) => params_vec.push(value.as_ref()),
368            SQLChunk::SQL(sql) => params_vec.extend(sql.params()),
369            SQLChunk::Alias { chunk, .. } => Self::collect_chunk_params(chunk, params_vec),
370            SQLChunk::Subquery(sql) => params_vec.extend(sql.params()),
371            _ => {}
372        }
373    }
374
375    /// Returns the SQL string represented by this SQL fragment, using placeholders for parameters.
376    pub fn sql(&self) -> String {
377        match self.chunks.len() {
378            0 => String::new(),
379            1 => self.render_single_chunk(0).to_string(),
380            _ => {
381                let capacity = self.estimate_capacity();
382                let mut buf = CompactString::with_capacity(capacity);
383                self.write_sql(&mut buf);
384                buf.into()
385            }
386        }
387    }
388
389    pub fn bind(self, params: impl IntoIterator<Item = ParamBind<'a, V>>) -> SQL<'a, V> {
390        let param_map: HashMap<&str, V> = params.into_iter().map(|p| (p.name, p.value)).collect();
391
392        let bound_chunks = self
393            .into_iter()
394            .map(|chunk| match chunk {
395                SQLChunk::Param(mut param) => {
396                    // Only bind named placeholders
397                    if let Some(name) = param.placeholder.name
398                        && let Some(value) = param_map.get(name)
399                    {
400                        param.value = Some(Cow::Owned(value.clone()));
401                    }
402                    SQLChunk::Param(param)
403                }
404                other => other,
405            })
406            .collect();
407
408        SQL {
409            chunks: bound_chunks,
410        }
411    }
412
413    /// Write fully qualified columns to buffer - unified implementation
414    pub(crate) fn write_qualified_columns(
415        &self,
416        buf: &mut CompactString,
417        table: &'a dyn SQLTableInfo,
418    ) {
419        let columns = table.columns();
420        if columns.is_empty() {
421            buf.push('*');
422            return;
423        }
424
425        for (i, col) in columns.iter().enumerate() {
426            if i > 0 {
427                buf.push_str(", ");
428            }
429            buf.push('"');
430            buf.push_str(table.name());
431            buf.push_str(r#"".""#);
432            buf.push_str(col.name());
433            buf.push('"');
434        }
435    }
436
437    fn write_sql(&self, buf: &mut CompactString) {
438        for i in 0..self.chunks.len() {
439            self.write_chunk(buf, &self.chunks[i], i);
440
441            if self.needs_space(i) {
442                buf.push(' ');
443            }
444        }
445    }
446
447    /// Write a single chunk to buffer with pattern detection
448    pub(crate) fn write_chunk(
449        &self,
450        buf: &mut CompactString,
451        chunk: &SQLChunk<'a, V>,
452        index: usize,
453    ) {
454        match chunk {
455            SQLChunk::Text(text) if text.is_empty() => {
456                if let Some(table) = self.detect_pattern_at(index) {
457                    self.write_qualified_columns(buf, table);
458                }
459            }
460            SQLChunk::Text(text) if text.trim().eq_ignore_ascii_case("SELECT") => {
461                if let Some(table) = self.detect_select_from_table_pattern(index) {
462                    buf.push_str("SELECT ");
463                    self.write_qualified_columns(buf, table);
464                } else if self.detect_select_from_non_table_pattern(index) {
465                    buf.push_str("SELECT *");
466                } else {
467                    buf.push_str(text);
468                }
469            }
470            SQLChunk::Text(text) => buf.push_str(text),
471            SQLChunk::Param(Param { placeholder, .. }) => buf.push_str(&placeholder.to_string()),
472            SQLChunk::SQL(sql) => buf.push_str(&sql.sql()),
473            SQLChunk::Table(table) => {
474                buf.push('"');
475                buf.push_str(table.name());
476                buf.push('"');
477            }
478            SQLChunk::Column(column) => {
479                buf.push('"');
480                buf.push_str(column.table().name());
481                buf.push_str(r#"".""#);
482                buf.push_str(column.name());
483                buf.push('"');
484            }
485            SQLChunk::Alias { chunk, alias } => {
486                chunk.write_to_buffer(buf);
487                buf.push_str(" AS ");
488                buf.push_str(alias);
489            }
490            SQLChunk::Subquery(sql) => {
491                buf.push('(');
492                buf.push_str(&sql.sql());
493                buf.push(')');
494            }
495        }
496    }
497
498    /// Fast single chunk rendering - reuses write_chunk for consistency
499    fn render_single_chunk(&self, index: usize) -> CompactString {
500        let chunk = &self.chunks[index];
501        let capacity = match chunk {
502            SQLChunk::Text(text) if text.is_empty() => self
503                .detect_pattern_at(index)
504                .map(|table| table.columns().len() * 20)
505                .unwrap_or(0),
506            SQLChunk::Text(text) => text.len(),
507            SQLChunk::Table(table) => table.name().len() + 2,
508            SQLChunk::Column(column) => column.table().name().len() + column.name().len() + 5,
509            SQLChunk::Alias { alias, .. } => alias.len() + 10,
510            _ => 32,
511        };
512
513        let mut buf = CompactString::with_capacity(capacity);
514        self.write_chunk(&mut buf, chunk, index);
515        buf
516    }
517
518    /// Pattern detection: looks for SELECT ... FROM table patterns
519    pub(crate) fn detect_pattern_at(&self, empty_index: usize) -> Option<&'a dyn SQLTableInfo> {
520        let select_pos = self.find_select_before(empty_index)?;
521        self.find_table_after_from(select_pos)
522    }
523
524    /// Detect SELECT-FROM-TABLE pattern starting from SELECT index
525    pub(crate) fn detect_select_from_table_pattern(
526        &self,
527        select_index: usize,
528    ) -> Option<&'a dyn SQLTableInfo> {
529        if select_index + 2 < self.chunks.len()
530            && let (SQLChunk::Text(from_text), SQLChunk::Table(table)) = (
531                &self.chunks[select_index + 1],
532                &self.chunks[select_index + 2],
533            )
534            && from_text.trim().eq_ignore_ascii_case("FROM")
535        {
536            return Some(*table);
537        }
538        None
539    }
540
541    /// Detect SELECT-FROM-NON_TABLE pattern (e.g., CTE, subquery, etc.)
542    fn detect_select_from_non_table_pattern(&self, select_index: usize) -> bool {
543        if select_index + 2 < self.chunks.len() {
544            if let SQLChunk::Text(from_text) = &self.chunks[select_index + 1] {
545                if from_text.trim().eq_ignore_ascii_case("FROM") {
546                    // Check if what follows FROM is NOT a table
547                    match &self.chunks[select_index + 2] {
548                        SQLChunk::Table(_) => false, // This is a table, handled by other pattern
549                        SQLChunk::Text(_) | SQLChunk::SQL(_) | SQLChunk::Subquery(_) => true, // CTE name, subquery, etc.
550                        _ => true, // Any other chunk type is not a table
551                    }
552                } else {
553                    false
554                }
555            } else {
556                false
557            }
558        } else {
559            false
560        }
561    }
562
563    /// Find SELECT keyword before the given position
564    fn find_select_before(&self, pos: usize) -> Option<usize> {
565        self.chunks[..pos]
566            .iter()
567            .enumerate()
568            .rev()
569            .find_map(|(i, chunk)| match chunk {
570                SQLChunk::Text(text) if text.trim().eq_ignore_ascii_case("SELECT") => Some(i),
571                _ => None,
572            })
573    }
574
575    /// Find table after FROM keyword starting from SELECT position
576    fn find_table_after_from(&self, select_pos: usize) -> Option<&'a dyn SQLTableInfo> {
577        let chunks = &self.chunks[select_pos..];
578        let mut found_from = false;
579
580        for chunk in chunks {
581            match chunk {
582                SQLChunk::Text(text) if text.trim().eq_ignore_ascii_case("FROM") => {
583                    found_from = true;
584                }
585                SQLChunk::Table(table) if found_from => return Some(*table),
586                _ => {}
587            }
588        }
589        None
590    }
591
592    /// Smart capacity estimation
593    fn estimate_capacity(&self) -> usize {
594        let chunk_content_size: usize = self
595            .chunks
596            .iter()
597            .map(|chunk| match chunk {
598                SQLChunk::Text(t) => t.len(),
599                SQLChunk::Param { .. } => 1, // Single placeholder character
600                SQLChunk::SQL(sql) => sql.chunks.len() * 8, // Average 8 chars per nested chunk
601                SQLChunk::Table(t) => t.name().len() + 2, // Quotes around table name
602                SQLChunk::Column(c) => c.table().name().len() + c.name().len() + 5, // "table"."column"
603                SQLChunk::Alias { alias, .. } => alias.len() + 4, // " AS " + alias
604                SQLChunk::Subquery(sql) => (sql.chunks.len() * 8) + 2, // Parentheses + content
605            })
606            .sum();
607
608        // Add space for potential spaces between chunks
609        chunk_content_size + self.chunks.len()
610    }
611
612    pub(crate) fn needs_space(&self, index: usize) -> bool {
613        if index + 1 >= self.chunks.len() {
614            return false;
615        }
616
617        let current = &self.chunks[index];
618
619        // Find next non-empty chunk
620        let mut next_index = index + 1;
621        let next = loop {
622            if next_index >= self.chunks.len() {
623                return false;
624            }
625
626            let candidate = &self.chunks[next_index];
627            if let SQLChunk::Text(t) = candidate
628                && t.is_empty()
629            {
630                next_index += 1;
631                continue;
632            }
633            break candidate;
634        };
635
636        let ends_word = chunk_ends_word(current);
637        let starts_word = chunk_starts_word(next);
638
639        ends_word && starts_word
640    }
641
642    /// Returns references to parameter values from this SQL fragment in the correct order.
643    pub fn params(&self) -> Vec<&V> {
644        let mut params_vec = Vec::with_capacity(self.chunks.len().min(8));
645        for chunk in &self.chunks {
646            Self::collect_chunk_params(chunk, &mut params_vec);
647        }
648        params_vec
649    }
650
651    /// Creates an aliased version of this SQL using the Alias chunk
652    pub fn alias(self, alias: impl Into<CompactString>) -> SQL<'a, V> {
653        SQL {
654            chunks: smallvec![SQLChunk::Alias {
655                chunk: Box::new(SQLChunk::SQL(Box::new(self))),
656                alias: alias.into(),
657            }],
658        }
659    }
660
661    /// Wraps this SQL as a subquery
662    pub fn subquery(self) -> SQL<'a, V> {
663        SQL {
664            chunks: smallvec![SQLChunk::subquery(self)],
665        }
666    }
667
668    /// Creates a comma-separated list of parameter placeholders with values: "?, ?, ?"
669    pub fn parameters<I>(values: I) -> Self
670    where
671        I: IntoIterator,
672        I::Item: Into<Cow<'a, V>>,
673    {
674        let values: Vec<_> = values.into_iter().map(|v| v.into()).collect();
675
676        if values.is_empty() {
677            return Self::empty();
678        }
679
680        if values.len() == 1 {
681            return Self::parameter(values.into_iter().next().unwrap());
682        }
683
684        // Pre-calculate capacity: each value creates a param chunk, plus separators
685        let mut chunks = SmallVec::with_capacity(values.len() * 2 - 1);
686        let separator_chunk = SQLChunk::Text(Cow::Owned(CompactString::const_new(", ")));
687
688        for (i, value) in values.into_iter().enumerate() {
689            if i > 0 {
690                chunks.push(separator_chunk.clone());
691            }
692            chunks.push(SQLChunk::Param(Param {
693                value: Some(value),
694                placeholder: Self::POSITIONAL_PLACEHOLDER,
695            }));
696        }
697
698        SQL { chunks }
699    }
700
701    /// Creates a comma-separated list of column assignments: "col1 = ?, col2 = ?"
702    pub fn assignments<I, T>(pairs: I) -> Self
703    where
704        I: IntoIterator<Item = (&'a str, T)>,
705        T: Into<Cow<'a, V>>,
706    {
707        let pairs: Vec<_> = pairs
708            .into_iter()
709            .map(|(col, val)| (col, val.into()))
710            .collect();
711
712        if pairs.is_empty() {
713            return Self::empty();
714        }
715
716        if pairs.len() == 1 {
717            let (col, val) = pairs.into_iter().next().unwrap();
718            return Self::raw(col)
719                .append_raw(" = ")
720                .append(Self::parameter(val));
721        }
722
723        // Pre-calculate capacity: each pair creates 3 chunks (col, " = ", param), plus separators
724        let mut chunks = SmallVec::with_capacity(pairs.len() * 4 - 1);
725        let separator_chunk = SQLChunk::Text(Cow::Owned(CompactString::const_new(", ")));
726        let equals_chunk = SQLChunk::Text(Cow::Owned(CompactString::const_new(" = ")));
727
728        for (i, (col, val)) in pairs.into_iter().enumerate() {
729            if i > 0 {
730                chunks.push(separator_chunk.clone());
731            }
732            chunks.push(SQLChunk::Text(Cow::Owned(col.to_compact_string())));
733            chunks.push(equals_chunk.clone());
734            chunks.push(SQLChunk::Param(Param {
735                value: Some(val),
736                placeholder: Self::POSITIONAL_PLACEHOLDER,
737            }));
738        }
739
740        SQL { chunks }
741    }
742}
743
744/// Helper function to determine if a chunk ends with a word character
745fn chunk_ends_word<V: SQLParam>(chunk: &SQLChunk<'_, V>) -> bool {
746    match chunk {
747        SQLChunk::Text(t) => {
748            let last = t.chars().last().unwrap_or(' ');
749            !last.is_whitespace() && !['(', ',', '.', ')'].contains(&last)
750        }
751        SQLChunk::Table(_)
752        | SQLChunk::Column(_)
753        | SQLChunk::Param(_)
754        | SQLChunk::Alias { .. }
755        | SQLChunk::Subquery(_) => true,
756        _ => false,
757    }
758}
759
760/// Helper function to determine if a chunk starts with a word character  
761fn chunk_starts_word<V: SQLParam>(chunk: &SQLChunk<'_, V>) -> bool {
762    match chunk {
763        SQLChunk::Text(t) => {
764            let first = t.chars().next().unwrap_or(' ');
765            !first.is_whitespace() && !['(', ',', ')', ';'].contains(&first)
766        }
767        SQLChunk::Table(_)
768        | SQLChunk::Column(_)
769        | SQLChunk::Param(_)
770        | SQLChunk::Alias { .. }
771        | SQLChunk::Subquery(_) => true,
772        _ => false,
773    }
774}
775
776impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
777    type Item = SQLChunk<'a, V>;
778    type IntoIter = std::iter::FlatMap<
779        smallvec::IntoIter<[SQLChunk<'a, V>; 3]>,
780        Box<dyn Iterator<Item = SQLChunk<'a, V>> + 'a>,
781        fn(SQLChunk<'a, V>) -> Box<dyn Iterator<Item = SQLChunk<'a, V>> + 'a>,
782    >;
783
784    fn into_iter(self) -> Self::IntoIter {
785        fn flatten_chunk<'a, V: SQLParam>(
786            chunk: SQLChunk<'a, V>,
787        ) -> Box<dyn Iterator<Item = SQLChunk<'a, V>> + 'a> {
788            match chunk {
789                SQLChunk::SQL(nested_sql) => Box::new(nested_sql.into_iter()),
790                other => Box::new(std::iter::once(other)),
791            }
792        }
793
794        self.chunks
795            .into_iter()
796            .flat_map(flatten_chunk as fn(_) -> _)
797    }
798}
799
800impl<'a, V: SQLParam> Default for SQL<'a, V> {
801    fn default() -> Self {
802        Self::empty()
803    }
804}
805
806impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
807    fn from(s: &'a str) -> Self {
808        SQL::raw(s)
809    }
810}
811
812impl<'a, V: SQLParam + 'a> AsRef<SQL<'a, V>> for SQL<'a, V> {
813    fn as_ref(&self) -> &SQL<'a, V> {
814        self
815    }
816}
817
818impl<'a, V: SQLParam + std::fmt::Display> Display for SQL<'a, V> {
819    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
820        let params = self.params();
821        write!(f, r#"sql: "{}", params: {:?} "#, self.sql(), params)
822    }
823}
824
825use crate::ToSQL;
826
827impl<V: SQLParam> ToSQL<'static, V> for OwnedSQL<V> {
828    fn to_sql(&self) -> SQL<'static, V> {
829        SQL::from(self)
830    }
831}
832
833impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
834    fn to_sql(&self) -> SQL<'a, V> {
835        self.clone()
836    }
837}