Skip to main content

toolu_orm_core/sql/
gen.rs

1//! SQL generation from a list of migration operations for both dialects.
2
3use crate::dialect::Dialect;
4use crate::diff::Operation;
5use crate::ordering::order_operations;
6
7use super::ddl::{add_column_sql, create_index_sql, create_table_sql, recreation_sql};
8use super::postgres::{alter_column_statements_postgres, needs_recreation_sqlite};
9
10pub fn generate_sql(operations: &[Operation]) -> String {
11  generate_sql_for(operations, Dialect::CURRENT)
12}
13
14pub fn generate_sql_for(operations: &[Operation], dialect: Dialect) -> String {
15  let ordered = order_operations(operations.to_vec());
16  let mut parts: Vec<String> = Vec::new();
17  for op in ordered {
18    let chunk = operation_sql(&op, dialect);
19    if !chunk.trim().is_empty() {
20      parts.push(chunk);
21    }
22  }
23  parts.join("\n\n--> statement-breakpoint\n\n")
24}
25
26fn operation_sql(op: &Operation, dialect: Dialect) -> String {
27  match op {
28    Operation::CreateEnum { name, variants } => match dialect {
29      Dialect::Postgres => {
30        let vals = variants
31          .iter()
32          .map(|v| format!("'{v}'"))
33          .collect::<Vec<_>>()
34          .join(", ");
35        format!("CREATE TYPE \"{name}\" AS ENUM ({vals});")
36      },
37      Dialect::Sqlite => format!("-- enum \"{name}\" (SQLite: TEXT + CHECK)"),
38    },
39    Operation::AlterEnum {
40      name,
41      added,
42      removed,
43    } => {
44      let mut s = String::new();
45      if !added.is_empty() {
46        match dialect {
47          Dialect::Postgres => {
48            let stmts: Vec<String> = added
49              .iter()
50              .map(|v| format!("ALTER TYPE \"{name}\" ADD VALUE '{v}';"))
51              .collect();
52            s.push_str(&stmts.join("\n\n--> statement-breakpoint\n\n"));
53          },
54          Dialect::Sqlite => {
55            s.push_str(&format!(
56              "-- ALTER ENUM \"{name}\" add variants (SQLite: adjust CHECK)\n",
57            ));
58          },
59        }
60      }
61      if !removed.is_empty() {
62        match dialect {
63          Dialect::Postgres => {
64            s.push_str("-- TODO: enum variant removal requires type recreation on Postgres for ");
65            s.push_str(name);
66            s.push('\n');
67          },
68          Dialect::Sqlite => {
69            s.push_str("-- SQLite: adjust CHECK for enum variant removal\n");
70          },
71        }
72      }
73      s.trim_end().to_owned()
74    },
75    Operation::DropEnum { name } => match dialect {
76      Dialect::Postgres => format!("DROP TYPE IF EXISTS \"{name}\";"),
77      Dialect::Sqlite => format!("-- drop enum \"{name}\" (no-op on SQLite)"),
78    },
79    Operation::CreateTable { table } => create_table_sql(table, dialect),
80    Operation::DropTable { name } => format!("DROP TABLE IF EXISTS \"{name}\";"),
81    Operation::RenameTable { old, new } => {
82      format!("ALTER TABLE \"{old}\" RENAME TO \"{new}\";")
83    },
84    Operation::RenameColumn { table, old, new } => {
85      format!("ALTER TABLE \"{table}\" RENAME COLUMN \"{old}\" TO \"{new}\";")
86    },
87    Operation::AddColumn { table, column } => add_column_sql(table, column, dialect),
88    Operation::DropColumn { table, column } => match dialect {
89      Dialect::Postgres => format!("ALTER TABLE \"{table}\" DROP COLUMN IF EXISTS \"{column}\";"),
90      Dialect::Sqlite => format!("ALTER TABLE \"{table}\" DROP COLUMN \"{column}\";"),
91    },
92    Operation::AlterColumn {
93      table,
94      changes,
95      table_def,
96    } => {
97      if changes.is_empty() {
98        return String::new();
99      }
100      match dialect {
101        Dialect::Postgres => alter_column_statements_postgres(table, changes, table_def)
102          .join("\n\n--> statement-breakpoint\n\n"),
103        Dialect::Sqlite => {
104          if needs_recreation_sqlite(changes) {
105            recreation_sql(table, table_def)
106          } else {
107            String::new()
108          }
109        },
110      }
111    },
112    Operation::CreateIndex { table, index } => create_index_sql(table, index),
113    Operation::DropIndex { name } => format!("DROP INDEX IF EXISTS \"{name}\";"),
114    Operation::AddForeignKey { table, fk } => match dialect {
115      Dialect::Postgres => {
116        let cols = fk
117          .columns
118          .iter()
119          .map(|c| format!("\"{c}\""))
120          .collect::<Vec<_>>()
121          .join(", ");
122        let ref_cols = fk
123          .references_columns
124          .iter()
125          .map(|c| format!("\"{c}\""))
126          .collect::<Vec<_>>()
127          .join(", ");
128        let mut s = format!(
129          "ALTER TABLE \"{table}\" ADD CONSTRAINT \"{}\" FOREIGN KEY ({cols}) REFERENCES \"{}\" ({ref_cols})",
130          fk.name, fk.references_table
131        );
132        if let Some(a) = fk.on_delete {
133          s.push_str(&format!(" ON DELETE {}", a.as_sql()));
134        }
135        if let Some(a) = fk.on_update {
136          s.push_str(&format!(" ON UPDATE {}", a.as_sql()));
137        }
138        s.push(';');
139        s
140      },
141      Dialect::Sqlite => format!(
142        "-- FOREIGN KEY \"{}\" on \"{table}\" (SQLite: rebuild table to attach constraint)",
143        fk.name
144      ),
145    },
146    Operation::DropForeignKey { table, name } => match dialect {
147      Dialect::Postgres => {
148        format!("ALTER TABLE \"{table}\" DROP CONSTRAINT IF EXISTS \"{name}\";")
149      },
150      Dialect::Sqlite => {
151        format!("-- drop FOREIGN KEY \"{name}\" on \"{table}\" (SQLite: rebuild table)")
152      },
153    },
154    Operation::AddCheckConstraint { table, name, expr } => match dialect {
155      Dialect::Postgres => {
156        format!("ALTER TABLE \"{table}\" ADD CONSTRAINT \"{name}\" CHECK ({expr});")
157      },
158      Dialect::Sqlite => format!(
159        "-- ADD CHECK \"{name}\" on \"{table}\" ({expr}) — SQLite may require table rebuild"
160      ),
161    },
162    Operation::DropCheckConstraint { table, name } => match dialect {
163      Dialect::Postgres => {
164        format!("ALTER TABLE \"{table}\" DROP CONSTRAINT IF EXISTS \"{name}\";")
165      },
166      Dialect::Sqlite => {
167        format!("-- DROP CHECK \"{name}\" on \"{table}\" (SQLite may require table rebuild)")
168      },
169    },
170  }
171}