Skip to main content

drizzle_core/sql/
chunk.rs

1use crate::SQLConstraintKind;
2use crate::prelude::*;
3use crate::{Dialect, Param, Placeholder, SQLParam, sql::tokens::Token};
4
5// ==================== Dialect enums ====================
6
7/// One value of an enum column type.
8///
9/// `label` is what a text column (or a `PostgreSQL` enum) stores, and
10/// `discriminant` is what an integer column stores.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct EnumVariantRef {
13    /// The stored text label, usually the variant name.
14    pub label: &'static str,
15    /// The integer value of the variant.
16    pub discriminant: i64,
17}
18
19/// The values an enum column type can hold, in declaration order.
20///
21/// Implemented by `#[derive(SQLiteEnum)]` and `#[derive(PostgresEnum)]`.
22/// The table macros copy these into the column's [`ColumnDialect`]
23/// metadata (`enum_variants`), so tools such as `drizzle-seed` can generate
24/// only valid values.
25pub trait SQLEnumVariants {
26    /// Every variant, in declaration order.
27    const VARIANTS: &'static [EnumVariantRef];
28
29    /// The SQL literal a column `DEFAULT` writes for each variant, in
30    /// [`VARIANTS`](Self::VARIANTS) order: the discriminant for an
31    /// integer-stored enum, the quoted label otherwise.
32    const DEFAULT_LITERALS: &'static [&'static str] = &[];
33}
34
35/// The SQL literal of `T`'s variant `label` as a column `DEFAULT`, from
36/// [`SQLEnumVariants::DEFAULT_LITERALS`].
37///
38/// The table macros call this in a const context, so a `default` that names
39/// no variant fails to compile.
40///
41/// # Panics
42///
43/// When `label` is not a variant of `T`.
44#[must_use]
45pub const fn enum_default_literal<T: SQLEnumVariants>(label: &str) -> &'static str {
46    let mut index = 0;
47    while index < T::VARIANTS.len() {
48        if str_eq(T::VARIANTS[index].label, label) && index < T::DEFAULT_LITERALS.len() {
49            return T::DEFAULT_LITERALS[index];
50        }
51        index += 1;
52    }
53    panic!("the column default is not a variant of its enum type")
54}
55
56const fn str_eq(left: &str, right: &str) -> bool {
57    let (left, right) = (left.as_bytes(), right.as_bytes());
58    if left.len() != right.len() {
59        return false;
60    }
61    let mut index = 0;
62    while index < left.len() {
63        if left[index] != right[index] {
64            return false;
65        }
66        index += 1;
67    }
68    true
69}
70
71/// Column metadata that applies to one dialect only.
72///
73/// Generated by the table macros and read by migrations. The fields mirror
74/// the column attributes (`default`, `generated`, `collate`, ...).
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum ColumnDialect {
77    SQLite {
78        autoincrement: bool,
79        default: Option<&'static str>,
80        generated_expression: Option<&'static str>,
81        generated_stored: bool,
82        collate: Option<&'static str>,
83        /// The values of an enum column, `None` for other columns.
84        enum_variants: Option<&'static [EnumVariantRef]>,
85    },
86    PostgreSQL {
87        postgres_type: &'static str,
88        dimensions: Option<i32>,
89        is_serial: bool,
90        is_bigserial: bool,
91        is_generated_identity: bool,
92        is_identity_always: bool,
93        default: Option<&'static str>,
94        generated_expression: Option<&'static str>,
95        generated_stored: bool,
96        collate: Option<&'static str>,
97        comment: Option<&'static str>,
98        /// The values of an enum column, `None` for other columns.
99        enum_variants: Option<&'static [EnumVariantRef]>,
100    },
101    MySQL {
102        auto_increment: bool,
103        default: Option<&'static str>,
104        generated_expression: Option<&'static str>,
105        generated_stored: bool,
106        charset: Option<&'static str>,
107        collate: Option<&'static str>,
108        on_update: Option<&'static str>,
109    },
110}
111
112/// Table metadata that applies to one dialect only.
113///
114/// Generated by the table macros and read by migrations.
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum TableDialect {
117    PostgreSQL {
118        is_unlogged: bool,
119        is_temporary: bool,
120        inherits: Option<&'static str>,
121        tablespace: Option<&'static str>,
122        is_rls_enabled: bool,
123        comment: Option<&'static str>,
124    },
125    SQLite {
126        without_rowid: bool,
127        strict: bool,
128    },
129    MySQL {
130        is_temporary: bool,
131        engine: Option<&'static str>,
132        charset: Option<&'static str>,
133        collate: Option<&'static str>,
134        comment: Option<&'static str>,
135    },
136}
137
138impl Default for TableDialect {
139    fn default() -> Self {
140        Self::PostgreSQL {
141            is_unlogged: false,
142            is_temporary: false,
143            inherits: None,
144            tablespace: None,
145            is_rls_enabled: false,
146            comment: None,
147        }
148    }
149}
150
151// ==================== Ref structs ====================
152
153/// A foreign key constraint, as static metadata generated by the table
154/// macros.
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub struct ForeignKeyRef {
157    pub name: &'static str,
158    pub name_explicit: bool,
159    pub target_table: &'static str,
160    pub target_schema: &'static str,
161    pub source_columns: &'static [&'static str],
162    pub target_columns: &'static [&'static str],
163    pub on_delete: Option<&'static str>,
164    pub on_update: Option<&'static str>,
165    pub deferrable: bool,
166    pub initially_deferred: bool,
167}
168
169/// A table's primary key columns, as static metadata.
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub struct PrimaryKeyRef {
172    pub columns: &'static [&'static str],
173}
174
175/// A table constraint (`UNIQUE`, `CHECK`, ...), as static metadata.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct ConstraintRef {
178    pub name: Option<&'static str>,
179    pub name_explicit: bool,
180    pub kind: SQLConstraintKind,
181    pub columns: &'static [&'static str],
182    pub check_expression: Option<&'static str>,
183    pub deferrable: bool,
184    pub initially_deferred: bool,
185}
186
187// ==================== Enhanced TableRef and ColumnRef ====================
188
189/// A table's name, columns, and schema metadata, generated by the table
190/// macros.
191///
192/// SQL rendering only reads `schema`, `name` and `column_names` (through
193/// [`TableSqlRef`]). Migrations read the rest.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct TableRef {
196    // SQL rendering fields
197    pub name: &'static str,
198    pub column_names: &'static [&'static str],
199
200    // Schema metadata
201    pub schema: Option<&'static str>,
202    pub qualified_name: &'static str,
203    pub columns: &'static [ColumnRef],
204    pub primary_key: Option<PrimaryKeyRef>,
205    pub foreign_keys: &'static [ForeignKeyRef],
206    pub constraints: &'static [ConstraintRef],
207    pub dependency_names: &'static [&'static str],
208
209    // Dialect-specific
210    pub dialect: TableDialect,
211}
212
213impl TableRef {
214    /// Creates a `TableRef` with just a name and column names, for SQL
215    /// rendering.
216    ///
217    /// Metadata fields are empty. Use a struct literal when metadata
218    /// matters.
219    #[must_use]
220    pub const fn sql(name: &'static str, column_names: &'static [&'static str]) -> Self {
221        Self {
222            name,
223            column_names,
224            schema: None,
225            qualified_name: "",
226            columns: &[],
227            primary_key: None,
228            foreign_keys: &[],
229            constraints: &[],
230            dependency_names: &[],
231            dialect: TableDialect::PostgreSQL {
232                is_unlogged: false,
233                is_temporary: false,
234                inherits: None,
235                tablespace: None,
236                is_rls_enabled: false,
237                comment: None,
238            },
239        }
240    }
241}
242
243/// The parts of a [`TableRef`] that SQL rendering needs.
244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub struct TableSqlRef {
246    pub schema: Option<&'static str>,
247    pub name: &'static str,
248    pub column_names: &'static [&'static str],
249}
250
251impl TableSqlRef {
252    /// Copies the rendering fields out of a [`TableRef`].
253    #[inline]
254    #[must_use]
255    pub const fn from_table_ref(table: TableRef) -> Self {
256        Self {
257            schema: table.schema,
258            name: table.name,
259            column_names: table.column_names,
260        }
261    }
262
263    /// Copies the rendering fields out of a borrowed [`TableRef`].
264    #[inline]
265    #[must_use]
266    pub const fn from_table_ref_ref(table: &TableRef) -> Self {
267        Self {
268            schema: table.schema,
269            name: table.name,
270            column_names: table.column_names,
271        }
272    }
273}
274
275impl From<&TableRef> for TableSqlRef {
276    #[inline]
277    fn from(value: &TableRef) -> Self {
278        Self::from_table_ref_ref(value)
279    }
280}
281
282impl From<TableRef> for TableSqlRef {
283    #[inline]
284    fn from(value: TableRef) -> Self {
285        Self::from_table_ref(value)
286    }
287}
288
289/// Column metadata flags for [`ColumnRef`], packed into one byte.
290///
291/// Holds the `NOT NULL`, primary-key, `UNIQUE` and has-default bits.
292///
293/// # Examples
294///
295/// ```
296/// use drizzle_core::ColumnFlags;
297///
298/// let flags = ColumnFlags::NOT_NULL | ColumnFlags::PRIMARY_KEY;
299/// assert!(flags.contains(ColumnFlags::NOT_NULL));
300/// assert!(!flags.contains(ColumnFlags::UNIQUE));
301/// ```
302#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
303pub struct ColumnFlags(u8);
304
305impl ColumnFlags {
306    /// Column is declared `NOT NULL`.
307    pub const NOT_NULL: Self = Self(1 << 0);
308    /// Column participates in the table's primary key.
309    pub const PRIMARY_KEY: Self = Self(1 << 1);
310    /// Column has a `UNIQUE` constraint.
311    pub const UNIQUE: Self = Self(1 << 2);
312    /// Column has a `DEFAULT` clause.
313    pub const HAS_DEFAULT: Self = Self(1 << 3);
314
315    /// Returns a flag set with no bits set.
316    #[must_use]
317    pub const fn empty() -> Self {
318        Self(0)
319    }
320
321    /// Reconstructs a flag set from its raw byte representation.
322    #[must_use]
323    pub const fn from_bits(bits: u8) -> Self {
324        Self(bits)
325    }
326
327    /// Returns the raw byte representation.
328    #[must_use]
329    pub const fn bits(self) -> u8 {
330        self.0
331    }
332
333    /// Returns `true` when every bit in `other` is set in `self`.
334    #[must_use]
335    pub const fn contains(self, other: Self) -> bool {
336        (self.0 & other.0) == other.0
337    }
338
339    /// Returns the union of two flag sets.
340    #[must_use]
341    pub const fn union(self, other: Self) -> Self {
342        Self(self.0 | other.0)
343    }
344}
345
346impl core::ops::BitOr for ColumnFlags {
347    type Output = Self;
348    fn bitor(self, rhs: Self) -> Self {
349        self.union(rhs)
350    }
351}
352
353impl core::ops::BitOrAssign for ColumnFlags {
354    fn bitor_assign(&mut self, rhs: Self) {
355        *self = self.union(rhs);
356    }
357}
358
359/// A column's table, name, and schema metadata, generated by the table
360/// macros.
361///
362/// SQL rendering only reads `table` and `name` (through [`ColumnSqlRef`]).
363/// Migrations read the rest.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub struct ColumnRef {
366    // SQL rendering fields
367    pub table: &'static str,
368    pub name: &'static str,
369
370    // Schema metadata
371    pub sql_type: &'static str,
372    pub flags: ColumnFlags,
373
374    // Dialect-specific
375    pub dialect: ColumnDialect,
376}
377
378impl ColumnRef {
379    /// Creates a `ColumnRef` with just a table and column name, for SQL
380    /// rendering.
381    ///
382    /// Metadata fields are empty. Use a struct literal when metadata
383    /// matters.
384    #[must_use]
385    pub const fn sql(table: &'static str, name: &'static str) -> Self {
386        Self {
387            table,
388            name,
389            sql_type: "",
390            flags: ColumnFlags::empty(),
391            dialect: ColumnDialect::SQLite {
392                autoincrement: false,
393                default: None,
394                generated_expression: None,
395                generated_stored: false,
396                collate: None,
397                enum_variants: None,
398            },
399        }
400    }
401
402    /// Returns `true` if this column is declared `NOT NULL`.
403    #[must_use]
404    pub const fn not_null(&self) -> bool {
405        self.flags.contains(ColumnFlags::NOT_NULL)
406    }
407
408    /// Returns `true` if this column participates in the primary key.
409    #[must_use]
410    pub const fn primary_key(&self) -> bool {
411        self.flags.contains(ColumnFlags::PRIMARY_KEY)
412    }
413
414    /// Returns `true` if this column has a `UNIQUE` constraint.
415    #[must_use]
416    pub const fn unique(&self) -> bool {
417        self.flags.contains(ColumnFlags::UNIQUE)
418    }
419
420    /// Returns `true` if this column has a `DEFAULT` clause.
421    #[must_use]
422    pub const fn has_default(&self) -> bool {
423        self.flags.contains(ColumnFlags::HAS_DEFAULT)
424    }
425}
426
427/// The parts of a [`ColumnRef`] that SQL rendering needs.
428#[derive(Clone, Copy, Debug, PartialEq, Eq)]
429pub struct ColumnSqlRef {
430    pub table: &'static str,
431    pub name: &'static str,
432}
433
434impl ColumnSqlRef {
435    /// Copies the rendering fields out of a [`ColumnRef`].
436    #[inline]
437    #[must_use]
438    pub const fn from_column_ref(column: ColumnRef) -> Self {
439        Self {
440            table: column.table,
441            name: column.name,
442        }
443    }
444
445    /// Copies the rendering fields out of a borrowed [`ColumnRef`].
446    #[inline]
447    #[must_use]
448    pub const fn from_column_ref_ref(column: &ColumnRef) -> Self {
449        Self {
450            table: column.table,
451            name: column.name,
452        }
453    }
454}
455
456impl From<&ColumnRef> for ColumnSqlRef {
457    #[inline]
458    fn from(value: &ColumnRef) -> Self {
459        Self::from_column_ref_ref(value)
460    }
461}
462
463impl From<ColumnRef> for ColumnSqlRef {
464    #[inline]
465    fn from(value: ColumnRef) -> Self {
466        Self::from_column_ref(value)
467    }
468}
469
470// ==================== Identifier quoting ====================
471
472/// Writes `name` as a double-quoted SQL identifier, doubling any embedded
473/// `"`.
474///
475/// Always uses double quotes, so it is wrong for MySQL. Rendering a [`SQL`](crate::SQL)
476/// fragment picks the right quote for its dialect on its own.
477///
478/// # Examples
479///
480/// ```
481/// let mut buf = String::new();
482/// drizzle_core::sql::write_quoted_ident(&mut buf, r#"odd"name"#);
483/// assert_eq!(buf, r#""odd""name""#);
484/// ```
485#[inline]
486pub fn write_quoted_ident(buf: &mut impl core::fmt::Write, name: &str) {
487    write_dialect_quoted_ident(Dialect::SQLite, buf, name);
488}
489
490/// Writes `name` as a quoted identifier for `dialect`.
491///
492/// PostgreSQL and SQLite use `"..."`; MySQL uses backticks. Embedded quote
493/// characters are doubled so a runtime name cannot end the identifier
494/// early.
495#[inline]
496pub(crate) fn write_dialect_quoted_ident(
497    dialect: Dialect,
498    buf: &mut impl core::fmt::Write,
499    name: &str,
500) {
501    let delimiter = match dialect {
502        Dialect::MySQL => '`',
503        Dialect::SQLite | Dialect::PostgreSQL => '"',
504    };
505
506    let _ = buf.write_char(delimiter);
507    if name.contains(delimiter) {
508        for ch in name.chars() {
509            if ch == delimiter {
510                let _ = buf.write_char(delimiter);
511                let _ = buf.write_char(delimiter);
512            } else {
513                let _ = buf.write_char(ch);
514            }
515        }
516    } else {
517        let _ = buf.write_str(name);
518    }
519    let _ = buf.write_char(delimiter);
520}
521
522// ==================== SQLChunk ====================
523
524/// One piece of a [`SQL`](crate::SQL) fragment.
525///
526/// The renderer puts spaces between chunks where SQL needs them, so chunks
527/// never carry their own surrounding whitespace.
528#[derive(Clone)]
529pub enum SQLChunk<'a, V: SQLParam> {
530    /// A keyword, operator or punctuation: `SELECT`, `=`, `(`, ...
531    Token(Token),
532
533    /// A quoted identifier (table, column or alias name): `"name"`, or
534    /// `` `name` `` for MySQL.
535    Ident(Cow<'a, str>),
536
537    /// Raw SQL text, written as-is with no quoting: function names,
538    /// expressions.
539    Raw(Cow<'a, str>),
540
541    /// An unsigned integer literal written into the SQL text, for clauses
542    /// such as `LIMIT` and `OFFSET` that are not parameterized.
543    Number(usize),
544
545    /// A placeholder and its bound value: `?`, `$1` or `:name`, depending on
546    /// the dialect and the placeholder.
547    Param(Param<'a, V>),
548
549    /// A table, rendered as `"schema"."table"` or `"table"`. Its column names
550    /// are used to expand an empty `SELECT` projection.
551    Table(TableSqlRef),
552
553    /// A column, rendered as `"table"."column"`.
554    Column(ColumnSqlRef),
555}
556
557impl<'a, V: SQLParam> SQLChunk<'a, V> {
558    // ==================== const constructors ====================
559
560    /// Creates a token chunk.
561    #[inline]
562    #[must_use]
563    pub const fn token(t: Token) -> Self {
564        Self::Token(t)
565    }
566
567    /// Creates a quoted identifier from a static string.
568    #[inline]
569    #[must_use]
570    pub const fn ident_static(name: &'static str) -> Self {
571        Self::Ident(Cow::Borrowed(name))
572    }
573
574    /// Creates raw SQL text from a static string.
575    #[inline]
576    #[must_use]
577    pub const fn raw_static(text: &'static str) -> Self {
578        Self::Raw(Cow::Borrowed(text))
579    }
580
581    /// Creates a table chunk.
582    #[inline]
583    #[must_use]
584    pub const fn table(table: TableRef) -> Self {
585        Self::Table(TableSqlRef::from_table_ref(table))
586    }
587
588    /// Creates a column chunk.
589    #[inline]
590    #[must_use]
591    pub const fn column(column: ColumnRef) -> Self {
592        Self::Column(ColumnSqlRef::from_column_ref(column))
593    }
594
595    /// Creates a parameter chunk that borrows its value.
596    #[inline]
597    pub const fn param_borrowed(value: &'a V, placeholder: Placeholder) -> Self {
598        Self::Param(Param {
599            value: Some(Cow::Borrowed(value)),
600            placeholder,
601        })
602    }
603
604    // ==================== non-const constructors ====================
605
606    /// Creates a quoted identifier.
607    #[inline]
608    pub fn ident(name: impl Into<Cow<'a, str>>) -> Self {
609        Self::Ident(name.into())
610    }
611
612    /// Creates raw SQL text.
613    #[inline]
614    pub fn raw(text: impl Into<Cow<'a, str>>) -> Self {
615        Self::Raw(text.into())
616    }
617
618    /// Creates an unsigned integer literal chunk.
619    #[inline]
620    #[must_use]
621    pub const fn number(value: usize) -> Self {
622        Self::Number(value)
623    }
624
625    /// Creates a parameter chunk with a value and placeholder.
626    #[inline]
627    pub fn param(value: impl Into<Cow<'a, V>>, placeholder: Placeholder) -> Self {
628        Self::Param(Param {
629            value: Some(value.into()),
630            placeholder,
631        })
632    }
633
634    // ==================== write implementation ====================
635
636    /// Writes the chunk's text to `buf`, without surrounding spaces.
637    #[inline]
638    pub(crate) fn write(&self, buf: &mut impl core::fmt::Write) {
639        match self {
640            SQLChunk::Token(token) => {
641                let _ = buf.write_str(token.as_str());
642            }
643            SQLChunk::Ident(name) => {
644                write_dialect_quoted_ident(V::DIALECT, buf, name);
645            }
646            SQLChunk::Raw(text) => {
647                let _ = buf.write_str(text);
648            }
649            SQLChunk::Number(value) => {
650                let _ = write!(buf, "{value}");
651            }
652            SQLChunk::Param(Param { placeholder, .. }) => {
653                let _ = write!(buf, "{placeholder}");
654            }
655            SQLChunk::Table(t) => {
656                if let Some(schema) = t.schema {
657                    write_dialect_quoted_ident(V::DIALECT, buf, schema);
658                    let _ = buf.write_char('.');
659                }
660                write_dialect_quoted_ident(V::DIALECT, buf, t.name);
661            }
662            SQLChunk::Column(c) => {
663                write_dialect_quoted_ident(V::DIALECT, buf, c.table);
664                let _ = buf.write_char('.');
665                write_dialect_quoted_ident(V::DIALECT, buf, c.name);
666            }
667        }
668    }
669
670    /// Whether the chunk is word-like, needing a space before another
671    /// word-like chunk.
672    #[inline]
673    pub(crate) const fn is_word_like(&self) -> bool {
674        match self {
675            SQLChunk::Token(t) => !t.is_punctuation() && !t.is_operator(),
676            SQLChunk::Ident(_)
677            | SQLChunk::Raw(_)
678            | SQLChunk::Number(_)
679            | SQLChunk::Param(_)
680            | SQLChunk::Table(_)
681            | SQLChunk::Column(_) => true,
682        }
683    }
684}
685
686impl<V: SQLParam + core::fmt::Debug> core::fmt::Debug for SQLChunk<'_, V> {
687    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
688        match self {
689            SQLChunk::Token(token) => f.debug_tuple("Token").field(token).finish(),
690            SQLChunk::Ident(name) => f.debug_tuple("Ident").field(name).finish(),
691            SQLChunk::Raw(text) => f.debug_tuple("Raw").field(text).finish(),
692            SQLChunk::Number(value) => f.debug_tuple("Number").field(value).finish(),
693            SQLChunk::Param(param) => f.debug_tuple("Param").field(param).finish(),
694            SQLChunk::Table(t) => f
695                .debug_tuple("Table")
696                .field(&t.schema)
697                .field(&t.name)
698                .finish(),
699            SQLChunk::Column(c) => f
700                .debug_tuple("Column")
701                .field(&format!("{}.{}", c.table, c.name))
702                .finish(),
703        }
704    }
705}
706
707// ==================== From implementations ====================
708
709impl<V: SQLParam> From<Token> for SQLChunk<'_, V> {
710    #[inline]
711    fn from(value: Token) -> Self {
712        Self::Token(value)
713    }
714}
715
716impl<V: SQLParam> From<TableRef> for SQLChunk<'_, V> {
717    #[inline]
718    fn from(value: TableRef) -> Self {
719        Self::Table(value.into())
720    }
721}
722
723impl<V: SQLParam> From<ColumnRef> for SQLChunk<'_, V> {
724    #[inline]
725    fn from(value: ColumnRef) -> Self {
726        Self::Column(value.into())
727    }
728}
729
730impl<'a, V: SQLParam> From<Param<'a, V>> for SQLChunk<'a, V> {
731    #[inline]
732    fn from(value: Param<'a, V>) -> Self {
733        Self::Param(value)
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use crate::dialect::{Dialect, MySQLDialect, SQLiteDialect};
741    use core::mem::size_of;
742
743    #[allow(dead_code)]
744    #[derive(Clone, Debug)]
745    struct TestParam([usize; 4]);
746
747    impl SQLParam for TestParam {
748        const DIALECT: Dialect = Dialect::SQLite;
749        type DialectMarker = SQLiteDialect;
750    }
751
752    #[derive(Clone, Debug)]
753    struct MySQLTestParam;
754
755    impl SQLParam for MySQLTestParam {
756        const DIALECT: Dialect = Dialect::MySQL;
757        type DialectMarker = MySQLDialect;
758    }
759
760    #[test]
761    fn sql_chunk_stays_slim() {
762        // Param is the dominant variant for this 32-byte test parameter.
763        assert!(size_of::<SQLChunk<'static, TestParam>>() <= 64);
764    }
765
766    #[test]
767    fn quoted_ident_uses_the_dialect_delimiter() {
768        let mut sqlite = String::new();
769        write_dialect_quoted_ident(Dialect::SQLite, &mut sqlite, "account\"owner");
770        assert_eq!(sqlite, "\"account\"\"owner\"");
771
772        let mut postgres = String::new();
773        write_dialect_quoted_ident(Dialect::PostgreSQL, &mut postgres, "account\"owner");
774        assert_eq!(postgres, "\"account\"\"owner\"");
775
776        let mut mysql = String::new();
777        write_dialect_quoted_ident(Dialect::MySQL, &mut mysql, "account`owner");
778        assert_eq!(mysql, "`account``owner`");
779    }
780
781    #[test]
782    fn quoted_ident_keeps_injection_text_inside_the_identifier() {
783        let mut mysql = String::new();
784        write_dialect_quoted_ident(Dialect::MySQL, &mut mysql, "users`; DROP TABLE audit; --");
785        assert_eq!(mysql, "`users``; DROP TABLE audit; --`");
786    }
787
788    #[test]
789    fn table_chunk_preserves_structured_mysql_database_qualification() {
790        let table = TableRef {
791            schema: Some("tenant`db"),
792            ..TableRef::sql("user`accounts", &["id"])
793        };
794        let chunk = SQLChunk::<MySQLTestParam>::table(table);
795        let mut sql = String::new();
796
797        chunk.write(&mut sql);
798
799        assert_eq!(sql, "`tenant``db`.`user``accounts`");
800    }
801}