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 SQL with raw text (unquoted)
75    #[inline]
76    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
77        Self {
78            chunks: smallvec::smallvec![SQLChunk::Raw(text.into())],
79        }
80    }
81
82    /// Creates SQL with a single unsigned integer literal.
83    #[inline]
84    #[must_use]
85    pub fn number(value: usize) -> Self {
86        Self {
87            chunks: smallvec::smallvec![SQLChunk::Number(value)],
88        }
89    }
90
91    /// Creates SQL with a single parameter value
92    #[inline]
93    pub fn param(value: impl Into<Cow<'a, V>>) -> Self {
94        Self {
95            chunks: smallvec::smallvec![SQLChunk::Param(Param {
96                value: Some(value.into()),
97                placeholder: Self::POSITIONAL_PLACEHOLDER,
98            })],
99        }
100    }
101
102    /// Creates SQL with a binary parameter value (BLOB/bytea)
103    ///
104    /// Prefer this over `SQL::param(Vec<u8>)` to avoid list semantics.
105    #[inline]
106    pub fn bytes(bytes: impl Into<Cow<'a, [u8]>>) -> Self
107    where
108        V: From<&'a [u8]> + From<Vec<u8>> + Into<Cow<'a, V>>,
109    {
110        match bytes.into() {
111            Cow::Borrowed(value) => Self::param(V::from(value)),
112            Cow::Owned(value) => Self::param(V::from(value)),
113        }
114    }
115
116    /// Creates SQL referencing a table
117    #[inline]
118    #[must_use]
119    pub fn table(table: TableRef) -> Self {
120        Self {
121            chunks: smallvec::smallvec![SQLChunk::Table(table)],
122        }
123    }
124
125    /// Creates SQL referencing a column
126    #[inline]
127    #[must_use]
128    pub fn column(column: ColumnRef) -> Self {
129        Self {
130            chunks: smallvec::smallvec![SQLChunk::Column(column)],
131        }
132    }
133
134    /// Creates SQL for a function call: NAME(args)
135    /// Subqueries are automatically wrapped in parentheses: NAME((SELECT ...))
136    #[inline]
137    pub fn func(name: &'static str, args: Self) -> Self {
138        let args = args.parens_if_subquery();
139        SQL::raw(name)
140            .push(Token::LPAREN)
141            .append(args)
142            .push(Token::RPAREN)
143    }
144
145    // ==================== builder methods ====================
146
147    /// Append another SQL fragment (flat extend)
148    #[inline]
149    #[must_use]
150    pub fn append(mut self, other: impl Into<Self>) -> Self {
151        #[cfg(feature = "profiling")]
152        profile_sql!("append");
153        let other = other.into();
154
155        if self.chunks.is_empty() {
156            return other;
157        }
158        if other.chunks.is_empty() {
159            return self;
160        }
161
162        self.chunks.extend(other.chunks);
163        self
164    }
165
166    #[inline]
167    pub fn append_mut(&mut self, other: impl Into<Self>) {
168        #[cfg(feature = "profiling")]
169        profile_sql!("append_mut");
170        let other = other.into();
171
172        if self.chunks.is_empty() {
173            self.chunks = other.chunks;
174            return;
175        }
176        if other.chunks.is_empty() {
177            return;
178        }
179
180        self.chunks.extend(other.chunks);
181    }
182
183    /// Push a single chunk
184    #[inline]
185    #[must_use]
186    pub fn push(mut self, chunk: impl Into<SQLChunk<'a, V>>) -> Self {
187        self.chunks.push(chunk.into());
188        self
189    }
190
191    #[inline]
192    pub fn push_mut(&mut self, chunk: impl Into<SQLChunk<'a, V>>) {
193        self.chunks.push(chunk.into());
194    }
195
196    /// Pre-allocates capacity for additional chunks
197    #[inline]
198    #[must_use]
199    pub fn with_capacity(mut self, additional: usize) -> Self {
200        self.chunks.reserve(additional);
201        self
202    }
203
204    // ==================== combinators ====================
205
206    /// Joins multiple SQL fragments with a separator
207    pub fn join<T>(sqls: T, separator: Token) -> Self
208    where
209        T: IntoIterator,
210        T::Item: ToSQL<'a, V>,
211    {
212        #[cfg(feature = "profiling")]
213        profile_sql!("join");
214
215        let mut iter = sqls.into_iter();
216        let Some(first) = iter.next() else {
217            return SQL::empty();
218        };
219
220        let mut result = first.into_sql();
221        let (lower, upper) = iter.size_hint();
222        if let Some(upper) = upper {
223            result.chunks.reserve(upper.saturating_mul(2));
224        } else if lower > 0 {
225            result.chunks.reserve(lower * 2);
226        }
227
228        for item in iter {
229            result.chunks.push(SQLChunk::Token(separator));
230            let other = item.into_sql();
231            if !other.chunks.is_empty() {
232                result.chunks.extend(other.chunks);
233            }
234        }
235        result
236    }
237
238    /// Wrap in parentheses: (self)
239    #[inline]
240    #[must_use]
241    pub fn parens(self) -> Self {
242        SQL::token(Token::LPAREN).append(self).push(Token::RPAREN)
243    }
244
245    /// Wrap this SQL fragment in parentheses only when it is a subquery.
246    #[inline]
247    #[must_use]
248    pub fn parens_if_subquery(self) -> Self {
249        if self.is_subquery() {
250            self.parens()
251        } else {
252            self
253        }
254    }
255
256    /// Check if this SQL fragment is a subquery (starts with SELECT/WITH).
257    #[inline]
258    pub fn is_subquery(&self) -> bool {
259        matches!(
260            self.chunks.first(),
261            Some(SQLChunk::Token(Token::SELECT | Token::WITH))
262        )
263    }
264
265    /// Creates an aliased version: self AS "name"
266    #[must_use]
267    pub fn alias(self, name: impl Into<Cow<'a, str>>) -> Self {
268        self.push(Token::AS).push(SQLChunk::Ident(name.into()))
269    }
270
271    /// Creates a comma-separated list of parameters.
272    /// Builds chunks directly without intermediate SQL allocations.
273    pub fn param_list<I>(values: I) -> Self
274    where
275        I: IntoIterator,
276        I::Item: Into<Cow<'a, V>>,
277    {
278        let iter = values.into_iter();
279        let (lower, _) = iter.size_hint();
280        let mut chunks = SmallVec::with_capacity(lower.saturating_mul(2));
281        for (i, v) in iter.enumerate() {
282            if i > 0 {
283                chunks.push(SQLChunk::Token(Token::COMMA));
284            }
285            chunks.push(SQLChunk::Param(Param {
286                value: Some(v.into()),
287                placeholder: Self::POSITIONAL_PLACEHOLDER,
288            }));
289        }
290        SQL { chunks }
291    }
292
293    /// Creates a comma-separated list of column assignments: "col" = ?
294    /// Builds chunks directly without intermediate SQL allocations.
295    pub fn assignments<I, T>(pairs: I) -> Self
296    where
297        I: IntoIterator<Item = (&'static str, T)>,
298        T: Into<Cow<'a, V>>,
299    {
300        let iter = pairs.into_iter();
301        let (lower, _) = iter.size_hint();
302        // Each assignment: Ident + EQ + Param = 3 chunks, plus commas
303        let mut chunks = SmallVec::with_capacity(lower.saturating_mul(4));
304        for (i, (col, val)) in iter.enumerate() {
305            if i > 0 {
306                chunks.push(SQLChunk::Token(Token::COMMA));
307            }
308            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
309            chunks.push(SQLChunk::Token(Token::EQ));
310            chunks.push(SQLChunk::Param(Param {
311                value: Some(val.into()),
312                placeholder: Self::POSITIONAL_PLACEHOLDER,
313            }));
314        }
315        SQL { chunks }
316    }
317
318    /// Creates a comma-separated list of column assignments from pre-built SQL fragments: "col" = <sql>
319    ///
320    /// Unlike `assignments()` which wraps each value in `SQL::param()`, this variant
321    /// accepts pre-built `SQL` fragments, preserving placeholders and raw expressions.
322    /// Builds chunks directly without intermediate SQL allocations.
323    pub fn assignments_sql<I>(pairs: I) -> Self
324    where
325        I: IntoIterator<Item = (&'static str, Self)>,
326    {
327        let iter = pairs.into_iter();
328        let (lower, _) = iter.size_hint();
329        let mut chunks = SmallVec::with_capacity(lower.saturating_mul(4));
330        for (i, (col, sql)) in iter.enumerate() {
331            if i > 0 {
332                chunks.push(SQLChunk::Token(Token::COMMA));
333            }
334            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
335            chunks.push(SQLChunk::Token(Token::EQ));
336            chunks.extend(sql.chunks);
337        }
338        SQL { chunks }
339    }
340
341    // ==================== output methods ====================
342
343    /// Maps parameter values from type `V` to type `U` using the provided function.
344    ///
345    /// Only `Param` chunks are affected; all other chunks pass through unchanged.
346    /// This is useful for converting between owned and borrowed value types
347    /// (e.g. `OwnedPostgresValue` → `PostgresValue<'a>`).
348    pub fn map_params<U: SQLParam>(self, mut f: impl FnMut(V) -> U) -> SQL<'a, U> {
349        let chunks = self
350            .chunks
351            .into_iter()
352            .map(|chunk| match chunk {
353                SQLChunk::Token(t) => SQLChunk::Token(t),
354                SQLChunk::Ident(s) => SQLChunk::Ident(s),
355                SQLChunk::Raw(s) => SQLChunk::Raw(s),
356                SQLChunk::Number(n) => SQLChunk::Number(n),
357                SQLChunk::Param(param) => SQLChunk::Param(Param::new(
358                    param.placeholder,
359                    param.value.map(|cow| Cow::Owned(f(cow.into_owned()))),
360                )),
361                SQLChunk::Table(t) => SQLChunk::Table(t),
362                SQLChunk::Column(c) => SQLChunk::Column(c),
363            })
364            .collect();
365        SQL { chunks }
366    }
367
368    /// Converts to owned version (consuming self to avoid clone)
369    pub fn into_owned(self) -> OwnedSQL<V> {
370        OwnedSQL::from(self)
371    }
372
373    /// Returns the SQL string with dialect-appropriate placeholders.
374    /// Uses `$1, $2, ...` for `PostgreSQL`, `:name` or `?` for `SQLite`, `?` for `MySQL`.
375    pub fn sql(&self) -> String {
376        #[cfg(feature = "profiling")]
377        profile_sql!("sql");
378        #[cfg(feature = "profiling")]
379        crate::drizzle_profile_scope!("sql_render", "sql.estimate");
380        let sql_cap = self.chunks.len().saturating_mul(8).max(128);
381        let mut buf = String::with_capacity(sql_cap);
382        self.write_to(&mut buf);
383        buf
384    }
385
386    /// Generates the SQL string and collects parameter references in a single pass.
387    ///
388    /// This is the preferred method for driver execution paths since it avoids
389    /// iterating the chunk list twice (once for `sql()`, once for `params()`).
390    pub fn build(&self) -> (String, SmallVec<[&V; 8]>) {
391        self.build_with(crate::dialect::ParamStyle::for_dialect(V::DIALECT))
392    }
393
394    /// Same as [`build`](Self::build) but lets the caller override the
395    /// placeholder style. Drivers that speak the dialect but bind parameters
396    /// differently (e.g. AWS Data API on Postgres) use this to emit
397    /// `:1, :2, ...` instead of `$1, $2, ...` without any post-hoc rewriting.
398    pub fn build_with(&self, style: crate::dialect::ParamStyle) -> (String, SmallVec<[&V; 8]>) {
399        use crate::dialect::Dialect;
400
401        #[cfg(feature = "profiling")]
402        crate::drizzle_profile_scope!("sql_render", "build");
403        #[cfg(feature = "profiling")]
404        crate::drizzle_profile_scope!("sql_render", "build.estimate");
405        let sql_cap = self.chunks.len().saturating_mul(8).max(128);
406        let param_cap = self.chunks.len().saturating_div(8).max(8);
407        let mut buf = String::with_capacity(sql_cap);
408        let mut params: SmallVec<[&V; 8]> = SmallVec::with_capacity(param_cap);
409        let mut param_index = 1usize;
410
411        #[cfg(feature = "profiling")]
412        crate::drizzle_profile_scope!("sql_render", "build.render");
413        for (i, chunk) in self.chunks.iter().enumerate() {
414            match chunk {
415                SQLChunk::Token(Token::SELECT) => {
416                    chunk.write(&mut buf);
417                    self.write_select_columns(&mut buf, i);
418                }
419                SQLChunk::Param(param) => {
420                    if let Some(name) = param.placeholder.name
421                        && V::DIALECT == Dialect::SQLite
422                    {
423                        let _ = buf.write_char(':');
424                        let _ = buf.write_str(name);
425                    } else {
426                        style.write(param_index, &mut buf);
427                    }
428                    param_index += 1;
429                    if let Some(value) = &param.value {
430                        params.push(value.as_ref());
431                    }
432                }
433                _ => chunk.write(&mut buf),
434            }
435
436            if self.needs_space(i) {
437                let _ = buf.write_char(' ');
438            }
439        }
440
441        (buf, params)
442    }
443
444    /// Write SQL to a buffer with dialect-appropriate placeholders.
445    /// Uses `$1, $2, ...` for `PostgreSQL`, `?` or `:name` for `SQLite`, `?` for `MySQL`.
446    pub fn write_to(&self, buf: &mut impl core::fmt::Write) {
447        self.write_to_with(buf, crate::dialect::ParamStyle::for_dialect(V::DIALECT));
448    }
449
450    /// Same as [`write_to`](Self::write_to) but with a caller-chosen
451    /// placeholder style.
452    pub fn write_to_with(
453        &self,
454        buf: &mut impl core::fmt::Write,
455        style: crate::dialect::ParamStyle,
456    ) {
457        use crate::dialect::Dialect;
458
459        #[cfg(feature = "profiling")]
460        crate::drizzle_profile_scope!("sql_render", "write_to");
461        let mut param_index = 1usize;
462        for (i, chunk) in self.chunks.iter().enumerate() {
463            match chunk {
464                SQLChunk::Token(Token::SELECT) => {
465                    chunk.write(buf);
466                    self.write_select_columns(buf, i);
467                }
468                SQLChunk::Param(param) => {
469                    if let Some(name) = param.placeholder.name
470                        && V::DIALECT == Dialect::SQLite
471                    {
472                        let _ = buf.write_char(':');
473                        let _ = buf.write_str(name);
474                    } else {
475                        style.write(param_index, buf);
476                    }
477                    param_index += 1;
478                }
479                _ => chunk.write(buf),
480            }
481
482            if self.needs_space(i) {
483                let _ = buf.write_char(' ');
484            }
485        }
486    }
487
488    /// Write a single chunk with pattern detection
489    pub fn write_chunk_to(
490        &self,
491        buf: &mut impl core::fmt::Write,
492        chunk: &SQLChunk<'a, V>,
493        index: usize,
494    ) {
495        match chunk {
496            SQLChunk::Token(Token::SELECT) => {
497                chunk.write(buf);
498                self.write_select_columns(buf, index);
499            }
500            _ => chunk.write(buf),
501        }
502    }
503
504    /// Write appropriate columns for SELECT statement
505    pub(crate) fn write_select_columns(
506        &self,
507        buf: &mut impl core::fmt::Write,
508        select_index: usize,
509    ) {
510        let chunks = self.chunks.get(select_index + 1..select_index + 3);
511        match chunks {
512            Some([SQLChunk::Token(Token::FROM), SQLChunk::Table(table)]) => {
513                let _ = buf.write_char(' ');
514                Self::write_qualified_columns(buf, table);
515            }
516            Some([SQLChunk::Token(Token::FROM), _]) => {
517                let _ = buf.write_char(' ');
518                let _ = buf.write_str(Token::STAR.as_str());
519            }
520            _ => {}
521        }
522    }
523
524    /// Write fully qualified columns for a table
525    pub fn write_qualified_columns(buf: &mut impl core::fmt::Write, table: &TableRef) {
526        if table.column_names.is_empty() {
527            let _ = buf.write_char('*');
528            return;
529        }
530
531        for (i, col_name) in table.column_names.iter().enumerate() {
532            if i > 0 {
533                let _ = buf.write_str(", ");
534            }
535            chunk::write_quoted_ident(buf, table.name);
536            let _ = buf.write_char('.');
537            chunk::write_quoted_ident(buf, col_name);
538        }
539    }
540
541    /// Simplified spacing logic
542    fn needs_space(&self, index: usize) -> bool {
543        let Some(next) = self.chunks.get(index + 1) else {
544            return false;
545        };
546
547        let current = &self.chunks[index];
548        chunk_needs_space(current, next)
549    }
550
551    /// Returns an iterator over references to parameter values
552    /// (avoids allocating a Vec - callers can collect if needed)
553    pub fn params(&self) -> impl Iterator<Item = &V> + use<'_, V> {
554        self.chunks.iter().filter_map(|chunk| {
555            if let SQLChunk::Param(Param {
556                value: Some(value), ..
557            }) = chunk
558            {
559                Some(value.as_ref())
560            } else {
561                None
562            }
563        })
564    }
565
566    /// Bind named parameters
567    #[must_use]
568    pub fn bind<T: SQLParam + Into<V>>(
569        self,
570        params: impl IntoIterator<Item: Into<ParamBind<'a, T>>>,
571    ) -> Self {
572        #[cfg(feature = "profiling")]
573        profile_sql!("bind");
574
575        let param_map: HashMap<&str, V> = params
576            .into_iter()
577            .map(Into::into)
578            .map(|p| (p.name, p.value.into()))
579            .collect();
580
581        let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
582            .chunks
583            .into_iter()
584            .map(|chunk| match chunk {
585                SQLChunk::Param(mut param) => {
586                    if let Some(name) = param.placeholder.name
587                        && let Some(value) = param_map.get(name)
588                    {
589                        param.value = Some(Cow::Owned(value.clone()));
590                    }
591                    SQLChunk::Param(param)
592                }
593                other => other,
594            })
595            .collect();
596
597        SQL {
598            chunks: bound_chunks,
599        }
600    }
601}
602
603/// Canonical spacing logic for SQL chunk rendering.
604/// Used by both `SQL::write_to()` and `prepare_render()`.
605pub(crate) fn chunk_needs_space<V: SQLParam>(
606    current: &SQLChunk<'_, V>,
607    next: &SQLChunk<'_, V>,
608) -> bool {
609    // No space if current raw text ends with space
610    if let SQLChunk::Raw(text) = current
611        && text.ends_with(' ')
612    {
613        return false;
614    }
615
616    // No space if next raw text starts with space
617    if let SQLChunk::Raw(text) = next
618        && text.starts_with(' ')
619    {
620        return false;
621    }
622
623    match (current, next) {
624        // No space before closing/separator punctuation
625        // or after opening punctuation
626        (_, SQLChunk::Token(Token::RPAREN | Token::COMMA | Token::SEMI | Token::DOT))
627        | (SQLChunk::Token(Token::LPAREN | Token::DOT), _) => false,
628        // Space after comma
629        (SQLChunk::Token(Token::COMMA), _) => true,
630        // Space after closing paren if next is word-like (e.g., ") FROM")
631        (SQLChunk::Token(Token::RPAREN), next) => next.is_word_like(),
632        // Space before opening paren if preceded by word-like (e.g., "AS (")
633        (current, SQLChunk::Token(Token::LPAREN)) => current.is_word_like(),
634        // Space around comparison/arithmetic operators
635        (SQLChunk::Token(t), _) if t.is_operator() => true,
636        (_, SQLChunk::Token(t)) if t.is_operator() => true,
637        // Space between all word-like chunks
638        _ => current.is_word_like() && next.is_word_like(),
639    }
640}
641
642// ==================== trait implementations ====================
643
644impl<V: SQLParam> Default for SQL<'_, V> {
645    fn default() -> Self {
646        Self::empty()
647    }
648}
649
650impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
651    fn from(s: &'a str) -> Self {
652        SQL::raw(s)
653    }
654}
655
656impl<V: SQLParam> From<Token> for SQL<'_, V> {
657    fn from(value: Token) -> Self {
658        SQL::token(value)
659    }
660}
661
662impl<'a, V: SQLParam + 'a> AsRef<Self> for SQL<'a, V> {
663    fn as_ref(&self) -> &Self {
664        self
665    }
666}
667
668impl<V: SQLParam + core::fmt::Display> Display for SQL<'_, V> {
669    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
670        // Collect params for Debug formatting (iterator can't be used with :?)
671        let params: Vec<_> = self.params().collect();
672        write!(f, r#"sql: "{}", params: {:?}"#, self.sql(), params)
673    }
674}
675
676impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
677    fn to_sql(&self) -> Self {
678        self.clone()
679    }
680
681    fn into_sql(self) -> Self {
682        self
683    }
684}
685
686impl<'a, V: SQLParam, T> FromIterator<T> for SQL<'a, V>
687where
688    SQLChunk<'a, V>: From<T>,
689{
690    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
691        let chunks = iter
692            .into_iter()
693            .map(SQLChunk::from)
694            .collect::<SmallVec<_>>();
695        Self { chunks }
696    }
697}
698
699impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
700    type Item = SQLChunk<'a, V>;
701    type IntoIter = smallvec::IntoIter<[SQLChunk<'a, V>; 8]>;
702
703    fn into_iter(self) -> Self::IntoIter {
704        self.chunks.into_iter()
705    }
706}