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 alter(&self, table: &str, define: impl FnOnce(&mut Table)) -> Result<()> {
347 for statement in alter_statements(self.db.dialect(), table, define)? {
348 self.db.run(&statement).await?;
349 }
350 Ok(())
351 }
352
353 pub async fn drop(&self, table: &str) -> Result<()> {
354 let dialect = self.db.dialect();
355 let quoted = quote_qualified(dialect, table)?;
356 let sql = match dialect.name() {
359 "postgres" => format!("drop table if exists {quoted} cascade"),
360 "sqlserver" => format!("drop table if exists {quoted}"),
361 _ => format!("drop table if exists {quoted}"),
362 };
363 self.db.run(&sql).await?;
364 Ok(())
365 }
366
367 pub async fn rename(&self, from: &str, to: &str) -> Result<()> {
368 self.db
369 .run(&format!(
370 "alter table {} rename to {}",
371 quote_qualified(self.db.dialect(), from)?,
372 quote_qualified(self.db.dialect(), to)?
373 ))
374 .await?;
375 Ok(())
376 }
377
378 pub async fn has_table(&self, table: &str) -> Result<bool> {
380 let dialect = self.db.dialect();
381 let found = self
382 .db
383 .scalar::<i64>(
384 &format!(
385 "select count(*) from information_schema.tables \
386 where table_schema = {} and table_name = {}",
387 dialect.current_schema_expression(),
388 dialect.placeholder(1)
389 ),
390 &[crate::Value::from(table)],
391 )
392 .await?;
393 Ok(found.unwrap_or(0) > 0)
394 }
395
396 pub async fn has_column(&self, table: &str, column: &str) -> Result<bool> {
397 let dialect = self.db.dialect();
398 let found = self
399 .db
400 .scalar::<i64>(
401 &format!(
402 "select count(*) from information_schema.columns \
403 where table_schema = {} and table_name = {} and column_name = {}",
404 dialect.current_schema_expression(),
405 dialect.placeholder(1),
406 dialect.placeholder(2)
407 ),
408 &[crate::Value::from(table), crate::Value::from(column)],
409 )
410 .await?;
411 Ok(found.unwrap_or(0) > 0)
412 }
413}
414
415pub fn create_statements(
417 dialect: &dyn Dialect,
418 table: &str,
419 define: impl FnOnce(&mut Table),
420) -> Result<Vec<String>> {
421 let mut definition = Table::default();
422 define(&mut definition);
423
424 let quoted = quote_qualified(dialect, table)?;
425 let mut lines: Vec<String> = definition
426 .columns
427 .iter()
428 .map(|column| column.to_sql(dialect))
429 .collect::<Result<_>>()?;
430
431 for column in &definition.columns {
432 if let Some(constraint) = column.foreign_key_sql(dialect, table)? {
433 lines.push(constraint);
434 }
435 }
436
437 let mut statements = vec![format!("create table {quoted} (\n {}\n)", lines.join(",\n "))];
438 statements.extend(index_statements(dialect, table, &definition)?);
439 Ok(statements)
440}
441
442pub fn alter_statements(
444 dialect: &dyn Dialect,
445 table: &str,
446 define: impl FnOnce(&mut Table),
447) -> Result<Vec<String>> {
448 let mut definition = Table::default();
449 define(&mut definition);
450
451 let quoted = quote_qualified(dialect, table)?;
452 let mut statements = Vec::new();
453
454 for column in &definition.columns {
455 statements.push(format!(
456 "alter table {quoted} {} {}",
457 dialect.add_column_clause(),
458 column.to_sql(dialect)?
459 ));
460 if let Some(constraint) = column.foreign_key_sql(dialect, table)? {
463 statements.push(format!("alter table {quoted} add {constraint}"));
464 }
465 }
466 for name in &definition.drops {
467 validate_identifier(name, dialect.max_identifier_length())?;
468 statements.push(format!("alter table {quoted} drop column {}", dialect.quote(name)));
469 }
470
471 statements.extend(index_statements(dialect, table, &definition)?);
472 Ok(statements)
473}
474
475fn index_statements(
476 dialect: &dyn Dialect,
477 table: &str,
478 definition: &Table,
479) -> Result<Vec<String>> {
480 let quoted = quote_qualified(dialect, table)?;
481 let guard = if dialect.supports_if_not_exists_index() { "if not exists " } else { "" };
485 let limit = dialect.max_identifier_length();
486 let mut statements = Vec::new();
487
488 for column in definition.columns.iter().filter(|c| c.index) {
489 validate_identifier(&column.name, limit)?;
490 let name = format!("{table}_{}_index", column.name);
491 validate_identifier(&name, limit)?;
492 statements.push(format!(
493 "create index {guard}{} on {quoted} ({})",
494 dialect.quote(&name),
495 dialect.quote(&column.name)
496 ));
497 }
498
499 for (columns, unique) in &definition.indexes {
500 for column in columns {
501 validate_identifier(column, limit)?;
502 }
503 let name =
504 format!("{table}_{}_{}", columns.join("_"), if *unique { "unique" } else { "index" });
505 validate_identifier(&name, limit)?;
506 let quoted_columns: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
507 statements.push(format!(
508 "create {}index {guard}{} on {quoted} ({})",
509 if *unique { "unique " } else { "" },
510 dialect.quote(&name),
511 quoted_columns.join(", ")
512 ));
513 }
514
515 Ok(statements)
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::dialect::{MySql, Postgres, SqlServer};
522
523 #[test]
524 fn builds_a_create_table_statement() {
525 let statements = create_statements(&Postgres, "users", |t| {
526 t.id();
527 t.string("name");
528 t.string("email").unique();
529 t.boolean("active").default_bool(true);
530 t.text("bio").nullable();
531 t.timestamps();
532 })
533 .unwrap();
534
535 assert_eq!(
536 statements[0],
537 "create table \"users\" (\n \
538 \"id\" bigserial primary key,\n \
539 \"name\" varchar(255) not null,\n \
540 \"email\" varchar(255) not null unique,\n \
541 \"active\" boolean not null default true,\n \
542 \"bio\" text,\n \
543 \"created_at\" timestamptz not null default now(),\n \
544 \"updated_at\" timestamptz not null default now()\n)"
545 );
546 }
547
548 #[test]
549 fn a_foreign_id_points_at_the_pluralized_table_and_gets_an_index() {
550 let statements = create_statements(&Postgres, "posts", |t| {
551 t.id();
552 t.foreign_id("user").cascade_on_delete();
553 })
554 .unwrap();
555
556 assert!(statements[0].contains("\"user_id\" bigint not null,"), "{}", statements[0]);
559 assert!(
560 statements[0].contains(
561 "constraint \"posts_user_id_foreign\" foreign key (\"user_id\") \
562 references \"users\" (\"id\") on delete cascade"
563 ),
564 "{}",
565 statements[0]
566 );
567 assert_eq!(
568 statements[1],
569 "create index if not exists \"posts_user_id_index\" on \"posts\" (\"user_id\")"
570 );
571 }
572
573 #[test]
574 fn composite_indexes_get_their_own_statements() {
575 let statements = create_statements(&Postgres, "memberships", |t| {
576 t.id();
577 t.integer("team_id");
578 t.integer("user_id");
579 t.unique(&["team_id", "user_id"]);
580 })
581 .unwrap();
582
583 assert_eq!(
584 statements[1],
585 "create unique index if not exists \"memberships_team_id_user_id_unique\" \
586 on \"memberships\" (\"team_id\", \"user_id\")"
587 );
588 }
589
590 #[test]
591 fn alter_adds_and_drops_columns() {
592 let statements = alter_statements(&Postgres, "users", |t| {
593 t.string("nickname").nullable();
594 t.drop_column("legacy_flag");
595 })
596 .unwrap();
597
598 assert_eq!(statements[0], "alter table \"users\" add column \"nickname\" varchar(255)");
599 assert_eq!(statements[1], "alter table \"users\" drop column \"legacy_flag\"");
600
601 let sqlserver = alter_statements(&SqlServer, "users", |t| {
603 t.string("nickname").nullable();
604 t.drop_column("legacy_flag");
605 })
606 .unwrap();
607 assert_eq!(sqlserver[0], "alter table [users] add [nickname] nvarchar(255)");
608 assert_eq!(sqlserver[1], "alter table [users] drop column [legacy_flag]");
609 }
610
611 #[test]
612 fn a_malicious_column_name_is_rejected() {
613 let error = create_statements(&Postgres, "users", |t| {
614 t.string("name\"; drop table users; --");
615 })
616 .unwrap_err();
617
618 assert!(error.to_string().contains("not a valid SQL identifier"));
619 }
620
621 #[test]
622 fn a_string_default_is_quoted_and_escaped() {
623 let statements = create_statements(&Postgres, "t", |t| {
624 t.string("motto").default("it's fine");
625 })
626 .unwrap();
627
628 assert!(statements[0].contains("default 'it''s fine'"));
629 }
630
631 #[test]
632 fn one_schema_definition_produces_correct_ddl_for_every_database() {
633 let define = |t: &mut Table| {
634 t.id();
635 t.string("email").unique();
636 t.boolean("active").default_bool(true);
637 t.timestamps();
638 };
639
640 let postgres = create_statements(&Postgres, "users", define).unwrap();
641 assert!(postgres[0].contains("\"id\" bigserial primary key"), "{}", postgres[0]);
642 assert!(postgres[0].contains("\"active\" boolean not null default true"));
643 assert!(postgres[0].contains("default now()"));
644
645 let mysql = create_statements(&MySql, "users", define).unwrap();
646 assert!(
647 mysql[0].contains("`id` bigint not null auto_increment primary key"),
648 "{}",
649 mysql[0]
650 );
651 assert!(mysql[0].contains("`active` tinyint(1) not null default 1"), "{}", mysql[0]);
653 assert!(mysql[0].contains("default current_timestamp(6)"));
654
655 let sqlserver = create_statements(&SqlServer, "users", define).unwrap();
656 assert!(sqlserver[0].contains("[id] bigint identity(1,1) primary key"), "{}", sqlserver[0]);
657 assert!(sqlserver[0].contains("[active] bit not null default 1"), "{}", sqlserver[0]);
658 assert!(sqlserver[0].contains("default sysutcdatetime()"));
659 }
660
661 #[test]
662 fn only_postgres_guards_an_index_with_if_not_exists() {
663 let define = |t: &mut Table| {
664 t.id();
665 t.integer("team_id").index();
666 };
667
668 assert!(create_statements(&Postgres, "m", define).unwrap()[1].contains("if not exists"));
669 assert!(!create_statements(&MySql, "m", define).unwrap()[1].contains("if not exists"));
672 assert!(!create_statements(&SqlServer, "m", define).unwrap()[1].contains("if not exists"));
673 }
674
675 #[test]
676 fn a_uuid_default_mysql_cannot_express_is_refused_rather_than_faked() {
677 let define = |t: &mut Table| {
678 t.uuid_id();
679 };
680
681 assert!(create_statements(&Postgres, "t", define).unwrap()[0].contains("gen_random_uuid()"));
682 assert!(create_statements(&SqlServer, "t", define).unwrap()[0].contains("newid()"));
683
684 let error = create_statements(&MySql, "t", define).unwrap_err().to_string();
685 assert!(error.contains("has no expression for it"), "{error}");
686 }
687
688 #[test]
689 fn soft_deletes_add_a_nullable_timestamp() {
690 let statements = create_statements(&Postgres, "posts", |t| {
691 t.id();
692 t.soft_deletes();
693 })
694 .unwrap();
695
696 assert!(statements[0].contains("\"deleted_at\" timestamptz"));
697 assert!(!statements[0].contains("\"deleted_at\" timestamptz not null"));
698 }
699}