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