1use super::MigrationDialect;
4use crate::ast::DataType;
5use crate::migrations::column_builder::{ColumnDefinition, DefaultValue};
6use crate::migrations::operation::{
7 AlterColumnChange, AlterColumnOp, CreateTableOp, DropIndexOp, RenameColumnOp, RenameTableOp,
8};
9use crate::schema::RustTypeMapping;
10
11#[derive(Debug, Clone, Copy, Default)]
19pub struct DuckDbDialect;
20
21impl DuckDbDialect {
22 #[must_use]
24 pub const fn new() -> Self {
25 Self
26 }
27
28 fn column_def_with_table(&self, col: &ColumnDefinition, table: &str) -> String {
32 let data_type = self.map_data_type(&col.data_type);
33 let mut sql = format!("{} {}", self.quote_identifier(&col.name), data_type);
34
35 if col.primary_key {
36 sql.push_str(" PRIMARY KEY");
37 } else {
38 if !col.nullable {
39 sql.push_str(" NOT NULL");
40 }
41 if col.unique {
42 sql.push_str(" UNIQUE");
43 }
44 }
45
46 if col.autoincrement && col.default.is_none() {
47 sql.push_str(&format!(" DEFAULT nextval('seq_{}_{}')", table, col.name,));
48 } else if let Some(ref default) = col.default {
49 sql.push_str(" DEFAULT ");
50 sql.push_str(&self.render_default(default));
51 }
52
53 if let Some(ref fk) = col.references {
54 sql.push_str(" REFERENCES ");
55 sql.push_str(&self.quote_identifier(&fk.table));
56 sql.push_str(" (");
57 sql.push_str(&self.quote_identifier(&fk.column));
58 sql.push(')');
59 if let Some(action) = fk.on_delete {
60 sql.push_str(" ON DELETE ");
61 sql.push_str(action.as_sql());
62 }
63 if let Some(action) = fk.on_update {
64 sql.push_str(" ON UPDATE ");
65 sql.push_str(action.as_sql());
66 }
67 }
68
69 if let Some(ref check) = col.check {
70 sql.push_str(&format!(" CHECK ({})", check));
71 }
72
73 if let Some(ref collation) = col.collation {
74 sql.push_str(&format!(" COLLATE \"{}\"", collation));
75 }
76
77 sql
78 }
79}
80
81impl MigrationDialect for DuckDbDialect {
82 fn name(&self) -> &'static str {
83 "duckdb"
84 }
85
86 fn map_data_type(&self, dt: &DataType) -> String {
87 match dt {
88 DataType::Smallint => "SMALLINT".to_string(),
89 DataType::Integer => "INTEGER".to_string(),
90 DataType::Bigint => "BIGINT".to_string(),
91 DataType::Real => "REAL".to_string(),
92 DataType::Double => "DOUBLE".to_string(),
93 DataType::Decimal { precision, scale } => match (precision, scale) {
94 (Some(p), Some(s)) => format!("DECIMAL({p}, {s})"),
95 (Some(p), None) => format!("DECIMAL({p})"),
96 _ => "DECIMAL".to_string(),
97 },
98 DataType::Numeric { precision, scale } => match (precision, scale) {
99 (Some(p), Some(s)) => format!("NUMERIC({p}, {s})"),
100 (Some(p), None) => format!("NUMERIC({p})"),
101 _ => "NUMERIC".to_string(),
102 },
103 DataType::Char(len) => match len {
104 Some(n) => format!("CHAR({n})"),
105 None => "CHAR".to_string(),
106 },
107 DataType::Varchar(len) => match len {
108 Some(n) => format!("VARCHAR({n})"),
109 None => "VARCHAR".to_string(),
110 },
111 DataType::Text => "TEXT".to_string(),
112 DataType::Blob => "BLOB".to_string(),
113 DataType::Binary(len) => match len {
114 Some(n) => format!("BLOB({n})"),
115 None => "BLOB".to_string(),
116 },
117 DataType::Varbinary(len) => match len {
118 Some(n) => format!("BLOB({n})"),
119 None => "BLOB".to_string(),
120 },
121 DataType::Date => "DATE".to_string(),
122 DataType::Time => "TIME".to_string(),
123 DataType::Timestamp => "TIMESTAMP".to_string(),
124 DataType::Datetime => "TIMESTAMP".to_string(),
125 DataType::Boolean => "BOOLEAN".to_string(),
126 DataType::Custom(name) => name.clone(),
127 }
128 }
129
130 fn autoincrement_keyword(&self) -> String {
131 String::new()
133 }
134
135 fn create_table(&self, op: &CreateTableOp) -> String {
136 let mut sql = String::new();
138 for col in &op.columns {
139 if col.autoincrement {
140 sql.push_str(&format!(
141 "CREATE SEQUENCE IF NOT EXISTS \
142 \"seq_{table}_{col}\" START 1;\n",
143 table = op.name,
144 col = col.name,
145 ));
146 }
147 }
148
149 sql.push_str("CREATE TABLE ");
150 if op.if_not_exists {
151 sql.push_str("IF NOT EXISTS ");
152 }
153 sql.push_str(&self.quote_identifier(&op.name));
154 sql.push_str(" (\n");
155
156 let column_defs: Vec<String> = op
157 .columns
158 .iter()
159 .map(|c| format!(" {}", self.column_def_with_table(c, &op.name)))
160 .collect();
161 sql.push_str(&column_defs.join(",\n"));
162
163 if !op.constraints.is_empty() {
164 sql.push_str(",\n");
165 let constraint_defs: Vec<String> = op
166 .constraints
167 .iter()
168 .map(|c| format!(" {}", self.table_constraint(c)))
169 .collect();
170 sql.push_str(&constraint_defs.join(",\n"));
171 }
172
173 sql.push_str("\n)");
174 sql
175 }
176
177 fn render_default(&self, default: &DefaultValue) -> String {
178 match default {
179 DefaultValue::Boolean(b) => {
180 if *b {
181 "TRUE".to_string()
182 } else {
183 "FALSE".to_string()
184 }
185 }
186 _ => default.to_sql(),
187 }
188 }
189
190 fn rename_table(&self, op: &RenameTableOp) -> String {
191 format!(
192 "ALTER TABLE {} RENAME TO {}",
193 self.quote_identifier(&op.old_name),
194 self.quote_identifier(&op.new_name)
195 )
196 }
197
198 fn rename_column(&self, op: &RenameColumnOp) -> String {
199 format!(
200 "ALTER TABLE {} RENAME COLUMN {} TO {}",
201 self.quote_identifier(&op.table),
202 self.quote_identifier(&op.old_name),
203 self.quote_identifier(&op.new_name)
204 )
205 }
206
207 fn alter_column(&self, op: &AlterColumnOp) -> String {
208 let table = self.quote_identifier(&op.table);
209 let column = self.quote_identifier(&op.column);
210
211 match &op.change {
212 AlterColumnChange::SetDataType(dt) => {
213 format!(
214 "ALTER TABLE {} ALTER COLUMN {} SET DATA TYPE {}",
215 table,
216 column,
217 self.map_data_type(dt)
218 )
219 }
220 AlterColumnChange::SetNullable(nullable) => {
221 if *nullable {
222 format!(
223 "ALTER TABLE {} ALTER COLUMN {} DROP NOT NULL",
224 table, column
225 )
226 } else {
227 format!("ALTER TABLE {} ALTER COLUMN {} SET NOT NULL", table, column)
228 }
229 }
230 AlterColumnChange::SetDefault(default) => {
231 format!(
232 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {}",
233 table,
234 column,
235 self.render_default(default)
236 )
237 }
238 AlterColumnChange::DropDefault => {
239 format!("ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT", table, column)
240 }
241 AlterColumnChange::SetUnique(true) => {
242 format!("ALTER TABLE {} ADD UNIQUE ({})", table, column)
243 }
244 AlterColumnChange::SetUnique(false) => {
245 format!(
246 "ALTER TABLE {} DROP CONSTRAINT \"{}_key\"",
247 table, op.column
248 )
249 }
250 AlterColumnChange::SetAutoincrement(_) => {
251 format!(
252 "-- DuckDB cannot ALTER autoincrement \
253 for {}.{}; table recreation required",
254 op.table, op.column
255 )
256 }
257 }
258 }
259
260 fn drop_index(&self, op: &DropIndexOp) -> String {
261 let mut sql = String::from("DROP INDEX ");
262 if op.if_exists {
263 sql.push_str("IF EXISTS ");
264 }
265 sql.push_str(&self.quote_identifier(&op.name));
266 sql
267 }
268
269 fn drop_foreign_key(&self, op: &super::super::operation::DropForeignKeyOp) -> String {
270 format!(
271 "ALTER TABLE {} DROP CONSTRAINT {}",
272 self.quote_identifier(&op.table),
273 self.quote_identifier(&op.name)
274 )
275 }
276}
277
278impl RustTypeMapping for DuckDbDialect {
279 fn map_type(&self, rust_type: &str) -> DataType {
280 match rust_type {
281 "bool" => DataType::Boolean,
282 "i8" | "i16" | "u8" | "u16" => DataType::Smallint,
283 "i32" | "u32" => DataType::Integer,
284 "i64" | "u64" | "i128" | "u128" | "isize" | "usize" => DataType::Bigint,
285 "f32" => DataType::Real,
286 "f64" => DataType::Double,
287 "String" => DataType::Varchar(None),
288 "Vec<u8>" => DataType::Blob,
289 s if s.contains("DateTime") => DataType::Timestamp,
290 s if s.contains("NaiveDate") => DataType::Date,
291 _ => DataType::Text,
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::migrations::column_builder::{integer, varchar};
300 use crate::migrations::operation::{DropTableOp, Operation, RenameColumnOp, RenameTableOp};
301 use crate::migrations::table_builder::CreateTableBuilder;
302
303 #[test]
304 fn test_duckdb_data_types() {
305 let d = DuckDbDialect::new();
306 assert_eq!(d.map_data_type(&DataType::Integer), "INTEGER");
307 assert_eq!(d.map_data_type(&DataType::Bigint), "BIGINT");
308 assert_eq!(d.map_data_type(&DataType::Text), "TEXT");
309 assert_eq!(
310 d.map_data_type(&DataType::Varchar(Some(255))),
311 "VARCHAR(255)"
312 );
313 assert_eq!(d.map_data_type(&DataType::Blob), "BLOB");
314 assert_eq!(d.map_data_type(&DataType::Boolean), "BOOLEAN");
315 assert_eq!(d.map_data_type(&DataType::Timestamp), "TIMESTAMP");
316 assert_eq!(d.map_data_type(&DataType::Double), "DOUBLE");
317 assert_eq!(d.map_data_type(&DataType::Real), "REAL");
318 assert_eq!(d.map_data_type(&DataType::Date), "DATE");
319 assert_eq!(d.map_data_type(&DataType::Time), "TIME");
320 assert_eq!(
321 d.map_data_type(&DataType::Decimal {
322 precision: Some(10),
323 scale: Some(2)
324 }),
325 "DECIMAL(10, 2)"
326 );
327 }
328
329 #[test]
330 fn test_create_table_basic() {
331 let d = DuckDbDialect::new();
332 let op = CreateTableBuilder::new()
333 .name("users")
334 .column(varchar("username", 255).not_null().unique().build())
335 .build();
336
337 let sql = d.create_table(&op);
338 assert_eq!(
339 sql,
340 "CREATE TABLE \"users\" (\n\
341 \x20 \"username\" VARCHAR(255) NOT NULL UNIQUE\n\
342 )"
343 );
344 }
345
346 #[test]
347 fn test_create_table_if_not_exists() {
348 let d = DuckDbDialect::new();
349 let op = CreateTableBuilder::new()
350 .if_not_exists()
351 .name("users")
352 .column(varchar("username", 255).not_null().build())
353 .build();
354
355 let sql = d.create_table(&op);
356 assert!(sql.contains("CREATE TABLE IF NOT EXISTS \"users\""));
357 }
358
359 #[test]
360 fn test_autoincrement_generates_sequence() {
361 let d = DuckDbDialect::new();
362 let op = CreateTableBuilder::new()
363 .name("users")
364 .column(integer("id").primary_key().autoincrement().build())
365 .column(varchar("username", 255).not_null().unique().build())
366 .build();
367
368 let sql = d.create_table(&op);
369
370 assert!(
371 sql.contains(
372 "CREATE SEQUENCE IF NOT EXISTS \
373 \"seq_users_id\" START 1;"
374 ),
375 "Missing sequence DDL in:\n{sql}"
376 );
377 assert!(
378 sql.contains("DEFAULT nextval('seq_users_id')"),
379 "Missing nextval default in:\n{sql}"
380 );
381 assert!(
382 !sql.contains("AUTOINCREMENT"),
383 "Should not contain AUTOINCREMENT keyword"
384 );
385 }
386
387 #[test]
388 fn test_varchar_unique_not_null() {
389 let d = DuckDbDialect::new();
390 let op = CreateTableBuilder::new()
391 .name("items")
392 .column(varchar("domain", 255).not_null().unique().build())
393 .build();
394
395 let sql = d.create_table(&op);
396 assert!(
397 sql.contains("\"domain\" VARCHAR(255) NOT NULL UNIQUE"),
398 "Expected NOT NULL UNIQUE in:\n{sql}"
399 );
400 }
401
402 #[test]
403 fn test_drop_table() {
404 let d = DuckDbDialect::new();
405
406 let op = DropTableOp {
407 name: "users".to_string(),
408 if_exists: false,
409 cascade: false,
410 };
411 assert_eq!(d.drop_table(&op), "DROP TABLE \"users\"");
412
413 let op = DropTableOp {
414 name: "users".to_string(),
415 if_exists: true,
416 cascade: true,
417 };
418 assert_eq!(d.drop_table(&op), "DROP TABLE IF EXISTS \"users\" CASCADE");
419 }
420
421 #[test]
422 fn test_rename_table() {
423 let d = DuckDbDialect::new();
424 let op = RenameTableOp {
425 old_name: "old_users".to_string(),
426 new_name: "users".to_string(),
427 };
428 assert_eq!(
429 d.rename_table(&op),
430 "ALTER TABLE \"old_users\" RENAME TO \"users\""
431 );
432 }
433
434 #[test]
435 fn test_add_column() {
436 let d = DuckDbDialect::new();
437 let op = Operation::add_column("users", varchar("email", 255).not_null().build());
438 if let Operation::AddColumn(ref add_op) = op {
439 let sql = d.add_column(add_op);
440 assert_eq!(
441 sql,
442 "ALTER TABLE \"users\" ADD COLUMN \
443 \"email\" VARCHAR(255) NOT NULL"
444 );
445 }
446 }
447
448 #[test]
449 fn test_drop_column() {
450 let d = DuckDbDialect::new();
451 let op = Operation::drop_column("users", "email");
452 if let Operation::DropColumn(ref drop_op) = op {
453 let sql = d.drop_column(drop_op);
454 assert_eq!(sql, "ALTER TABLE \"users\" DROP COLUMN \"email\"");
455 }
456 }
457
458 #[test]
459 fn test_rename_column() {
460 let d = DuckDbDialect::new();
461 let op = RenameColumnOp {
462 table: "users".to_string(),
463 old_name: "name".to_string(),
464 new_name: "full_name".to_string(),
465 };
466 assert_eq!(
467 d.rename_column(&op),
468 "ALTER TABLE \"users\" RENAME COLUMN \
469 \"name\" TO \"full_name\""
470 );
471 }
472
473 #[test]
474 fn test_alter_column_set_data_type() {
475 let d = DuckDbDialect::new();
476 let op = AlterColumnOp {
477 table: "users".to_string(),
478 column: "age".to_string(),
479 change: AlterColumnChange::SetDataType(DataType::Bigint),
480 };
481 assert_eq!(
482 d.alter_column(&op),
483 "ALTER TABLE \"users\" ALTER COLUMN \"age\" \
484 SET DATA TYPE BIGINT"
485 );
486 }
487
488 #[test]
489 fn test_alter_column_set_not_null() {
490 let d = DuckDbDialect::new();
491 let op = AlterColumnOp {
492 table: "users".to_string(),
493 column: "email".to_string(),
494 change: AlterColumnChange::SetNullable(false),
495 };
496 assert_eq!(
497 d.alter_column(&op),
498 "ALTER TABLE \"users\" ALTER COLUMN \"email\" SET NOT NULL"
499 );
500 }
501
502 #[test]
503 fn test_alter_column_drop_not_null() {
504 let d = DuckDbDialect::new();
505 let op = AlterColumnOp {
506 table: "users".to_string(),
507 column: "email".to_string(),
508 change: AlterColumnChange::SetNullable(true),
509 };
510 assert_eq!(
511 d.alter_column(&op),
512 "ALTER TABLE \"users\" ALTER COLUMN \"email\" DROP NOT NULL"
513 );
514 }
515
516 #[test]
517 fn test_alter_column_set_default() {
518 let d = DuckDbDialect::new();
519 let op = AlterColumnOp {
520 table: "users".to_string(),
521 column: "active".to_string(),
522 change: AlterColumnChange::SetDefault(DefaultValue::Boolean(true)),
523 };
524 assert_eq!(
525 d.alter_column(&op),
526 "ALTER TABLE \"users\" ALTER COLUMN \"active\" \
527 SET DEFAULT TRUE"
528 );
529 }
530
531 #[test]
532 fn test_alter_column_drop_default() {
533 let d = DuckDbDialect::new();
534 let op = AlterColumnOp {
535 table: "users".to_string(),
536 column: "active".to_string(),
537 change: AlterColumnChange::DropDefault,
538 };
539 assert_eq!(
540 d.alter_column(&op),
541 "ALTER TABLE \"users\" ALTER COLUMN \"active\" DROP DEFAULT"
542 );
543 }
544
545 #[test]
546 fn test_create_index() {
547 let d = DuckDbDialect::new();
548 let op = crate::migrations::operation::CreateIndexOp {
549 name: "idx_users_email".to_string(),
550 table: "users".to_string(),
551 columns: vec!["email".to_string()],
552 unique: true,
553 index_type: crate::migrations::operation::IndexType::BTree,
554 if_not_exists: true,
555 condition: None,
556 };
557 assert_eq!(
558 d.create_index(&op),
559 "CREATE UNIQUE INDEX IF NOT EXISTS \"idx_users_email\" \
560 ON \"users\" (\"email\")"
561 );
562 }
563
564 #[test]
565 fn test_drop_index() {
566 let d = DuckDbDialect::new();
567
568 let op = crate::migrations::operation::DropIndexOp {
569 name: "idx_users_email".to_string(),
570 table: None,
571 if_exists: false,
572 };
573 assert_eq!(d.drop_index(&op), "DROP INDEX \"idx_users_email\"");
574
575 let op = crate::migrations::operation::DropIndexOp {
576 name: "idx_users_email".to_string(),
577 table: None,
578 if_exists: true,
579 };
580 assert_eq!(
581 d.drop_index(&op),
582 "DROP INDEX IF EXISTS \"idx_users_email\""
583 );
584 }
585
586 #[test]
587 fn test_drop_foreign_key() {
588 let d = DuckDbDialect::new();
589 let op = crate::migrations::operation::DropForeignKeyOp {
590 table: "invoices".to_string(),
591 name: "fk_invoices_user".to_string(),
592 };
593 assert_eq!(
594 d.drop_foreign_key(&op),
595 "ALTER TABLE \"invoices\" DROP CONSTRAINT \
596 \"fk_invoices_user\""
597 );
598 }
599
600 #[test]
601 fn test_consumer_scenario_two_tables_with_sequences() {
602 let d = DuckDbDialect::new();
603
604 let ops: Vec<Operation> = vec![
605 CreateTableBuilder::new()
606 .if_not_exists()
607 .name("excluded_domains")
608 .column(integer("id").primary_key().autoincrement().build())
609 .column(varchar("domain", 255).not_null().unique().build())
610 .build()
611 .into(),
612 CreateTableBuilder::new()
613 .if_not_exists()
614 .name("excluded_ips")
615 .column(integer("id").primary_key().autoincrement().build())
616 .column(varchar("cidr", 255).not_null().unique().build())
617 .build()
618 .into(),
619 ];
620
621 let sqls: Vec<String> = ops.iter().map(|op| d.generate_sql(op)).collect();
622
623 assert!(
625 sqls[0].contains(
626 "CREATE SEQUENCE IF NOT EXISTS \
627 \"seq_excluded_domains_id\" START 1;"
628 ),
629 "Missing sequence for excluded_domains:\n{}",
630 sqls[0]
631 );
632 assert!(
633 sqls[0].contains("CREATE TABLE IF NOT EXISTS \"excluded_domains\""),
634 "Missing CREATE TABLE:\n{}",
635 sqls[0]
636 );
637 assert!(
638 sqls[0].contains("DEFAULT nextval('seq_excluded_domains_id')"),
639 "Missing nextval default:\n{}",
640 sqls[0]
641 );
642 assert!(
643 sqls[0].contains("\"domain\" VARCHAR(255) NOT NULL UNIQUE"),
644 "Missing domain column:\n{}",
645 sqls[0]
646 );
647
648 assert!(
650 sqls[1].contains(
651 "CREATE SEQUENCE IF NOT EXISTS \
652 \"seq_excluded_ips_id\" START 1;"
653 ),
654 "Missing sequence for excluded_ips:\n{}",
655 sqls[1]
656 );
657 assert!(
658 sqls[1].contains("DEFAULT nextval('seq_excluded_ips_id')"),
659 "Missing nextval default:\n{}",
660 sqls[1]
661 );
662 assert!(
663 sqls[1].contains("\"cidr\" VARCHAR(255) NOT NULL UNIQUE"),
664 "Missing cidr column:\n{}",
665 sqls[1]
666 );
667 }
668}