Skip to main content

drizzle_core/sql/
mod.rs

1mod chunk;
2mod comment;
3mod cte;
4mod owned;
5mod tokens;
6
7use crate::prelude::*;
8use crate::{
9    param::{Param, ParamBind},
10    placeholder::Placeholder,
11    traits::{SQLParam, ToSQL},
12};
13pub use chunk::*;
14pub use comment::{comment, comment_tags};
15use core::fmt::{Display, Write};
16pub use owned::*;
17use smallvec::SmallVec;
18pub use tokens::*;
19
20#[cfg(feature = "profiling")]
21use crate::profile_sql;
22
23/// SQL fragment builder with flat chunk storage.
24///
25/// Uses `SmallVec<[SQLChunk; 8]>` for inline storage of typical SQL fragments
26/// without heap allocation.
27#[derive(Debug, Clone)]
28pub struct SQL<'a, V: SQLParam> {
29    pub chunks: SmallVec<[SQLChunk<'a, V>; 8]>,
30}
31
32impl<'a, V: SQLParam> SQL<'a, V> {
33    const POSITIONAL_PLACEHOLDER: Placeholder = Placeholder::anonymous();
34
35    // ==================== constructors ====================
36
37    /// Creates an empty SQL fragment
38    #[inline]
39    #[must_use]
40    pub const fn empty() -> Self {
41        Self {
42            chunks: SmallVec::new_const(),
43        }
44    }
45
46    // ==================== constructors ====================
47
48    /// Creates SQL with a single token
49    #[inline]
50    #[must_use]
51    pub fn token(t: Token) -> Self {
52        Self {
53            chunks: smallvec::smallvec![SQLChunk::Token(t)],
54        }
55    }
56
57    /// Creates an empty SQL fragment with pre-allocated chunk capacity.
58    #[inline]
59    #[must_use]
60    pub fn with_capacity_chunks(capacity: usize) -> Self {
61        Self {
62            chunks: SmallVec::with_capacity(capacity),
63        }
64    }
65
66    /// Creates SQL with a quoted identifier
67    #[inline]
68    pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
69        Self {
70            chunks: smallvec::smallvec![SQLChunk::Ident(name.into())],
71        }
72    }
73
74    /// Creates a comma-separated list of unqualified column identifiers.
75    #[must_use]
76    pub fn columns(columns: &[ColumnRef]) -> Self {
77        let mut sql = Self::with_capacity_chunks(columns.len().saturating_mul(2));
78        for (index, column) in columns.iter().enumerate() {
79            if index > 0 {
80                sql.push_mut(Token::COMMA);
81            }
82            sql.append_mut(Self::ident(column.name));
83        }
84        sql
85    }
86
87    /// Creates SQL with raw text (unquoted)
88    #[inline]
89    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
90        Self {
91            chunks: smallvec::smallvec![SQLChunk::Raw(text.into())],
92        }
93    }
94
95    /// Creates SQL with a single unsigned integer literal.
96    #[inline]
97    #[must_use]
98    pub fn number(value: usize) -> Self {
99        Self {
100            chunks: smallvec::smallvec![SQLChunk::Number(value)],
101        }
102    }
103
104    /// Creates SQL with a single parameter value
105    #[inline]
106    pub fn param(value: impl Into<Cow<'a, V>>) -> Self {
107        Self {
108            chunks: smallvec::smallvec![SQLChunk::Param(Param {
109                value: Some(value.into()),
110                placeholder: Self::POSITIONAL_PLACEHOLDER,
111            })],
112        }
113    }
114
115    /// Creates SQL with a binary parameter value (BLOB/bytea)
116    ///
117    /// Prefer this over `SQL::param(Vec<u8>)` to avoid list semantics.
118    #[inline]
119    pub fn bytes(bytes: impl Into<Cow<'a, [u8]>>) -> Self
120    where
121        V: From<&'a [u8]> + From<Vec<u8>> + Into<Cow<'a, V>>,
122    {
123        match bytes.into() {
124            Cow::Borrowed(value) => Self::param(V::from(value)),
125            Cow::Owned(value) => Self::param(V::from(value)),
126        }
127    }
128
129    /// Creates SQL referencing a table
130    #[inline]
131    #[must_use]
132    pub fn table(table: TableRef) -> Self {
133        Self {
134            chunks: smallvec::smallvec![SQLChunk::Table(TableSqlRef::from_table_ref(table))],
135        }
136    }
137
138    /// Creates SQL referencing a column
139    #[inline]
140    #[must_use]
141    pub fn column(column: ColumnRef) -> Self {
142        Self {
143            chunks: smallvec::smallvec![SQLChunk::Column(ColumnSqlRef::from_column_ref(column))],
144        }
145    }
146
147    /// Creates SQL for a function call: NAME(args)
148    /// Subqueries are automatically wrapped in parentheses: NAME((SELECT ...))
149    #[inline]
150    pub fn func(name: &'static str, args: Self) -> Self {
151        let args = args.parens_if_subquery();
152        SQL::raw(name)
153            .push(Token::LPAREN)
154            .append(args)
155            .push(Token::RPAREN)
156    }
157
158    // ==================== builder methods ====================
159
160    /// Append another SQL fragment (flat extend)
161    #[inline]
162    #[must_use]
163    pub fn append(mut self, other: impl Into<Self>) -> Self {
164        #[cfg(feature = "profiling")]
165        profile_sql!("append");
166        let other = other.into();
167
168        if self.chunks.is_empty() {
169            return other;
170        }
171        if other.chunks.is_empty() {
172            return self;
173        }
174
175        self.chunks.extend(other.chunks);
176        self
177    }
178
179    #[inline]
180    pub fn append_mut(&mut self, other: impl Into<Self>) {
181        #[cfg(feature = "profiling")]
182        profile_sql!("append_mut");
183        let other = other.into();
184
185        if self.chunks.is_empty() {
186            self.chunks = other.chunks;
187            return;
188        }
189        if other.chunks.is_empty() {
190            return;
191        }
192
193        self.chunks.extend(other.chunks);
194    }
195
196    /// Push a single chunk
197    #[inline]
198    #[must_use]
199    pub fn push(mut self, chunk: impl Into<SQLChunk<'a, V>>) -> Self {
200        self.chunks.push(chunk.into());
201        self
202    }
203
204    #[inline]
205    pub fn push_mut(&mut self, chunk: impl Into<SQLChunk<'a, V>>) {
206        self.chunks.push(chunk.into());
207    }
208
209    /// Pre-allocates capacity for additional chunks
210    #[inline]
211    #[must_use]
212    pub fn with_capacity(mut self, additional: usize) -> Self {
213        self.chunks.reserve(additional);
214        self
215    }
216
217    // ==================== combinators ====================
218
219    /// Joins multiple SQL fragments with a separator
220    pub fn join<T>(sqls: T, separator: Token) -> Self
221    where
222        T: IntoIterator,
223        T::Item: ToSQL<'a, V>,
224    {
225        #[cfg(feature = "profiling")]
226        profile_sql!("join");
227
228        let mut iter = sqls.into_iter();
229        let Some(first) = iter.next() else {
230            return SQL::empty();
231        };
232
233        let mut result = first.into_sql();
234        let (lower, upper) = iter.size_hint();
235        if let Some(upper) = upper {
236            result.chunks.reserve(upper.saturating_mul(2));
237        } else if lower > 0 {
238            result.chunks.reserve(lower * 2);
239        }
240
241        for item in iter {
242            result.chunks.push(SQLChunk::Token(separator));
243            let other = item.into_sql();
244            if !other.chunks.is_empty() {
245                result.chunks.extend(other.chunks);
246            }
247        }
248        result
249    }
250
251    /// Wrap in parentheses: (self)
252    #[inline]
253    #[must_use]
254    pub fn parens(self) -> Self {
255        SQL::token(Token::LPAREN).append(self).push(Token::RPAREN)
256    }
257
258    /// Wrap this SQL fragment in parentheses only when it is a subquery.
259    #[inline]
260    #[must_use]
261    pub fn parens_if_subquery(self) -> Self {
262        if self.is_subquery() {
263            self.parens()
264        } else {
265            self
266        }
267    }
268
269    /// Check if this SQL fragment is a subquery (starts with SELECT/WITH).
270    #[inline]
271    pub fn is_subquery(&self) -> bool {
272        matches!(
273            self.chunks.first(),
274            Some(SQLChunk::Token(Token::SELECT | Token::WITH))
275        )
276    }
277
278    /// Creates an aliased version: self AS "name"
279    #[inline]
280    #[must_use]
281    pub fn alias(self, name: impl Into<Cow<'a, str>>) -> Self {
282        self.push(Token::AS).push(SQLChunk::Ident(name.into()))
283    }
284
285    /// Creates a comma-separated list of parameters.
286    /// Builds chunks directly without intermediate SQL allocations.
287    #[inline]
288    pub fn param_list<I>(values: I) -> Self
289    where
290        I: IntoIterator,
291        I::Item: Into<Cow<'a, V>>,
292    {
293        let iter = values.into_iter();
294        let (lower, upper) = iter.size_hint();
295        let count = upper.unwrap_or(lower);
296        let mut chunks = SmallVec::with_capacity(count.saturating_mul(2).saturating_sub(1));
297        for (i, v) in iter.enumerate() {
298            if i > 0 {
299                chunks.push(SQLChunk::Token(Token::COMMA));
300            }
301            chunks.push(SQLChunk::Param(Param {
302                value: Some(v.into()),
303                placeholder: Self::POSITIONAL_PLACEHOLDER,
304            }));
305        }
306        SQL { chunks }
307    }
308
309    /// Creates a comma-separated list of column assignments: "col" = ?
310    /// Builds chunks directly without intermediate SQL allocations.
311    #[inline]
312    pub fn assignments<I, T>(pairs: I) -> Self
313    where
314        I: IntoIterator<Item = (&'static str, T)>,
315        T: Into<Cow<'a, V>>,
316    {
317        let iter = pairs.into_iter();
318        let (lower, upper) = iter.size_hint();
319        let count = upper.unwrap_or(lower);
320        // Each assignment: Ident + EQ + Param = 3 chunks, plus commas
321        let mut chunks = SmallVec::with_capacity(count.saturating_mul(4).saturating_sub(1));
322        for (i, (col, val)) in iter.enumerate() {
323            if i > 0 {
324                chunks.push(SQLChunk::Token(Token::COMMA));
325            }
326            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
327            chunks.push(SQLChunk::Token(Token::EQ));
328            chunks.push(SQLChunk::Param(Param {
329                value: Some(val.into()),
330                placeholder: Self::POSITIONAL_PLACEHOLDER,
331            }));
332        }
333        SQL { chunks }
334    }
335
336    /// Creates comma-separated column assignments from pre-built SQL fragments,
337    /// such as `"col" = <expression>`.
338    ///
339    /// Unlike `assignments()` which wraps each value in `SQL::param()`, this variant
340    /// accepts pre-built `SQL` fragments, preserving placeholders and raw expressions.
341    /// Builds chunks directly without intermediate SQL allocations.
342    #[inline]
343    pub fn assignments_sql<I>(pairs: I) -> Self
344    where
345        I: IntoIterator<Item = (&'static str, Self)>,
346    {
347        let iter = pairs.into_iter();
348        let (lower, upper) = iter.size_hint();
349        let count = upper.unwrap_or(lower);
350        let mut chunks = SmallVec::with_capacity(count.saturating_mul(4).saturating_sub(1));
351        for (i, (col, sql)) in iter.enumerate() {
352            if i > 0 {
353                chunks.push(SQLChunk::Token(Token::COMMA));
354            }
355            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
356            chunks.push(SQLChunk::Token(Token::EQ));
357            chunks.extend(sql.chunks);
358        }
359        SQL { chunks }
360    }
361
362    // ==================== output methods ====================
363
364    /// Maps parameter values from type `V` to type `U` using the provided function.
365    ///
366    /// Only `Param` chunks are affected; all other chunks pass through unchanged.
367    /// This is useful for converting between owned and borrowed value types
368    /// (e.g. `OwnedPostgresValue` → `PostgresValue<'a>`).
369    pub fn map_params<U: SQLParam>(self, mut f: impl FnMut(V) -> U) -> SQL<'a, U> {
370        let chunks = self
371            .chunks
372            .into_iter()
373            .map(|chunk| match chunk {
374                SQLChunk::Token(t) => SQLChunk::Token(t),
375                SQLChunk::Ident(s) => SQLChunk::Ident(s),
376                SQLChunk::Raw(s) => SQLChunk::Raw(s),
377                SQLChunk::Number(n) => SQLChunk::Number(n),
378                SQLChunk::Param(param) => SQLChunk::Param(Param::new(
379                    param.placeholder,
380                    param.value.map(|cow| Cow::Owned(f(cow.into_owned()))),
381                )),
382                SQLChunk::Table(t) => SQLChunk::Table(t),
383                SQLChunk::Column(c) => SQLChunk::Column(c),
384            })
385            .collect();
386        SQL { chunks }
387    }
388
389    /// Own every borrowed SQL fragment while mapping parameter values.
390    ///
391    /// This is used by generated models that outlive their source values. It
392    /// preserves identifiers, raw fragments, placeholders, tables, and columns
393    /// instead of assuming the fragment consists of a single parameter.
394    pub fn into_owned_with<U: SQLParam>(self, mut f: impl FnMut(V) -> U) -> SQL<'static, U> {
395        let chunks = self
396            .chunks
397            .into_iter()
398            .map(|chunk| match chunk {
399                SQLChunk::Token(token) => SQLChunk::Token(token),
400                SQLChunk::Ident(value) => SQLChunk::Ident(Cow::Owned(value.into_owned())),
401                SQLChunk::Raw(value) => SQLChunk::Raw(Cow::Owned(value.into_owned())),
402                SQLChunk::Number(value) => SQLChunk::Number(value),
403                SQLChunk::Param(param) => SQLChunk::Param(Param::new(
404                    param.placeholder,
405                    param.value.map(|value| Cow::Owned(f(value.into_owned()))),
406                )),
407                SQLChunk::Table(table) => SQLChunk::Table(table),
408                SQLChunk::Column(column) => SQLChunk::Column(column),
409            })
410            .collect();
411        SQL { chunks }
412    }
413
414    /// Converts to owned version (consuming self to avoid clone)
415    #[inline]
416    pub fn into_owned(self) -> OwnedSQL<V> {
417        OwnedSQL::from(self)
418    }
419
420    /// Returns the SQL string with dialect-appropriate placeholders.
421    /// Uses `$1, $2, ...` for `PostgreSQL`, `:name` or `?` for `SQLite`, `?` for `MySQL`.
422    pub fn sql(&self) -> String {
423        #[cfg(feature = "profiling")]
424        profile_sql!("sql");
425        #[cfg(feature = "profiling")]
426        crate::drizzle_profile_scope!("sql_render", "sql.estimate");
427        let (sql_cap, _) = self.render_capacity_estimate();
428        let mut buf = String::with_capacity(sql_cap);
429        self.write_to(&mut buf);
430        buf
431    }
432
433    /// Whether this statement has a `RETURNING` clause (outside any
434    /// parentheses), so it returns rows although it changes data.
435    #[must_use]
436    pub fn has_returning(&self) -> bool {
437        let mut depth = 0usize;
438        for chunk in &self.chunks {
439            match chunk {
440                SQLChunk::Token(Token::LPAREN) => depth += 1,
441                SQLChunk::Token(Token::RPAREN) => depth = depth.saturating_sub(1),
442                SQLChunk::Token(Token::RETURNING) if depth == 0 => return true,
443                _ => {}
444            }
445        }
446        false
447    }
448
449    /// Returns the SQL string with every bound value written as a literal
450    /// instead of a placeholder, for statements that cannot take parameters,
451    /// such as the body of a `CREATE VIEW`.
452    ///
453    /// Returns `None` when a placeholder has no bound value, or a value has
454    /// no literal form in this dialect (see [`SQLParam::write_literal`]).
455    #[must_use]
456    pub fn inline_sql(&self) -> Option<String> {
457        let (sql_cap, _) = self.render_capacity_estimate();
458        let mut buf = String::with_capacity(sql_cap);
459        for (i, chunk) in self.chunks.iter().enumerate() {
460            match chunk {
461                SQLChunk::Param(param) => {
462                    let value = param.value.as_ref()?;
463                    if !value.as_ref().write_literal(&mut buf) {
464                        return None;
465                    }
466                }
467                _ => chunk.write(&mut buf),
468            }
469
470            if self.ends_select_head(i) {
471                self.write_select_columns(&mut buf, i);
472            }
473
474            if self.needs_space(i) {
475                buf.push(' ');
476            }
477        }
478        Some(buf)
479    }
480
481    /// Generates the SQL string and collects parameter references in a single pass.
482    ///
483    /// This is the preferred method for driver execution paths since it avoids
484    /// iterating the chunk list twice (once for `sql()`, once for `params()`).
485    pub fn build(&self) -> (String, SmallVec<[&V; 8]>) {
486        self.build_with(crate::dialect::ParamStyle::for_dialect(V::DIALECT))
487    }
488
489    /// Same as [`build`](Self::build) but lets the caller override the
490    /// placeholder style. Drivers that speak the dialect but bind parameters
491    /// differently (e.g. AWS Data API on Postgres) use this to emit
492    /// `:1, :2, ...` instead of `$1, $2, ...` without any post-hoc rewriting.
493    pub fn build_with(&self, style: crate::dialect::ParamStyle) -> (String, SmallVec<[&V; 8]>) {
494        use crate::dialect::Dialect;
495
496        #[cfg(feature = "profiling")]
497        crate::drizzle_profile_scope!("sql_render", "build");
498        #[cfg(feature = "profiling")]
499        crate::drizzle_profile_scope!("sql_render", "build.estimate");
500        let (sql_cap, param_cap) = self.render_capacity_estimate();
501        let mut buf = String::with_capacity(sql_cap);
502        let mut params: SmallVec<[&V; 8]> = SmallVec::with_capacity(param_cap);
503        let mut param_index = 1usize;
504        let mut sqlite_names = SQLiteNamedParams::default();
505
506        #[cfg(feature = "profiling")]
507        crate::drizzle_profile_scope!("sql_render", "build.render");
508        for (i, chunk) in self.chunks.iter().enumerate() {
509            match chunk {
510                SQLChunk::Param(param) => {
511                    let mut repeated_name = false;
512                    if let Some(name) = param.placeholder.name
513                        && V::DIALECT == Dialect::SQLite
514                    {
515                        let _ = buf.write_char(':');
516                        let _ = buf.write_str(name);
517                        repeated_name = sqlite_names.is_repeat(name);
518                    } else {
519                        style.write(param_index, &mut buf);
520                    }
521                    param_index += 1;
522                    // SQLite gives every distinct `:name` one parameter
523                    // slot, so a repeated name binds its value only once.
524                    if !repeated_name && let Some(value) = &param.value {
525                        params.push(value.as_ref());
526                    }
527                }
528                _ => chunk.write(&mut buf),
529            }
530
531            if self.ends_select_head(i) {
532                self.write_select_columns(&mut buf, i);
533            }
534
535            if self.needs_space(i) {
536                let _ = buf.write_char(' ');
537            }
538        }
539
540        (buf, params)
541    }
542
543    /// Write SQL to a buffer with dialect-appropriate placeholders.
544    /// Uses `$1, $2, ...` for `PostgreSQL`, `?` or `:name` for `SQLite`, `?` for `MySQL`.
545    #[inline]
546    pub fn write_to(&self, buf: &mut impl core::fmt::Write) {
547        self.write_to_with(buf, crate::dialect::ParamStyle::for_dialect(V::DIALECT));
548    }
549
550    /// Same as [`write_to`](Self::write_to) but with a caller-chosen
551    /// placeholder style.
552    pub fn write_to_with(
553        &self,
554        buf: &mut impl core::fmt::Write,
555        style: crate::dialect::ParamStyle,
556    ) {
557        use crate::dialect::Dialect;
558
559        #[cfg(feature = "profiling")]
560        crate::drizzle_profile_scope!("sql_render", "write_to");
561        let mut param_index = 1usize;
562        for (i, chunk) in self.chunks.iter().enumerate() {
563            match chunk {
564                SQLChunk::Param(param) => {
565                    if let Some(name) = param.placeholder.name
566                        && V::DIALECT == Dialect::SQLite
567                    {
568                        let _ = buf.write_char(':');
569                        let _ = buf.write_str(name);
570                    } else {
571                        style.write(param_index, buf);
572                    }
573                    param_index += 1;
574                }
575                _ => chunk.write(buf),
576            }
577
578            if self.ends_select_head(i) {
579                self.write_select_columns(buf, i);
580            }
581
582            if self.needs_space(i) {
583                let _ = buf.write_char(' ');
584            }
585        }
586    }
587
588    /// Write a single chunk with pattern detection
589    #[inline]
590    pub fn write_chunk_to(
591        &self,
592        buf: &mut impl core::fmt::Write,
593        chunk: &SQLChunk<'a, V>,
594        index: usize,
595    ) {
596        chunk.write(buf);
597        if self.ends_select_head(index) {
598            self.write_select_columns(buf, index);
599        }
600    }
601
602    /// Whether the chunk at `index` ends a `SELECT` head with no projection,
603    /// so the projection must be expanded before the `FROM` that follows.
604    ///
605    /// The head is `SELECT`, `SELECT DISTINCT`, or `PostgreSQL`'s
606    /// `SELECT DISTINCT ON (...)`.
607    fn ends_select_head(&self, index: usize) -> bool {
608        if !matches!(
609            self.chunks.get(index + 1),
610            Some(SQLChunk::Token(Token::FROM))
611        ) {
612            return false;
613        }
614        let token_at = |position: Option<usize>| match position.and_then(|p| self.chunks.get(p)) {
615            Some(SQLChunk::Token(token)) => Some(*token),
616            _ => None,
617        };
618
619        match self.chunks[index] {
620            SQLChunk::Token(Token::SELECT) => true,
621            SQLChunk::Token(Token::DISTINCT) => {
622                matches!(token_at(index.checked_sub(1)), Some(Token::SELECT))
623            }
624            SQLChunk::Token(Token::RPAREN) => {
625                // Find the `(` this `)` closes, then look for `SELECT DISTINCT ON`.
626                let mut depth = 0usize;
627                let mut open = None;
628                for position in (0..index).rev() {
629                    match self.chunks[position] {
630                        SQLChunk::Token(Token::RPAREN) => depth += 1,
631                        SQLChunk::Token(Token::LPAREN) if depth == 0 => {
632                            open = Some(position);
633                            break;
634                        }
635                        SQLChunk::Token(Token::LPAREN) => depth -= 1,
636                        _ => {}
637                    }
638                }
639                open.is_some_and(|open| {
640                    matches!(token_at(open.checked_sub(1)), Some(Token::ON))
641                        && matches!(token_at(open.checked_sub(2)), Some(Token::DISTINCT))
642                        && matches!(token_at(open.checked_sub(3)), Some(Token::SELECT))
643                })
644            }
645            _ => false,
646        }
647    }
648
649    /// Write the projection of a `SELECT` head that ends at `head_end` and
650    /// has no explicit column list: every column of the tables in the
651    /// following `FROM` clause, or `*` for any other source.
652    #[inline]
653    pub(crate) fn write_select_columns(&self, buf: &mut impl core::fmt::Write, head_end: usize) {
654        let chunks = self.chunks.get(head_end + 1..head_end + 3);
655        match chunks {
656            Some([SQLChunk::Token(Token::FROM), SQLChunk::Table(_)]) => {
657                let _ = buf.write_char(' ');
658                let mut first = true;
659                let mut depth = 0usize;
660
661                for (index, chunk) in self.chunks.iter().enumerate().skip(head_end + 2) {
662                    match chunk {
663                        SQLChunk::Token(Token::LPAREN) => depth += 1,
664                        SQLChunk::Token(Token::RPAREN) if depth == 0 => break,
665                        SQLChunk::Token(Token::RPAREN) => depth -= 1,
666                        SQLChunk::Token(
667                            Token::WHERE
668                            | Token::GROUP
669                            | Token::HAVING
670                            | Token::ORDER
671                            | Token::LIMIT
672                            | Token::OFFSET
673                            | Token::WINDOW
674                            | Token::FOR
675                            | Token::UNION
676                            | Token::INTERSECT
677                            | Token::EXCEPT
678                            | Token::SELECT,
679                        ) if depth == 0 => break,
680                        SQLChunk::Table(table) if depth == 0 => {
681                            if !first {
682                                let _ = buf.write_str(", ");
683                            }
684                            let alias = match self.chunks.get(index + 1..index + 3) {
685                                Some([SQLChunk::Token(Token::AS), SQLChunk::Ident(alias)]) => {
686                                    Some(alias.as_ref())
687                                }
688                                _ => None,
689                            };
690                            Self::write_qualified_columns_as(buf, table, alias);
691                            first = false;
692                        }
693                        _ => {}
694                    }
695                }
696            }
697            Some([SQLChunk::Token(Token::FROM), _]) => {
698                let _ = buf.write_char(' ');
699                let _ = buf.write_str(Token::STAR.as_str());
700            }
701            _ => {}
702        }
703    }
704
705    /// Write fully qualified columns for a table
706    #[inline]
707    pub fn write_qualified_columns(buf: &mut impl core::fmt::Write, table: &TableSqlRef) {
708        Self::write_qualified_columns_as(buf, table, None);
709    }
710
711    #[inline]
712    fn write_qualified_columns_as(
713        buf: &mut impl core::fmt::Write,
714        table: &TableSqlRef,
715        alias: Option<&str>,
716    ) {
717        if table.column_names.is_empty() {
718            if let Some(alias) = alias {
719                chunk::write_dialect_quoted_ident(V::DIALECT, buf, alias);
720            } else {
721                if let Some(schema) = table.schema {
722                    chunk::write_dialect_quoted_ident(V::DIALECT, buf, schema);
723                    let _ = buf.write_char('.');
724                }
725                chunk::write_dialect_quoted_ident(V::DIALECT, buf, table.name);
726            }
727            let _ = buf.write_str(".*");
728            return;
729        }
730
731        for (i, col_name) in table.column_names.iter().enumerate() {
732            if i > 0 {
733                let _ = buf.write_str(", ");
734            }
735            if let Some(alias) = alias {
736                chunk::write_dialect_quoted_ident(V::DIALECT, buf, alias);
737            } else {
738                if let Some(schema) = table.schema {
739                    chunk::write_dialect_quoted_ident(V::DIALECT, buf, schema);
740                    let _ = buf.write_char('.');
741                }
742                chunk::write_dialect_quoted_ident(V::DIALECT, buf, table.name);
743            }
744            let _ = buf.write_char('.');
745            chunk::write_dialect_quoted_ident(V::DIALECT, buf, col_name);
746        }
747    }
748
749    /// Simplified spacing logic
750    #[inline]
751    fn needs_space(&self, index: usize) -> bool {
752        let Some(next) = self.chunks.get(index + 1) else {
753            return false;
754        };
755
756        let current = &self.chunks[index];
757        chunk_needs_space(current, next)
758    }
759
760    #[inline]
761    fn render_capacity_estimate(&self) -> (usize, usize) {
762        let mut sql_cap = 0usize;
763        let mut param_cap = 0usize;
764
765        for chunk in &self.chunks {
766            sql_cap = sql_cap.saturating_add(match chunk {
767                SQLChunk::Ident(_) | SQLChunk::Raw(_) => 20,
768                SQLChunk::Column(_) => 30,
769                SQLChunk::Table(_) => 15,
770                SQLChunk::Token(_) => 8,
771                SQLChunk::Number(_) | SQLChunk::Param(_) => 4,
772            });
773            if matches!(chunk, SQLChunk::Param(_)) {
774                param_cap = param_cap.saturating_add(1);
775            }
776        }
777
778        (sql_cap.max(128), param_cap)
779    }
780
781    /// Returns an iterator over references to parameter values
782    /// (avoids allocating a Vec - callers can collect if needed)
783    #[inline]
784    pub fn params(&self) -> impl Iterator<Item = &V> + use<'_, V> {
785        self.chunks.iter().filter_map(|chunk| {
786            if let SQLChunk::Param(Param {
787                value: Some(value), ..
788            }) = chunk
789            {
790                Some(value.as_ref())
791            } else {
792                None
793            }
794        })
795    }
796
797    /// Bind named parameters
798    #[must_use]
799    pub fn bind<T: SQLParam + Into<V>>(
800        self,
801        params: impl IntoIterator<Item: Into<ParamBind<'a, T>>>,
802    ) -> Self {
803        #[cfg(feature = "profiling")]
804        profile_sql!("bind");
805
806        let binds: SmallVec<[(&str, V); 4]> = params
807            .into_iter()
808            .map(Into::into)
809            .map(|p| (p.name, p.value.into()))
810            .collect();
811
812        if binds.len() <= 4 {
813            let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
814                .chunks
815                .into_iter()
816                .map(|chunk| match chunk {
817                    SQLChunk::Param(mut param) => {
818                        if let Some(name) = param.placeholder.name
819                            && let Some((_, value)) =
820                                binds.iter().find(|(param_name, _)| *param_name == name)
821                        {
822                            param.value = Some(Cow::Owned(value.clone()));
823                        }
824                        SQLChunk::Param(param)
825                    }
826                    other => other,
827                })
828                .collect();
829
830            return SQL {
831                chunks: bound_chunks,
832            };
833        }
834
835        let param_map: HashMap<&str, V> = binds.into_iter().collect();
836        let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
837            .chunks
838            .into_iter()
839            .map(|chunk| match chunk {
840                SQLChunk::Param(mut param) => {
841                    if let Some(name) = param.placeholder.name
842                        && let Some(value) = param_map.get(name)
843                    {
844                        param.value = Some(Cow::Owned(value.clone()));
845                    }
846                    SQLChunk::Param(param)
847                }
848                other => other,
849            })
850            .collect();
851
852        SQL {
853            chunks: bound_chunks,
854        }
855    }
856}
857
858/// Tracks the `:name` parameters already bound for one `SQLite` statement.
859///
860/// `SQLite` gives every distinct parameter name a single slot, however often
861/// the name appears, while each positional `?` takes its own slot. A value
862/// list for the statement therefore holds one entry per distinct name, at the
863/// position of the name's first occurrence.
864#[derive(Default)]
865pub(crate) struct SQLiteNamedParams<'n> {
866    seen: SmallVec<[&'n str; 4]>,
867}
868
869impl<'n> SQLiteNamedParams<'n> {
870    /// Records `name` and reports whether an earlier occurrence already took
871    /// its slot.
872    pub(crate) fn is_repeat(&mut self, name: &'n str) -> bool {
873        if name.is_empty() {
874            return false;
875        }
876        if self.seen.contains(&name) {
877            true
878        } else {
879            self.seen.push(name);
880            false
881        }
882    }
883}
884
885/// Canonical spacing logic for SQL chunk rendering.
886/// Used by both `SQL::write_to()` and `prepare_render()`.
887#[inline]
888pub(crate) fn chunk_needs_space<V: SQLParam>(
889    current: &SQLChunk<'_, V>,
890    next: &SQLChunk<'_, V>,
891) -> bool {
892    // No space if current raw text ends with space
893    if let SQLChunk::Raw(text) = current
894        && text.ends_with(' ')
895    {
896        return false;
897    }
898
899    // No space if next raw text starts with space
900    if let SQLChunk::Raw(text) = next
901        && text.starts_with(' ')
902    {
903        return false;
904    }
905
906    match (current, next) {
907        // No space before closing/separator punctuation
908        // or after opening punctuation
909        (_, SQLChunk::Token(Token::RPAREN | Token::COMMA | Token::SEMI | Token::DOT))
910        | (SQLChunk::Token(Token::LPAREN | Token::DOT), _) => false,
911        // Space after comma
912        (SQLChunk::Token(Token::COMMA), _) => true,
913        // Space after closing paren if next is word-like (e.g., ") FROM")
914        (SQLChunk::Token(Token::RPAREN), next) => next.is_word_like(),
915        // MySQL requires built-in function names to touch the opening
916        // parenthesis unless the session enables IGNORE_SPACE. SQL::func uses
917        // a raw static function name followed by LPAREN.
918        (SQLChunk::Raw(_), SQLChunk::Token(Token::LPAREN))
919            if V::DIALECT == crate::Dialect::MySQL =>
920        {
921            false
922        }
923        // Space before opening paren if preceded by word-like (e.g., "AS (")
924        (current, SQLChunk::Token(Token::LPAREN)) => current.is_word_like(),
925        // Space around comparison/arithmetic operators
926        (SQLChunk::Token(t), _) if t.is_operator() => true,
927        (_, SQLChunk::Token(t)) if t.is_operator() => true,
928        // Space between all word-like chunks
929        _ => current.is_word_like() && next.is_word_like(),
930    }
931}
932
933// ==================== trait implementations ====================
934
935impl<V: SQLParam> Default for SQL<'_, V> {
936    #[inline]
937    fn default() -> Self {
938        Self::empty()
939    }
940}
941
942impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
943    #[inline]
944    fn from(s: &'a str) -> Self {
945        SQL::raw(s)
946    }
947}
948
949impl<V: SQLParam> From<Token> for SQL<'_, V> {
950    #[inline]
951    fn from(value: Token) -> Self {
952        SQL::token(value)
953    }
954}
955
956impl<'a, V: SQLParam + 'a> AsRef<Self> for SQL<'a, V> {
957    #[inline]
958    fn as_ref(&self) -> &Self {
959        self
960    }
961}
962
963impl<V: SQLParam + core::fmt::Display> Display for SQL<'_, V> {
964    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
965        // Collect params for Debug formatting (iterator can't be used with :?)
966        let params: Vec<_> = self.params().collect();
967        write!(f, r#"sql: "{}", params: {:?}"#, self.sql(), params)
968    }
969}
970
971impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
972    fn to_sql(&self) -> Self {
973        self.clone()
974    }
975
976    fn into_sql(self) -> Self {
977        self
978    }
979}
980
981impl<'a, V: SQLParam, T> FromIterator<T> for SQL<'a, V>
982where
983    SQLChunk<'a, V>: From<T>,
984{
985    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
986        let chunks = iter
987            .into_iter()
988            .map(SQLChunk::from)
989            .collect::<SmallVec<_>>();
990        Self { chunks }
991    }
992}
993
994impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
995    type Item = SQLChunk<'a, V>;
996    type IntoIter = smallvec::IntoIter<[SQLChunk<'a, V>; 8]>;
997
998    fn into_iter(self) -> Self::IntoIter {
999        self.chunks.into_iter()
1000    }
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use crate::{Dialect, MySQLDialect};
1007
1008    #[derive(Clone, Debug)]
1009    struct TestParam;
1010
1011    impl SQLParam for TestParam {
1012        const DIALECT: Dialect = Dialect::MySQL;
1013        type DialectMarker = MySQLDialect;
1014    }
1015
1016    impl From<TestParam> for Cow<'_, TestParam> {
1017        fn from(value: TestParam) -> Self {
1018            Cow::Owned(value)
1019        }
1020    }
1021
1022    #[test]
1023    fn owning_mapped_params_preserves_every_chunk_kind() {
1024        let raw = String::from("COALESCE(");
1025        let identifier = String::from("display_name");
1026        let sql = SQL::raw(raw.as_str())
1027            .append(SQL::ident(identifier.as_str()))
1028            .push(Token::COMMA)
1029            .append(SQL::param(TestParam))
1030            .push(Token::COMMA)
1031            .push(Param::<TestParam>::from(Placeholder::named("fallback")))
1032            .push(Token::RPAREN);
1033
1034        let owned = sql.into_owned_with(|value| value);
1035        drop(raw);
1036        drop(identifier);
1037
1038        assert_eq!(owned.sql(), "COALESCE( `display_name`, ?, ?)");
1039        assert_eq!(owned.params().count(), 1);
1040        assert_eq!(
1041            owned
1042                .chunks
1043                .iter()
1044                .filter(|chunk| matches!(chunk, SQLChunk::Param(param) if param.value.is_none()))
1045                .count(),
1046            1
1047        );
1048    }
1049
1050    #[test]
1051    fn columns_renders_an_identifier_list() {
1052        let columns = [
1053            ColumnRef::sql("users", "first"),
1054            ColumnRef::sql("users", "last`name"),
1055        ];
1056
1057        assert_eq!(
1058            SQL::<TestParam>::columns(&columns).sql(),
1059            "`first`, `last``name`"
1060        );
1061    }
1062
1063    #[test]
1064    fn select_star_uses_the_table_alias_to_qualify_columns() {
1065        let table = TableRef::sql("users", &["id", "name"]);
1066        let query = SQL::<TestParam>::from(Token::SELECT)
1067            .push(Token::FROM)
1068            .append(SQL::table(table).alias("u"));
1069
1070        assert_eq!(
1071            query.sql(),
1072            "SELECT `u`.`id`, `u`.`name` FROM `users` AS `u`"
1073        );
1074    }
1075
1076    #[test]
1077    fn nested_select_star_keeps_derived_alias_in_outer_projection() {
1078        let source = SQL::<TestParam>::from(Token::SELECT)
1079            .push(Token::FROM)
1080            .append(SQL::table(TableRef::sql("posts", &["id", "name"])))
1081            .parens()
1082            .push(Token::AS)
1083            .append(SQL::table(TableRef::sql("post_rows", &[])));
1084        let query = SQL::<TestParam>::from(Token::SELECT)
1085            .push(Token::FROM)
1086            .append(SQL::table(TableRef::sql("users", &["id"])))
1087            .append(SQL::raw(" INNER JOIN LATERAL "))
1088            .append(source)
1089            .push(Token::ON)
1090            .append(SQL::raw("TRUE"));
1091
1092        assert_eq!(
1093            query.sql(),
1094            "SELECT `users`.`id`, `post_rows`.* FROM `users` INNER JOIN LATERAL (SELECT `posts`.`id`, `posts`.`name` FROM `posts`) AS `post_rows` ON TRUE"
1095        );
1096    }
1097}