Skip to main content

drizzle_core/sql/
chunk.rs

1use crate::SQLConstraintKind;
2use crate::prelude::*;
3use crate::{Param, Placeholder, SQLParam, sql::tokens::Token};
4
5// ==================== Dialect enums ====================
6
7/// Dialect-specific column metadata. Const-compatible enum grouping
8/// fields that only apply to one dialect.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ColumnDialect {
11    SQLite {
12        autoincrement: bool,
13    },
14    PostgreSQL {
15        postgres_type: &'static str,
16        is_serial: bool,
17        is_bigserial: bool,
18        is_generated_identity: bool,
19        is_identity_always: bool,
20        generated_expression: Option<&'static str>,
21        collate: Option<&'static str>,
22    },
23}
24
25/// Dialect-specific table metadata.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub enum TableDialect {
28    #[default]
29    PostgreSQL,
30    SQLite {
31        without_rowid: bool,
32        strict: bool,
33    },
34}
35
36// ==================== Ref structs ====================
37
38/// Foreign key reference as a const Copy struct.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct ForeignKeyRef {
41    pub target_table: &'static str,
42    pub source_columns: &'static [&'static str],
43    pub target_columns: &'static [&'static str],
44}
45
46/// Primary key reference as a const Copy struct.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct PrimaryKeyRef {
49    pub columns: &'static [&'static str],
50}
51
52/// Constraint reference as a const Copy struct.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct ConstraintRef {
55    pub name: Option<&'static str>,
56    pub kind: SQLConstraintKind,
57    pub columns: &'static [&'static str],
58    pub check_expression: Option<&'static str>,
59}
60
61// ==================== Enhanced TableRef and ColumnRef ====================
62
63/// Table reference with full schema metadata.
64///
65/// Carries both the SQL rendering fields (`name`, `column_names`) and
66/// complete schema metadata (columns, keys, constraints). SQL rendering
67/// code only uses `name`/`column_names` and ignores extra fields.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct TableRef {
70    // SQL rendering fields
71    pub name: &'static str,
72    pub column_names: &'static [&'static str],
73
74    // Schema metadata
75    pub schema: Option<&'static str>,
76    pub qualified_name: &'static str,
77    pub columns: &'static [ColumnRef],
78    pub primary_key: Option<PrimaryKeyRef>,
79    pub foreign_keys: &'static [ForeignKeyRef],
80    pub constraints: &'static [ConstraintRef],
81    pub dependency_names: &'static [&'static str],
82
83    // Dialect-specific
84    pub dialect: TableDialect,
85}
86
87impl TableRef {
88    /// Creates a lightweight `TableRef` for SQL rendering only.
89    ///
90    /// Only `name` and `column_names` are populated; metadata fields use
91    /// empty defaults. Use a full struct literal for metadata-carrying refs.
92    #[must_use]
93    pub const fn sql(name: &'static str, column_names: &'static [&'static str]) -> Self {
94        Self {
95            name,
96            column_names,
97            schema: None,
98            qualified_name: "",
99            columns: &[],
100            primary_key: None,
101            foreign_keys: &[],
102            constraints: &[],
103            dependency_names: &[],
104            dialect: TableDialect::PostgreSQL,
105        }
106    }
107}
108
109/// Packed column metadata flags for [`ColumnRef`].
110///
111/// Encodes the nullability, primary-key, unique, and has-default bits in a
112/// single byte so that [`ColumnRef`] stays below the "too many bools" threshold
113/// while keeping each bit independently addressable.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
115pub struct ColumnFlags(u8);
116
117impl ColumnFlags {
118    /// Column is declared `NOT NULL`.
119    pub const NOT_NULL: Self = Self(1 << 0);
120    /// Column participates in the table's primary key.
121    pub const PRIMARY_KEY: Self = Self(1 << 1);
122    /// Column has a `UNIQUE` constraint.
123    pub const UNIQUE: Self = Self(1 << 2);
124    /// Column has a `DEFAULT` clause.
125    pub const HAS_DEFAULT: Self = Self(1 << 3);
126
127    /// Returns a flag set with no bits set.
128    #[must_use]
129    pub const fn empty() -> Self {
130        Self(0)
131    }
132
133    /// Reconstructs a flag set from its raw byte representation.
134    #[must_use]
135    pub const fn from_bits(bits: u8) -> Self {
136        Self(bits)
137    }
138
139    /// Returns the raw byte representation.
140    #[must_use]
141    pub const fn bits(self) -> u8 {
142        self.0
143    }
144
145    /// Returns `true` when every bit in `other` is set in `self`.
146    #[must_use]
147    pub const fn contains(self, other: Self) -> bool {
148        (self.0 & other.0) == other.0
149    }
150
151    /// Returns the union of two flag sets.
152    #[must_use]
153    pub const fn union(self, other: Self) -> Self {
154        Self(self.0 | other.0)
155    }
156}
157
158impl core::ops::BitOr for ColumnFlags {
159    type Output = Self;
160    fn bitor(self, rhs: Self) -> Self {
161        self.union(rhs)
162    }
163}
164
165impl core::ops::BitOrAssign for ColumnFlags {
166    fn bitor_assign(&mut self, rhs: Self) {
167        *self = self.union(rhs);
168    }
169}
170
171/// Column reference with full schema metadata.
172///
173/// Carries both the SQL rendering fields (`table`, `name`) and
174/// complete column metadata. SQL rendering code only uses the name fields
175/// and ignores extra metadata.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct ColumnRef {
178    // SQL rendering fields
179    pub table: &'static str,
180    pub name: &'static str,
181
182    // Schema metadata
183    pub sql_type: &'static str,
184    pub flags: ColumnFlags,
185
186    // Dialect-specific
187    pub dialect: ColumnDialect,
188}
189
190impl ColumnRef {
191    /// Creates a lightweight `ColumnRef` for SQL rendering only.
192    ///
193    /// Only `table` and `name` are populated; metadata fields
194    /// use empty defaults. Use a full struct literal for metadata-carrying refs.
195    #[must_use]
196    pub const fn sql(table: &'static str, name: &'static str) -> Self {
197        Self {
198            table,
199            name,
200            sql_type: "",
201            flags: ColumnFlags::empty(),
202            dialect: ColumnDialect::SQLite {
203                autoincrement: false,
204            },
205        }
206    }
207
208    /// Returns `true` if this column is declared `NOT NULL`.
209    #[must_use]
210    pub const fn not_null(&self) -> bool {
211        self.flags.contains(ColumnFlags::NOT_NULL)
212    }
213
214    /// Returns `true` if this column participates in the primary key.
215    #[must_use]
216    pub const fn primary_key(&self) -> bool {
217        self.flags.contains(ColumnFlags::PRIMARY_KEY)
218    }
219
220    /// Returns `true` if this column has a `UNIQUE` constraint.
221    #[must_use]
222    pub const fn unique(&self) -> bool {
223        self.flags.contains(ColumnFlags::UNIQUE)
224    }
225
226    /// Returns `true` if this column has a `DEFAULT` clause.
227    #[must_use]
228    pub const fn has_default(&self) -> bool {
229        self.flags.contains(ColumnFlags::HAS_DEFAULT)
230    }
231}
232
233// ==================== Identifier quoting ====================
234
235/// Writes a SQL identifier enclosed in double quotes.
236///
237/// Any embedded `"` characters are doubled to prevent identifier-injection
238/// (CWE-89). Both `PostgreSQL` and `SQLite` accept `"..."` as a delimited
239/// identifier and treat `""` as an escaped double-quote character inside
240/// such an identifier.
241///
242/// Fast path: identifiers with no embedded `"` are written with three calls
243/// (open quote, name, close quote). Only identifiers containing a `"` take
244/// the character-by-character escaping path.
245#[inline]
246pub fn write_quoted_ident(buf: &mut impl core::fmt::Write, name: &str) {
247    let _ = buf.write_char('"');
248    if name.contains('"') {
249        for ch in name.chars() {
250            if ch == '"' {
251                let _ = buf.write_str("\"\"");
252            } else {
253                let _ = buf.write_char(ch);
254            }
255        }
256    } else {
257        let _ = buf.write_str(name);
258    }
259    let _ = buf.write_char('"');
260}
261
262// ==================== SQLChunk ====================
263
264/// A SQL chunk represents a part of an SQL statement.
265///
266/// Each variant has a clear semantic purpose:
267/// - `Token` - SQL keywords and operators (SELECT, FROM, =, etc.)
268/// - `Ident` - Quoted identifiers ("`table_name`", "`column_name`")
269/// - `Raw` - Unquoted raw SQL text (function names, expressions)
270/// - `Param` - Parameter placeholders with values
271/// - `Table` - Table reference via `TableRef`
272/// - `Column` - Column reference via `ColumnRef`
273#[derive(Clone)]
274pub enum SQLChunk<'a, V: SQLParam> {
275    /// SQL keywords and operators: SELECT, FROM, WHERE, =, AND, etc.
276    /// Renders as: keyword with automatic spacing rules
277    Token(Token),
278
279    /// Quoted identifier for user-provided names
280    /// Renders as: "name" (with quotes)
281    /// Use for: table names, column names, alias names
282    Ident(Cow<'a, str>),
283
284    /// Raw SQL text (unquoted) for expressions, function names
285    /// Renders as: text (no quotes, as-is)
286    /// Use for: function names like COUNT, expressions, numeric literals
287    Raw(Cow<'a, str>),
288
289    /// Unsigned integer SQL literal rendered directly without heap allocation.
290    ///
291    /// Primarily used for clauses like LIMIT/OFFSET where numeric literals are
292    /// embedded directly in SQL text rather than parameterized.
293    Number(usize),
294
295    /// Parameter with value and placeholder
296    /// Renders as: ? or $1 or :name depending on placeholder style
297    Param(Param<'a, V>),
298
299    /// Table reference with static name and column names.
300    /// Renders as: "`table_name`"
301    /// Column names used for SELECT * expansion.
302    Table(TableRef),
303
304    /// Column reference with static table and column names.
305    /// Renders as: "`table_name"."column_name`"
306    Column(ColumnRef),
307}
308
309impl<'a, V: SQLParam> SQLChunk<'a, V> {
310    // ==================== const constructors ====================
311
312    /// Creates a token chunk - const
313    #[inline]
314    #[must_use]
315    pub const fn token(t: Token) -> Self {
316        Self::Token(t)
317    }
318
319    /// Creates a quoted identifier from a static string - const
320    #[inline]
321    #[must_use]
322    pub const fn ident_static(name: &'static str) -> Self {
323        Self::Ident(Cow::Borrowed(name))
324    }
325
326    /// Creates raw SQL text from a static string - const
327    #[inline]
328    #[must_use]
329    pub const fn raw_static(text: &'static str) -> Self {
330        Self::Raw(Cow::Borrowed(text))
331    }
332
333    /// Creates a table chunk - const
334    #[inline]
335    #[must_use]
336    pub const fn table(table: TableRef) -> Self {
337        Self::Table(table)
338    }
339
340    /// Creates a column chunk - const
341    #[inline]
342    #[must_use]
343    pub const fn column(column: ColumnRef) -> Self {
344        Self::Column(column)
345    }
346
347    /// Creates a parameter chunk with borrowed value - const
348    #[inline]
349    pub const fn param_borrowed(value: &'a V, placeholder: Placeholder) -> Self {
350        Self::Param(Param {
351            value: Some(Cow::Borrowed(value)),
352            placeholder,
353        })
354    }
355
356    // ==================== non-const constructors ====================
357
358    /// Creates a quoted identifier from a runtime string
359    #[inline]
360    pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
361        Self::Ident(name.into())
362    }
363
364    /// Creates raw SQL text from a runtime string
365    #[inline]
366    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
367        Self::Raw(text.into())
368    }
369
370    /// Creates an unsigned integer SQL literal chunk.
371    #[inline]
372    #[must_use]
373    pub const fn number(value: usize) -> Self {
374        Self::Number(value)
375    }
376
377    /// Creates a parameter chunk with owned value
378    #[inline]
379    pub fn param(value: impl Into<Cow<'a, V>>, placeholder: Placeholder) -> Self {
380        Self::Param(Param {
381            value: Some(value.into()),
382            placeholder,
383        })
384    }
385
386    // ==================== write implementation ====================
387
388    /// Write chunk content to buffer
389    pub(crate) fn write(&self, buf: &mut impl core::fmt::Write) {
390        match self {
391            SQLChunk::Token(token) => {
392                let _ = buf.write_str(token.as_str());
393            }
394            SQLChunk::Ident(name) => {
395                write_quoted_ident(buf, name);
396            }
397            SQLChunk::Raw(text) => {
398                let _ = buf.write_str(text);
399            }
400            SQLChunk::Number(value) => {
401                let _ = write!(buf, "{value}");
402            }
403            SQLChunk::Param(Param { placeholder, .. }) => {
404                let _ = write!(buf, "{placeholder}");
405            }
406            SQLChunk::Table(t) => {
407                write_quoted_ident(buf, t.name);
408            }
409            SQLChunk::Column(c) => {
410                write_quoted_ident(buf, c.table);
411                let _ = buf.write_char('.');
412                write_quoted_ident(buf, c.name);
413            }
414        }
415    }
416
417    /// Check if this chunk is "word-like" (needs space separation from other word-like chunks)
418    #[inline]
419    pub(crate) const fn is_word_like(&self) -> bool {
420        match self {
421            SQLChunk::Token(t) => !t.is_punctuation() && !t.is_operator(),
422            SQLChunk::Ident(_)
423            | SQLChunk::Raw(_)
424            | SQLChunk::Number(_)
425            | SQLChunk::Param(_)
426            | SQLChunk::Table(_)
427            | SQLChunk::Column(_) => true,
428        }
429    }
430}
431
432impl<V: SQLParam + core::fmt::Debug> core::fmt::Debug for SQLChunk<'_, V> {
433    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
434        match self {
435            SQLChunk::Token(token) => f.debug_tuple("Token").field(token).finish(),
436            SQLChunk::Ident(name) => f.debug_tuple("Ident").field(name).finish(),
437            SQLChunk::Raw(text) => f.debug_tuple("Raw").field(text).finish(),
438            SQLChunk::Number(value) => f.debug_tuple("Number").field(value).finish(),
439            SQLChunk::Param(param) => f.debug_tuple("Param").field(param).finish(),
440            SQLChunk::Table(t) => f.debug_tuple("Table").field(&t.name).finish(),
441            SQLChunk::Column(c) => f
442                .debug_tuple("Column")
443                .field(&format!("{}.{}", c.table, c.name))
444                .finish(),
445        }
446    }
447}
448
449// ==================== From implementations ====================
450
451impl<V: SQLParam> From<Token> for SQLChunk<'_, V> {
452    #[inline]
453    fn from(value: Token) -> Self {
454        Self::Token(value)
455    }
456}
457
458impl<V: SQLParam> From<TableRef> for SQLChunk<'_, V> {
459    #[inline]
460    fn from(value: TableRef) -> Self {
461        Self::Table(value)
462    }
463}
464
465impl<V: SQLParam> From<ColumnRef> for SQLChunk<'_, V> {
466    #[inline]
467    fn from(value: ColumnRef) -> Self {
468        Self::Column(value)
469    }
470}
471
472impl<'a, V: SQLParam> From<Param<'a, V>> for SQLChunk<'a, V> {
473    #[inline]
474    fn from(value: Param<'a, V>) -> Self {
475        Self::Param(value)
476    }
477}