diff --git a/.changepacks/changepack_log_P-BaCMMqTbkbBgarbYpYM.json b/.changepacks/changepack_log_P-BaCMMqTbkbBgarbYpYM.json new file mode 100644 index 0000000..af35d3d --- /dev/null +++ b/.changepacks/changepack_log_P-BaCMMqTbkbBgarbYpYM.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch"},"note":"Fix json convert issue","date":"2026-02-09T07:54:00.243061300Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f7bedd6..a17687e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3342,7 +3342,7 @@ dependencies = [ [[package]] name = "vespertide" -version = "0.1.42" +version = "0.1.43" dependencies = [ "vespertide-core", "vespertide-macro", @@ -3350,7 +3350,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.42" +version = "0.1.43" dependencies = [ "anyhow", "assert_cmd", @@ -3378,7 +3378,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.42" +version = "0.1.43" dependencies = [ "clap", "schemars", @@ -3388,7 +3388,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.42" +version = "0.1.43" dependencies = [ "rstest", "schemars", @@ -3400,7 +3400,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.42" +version = "0.1.43" dependencies = [ "insta", "rstest", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.42" +version = "0.1.43" dependencies = [ "anyhow", "rstest", @@ -3427,7 +3427,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.42" +version = "0.1.43" dependencies = [ "proc-macro2", "quote", @@ -3444,11 +3444,11 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.42" +version = "0.1.43" [[package]] name = "vespertide-planner" -version = "0.1.42" +version = "0.1.43" dependencies = [ "insta", "rstest", @@ -3459,7 +3459,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.42" +version = "0.1.43" dependencies = [ "insta", "rstest", diff --git a/crates/vespertide-query/src/sql/add_column.rs b/crates/vespertide-query/src/sql/add_column.rs index 4aca993..6655330 100644 --- a/crates/vespertide-query/src/sql/add_column.rs +++ b/crates/vespertide-query/src/sql/add_column.rs @@ -4,7 +4,8 @@ use vespertide_core::{ColumnDef, TableDef}; use super::helpers::{ build_create_enum_type_sql, build_sea_column_def_with_table, build_sqlite_temp_table_create, - normalize_enum_default, normalize_fill_with, recreate_indexes_after_rebuild, + convert_default_for_backend, normalize_enum_default, normalize_fill_with, + recreate_indexes_after_rebuild, }; use super::rename_table::build_rename_table; use super::types::{BuiltQuery, DatabaseBackend}; @@ -66,9 +67,11 @@ pub fn build_add_column( } let normalized_fill = normalize_fill_with(fill_with); let fill_expr = if let Some(fill) = normalized_fill.as_deref() { - Expr::cust(normalize_enum_default(&column.r#type, fill)) + let converted = convert_default_for_backend(fill, backend); + Expr::cust(normalize_enum_default(&column.r#type, &converted)) } else if let Some(def) = &column.default { - Expr::cust(normalize_enum_default(&column.r#type, &def.to_sql())) + let converted = convert_default_for_backend(&def.to_sql(), backend); + Expr::cust(normalize_enum_default(&column.r#type, &converted)) } else { Expr::cust("NULL") }; @@ -124,6 +127,7 @@ pub fn build_add_column( // Backfill with provided value if let Some(fill) = normalize_fill_with(fill_with) { + let fill = convert_default_for_backend(&fill, backend); let update_stmt = Query::update() .table(Alias::new(table)) .value(Alias::new(&column.name), Expr::cust(fill)) @@ -604,6 +608,70 @@ mod tests { }); } + /// Test adding NOT NULL column with '[]'::json default on SQLite + /// SQLite should strip the ::json cast, MySQL should use CAST(... AS JSON) + #[rstest] + #[case::postgres(DatabaseBackend::Postgres)] + #[case::mysql(DatabaseBackend::MySql)] + #[case::sqlite(DatabaseBackend::Sqlite)] + fn test_add_column_with_pg_type_cast_default(#[case] backend: DatabaseBackend) { + let column = ColumnDef { + name: "story_index".into(), + r#type: ColumnType::Simple(SimpleColumnType::Json), + nullable: false, + default: Some("'[]'::json".into()), + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }; + let current_schema = vec![TableDef { + name: "project".into(), + description: None, + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }]; + let result = build_add_column(&backend, "project", &column, None, ¤t_schema).unwrap(); + let sql = result + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // SQLite must NOT contain ::json syntax + if backend == DatabaseBackend::Sqlite { + assert!( + !sql.contains("::json"), + "SQLite SQL should not contain ::json cast, got: {}", + sql + ); + } + + // MySQL should use CAST syntax + if backend == DatabaseBackend::MySql { + assert!( + !sql.contains("::json"), + "MySQL SQL should not contain ::json cast, got: {}", + sql + ); + } + + with_settings!({ snapshot_suffix => format!("pg_type_cast_default_{:?}", backend) }, { + assert_snapshot!(sql); + }); + } + #[rstest] #[case::postgres(DatabaseBackend::Postgres)] #[case::mysql(DatabaseBackend::MySql)] diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 7ae10cb..aef010a 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -189,9 +189,96 @@ pub fn convert_default_for_backend(default: &str, backend: &DatabaseBackend) -> }; } + // PostgreSQL-style type casts: 'value'::type or expr::type + if let Some((value, cast_type)) = parse_pg_type_cast(default) { + return convert_type_cast(&value, &cast_type, backend); + } + default.to_string() } +/// Parse a PostgreSQL-style type cast expression (e.g., `'[]'::json`, `0::boolean`) +/// Returns `(value, type)` if parsed, or None if not a type cast. +fn parse_pg_type_cast(expr: &str) -> Option<(String, String)> { + let trimmed = expr.trim(); + + // Handle quoted values: 'value'::type + if let Some(after_open) = trimmed.strip_prefix('\'') { + // Find the closing quote (handle escaped quotes '') + let mut i = 0; + let bytes = after_open.as_bytes(); + while i < bytes.len() { + if bytes[i] == b'\'' { + // Check for escaped quote '' + if i + 1 < bytes.len() && bytes[i + 1] == b'\'' { + i += 2; + continue; + } + // Found closing quote + let value_end = i + 1; // index in `after_open` + let rest = &after_open[value_end..]; + if let Some(stripped) = rest.strip_prefix("::") { + let cast_type = stripped.trim().to_lowercase(); + if !cast_type.is_empty() { + let value = format!("'{}'", &after_open[..i]); + return Some((value, cast_type)); + } + } + return None; + } + i += 1; + } + return None; + } + + // Handle unquoted values: expr::type (e.g., 0::boolean, NULL::json) + if let Some(pos) = trimmed.find("::") { + let value = trimmed[..pos].trim().to_string(); + let cast_type = trimmed[pos + 2..].trim().to_lowercase(); + if !value.is_empty() && !cast_type.is_empty() { + return Some((value, cast_type)); + } + } + + None +} + +/// Map PostgreSQL type name to MySQL CAST target type +fn pg_type_to_mysql_cast(pg_type: &str) -> &'static str { + match pg_type { + "json" | "jsonb" => "JSON", + "text" | "varchar" | "char" | "character varying" => "CHAR", + "integer" | "int" | "int4" | "smallint" | "int2" => "SIGNED", + "bigint" | "int8" => "SIGNED", + "real" | "float4" | "double precision" | "float8" => "DECIMAL", + "boolean" | "bool" => "UNSIGNED", + "date" => "DATE", + "time" => "TIME", + "timestamp" + | "timestamptz" + | "timestamp with time zone" + | "timestamp without time zone" => "DATETIME", + "numeric" | "decimal" => "DECIMAL", + "bytea" => "BINARY", + _ => "CHAR", + } +} + +/// Convert a type cast expression to the appropriate backend syntax +fn convert_type_cast(value: &str, cast_type: &str, backend: &DatabaseBackend) -> String { + match backend { + // PostgreSQL: keep native :: syntax + DatabaseBackend::Postgres => format!("{}::{}", value, cast_type), + // MySQL: CAST(value AS type) + DatabaseBackend::MySql => { + let mysql_type = pg_type_to_mysql_cast(cast_type); + format!("CAST({} AS {})", value, mysql_type) + } + // SQLite: strip the cast, use raw value (SQLite is dynamically typed) + DatabaseBackend::Sqlite => value.to_string(), + } +} + /// Check if the column type is an enum type fn is_enum_type(column_type: &ColumnType) -> bool { matches!( @@ -675,6 +762,119 @@ mod tests { assert_eq!(result, expected); } + // --- PostgreSQL type cast conversion tests --- + + #[rstest] + // JSON type cast: '[]'::json + #[case::json_cast_postgres("'[]'::json", DatabaseBackend::Postgres, "'[]'::json")] + #[case::json_cast_mysql("'[]'::json", DatabaseBackend::MySql, "CAST('[]' AS JSON)")] + #[case::json_cast_sqlite("'[]'::json", DatabaseBackend::Sqlite, "'[]'")] + // JSONB type cast: '{}'::jsonb + #[case::jsonb_cast_postgres("'{}'::jsonb", DatabaseBackend::Postgres, "'{}'::jsonb")] + #[case::jsonb_cast_mysql("'{}'::jsonb", DatabaseBackend::MySql, "CAST('{}' AS JSON)")] + #[case::jsonb_cast_sqlite("'{}'::jsonb", DatabaseBackend::Sqlite, "'{}'")] + // Text type cast: 'hello'::text + #[case::text_cast_postgres("'hello'::text", DatabaseBackend::Postgres, "'hello'::text")] + #[case::text_cast_mysql("'hello'::text", DatabaseBackend::MySql, "CAST('hello' AS CHAR)")] + #[case::text_cast_sqlite("'hello'::text", DatabaseBackend::Sqlite, "'hello'")] + // Integer type cast: 0::integer + #[case::int_cast_postgres("0::integer", DatabaseBackend::Postgres, "0::integer")] + #[case::int_cast_mysql("0::integer", DatabaseBackend::MySql, "CAST(0 AS SIGNED)")] + #[case::int_cast_sqlite("0::integer", DatabaseBackend::Sqlite, "0")] + // Boolean type cast: 0::boolean + #[case::bool_cast_postgres("0::boolean", DatabaseBackend::Postgres, "0::boolean")] + #[case::bool_cast_mysql("0::boolean", DatabaseBackend::MySql, "CAST(0 AS UNSIGNED)")] + #[case::bool_cast_sqlite("0::boolean", DatabaseBackend::Sqlite, "0")] + // Nested JSON object: '{"key":"value"}'::json + #[case::json_obj_cast_postgres( + "'{\"key\":\"value\"}'::json", + DatabaseBackend::Postgres, + "'{\"key\":\"value\"}'::json" + )] + #[case::json_obj_cast_mysql( + "'{\"key\":\"value\"}'::json", + DatabaseBackend::MySql, + "CAST('{\"key\":\"value\"}' AS JSON)" + )] + #[case::json_obj_cast_sqlite( + "'{\"key\":\"value\"}'::json", + DatabaseBackend::Sqlite, + "'{\"key\":\"value\"}'" + )] + // Timestamp type cast: '2024-01-01'::timestamp + #[case::timestamp_cast_postgres( + "'2024-01-01'::timestamp", + DatabaseBackend::Postgres, + "'2024-01-01'::timestamp" + )] + #[case::timestamp_cast_mysql( + "'2024-01-01'::timestamp", + DatabaseBackend::MySql, + "CAST('2024-01-01' AS DATETIME)" + )] + #[case::timestamp_cast_sqlite( + "'2024-01-01'::timestamp", + DatabaseBackend::Sqlite, + "'2024-01-01'" + )] + fn test_convert_default_for_backend_type_cast( + #[case] default: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &str, + ) { + let result = convert_default_for_backend(default, &backend); + assert_eq!(result, expected); + } + + #[test] + fn test_parse_pg_type_cast_no_cast() { + // Regular values should not be parsed as type casts + assert!(parse_pg_type_cast("'hello'").is_none()); + assert!(parse_pg_type_cast("42").is_none()); + assert!(parse_pg_type_cast("NOW()").is_none()); + assert!(parse_pg_type_cast("CURRENT_TIMESTAMP").is_none()); + } + + #[test] + fn test_parse_pg_type_cast_valid() { + let (value, cast_type) = parse_pg_type_cast("'[]'::json").unwrap(); + assert_eq!(value, "'[]'"); + assert_eq!(cast_type, "json"); + + let (value, cast_type) = parse_pg_type_cast("0::boolean").unwrap(); + assert_eq!(value, "0"); + assert_eq!(cast_type, "boolean"); + } + + #[test] + fn test_parse_pg_type_cast_escaped_quotes() { + // Value with escaped quotes: 'it''s'::text + let (value, cast_type) = parse_pg_type_cast("'it''s'::text").unwrap(); + assert_eq!(value, "'it''s'"); + assert_eq!(cast_type, "text"); + } + + #[test] + fn test_parse_pg_type_cast_unterminated_quote() { + // Unterminated quoted string should return None (line 203) + assert!(parse_pg_type_cast("'unclosed").is_none()); + assert!(parse_pg_type_cast("'no close quote::json").is_none()); + } + + #[rstest] + #[case::numeric("'0.5'::numeric", DatabaseBackend::MySql, "CAST('0.5' AS DECIMAL)")] + #[case::decimal("'1.23'::decimal", DatabaseBackend::MySql, "CAST('1.23' AS DECIMAL)")] + #[case::bytea("'\\xDE'::bytea", DatabaseBackend::MySql, "CAST('\\xDE' AS BINARY)")] + #[case::unknown("'x'::citext", DatabaseBackend::MySql, "CAST('x' AS CHAR)")] + fn test_convert_default_for_backend_type_cast_extra( + #[case] default: &str, + #[case] backend: DatabaseBackend, + #[case] expected: &str, + ) { + let result = convert_default_for_backend(default, &backend); + assert_eq!(result, expected); + } + #[test] fn test_is_enum_type_true() { use vespertide_core::EnumValues; diff --git a/crates/vespertide-query/src/sql/modify_column_nullable.rs b/crates/vespertide-query/src/sql/modify_column_nullable.rs index eba103e..b8ce98f 100644 --- a/crates/vespertide-query/src/sql/modify_column_nullable.rs +++ b/crates/vespertide-query/src/sql/modify_column_nullable.rs @@ -3,8 +3,8 @@ use sea_query::{Alias, Query, Table}; use vespertide_core::{ColumnDef, TableDef}; use super::helpers::{ - build_sea_column_def_with_table, build_sqlite_temp_table_create, normalize_fill_with, - recreate_indexes_after_rebuild, + build_sea_column_def_with_table, build_sqlite_temp_table_create, convert_default_for_backend, + normalize_fill_with, recreate_indexes_after_rebuild, }; use super::rename_table::build_rename_table; use super::types::{BuiltQuery, DatabaseBackend, RawSql}; @@ -24,6 +24,7 @@ pub fn build_modify_column_nullable( // If changing to NOT NULL, first update existing NULL values if fill_with is provided if !nullable && let Some(fill_value) = normalize_fill_with(fill_with) { + let fill_value = convert_default_for_backend(&fill_value, backend); let update_sql = match backend { DatabaseBackend::Postgres | DatabaseBackend::Sqlite => format!( "UPDATE \"{}\" SET \"{}\" = {} WHERE \"{}\" IS NULL", @@ -322,6 +323,67 @@ mod tests { }); } + /// Test fill_with containing NOW() should be converted to CURRENT_TIMESTAMP for all backends + #[rstest] + #[case::postgres_fill_now(DatabaseBackend::Postgres)] + #[case::mysql_fill_now(DatabaseBackend::MySql)] + #[case::sqlite_fill_now(DatabaseBackend::Sqlite)] + fn test_fill_with_now_converted_to_current_timestamp(#[case] backend: DatabaseBackend) { + let schema = vec![table_def( + "orders", + vec![ + col("id", ColumnType::Simple(SimpleColumnType::Integer), false), + col( + "paid_at", + ColumnType::Simple(SimpleColumnType::Timestamptz), + true, + ), + ], + vec![], + )]; + + let result = build_modify_column_nullable( + &backend, + "orders", + "paid_at", + false, + Some("NOW()"), + &schema, + ); + assert!(result.is_ok()); + let queries = result.unwrap(); + let sql = queries + .iter() + .map(|q| q.build(backend)) + .collect::>() + .join("\n"); + + // NOW() should be converted to CURRENT_TIMESTAMP for all backends + assert!( + !sql.contains("NOW()"), + "SQL should not contain NOW(), got: {}", + sql + ); + assert!( + sql.contains("CURRENT_TIMESTAMP"), + "SQL should contain CURRENT_TIMESTAMP, got: {}", + sql + ); + + let suffix = format!( + "{}_fill_now", + match backend { + DatabaseBackend::Postgres => "postgres", + DatabaseBackend::MySql => "mysql", + DatabaseBackend::Sqlite => "sqlite", + } + ); + + with_settings!({ snapshot_suffix => suffix }, { + assert_snapshot!(sql); + }); + } + /// Test with default value - should preserve default in MODIFY COLUMN (MySQL) #[rstest] #[case::postgres_with_default(DatabaseBackend::Postgres)] diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_MySql.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_MySql.snap new file mode 100644 index 0000000..a3296f4 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_MySql.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: sql +--- +ALTER TABLE `project` ADD COLUMN `story_index` json NOT NULL DEFAULT CAST('[]' AS JSON) diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_Postgres.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_Postgres.snap new file mode 100644 index 0000000..9e05ff5 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_Postgres.snap @@ -0,0 +1,5 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: sql +--- +ALTER TABLE "project" ADD COLUMN "story_index" json NOT NULL DEFAULT '[]'::json diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_Sqlite.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_Sqlite.snap new file mode 100644 index 0000000..8842654 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__add_column__tests__add_column_with_pg_type_cast_default@pg_type_cast_default_Sqlite.snap @@ -0,0 +1,8 @@ +--- +source: crates/vespertide-query/src/sql/add_column.rs +expression: sql +--- +CREATE TABLE "project_temp" ( "id" integer NOT NULL, "story_index" json_text NOT NULL DEFAULT '[]' ) +INSERT INTO "project_temp" ("id", "story_index") SELECT "id", '[]' AS "story_index" FROM "project" +DROP TABLE "project" +ALTER TABLE "project_temp" RENAME TO "project" diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@mysql_fill_now.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@mysql_fill_now.snap new file mode 100644 index 0000000..46c789d --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@mysql_fill_now.snap @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +UPDATE `orders` SET `paid_at` = CURRENT_TIMESTAMP WHERE `paid_at` IS NULL +ALTER TABLE `orders` MODIFY COLUMN `paid_at` timestamp NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@postgres_fill_now.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@postgres_fill_now.snap new file mode 100644 index 0000000..8122366 --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@postgres_fill_now.snap @@ -0,0 +1,6 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +UPDATE "orders" SET "paid_at" = CURRENT_TIMESTAMP WHERE "paid_at" IS NULL +ALTER TABLE "orders" ALTER COLUMN "paid_at" SET NOT NULL diff --git a/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@sqlite_fill_now.snap b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@sqlite_fill_now.snap new file mode 100644 index 0000000..6657fea --- /dev/null +++ b/crates/vespertide-query/src/sql/snapshots/vespertide_query__sql__modify_column_nullable__tests__fill_with_now_converted_to_current_timestamp@sqlite_fill_now.snap @@ -0,0 +1,9 @@ +--- +source: crates/vespertide-query/src/sql/modify_column_nullable.rs +expression: sql +--- +UPDATE "orders" SET "paid_at" = CURRENT_TIMESTAMP WHERE "paid_at" IS NULL +CREATE TABLE "orders_temp" ( "id" integer NOT NULL, "paid_at" timestamp_with_timezone_text NOT NULL ) +INSERT INTO "orders_temp" ("id", "paid_at") SELECT "id", "paid_at" FROM "orders" +DROP TABLE "orders" +ALTER TABLE "orders_temp" RENAME TO "orders"