drizzle_core/sql/
chunk.rs

1use crate::prelude::*;
2use crate::{Param, Placeholder, SQLColumnInfo, SQLParam, SQLTableInfo, sql::tokens::Token};
3
4/// A SQL chunk represents a part of an SQL statement.
5///
6/// This enum has 8 variants, each with a clear semantic purpose:
7/// - `Empty` - Placeholder for const array padding (renders as nothing, skipped)
8/// - `Token` - SQL keywords and operators (SELECT, FROM, =, etc.)
9/// - `Ident` - Quoted identifiers ("table_name", "column_name")
10/// - `Raw` - Unquoted raw SQL text (function names, expressions)
11/// - `Param` - Parameter placeholders with values
12/// - `Table` - Table reference with metadata access
13/// - `Column` - Column reference with metadata access
14/// - `Alias` - Alias wrapper (expr AS "name")
15#[derive(Default, Clone)]
16pub enum SQLChunk<'a, V: SQLParam> {
17    /// Empty placeholder - used for const array padding
18    /// Renders as: nothing (skipped entirely)
19    #[default]
20    Empty,
21
22    /// SQL keywords and operators: SELECT, FROM, WHERE, =, AND, etc.
23    /// Renders as: keyword with automatic spacing rules
24    Token(Token),
25
26    /// Quoted identifier for user-provided names
27    /// Renders as: "name" (with quotes)
28    /// Use for: table names, column names, alias names
29    Ident(Cow<'a, str>),
30
31    /// Raw SQL text (unquoted) for expressions, function names
32    /// Renders as: text (no quotes, as-is)
33    /// Use for: function names like COUNT, expressions, numeric literals
34    Raw(Cow<'a, str>),
35
36    /// Parameter with value and placeholder
37    /// Renders as: ? or $1 or :name depending on placeholder style
38    Param(Param<'a, V>),
39
40    /// Table reference with full metadata access
41    /// Renders as: "table_name"
42    /// Provides: columns() for SELECT *, dependencies() for FK tracking
43    Table(&'static dyn SQLTableInfo),
44
45    /// Column reference with full metadata access
46    /// Renders as: "table"."column"
47    /// Provides: table(), is_primary_key(), foreign_key(), etc.
48    Column(&'static dyn SQLColumnInfo),
49
50    /// Alias wrapper: renders inner chunk followed by AS "alias"
51    /// Renders as: {inner} AS "alias"
52    Alias {
53        inner: Box<SQLChunk<'a, V>>,
54        alias: Cow<'a, str>,
55    },
56}
57
58impl<'a, V: SQLParam> SQLChunk<'a, V> {
59    // ==================== const constructors ====================
60
61    /// Creates a token chunk - const
62    #[inline]
63    pub const fn token(t: Token) -> Self {
64        Self::Token(t)
65    }
66
67    /// Creates a quoted identifier from a static string - const
68    #[inline]
69    pub const fn ident_static(name: &'static str) -> Self {
70        Self::Ident(Cow::Borrowed(name))
71    }
72
73    /// Creates raw SQL text from a static string - const
74    #[inline]
75    pub const fn raw_static(text: &'static str) -> Self {
76        Self::Raw(Cow::Borrowed(text))
77    }
78
79    /// Creates a table chunk - const
80    #[inline]
81    pub const fn table(table: &'static dyn SQLTableInfo) -> Self {
82        Self::Table(table)
83    }
84
85    /// Creates a column chunk - const
86    #[inline]
87    pub const fn column(column: &'static dyn SQLColumnInfo) -> Self {
88        Self::Column(column)
89    }
90
91    /// Creates a parameter chunk with borrowed value - const
92    #[inline]
93    pub const fn param_borrowed(value: &'a V, placeholder: Placeholder) -> Self {
94        Self::Param(Param {
95            value: Some(Cow::Borrowed(value)),
96            placeholder,
97        })
98    }
99
100    // ==================== non-const constructors ====================
101
102    /// Creates a quoted identifier from a runtime string
103    #[inline]
104    pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
105        Self::Ident(name.into())
106    }
107
108    /// Creates raw SQL text from a runtime string
109    #[inline]
110    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
111        Self::Raw(text.into())
112    }
113
114    /// Creates a parameter chunk with owned value
115    #[inline]
116    pub fn param(value: impl Into<Cow<'a, V>>, placeholder: Placeholder) -> Self {
117        Self::Param(Param {
118            value: Some(value.into()),
119            placeholder,
120        })
121    }
122
123    /// Creates an alias chunk wrapping any SQLChunk
124    #[inline]
125    pub fn alias(inner: SQLChunk<'a, V>, alias: impl Into<Cow<'a, str>>) -> Self {
126        Self::Alias {
127            inner: Box::new(inner),
128            alias: alias.into(),
129        }
130    }
131
132    // ==================== write implementation ====================
133
134    /// Write chunk content to buffer
135    pub(crate) fn write(&self, buf: &mut impl core::fmt::Write) {
136        match self {
137            SQLChunk::Empty => {} // Skip empty chunks
138            SQLChunk::Token(token) => {
139                let _ = buf.write_str(token.as_str());
140            }
141            SQLChunk::Ident(name) => {
142                let _ = buf.write_char('"');
143                let _ = buf.write_str(name);
144                let _ = buf.write_char('"');
145            }
146            SQLChunk::Raw(text) => {
147                let _ = buf.write_str(text);
148            }
149            SQLChunk::Param(Param { placeholder, .. }) => {
150                let _ = write!(buf, "{}", placeholder);
151            }
152            SQLChunk::Table(table) => {
153                let _ = buf.write_char('"');
154                let _ = buf.write_str(table.name());
155                let _ = buf.write_char('"');
156            }
157            SQLChunk::Column(column) => {
158                let _ = buf.write_char('"');
159                let _ = buf.write_str(column.table().name());
160                let _ = buf.write_str("\".\"");
161                let _ = buf.write_str(column.name());
162                let _ = buf.write_char('"');
163            }
164            SQLChunk::Alias { inner, alias } => {
165                inner.write(buf);
166                let _ = buf.write_str(" AS \"");
167                let _ = buf.write_str(alias);
168                let _ = buf.write_char('"');
169            }
170        }
171    }
172
173    /// Check if this chunk is "word-like" (needs space separation from other word-like chunks)
174    #[inline]
175    pub(crate) fn is_word_like(&self) -> bool {
176        match self {
177            SQLChunk::Empty => false,
178            SQLChunk::Token(t) => !matches!(
179                t,
180                Token::LPAREN
181                    | Token::RPAREN
182                    | Token::COMMA
183                    | Token::SEMI
184                    | Token::DOT
185                    | Token::EQ
186                    | Token::NE
187                    | Token::LT
188                    | Token::GT
189                    | Token::LE
190                    | Token::GE
191            ),
192            SQLChunk::Ident(_)
193            | SQLChunk::Raw(_)
194            | SQLChunk::Param(_)
195            | SQLChunk::Table(_)
196            | SQLChunk::Column(_)
197            | SQLChunk::Alias { .. } => true,
198        }
199    }
200}
201
202impl<'a, V: SQLParam + core::fmt::Debug> core::fmt::Debug for SQLChunk<'a, V> {
203    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
204        match self {
205            SQLChunk::Empty => f.write_str("Empty"),
206            SQLChunk::Token(token) => f.debug_tuple("Token").field(token).finish(),
207            SQLChunk::Ident(name) => f.debug_tuple("Ident").field(name).finish(),
208            SQLChunk::Raw(text) => f.debug_tuple("Raw").field(text).finish(),
209            SQLChunk::Param(param) => f.debug_tuple("Param").field(param).finish(),
210            SQLChunk::Table(table) => f.debug_tuple("Table").field(&table.name()).finish(),
211            SQLChunk::Column(column) => f
212                .debug_tuple("Column")
213                .field(&format!("{}.{}", column.table().name(), column.name()))
214                .finish(),
215            SQLChunk::Alias { inner, alias } => f
216                .debug_struct("Alias")
217                .field("inner", inner)
218                .field("alias", alias)
219                .finish(),
220        }
221    }
222}
223
224// ==================== From implementations ====================
225
226impl<'a, V: SQLParam> From<Token> for SQLChunk<'a, V> {
227    #[inline]
228    fn from(value: Token) -> Self {
229        Self::Token(value)
230    }
231}
232
233impl<'a, V: SQLParam> From<&'static dyn SQLColumnInfo> for SQLChunk<'a, V> {
234    #[inline]
235    fn from(value: &'static dyn SQLColumnInfo) -> Self {
236        Self::Column(value)
237    }
238}
239
240impl<'a, V: SQLParam> From<&'static dyn SQLTableInfo> for SQLChunk<'a, V> {
241    #[inline]
242    fn from(value: &'static dyn SQLTableInfo) -> Self {
243        Self::Table(value)
244    }
245}
246
247impl<'a, V: SQLParam> From<Param<'a, V>> for SQLChunk<'a, V> {
248    #[inline]
249    fn from(value: Param<'a, V>) -> Self {
250        Self::Param(value)
251    }
252}