1use crate::dialect::{ColumnType, Dialect, quote_qualified, validate_identifier};
16use crate::Database;
17use rustlavel_core::Result;
18
19#[derive(Debug, Clone)]
25enum Default {
26 Literal(String),
28 Now,
29 Uuid,
30}
31
32#[derive(Debug, Clone)]
34pub struct Column {
35 name: String,
36 kind: ColumnType,
37 nullable: bool,
38 unique: bool,
39 primary: bool,
40 default: Option<Default>,
41 references: Option<(String, String)>,
43 on_delete: Option<&'static str>,
44 index: bool,
45}
46
47impl Column {
48 fn new(name: &str, kind: ColumnType) -> Self {
49 Column {
50 name: name.to_string(),
51 kind,
52 nullable: false,
53 unique: false,
54 primary: false,
55 default: None,
56 references: None,
57 on_delete: None,
58 index: false,
59 }
60 }
61
62 pub fn nullable(&mut self) -> &mut Self {
65 self.nullable = true;
66 self
67 }
68
69 pub fn unique(&mut self) -> &mut Self {
70 self.unique = true;
71 self
72 }
73
74 pub fn primary(&mut self) -> &mut Self {
75 self.primary = true;
76 self
77 }
78
79 pub fn index(&mut self) -> &mut Self {
80 self.index = true;
81 self
82 }
83
84 pub fn default(&mut self, value: &str) -> &mut Self {
87 self.default = Some(Default::Literal(format!("'{}'", value.replace('\'', "''"))));
88 self
89 }
90
91 pub fn default_int(&mut self, value: i64) -> &mut Self {
92 self.default = Some(Default::Literal(value.to_string()));
93 self
94 }
95
96 pub fn default_bool(&mut self, value: bool) -> &mut Self {
99 self.default = Some(Default::Literal(if value { "TRUE_LITERAL" } else { "FALSE_LITERAL" }.into()));
100 self
101 }
102
103 pub fn default_now(&mut self) -> &mut Self {
105 self.default = Some(Default::Now);
106 self
107 }
108
109 pub fn default_raw(&mut self, expression: &str) -> &mut Self {
112 self.default = Some(Default::Literal(expression.to_string()));
113 self
114 }
115
116 pub fn references(&mut self, table: &str, column: &str) -> &mut Self {
118 self.references = Some((table.to_string(), column.to_string()));
119 self
120 }
121
122 pub fn cascade_on_delete(&mut self) -> &mut Self {
124 self.on_delete = Some("cascade");
125 self
126 }
127
128 pub fn null_on_delete(&mut self) -> &mut Self {
129 self.on_delete = Some("set null");
130 self
131 }
132
133 fn to_sql(&self, dialect: &dyn Dialect) -> Result<String> {
134 validate_identifier(&self.name, dialect.max_identifier_length())?;
135 let mut sql = format!("{} {}", dialect.quote(&self.name), dialect.column_type(&self.kind));
136
137 if self.primary {
138 sql.push_str(" primary key");
139 } else if !self.nullable {
140 sql.push_str(" not null");
141 }
142
143 if self.unique && !self.primary {
144 sql.push_str(" unique");
145 }
146
147 if let Some(default) = &self.default {
148 let rendered = match default {
149 Default::Now => dialect.now().to_string(),
150 Default::Uuid => match dialect.uuid_default() {
151 Some(expression) => expression.to_string(),
152 None => return Err(rustlavel_core::Error::msg(format!(
155 "`{}` cannot default a uuid column: {} has no expression for it. \
156 Generate the id in the application instead.",
157 self.name,
158 dialect.name()
159 ))),
160 },
161 Default::Literal(literal) if literal == "TRUE_LITERAL" => {
162 if dialect.booleans_are_integers() { "1".into() } else { "true".into() }
163 }
164 Default::Literal(literal) if literal == "FALSE_LITERAL" => {
165 if dialect.booleans_are_integers() { "0".into() } else { "false".into() }
166 }
167 Default::Literal(literal) => literal.clone(),
168 };
169 sql.push_str(&format!(" default {rendered}"));
170 }
171
172 Ok(sql)
177 }
178
179 fn foreign_key_sql(&self, dialect: &dyn Dialect, table: &str) -> Result<Option<String>> {
181 let Some((target, target_column)) = &self.references else { return Ok(None) };
182
183 let limit = dialect.max_identifier_length();
184 validate_identifier(target_column, limit)?;
185
186 let name = format!("{table}_{}_foreign", self.name);
188 validate_identifier(&name, limit)?;
189
190 let mut sql = format!(
191 "constraint {} foreign key ({}) references {} ({})",
192 dialect.quote(&name),
193 dialect.quote(&self.name),
194 quote_qualified(dialect, target)?,
195 dialect.quote(target_column)
196 );
197 if let Some(action) = self.on_delete {
198 sql.push_str(&format!(" on delete {action}"));
199 }
200 Ok(Some(sql))
201 }
202}
203
204#[derive(Default)]
206pub struct Table {
207 columns: Vec<Column>,
208 indexes: Vec<(Vec<String>, bool)>,
210 drops: Vec<String>,
211}
212
213impl Table {
214 fn add(&mut self, column: Column) -> &mut Column {
215 self.columns.push(column);
216 self.columns.last_mut().expect("just pushed")
217 }
218
219 pub fn id(&mut self) -> &mut Column {
221 let mut column = Column::new("id", ColumnType::Id);
222 column.primary = true;
223 self.add(column)
224 }
225
226 pub fn uuid_id(&mut self) -> &mut Column {
228 let mut column = Column::new("id", ColumnType::UuidId);
229 column.primary = true;
230 column.default = Some(Default::Uuid);
231 self.add(column)
232 }
233
234 pub fn string(&mut self, name: &str) -> &mut Column {
235 self.add(Column::new(name, ColumnType::String { length: 255 }))
236 }
237
238 pub fn string_with(&mut self, name: &str, length: u32) -> &mut Column {
239 self.add(Column::new(name, ColumnType::String { length }))
240 }
241
242 pub fn text(&mut self, name: &str) -> &mut Column {
243 self.add(Column::new(name, ColumnType::Text))
244 }
245
246 pub fn integer(&mut self, name: &str) -> &mut Column {
247 self.add(Column::new(name, ColumnType::Integer))
248 }
249
250 pub fn big_integer(&mut self, name: &str) -> &mut Column {
251 self.add(Column::new(name, ColumnType::BigInteger))
252 }
253
254 pub fn float(&mut self, name: &str) -> &mut Column {
255 self.add(Column::new(name, ColumnType::Float))
256 }
257
258 pub fn decimal(&mut self, name: &str, precision: u32, scale: u32) -> &mut Column {
260 self.add(Column::new(name, ColumnType::Decimal { precision, scale }))
261 }
262
263 pub fn boolean(&mut self, name: &str) -> &mut Column {
264 self.add(Column::new(name, ColumnType::Boolean))
265 }
266
267 pub fn json(&mut self, name: &str) -> &mut Column {
268 self.add(Column::new(name, ColumnType::Json))
269 }
270
271 pub fn uuid(&mut self, name: &str) -> &mut Column {
272 self.add(Column::new(name, ColumnType::Uuid))
273 }
274
275 pub fn date(&mut self, name: &str) -> &mut Column {
276 self.add(Column::new(name, ColumnType::Date))
277 }
278
279 pub fn timestamp(&mut self, name: &str) -> &mut Column {
280 self.add(Column::new(name, ColumnType::Timestamp))
281 }
282
283 pub fn binary(&mut self, name: &str) -> &mut Column {
284 self.add(Column::new(name, ColumnType::Binary))
285 }
286
287 pub fn foreign_id(&mut self, singular: &str) -> &mut Column {
290 let name = format!("{singular}_id");
291 let table = crate::migration::pluralize(singular);
292 let mut column = Column::new(&name, ColumnType::BigInteger);
293 column.references = Some((table, "id".to_string()));
294 column.index = true;
295 self.add(column)
296 }
297
298 pub fn timestamps(&mut self) {
303 self.add(Column::new("created_at", ColumnType::Timestamp)).default_now();
304 self.add(Column::new("updated_at", ColumnType::Timestamp)).default_now();
305 }
306
307 pub fn soft_deletes(&mut self) {
309 self.add(Column::new("deleted_at", ColumnType::Timestamp)).nullable();
310 }
311
312 pub fn index(&mut self, columns: &[&str]) {
314 self.indexes.push((columns.iter().map(|c| (*c).to_string()).collect(), false));
315 }
316
317 pub fn unique(&mut self, columns: &[&str]) {
318 self.indexes.push((columns.iter().map(|c| (*c).to_string()).collect(), true));
319 }
320
321 pub fn drop_column(&mut self, name: &str) {
323 self.drops.push(name.to_string());
324 }
325}
326
327pub struct Schema<'a> {
329 db: &'a Database,
330}
331
332impl<'a> Schema<'a> {
333 pub fn new(db: &'a Database) -> Self {
334 Schema { db }
335 }
336
337 pub async fn create(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
339 for statement in create_statements(self.db.dialect(), table, define)? {
340 self.db.run(&statement).await?;
341 }
342 Ok(())
343 }
344
345 pub async fn create_if_missing(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
360 let dialect = self.db.dialect();
361 let mut statements = create_statements(dialect, table, define)?;
362 let create = statements.remove(0);
363
364 let create = match dialect.supports_if_not_exists_table() {
365 true => create.replacen("create table ", "create table if not exists ", 1),
366 false => create,
367 };
368 match self.db.run(&create).await {
369 Ok(_) => {}
370 Err(error) if is_already_exists(&error) => return Ok(()),
371 Err(error) => return Err(error),
372 }
373
374 for statement in statements {
375 if let Err(error) = self.db.run(&statement).await
376 && !is_already_exists(&error)
377 {
378 return Err(error);
379 }
380 }
381 Ok(())
382 }
383
384 pub async fn alter(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
386 for statement in alter_statements(self.db.dialect(), table, define)? {
387 self.db.run(&statement).await?;
388 }
389 Ok(())
390 }
391
392 pub async fn drop(&self, table: &str) -> Result<()> {
393 let dialect = self.db.dialect();
394 let quoted = quote_qualified(dialect, table)?;
395 let sql = match dialect.name() {
398 "postgres" => format!("drop table if exists {quoted} cascade"),
399 "sqlserver" => format!("drop table if exists {quoted}"),
400 _ => format!("drop table if exists {quoted}"),
401 };
402 self.db.run(&sql).await?;
403 Ok(())
404 }
405
406 pub async fn rename(&self, from: &str, to: &str) -> Result<()> {
407 self.db
408 .run(&format!(
409 "alter table {} rename to {}",
410 quote_qualified(self.db.dialect(), from)?,
411 quote_qualified(self.db.dialect(), to)?
412 ))
413 .await?;
414 Ok(())
415 }
416
417 pub async fn has_table(&self, table: &str) -> Result<bool> {
419 let dialect = self.db.dialect();
420 let found = self
421 .db
422 .scalar::<i64>(
423 &format!(
424 "select count(*) from information_schema.tables \
425 where table_schema = {} and table_name = {}",
426 dialect.current_schema_expression(),
427 dialect.placeholder(1)
428 ),
429 &[crate::Value::from(table)],
430 )
431 .await?;
432 Ok(found.unwrap_or(0) > 0)
433 }
434
435 pub async fn has_column(&self, table: &str, column: &str) -> Result<bool> {
436 let dialect = self.db.dialect();
437 let found = self
438 .db
439 .scalar::<i64>(
440 &format!(
441 "select count(*) from information_schema.columns \
442 where table_schema = {} and table_name = {} and column_name = {}",
443 dialect.current_schema_expression(),
444 dialect.placeholder(1),
445 dialect.placeholder(2)
446 ),
447 &[crate::Value::from(table), crate::Value::from(column)],
448 )
449 .await?;
450 Ok(found.unwrap_or(0) > 0)
451 }
452}
453
454pub fn create_statements(
456 dialect: &dyn Dialect,
457 table: &str,
458 define: impl FnOnce(&mut Table),
459) -> Result<Vec<String>> {
460 let mut definition = Table::default();
461 define(&mut definition);
462
463 let quoted = quote_qualified(dialect, table)?;
464 let mut lines: Vec<String> = definition
465 .columns
466 .iter()
467 .map(|column| column.to_sql(dialect))
468 .collect::<Result<_>>()?;
469
470 for column in &definition.columns {
471 if let Some(constraint) = column.foreign_key_sql(dialect, table)? {
472 lines.push(constraint);
473 }
474 }
475
476 let mut statements = vec![format!("create table {quoted} (\n {}\n)", lines.join(",\n "))];
477 statements.extend(index_statements(dialect, table, &definition)?);
478 Ok(statements)
479}
480
481fn is_already_exists(error: &rustlavel_core::Error) -> bool {
484 let text = error.to_string().to_ascii_lowercase();
485 text.contains("already exists")
486 || text.contains("42p07")
487 || text.contains("pg_type_typname_nsp_index")
490 || text.contains("there is already an object")
491}
492
493pub fn alter_statements(
495 dialect: &dyn Dialect,
496 table: &str,
497 define: impl FnOnce(&mut Table),
498) -> Result<Vec<String>> {
499 let mut definition = Table::default();
500 define(&mut definition);
501
502 let quoted = quote_qualified(dialect, table)?;
503 let mut statements = Vec::new();
504
505 for column in &definition.columns {
506 statements.push(format!(
507 "alter table {quoted} {} {}",
508 dialect.add_column_clause(),
509 column.to_sql(dialect)?
510 ));
511 if let Some(constraint) = column.foreign_key_sql(dialect, table)? {
514 statements.push(format!("alter table {quoted} add {constraint}"));
515 }
516 }
517 for name in &definition.drops {
518 validate_identifier(name, dialect.max_identifier_length())?;
519 statements.push(format!("alter table {quoted} drop column {}", dialect.quote(name)));
520 }
521
522 statements.extend(index_statements(dialect, table, &definition)?);
523 Ok(statements)
524}
525
526fn index_statements(
527 dialect: &dyn Dialect,
528 table: &str,
529 definition: &Table,
530) -> Result<Vec<String>> {
531 let quoted = quote_qualified(dialect, table)?;
532 let guard = if dialect.supports_if_not_exists_index() { "if not exists " } else { "" };
536 let limit = dialect.max_identifier_length();
537 let mut statements = Vec::new();
538
539 for column in definition.columns.iter().filter(|c| c.index) {
540 validate_identifier(&column.name, limit)?;
541 let name = format!("{table}_{}_index", column.name);
542 validate_identifier(&name, limit)?;
543 statements.push(format!(
544 "create index {guard}{} on {quoted} ({})",
545 dialect.quote(&name),
546 dialect.quote(&column.name)
547 ));
548 }
549
550 for (columns, unique) in &definition.indexes {
551 for column in columns {
552 validate_identifier(column, limit)?;
553 }
554 let name =
555 format!("{table}_{}_{}", columns.join("_"), if *unique { "unique" } else { "index" });
556 validate_identifier(&name, limit)?;
557 let quoted_columns: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
558 statements.push(format!(
559 "create {}index {guard}{} on {quoted} ({})",
560 if *unique { "unique " } else { "" },
561 dialect.quote(&name),
562 quoted_columns.join(", ")
563 ));
564 }
565
566 Ok(statements)
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572 use crate::dialect::{MySql, Postgres, SqlServer};
573
574 #[test]
575 fn builds_a_create_table_statement() {
576 let statements = create_statements(&Postgres, "users", |t| {
577 t.id();
578 t.string("name");
579 t.string("email").unique();
580 t.boolean("active").default_bool(true);
581 t.text("bio").nullable();
582 t.timestamps();
583 })
584 .unwrap();
585
586 assert_eq!(
587 statements[0],
588 "create table \"users\" (\n \
589 \"id\" bigserial primary key,\n \
590 \"name\" varchar(255) not null,\n \
591 \"email\" varchar(255) not null unique,\n \
592 \"active\" boolean not null default true,\n \
593 \"bio\" text,\n \
594 \"created_at\" timestamptz not null default now(),\n \
595 \"updated_at\" timestamptz not null default now()\n)"
596 );
597 }
598
599 #[test]
600 fn a_foreign_id_points_at_the_pluralized_table_and_gets_an_index() {
601 let statements = create_statements(&Postgres, "posts", |t| {
602 t.id();
603 t.foreign_id("user").cascade_on_delete();
604 })
605 .unwrap();
606
607 assert!(statements[0].contains("\"user_id\" bigint not null,"), "{}", statements[0]);
610 assert!(
611 statements[0].contains(
612 "constraint \"posts_user_id_foreign\" foreign key (\"user_id\") \
613 references \"users\" (\"id\") on delete cascade"
614 ),
615 "{}",
616 statements[0]
617 );
618 assert_eq!(
619 statements[1],
620 "create index if not exists \"posts_user_id_index\" on \"posts\" (\"user_id\")"
621 );
622 }
623
624 #[test]
625 fn composite_indexes_get_their_own_statements() {
626 let statements = create_statements(&Postgres, "memberships", |t| {
627 t.id();
628 t.integer("team_id");
629 t.integer("user_id");
630 t.unique(&["team_id", "user_id"]);
631 })
632 .unwrap();
633
634 assert_eq!(
635 statements[1],
636 "create unique index if not exists \"memberships_team_id_user_id_unique\" \
637 on \"memberships\" (\"team_id\", \"user_id\")"
638 );
639 }
640
641 #[test]
642 fn alter_adds_and_drops_columns() {
643 let statements = alter_statements(&Postgres, "users", |t| {
644 t.string("nickname").nullable();
645 t.drop_column("legacy_flag");
646 })
647 .unwrap();
648
649 assert_eq!(statements[0], "alter table \"users\" add column \"nickname\" varchar(255)");
650 assert_eq!(statements[1], "alter table \"users\" drop column \"legacy_flag\"");
651
652 let sqlserver = alter_statements(&SqlServer, "users", |t| {
654 t.string("nickname").nullable();
655 t.drop_column("legacy_flag");
656 })
657 .unwrap();
658 assert_eq!(sqlserver[0], "alter table [users] add [nickname] nvarchar(255)");
659 assert_eq!(sqlserver[1], "alter table [users] drop column [legacy_flag]");
660 }
661
662 #[test]
663 fn a_malicious_column_name_is_rejected() {
664 let error = create_statements(&Postgres, "users", |t| {
665 t.string("name\"; drop table users; --");
666 })
667 .unwrap_err();
668
669 assert!(error.to_string().contains("not a valid SQL identifier"));
670 }
671
672 #[test]
673 fn a_string_default_is_quoted_and_escaped() {
674 let statements = create_statements(&Postgres, "t", |t| {
675 t.string("motto").default("it's fine");
676 })
677 .unwrap();
678
679 assert!(statements[0].contains("default 'it''s fine'"));
680 }
681
682 #[test]
683 fn one_schema_definition_produces_correct_ddl_for_every_database() {
684 let define = |t: &mut Table| {
685 t.id();
686 t.string("email").unique();
687 t.boolean("active").default_bool(true);
688 t.timestamps();
689 };
690
691 let postgres = create_statements(&Postgres, "users", define).unwrap();
692 assert!(postgres[0].contains("\"id\" bigserial primary key"), "{}", postgres[0]);
693 assert!(postgres[0].contains("\"active\" boolean not null default true"));
694 assert!(postgres[0].contains("default now()"));
695
696 let mysql = create_statements(&MySql, "users", define).unwrap();
697 assert!(
698 mysql[0].contains("`id` bigint not null auto_increment primary key"),
699 "{}",
700 mysql[0]
701 );
702 assert!(mysql[0].contains("`active` tinyint(1) not null default 1"), "{}", mysql[0]);
704 assert!(mysql[0].contains("default current_timestamp(6)"));
705
706 let sqlserver = create_statements(&SqlServer, "users", define).unwrap();
707 assert!(sqlserver[0].contains("[id] bigint identity(1,1) primary key"), "{}", sqlserver[0]);
708 assert!(sqlserver[0].contains("[active] bit not null default 1"), "{}", sqlserver[0]);
709 assert!(sqlserver[0].contains("default sysutcdatetime()"));
710 }
711
712 #[test]
713 fn only_postgres_guards_an_index_with_if_not_exists() {
714 let define = |t: &mut Table| {
715 t.id();
716 t.integer("team_id").index();
717 };
718
719 assert!(create_statements(&Postgres, "m", define).unwrap()[1].contains("if not exists"));
720 assert!(!create_statements(&MySql, "m", define).unwrap()[1].contains("if not exists"));
723 assert!(!create_statements(&SqlServer, "m", define).unwrap()[1].contains("if not exists"));
724 }
725
726 #[test]
727 fn a_uuid_default_mysql_cannot_express_is_refused_rather_than_faked() {
728 let define = |t: &mut Table| {
729 t.uuid_id();
730 };
731
732 assert!(create_statements(&Postgres, "t", define).unwrap()[0].contains("gen_random_uuid()"));
733 assert!(create_statements(&SqlServer, "t", define).unwrap()[0].contains("newid()"));
734
735 let error = create_statements(&MySql, "t", define).unwrap_err().to_string();
736 assert!(error.contains("has no expression for it"), "{error}");
737 }
738
739 #[test]
740 fn soft_deletes_add_a_nullable_timestamp() {
741 let statements = create_statements(&Postgres, "posts", |t| {
742 t.id();
743 t.soft_deletes();
744 })
745 .unwrap();
746
747 assert!(statements[0].contains("\"deleted_at\" timestamptz"));
748 assert!(!statements[0].contains("\"deleted_at\" timestamptz not null"));
749 }
750}