1use crate::diff::{
4 EnumAlterDiff, EnumDiff, ExtensionDiff, FieldAlterDiff, FieldDiff, IndexDiff, ModelAlterDiff,
5 ModelDiff, SchemaDiff, ViewDiff,
6};
7
8pub struct PostgresSqlGenerator;
10
11impl PostgresSqlGenerator {
12 pub fn generate(&self, diff: &SchemaDiff) -> MigrationSql {
14 let mut up = Vec::new();
15 let mut down = Vec::new();
16
17 for ext in &diff.create_extensions {
19 up.push(self.create_extension(ext));
20 down.push(self.drop_extension(&ext.name));
21 }
22
23 for name in &diff.drop_extensions {
25 up.push(self.drop_extension(name));
26 }
28
29 for enum_diff in &diff.create_enums {
31 up.push(self.create_enum(enum_diff));
32 down.push(self.drop_enum(&enum_diff.name));
33 }
34
35 for name in &diff.drop_enums {
37 up.push(self.drop_enum(name));
38 }
40
41 for alter in &diff.alter_enums {
43 up.extend(self.alter_enum(alter));
44 }
46
47 for model in &diff.create_models {
49 up.push(self.create_table(model));
50 down.push(self.drop_table(&model.table_name));
51 }
52
53 for name in &diff.drop_models {
55 up.push(self.drop_table(name));
56 }
58
59 for alter in &diff.alter_models {
61 up.extend(self.alter_table(alter));
62 }
64
65 for index in &diff.create_indexes {
67 up.push(self.create_index(index));
68 down.push(self.drop_index(&index.name, &index.table_name));
69 }
70
71 for index in &diff.drop_indexes {
73 up.push(self.drop_index(&index.name, &index.table_name));
74 }
75
76 for view in &diff.create_views {
78 up.push(self.create_view(view));
79 down.push(self.drop_view(&view.view_name, view.is_materialized));
80 }
81
82 for name in &diff.drop_views {
84 up.push(self.drop_view(name, false));
86 }
87
88 for view in &diff.alter_views {
90 up.push(self.drop_view(&view.view_name, view.is_materialized));
92 up.push(self.create_view(view));
94 }
95
96 MigrationSql {
97 up: up.join("\n\n"),
98 down: down.join("\n\n"),
99 }
100 }
101
102 fn create_extension(&self, ext: &ExtensionDiff) -> String {
104 let mut sql = format!("CREATE EXTENSION IF NOT EXISTS \"{}\"", ext.name);
105 if let Some(schema) = &ext.schema {
106 sql.push_str(&format!(" SCHEMA \"{}\"", schema));
107 }
108 if let Some(version) = &ext.version {
109 sql.push_str(&format!(" VERSION '{}'", version));
110 }
111 sql.push(';');
112 sql
113 }
114
115 fn drop_extension(&self, name: &str) -> String {
117 format!("DROP EXTENSION IF EXISTS \"{}\" CASCADE;", name)
118 }
119
120 fn create_enum(&self, enum_diff: &EnumDiff) -> String {
122 let values: Vec<String> = enum_diff
123 .values
124 .iter()
125 .map(|v| format!("'{}'", v))
126 .collect();
127 format!(
128 "CREATE TYPE \"{}\" AS ENUM ({});",
129 enum_diff.name,
130 values.join(", ")
131 )
132 }
133
134 fn drop_enum(&self, name: &str) -> String {
136 format!("DROP TYPE IF EXISTS \"{}\";", name)
137 }
138
139 fn alter_enum(&self, alter: &EnumAlterDiff) -> Vec<String> {
141 let mut stmts = Vec::new();
142
143 for value in &alter.add_values {
144 stmts.push(format!(
145 "ALTER TYPE \"{}\" ADD VALUE IF NOT EXISTS '{}';",
146 alter.name, value
147 ));
148 }
149
150 stmts
154 }
155
156 fn create_table(&self, model: &ModelDiff) -> String {
158 let mut columns = Vec::new();
159
160 for field in &model.fields {
161 columns.push(self.column_definition(field));
162 }
163
164 if !model.primary_key.is_empty() {
166 let pk_cols: Vec<String> = model
167 .primary_key
168 .iter()
169 .map(|c| format!("\"{}\"", c))
170 .collect();
171 columns.push(format!("PRIMARY KEY ({})", pk_cols.join(", ")));
172 }
173
174 for uc in &model.unique_constraints {
176 let cols: Vec<String> = uc.columns.iter().map(|c| format!("\"{}\"", c)).collect();
177 let constraint = if let Some(name) = &uc.name {
178 format!("CONSTRAINT \"{}\" UNIQUE ({})", name, cols.join(", "))
179 } else {
180 format!("UNIQUE ({})", cols.join(", "))
181 };
182 columns.push(constraint);
183 }
184
185 format!(
186 "CREATE TABLE \"{}\" (\n {}\n);",
187 model.table_name,
188 columns.join(",\n ")
189 )
190 }
191
192 fn column_definition(&self, field: &FieldDiff) -> String {
194 let mut parts = vec![format!("\"{}\"", field.column_name), field.sql_type.clone()];
195
196 if field.is_auto_increment {
197 if field.sql_type == "INTEGER" {
199 parts[1] = "SERIAL".to_string();
200 } else if field.sql_type == "BIGINT" {
201 parts[1] = "BIGSERIAL".to_string();
202 }
203 }
204
205 if !field.nullable && !field.is_primary_key {
206 parts.push("NOT NULL".to_string());
207 }
208
209 if field.is_unique && !field.is_primary_key {
210 parts.push("UNIQUE".to_string());
211 }
212
213 if let Some(default) = &field.default {
214 parts.push(format!("DEFAULT {}", default));
215 }
216
217 parts.join(" ")
218 }
219
220 fn drop_table(&self, name: &str) -> String {
222 format!("DROP TABLE IF EXISTS \"{}\" CASCADE;", name)
223 }
224
225 fn alter_table(&self, alter: &ModelAlterDiff) -> Vec<String> {
227 let mut stmts = Vec::new();
228
229 for field in &alter.add_fields {
231 stmts.push(format!(
232 "ALTER TABLE \"{}\" ADD COLUMN {};",
233 alter.table_name,
234 self.column_definition(field)
235 ));
236 }
237
238 for name in &alter.drop_fields {
240 stmts.push(format!(
241 "ALTER TABLE \"{}\" DROP COLUMN IF EXISTS \"{}\";",
242 alter.table_name, name
243 ));
244 }
245
246 for field in &alter.alter_fields {
248 stmts.extend(self.alter_column(&alter.table_name, field));
249 }
250
251 for index in &alter.add_indexes {
253 stmts.push(self.create_index(index));
254 }
255
256 for name in &alter.drop_indexes {
258 stmts.push(format!("DROP INDEX IF EXISTS \"{}\";", name));
259 }
260
261 stmts
262 }
263
264 fn alter_column(&self, table: &str, field: &FieldAlterDiff) -> Vec<String> {
266 let mut stmts = Vec::new();
267
268 if let Some(new_type) = &field.new_type {
269 stmts.push(format!(
270 "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" TYPE {} USING \"{}\"::{};",
271 table, field.column_name, new_type, field.column_name, new_type
272 ));
273 }
274
275 if let Some(new_nullable) = field.new_nullable {
276 if new_nullable {
277 stmts.push(format!(
278 "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" DROP NOT NULL;",
279 table, field.column_name
280 ));
281 } else {
282 stmts.push(format!(
283 "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" SET NOT NULL;",
284 table, field.column_name
285 ));
286 }
287 }
288
289 if let Some(new_default) = &field.new_default {
290 stmts.push(format!(
291 "ALTER TABLE \"{}\" ALTER COLUMN \"{}\" SET DEFAULT {};",
292 table, field.column_name, new_default
293 ));
294 }
295
296 stmts
297 }
298
299 fn create_index(&self, index: &IndexDiff) -> String {
301 let unique = if index.unique { "UNIQUE " } else { "" };
302
303 if index.is_vector_index() {
305 return self.create_vector_index(index);
306 }
307
308 let using_clause = match &index.index_type {
310 Some(idx_type) => format!(" USING {}", idx_type.as_sql()),
311 None => String::new(),
312 };
313
314 let cols: Vec<String> = index.columns.iter().map(|c| format!("\"{}\"", c)).collect();
315 format!(
316 "CREATE {}INDEX \"{}\" ON \"{}\"{}({});",
317 unique,
318 index.name,
319 index.table_name,
320 using_clause,
321 cols.join(", ")
322 )
323 }
324
325 fn create_vector_index(&self, index: &IndexDiff) -> String {
327 let index_type = index.index_type.as_ref().unwrap();
328 let ops_class = index
329 .vector_ops
330 .as_ref()
331 .map(|o| o.as_ops_class())
332 .unwrap_or("vector_cosine_ops");
333
334 let col_expr = if index.columns.len() == 1 {
336 format!("\"{}\" {}", index.columns[0], ops_class)
337 } else {
338 index
340 .columns
341 .iter()
342 .map(|c| format!("\"{}\" {}", c, ops_class))
343 .collect::<Vec<_>>()
344 .join(", ")
345 };
346
347 let with_clause = match index_type {
349 prax_schema::ast::IndexType::Hnsw => {
350 let mut params = Vec::new();
351 if let Some(m) = index.hnsw_m {
352 params.push(format!("m = {}", m));
353 }
354 if let Some(ef) = index.hnsw_ef_construction {
355 params.push(format!("ef_construction = {}", ef));
356 }
357 if params.is_empty() {
358 String::new()
359 } else {
360 format!(" WITH ({})", params.join(", "))
361 }
362 }
363 prax_schema::ast::IndexType::IvfFlat => {
364 if let Some(lists) = index.ivfflat_lists {
365 format!(" WITH (lists = {})", lists)
366 } else {
367 String::new()
368 }
369 }
370 _ => String::new(),
371 };
372
373 format!(
374 "CREATE INDEX \"{}\" ON \"{}\" USING {} ({}){};",
375 index.name,
376 index.table_name,
377 index_type.as_sql(),
378 col_expr,
379 with_clause
380 )
381 }
382
383 fn drop_index(&self, name: &str, _table: &str) -> String {
385 format!("DROP INDEX IF EXISTS \"{}\";", name)
386 }
387
388 fn create_view(&self, view: &ViewDiff) -> String {
390 if view.is_materialized {
391 format!(
392 "CREATE MATERIALIZED VIEW \"{}\" AS\n{};",
393 view.view_name, view.sql_query
394 )
395 } else {
396 format!(
397 "CREATE OR REPLACE VIEW \"{}\" AS\n{};",
398 view.view_name, view.sql_query
399 )
400 }
401 }
402
403 fn drop_view(&self, name: &str, is_materialized: bool) -> String {
405 if is_materialized {
406 format!("DROP MATERIALIZED VIEW IF EXISTS \"{}\" CASCADE;", name)
407 } else {
408 format!("DROP VIEW IF EXISTS \"{}\" CASCADE;", name)
409 }
410 }
411
412 #[allow(dead_code)]
414 fn refresh_materialized_view(&self, name: &str, concurrently: bool) -> String {
415 if concurrently {
416 format!("REFRESH MATERIALIZED VIEW CONCURRENTLY \"{}\";", name)
417 } else {
418 format!("REFRESH MATERIALIZED VIEW \"{}\";", name)
419 }
420 }
421}
422
423#[derive(Debug, Clone)]
425pub struct MigrationSql {
426 pub up: String,
428 pub down: String,
430}
431
432impl MigrationSql {
433 pub fn is_empty(&self) -> bool {
435 self.up.trim().is_empty()
436 }
437}
438
439pub struct MySqlGenerator;
441
442impl MySqlGenerator {
443 pub fn generate(&self, diff: &SchemaDiff) -> MigrationSql {
445 let mut up = Vec::new();
446 let mut down = Vec::new();
447
448 for model in &diff.create_models {
453 up.push(self.create_table(model));
454 down.push(self.drop_table(&model.table_name));
455 }
456
457 for name in &diff.drop_models {
459 up.push(self.drop_table(name));
460 }
461
462 for alter in &diff.alter_models {
464 up.extend(self.alter_table(alter));
465 }
466
467 for index in &diff.create_indexes {
469 up.push(self.create_index(index));
470 down.push(self.drop_index(&index.name, &index.table_name));
471 }
472
473 for index in &diff.drop_indexes {
475 up.push(self.drop_index(&index.name, &index.table_name));
476 }
477
478 for view in &diff.create_views {
480 up.push(self.create_view(view));
481 down.push(self.drop_view(&view.view_name));
482 }
483
484 for name in &diff.drop_views {
486 up.push(self.drop_view(name));
487 }
488
489 for view in &diff.alter_views {
491 up.push(self.drop_view(&view.view_name));
492 up.push(self.create_view(view));
493 }
494
495 MigrationSql {
496 up: up.join("\n\n"),
497 down: down.join("\n\n"),
498 }
499 }
500
501 fn create_table(&self, model: &ModelDiff) -> String {
503 let mut columns = Vec::new();
504
505 for field in &model.fields {
506 columns.push(self.column_definition(field));
507 }
508
509 if !model.primary_key.is_empty() {
511 let pk_cols: Vec<String> = model
512 .primary_key
513 .iter()
514 .map(|c| format!("`{}`", c))
515 .collect();
516 columns.push(format!("PRIMARY KEY ({})", pk_cols.join(", ")));
517 }
518
519 for uc in &model.unique_constraints {
521 let cols: Vec<String> = uc.columns.iter().map(|c| format!("`{}`", c)).collect();
522 let constraint = if let Some(name) = &uc.name {
523 format!("CONSTRAINT `{}` UNIQUE ({})", name, cols.join(", "))
524 } else {
525 format!("UNIQUE ({})", cols.join(", "))
526 };
527 columns.push(constraint);
528 }
529
530 format!(
531 "CREATE TABLE `{}` (\n {}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
532 model.table_name,
533 columns.join(",\n ")
534 )
535 }
536
537 fn column_definition(&self, field: &FieldDiff) -> String {
539 let mut parts = vec![format!("`{}`", field.column_name)];
540
541 let sql_type = match field.sql_type.as_str() {
543 "INTEGER" if field.is_auto_increment => "INT AUTO_INCREMENT".to_string(),
544 "INTEGER" => "INT".to_string(),
545 "BIGINT" if field.is_auto_increment => "BIGINT AUTO_INCREMENT".to_string(),
546 "TEXT" => "VARCHAR(255)".to_string(), "DOUBLE PRECISION" => "DOUBLE".to_string(),
548 "TIMESTAMP WITH TIME ZONE" => "DATETIME".to_string(),
549 "BOOLEAN" => "TINYINT(1)".to_string(),
550 "BYTEA" => "BLOB".to_string(),
551 "JSONB" | "JSON" => "JSON".to_string(),
552 other => other.to_string(),
553 };
554 parts.push(sql_type);
555
556 if !field.nullable && !field.is_primary_key {
557 parts.push("NOT NULL".to_string());
558 }
559
560 if field.is_unique && !field.is_primary_key {
561 parts.push("UNIQUE".to_string());
562 }
563
564 if let Some(default) = &field.default {
565 parts.push(format!("DEFAULT {}", default));
566 }
567
568 parts.join(" ")
569 }
570
571 fn drop_table(&self, name: &str) -> String {
573 format!("DROP TABLE IF EXISTS `{}`;", name)
574 }
575
576 fn alter_table(&self, alter: &ModelAlterDiff) -> Vec<String> {
578 let mut stmts = Vec::new();
579
580 for field in &alter.add_fields {
582 stmts.push(format!(
583 "ALTER TABLE `{}` ADD COLUMN {};",
584 alter.table_name,
585 self.column_definition(field)
586 ));
587 }
588
589 for name in &alter.drop_fields {
591 stmts.push(format!(
592 "ALTER TABLE `{}` DROP COLUMN `{}`;",
593 alter.table_name, name
594 ));
595 }
596
597 for field in &alter.alter_fields {
599 stmts.extend(self.alter_column(&alter.table_name, field));
600 }
601
602 stmts
603 }
604
605 fn alter_column(&self, table: &str, field: &FieldAlterDiff) -> Vec<String> {
607 let mut stmts = Vec::new();
608
609 if let Some(new_type) = &field.new_type {
610 stmts.push(format!(
611 "ALTER TABLE `{}` MODIFY COLUMN `{}` {};",
612 table, field.column_name, new_type
613 ));
614 }
615
616 stmts
617 }
618
619 fn create_index(&self, index: &IndexDiff) -> String {
621 let unique = if index.unique { "UNIQUE " } else { "" };
622
623 let index_type = match &index.index_type {
625 Some(prax_schema::ast::IndexType::FullText) => "FULLTEXT ",
626 _ => "",
627 };
628
629 let cols: Vec<String> = index.columns.iter().map(|c| format!("`{}`", c)).collect();
630 format!(
631 "CREATE {}{}INDEX `{}` ON `{}`({});",
632 unique,
633 index_type,
634 index.name,
635 index.table_name,
636 cols.join(", ")
637 )
638 }
639
640 fn drop_index(&self, name: &str, table: &str) -> String {
642 format!("DROP INDEX `{}` ON `{}`;", name, table)
643 }
644
645 fn create_view(&self, view: &ViewDiff) -> String {
647 format!(
650 "CREATE OR REPLACE VIEW `{}` AS\n{};",
651 view.view_name, view.sql_query
652 )
653 }
654
655 fn drop_view(&self, name: &str) -> String {
657 format!("DROP VIEW IF EXISTS `{}`;", name)
658 }
659}
660
661pub struct SqliteGenerator;
663
664impl SqliteGenerator {
665 pub fn generate(&self, diff: &SchemaDiff) -> MigrationSql {
667 let mut up = Vec::new();
668 let mut down = Vec::new();
669
670 for model in &diff.create_models {
672 up.push(self.create_table(model));
673 down.push(self.drop_table(&model.table_name));
674 }
675
676 for name in &diff.drop_models {
678 up.push(self.drop_table(name));
679 }
680
681 for index in &diff.create_indexes {
683 up.push(self.create_index(index));
684 down.push(self.drop_index(&index.name));
685 }
686
687 for index in &diff.drop_indexes {
689 up.push(self.drop_index(&index.name));
690 }
691
692 for view in &diff.create_views {
694 up.push(self.create_view(view));
695 down.push(self.drop_view(&view.view_name));
696 }
697
698 for name in &diff.drop_views {
700 up.push(self.drop_view(name));
701 }
702
703 for view in &diff.alter_views {
705 up.push(self.drop_view(&view.view_name));
706 up.push(self.create_view(view));
707 }
708
709 MigrationSql {
710 up: up.join("\n\n"),
711 down: down.join("\n\n"),
712 }
713 }
714
715 fn create_table(&self, model: &ModelDiff) -> String {
717 let mut columns = Vec::new();
718
719 for field in &model.fields {
720 columns.push(self.column_definition(field));
721 }
722
723 let has_integer_pk = model
725 .fields
726 .iter()
727 .any(|f| f.is_primary_key && f.sql_type == "INTEGER" && f.is_auto_increment);
728
729 if !model.primary_key.is_empty() && !has_integer_pk {
731 let pk_cols: Vec<String> = model
732 .primary_key
733 .iter()
734 .map(|c| format!("\"{}\"", c))
735 .collect();
736 columns.push(format!("PRIMARY KEY ({})", pk_cols.join(", ")));
737 }
738
739 for uc in &model.unique_constraints {
741 let cols: Vec<String> = uc.columns.iter().map(|c| format!("\"{}\"", c)).collect();
742 let constraint = if let Some(name) = &uc.name {
743 format!("CONSTRAINT \"{}\" UNIQUE ({})", name, cols.join(", "))
744 } else {
745 format!("UNIQUE ({})", cols.join(", "))
746 };
747 columns.push(constraint);
748 }
749
750 format!(
751 "CREATE TABLE \"{}\" (\n {}\n);",
752 model.table_name,
753 columns.join(",\n ")
754 )
755 }
756
757 fn column_definition(&self, field: &FieldDiff) -> String {
759 let mut parts = vec![format!("\"{}\"", field.column_name)];
760
761 let sql_type = match field.sql_type.as_str() {
763 "INTEGER" if field.is_primary_key && field.is_auto_increment => {
764 parts.push("INTEGER PRIMARY KEY".to_string());
766 return parts.join(" ");
767 }
768 "BIGINT" => "INTEGER".to_string(),
769 "DOUBLE PRECISION" => "REAL".to_string(),
770 "TIMESTAMP WITH TIME ZONE" | "DATETIME" => "TEXT".to_string(), "BOOLEAN" => "INTEGER".to_string(),
772 "BYTEA" | "BLOB" => "BLOB".to_string(),
773 "JSONB" | "JSON" => "TEXT".to_string(), other => other.to_string(),
775 };
776 parts.push(sql_type);
777
778 if !field.nullable && !field.is_primary_key {
779 parts.push("NOT NULL".to_string());
780 }
781
782 if field.is_unique && !field.is_primary_key {
783 parts.push("UNIQUE".to_string());
784 }
785
786 if let Some(default) = &field.default {
787 parts.push(format!("DEFAULT {}", default));
788 }
789
790 parts.join(" ")
791 }
792
793 fn drop_table(&self, name: &str) -> String {
795 format!("DROP TABLE IF EXISTS \"{}\";", name)
796 }
797
798 fn create_index(&self, index: &IndexDiff) -> String {
800 let unique = if index.unique { "UNIQUE " } else { "" };
801
802 let cols: Vec<String> = index.columns.iter().map(|c| format!("\"{}\"", c)).collect();
803 format!(
804 "CREATE {}INDEX \"{}\" ON \"{}\"({});",
805 unique,
806 index.name,
807 index.table_name,
808 cols.join(", ")
809 )
810 }
811
812 fn drop_index(&self, name: &str) -> String {
814 format!("DROP INDEX IF EXISTS \"{}\";", name)
815 }
816
817 fn create_view(&self, view: &ViewDiff) -> String {
819 format!(
822 "CREATE VIEW IF NOT EXISTS \"{}\" AS\n{};",
823 view.view_name, view.sql_query
824 )
825 }
826
827 fn drop_view(&self, name: &str) -> String {
829 format!("DROP VIEW IF EXISTS \"{}\";", name)
830 }
831}
832
833pub struct MssqlGenerator;
835
836impl MssqlGenerator {
837 pub fn generate(&self, diff: &SchemaDiff) -> MigrationSql {
839 let mut up = Vec::new();
840 let mut down = Vec::new();
841
842 for model in &diff.create_models {
844 up.push(self.create_table(model));
845 down.push(self.drop_table(&model.table_name));
846 }
847
848 for name in &diff.drop_models {
850 up.push(self.drop_table(name));
851 }
852
853 for alter in &diff.alter_models {
855 up.extend(self.alter_table(alter));
856 }
857
858 for index in &diff.create_indexes {
860 up.push(self.create_index(index));
861 down.push(self.drop_index(&index.name, &index.table_name));
862 }
863
864 for index in &diff.drop_indexes {
866 up.push(self.drop_index(&index.name, &index.table_name));
867 }
868
869 for view in &diff.create_views {
871 up.push(self.create_view(view));
872 down.push(self.drop_view(&view.view_name, view.is_materialized));
873 }
874
875 for name in &diff.drop_views {
877 up.push(self.drop_view(name, false));
878 }
879
880 for view in &diff.alter_views {
882 up.push(self.drop_view(&view.view_name, view.is_materialized));
883 up.push(self.create_view(view));
884 }
885
886 MigrationSql {
887 up: up.join("\n\nGO\n\n"),
888 down: down.join("\n\nGO\n\n"),
889 }
890 }
891
892 fn create_table(&self, model: &ModelDiff) -> String {
894 let mut columns = Vec::new();
895
896 for field in &model.fields {
897 columns.push(self.column_definition(field));
898 }
899
900 if !model.primary_key.is_empty() {
902 let pk_cols: Vec<String> = model
903 .primary_key
904 .iter()
905 .map(|c| format!("[{}]", c))
906 .collect();
907 columns.push(format!(
908 "CONSTRAINT [PK_{}] PRIMARY KEY ({})",
909 model.table_name,
910 pk_cols.join(", ")
911 ));
912 }
913
914 for uc in &model.unique_constraints {
916 let cols: Vec<String> = uc.columns.iter().map(|c| format!("[{}]", c)).collect();
917 let name = uc
918 .name
919 .clone()
920 .unwrap_or_else(|| format!("UQ_{}_{}", model.table_name, uc.columns.join("_")));
921 columns.push(format!(
922 "CONSTRAINT [{}] UNIQUE ({})",
923 name,
924 cols.join(", ")
925 ));
926 }
927
928 format!(
929 "CREATE TABLE [{}] (\n {}\n);",
930 model.table_name,
931 columns.join(",\n ")
932 )
933 }
934
935 fn column_definition(&self, field: &FieldDiff) -> String {
937 let mut parts = vec![format!("[{}]", field.column_name)];
938
939 let sql_type = match field.sql_type.as_str() {
941 "INTEGER" => "INT".to_string(),
942 "BIGINT" => "BIGINT".to_string(),
943 "TEXT" => "NVARCHAR(MAX)".to_string(),
944 "DOUBLE PRECISION" => "FLOAT".to_string(),
945 "TIMESTAMP WITH TIME ZONE" => "DATETIMEOFFSET".to_string(),
946 "BOOLEAN" => "BIT".to_string(),
947 "BYTEA" => "VARBINARY(MAX)".to_string(),
948 "JSONB" | "JSON" => "NVARCHAR(MAX)".to_string(), "UUID" => "UNIQUEIDENTIFIER".to_string(),
950 "DECIMAL" => "DECIMAL(18,2)".to_string(),
951 other => other.to_string(),
952 };
953 parts.push(sql_type);
954
955 if field.is_auto_increment {
956 parts.push("IDENTITY(1,1)".to_string());
957 }
958
959 if !field.nullable && !field.is_primary_key {
960 parts.push("NOT NULL".to_string());
961 }
962
963 if field.is_unique && !field.is_primary_key {
964 }
966
967 if let Some(default) = &field.default {
968 parts.push(format!("DEFAULT {}", default));
969 }
970
971 parts.join(" ")
972 }
973
974 fn drop_table(&self, name: &str) -> String {
976 format!("DROP TABLE IF EXISTS [{}];", name)
977 }
978
979 fn alter_table(&self, alter: &ModelAlterDiff) -> Vec<String> {
981 let mut stmts = Vec::new();
982
983 for field in &alter.add_fields {
985 stmts.push(format!(
986 "ALTER TABLE [{}] ADD {};",
987 alter.table_name,
988 self.column_definition(field)
989 ));
990 }
991
992 for name in &alter.drop_fields {
994 stmts.push(format!(
995 "ALTER TABLE [{}] DROP COLUMN [{}];",
996 alter.table_name, name
997 ));
998 }
999
1000 for field in &alter.alter_fields {
1002 stmts.extend(self.alter_column(&alter.table_name, field));
1003 }
1004
1005 stmts
1006 }
1007
1008 fn alter_column(&self, table: &str, field: &FieldAlterDiff) -> Vec<String> {
1010 let mut stmts = Vec::new();
1011
1012 if let Some(new_type) = &field.new_type {
1013 stmts.push(format!(
1014 "ALTER TABLE [{}] ALTER COLUMN [{}] {};",
1015 table, field.column_name, new_type
1016 ));
1017 }
1018
1019 stmts
1020 }
1021
1022 fn create_index(&self, index: &IndexDiff) -> String {
1024 let unique = if index.unique { "UNIQUE " } else { "" };
1025
1026 let cols: Vec<String> = index.columns.iter().map(|c| format!("[{}]", c)).collect();
1027 format!(
1028 "CREATE {}INDEX [{}] ON [{}]({});",
1029 unique,
1030 index.name,
1031 index.table_name,
1032 cols.join(", ")
1033 )
1034 }
1035
1036 fn drop_index(&self, name: &str, table: &str) -> String {
1038 format!("DROP INDEX [{}] ON [{}];", name, table)
1039 }
1040
1041 fn create_view(&self, view: &ViewDiff) -> String {
1047 if view.is_materialized {
1048 format!(
1051 "CREATE VIEW [{}] WITH SCHEMABINDING AS\n{};\n\n-- Create unique clustered index for indexed view\n-- CREATE UNIQUE CLUSTERED INDEX [IX_{}_Clustered] ON [{}] ([id]);",
1052 view.view_name, view.sql_query, view.view_name, view.view_name
1053 )
1054 } else {
1055 format!(
1056 "CREATE OR ALTER VIEW [{}] AS\n{};",
1057 view.view_name, view.sql_query
1058 )
1059 }
1060 }
1061
1062 fn drop_view(&self, name: &str, _is_materialized: bool) -> String {
1064 format!("DROP VIEW IF EXISTS [{}];", name)
1066 }
1067
1068 #[allow(dead_code)]
1070 fn refresh_view(&self, name: &str) -> String {
1071 format!("EXEC sp_refreshview N'{}';", name)
1072 }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use super::*;
1078
1079 #[test]
1080 fn test_create_enum() {
1081 let generator = PostgresSqlGenerator;
1082 let enum_diff = EnumDiff {
1083 name: "Status".to_string(),
1084 values: vec!["PENDING".to_string(), "ACTIVE".to_string()],
1085 };
1086
1087 let sql = generator.create_enum(&enum_diff);
1088 assert!(sql.contains("CREATE TYPE"));
1089 assert!(sql.contains("Status"));
1090 assert!(sql.contains("PENDING"));
1091 assert!(sql.contains("ACTIVE"));
1092 }
1093
1094 #[test]
1095 fn test_create_table() {
1096 let generator = PostgresSqlGenerator;
1097 let model = ModelDiff {
1098 name: "User".to_string(),
1099 table_name: "users".to_string(),
1100 fields: vec![
1101 FieldDiff {
1102 name: "id".to_string(),
1103 column_name: "id".to_string(),
1104 sql_type: "INTEGER".to_string(),
1105 nullable: false,
1106 default: None,
1107 is_primary_key: true,
1108 is_auto_increment: true,
1109 is_unique: false,
1110 },
1111 FieldDiff {
1112 name: "email".to_string(),
1113 column_name: "email".to_string(),
1114 sql_type: "TEXT".to_string(),
1115 nullable: false,
1116 default: None,
1117 is_primary_key: false,
1118 is_auto_increment: false,
1119 is_unique: true,
1120 },
1121 ],
1122 primary_key: vec!["id".to_string()],
1123 indexes: Vec::new(),
1124 unique_constraints: Vec::new(),
1125 };
1126
1127 let sql = generator.create_table(&model);
1128 assert!(sql.contains("CREATE TABLE"));
1129 assert!(sql.contains("users"));
1130 assert!(sql.contains("SERIAL"));
1131 assert!(sql.contains("email"));
1132 assert!(sql.contains("UNIQUE"));
1133 assert!(sql.contains("PRIMARY KEY"));
1134 }
1135
1136 #[test]
1137 fn test_create_index() {
1138 let generator = PostgresSqlGenerator;
1139 let index = IndexDiff::new("idx_users_email", "users", vec!["email".to_string()]).unique();
1140
1141 let sql = generator.create_index(&index);
1142 assert!(sql.contains("CREATE UNIQUE INDEX"));
1143 assert!(sql.contains("idx_users_email"));
1144 assert!(sql.contains("users"));
1145 }
1146
1147 #[test]
1148 fn test_create_hnsw_index() {
1149 use prax_schema::ast::{IndexType, VectorOps};
1150
1151 let generator = PostgresSqlGenerator;
1152 let index = IndexDiff::new("idx_embedding", "documents", vec!["embedding".to_string()])
1153 .with_type(IndexType::Hnsw)
1154 .with_vector_ops(VectorOps::Cosine)
1155 .with_hnsw_m(16)
1156 .with_hnsw_ef_construction(64);
1157
1158 let sql = generator.create_index(&index);
1159 assert!(sql.contains("CREATE INDEX"));
1160 assert!(sql.contains("USING hnsw"));
1161 assert!(sql.contains("vector_cosine_ops"));
1162 assert!(sql.contains("m = 16"));
1163 assert!(sql.contains("ef_construction = 64"));
1164 }
1165
1166 #[test]
1167 fn test_create_ivfflat_index() {
1168 use prax_schema::ast::{IndexType, VectorOps};
1169
1170 let generator = PostgresSqlGenerator;
1171 let index = IndexDiff::new(
1172 "idx_embedding_l2",
1173 "documents",
1174 vec!["embedding".to_string()],
1175 )
1176 .with_type(IndexType::IvfFlat)
1177 .with_vector_ops(VectorOps::L2)
1178 .with_ivfflat_lists(100);
1179
1180 let sql = generator.create_index(&index);
1181 assert!(sql.contains("CREATE INDEX"));
1182 assert!(sql.contains("USING ivfflat"));
1183 assert!(sql.contains("vector_l2_ops"));
1184 assert!(sql.contains("lists = 100"));
1185 }
1186
1187 #[test]
1188 fn test_create_gin_index() {
1189 use prax_schema::ast::IndexType;
1190
1191 let generator = PostgresSqlGenerator;
1192 let index =
1193 IndexDiff::new("idx_tags", "posts", vec!["tags".to_string()]).with_type(IndexType::Gin);
1194
1195 let sql = generator.create_index(&index);
1196 assert!(sql.contains("CREATE INDEX"));
1197 assert!(sql.contains("USING GIN"));
1198 assert!(sql.contains("idx_tags"));
1199 }
1200
1201 #[test]
1202 fn test_alter_table_add_column() {
1203 let generator = PostgresSqlGenerator;
1204 let alter = ModelAlterDiff {
1205 name: "User".to_string(),
1206 table_name: "users".to_string(),
1207 add_fields: vec![FieldDiff {
1208 name: "age".to_string(),
1209 column_name: "age".to_string(),
1210 sql_type: "INTEGER".to_string(),
1211 nullable: true,
1212 default: None,
1213 is_primary_key: false,
1214 is_auto_increment: false,
1215 is_unique: false,
1216 }],
1217 drop_fields: Vec::new(),
1218 alter_fields: Vec::new(),
1219 add_indexes: Vec::new(),
1220 drop_indexes: Vec::new(),
1221 };
1222
1223 let stmts = generator.alter_table(&alter);
1224 assert_eq!(stmts.len(), 1);
1225 assert!(stmts[0].contains("ADD COLUMN"));
1226 assert!(stmts[0].contains("age"));
1227 }
1228
1229 #[test]
1230 fn test_create_view() {
1231 let generator = PostgresSqlGenerator;
1232 let view = ViewDiff {
1233 name: "UserStats".to_string(),
1234 view_name: "user_stats".to_string(),
1235 sql_query: "SELECT id, COUNT(*) as post_count FROM users GROUP BY id".to_string(),
1236 is_materialized: false,
1237 refresh_interval: None,
1238 fields: vec![],
1239 };
1240
1241 let sql = generator.create_view(&view);
1242 assert!(sql.contains("CREATE OR REPLACE VIEW"));
1243 assert!(sql.contains("user_stats"));
1244 assert!(sql.contains("SELECT id"));
1245 assert!(sql.contains("post_count"));
1246 }
1247
1248 #[test]
1249 fn test_create_materialized_view() {
1250 let generator = PostgresSqlGenerator;
1251 let view = ViewDiff {
1252 name: "UserStats".to_string(),
1253 view_name: "user_stats".to_string(),
1254 sql_query: "SELECT id, COUNT(*) as post_count FROM users GROUP BY id".to_string(),
1255 is_materialized: true,
1256 refresh_interval: Some("1h".to_string()),
1257 fields: vec![],
1258 };
1259
1260 let sql = generator.create_view(&view);
1261 assert!(sql.contains("CREATE MATERIALIZED VIEW"));
1262 assert!(sql.contains("user_stats"));
1263 assert!(!sql.contains("OR REPLACE")); }
1265
1266 #[test]
1267 fn test_drop_view() {
1268 let generator = PostgresSqlGenerator;
1269
1270 let sql = generator.drop_view("user_stats", false);
1271 assert!(sql.contains("DROP VIEW"));
1272 assert!(sql.contains("user_stats"));
1273 assert!(sql.contains("CASCADE"));
1274
1275 let sql_mat = generator.drop_view("user_stats", true);
1276 assert!(sql_mat.contains("DROP MATERIALIZED VIEW"));
1277 assert!(sql_mat.contains("user_stats"));
1278 }
1279
1280 #[test]
1281 fn test_refresh_materialized_view() {
1282 let generator = PostgresSqlGenerator;
1283
1284 let sql = generator.refresh_materialized_view("user_stats", false);
1285 assert!(sql.contains("REFRESH MATERIALIZED VIEW"));
1286 assert!(sql.contains("user_stats"));
1287 assert!(!sql.contains("CONCURRENTLY"));
1288
1289 let sql_concurrent = generator.refresh_materialized_view("user_stats", true);
1290 assert!(sql_concurrent.contains("CONCURRENTLY"));
1291 }
1292
1293 #[test]
1294 fn test_generate_with_views() {
1295 use crate::diff::SchemaDiff;
1296
1297 let generator = PostgresSqlGenerator;
1298 let mut diff = SchemaDiff::default();
1299 diff.create_views.push(ViewDiff {
1300 name: "ActiveUsers".to_string(),
1301 view_name: "active_users".to_string(),
1302 sql_query: "SELECT * FROM users WHERE active = true".to_string(),
1303 is_materialized: false,
1304 refresh_interval: None,
1305 fields: vec![],
1306 });
1307
1308 let sql = generator.generate(&diff);
1309 assert!(!sql.is_empty());
1310 assert!(sql.up.contains("CREATE OR REPLACE VIEW"));
1311 assert!(sql.up.contains("active_users"));
1312 assert!(sql.down.contains("DROP VIEW"));
1313 }
1314
1315 #[test]
1318 fn test_mysql_create_view() {
1319 let generator = MySqlGenerator;
1320 let view = ViewDiff {
1321 name: "UserStats".to_string(),
1322 view_name: "user_stats".to_string(),
1323 sql_query: "SELECT id, COUNT(*) as post_count FROM users GROUP BY id".to_string(),
1324 is_materialized: false,
1325 refresh_interval: None,
1326 fields: vec![],
1327 };
1328
1329 let sql = generator.create_view(&view);
1330 assert!(sql.contains("CREATE OR REPLACE VIEW"));
1331 assert!(sql.contains("`user_stats`"));
1332 assert!(sql.contains("SELECT id"));
1333 }
1334
1335 #[test]
1336 fn test_mysql_drop_view() {
1337 let generator = MySqlGenerator;
1338 let sql = generator.drop_view("user_stats");
1339 assert!(sql.contains("DROP VIEW IF EXISTS"));
1340 assert!(sql.contains("`user_stats`"));
1341 }
1342
1343 #[test]
1344 fn test_mysql_generate_with_views() {
1345 use crate::diff::SchemaDiff;
1346
1347 let generator = MySqlGenerator;
1348 let mut diff = SchemaDiff::default();
1349 diff.create_views.push(ViewDiff {
1350 name: "ActiveUsers".to_string(),
1351 view_name: "active_users".to_string(),
1352 sql_query: "SELECT * FROM users WHERE active = 1".to_string(),
1353 is_materialized: false,
1354 refresh_interval: None,
1355 fields: vec![],
1356 });
1357
1358 let sql = generator.generate(&diff);
1359 assert!(!sql.is_empty());
1360 assert!(sql.up.contains("CREATE OR REPLACE VIEW"));
1361 assert!(sql.up.contains("`active_users`"));
1362 assert!(sql.down.contains("DROP VIEW"));
1363 }
1364
1365 #[test]
1366 fn test_mysql_create_table() {
1367 let generator = MySqlGenerator;
1368 let model = ModelDiff {
1369 name: "User".to_string(),
1370 table_name: "users".to_string(),
1371 fields: vec![FieldDiff {
1372 name: "id".to_string(),
1373 column_name: "id".to_string(),
1374 sql_type: "INTEGER".to_string(),
1375 nullable: false,
1376 default: None,
1377 is_primary_key: true,
1378 is_auto_increment: true,
1379 is_unique: false,
1380 }],
1381 primary_key: vec!["id".to_string()],
1382 indexes: Vec::new(),
1383 unique_constraints: Vec::new(),
1384 };
1385
1386 let sql = generator.create_table(&model);
1387 assert!(sql.contains("CREATE TABLE `users`"));
1388 assert!(sql.contains("AUTO_INCREMENT"));
1389 assert!(sql.contains("ENGINE=InnoDB"));
1390 }
1391
1392 #[test]
1395 fn test_sqlite_create_view() {
1396 let generator = SqliteGenerator;
1397 let view = ViewDiff {
1398 name: "UserStats".to_string(),
1399 view_name: "user_stats".to_string(),
1400 sql_query: "SELECT id, COUNT(*) as post_count FROM users GROUP BY id".to_string(),
1401 is_materialized: false,
1402 refresh_interval: None,
1403 fields: vec![],
1404 };
1405
1406 let sql = generator.create_view(&view);
1407 assert!(sql.contains("CREATE VIEW IF NOT EXISTS"));
1408 assert!(sql.contains("\"user_stats\""));
1409 assert!(sql.contains("SELECT id"));
1410 }
1411
1412 #[test]
1413 fn test_sqlite_drop_view() {
1414 let generator = SqliteGenerator;
1415 let sql = generator.drop_view("user_stats");
1416 assert!(sql.contains("DROP VIEW IF EXISTS"));
1417 assert!(sql.contains("\"user_stats\""));
1418 }
1419
1420 #[test]
1421 fn test_sqlite_generate_with_views() {
1422 use crate::diff::SchemaDiff;
1423
1424 let generator = SqliteGenerator;
1425 let mut diff = SchemaDiff::default();
1426 diff.create_views.push(ViewDiff {
1427 name: "ActiveUsers".to_string(),
1428 view_name: "active_users".to_string(),
1429 sql_query: "SELECT * FROM users WHERE active = 1".to_string(),
1430 is_materialized: false,
1431 refresh_interval: None,
1432 fields: vec![],
1433 });
1434
1435 let sql = generator.generate(&diff);
1436 assert!(!sql.is_empty());
1437 assert!(sql.up.contains("CREATE VIEW IF NOT EXISTS"));
1438 assert!(sql.up.contains("\"active_users\""));
1439 assert!(sql.down.contains("DROP VIEW"));
1440 }
1441
1442 #[test]
1443 fn test_sqlite_create_table_with_autoincrement() {
1444 let generator = SqliteGenerator;
1445 let model = ModelDiff {
1446 name: "User".to_string(),
1447 table_name: "users".to_string(),
1448 fields: vec![FieldDiff {
1449 name: "id".to_string(),
1450 column_name: "id".to_string(),
1451 sql_type: "INTEGER".to_string(),
1452 nullable: false,
1453 default: None,
1454 is_primary_key: true,
1455 is_auto_increment: true,
1456 is_unique: false,
1457 }],
1458 primary_key: vec!["id".to_string()],
1459 indexes: Vec::new(),
1460 unique_constraints: Vec::new(),
1461 };
1462
1463 let sql = generator.create_table(&model);
1464 assert!(sql.contains("CREATE TABLE \"users\""));
1465 assert!(sql.contains("INTEGER PRIMARY KEY"));
1466 }
1467
1468 #[test]
1471 fn test_mssql_create_view() {
1472 let generator = MssqlGenerator;
1473 let view = ViewDiff {
1474 name: "UserStats".to_string(),
1475 view_name: "user_stats".to_string(),
1476 sql_query: "SELECT id, COUNT(*) as post_count FROM users GROUP BY id".to_string(),
1477 is_materialized: false,
1478 refresh_interval: None,
1479 fields: vec![],
1480 };
1481
1482 let sql = generator.create_view(&view);
1483 assert!(sql.contains("CREATE OR ALTER VIEW"));
1484 assert!(sql.contains("[user_stats]"));
1485 assert!(sql.contains("SELECT id"));
1486 }
1487
1488 #[test]
1489 fn test_mssql_create_indexed_view() {
1490 let generator = MssqlGenerator;
1491 let view = ViewDiff {
1492 name: "UserStats".to_string(),
1493 view_name: "user_stats".to_string(),
1494 sql_query: "SELECT id, COUNT(*) as post_count FROM users GROUP BY id".to_string(),
1495 is_materialized: true,
1496 refresh_interval: None,
1497 fields: vec![],
1498 };
1499
1500 let sql = generator.create_view(&view);
1501 assert!(sql.contains("WITH SCHEMABINDING"));
1502 assert!(sql.contains("[user_stats]"));
1503 assert!(sql.contains("CLUSTERED INDEX"));
1505 }
1506
1507 #[test]
1508 fn test_mssql_drop_view() {
1509 let generator = MssqlGenerator;
1510 let sql = generator.drop_view("user_stats", false);
1511 assert!(sql.contains("DROP VIEW IF EXISTS"));
1512 assert!(sql.contains("[user_stats]"));
1513 }
1514
1515 #[test]
1516 fn test_mssql_generate_with_views() {
1517 use crate::diff::SchemaDiff;
1518
1519 let generator = MssqlGenerator;
1520 let mut diff = SchemaDiff::default();
1521 diff.create_views.push(ViewDiff {
1522 name: "ActiveUsers".to_string(),
1523 view_name: "active_users".to_string(),
1524 sql_query: "SELECT * FROM users WHERE active = 1".to_string(),
1525 is_materialized: false,
1526 refresh_interval: None,
1527 fields: vec![],
1528 });
1529
1530 let sql = generator.generate(&diff);
1531 assert!(!sql.is_empty());
1532 assert!(sql.up.contains("CREATE OR ALTER VIEW"));
1533 assert!(sql.up.contains("[active_users]"));
1534 assert!(sql.down.contains("DROP VIEW"));
1535 }
1536
1537 #[test]
1538 fn test_mssql_create_table() {
1539 let generator = MssqlGenerator;
1540 let model = ModelDiff {
1541 name: "User".to_string(),
1542 table_name: "users".to_string(),
1543 fields: vec![FieldDiff {
1544 name: "id".to_string(),
1545 column_name: "id".to_string(),
1546 sql_type: "INTEGER".to_string(),
1547 nullable: false,
1548 default: None,
1549 is_primary_key: true,
1550 is_auto_increment: true,
1551 is_unique: false,
1552 }],
1553 primary_key: vec!["id".to_string()],
1554 indexes: Vec::new(),
1555 unique_constraints: Vec::new(),
1556 };
1557
1558 let sql = generator.create_table(&model);
1559 assert!(sql.contains("CREATE TABLE [users]"));
1560 assert!(sql.contains("IDENTITY(1,1)"));
1561 assert!(sql.contains("[PK_users]"));
1562 }
1563}