1use 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
21fn 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#[derive(Clone, Debug)]
74pub struct TableSql<'a> {
75 pub table: &'a Table,
77 pub columns: &'a [Column],
79 pub primary_key: Option<&'a PrimaryKey>,
81 pub foreign_keys: &'a [ForeignKey],
83 pub unique_constraints: &'a [UniqueConstraint],
85 pub check_constraints: &'a [CheckConstraint],
87}
88
89impl<'a> TableSql<'a> {
90 #[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 #[must_use]
105 pub const fn columns(mut self, columns: &'a [Column]) -> Self {
106 self.columns = columns;
107 self
108 }
109
110 #[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 #[must_use]
119 pub const fn foreign_keys(mut self, fks: &'a [ForeignKey]) -> Self {
120 self.foreign_keys = fks;
121 self
122 }
123
124 #[must_use]
126 pub const fn unique_constraints(mut self, uniques: &'a [UniqueConstraint]) -> Self {
127 self.unique_constraints = uniques;
128 self
129 }
130
131 #[must_use]
133 pub const fn check_constraints(mut self, checks: &'a [CheckConstraint]) -> Self {
134 self.check_constraints = checks;
135 self
136 }
137
138 #[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 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 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 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 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 for fk in self.foreign_keys {
221 lines.push(format!("\t{}", fk.to_constraint_sql()));
222 }
223
224 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 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 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 #[must_use]
273 pub fn drop_table_sql(&self) -> String {
274 format!("DROP TABLE {};", quote_ident(self.table.name()))
275 }
276}
277
278impl Column {
283 #[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 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 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 if let Some(collate) = self.collate.as_ref() {
331 let _ = write!(sql, " COLLATE {collate}");
332 }
333
334 sql
335 }
336
337 #[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 #[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
358impl Generated {
363 #[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
384impl ForeignKey {
389 #[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 #[must_use]
433 pub fn add_fk_sql(&self) -> String {
434 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 #[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
455impl Index {
460 #[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 sql.pop();
484 let _ = write!(sql, " WHERE {where_clause};");
485 }
486
487 sql
488 }
489
490 #[must_use]
492 pub fn drop_index_sql(&self) -> String {
493 format!("DROP INDEX {};", quote_ident(self.name()))
494 }
495}
496
497impl IndexColumnDef {
498 #[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
510impl View {
515 #[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 #[must_use]
527 pub fn drop_view_sql(&self) -> String {
528 format!("DROP VIEW {};", quote_ident(self.name()))
529 }
530}
531
532impl Table {
537 #[must_use]
539 pub fn drop_table_sql(&self) -> String {
540 format!("DROP TABLE {};", quote_ident(self.name()))
541 }
542
543 #[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
554impl PrimaryKey {
559 #[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
577impl UniqueConstraint {
582 #[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
596impl CheckConstraint {
601 #[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 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 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 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 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}