Skip to main content

drizzle_types/sqlite/ddl/
sql.rs

1//! Renders `SQLite` DDL statements from the runtime schema types.
2//!
3//! [`TableSql`] renders `CREATE TABLE` with all of a table's columns and
4//! constraints. The entity types also get methods here for single
5//! statements and clauses (`add_column_sql`, `create_index_sql`,
6//! `to_constraint_sql`, ...). Identifiers are quoted with backticks; SQL
7//! fragments (types, defaults, expressions) are written as stored.
8
9use crate::alloc_prelude::*;
10use core::fmt::Write;
11
12use super::{
13    CheckConstraint, Column, ForeignKey, Generated, GeneratedType, Index, IndexColumnDef,
14    PrimaryKey, Table, UniqueConstraint, View,
15};
16
17fn quote_ident(ident: &str) -> String {
18    format!("`{}`", ident.replace('`', "``"))
19}
20
21/// Returns `true` when `expr` is fully wrapped in a single pair of balanced
22/// parentheses, e.g. `(a + b)` but not `(a) + (b)`.
23///
24/// This is a tolerant scanner (it does not understand string literals), which
25/// matches the tolerance level of the rest of this module.
26fn is_wrapped_in_parens(expr: &str) -> bool {
27    let bytes = expr.as_bytes();
28    if bytes.len() < 2 || bytes[0] != b'(' || bytes[bytes.len() - 1] != b')' {
29        return false;
30    }
31    let mut depth = 0i32;
32    for (i, ch) in expr.char_indices() {
33        match ch {
34            '(' => depth += 1,
35            ')' => {
36                depth -= 1;
37                if depth == 0 {
38                    return i == expr.len() - 1;
39                }
40            }
41            _ => {}
42        }
43    }
44    false
45}
46
47// =============================================================================
48// Table SQL Generation
49// =============================================================================
50
51/// A table and the entities that belong to it, for rendering
52/// `CREATE TABLE`.
53///
54/// Start with [`TableSql::new`] and add the rest with the builder methods.
55///
56/// # Examples
57///
58/// ```
59/// use drizzle_types::sqlite::ddl::{ColumnDef, TableDef, TableSql};
60///
61/// let table = TableDef::new("users").strict().into_table();
62/// let columns = [
63///     ColumnDef::new("users", "id", "INTEGER").primary_key().into_column(),
64///     ColumnDef::new("users", "name", "TEXT").not_null().into_column(),
65/// ];
66///
67/// let sql = TableSql::new(&table).columns(&columns).create_table_sql();
68/// assert_eq!(
69///     sql,
70///     "CREATE TABLE `users` (\n\t`id` INTEGER PRIMARY KEY,\n\t`name` TEXT NOT NULL\n) STRICT;"
71/// );
72/// ```
73#[derive(Clone, Debug)]
74pub struct TableSql<'a> {
75    /// The table.
76    pub table: &'a Table,
77    /// Its columns, in order.
78    pub columns: &'a [Column],
79    /// Its primary key entity, if any.
80    pub primary_key: Option<&'a PrimaryKey>,
81    /// Its foreign keys.
82    pub foreign_keys: &'a [ForeignKey],
83    /// Its unique constraints.
84    pub unique_constraints: &'a [UniqueConstraint],
85    /// Its check constraints.
86    pub check_constraints: &'a [CheckConstraint],
87}
88
89impl<'a> TableSql<'a> {
90    /// Starts with just the table: no columns or constraints.
91    #[must_use]
92    pub const fn new(table: &'a Table) -> Self {
93        Self {
94            table,
95            columns: &[],
96            primary_key: None,
97            foreign_keys: &[],
98            unique_constraints: &[],
99            check_constraints: &[],
100        }
101    }
102
103    /// Sets the columns.
104    #[must_use]
105    pub const fn columns(mut self, columns: &'a [Column]) -> Self {
106        self.columns = columns;
107        self
108    }
109
110    /// Sets the primary key entity.
111    #[must_use]
112    pub const fn primary_key(mut self, pk: Option<&'a PrimaryKey>) -> Self {
113        self.primary_key = pk;
114        self
115    }
116
117    /// Sets the foreign keys.
118    #[must_use]
119    pub const fn foreign_keys(mut self, fks: &'a [ForeignKey]) -> Self {
120        self.foreign_keys = fks;
121        self
122    }
123
124    /// Sets the unique constraints.
125    #[must_use]
126    pub const fn unique_constraints(mut self, uniques: &'a [UniqueConstraint]) -> Self {
127        self.unique_constraints = uniques;
128        self
129    }
130
131    /// Sets the check constraints.
132    #[must_use]
133    pub const fn check_constraints(mut self, checks: &'a [CheckConstraint]) -> Self {
134        self.check_constraints = checks;
135        self
136    }
137
138    /// Renders the `CREATE TABLE` statement, ending in `;`.
139    ///
140    /// A single-column primary key or unique constraint without an explicit
141    /// name is written inline on the column; composite or named ones become
142    /// table constraints. Columns flagged `primary_key` but not covered by
143    /// the primary key entity also get a primary key. `WITHOUT ROWID` and
144    /// `STRICT` are appended from the table.
145    #[must_use]
146    pub fn create_table_sql(&self) -> String {
147        let mut sql = format!("CREATE TABLE {} (\n", quote_ident(self.table.name()));
148
149        let mut lines = Vec::new();
150
151        // Columns whose `primary_key` flag is set but which no PrimaryKey
152        // entity covers still need a PRIMARY KEY clause (e.g. columns built via
153        // `ColumnDef::new(..).primary_key()` without a PrimaryKey entity).
154        let flag_pk_columns: Vec<&str> = self
155            .columns
156            .iter()
157            .filter(|c| {
158                c.is_primary_key()
159                    && !self
160                        .primary_key
161                        .as_ref()
162                        .is_some_and(|pk| pk.columns.iter().any(|pc| *pc == c.name()))
163            })
164            .map(Column::name)
165            .collect();
166
167        // Column definitions
168        for column in self.columns {
169            let is_entity_inline_pk = self.primary_key.as_ref().is_some_and(|pk| {
170                pk.columns.len() == 1
171                    && pk.columns.iter().any(|c| *c == column.name())
172                    && !pk.name_explicit
173            });
174            let is_flag_inline_pk =
175                flag_pk_columns.len() == 1 && flag_pk_columns[0] == column.name();
176            let is_inline_pk = is_entity_inline_pk || is_flag_inline_pk;
177
178            let is_inline_unique = self.unique_constraints.iter().any(|u| {
179                u.columns.len() == 1
180                    && u.columns.iter().any(|c| *c == column.name())
181                    && !u.name_explicit
182            });
183
184            lines.push(format!(
185                "\t{}",
186                column.to_column_sql(is_inline_pk, is_inline_unique)
187            ));
188        }
189
190        // Composite or named primary key
191        if let Some(pk) = &self.primary_key
192            && (pk.columns.len() > 1 || pk.name_explicit)
193        {
194            let cols = pk
195                .columns
196                .iter()
197                .map(|c| quote_ident(c))
198                .collect::<Vec<_>>()
199                .join(", ");
200            lines.push(format!(
201                "\tCONSTRAINT {} PRIMARY KEY({})",
202                quote_ident(pk.name()),
203                cols
204            ));
205        }
206
207        // Composite primary key declared only through column flags (no entity):
208        // rendering each column with an inline PRIMARY KEY would be invalid SQL,
209        // so emit a single table-level PRIMARY KEY clause instead.
210        if self.primary_key.is_none() && flag_pk_columns.len() > 1 {
211            let cols = flag_pk_columns
212                .iter()
213                .map(|c| quote_ident(c))
214                .collect::<Vec<_>>()
215                .join(", ");
216            lines.push(format!("\tPRIMARY KEY({cols})"));
217        }
218
219        // Foreign keys
220        for fk in self.foreign_keys {
221            lines.push(format!("\t{}", fk.to_constraint_sql()));
222        }
223
224        // Multi-column unique constraints
225        for unique in self
226            .unique_constraints
227            .iter()
228            .filter(|u| u.columns.len() > 1 || u.name_explicit)
229        {
230            let cols = unique
231                .columns
232                .iter()
233                .map(|c| quote_ident(c))
234                .collect::<Vec<_>>()
235                .join(", ");
236            lines.push(format!(
237                "\tCONSTRAINT {} UNIQUE({})",
238                quote_ident(unique.name()),
239                cols
240            ));
241        }
242
243        // Check constraints
244        for check in self.check_constraints {
245            lines.push(format!(
246                "\tCONSTRAINT {} CHECK({})",
247                quote_ident(check.name()),
248                check.value
249            ));
250        }
251
252        sql.push_str(&lines.join(",\n"));
253        sql.push_str("\n)");
254
255        // Table options
256        let mut options = Vec::new();
257        if self.table.without_rowid {
258            options.push("WITHOUT ROWID");
259        }
260        if self.table.strict {
261            options.push("STRICT");
262        }
263        if !options.is_empty() {
264            let _ = write!(sql, " {}", options.join(", "));
265        }
266
267        sql.push(';');
268        sql
269    }
270
271    /// Renders `DROP TABLE` for the table.
272    #[must_use]
273    pub fn drop_table_sql(&self) -> String {
274        format!("DROP TABLE {};", quote_ident(self.table.name()))
275    }
276}
277
278// =============================================================================
279// Column SQL Generation
280// =============================================================================
281
282impl Column {
283    /// Renders the column definition as used inside `CREATE TABLE`, such as
284    /// `` `name` TEXT NOT NULL ``.
285    ///
286    /// `inline_pk` adds `PRIMARY KEY` (and `AUTOINCREMENT`, which is
287    /// otherwise dropped); `inline_unique` adds `UNIQUE`. `NOT NULL` is left
288    /// out only on an inline `INTEGER PRIMARY KEY`, which aliases the rowid
289    /// and is never NULL.
290    #[must_use]
291    pub fn to_column_sql(&self, inline_pk: bool, inline_unique: bool) -> String {
292        let mut sql = format!(
293            "{} {}",
294            quote_ident(self.name()),
295            self.sql_type().to_uppercase()
296        );
297
298        if inline_pk {
299            sql.push_str(" PRIMARY KEY");
300            // AUTOINCREMENT is only valid immediately after an inline
301            // `PRIMARY KEY`; emitting it anywhere else is a syntax error, so it
302            // is intentionally dropped when this column is not the inline PK.
303            if self.autoincrement.unwrap_or(false) {
304                sql.push_str(" AUTOINCREMENT");
305            }
306        }
307
308        if let Some(default) = self.default.as_ref() {
309            let _ = write!(sql, " DEFAULT {default}");
310        }
311
312        if let Some(generated) = &self.generated {
313            sql.push_str(&generated.to_sql());
314        }
315
316        // NOT NULL is implied for an INTEGER PRIMARY KEY, which aliases the
317        // rowid. Only the exact type name `INTEGER` does that: any other
318        // primary key (`INT`, `BIGINT`, ...) accepts NULL unless it says
319        // NOT NULL, so keep the constraint there.
320        if self.not_null && !(inline_pk && self.sql_type().eq_ignore_ascii_case("integer")) {
321            sql.push_str(" NOT NULL");
322        }
323
324        if inline_unique && !inline_pk {
325            sql.push_str(" UNIQUE");
326        }
327
328        // COLLATE applies to comparisons on this column. SQLite parses it as a
329        // column-constraint, so it follows other inline constraints.
330        if let Some(collate) = self.collate.as_ref() {
331            let _ = write!(sql, " COLLATE {collate}");
332        }
333
334        sql
335    }
336
337    /// Renders `ALTER TABLE ... ADD COLUMN ...;`.
338    #[must_use]
339    pub fn add_column_sql(&self) -> String {
340        format!(
341            "ALTER TABLE {} ADD COLUMN {};",
342            quote_ident(self.table()),
343            self.to_column_sql(false, false)
344        )
345    }
346
347    /// Renders `ALTER TABLE ... DROP COLUMN ...;`.
348    #[must_use]
349    pub fn drop_column_sql(&self) -> String {
350        format!(
351            "ALTER TABLE {} DROP COLUMN {};",
352            quote_ident(self.table()),
353            quote_ident(self.name())
354        )
355    }
356}
357
358// =============================================================================
359// Generated Column SQL
360// =============================================================================
361
362impl Generated {
363    /// Renders ` GENERATED ALWAYS AS (expr) STORED` (or `VIRTUAL`), with a
364    /// leading space.
365    ///
366    /// The expression is wrapped in parentheses unless it already is (the
367    /// table macros store parenthesized expressions; introspection stores
368    /// bare ones).
369    #[must_use]
370    pub fn to_sql(&self) -> String {
371        let gen_type = match self.gen_type {
372            GeneratedType::Stored => "STORED",
373            GeneratedType::Virtual => "VIRTUAL",
374        };
375        let expression = self.expression.trim();
376        if is_wrapped_in_parens(expression) {
377            format!(" GENERATED ALWAYS AS {expression} {gen_type}")
378        } else {
379            format!(" GENERATED ALWAYS AS ({expression}) {gen_type}")
380        }
381    }
382}
383
384// =============================================================================
385// Foreign Key SQL Generation
386// =============================================================================
387
388impl ForeignKey {
389    /// Renders `CONSTRAINT name FOREIGN KEY (...) REFERENCES table(...)`,
390    /// with `ON UPDATE` / `ON DELETE` unless they are `NO ACTION`.
391    #[must_use]
392    pub fn to_constraint_sql(&self) -> String {
393        let from_cols = self
394            .columns
395            .iter()
396            .map(|c| quote_ident(c))
397            .collect::<Vec<_>>()
398            .join(", ");
399
400        let to_cols = self
401            .columns_to
402            .iter()
403            .map(|c| quote_ident(c))
404            .collect::<Vec<_>>()
405            .join(", ");
406
407        let mut sql = format!(
408            "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({})",
409            quote_ident(self.name()),
410            from_cols,
411            quote_ident(&self.table_to),
412            to_cols
413        );
414
415        if let Some(on_update) = self.on_update.as_ref()
416            && on_update != "NO ACTION"
417        {
418            let _ = write!(sql, " ON UPDATE {on_update}");
419        }
420
421        if let Some(on_delete) = self.on_delete.as_ref()
422            && on_delete != "NO ACTION"
423        {
424            let _ = write!(sql, " ON DELETE {on_delete}");
425        }
426
427        sql
428    }
429
430    /// Returns a SQL comment: `SQLite` cannot add a foreign key to an
431    /// existing table without recreating it.
432    #[must_use]
433    pub fn add_fk_sql(&self) -> String {
434        // SQLite doesn't support ADD CONSTRAINT for foreign keys directly
435        // This would require table recreation
436        format!(
437            "-- SQLite requires table recreation to add foreign keys\n-- FK: {} on {}",
438            self.name(),
439            quote_ident(self.table())
440        )
441    }
442
443    /// Returns a SQL comment: `SQLite` cannot drop a foreign key without
444    /// recreating the table.
445    #[must_use]
446    pub fn drop_fk_sql(&self) -> String {
447        format!(
448            "-- SQLite requires table recreation to drop foreign keys\n-- FK: {} on {}",
449            self.name(),
450            quote_ident(self.table())
451        )
452    }
453}
454
455// =============================================================================
456// Index SQL Generation
457// =============================================================================
458
459impl Index {
460    /// Renders `CREATE [UNIQUE] INDEX ... ON table(...)`, with the partial
461    /// index `WHERE` clause if set.
462    #[must_use]
463    pub fn create_index_sql(&self) -> String {
464        let unique = if self.is_unique { "UNIQUE " } else { "" };
465
466        let columns = self
467            .columns
468            .iter()
469            .map(super::index::IndexColumn::to_sql)
470            .collect::<Vec<_>>()
471            .join(", ");
472
473        let mut sql = format!(
474            "CREATE {}INDEX {} ON {}({});",
475            unique,
476            quote_ident(self.name()),
477            quote_ident(self.table()),
478            columns
479        );
480
481        if let Some(where_clause) = self.where_clause.as_ref() {
482            // Remove trailing semicolon to add WHERE
483            sql.pop();
484            let _ = write!(sql, " WHERE {where_clause};");
485        }
486
487        sql
488    }
489
490    /// Renders `DROP INDEX ...;`.
491    #[must_use]
492    pub fn drop_index_sql(&self) -> String {
493        format!("DROP INDEX {};", quote_ident(self.name()))
494    }
495}
496
497impl IndexColumnDef {
498    /// Renders the key part: the quoted column name, or the expression as
499    /// written.
500    #[must_use]
501    pub fn to_sql(&self) -> String {
502        if self.is_expression {
503            self.value.to_string()
504        } else {
505            quote_ident(self.value)
506        }
507    }
508}
509
510// =============================================================================
511// View SQL Generation
512// =============================================================================
513
514impl View {
515    /// Renders `CREATE VIEW name AS definition;`, or a SQL comment when the
516    /// view has no definition.
517    #[must_use]
518    pub fn create_view_sql(&self) -> String {
519        self.definition.as_ref().map_or_else(
520            || format!("-- View {} has no definition", quote_ident(self.name())),
521            |def| format!("CREATE VIEW {} AS {};", quote_ident(self.name()), def),
522        )
523    }
524
525    /// Renders `DROP VIEW ...;`.
526    #[must_use]
527    pub fn drop_view_sql(&self) -> String {
528        format!("DROP VIEW {};", quote_ident(self.name()))
529    }
530}
531
532// =============================================================================
533// Table-level utilities
534// =============================================================================
535
536impl Table {
537    /// Renders `DROP TABLE ...;`.
538    #[must_use]
539    pub fn drop_table_sql(&self) -> String {
540        format!("DROP TABLE {};", quote_ident(self.name()))
541    }
542
543    /// Renders `ALTER TABLE ... RENAME TO new_name;`.
544    #[must_use]
545    pub fn rename_table_sql(&self, new_name: &str) -> String {
546        format!(
547            "ALTER TABLE {} RENAME TO {};",
548            quote_ident(self.name()),
549            quote_ident(new_name)
550        )
551    }
552}
553
554// =============================================================================
555// Primary Key SQL Generation
556// =============================================================================
557
558impl PrimaryKey {
559    /// Renders `CONSTRAINT name PRIMARY KEY(...)`.
560    #[must_use]
561    pub fn to_constraint_sql(&self) -> String {
562        let cols = self
563            .columns
564            .iter()
565            .map(|c| quote_ident(c))
566            .collect::<Vec<_>>()
567            .join(", ");
568
569        format!(
570            "CONSTRAINT {} PRIMARY KEY({})",
571            quote_ident(self.name()),
572            cols
573        )
574    }
575}
576
577// =============================================================================
578// Unique Constraint SQL Generation
579// =============================================================================
580
581impl UniqueConstraint {
582    /// Renders `CONSTRAINT name UNIQUE(...)`.
583    #[must_use]
584    pub fn to_constraint_sql(&self) -> String {
585        let cols = self
586            .columns
587            .iter()
588            .map(|c| quote_ident(c))
589            .collect::<Vec<_>>()
590            .join(", ");
591
592        format!("CONSTRAINT {} UNIQUE({})", quote_ident(self.name()), cols)
593    }
594}
595
596// =============================================================================
597// Check Constraint SQL Generation
598// =============================================================================
599
600impl CheckConstraint {
601    /// Renders `CONSTRAINT name CHECK(expression)`.
602    #[must_use]
603    pub fn to_constraint_sql(&self) -> String {
604        format!(
605            "CONSTRAINT {} CHECK({})",
606            quote_ident(self.name()),
607            self.value
608        )
609    }
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use crate::sqlite::ddl::{
616        ColumnDef, ForeignKeyDef, IndexColumnDef, IndexDef, PrimaryKeyDef, ReferentialAction,
617        TableDef,
618    };
619    use std::borrow::Cow;
620
621    #[test]
622    fn test_simple_create_table() {
623        let table = TableDef::new("users").into_table();
624        let columns = [
625            ColumnDef::new("users", "id", "INTEGER")
626                .primary_key()
627                .autoincrement()
628                .into_column(),
629            ColumnDef::new("users", "name", "TEXT")
630                .not_null()
631                .into_column(),
632            ColumnDef::new("users", "email", "TEXT").into_column(),
633        ];
634        const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
635        let pk = PrimaryKeyDef::new("users", "users_pk")
636            .columns(PK_COLS)
637            .into_primary_key();
638
639        let sql = TableSql::new(&table)
640            .columns(&columns)
641            .primary_key(Some(&pk))
642            .create_table_sql();
643
644        assert!(sql.contains("CREATE TABLE `users`"));
645        assert!(sql.contains("`id` INTEGER PRIMARY KEY AUTOINCREMENT"));
646        assert!(sql.contains("`name` TEXT NOT NULL"));
647        assert!(sql.contains("`email` TEXT"));
648    }
649
650    #[test]
651    fn test_only_integer_primary_key_drops_not_null() {
652        // `INTEGER PRIMARY KEY` aliases the rowid and can never hold NULL.
653        // Any other primary key type accepts NULL without the constraint.
654        for (sql_type, expected) in [
655            ("INTEGER", "`id` INTEGER PRIMARY KEY"),
656            ("integer", "`id` INTEGER PRIMARY KEY"),
657            ("INT", "`id` INT PRIMARY KEY NOT NULL"),
658            ("BIGINT", "`id` BIGINT PRIMARY KEY NOT NULL"),
659            ("TEXT", "`id` TEXT PRIMARY KEY NOT NULL"),
660        ] {
661            let column = ColumnDef::new("t", "id", sql_type)
662                .primary_key()
663                .not_null()
664                .into_column();
665            assert_eq!(column.to_column_sql(true, false), expected);
666        }
667    }
668
669    #[test]
670    fn test_table_with_foreign_key() {
671        let table = TableDef::new("posts").into_table();
672        let columns = [
673            ColumnDef::new("posts", "id", "INTEGER")
674                .primary_key()
675                .into_column(),
676            ColumnDef::new("posts", "user_id", "INTEGER")
677                .not_null()
678                .into_column(),
679        ];
680        const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
681        let pk = PrimaryKeyDef::new("posts", "posts_pk")
682            .columns(PK_COLS)
683            .into_primary_key();
684        const FK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("user_id")];
685        const FK_REFS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
686        let fks = [ForeignKeyDef::new("posts", "posts_user_id_fk")
687            .columns(FK_COLS)
688            .references("users", FK_REFS)
689            .on_delete(ReferentialAction::Cascade)
690            .into_foreign_key()];
691
692        let sql = TableSql::new(&table)
693            .columns(&columns)
694            .primary_key(Some(&pk))
695            .foreign_keys(&fks)
696            .create_table_sql();
697
698        assert!(sql.contains("FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)"));
699        assert!(sql.contains("ON DELETE CASCADE"));
700    }
701
702    #[test]
703    fn test_create_index() {
704        const COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
705        let index = IndexDef::new("users", "users_email_idx")
706            .columns(COLS)
707            .unique()
708            .into_index();
709
710        let sql = index.create_index_sql();
711        assert_eq!(
712            sql,
713            "CREATE UNIQUE INDEX `users_email_idx` ON `users`(`email`);"
714        );
715    }
716
717    #[test]
718    fn test_column_flag_primary_key_renders_inline_without_entity() {
719        // A column built via ColumnDef::primary_key().autoincrement() must
720        // render an inline PRIMARY KEY AUTOINCREMENT even when no PrimaryKey
721        // entity exists.
722        let table = TableDef::new("users").into_table();
723        let columns = [
724            ColumnDef::new("users", "id", "INTEGER")
725                .primary_key()
726                .autoincrement()
727                .into_column(),
728            ColumnDef::new("users", "name", "TEXT")
729                .not_null()
730                .into_column(),
731        ];
732
733        let sql = TableSql::new(&table).columns(&columns).create_table_sql();
734
735        assert!(
736            sql.contains("`id` INTEGER PRIMARY KEY AUTOINCREMENT"),
737            "expected inline PRIMARY KEY AUTOINCREMENT, got: {sql}"
738        );
739        assert!(
740            !sql.contains("INTEGER AUTOINCREMENT"),
741            "orphan AUTOINCREMENT without PRIMARY KEY: {sql}"
742        );
743    }
744
745    #[test]
746    fn test_column_flag_composite_primary_key_renders_table_constraint() {
747        let table = TableDef::new("pair").into_table();
748        let columns = [
749            ColumnDef::new("pair", "a", "INTEGER")
750                .primary_key()
751                .into_column(),
752            ColumnDef::new("pair", "b", "INTEGER")
753                .primary_key()
754                .into_column(),
755        ];
756
757        let sql = TableSql::new(&table).columns(&columns).create_table_sql();
758
759        assert!(
760            sql.contains("PRIMARY KEY(`a`, `b`)"),
761            "expected composite PRIMARY KEY clause, got: {sql}"
762        );
763        assert_eq!(
764            sql.matches("PRIMARY KEY").count(),
765            1,
766            "composite flag PK must render exactly one PRIMARY KEY clause: {sql}"
767        );
768    }
769
770    #[test]
771    fn test_generated_expression_is_parenthesized() {
772        use crate::sqlite::ddl::{Generated, GeneratedType};
773
774        let bare = Generated {
775            expression: Cow::Borrowed("length(name)"),
776            gen_type: GeneratedType::Stored,
777        };
778        assert_eq!(bare.to_sql(), " GENERATED ALWAYS AS (length(name)) STORED");
779
780        // Already-parenthesized expressions (macro canonical form) must not be
781        // double-wrapped.
782        let wrapped = Generated {
783            expression: Cow::Borrowed("(length(name))"),
784            gen_type: GeneratedType::Virtual,
785        };
786        assert_eq!(
787            wrapped.to_sql(),
788            " GENERATED ALWAYS AS (length(name)) VIRTUAL"
789        );
790
791        // `(a) + (b)` starts and ends with parens but is NOT fully wrapped.
792        let tricky = Generated {
793            expression: Cow::Borrowed("(a) + (b)"),
794            gen_type: GeneratedType::Virtual,
795        };
796        assert_eq!(tricky.to_sql(), " GENERATED ALWAYS AS ((a) + (b)) VIRTUAL");
797    }
798
799    #[test]
800    fn test_strict_without_rowid() {
801        let table = TableDef::new("data").strict().without_rowid().into_table();
802        let columns = [ColumnDef::new("data", "key", "TEXT")
803            .primary_key()
804            .not_null()
805            .into_column()];
806        const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("key")];
807        let pk = PrimaryKeyDef::new("data", "data_pk")
808            .columns(PK_COLS)
809            .into_primary_key();
810
811        let sql = TableSql::new(&table)
812            .columns(&columns)
813            .primary_key(Some(&pk))
814            .create_table_sql();
815
816        assert!(sql.ends_with("WITHOUT ROWID, STRICT;"));
817    }
818}