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/// A SQL fragment: tokens, identifiers, raw text, and bound parameters.
24///
25/// Every query builder and expression renders to a `SQL`. `V` is the
26/// driver's value type (`SQLiteValue`, `PostgresValue`, ...), which also
27/// fixes the dialect used to render placeholders: `?` for SQLite and MySQL,
28/// `$1, $2, ...` for PostgreSQL.
29///
30/// Chunks are stored inline for short fragments (up to 8) without a heap
31/// allocation.
32///
33/// # Examples
34///
35/// ```
36/// use drizzle_core::{SQL, Token};
37/// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
38/// # use std::borrow::Cow;
39/// # #[derive(Debug, Clone, PartialEq)]
40/// # struct Value(i64);
41/// # impl SQLParam for Value {
42/// #     const DIALECT: Dialect = Dialect::SQLite;
43/// #     type DialectMarker = SQLiteDialect;
44/// # }
45/// # impl From<Value> for Cow<'_, Value> {
46/// #     fn from(value: Value) -> Self { Cow::Owned(value) }
47/// # }
48///
49/// let sql: SQL<'_, Value> = SQL::raw("SELECT")
50///     .append(SQL::ident("name"))
51///     .push(Token::FROM)
52///     .append(SQL::ident("users"))
53///     .push(Token::WHERE)
54///     .append(SQL::ident("id"))
55///     .push(Token::EQ)
56///     .append(SQL::param(Value(7)));
57///
58/// assert_eq!(sql.sql(), r#"SELECT "name" FROM "users" WHERE "id" = ?"#);
59/// assert_eq!(sql.params().collect::<Vec<_>>(), [&Value(7)]);
60/// ```
61#[derive(Debug, Clone)]
62pub struct SQL<'a, V: SQLParam> {
63    /// The fragment's chunks, in order.
64    pub chunks: SmallVec<[SQLChunk<'a, V>; 8]>,
65}
66
67impl<'a, V: SQLParam> SQL<'a, V> {
68    const POSITIONAL_PLACEHOLDER: Placeholder = Placeholder::anonymous();
69
70    // ==================== constructors ====================
71
72    /// Creates an empty fragment.
73    #[inline]
74    #[must_use]
75    pub const fn empty() -> Self {
76        Self {
77            chunks: SmallVec::new_const(),
78        }
79    }
80
81    /// Creates a fragment holding one keyword or punctuation [`Token`].
82    #[inline]
83    #[must_use]
84    pub fn token(t: Token) -> Self {
85        Self {
86            chunks: smallvec::smallvec![SQLChunk::Token(t)],
87        }
88    }
89
90    /// Creates an empty fragment with room for `capacity` chunks.
91    #[inline]
92    #[must_use]
93    pub fn with_capacity_chunks(capacity: usize) -> Self {
94        Self {
95            chunks: SmallVec::with_capacity(capacity),
96        }
97    }
98
99    /// Creates a quoted identifier, such as a table or column name.
100    ///
101    /// The quote character follows the dialect: `"name"` for SQLite and
102    /// PostgreSQL, `` `name` `` for MySQL.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use drizzle_core::SQL;
108    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
109    /// # use std::borrow::Cow;
110    /// # #[derive(Debug, Clone, PartialEq)]
111    /// # struct Value(i64);
112    /// # impl SQLParam for Value {
113    /// #     const DIALECT: Dialect = Dialect::SQLite;
114    /// #     type DialectMarker = SQLiteDialect;
115    /// # }
116    /// # impl From<Value> for Cow<'_, Value> {
117    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
118    /// # }
119    ///
120    /// assert_eq!(SQL::<Value>::ident("user name").sql(), r#""user name""#);
121    /// ```
122    #[inline]
123    pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
124        Self {
125            chunks: smallvec::smallvec![SQLChunk::Ident(name.into())],
126        }
127    }
128
129    /// Creates a comma-separated list of quoted column names, without table
130    /// qualifiers: `"id", "name"`.
131    #[must_use]
132    pub fn columns(columns: &[ColumnRef]) -> Self {
133        let mut sql = Self::with_capacity_chunks(columns.len().saturating_mul(2));
134        for (index, column) in columns.iter().enumerate() {
135            if index > 0 {
136                sql.push_mut(Token::COMMA);
137            }
138            sql.append_mut(Self::ident(column.name));
139        }
140        sql
141    }
142
143    /// Creates a fragment of raw SQL text, written as-is.
144    ///
145    /// The text is not quoted or escaped. Never pass user input here; bind it
146    /// with [`SQL::param`] instead.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use drizzle_core::SQL;
152    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
153    /// # use std::borrow::Cow;
154    /// # #[derive(Debug, Clone, PartialEq)]
155    /// # struct Value(i64);
156    /// # impl SQLParam for Value {
157    /// #     const DIALECT: Dialect = Dialect::SQLite;
158    /// #     type DialectMarker = SQLiteDialect;
159    /// # }
160    /// # impl From<Value> for Cow<'_, Value> {
161    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
162    /// # }
163    ///
164    /// assert_eq!(SQL::<Value>::raw("CURRENT_TIMESTAMP").sql(), "CURRENT_TIMESTAMP");
165    /// ```
166    #[inline]
167    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
168        Self {
169            chunks: smallvec::smallvec![SQLChunk::Raw(text.into())],
170        }
171    }
172
173    /// Creates an unsigned integer literal, written into the SQL text rather
174    /// than bound.
175    #[inline]
176    #[must_use]
177    pub fn number(value: usize) -> Self {
178        Self {
179            chunks: smallvec::smallvec![SQLChunk::Number(value)],
180        }
181    }
182
183    /// Creates a bound parameter holding `value`.
184    ///
185    /// The SQL text gets a placeholder (`?` or `$n`); the value travels
186    /// separately and is returned by [`SQL::params`].
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use drizzle_core::SQL;
192    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
193    /// # use std::borrow::Cow;
194    /// # #[derive(Debug, Clone, PartialEq)]
195    /// # struct Value(i64);
196    /// # impl SQLParam for Value {
197    /// #     const DIALECT: Dialect = Dialect::SQLite;
198    /// #     type DialectMarker = SQLiteDialect;
199    /// # }
200    /// # impl From<Value> for Cow<'_, Value> {
201    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
202    /// # }
203    ///
204    /// let sql = SQL::param(Value(1)).append(SQL::raw("+")).append(SQL::param(Value(2)));
205    /// assert_eq!(sql.sql(), "? + ?");
206    /// assert_eq!(sql.params().count(), 2);
207    /// ```
208    #[inline]
209    pub fn param(value: impl Into<Cow<'a, V>>) -> Self {
210        Self {
211            chunks: smallvec::smallvec![SQLChunk::Param(Param {
212                value: Some(value.into()),
213                placeholder: Self::POSITIONAL_PLACEHOLDER,
214            })],
215        }
216    }
217
218    /// Creates a bound binary parameter (BLOB / `bytea`).
219    ///
220    /// Prefer this over `SQL::param(Vec<u8>)`, which some value types treat
221    /// as a list rather than one binary value.
222    #[inline]
223    pub fn bytes(bytes: impl Into<Cow<'a, [u8]>>) -> Self
224    where
225        V: From<&'a [u8]> + From<Vec<u8>> + Into<Cow<'a, V>>,
226    {
227        match bytes.into() {
228            Cow::Borrowed(value) => Self::param(V::from(value)),
229            Cow::Owned(value) => Self::param(V::from(value)),
230        }
231    }
232
233    /// Creates a reference to a table, rendered as its quoted name.
234    ///
235    /// Right after `SELECT ... FROM`, a table reference also lets the
236    /// renderer expand an empty projection into the table's columns.
237    #[inline]
238    #[must_use]
239    pub fn table(table: TableRef) -> Self {
240        Self {
241            chunks: smallvec::smallvec![SQLChunk::Table(TableSqlRef::from_table_ref(table))],
242        }
243    }
244
245    /// Creates a reference to a column, rendered as `"table"."column"`.
246    #[inline]
247    #[must_use]
248    pub fn column(column: ColumnRef) -> Self {
249        Self {
250            chunks: smallvec::smallvec![SQLChunk::Column(ColumnSqlRef::from_column_ref(column))],
251        }
252    }
253
254    /// Creates a function call: `NAME(args)`.
255    ///
256    /// A subquery argument gets its own parentheses: `NAME((SELECT ...))`.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use drizzle_core::SQL;
262    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
263    /// # use std::borrow::Cow;
264    /// # #[derive(Debug, Clone, PartialEq)]
265    /// # struct Value(i64);
266    /// # impl SQLParam for Value {
267    /// #     const DIALECT: Dialect = Dialect::SQLite;
268    /// #     type DialectMarker = SQLiteDialect;
269    /// # }
270    /// # impl From<Value> for Cow<'_, Value> {
271    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
272    /// # }
273    ///
274    /// let sql = SQL::<Value>::func("LOWER", SQL::ident("email"));
275    /// // SQLite and PostgreSQL put a space before `(`; MySQL does not.
276    /// assert_eq!(sql.sql(), r#"LOWER ("email")"#);
277    /// ```
278    #[inline]
279    pub fn func(name: &'static str, args: Self) -> Self {
280        let args = args.parens_if_subquery();
281        SQL::raw(name)
282            .push(Token::LPAREN)
283            .append(args)
284            .push(Token::RPAREN)
285    }
286
287    // ==================== builder methods ====================
288
289    /// Appends another fragment and returns the result.
290    #[inline]
291    #[must_use]
292    pub fn append(mut self, other: impl Into<Self>) -> Self {
293        #[cfg(feature = "profiling")]
294        profile_sql!("append");
295        let other = other.into();
296
297        if self.chunks.is_empty() {
298            return other;
299        }
300        if other.chunks.is_empty() {
301            return self;
302        }
303
304        self.chunks.extend(other.chunks);
305        self
306    }
307
308    /// Appends another fragment in place.
309    #[inline]
310    pub fn append_mut(&mut self, other: impl Into<Self>) {
311        #[cfg(feature = "profiling")]
312        profile_sql!("append_mut");
313        let other = other.into();
314
315        if self.chunks.is_empty() {
316            self.chunks = other.chunks;
317            return;
318        }
319        if other.chunks.is_empty() {
320            return;
321        }
322
323        self.chunks.extend(other.chunks);
324    }
325
326    /// Appends one chunk (a [`Token`], parameter, ...) and returns the result.
327    #[inline]
328    #[must_use]
329    pub fn push(mut self, chunk: impl Into<SQLChunk<'a, V>>) -> Self {
330        self.chunks.push(chunk.into());
331        self
332    }
333
334    /// Appends one chunk in place.
335    #[inline]
336    pub fn push_mut(&mut self, chunk: impl Into<SQLChunk<'a, V>>) {
337        self.chunks.push(chunk.into());
338    }
339
340    /// Reserves room for `additional` more chunks.
341    #[inline]
342    #[must_use]
343    pub fn with_capacity(mut self, additional: usize) -> Self {
344        self.chunks.reserve(additional);
345        self
346    }
347
348    // ==================== combinators ====================
349
350    /// Joins fragments with a separator token. Returns an empty fragment for
351    /// an empty iterator.
352    ///
353    /// # Examples
354    ///
355    /// ```
356    /// use drizzle_core::{SQL, Token};
357    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
358    /// # use std::borrow::Cow;
359    /// # #[derive(Debug, Clone, PartialEq)]
360    /// # struct Value(i64);
361    /// # impl SQLParam for Value {
362    /// #     const DIALECT: Dialect = Dialect::SQLite;
363    /// #     type DialectMarker = SQLiteDialect;
364    /// # }
365    /// # impl From<Value> for Cow<'_, Value> {
366    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
367    /// # }
368    ///
369    /// let cols = [SQL::<Value>::ident("id"), SQL::ident("name")];
370    /// assert_eq!(SQL::join(cols, Token::COMMA).sql(), r#""id", "name""#);
371    /// ```
372    pub fn join<T>(sqls: T, separator: Token) -> Self
373    where
374        T: IntoIterator,
375        T::Item: ToSQL<'a, V>,
376    {
377        #[cfg(feature = "profiling")]
378        profile_sql!("join");
379
380        let mut iter = sqls.into_iter();
381        let Some(first) = iter.next() else {
382            return SQL::empty();
383        };
384
385        let mut result = first.into_sql();
386        let (lower, upper) = iter.size_hint();
387        if let Some(upper) = upper {
388            result.chunks.reserve(upper.saturating_mul(2));
389        } else if lower > 0 {
390            result.chunks.reserve(lower * 2);
391        }
392
393        for item in iter {
394            result.chunks.push(SQLChunk::Token(separator));
395            let other = item.into_sql();
396            if !other.chunks.is_empty() {
397                result.chunks.extend(other.chunks);
398            }
399        }
400        result
401    }
402
403    /// Wraps the fragment in parentheses: `(self)`.
404    #[inline]
405    #[must_use]
406    pub fn parens(self) -> Self {
407        SQL::token(Token::LPAREN).append(self).push(Token::RPAREN)
408    }
409
410    /// Wraps the fragment in parentheses only when it is a subquery (see
411    /// [`SQL::is_subquery`]).
412    #[inline]
413    #[must_use]
414    pub fn parens_if_subquery(self) -> Self {
415        if self.is_subquery() {
416            self.parens()
417        } else {
418            self
419        }
420    }
421
422    /// Returns `true` if the fragment starts with a `SELECT` or `WITH` token.
423    #[inline]
424    pub fn is_subquery(&self) -> bool {
425        matches!(
426            self.chunks.first(),
427            Some(SQLChunk::Token(Token::SELECT | Token::WITH))
428        )
429    }
430
431    /// Appends an alias: `self AS "name"`.
432    #[inline]
433    #[must_use]
434    pub fn alias(self, name: impl Into<Cow<'a, str>>) -> Self {
435        self.push(Token::AS).push(SQLChunk::Ident(name.into()))
436    }
437
438    /// Creates a comma-separated list of bound parameters: `?, ?, ?`.
439    #[inline]
440    pub fn param_list<I>(values: I) -> Self
441    where
442        I: IntoIterator,
443        I::Item: Into<Cow<'a, V>>,
444    {
445        let iter = values.into_iter();
446        let (lower, upper) = iter.size_hint();
447        let count = upper.unwrap_or(lower);
448        let mut chunks = SmallVec::with_capacity(count.saturating_mul(2).saturating_sub(1));
449        for (i, v) in iter.enumerate() {
450            if i > 0 {
451                chunks.push(SQLChunk::Token(Token::COMMA));
452            }
453            chunks.push(SQLChunk::Param(Param {
454                value: Some(v.into()),
455                placeholder: Self::POSITIONAL_PLACEHOLDER,
456            }));
457        }
458        SQL { chunks }
459    }
460
461    /// Creates comma-separated assignments with bound values:
462    /// `"col" = ?, "other" = ?`.
463    #[inline]
464    pub fn assignments<I, T>(pairs: I) -> Self
465    where
466        I: IntoIterator<Item = (&'static str, T)>,
467        T: Into<Cow<'a, V>>,
468    {
469        let iter = pairs.into_iter();
470        let (lower, upper) = iter.size_hint();
471        let count = upper.unwrap_or(lower);
472        // Each assignment: Ident + EQ + Param = 3 chunks, plus commas
473        let mut chunks = SmallVec::with_capacity(count.saturating_mul(4).saturating_sub(1));
474        for (i, (col, val)) in iter.enumerate() {
475            if i > 0 {
476                chunks.push(SQLChunk::Token(Token::COMMA));
477            }
478            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
479            chunks.push(SQLChunk::Token(Token::EQ));
480            chunks.push(SQLChunk::Param(Param {
481                value: Some(val.into()),
482                placeholder: Self::POSITIONAL_PLACEHOLDER,
483            }));
484        }
485        SQL { chunks }
486    }
487
488    /// Creates comma-separated assignments from fragments:
489    /// `"col" = <expression>`.
490    ///
491    /// Unlike [`SQL::assignments`], which binds each value as a parameter,
492    /// this keeps each fragment as-is, so placeholders and raw expressions
493    /// survive.
494    #[inline]
495    pub fn assignments_sql<I>(pairs: I) -> Self
496    where
497        I: IntoIterator<Item = (&'static str, Self)>,
498    {
499        let iter = pairs.into_iter();
500        let (lower, upper) = iter.size_hint();
501        let count = upper.unwrap_or(lower);
502        let mut chunks = SmallVec::with_capacity(count.saturating_mul(4).saturating_sub(1));
503        for (i, (col, sql)) in iter.enumerate() {
504            if i > 0 {
505                chunks.push(SQLChunk::Token(Token::COMMA));
506            }
507            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
508            chunks.push(SQLChunk::Token(Token::EQ));
509            chunks.extend(sql.chunks);
510        }
511        SQL { chunks }
512    }
513
514    // ==================== output methods ====================
515
516    /// Converts every bound value with `f`, changing the value type to `U`.
517    ///
518    /// Only parameter chunks change. Useful for converting between owned and
519    /// borrowed value types (for example `OwnedPostgresValue` to
520    /// `PostgresValue<'a>`).
521    pub fn map_params<U: SQLParam>(self, mut f: impl FnMut(V) -> U) -> SQL<'a, U> {
522        let chunks = self
523            .chunks
524            .into_iter()
525            .map(|chunk| match chunk {
526                SQLChunk::Token(t) => SQLChunk::Token(t),
527                SQLChunk::Ident(s) => SQLChunk::Ident(s),
528                SQLChunk::Raw(s) => SQLChunk::Raw(s),
529                SQLChunk::Number(n) => SQLChunk::Number(n),
530                SQLChunk::Param(param) => SQLChunk::Param(Param::new(
531                    param.placeholder,
532                    param.value.map(|cow| Cow::Owned(f(cow.into_owned()))),
533                )),
534                SQLChunk::Table(t) => SQLChunk::Table(t),
535                SQLChunk::Column(c) => SQLChunk::Column(c),
536            })
537            .collect();
538        SQL { chunks }
539    }
540
541    /// Copies every borrowed string into owned storage and converts bound
542    /// values with `f`, giving a `'static` fragment.
543    ///
544    /// Generated models use this to outlive the values they were built from.
545    /// Identifiers, raw text, placeholders, tables and columns are all kept.
546    pub fn into_owned_with<U: SQLParam>(self, mut f: impl FnMut(V) -> U) -> SQL<'static, U> {
547        let chunks = self
548            .chunks
549            .into_iter()
550            .map(|chunk| match chunk {
551                SQLChunk::Token(token) => SQLChunk::Token(token),
552                SQLChunk::Ident(value) => SQLChunk::Ident(Cow::Owned(value.into_owned())),
553                SQLChunk::Raw(value) => SQLChunk::Raw(Cow::Owned(value.into_owned())),
554                SQLChunk::Number(value) => SQLChunk::Number(value),
555                SQLChunk::Param(param) => SQLChunk::Param(Param::new(
556                    param.placeholder,
557                    param.value.map(|value| Cow::Owned(f(value.into_owned()))),
558                )),
559                SQLChunk::Table(table) => SQLChunk::Table(table),
560                SQLChunk::Column(column) => SQLChunk::Column(column),
561            })
562            .collect();
563        SQL { chunks }
564    }
565
566    /// Converts into an [`OwnedSQL`] with no borrowed data.
567    #[inline]
568    pub fn into_owned(self) -> OwnedSQL<V> {
569        OwnedSQL::from(self)
570    }
571
572    /// Renders the SQL text.
573    ///
574    /// Placeholders follow the dialect of `V`: `$1, $2, ...` for PostgreSQL,
575    /// `:name` (named) or `?` for SQLite, and `?` for MySQL. Use
576    /// [`SQL::build`] to get the parameters in the same pass.
577    pub fn sql(&self) -> String {
578        #[cfg(feature = "profiling")]
579        profile_sql!("sql");
580        #[cfg(feature = "profiling")]
581        crate::drizzle_profile_scope!("sql_render", "sql.estimate");
582        let (sql_cap, _) = self.render_capacity_estimate();
583        let mut buf = String::with_capacity(sql_cap);
584        self.write_to(&mut buf);
585        buf
586    }
587
588    /// Whether this statement has a `RETURNING` clause (outside any
589    /// parentheses), so it returns rows although it changes data.
590    #[must_use]
591    pub fn has_returning(&self) -> bool {
592        let mut depth = 0usize;
593        for chunk in &self.chunks {
594            match chunk {
595                SQLChunk::Token(Token::LPAREN) => depth += 1,
596                SQLChunk::Token(Token::RPAREN) => depth = depth.saturating_sub(1),
597                SQLChunk::Token(Token::RETURNING) if depth == 0 => return true,
598                _ => {}
599            }
600        }
601        false
602    }
603
604    /// Renders the SQL text with every bound value written as a literal
605    /// instead of a placeholder.
606    ///
607    /// For statements that cannot take parameters, such as the body of a
608    /// `CREATE VIEW`. Returns `None` when a placeholder has no bound value,
609    /// or a value has no literal form in this dialect (see
610    /// [`SQLParam::write_literal`]).
611    #[must_use]
612    pub fn inline_sql(&self) -> Option<String> {
613        let (sql_cap, _) = self.render_capacity_estimate();
614        let mut buf = String::with_capacity(sql_cap);
615        for (i, chunk) in self.chunks.iter().enumerate() {
616            match chunk {
617                SQLChunk::Param(param) => {
618                    let value = param.value.as_ref()?;
619                    if !value.as_ref().write_literal(&mut buf) {
620                        return None;
621                    }
622                }
623                _ => chunk.write(&mut buf),
624            }
625
626            if self.ends_select_head(i) {
627                self.write_select_columns(&mut buf, i);
628            }
629
630            if self.needs_space(i) {
631                buf.push(' ');
632            }
633        }
634        Some(buf)
635    }
636
637    /// Renders the SQL text and collects the bound values in one pass.
638    ///
639    /// Drivers use this to execute a statement. Placeholders without a bound
640    /// value are rendered but not collected. For SQLite, a named placeholder
641    /// that appears more than once is collected once, since SQLite gives each
642    /// distinct `:name` a single slot.
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// use drizzle_core::SQL;
648    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
649    /// # use std::borrow::Cow;
650    /// # #[derive(Debug, Clone, PartialEq)]
651    /// # struct Value(i64);
652    /// # impl SQLParam for Value {
653    /// #     const DIALECT: Dialect = Dialect::SQLite;
654    /// #     type DialectMarker = SQLiteDialect;
655    /// # }
656    /// # impl From<Value> for Cow<'_, Value> {
657    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
658    /// # }
659    ///
660    /// let sql = SQL::raw("SELECT").append(SQL::param(Value(1)));
661    /// let (text, params) = sql.build();
662    /// assert_eq!(text, "SELECT ?");
663    /// assert_eq!(params.as_slice(), [&Value(1)]);
664    /// ```
665    pub fn build(&self) -> (String, SmallVec<[&V; 8]>) {
666        self.build_with(crate::dialect::ParamStyle::for_dialect(V::DIALECT))
667    }
668
669    /// Like [`build`](Self::build), but with a caller-chosen placeholder
670    /// style.
671    ///
672    /// For drivers that speak the dialect but bind parameters differently,
673    /// such as the AWS Data API on PostgreSQL, which needs `:1, :2, ...`
674    /// instead of `$1, $2, ...`.
675    pub fn build_with(&self, style: crate::dialect::ParamStyle) -> (String, SmallVec<[&V; 8]>) {
676        use crate::dialect::Dialect;
677
678        #[cfg(feature = "profiling")]
679        crate::drizzle_profile_scope!("sql_render", "build");
680        #[cfg(feature = "profiling")]
681        crate::drizzle_profile_scope!("sql_render", "build.estimate");
682        let (sql_cap, param_cap) = self.render_capacity_estimate();
683        let mut buf = String::with_capacity(sql_cap);
684        let mut params: SmallVec<[&V; 8]> = SmallVec::with_capacity(param_cap);
685        let mut param_index = 1usize;
686        let mut sqlite_names = SQLiteNamedParams::default();
687
688        #[cfg(feature = "profiling")]
689        crate::drizzle_profile_scope!("sql_render", "build.render");
690        for (i, chunk) in self.chunks.iter().enumerate() {
691            match chunk {
692                SQLChunk::Param(param) => {
693                    let mut repeated_name = false;
694                    if let Some(name) = param.placeholder.name
695                        && V::DIALECT == Dialect::SQLite
696                    {
697                        let _ = buf.write_char(':');
698                        let _ = buf.write_str(name);
699                        repeated_name = sqlite_names.is_repeat(name);
700                    } else {
701                        style.write(param_index, &mut buf);
702                    }
703                    param_index += 1;
704                    // SQLite gives every distinct `:name` one parameter
705                    // slot, so a repeated name binds its value only once.
706                    if !repeated_name && let Some(value) = &param.value {
707                        params.push(value.as_ref());
708                    }
709                }
710                _ => chunk.write(&mut buf),
711            }
712
713            if self.ends_select_head(i) {
714                self.write_select_columns(&mut buf, i);
715            }
716
717            if self.needs_space(i) {
718                let _ = buf.write_char(' ');
719            }
720        }
721
722        (buf, params)
723    }
724
725    /// Writes the SQL text to `buf`, like [`SQL::sql`] without allocating a
726    /// new string.
727    #[inline]
728    pub fn write_to(&self, buf: &mut impl core::fmt::Write) {
729        self.write_to_with(buf, crate::dialect::ParamStyle::for_dialect(V::DIALECT));
730    }
731
732    /// Like [`write_to`](Self::write_to), but with a caller-chosen
733    /// placeholder style.
734    pub fn write_to_with(
735        &self,
736        buf: &mut impl core::fmt::Write,
737        style: crate::dialect::ParamStyle,
738    ) {
739        use crate::dialect::Dialect;
740
741        #[cfg(feature = "profiling")]
742        crate::drizzle_profile_scope!("sql_render", "write_to");
743        let mut param_index = 1usize;
744        for (i, chunk) in self.chunks.iter().enumerate() {
745            match chunk {
746                SQLChunk::Param(param) => {
747                    if let Some(name) = param.placeholder.name
748                        && V::DIALECT == Dialect::SQLite
749                    {
750                        let _ = buf.write_char(':');
751                        let _ = buf.write_str(name);
752                    } else {
753                        style.write(param_index, buf);
754                    }
755                    param_index += 1;
756                }
757                _ => chunk.write(buf),
758            }
759
760            if self.ends_select_head(i) {
761                self.write_select_columns(buf, i);
762            }
763
764            if self.needs_space(i) {
765                let _ = buf.write_char(' ');
766            }
767        }
768    }
769
770    /// Writes the chunk at `index` to `buf`, expanding an empty `SELECT`
771    /// projection that ends at this chunk.
772    ///
773    /// Does not write the space that may follow the chunk.
774    #[inline]
775    pub fn write_chunk_to(
776        &self,
777        buf: &mut impl core::fmt::Write,
778        chunk: &SQLChunk<'a, V>,
779        index: usize,
780    ) {
781        chunk.write(buf);
782        if self.ends_select_head(index) {
783            self.write_select_columns(buf, index);
784        }
785    }
786
787    /// Whether the chunk at `index` ends a `SELECT` head with no projection,
788    /// so the projection must be expanded before the `FROM` that follows.
789    ///
790    /// The head is `SELECT`, `SELECT DISTINCT`, or `PostgreSQL`'s
791    /// `SELECT DISTINCT ON (...)`.
792    fn ends_select_head(&self, index: usize) -> bool {
793        if !matches!(
794            self.chunks.get(index + 1),
795            Some(SQLChunk::Token(Token::FROM))
796        ) {
797            return false;
798        }
799        let token_at = |position: Option<usize>| match position.and_then(|p| self.chunks.get(p)) {
800            Some(SQLChunk::Token(token)) => Some(*token),
801            _ => None,
802        };
803
804        match self.chunks[index] {
805            SQLChunk::Token(Token::SELECT) => true,
806            SQLChunk::Token(Token::DISTINCT) => {
807                matches!(token_at(index.checked_sub(1)), Some(Token::SELECT))
808            }
809            SQLChunk::Token(Token::RPAREN) => {
810                // Find the `(` this `)` closes, then look for `SELECT DISTINCT ON`.
811                let mut depth = 0usize;
812                let mut open = None;
813                for position in (0..index).rev() {
814                    match self.chunks[position] {
815                        SQLChunk::Token(Token::RPAREN) => depth += 1,
816                        SQLChunk::Token(Token::LPAREN) if depth == 0 => {
817                            open = Some(position);
818                            break;
819                        }
820                        SQLChunk::Token(Token::LPAREN) => depth -= 1,
821                        _ => {}
822                    }
823                }
824                open.is_some_and(|open| {
825                    matches!(token_at(open.checked_sub(1)), Some(Token::ON))
826                        && matches!(token_at(open.checked_sub(2)), Some(Token::DISTINCT))
827                        && matches!(token_at(open.checked_sub(3)), Some(Token::SELECT))
828                })
829            }
830            _ => false,
831        }
832    }
833
834    /// Write the projection of a `SELECT` head that ends at `head_end` and
835    /// has no explicit column list: every column of the tables in the
836    /// following `FROM` clause, or `*` for any other source.
837    #[inline]
838    pub(crate) fn write_select_columns(&self, buf: &mut impl core::fmt::Write, head_end: usize) {
839        let chunks = self.chunks.get(head_end + 1..head_end + 3);
840        match chunks {
841            Some([SQLChunk::Token(Token::FROM), SQLChunk::Table(_)]) => {
842                let _ = buf.write_char(' ');
843                let mut first = true;
844                let mut depth = 0usize;
845
846                for (index, chunk) in self.chunks.iter().enumerate().skip(head_end + 2) {
847                    match chunk {
848                        SQLChunk::Token(Token::LPAREN) => depth += 1,
849                        SQLChunk::Token(Token::RPAREN) if depth == 0 => break,
850                        SQLChunk::Token(Token::RPAREN) => depth -= 1,
851                        SQLChunk::Token(
852                            Token::WHERE
853                            | Token::GROUP
854                            | Token::HAVING
855                            | Token::ORDER
856                            | Token::LIMIT
857                            | Token::OFFSET
858                            | Token::WINDOW
859                            | Token::FOR
860                            | Token::UNION
861                            | Token::INTERSECT
862                            | Token::EXCEPT
863                            | Token::SELECT,
864                        ) if depth == 0 => break,
865                        SQLChunk::Table(table) if depth == 0 => {
866                            if !first {
867                                let _ = buf.write_str(", ");
868                            }
869                            let alias = match self.chunks.get(index + 1..index + 3) {
870                                Some([SQLChunk::Token(Token::AS), SQLChunk::Ident(alias)]) => {
871                                    Some(alias.as_ref())
872                                }
873                                _ => None,
874                            };
875                            Self::write_qualified_columns_as(buf, table, alias);
876                            first = false;
877                        }
878                        _ => {}
879                    }
880                }
881            }
882            Some([SQLChunk::Token(Token::FROM), _]) => {
883                let _ = buf.write_char(' ');
884                let _ = buf.write_str(Token::STAR.as_str());
885            }
886            _ => {}
887        }
888    }
889
890    /// Writes every column of `table` as `"table"."column"`, separated by
891    /// commas, or `"table".*` when the column list is unknown.
892    #[inline]
893    pub fn write_qualified_columns(buf: &mut impl core::fmt::Write, table: &TableSqlRef) {
894        Self::write_qualified_columns_as(buf, table, None);
895    }
896
897    #[inline]
898    fn write_qualified_columns_as(
899        buf: &mut impl core::fmt::Write,
900        table: &TableSqlRef,
901        alias: Option<&str>,
902    ) {
903        if table.column_names.is_empty() {
904            if let Some(alias) = alias {
905                chunk::write_dialect_quoted_ident(V::DIALECT, buf, alias);
906            } else {
907                if let Some(schema) = table.schema {
908                    chunk::write_dialect_quoted_ident(V::DIALECT, buf, schema);
909                    let _ = buf.write_char('.');
910                }
911                chunk::write_dialect_quoted_ident(V::DIALECT, buf, table.name);
912            }
913            let _ = buf.write_str(".*");
914            return;
915        }
916
917        for (i, col_name) in table.column_names.iter().enumerate() {
918            if i > 0 {
919                let _ = buf.write_str(", ");
920            }
921            if let Some(alias) = alias {
922                chunk::write_dialect_quoted_ident(V::DIALECT, buf, alias);
923            } else {
924                if let Some(schema) = table.schema {
925                    chunk::write_dialect_quoted_ident(V::DIALECT, buf, schema);
926                    let _ = buf.write_char('.');
927                }
928                chunk::write_dialect_quoted_ident(V::DIALECT, buf, table.name);
929            }
930            let _ = buf.write_char('.');
931            chunk::write_dialect_quoted_ident(V::DIALECT, buf, col_name);
932        }
933    }
934
935    /// Whether a space goes between the chunk at `index` and the next one.
936    #[inline]
937    fn needs_space(&self, index: usize) -> bool {
938        let Some(next) = self.chunks.get(index + 1) else {
939            return false;
940        };
941
942        let current = &self.chunks[index];
943        chunk_needs_space(current, next)
944    }
945
946    #[inline]
947    fn render_capacity_estimate(&self) -> (usize, usize) {
948        let mut sql_cap = 0usize;
949        let mut param_cap = 0usize;
950
951        for chunk in &self.chunks {
952            sql_cap = sql_cap.saturating_add(match chunk {
953                SQLChunk::Ident(_) | SQLChunk::Raw(_) => 20,
954                SQLChunk::Column(_) => 30,
955                SQLChunk::Table(_) => 15,
956                SQLChunk::Token(_) => 8,
957                SQLChunk::Number(_) | SQLChunk::Param(_) => 4,
958            });
959            if matches!(chunk, SQLChunk::Param(_)) {
960                param_cap = param_cap.saturating_add(1);
961            }
962        }
963
964        (sql_cap.max(128), param_cap)
965    }
966
967    /// Returns the bound values, in order. Placeholders without a value are
968    /// skipped.
969    #[inline]
970    pub fn params(&self) -> impl Iterator<Item = &V> + use<'_, V> {
971        self.chunks.iter().filter_map(|chunk| {
972            if let SQLChunk::Param(Param {
973                value: Some(value), ..
974            }) = chunk
975            {
976                Some(value.as_ref())
977            } else {
978                None
979            }
980        })
981    }
982
983    /// Fills named placeholders with values, matching by name.
984    ///
985    /// Placeholders whose name is not in `params` keep no value. Values can
986    /// come from [`TypedPlaceholder::bind`](crate::TypedPlaceholder::bind),
987    /// which also checks the value's SQL type.
988    ///
989    /// # Examples
990    ///
991    /// ```
992    /// use drizzle_core::{ParamBind, Placeholder, SQL, ToSQL, Token};
993    /// # use drizzle_core::{Dialect, SQLParam, SQLiteDialect};
994    /// # use std::borrow::Cow;
995    /// # #[derive(Debug, Clone, PartialEq)]
996    /// # struct Value(i64);
997    /// # impl SQLParam for Value {
998    /// #     const DIALECT: Dialect = Dialect::SQLite;
999    /// #     type DialectMarker = SQLiteDialect;
1000    /// # }
1001    /// # impl From<Value> for Cow<'_, Value> {
1002    /// #     fn from(value: Value) -> Self { Cow::Owned(value) }
1003    /// # }
1004    ///
1005    /// let sql: SQL<'_, Value> = SQL::ident("id")
1006    ///     .push(Token::EQ)
1007    ///     .append(Placeholder::named("id").into_sql());
1008    /// assert_eq!(sql.params().count(), 0);
1009    ///
1010    /// let bound = sql.bind([ParamBind::new("id", Value(3))]);
1011    /// assert_eq!(bound.sql(), r#""id" = :id"#);
1012    /// assert_eq!(bound.params().collect::<Vec<_>>(), [&Value(3)]);
1013    /// ```
1014    #[must_use]
1015    pub fn bind<T: SQLParam + Into<V>>(
1016        self,
1017        params: impl IntoIterator<Item: Into<ParamBind<'a, T>>>,
1018    ) -> Self {
1019        #[cfg(feature = "profiling")]
1020        profile_sql!("bind");
1021
1022        let binds: SmallVec<[(&str, V); 4]> = params
1023            .into_iter()
1024            .map(Into::into)
1025            .map(|p| (p.name, p.value.into()))
1026            .collect();
1027
1028        if binds.len() <= 4 {
1029            let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
1030                .chunks
1031                .into_iter()
1032                .map(|chunk| match chunk {
1033                    SQLChunk::Param(mut param) => {
1034                        if let Some(name) = param.placeholder.name
1035                            && let Some((_, value)) =
1036                                binds.iter().find(|(param_name, _)| *param_name == name)
1037                        {
1038                            param.value = Some(Cow::Owned(value.clone()));
1039                        }
1040                        SQLChunk::Param(param)
1041                    }
1042                    other => other,
1043                })
1044                .collect();
1045
1046            return SQL {
1047                chunks: bound_chunks,
1048            };
1049        }
1050
1051        let param_map: HashMap<&str, V> = binds.into_iter().collect();
1052        let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
1053            .chunks
1054            .into_iter()
1055            .map(|chunk| match chunk {
1056                SQLChunk::Param(mut param) => {
1057                    if let Some(name) = param.placeholder.name
1058                        && let Some(value) = param_map.get(name)
1059                    {
1060                        param.value = Some(Cow::Owned(value.clone()));
1061                    }
1062                    SQLChunk::Param(param)
1063                }
1064                other => other,
1065            })
1066            .collect();
1067
1068        SQL {
1069            chunks: bound_chunks,
1070        }
1071    }
1072}
1073
1074/// Tracks the `:name` parameters already bound for one `SQLite` statement.
1075///
1076/// `SQLite` gives every distinct parameter name a single slot, however often
1077/// the name appears, while each positional `?` takes its own slot. A value
1078/// list for the statement therefore holds one entry per distinct name, at the
1079/// position of the name's first occurrence.
1080#[derive(Default)]
1081pub(crate) struct SQLiteNamedParams<'n> {
1082    seen: SmallVec<[&'n str; 4]>,
1083}
1084
1085impl<'n> SQLiteNamedParams<'n> {
1086    /// Records `name` and reports whether an earlier occurrence already took
1087    /// its slot.
1088    pub(crate) fn is_repeat(&mut self, name: &'n str) -> bool {
1089        if name.is_empty() {
1090            return false;
1091        }
1092        if self.seen.contains(&name) {
1093            true
1094        } else {
1095            self.seen.push(name);
1096            false
1097        }
1098    }
1099}
1100
1101/// Whether a space goes between two adjacent chunks when rendering.
1102/// Shared by `SQL::write_to()` and `prepare_render()`.
1103#[inline]
1104pub(crate) fn chunk_needs_space<V: SQLParam>(
1105    current: &SQLChunk<'_, V>,
1106    next: &SQLChunk<'_, V>,
1107) -> bool {
1108    // No space if current raw text ends with space
1109    if let SQLChunk::Raw(text) = current
1110        && text.ends_with(' ')
1111    {
1112        return false;
1113    }
1114
1115    // No space if next raw text starts with space
1116    if let SQLChunk::Raw(text) = next
1117        && text.starts_with(' ')
1118    {
1119        return false;
1120    }
1121
1122    match (current, next) {
1123        // No space before closing/separator punctuation
1124        // or after opening punctuation
1125        (_, SQLChunk::Token(Token::RPAREN | Token::COMMA | Token::SEMI | Token::DOT))
1126        | (SQLChunk::Token(Token::LPAREN | Token::DOT), _) => false,
1127        // Space after comma
1128        (SQLChunk::Token(Token::COMMA), _) => true,
1129        // Space after closing paren if next is word-like (e.g., ") FROM")
1130        (SQLChunk::Token(Token::RPAREN), next) => next.is_word_like(),
1131        // MySQL requires built-in function names to touch the opening
1132        // parenthesis unless the session enables IGNORE_SPACE. SQL::func uses
1133        // a raw static function name followed by LPAREN.
1134        (SQLChunk::Raw(_), SQLChunk::Token(Token::LPAREN))
1135            if V::DIALECT == crate::Dialect::MySQL =>
1136        {
1137            false
1138        }
1139        // Space before opening paren if preceded by word-like (e.g., "AS (")
1140        (current, SQLChunk::Token(Token::LPAREN)) => current.is_word_like(),
1141        // Space around comparison/arithmetic operators
1142        (SQLChunk::Token(t), _) if t.is_operator() => true,
1143        (_, SQLChunk::Token(t)) if t.is_operator() => true,
1144        // Space between all word-like chunks
1145        _ => current.is_word_like() && next.is_word_like(),
1146    }
1147}
1148
1149// ==================== trait implementations ====================
1150
1151impl<V: SQLParam> Default for SQL<'_, V> {
1152    #[inline]
1153    fn default() -> Self {
1154        Self::empty()
1155    }
1156}
1157
1158impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
1159    #[inline]
1160    fn from(s: &'a str) -> Self {
1161        SQL::raw(s)
1162    }
1163}
1164
1165impl<V: SQLParam> From<Token> for SQL<'_, V> {
1166    #[inline]
1167    fn from(value: Token) -> Self {
1168        SQL::token(value)
1169    }
1170}
1171
1172impl<'a, V: SQLParam + 'a> AsRef<Self> for SQL<'a, V> {
1173    #[inline]
1174    fn as_ref(&self) -> &Self {
1175        self
1176    }
1177}
1178
1179impl<V: SQLParam + core::fmt::Display> Display for SQL<'_, V> {
1180    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1181        // Collect params for Debug formatting (iterator can't be used with :?)
1182        let params: Vec<_> = self.params().collect();
1183        write!(f, r#"sql: "{}", params: {:?}"#, self.sql(), params)
1184    }
1185}
1186
1187impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
1188    fn to_sql(&self) -> Self {
1189        self.clone()
1190    }
1191
1192    fn into_sql(self) -> Self {
1193        self
1194    }
1195}
1196
1197impl<'a, V: SQLParam, T> FromIterator<T> for SQL<'a, V>
1198where
1199    SQLChunk<'a, V>: From<T>,
1200{
1201    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
1202        let chunks = iter
1203            .into_iter()
1204            .map(SQLChunk::from)
1205            .collect::<SmallVec<_>>();
1206        Self { chunks }
1207    }
1208}
1209
1210impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
1211    type Item = SQLChunk<'a, V>;
1212    type IntoIter = smallvec::IntoIter<[SQLChunk<'a, V>; 8]>;
1213
1214    fn into_iter(self) -> Self::IntoIter {
1215        self.chunks.into_iter()
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use crate::{Dialect, MySQLDialect};
1223
1224    #[derive(Clone, Debug)]
1225    struct TestParam;
1226
1227    impl SQLParam for TestParam {
1228        const DIALECT: Dialect = Dialect::MySQL;
1229        type DialectMarker = MySQLDialect;
1230    }
1231
1232    impl From<TestParam> for Cow<'_, TestParam> {
1233        fn from(value: TestParam) -> Self {
1234            Cow::Owned(value)
1235        }
1236    }
1237
1238    #[test]
1239    fn owning_mapped_params_preserves_every_chunk_kind() {
1240        let raw = String::from("COALESCE(");
1241        let identifier = String::from("display_name");
1242        let sql = SQL::raw(raw.as_str())
1243            .append(SQL::ident(identifier.as_str()))
1244            .push(Token::COMMA)
1245            .append(SQL::param(TestParam))
1246            .push(Token::COMMA)
1247            .push(Param::<TestParam>::from(Placeholder::named("fallback")))
1248            .push(Token::RPAREN);
1249
1250        let owned = sql.into_owned_with(|value| value);
1251        drop(raw);
1252        drop(identifier);
1253
1254        assert_eq!(owned.sql(), "COALESCE( `display_name`, ?, ?)");
1255        assert_eq!(owned.params().count(), 1);
1256        assert_eq!(
1257            owned
1258                .chunks
1259                .iter()
1260                .filter(|chunk| matches!(chunk, SQLChunk::Param(param) if param.value.is_none()))
1261                .count(),
1262            1
1263        );
1264    }
1265
1266    #[test]
1267    fn columns_renders_an_identifier_list() {
1268        let columns = [
1269            ColumnRef::sql("users", "first"),
1270            ColumnRef::sql("users", "last`name"),
1271        ];
1272
1273        assert_eq!(
1274            SQL::<TestParam>::columns(&columns).sql(),
1275            "`first`, `last``name`"
1276        );
1277    }
1278
1279    #[test]
1280    fn select_star_uses_the_table_alias_to_qualify_columns() {
1281        let table = TableRef::sql("users", &["id", "name"]);
1282        let query = SQL::<TestParam>::from(Token::SELECT)
1283            .push(Token::FROM)
1284            .append(SQL::table(table).alias("u"));
1285
1286        assert_eq!(
1287            query.sql(),
1288            "SELECT `u`.`id`, `u`.`name` FROM `users` AS `u`"
1289        );
1290    }
1291
1292    #[test]
1293    fn nested_select_star_keeps_derived_alias_in_outer_projection() {
1294        let source = SQL::<TestParam>::from(Token::SELECT)
1295            .push(Token::FROM)
1296            .append(SQL::table(TableRef::sql("posts", &["id", "name"])))
1297            .parens()
1298            .push(Token::AS)
1299            .append(SQL::table(TableRef::sql("post_rows", &[])));
1300        let query = SQL::<TestParam>::from(Token::SELECT)
1301            .push(Token::FROM)
1302            .append(SQL::table(TableRef::sql("users", &["id"])))
1303            .append(SQL::raw(" INNER JOIN LATERAL "))
1304            .append(source)
1305            .push(Token::ON)
1306            .append(SQL::raw("TRUE"));
1307
1308        assert_eq!(
1309            query.sql(),
1310            "SELECT `users`.`id`, `post_rows`.* FROM `users` INNER JOIN LATERAL (SELECT `posts`.`id`, `posts`.`name` FROM `posts`) AS `post_rows` ON TRUE"
1311        );
1312    }
1313}