From a782039e1f706302f18b45ed6318176f8b63f8f6 Mon Sep 17 00:00:00 2001 From: dadong <102886401+mr-dadong@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:11:33 +0800 Subject: [PATCH] fix(oracle): emit DEFAULT before NOT NULL in create-table columns --- .../src/table_structure_sql/create_table.rs | 20 +++++++-- .../dbx-sql/src/table_structure_sql/tests.rs | 44 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/crates/dbx-sql/src/table_structure_sql/create_table.rs b/crates/dbx-sql/src/table_structure_sql/create_table.rs index 24c75ca462..d33ac20505 100644 --- a/crates/dbx-sql/src/table_structure_sql/create_table.rs +++ b/crates/dbx-sql/src/table_structure_sql/create_table.rs @@ -110,6 +110,19 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr parts.push(format!("COLLATE {}", quote_ident(dialect, &column.collation))); } } + // Oracle's column grammar is `col type [DEFAULT expr] [inline constraint ...]`, so the + // DEFAULT clause has to precede NOT NULL. Emitting `NOT NULL DEFAULT ...` leaves the + // parser looking for the closing parenthesis of the column list and fails with + // ORA-00907 (t8y2/dbx#9477). Scoped to Oracle: every other engine keeps the historical + // NOT NULL-then-DEFAULT order. + let default_value = normalize_default(Some(&column.default_value)); + let default_clause = (!default_value.is_empty() && dialect != StructureDialect::ManticoreSearch) + .then(|| format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value))); + if dialect == StructureDialect::Oracle { + if let Some(clause) = default_clause.clone() { + parts.push(clause); + } + } if dialect == StructureDialect::Sqlite && column.is_primary_key && column.extra.as_ref().is_some_and(|e| e.auto_increment.unwrap_or(false)) @@ -130,9 +143,10 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr if let Some(extra_clause) = column_extra_clause(dialect, column) { parts.push(extra_clause); } - let default_value = normalize_default(Some(&column.default_value)); - if !default_value.is_empty() && dialect != StructureDialect::ManticoreSearch { - parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value))); + if dialect != StructureDialect::Oracle { + if let Some(clause) = default_clause { + parts.push(clause); + } } if let Some(on_update) = column.extra.as_ref().and_then(|e| e.on_update_current_timestamp).filter(|v| *v) { if on_update && dialect == StructureDialect::Mysql { diff --git a/crates/dbx-sql/src/table_structure_sql/tests.rs b/crates/dbx-sql/src/table_structure_sql/tests.rs index 11415398d2..610ec624a9 100644 --- a/crates/dbx-sql/src/table_structure_sql/tests.rs +++ b/crates/dbx-sql/src/table_structure_sql/tests.rs @@ -1207,6 +1207,50 @@ fn oracle_timestamp_default_precedes_nullability_in_modify_sql() { ); } +#[test] +fn oracle_create_table_places_default_before_not_null() { + // Oracle's column grammar is `col type [DEFAULT expr] [NOT NULL]`. Cloning a table whose + // NOT NULL column carries a default used to emit `NOT NULL DEFAULT ...`, which the server + // rejects with ORA-00907 (t8y2/dbx#9477). + let mut id = column("ID"); + id.data_type = "NUMBER(10)".to_string(); + id.is_primary_key = true; + let mut status = column("STATUS"); + status.data_type = "VARCHAR2(1 BYTE)".to_string(); + status.is_nullable = false; + status.default_value = "'A'".to_string(); + let mut created_at = column("CREATED_AT"); + created_at.data_type = "DATE".to_string(); + created_at.is_nullable = false; + created_at.default_value = "SYSDATE".to_string(); + + let result = build_create_table_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Oracle), + driver_profile: None, + schema: Some("APP".to_string()), + table_name: "ORDERS_COPY".to_string(), + columns: vec![id, status, created_at], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: None, + }); + + assert_eq!(result.warnings, Vec::::new()); + assert!( + result.statements[0].contains("\"STATUS\" VARCHAR2(1 BYTE) DEFAULT 'A' NOT NULL"), + "ddl: {}", + result.statements[0] + ); + assert!(result.statements[0].contains("CREATED_AT DATE DEFAULT SYSDATE NOT NULL"), "ddl: {}", result.statements[0]); + assert!(!result.statements[0].contains("NOT NULL DEFAULT"), "ddl: {}", result.statements[0]); +} + #[test] fn oracle_create_table_preserves_character_length_units() { let mut byte_col = column("BYTE_COL");