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::positional();
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::colon(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 mut other = other.into();
155        if !other.chunks.is_empty() {
156            self.chunks.reserve(other.chunks.len());
157            self.chunks.extend(other.chunks.drain(..));
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.to_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.to_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    pub fn param_list<I>(values: I) -> Self
223    where
224        I: IntoIterator,
225        I::Item: Into<Cow<'a, V>>,
226    {
227        Self::join(values.into_iter().map(Self::param), Token::COMMA)
228    }
229
230    /// Creates a comma-separated list of column assignments: "col" = ?
231    pub fn assignments<I, T>(pairs: I) -> Self
232    where
233        I: IntoIterator<Item = (&'static str, T)>,
234        T: Into<Cow<'a, V>>,
235    {
236        Self::join(
237            pairs
238                .into_iter()
239                .map(|(col, val)| SQL::ident(col).push(Token::EQ).append(SQL::param(val))),
240            Token::COMMA,
241        )
242    }
243
244    // ==================== output methods ====================
245
246    /// Converts to owned version (consuming self to avoid clone)
247    pub fn into_owned(self) -> OwnedSQL<V> {
248        OwnedSQL::from(self)
249    }
250
251    /// Returns the SQL string with dialect-appropriate placeholders
252    /// Uses `$1, $2, ...` for PostgreSQL, `?` for SQLite/MySQL
253    pub fn sql(&self) -> String {
254        #[cfg(feature = "profiling")]
255        profile_sql!("sql");
256        let capacity = self.estimate_capacity();
257        let mut buf = String::with_capacity(capacity);
258        self.write_to(&mut buf);
259        buf
260    }
261
262    /// Write SQL to a buffer with dialect-appropriate placeholders
263    /// Uses `$1, $2, ...` for PostgreSQL, `?` or `:name` for SQLite, `?` for MySQL
264    /// Named placeholders use `:name` syntax only for SQLite; PostgreSQL always uses `$N`
265    pub fn write_to(&self, buf: &mut impl core::fmt::Write) {
266        use crate::dialect::Dialect;
267        let mut param_index = 1usize;
268        for (i, chunk) in self.chunks.iter().enumerate() {
269            match chunk {
270                SQLChunk::Token(Token::SELECT) => {
271                    chunk.write(buf);
272                    self.write_select_columns(buf, i);
273                }
274                SQLChunk::Param(param) => {
275                    // Named placeholders use :name syntax only for SQLite
276                    // PostgreSQL always uses $N, MySQL always uses ?
277                    if let Some(name) = param.placeholder.name
278                        && V::DIALECT == Dialect::SQLite
279                    {
280                        let _ = buf.write_char(':');
281                        let _ = buf.write_str(name);
282                    } else {
283                        let _ = buf.write_str(&V::DIALECT.render_placeholder(param_index));
284                    }
285                    param_index += 1;
286                }
287                _ => chunk.write(buf),
288            }
289
290            if self.needs_space(i) {
291                let _ = buf.write_char(' ');
292            }
293        }
294    }
295
296    /// Write a single chunk with pattern detection
297    pub fn write_chunk_to(
298        &self,
299        buf: &mut impl core::fmt::Write,
300        chunk: &SQLChunk<'a, V>,
301        index: usize,
302    ) {
303        match chunk {
304            SQLChunk::Token(Token::SELECT) => {
305                chunk.write(buf);
306                self.write_select_columns(buf, index);
307            }
308            _ => chunk.write(buf),
309        }
310    }
311
312    /// Write appropriate columns for SELECT statement
313    fn write_select_columns(&self, buf: &mut impl core::fmt::Write, select_index: usize) {
314        let chunks = self.chunks.get(select_index + 1..select_index + 3);
315        match chunks {
316            Some([SQLChunk::Token(Token::FROM), SQLChunk::Table(table)]) => {
317                let _ = buf.write_char(' ');
318                self.write_qualified_columns(buf, *table);
319            }
320            Some([SQLChunk::Token(Token::FROM), _]) => {
321                let _ = buf.write_char(' ');
322                let _ = buf.write_str(Token::STAR.as_str());
323            }
324            _ => {}
325        }
326    }
327
328    /// Write fully qualified columns
329    pub fn write_qualified_columns(
330        &self,
331        buf: &mut impl core::fmt::Write,
332        table: &dyn SQLTableInfo,
333    ) {
334        let columns = table.columns();
335        if columns.is_empty() {
336            let _ = buf.write_char('*');
337            return;
338        }
339
340        for (i, col) in columns.iter().enumerate() {
341            if i > 0 {
342                let _ = buf.write_str(", ");
343            }
344            let _ = buf.write_char('"');
345            let _ = buf.write_str(table.name());
346            let _ = buf.write_str("\".\"");
347            let _ = buf.write_str(col.name());
348            let _ = buf.write_char('"');
349        }
350    }
351
352    fn estimate_capacity(&self) -> usize {
353        const PLACEHOLDER_SIZE: usize = 2;
354        const IDENT_OVERHEAD: usize = 2;
355        const COLUMN_OVERHEAD: usize = 5;
356        const ALIAS_OVERHEAD: usize = 6;
357
358        self.chunks
359            .iter()
360            .map(|chunk| match chunk {
361                SQLChunk::Token(t) => t.as_str().len(),
362                SQLChunk::Ident(s) => s.len() + IDENT_OVERHEAD,
363                SQLChunk::Raw(s) => s.len(),
364                SQLChunk::Param { .. } => PLACEHOLDER_SIZE,
365                SQLChunk::Table(t) => t.name().len() + IDENT_OVERHEAD,
366                SQLChunk::Column(c) => c.table().name().len() + c.name().len() + COLUMN_OVERHEAD,
367                SQLChunk::Alias { inner, alias } => {
368                    alias.len()
369                        + ALIAS_OVERHEAD
370                        + match inner.as_ref() {
371                            SQLChunk::Ident(s) => s.len() + IDENT_OVERHEAD,
372                            SQLChunk::Raw(s) => s.len(),
373                            _ => 10,
374                        }
375                }
376            })
377            .sum::<usize>()
378            + self.chunks.len()
379    }
380
381    /// Simplified spacing logic
382    fn needs_space(&self, index: usize) -> bool {
383        let Some(next) = self.chunks.get(index + 1) else {
384            return false;
385        };
386
387        let current = &self.chunks[index];
388        chunk_needs_space(current, next)
389    }
390
391    /// Returns an iterator over references to parameter values
392    /// (avoids allocating a Vec - callers can collect if needed)
393    pub fn params(&self) -> impl Iterator<Item = &V> {
394        self.chunks.iter().filter_map(|chunk| {
395            if let SQLChunk::Param(Param {
396                value: Some(value), ..
397            }) = chunk
398            {
399                Some(value.as_ref())
400            } else {
401                None
402            }
403        })
404    }
405
406    /// Bind named parameters
407    pub fn bind<T: SQLParam + Into<V>>(
408        self,
409        params: impl IntoIterator<Item: Into<ParamBind<'a, T>>>,
410    ) -> SQL<'a, V> {
411        #[cfg(feature = "profiling")]
412        profile_sql!("bind");
413
414        let param_map: HashMap<&str, V> = params
415            .into_iter()
416            .map(Into::into)
417            .map(|p| (p.name, p.value.into()))
418            .collect();
419
420        let bound_chunks: SmallVec<[SQLChunk<'a, V>; 8]> = self
421            .chunks
422            .into_iter()
423            .map(|chunk| match chunk {
424                SQLChunk::Param(mut param) => {
425                    if let Some(name) = param.placeholder.name
426                        && let Some(value) = param_map.get(name)
427                    {
428                        param.value = Some(Cow::Owned(value.clone()));
429                    }
430                    SQLChunk::Param(param)
431                }
432                other => other,
433            })
434            .collect();
435
436        SQL {
437            chunks: bound_chunks,
438        }
439    }
440}
441
442/// Simplified spacing logic
443fn chunk_needs_space<V: SQLParam>(current: &SQLChunk<'_, V>, next: &SQLChunk<'_, V>) -> bool {
444    // No space if current raw text ends with space
445    if let SQLChunk::Raw(text) = current
446        && text.ends_with(' ')
447    {
448        return false;
449    }
450
451    // No space if next raw text starts with space
452    if let SQLChunk::Raw(text) = next
453        && text.starts_with(' ')
454    {
455        return false;
456    }
457
458    match (current, next) {
459        // No space before closing/separator punctuation
460        (_, SQLChunk::Token(Token::RPAREN | Token::COMMA | Token::SEMI | Token::DOT)) => false,
461        // No space after opening punctuation
462        (SQLChunk::Token(Token::LPAREN | Token::DOT), _) => false,
463        // Space after comma
464        (SQLChunk::Token(Token::COMMA), _) => true,
465        // Space after closing paren if next is word-like (e.g., ") FROM")
466        (SQLChunk::Token(Token::RPAREN), next) => next.is_word_like(),
467        // Space before opening paren if preceded by word-like (e.g., "AS (")
468        (current, SQLChunk::Token(Token::LPAREN)) => current.is_word_like(),
469        // Space around comparison/arithmetic operators
470        (SQLChunk::Token(t), _) if t.is_operator() => true,
471        (_, SQLChunk::Token(t)) if t.is_operator() => true,
472        // Space between all word-like chunks
473        _ => current.is_word_like() && next.is_word_like(),
474    }
475}
476
477// ==================== trait implementations ====================
478
479impl<'a, V: SQLParam> Default for SQL<'a, V> {
480    fn default() -> Self {
481        Self::empty()
482    }
483}
484
485impl<'a, V: SQLParam + 'a> From<&'a str> for SQL<'a, V> {
486    fn from(s: &'a str) -> Self {
487        SQL::raw(s)
488    }
489}
490
491impl<'a, V: SQLParam> From<Token> for SQL<'a, V> {
492    fn from(value: Token) -> Self {
493        SQL::token(value)
494    }
495}
496
497impl<'a, V: SQLParam + 'a> AsRef<SQL<'a, V>> for SQL<'a, V> {
498    fn as_ref(&self) -> &SQL<'a, V> {
499        self
500    }
501}
502
503impl<'a, V: SQLParam + core::fmt::Display> Display for SQL<'a, V> {
504    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
505        // Collect params for Debug formatting (iterator can't be used with :?)
506        let params: Vec<_> = self.params().collect();
507        write!(f, r#"sql: "{}", params: {:?}"#, self.sql(), params)
508    }
509}
510
511impl<'a, V: SQLParam + 'a> ToSQL<'a, V> for SQL<'a, V> {
512    fn to_sql(&self) -> SQL<'a, V> {
513        self.clone()
514    }
515}
516
517impl<'a, V: SQLParam, T> FromIterator<T> for SQL<'a, V>
518where
519    SQLChunk<'a, V>: From<T>,
520{
521    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
522        let chunks = SmallVec::from_iter(iter.into_iter().map(SQLChunk::from));
523        Self { chunks }
524    }
525}
526
527impl<'a, V: SQLParam> IntoIterator for SQL<'a, V> {
528    type Item = SQLChunk<'a, V>;
529    type IntoIter = smallvec::IntoIter<[SQLChunk<'a, V>; 8]>;
530
531    fn into_iter(self) -> Self::IntoIter {
532        self.chunks.into_iter()
533    }
534}