Skip to main content

drizzle_core/sql/
mod.rs

1mod chunk;
2mod cte;
3mod owned;
4mod tokens;
5
6use crate::prelude::*;
7use crate::{
8    dialect::DialectExt,
9    param::{Param, ParamBind},
10    placeholder::Placeholder,
11    traits::{SQLColumnInfo, SQLParam, SQLTableInfo, ToSQL},
12};
13pub use chunk::*;
14use core::fmt::Display;
15pub use owned::*;
16use smallvec::SmallVec;
17pub use tokens::*;
18
19#[cfg(feature = "profiling")]
20use crate::profile_sql;
21
22/// SQL fragment builder with flat chunk storage.
23///
24/// Uses `SmallVec<[SQLChunk; 8]>` for inline storage of typical SQL fragments
25/// without heap allocation.
26#[derive(Debug, Clone)]
27pub struct SQL<'a, V: SQLParam> {
28    pub chunks: SmallVec<[SQLChunk<'a, V>; 8]>,
29}
30
31impl<'a, V: SQLParam> SQL<'a, V> {
32    const POSITIONAL_PLACEHOLDER: Placeholder = Placeholder::anonymous();
33
34    // ==================== constructors ====================
35
36    /// Creates an empty SQL fragment
37    #[inline]
38    pub const fn empty() -> Self {
39        Self {
40            chunks: SmallVec::new_const(),
41        }
42    }
43
44    // ==================== constructors ====================
45
46    /// Creates SQL with a single token
47    #[inline]
48    pub fn token(t: Token) -> Self {
49        Self {
50            chunks: smallvec::smallvec![SQLChunk::Token(t)],
51        }
52    }
53
54    /// Creates an empty SQL fragment with pre-allocated chunk capacity.
55    #[inline]
56    pub fn with_capacity_chunks(capacity: usize) -> Self {
57        Self {
58            chunks: SmallVec::with_capacity(capacity),
59        }
60    }
61
62    /// Creates SQL with a quoted identifier
63    #[inline]
64    pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
65        Self {
66            chunks: smallvec::smallvec![SQLChunk::Ident(name.into())],
67        }
68    }
69
70    /// Creates SQL with raw text (unquoted)
71    #[inline]
72    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
73        Self {
74            chunks: smallvec::smallvec![SQLChunk::Raw(text.into())],
75        }
76    }
77
78    /// Creates SQL with a single parameter value
79    #[inline]
80    pub fn param(value: impl Into<Cow<'a, V>>) -> Self {
81        Self {
82            chunks: smallvec::smallvec![SQLChunk::Param(Param {
83                value: Some(value.into()),
84                placeholder: Self::POSITIONAL_PLACEHOLDER,
85            })],
86        }
87    }
88
89    /// Creates SQL with a binary parameter value (BLOB/bytea)
90    ///
91    /// Prefer this over `SQL::param(Vec<u8>)` to avoid list semantics.
92    #[inline]
93    pub fn bytes(bytes: impl Into<Cow<'a, [u8]>>) -> Self
94    where
95        V: From<&'a [u8]>,
96        V: From<Vec<u8>>,
97        V: Into<Cow<'a, V>>,
98    {
99        match bytes.into() {
100            Cow::Borrowed(value) => Self::param(V::from(value)),
101            Cow::Owned(value) => Self::param(V::from(value)),
102        }
103    }
104
105    /// Creates SQL with a named placeholder (no value, for prepared statements)
106    #[inline]
107    pub fn placeholder(name: &'static str) -> Self {
108        Self {
109            chunks: smallvec::smallvec![SQLChunk::Param(Param {
110                value: None,
111                placeholder: Placeholder::named(name),
112            })],
113        }
114    }
115
116    /// Creates SQL referencing a table
117    #[inline]
118    pub fn table(table: &'static dyn SQLTableInfo) -> Self {
119        Self {
120            chunks: smallvec::smallvec![SQLChunk::Table(table)],
121        }
122    }
123
124    /// Creates SQL referencing a column
125    #[inline]
126    pub fn column(column: &'static dyn SQLColumnInfo) -> Self {
127        Self {
128            chunks: smallvec::smallvec![SQLChunk::Column(column)],
129        }
130    }
131
132    /// Creates SQL for a function call: NAME(args)
133    /// Subqueries are automatically wrapped in parentheses: NAME((SELECT ...))
134    #[inline]
135    pub fn func(name: &'static str, args: SQL<'a, V>) -> Self {
136        let args = if args.is_subquery() {
137            args.parens()
138        } else {
139            args
140        };
141        SQL::raw(name)
142            .push(Token::LPAREN)
143            .append(args)
144            .push(Token::RPAREN)
145    }
146
147    // ==================== builder methods ====================
148
149    /// Append another SQL fragment (flat extend)
150    #[inline]
151    pub fn append(mut self, other: impl Into<SQL<'a, V>>) -> Self {
152        #[cfg(feature = "profiling")]
153        profile_sql!("append");
154        let other = other.into();
155        if !other.chunks.is_empty() {
156            self.chunks.reserve(other.chunks.len());
157            self.chunks.extend(other.chunks);
158        }
159        self
160    }
161
162    /// Push a single chunk
163    #[inline]
164    pub fn push(mut self, chunk: impl Into<SQLChunk<'a, V>>) -> Self {
165        self.chunks.push(chunk.into());
166        self
167    }
168
169    /// Pre-allocates capacity for additional chunks
170    #[inline]
171    pub fn with_capacity(mut self, additional: usize) -> Self {
172        self.chunks.reserve(additional);
173        self
174    }
175
176    // ==================== combinators ====================
177
178    /// Joins multiple SQL fragments with a separator
179    pub fn join<T>(sqls: T, separator: Token) -> SQL<'a, V>
180    where
181        T: IntoIterator,
182        T::Item: ToSQL<'a, V>,
183    {
184        #[cfg(feature = "profiling")]
185        profile_sql!("join");
186
187        let mut iter = sqls.into_iter();
188        let Some(first) = iter.next() else {
189            return SQL::empty();
190        };
191
192        let mut result = first.into_sql();
193        let (lower, _) = iter.size_hint();
194        if lower > 0 {
195            // Reserve at least space for separators and minimal chunk growth.
196            result.chunks.reserve(lower * 2);
197        }
198        for item in iter {
199            result = result.push(separator).append(item.into_sql());
200        }
201        result
202    }
203
204    /// Wrap in parentheses: (self)
205    #[inline]
206    pub fn parens(self) -> Self {
207        SQL::token(Token::LPAREN).append(self).push(Token::RPAREN)
208    }
209
210    /// Check if this SQL fragment is a subquery (starts with SELECT)
211    #[inline]
212    pub fn is_subquery(&self) -> bool {
213        matches!(self.chunks.first(), Some(SQLChunk::Token(Token::SELECT)))
214    }
215
216    /// Creates an aliased version: self AS "name"
217    pub fn alias(self, name: impl Into<Cow<'a, str>>) -> SQL<'a, V> {
218        self.push(Token::AS).push(SQLChunk::Ident(name.into()))
219    }
220
221    /// Creates a comma-separated list of parameters.
222    /// Builds chunks directly without intermediate SQL allocations.
223    pub fn param_list<I>(values: I) -> Self
224    where
225        I: IntoIterator,
226        I::Item: Into<Cow<'a, V>>,
227    {
228        let iter = values.into_iter();
229        let (lower, _) = iter.size_hint();
230        let mut chunks = SmallVec::with_capacity(lower.saturating_mul(2));
231        for (i, v) in iter.enumerate() {
232            if i > 0 {
233                chunks.push(SQLChunk::Token(Token::COMMA));
234            }
235            chunks.push(SQLChunk::Param(Param {
236                value: Some(v.into()),
237                placeholder: Self::POSITIONAL_PLACEHOLDER,
238            }));
239        }
240        SQL { chunks }
241    }
242
243    /// Creates a comma-separated list of column assignments: "col" = ?
244    /// Builds chunks directly without intermediate SQL allocations.
245    pub fn assignments<I, T>(pairs: I) -> Self
246    where
247        I: IntoIterator<Item = (&'static str, T)>,
248        T: Into<Cow<'a, V>>,
249    {
250        let iter = pairs.into_iter();
251        let (lower, _) = iter.size_hint();
252        // Each assignment: Ident + EQ + Param = 3 chunks, plus commas
253        let mut chunks = SmallVec::with_capacity(lower.saturating_mul(4));
254        for (i, (col, val)) in iter.enumerate() {
255            if i > 0 {
256                chunks.push(SQLChunk::Token(Token::COMMA));
257            }
258            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
259            chunks.push(SQLChunk::Token(Token::EQ));
260            chunks.push(SQLChunk::Param(Param {
261                value: Some(val.into()),
262                placeholder: Self::POSITIONAL_PLACEHOLDER,
263            }));
264        }
265        SQL { chunks }
266    }
267
268    /// Creates a comma-separated list of column assignments from pre-built SQL fragments: "col" = <sql>
269    ///
270    /// Unlike `assignments()` which wraps each value in `SQL::param()`, this variant
271    /// accepts pre-built `SQL` fragments, preserving placeholders and raw expressions.
272    /// Builds chunks directly without intermediate SQL allocations.
273    pub fn assignments_sql<I>(pairs: I) -> Self
274    where
275        I: IntoIterator<Item = (&'static str, SQL<'a, V>)>,
276    {
277        let iter = pairs.into_iter();
278        let (lower, _) = iter.size_hint();
279        let mut chunks = SmallVec::with_capacity(lower.saturating_mul(4));
280        for (i, (col, sql)) in iter.enumerate() {
281            if i > 0 {
282                chunks.push(SQLChunk::Token(Token::COMMA));
283            }
284            chunks.push(SQLChunk::Ident(Cow::Borrowed(col)));
285            chunks.push(SQLChunk::Token(Token::EQ));
286            chunks.extend(sql.chunks);
287        }
288        SQL { chunks }
289    }
290
291    // ==================== output methods ====================
292
293    /// Converts to owned version (consuming self to avoid clone)
294    pub fn into_owned(self) -> OwnedSQL<V> {
295        OwnedSQL::from(self)
296    }
297
298    /// Returns the SQL string with dialect-appropriate placeholders
299    /// Uses `$1, $2, ...` for PostgreSQL, `:name` or `?` for SQLite, `?` for MySQL
300    pub fn sql(&self) -> String {
301        #[cfg(feature = "profiling")]
302        profile_sql!("sql");
303        let capacity = self.estimate_capacity();
304        let mut buf = String::with_capacity(capacity);
305        self.write_to(&mut buf);
306        buf
307    }
308
309    /// Write SQL to a buffer with dialect-appropriate placeholders
310    /// Uses `$1, $2, ...` for PostgreSQL, `?` or `:name` for SQLite, `?` for MySQL
311    /// Named placeholders use `:name` syntax only for SQLite; PostgreSQL always uses `$N`
312    pub fn write_to(&self, buf: &mut impl core::fmt::Write) {
313        use crate::dialect::Dialect;
314        let mut param_index = 1usize;
315        for (i, chunk) in self.chunks.iter().enumerate() {
316            match chunk {
317                SQLChunk::Token(Token::SELECT) => {
318                    chunk.write(buf);
319                    self.write_select_columns(buf, i);
320                }
321                SQLChunk::Param(param) => {
322                    // Named placeholders use :name syntax only for SQLite
323                    // PostgreSQL always uses $N, MySQL always uses ?
324                    if let Some(name) = param.placeholder.name
325                        && V::DIALECT == Dialect::SQLite
326                    {
327                        let _ = buf.write_char(':');
328                        let _ = buf.write_str(name);
329                    } else {
330                        let _ = buf.write_str(&V::DIALECT.render_placeholder(param_index));
331                    }
332                    param_index += 1;
333                }
334                _ => chunk.write(buf),
335            }
336
337            if self.needs_space(i) {
338                let _ = buf.write_char(' ');
339            }
340        }
341    }
342
343    /// Write a single chunk with pattern detection
344    pub fn write_chunk_to(
345        &self,
346        buf: &mut impl core::fmt::Write,
347        chunk: &SQLChunk<'a, V>,
348        index: usize,
349    ) {
350        match chunk {
351            SQLChunk::Token(Token::SELECT) => {
352                chunk.write(buf);
353                self.write_select_columns(buf, index);
354            }
355            _ => chunk.write(buf),
356        }
357    }
358
359    /// Write appropriate columns for SELECT statement
360    fn write_select_columns(&self, buf: &mut impl core::fmt::Write, select_index: usize) {
361        let chunks = self.chunks.get(select_index + 1..select_index + 3);
362        match chunks {
363            Some([SQLChunk::Token(Token::FROM), SQLChunk::Table(table)]) => {
364                let _ = buf.write_char(' ');
365                self.write_qualified_columns(buf, *table);
366            }
367            Some([SQLChunk::Token(Token::FROM), _]) => {
368                let _ = buf.write_char(' ');
369                let _ = buf.write_str(Token::STAR.as_str());
370            }
371            _ => {}
372        }
373    }
374
375    /// Write fully qualified columns
376    pub fn write_qualified_columns(
377        &self,
378        buf: &mut impl core::fmt::Write,
379        table: &dyn SQLTableInfo,
380    ) {
381        let columns = table.columns();
382        if columns.is_empty() {
383            let _ = buf.write_char('*');
384            return;
385        }
386
387        for (i, col) in columns.iter().enumerate() {
388            if i > 0 {
389                let _ = buf.write_str(", ");
390            }
391            let _ = buf.write_char('"');
392            let _ = buf.write_str(table.name());
393            let _ = buf.write_str("\".\"");
394            let _ = buf.write_str(col.name());
395            let _ = buf.write_char('"');
396        }
397    }
398
399    fn estimate_capacity(&self) -> usize {
400        const PLACEHOLDER_SIZE: usize = 2;
401        const IDENT_OVERHEAD: usize = 2;
402        const COLUMN_OVERHEAD: usize = 5;
403
404        self.chunks
405            .iter()
406            .map(|chunk| match chunk {
407                SQLChunk::Token(t) => t.as_str().len(),
408                SQLChunk::Ident(s) => s.len() + IDENT_OVERHEAD,
409                SQLChunk::Raw(s) => s.len(),
410                SQLChunk::Param { .. } => PLACEHOLDER_SIZE,
411                SQLChunk::Table(t) => t.name().len() + IDENT_OVERHEAD,
412                SQLChunk::Column(c) => c.table().name().len() + c.name().len() + COLUMN_OVERHEAD,
413            })
414            .sum::<usize>()
415            + self.chunks.len()
416    }
417
418    /// Simplified spacing logic
419    fn needs_space(&self, index: usize) -> bool {
420        let Some(next) = self.chunks.get(index + 1) else {
421            return false;
422        };
423
424        let current = &self.chunks[index];
425        chunk_needs_space(current, next)
426    }
427
428    /// Returns an iterator over references to parameter values
429    /// (avoids allocating a Vec - callers can collect if needed)
430    pub fn params(&self) -> impl Iterator<Item = &V> {
431        self.chunks.iter().filter_map(|chunk| {
432            if let SQLChunk::Param(Param {
433                value: Some(value), ..
434            }) = chunk
435            {
436                Some(value.as_ref())
437            } else {
438                None
439            }
440        })
441    }
442
443    /// Bind named parameters
444    pub fn bind<T: SQLParam + Into<V>>(
445        self,
446        params: impl IntoIterator<Item: Into<ParamBind<'a, T>>>,
447    ) -> SQL<'a, V> {
448        #[cfg(feature = "profiling")]
449        profile_sql!("bind");
450
451        let param_map: HashMap<&str, V> = params
452            .into_iter()
453            .map(Into::into)
454            .map(|p| (p.name, p.value.into()))
455            .collect();
456
457        let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
458            .chunks
459            .into_iter()
460            .map(|chunk| match chunk {
461                SQLChunk::Param(mut param) => {
462                    if let Some(name) = param.placeholder.name
463                        && let Some(value) = param_map.get(name)
464                    {
465                        param.value = Some(Cow::Owned(value.clone()));
466                    }
467                    SQLChunk::Param(param)
468                }
469                other => other,
470            })
471            .collect();
472
473        SQL {
474            chunks: bound_chunks,
475        }
476    }
477}
478
479/// Canonical spacing logic for SQL chunk rendering.
480/// Used by both `SQL::write_to()` and `prepare_render()`.
481pub(crate) fn chunk_needs_space<V: SQLParam>(
482    current: &SQLChunk<'_, V>,
483    next: &SQLChunk<'_, V>,
484) -> bool {
485    // No space if current raw text ends with space
486    if let SQLChunk::Raw(text) = current
487        && text.ends_with(' ')
488    {
489        return false;
490    }
491
492    // No space if next raw text starts with space
493    if let SQLChunk::Raw(text) = next
494        && text.starts_with(' ')
495    {
496        return false;
497    }
498
499    match (current, next) {
500        // No space before closing/separator punctuation
501        (_, SQLChunk::Token(Token::RPAREN | Token::COMMA | Token::SEMI | Token::DOT)) => false,
502        // No space after opening punctuation
503        (SQLChunk::Token(Token::LPAREN | Token::DOT), _) => false,
504        // Space after comma
505        (SQLChunk::Token(Token::COMMA), _) => true,
506        // Space after closing paren if next is word-like (e.g., ") FROM")
507        (SQLChunk::Token(Token::RPAREN), next) => next.is_word_like(),
508        // Space before opening paren if preceded by word-like (e.g., "AS (")
509        (current, SQLChunk::Token(Token::LPAREN)) => current.is_word_like(),
510        // Space around comparison/arithmetic operators
511        (SQLChunk::Token(t), _) if t.is_operator() => true,
512        (_, SQLChunk::Token(t)) if t.is_operator() => true,
513        // Space between all word-like chunks
514        _ => current.is_word_like() && next.is_word_like(),
515    }
516}
517
518// ==================== trait implementations ====================
519
520impl<'a, V: SQLParam> Default for SQL<'a, V> {
521    fn default() -> Self {
522        Self::empty()
523    }
524}
525
526impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
527    fn from(s: &'a str) -> Self {
528        SQL::raw(s)
529    }
530}
531
532impl<'a, V: SQLParam> From<Token> for SQL<'a, V> {
533    fn from(value: Token) -> Self {
534        SQL::token(value)
535    }
536}
537
538impl<'a, V: SQLParam + 'a> AsRef<SQL<'a, V>> for SQL<'a, V> {
539    fn as_ref(&self) -> &SQL<'a, V> {
540        self
541    }
542}
543
544impl<'a, V: SQLParam + core::fmt::Display> Display for SQL<'a, V> {
545    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
546        // Collect params for Debug formatting (iterator can't be used with :?)
547        let params: Vec<_> = self.params().collect();
548        write!(f, r#"sql: "{}", params: {:?}"#, self.sql(), params)
549    }
550}
551
552impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
553    fn to_sql(&self) -> SQL<'a, V> {
554        self.clone()
555    }
556
557    fn into_sql(self) -> SQL<'a, V> {
558        self
559    }
560}
561
562impl<'a, V: SQLParam, T> FromIterator<T> for SQL<'a, V>
563where
564    SQLChunk<'a, V>: From<T>,
565{
566    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
567        let chunks = SmallVec::from_iter(iter.into_iter().map(SQLChunk::from));
568        Self { chunks }
569    }
570}
571
572impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
573    type Item = SQLChunk<'a, V>;
574    type IntoIter = smallvec::IntoIter<[SQLChunk<'a, V>; 8]>;
575
576    fn into_iter(self) -> Self::IntoIter {
577        self.chunks.into_iter()
578    }
579}