diff --git a/agents/drivers/oscar/src/main/java/com/dbx/agent/oscar/OscarAgent.java b/agents/drivers/oscar/src/main/java/com/dbx/agent/oscar/OscarAgent.java
index 3f9f056a2..12aa4f676 100644
--- a/agents/drivers/oscar/src/main/java/com/dbx/agent/oscar/OscarAgent.java
+++ b/agents/drivers/oscar/src/main/java/com/dbx/agent/oscar/OscarAgent.java
@@ -2,8 +2,29 @@ package com.dbx.agent.oscar;
import com.dbx.agent.ConfiguredJdbcAgent;
import com.dbx.agent.JdbcAgentProfile;
+import com.dbx.agent.JdbcIdentifiers;
import com.dbx.agent.MultiSessionJsonRpcServer;
+import com.dbx.agent.ObjectSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Locale;
+/**
+ * 神通 OSCAR v7 agent。
+ *
+ *
神通是 Oracle 兼容的国产数据库,系统视图(ALL_VIEWS/ALL_SOURCE/ALL_TRIGGERS/ALL_SEQUENCES 等)
+ * 与 Oracle 基本一致,但有两处关键差异(实测 v7.0.8,见 issue #5505):
+ *
+ * - {@code DBMS_METADATA.GET_DDL} 不可用("Namespace DBMS_METADATA does not exist"),
+ * 故视图/过程/函数/触发器源码改用 ALL_VIEWS.TEXT、ALL_SOURCE.TEXT、ALL_TRIGGERS.TRIGGER_BODY 读取。
+ * - {@code SET SCHEMA "x"} 与 {@code ALTER SESSION SET CURRENT_SCHEMA} 均不被 parser 接受,
+ * schema 切换改用 PG 风格的 {@code SET SEARCH_PATH TO "x"}(已验证可用,且不影响 ALL_* 视图的 OWNER 过滤)。
+ *
+ *
+ * 建表/表结构编辑由 DBX 核心的 Oracle 方言 SQL 生成器负责(神通实测支持 ALTER TABLE
+ * ADD/MODIFY/DROP/RENAME COLUMN、DROP/ADD PRIMARY KEY、COMMENT ON、CREATE/DROP INDEX),
+ * 本 agent 仅需提供元数据(继承 ConfiguredJdbcAgent 的标准 JDBC metadata)与对象源码。
+ */
public final class OscarAgent extends ConfiguredJdbcAgent {
public static final JdbcAgentProfile OSCAR_PROFILE = new JdbcAgentProfile(
"com.oscar.Driver",
@@ -18,4 +39,148 @@ public final class OscarAgent extends ConfiguredJdbcAgent {
public static void main(String[] args) {
new MultiSessionJsonRpcServer(OscarAgent::new).run();
}
+
+ /**
+ * 神通用 {@code SET SEARCH_PATH TO "schema"} 切换默认 schema(PG 风格),
+ * 不支持 Oracle 的 {@code ALTER SESSION SET CURRENT_SCHEMA} 或 Dameng 的 {@code SET SCHEMA "x"}。
+ * 切换后 ALL_* 视图仍按 OWNER 正确过滤,不影响 owner 语义。
+ */
+ @Override
+ public String setSchemaSQL(String schema) {
+ if (schema == null || schema.trim().isEmpty()) {
+ return "";
+ }
+ return "SET SEARCH_PATH TO " + JdbcIdentifiers.INSTANCE.doubleQuote(schema);
+ }
+
+ /**
+ * 读取视图/过程/函数/触发器/序列等对象的 DDL 源码。
+ *
+ *
神通不支持 DBMS_METADATA.GET_DDL,按对象类型走对应系统视图:
+ *
+ * - VIEW → ALL_VIEWS.TEXT(返回完整 SELECT 语句)
+ * - PROCEDURE/FUNCTION/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → ALL_SOURCE.TEXT 按 LINE 排序拼接
+ * - TRIGGER → ALL_TRIGGERS.TRIGGER_BODY
+ * - SEQUENCE → 由 ALL_SEQUENCES 元数据重建 CREATE SEQUENCE 语句
+ * - MATERIALIZED_VIEW → 神通无 ALL_MVIEWS,返回空源码且不可编辑
+ *
+ */
+ @Override
+ public ObjectSource getObjectSource(String schema, String name, String objectType) {
+ String type = objectType == null ? "" : objectType.trim().toUpperCase(Locale.ROOT);
+ return unchecked(() -> {
+ String source = switch (type) {
+ case "VIEW" -> readViewSource(schema, name);
+ case "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY" -> readSourceText(schema, name, type);
+ case "TRIGGER" -> readTriggerSource(schema, name);
+ case "SEQUENCE" -> readSequenceSource(schema, name);
+ // 神通 v7 无物化视图系统视图(ALL_MVIEWS 不存在);返回空源码,标记不可编辑,避免 UI 误判为可改。
+ case "MATERIALIZED_VIEW" -> "";
+ default -> throw new IllegalArgumentException("Unsupported object type: " + objectType);
+ };
+ boolean editable = !"MATERIALIZED_VIEW".equals(type);
+ return new ObjectSource(name, objectType, schema, source, editable);
+ });
+ }
+
+ private String readViewSource(String schema, String name) throws Exception {
+ // ALL_VIEWS.TEXT 已包含完整视图定义(神通实测返回带 schema 限定的 SELECT 语句)。
+ String sql = "SELECT TEXT FROM ALL_VIEWS WHERE OWNER = ? AND VIEW_NAME = ?";
+ return scalarText(sql, schema, name);
+ }
+
+ private String readSourceText(String schema, String name, String type) throws Exception {
+ // ALL_SOURCE 按 LINE 存储过程/函数/包/类型的源码行,需按 LINE 排序拼接。
+ // 神通与 Oracle 的关键差异(实测 v7.0.8):function 也以 TYPE='PROCEDURE' 存储(不区分 FUNCTION),
+ // 包体随包一起存为 TYPE='PACKAGE',类型体随类型存为 TYPE='TYPE'。故按 objectType 归并到实际 TYPE 值。
+ String sourceType = switch (type) {
+ case "PROCEDURE", "FUNCTION" -> "PROCEDURE";
+ case "PACKAGE", "PACKAGE_BODY" -> "PACKAGE";
+ case "TYPE", "TYPE_BODY" -> "TYPE";
+ default -> type;
+ };
+ String sql = "SELECT TEXT FROM ALL_SOURCE WHERE OWNER = ? AND NAME = ? AND TYPE = ? ORDER BY LINE";
+ StringBuilder sb = new StringBuilder();
+ try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
+ stmt.setString(1, schema);
+ stmt.setString(2, name);
+ stmt.setString(3, sourceType);
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ String line = rs.getString(1);
+ if (line != null) {
+ sb.append(line);
+ }
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ private String readTriggerSource(String schema, String name) throws Exception {
+ String sql = "SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = ? AND TRIGGER_NAME = ?";
+ return scalarText(sql, schema, name);
+ }
+
+ private String readSequenceSource(String schema, String name) throws Exception {
+ // 神通无 DBMS_METADATA,序列源码由 ALL_SEQUENCES 元数据重建为 CREATE SEQUENCE 语句。
+ String sql = "SELECT MIN_VALUE, MAX_VALUE, INCREMENT_BY, CYCLE_FLAG, ORDER_FLAG, CACHE_SIZE "
+ + "FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = ? AND SEQUENCE_NAME = ?";
+ try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
+ stmt.setString(1, schema);
+ stmt.setString(2, name);
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (!rs.next()) {
+ return "";
+ }
+ String ref = JdbcIdentifiers.INSTANCE.doubleQuote(schema) + "." + JdbcIdentifiers.INSTANCE.doubleQuote(name);
+ StringBuilder sb = new StringBuilder("CREATE SEQUENCE ").append(ref);
+ String minValue = rs.getString("MIN_VALUE");
+ String maxValue = rs.getString("MAX_VALUE");
+ String increment = rs.getString("INCREMENT_BY");
+ if (increment != null && !"1".equals(increment.trim())) {
+ sb.append(" INCREMENT BY ").append(increment.trim());
+ }
+ if (minValue != null && !"1".equals(minValue.trim())) {
+ sb.append(" MINVALUE ").append(minValue.trim());
+ }
+ if (maxValue != null) {
+ sb.append(" MAXVALUE ").append(maxValue.trim());
+ }
+ String cache = rs.getString("CACHE_SIZE");
+ if (cache != null && !"0".equals(cache.trim())) {
+ sb.append(" CACHE ").append(cache.trim());
+ } else {
+ sb.append(" NOCACHE");
+ }
+ if ("Y".equalsIgnoreCase(rs.getString("CYCLE_FLAG"))) {
+ sb.append(" CYCLE");
+ } else {
+ sb.append(" NOCYCLE");
+ }
+ if ("Y".equalsIgnoreCase(rs.getString("ORDER_FLAG"))) {
+ sb.append(" ORDER");
+ } else {
+ sb.append(" NOORDER");
+ }
+ sb.append(";");
+ return sb.toString();
+ }
+ }
+ }
+
+ /** 读取单行单列文本值(视图/触发器源码),空结果返回空串。 */
+ private String scalarText(String sql, String schema, String name) throws Exception {
+ try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
+ stmt.setString(1, schema);
+ stmt.setString(2, name);
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (!rs.next()) {
+ return "";
+ }
+ String value = rs.getString(1);
+ return value == null ? "" : value;
+ }
+ }
+ }
}
diff --git a/apps/desktop/src/lib/table/tableStructureCapabilities.ts b/apps/desktop/src/lib/table/tableStructureCapabilities.ts
index 26f20db81..e9dcd9922 100644
--- a/apps/desktop/src/lib/table/tableStructureCapabilities.ts
+++ b/apps/desktop/src/lib/table/tableStructureCapabilities.ts
@@ -315,7 +315,7 @@ const capabilityByType: Partial
starrocks: mysqlCapabilities,
goldendb: mysqlCapabilities,
sundb: mysqlCapabilities,
- oscar: unsupportedCapabilities,
+ oscar: damengCapabilities,
databend: mysqlCapabilities,
gbase: gbaseCapabilities,
postgres: postgresCapabilities,
diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json
index bb3b06e97..22620dff6 100644
--- a/crates/dbx-core/assets/database-drivers.manifest.json
+++ b/crates/dbx-core/assets/database-drivers.manifest.json
@@ -1655,11 +1655,11 @@
"queryExecution": true,
"metadataBrowse": true,
"objectBrowser": true,
- "objectSource": false,
+ "objectSource": true,
"schemaSearch": true,
"diagram": false,
"tableDataEdit": true,
- "tableStructureEdit": false,
+ "tableStructureEdit": true,
"tableImport": false,
"dataTransfer": false,
"sqlFileExecution": true,
diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs
index 1ccb597f4..24fba5119 100644
--- a/crates/dbx-core/src/db_admin_sql.rs
+++ b/crates/dbx-core/src/db_admin_sql.rs
@@ -771,7 +771,8 @@ fn is_postgres_like_rename(database_type: DatabaseType) -> bool {
}
fn is_oracle_like_rename(database_type: DatabaseType) -> bool {
- matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng)
+ // 神通 Oscar 实测支持 `ALTER TABLE old RENAME TO new`(PG 风格,与 Dameng/Oracle 一致)。
+ matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::Oscar)
}
fn is_postgres_like_structure_copy(database_type: DatabaseType) -> bool {
@@ -1933,6 +1934,22 @@ mod tests {
);
}
+ #[test]
+ fn builds_oscar_table_rename_sql() {
+ // 神通实测支持 `ALTER TABLE old RENAME TO new`(issue #5505 探测)。
+ assert_eq!(
+ build_rename_object_sql(RenameObjectSqlOptions {
+ database_type: Some(DatabaseType::Oscar),
+ object_type: DatabaseObjectType::Table,
+ schema: Some("SYSDBA".to_string()),
+ old_name: "OLD_USERS".to_string(),
+ new_name: "NEW_USERS".to_string(),
+ })
+ .unwrap(),
+ "ALTER TABLE \"SYSDBA\".\"OLD_USERS\" RENAME TO \"NEW_USERS\";"
+ );
+ }
+
#[test]
fn rejects_unsupported_direct_routine_renames() {
assert!(!supports_object_rename(Some(DatabaseType::Oracle), DatabaseObjectType::Function));
diff --git a/crates/dbx-core/src/table_structure_sql/column_alter.rs b/crates/dbx-core/src/table_structure_sql/column_alter.rs
index ad787bc8d..2bfdfd65c 100644
--- a/crates/dbx-core/src/table_structure_sql/column_alter.rs
+++ b/crates/dbx-core/src/table_structure_sql/column_alter.rs
@@ -96,6 +96,7 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
statements.extend(build_oracle_like_existing_column_sql(dialect, &table, &options.column))
}
}
+ StructureDialect::Oscar => statements.extend(build_oscar_existing_column_sql(dialect, &table, &options.column)),
StructureDialect::H2 => statements.extend(build_h2_existing_column_sql(&table, &options.column)),
StructureDialect::ClickHouse => {
statements.extend(build_clickhouse_existing_column_sql(&table, &options.column, ""))
@@ -509,6 +510,64 @@ pub(super) fn build_iris_existing_column_sql(table: &str, column: &EditableStruc
statements
}
+/// 神通 Oscar 的 ALTER 已有列 SQL。
+///
+/// 神通的 `ALTER TABLE ... MODIFY` 语法与 Oracle 有重要差异(实测 v7.0.8):
+/// 带圆括号的 `MODIFY (col TYPE [DEFAULT ...])` 不允许出现 `NULL`/`NOT NULL`,否则
+/// parser 报 `syntax error at or near "NULL"`。要改可空性必须用不带括号、不带类型的
+/// `MODIFY col NOT NULL` / `MODIFY col NULL` 单独一条。因此类型/默认值变更与可空性
+/// 变更需拆成两条语句,而不能像 Oracle/Dameng 那样合并进单个 `MODIFY (...)`。
+pub(super) fn build_oscar_existing_column_sql(
+ dialect: StructureDialect,
+ table: &str,
+ column: &EditableStructureColumn,
+) -> Vec {
+ let Some(original) = &column.original else {
+ return Vec::new();
+ };
+ let mut statements = Vec::new();
+ let mut current_name = original.name.clone();
+ if column.name != original.name {
+ statements.push(format!(
+ "ALTER TABLE {table} RENAME COLUMN {} TO {};",
+ quote_ident(dialect, &original.name),
+ quote_ident(dialect, &column.name)
+ ));
+ current_name = column.name.clone();
+ }
+ let type_changed = column.data_type.trim() != original.data_type.trim();
+ let nullable_changed = column.is_nullable != original.is_nullable;
+ let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
+
+ // 类型或默认值变更:带括号的 MODIFY 只允许 "col TYPE [DEFAULT ...]",不含 NULL/NOT NULL。
+ if type_changed || default_changed {
+ let data_type = column_data_type(dialect, column);
+ let mut parts = vec![quote_ident(dialect, ¤t_name), data_type];
+ let default_value = normalize_default(Some(&column.default_value));
+ if !default_value.is_empty() {
+ parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
+ } else if default_changed {
+ // User cleared the default — explicitly drop it. MODIFY (col DEFAULT NULL) 合法。
+ parts.push("DEFAULT NULL".to_string());
+ }
+ statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
+ }
+
+ // 可空性变更:神通要求不带括号、不带类型的单独 MODIFY,否则 parser 报错。
+ if nullable_changed {
+ let nullability = if column.is_nullable { "NULL" } else { "NOT NULL" };
+ statements.push(format!("ALTER TABLE {table} MODIFY {} {};", quote_ident(dialect, ¤t_name), nullability));
+ }
+
+ if clean(&column.comment) != original_comment(column) {
+ let comment_value =
+ if clean(&column.comment).is_empty() { "NULL".to_string() } else { quote_string(&clean(&column.comment)) };
+ statements
+ .push(format!("COMMENT ON COLUMN {table}.{} IS {comment_value};", quote_ident(dialect, ¤t_name)));
+ }
+ statements
+}
+
pub(super) fn build_sqlserver_existing_column_sql(
table: &str,
column: &EditableStructureColumn,
diff --git a/crates/dbx-core/src/table_structure_sql/column_format.rs b/crates/dbx-core/src/table_structure_sql/column_format.rs
index 3d7c7913b..58b7c020b 100644
--- a/crates/dbx-core/src/table_structure_sql/column_format.rs
+++ b/crates/dbx-core/src/table_structure_sql/column_format.rs
@@ -323,7 +323,7 @@ pub(super) fn is_temporal_precision_type(dialect: StructureDialect, base_type: &
| "timestamp with time zone"
),
StructureDialect::SqlServer => matches!(normalized.as_str(), "time" | "datetime2" | "datetimeoffset"),
- StructureDialect::Oracle | StructureDialect::Dameng => {
+ StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::Oscar => {
matches!(normalized.as_str(), "timestamp" | "timestamp with time zone" | "timestamp with local time zone")
}
_ => false,
@@ -334,7 +334,11 @@ pub(super) fn is_valid_temporal_precision(params: &str, dialect: StructureDialec
let Ok(value) = params.parse::() else {
return false;
};
- let max = if matches!(dialect, StructureDialect::Oracle | StructureDialect::Dameng) { 9 } else { 6 };
+ let max = if matches!(dialect, StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::Oscar) {
+ 9
+ } else {
+ 6
+ };
value <= max && params == value.to_string()
}
diff --git a/crates/dbx-core/src/table_structure_sql/columns.rs b/crates/dbx-core/src/table_structure_sql/columns.rs
index d75722436..941a7da6b 100644
--- a/crates/dbx-core/src/table_structure_sql/columns.rs
+++ b/crates/dbx-core/src/table_structure_sql/columns.rs
@@ -1,9 +1,9 @@
use super::column_alter::{
build_clickhouse_existing_column_sql, build_doris_existing_column_sql, build_h2_existing_column_sql,
build_informix_existing_column_sql, build_iris_existing_column_sql, build_mysql_existing_column_sql,
- build_oracle_like_existing_column_sql, build_postgres_existing_column_sql, build_questdb_existing_column_sql,
- build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql, build_xugu_existing_column_sql,
- has_column_extra_change, has_existing_column_attribute_change,
+ build_oracle_like_existing_column_sql, build_oscar_existing_column_sql, build_postgres_existing_column_sql,
+ build_questdb_existing_column_sql, build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql,
+ build_xugu_existing_column_sql, has_column_extra_change, has_existing_column_attribute_change,
};
use super::column_format::{
column_definition, has_dameng_identity, is_dameng_identity_compatible_type, is_mysql_character_data_type,
@@ -186,6 +186,8 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
statements.extend(build_oracle_like_existing_column_sql(dialect, &table, column))
}
}
+ // 神通 MODIFY 语法与 Oracle 有差异(NULL/NOT NULL 须单独一条),用专属实现。
+ StructureDialect::Oscar => statements.extend(build_oscar_existing_column_sql(dialect, &table, column)),
StructureDialect::H2 => statements.extend(build_h2_existing_column_sql(&table, column)),
StructureDialect::ClickHouse => statements.extend(build_clickhouse_existing_column_sql(
&table,
@@ -301,7 +303,10 @@ fn drop_primary_key_statement(
let pk_name = format!("{}_pkey", clean(raw_table));
Some(format!("ALTER TABLE {table} DROP CONSTRAINT {};", quote_ident(dialect, &pk_name)))
}
- StructureDialect::Mysql | StructureDialect::Dameng => Some(format!("ALTER TABLE {table} DROP PRIMARY KEY;")),
+ // 神通 Oscar 实测支持 `ALTER TABLE ... DROP PRIMARY KEY`(与 Dameng/MySQL 一致)。
+ StructureDialect::Mysql | StructureDialect::Dameng | StructureDialect::Oscar => {
+ Some(format!("ALTER TABLE {table} DROP PRIMARY KEY;"))
+ }
_ => None,
}
}
@@ -362,7 +367,10 @@ pub(super) fn build_add_column_sql(
vec![format!("ALTER TABLE {table} {add_keyword} {definition}{position_clause};")]
};
if supports_comments
- && matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Dameng)
+ && matches!(
+ dialect,
+ StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::Oscar
+ )
&& !clean(&column.comment).is_empty()
{
statements.push(format!(
diff --git a/crates/dbx-core/src/table_structure_sql/comments.rs b/crates/dbx-core/src/table_structure_sql/comments.rs
index ede1f2357..74f7a8ff9 100644
--- a/crates/dbx-core/src/table_structure_sql/comments.rs
+++ b/crates/dbx-core/src/table_structure_sql/comments.rs
@@ -23,7 +23,11 @@ pub(super) fn build_table_comment_sql(options: &TableStructureSqlOptions, warnin
StructureDialect::Mysql => {
vec![format!("ALTER TABLE {table} COMMENT = {quoted};")]
}
- StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::H2 => {
+ StructureDialect::Postgres
+ | StructureDialect::Oracle
+ | StructureDialect::Dameng
+ | StructureDialect::Oscar
+ | StructureDialect::H2 => {
vec![format!("COMMENT ON TABLE {table} IS {quoted};")]
}
StructureDialect::ClickHouse => {
diff --git a/crates/dbx-core/src/table_structure_sql/create_table.rs b/crates/dbx-core/src/table_structure_sql/create_table.rs
index 198955096..01222b9ad 100644
--- a/crates/dbx-core/src/table_structure_sql/create_table.rs
+++ b/crates/dbx-core/src/table_structure_sql/create_table.rs
@@ -102,7 +102,11 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
}
} else if matches!(
dialect,
- StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::H2
+ StructureDialect::Postgres
+ | StructureDialect::Oracle
+ | StructureDialect::Dameng
+ | StructureDialect::Oscar
+ | StructureDialect::H2
) {
statements.push(format!("COMMENT ON TABLE {table} IS {};", quote_string(&table_comment)));
} else if dialect == StructureDialect::ClickHouse {
@@ -121,7 +125,11 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
if capabilities.comment
&& matches!(
dialect,
- StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::H2
+ StructureDialect::Postgres
+ | StructureDialect::Oracle
+ | StructureDialect::Dameng
+ | StructureDialect::Oscar
+ | StructureDialect::H2
)
{
for column in &active_columns {
diff --git a/crates/dbx-core/src/table_structure_sql/dialect.rs b/crates/dbx-core/src/table_structure_sql/dialect.rs
index 411f4834e..c2643eea8 100644
--- a/crates/dbx-core/src/table_structure_sql/dialect.rs
+++ b/crates/dbx-core/src/table_structure_sql/dialect.rs
@@ -11,6 +11,9 @@ pub(super) enum StructureDialect {
SqlServer,
Oracle,
Dameng,
+ // 神通 Oscar:Oracle 兼容方言,且支持 ALTER TABLE DROP/ADD PRIMARY KEY(与 Dameng 一致,
+ // 不同于 Oracle)。DDL 生成行为与 Dameng 共享分支;单独成 dialect 以保证 label/反向映射准确。
+ Oscar,
H2,
ClickHouse,
ManticoreSearch,
@@ -212,6 +215,22 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru
alter_primary_key: true,
..base
},
+ // 神通 Oscar v7:Oracle 兼容,实测支持 ALTER TABLE ADD/MODIFY/DROP/RENAME COLUMN、
+ // DROP/ADD PRIMARY KEY、COMMENT ON、CREATE/DROP INDEX(见 issue #5505 探测)。
+ Some(DatabaseType::Oscar) => TableStructureCapabilities {
+ dialect: StructureDialect::Oscar,
+ add_column: true,
+ drop_column: true,
+ rename_column: true,
+ alter_existing_column: true,
+ comment: true,
+ create_index: true,
+ drop_index: true,
+ rebuild_index: true,
+ index_type: true,
+ alter_primary_key: true,
+ ..base
+ },
Some(DatabaseType::Iris) => TableStructureCapabilities {
dialect: StructureDialect::Oracle,
add_column: true,
@@ -285,7 +304,7 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru
}
pub(super) fn is_oracle_like(dialect: StructureDialect) -> bool {
- matches!(dialect, StructureDialect::Oracle | StructureDialect::Dameng)
+ matches!(dialect, StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::Oscar)
}
pub(super) fn database_label(database_type: Option) -> String {
@@ -310,6 +329,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
StructureDialect::SqlServer => "sqlserver",
StructureDialect::Oracle => "oracle",
StructureDialect::Dameng => "dameng",
+ StructureDialect::Oscar => "oscar",
StructureDialect::H2 => "h2",
StructureDialect::ClickHouse => "clickhouse",
StructureDialect::ManticoreSearch => "manticoresearch",
@@ -331,6 +351,7 @@ pub(super) fn database_type_for_dialect(dialect: StructureDialect) -> Option Some(DatabaseType::SqlServer),
StructureDialect::Oracle => Some(DatabaseType::Oracle),
StructureDialect::Dameng => Some(DatabaseType::Dameng),
+ StructureDialect::Oscar => Some(DatabaseType::Oscar),
StructureDialect::H2 => Some(DatabaseType::H2),
StructureDialect::ClickHouse => Some(DatabaseType::ClickHouse),
StructureDialect::ManticoreSearch => Some(DatabaseType::ManticoreSearch),
diff --git a/crates/dbx-core/src/table_structure_sql/indexes.rs b/crates/dbx-core/src/table_structure_sql/indexes.rs
index 9525d98d0..7bff0e00a 100644
--- a/crates/dbx-core/src/table_structure_sql/indexes.rs
+++ b/crates/dbx-core/src/table_structure_sql/indexes.rs
@@ -144,6 +144,7 @@ pub(super) fn build_drop_index_sql(
StructureDialect::Postgres
| StructureDialect::Oracle
| StructureDialect::Dameng
+ | StructureDialect::Oscar
| StructureDialect::Informix
| StructureDialect::Sqlite
) && schema.is_some_and(|schema| !schema.trim().is_empty())
diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs
index 57aa1a519..748622144 100644
--- a/crates/dbx-core/src/table_structure_sql/tests.rs
+++ b/crates/dbx-core/src/table_structure_sql/tests.rs
@@ -4109,3 +4109,242 @@ fn mysql_character_column_preserves_charset_collation_on_other_change() {
vec!["ALTER TABLE `users` MODIFY COLUMN `name` varchar(255) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_ci` DEFAULT 'guest';"]
);
}
+
+// ---- Oscar (神通) ----
+// 神通 v7 是 Oracle 兼容方言,且实测支持 ALTER TABLE DROP/ADD PRIMARY KEY(与 Dameng 一致,
+// 不同于 Oracle)。DDL 生成走 StructureDialect::Oscar,与 Dameng 共享 Oracle-like 分支。
+// 这些测试锁定 issue #5505 的核心场景:建表/改列/主键/索引/注释,防回归。
+
+#[test]
+fn oscar_create_table_with_primary_key_and_comments() {
+ let mut id = column("ID");
+ id.data_type = "NUMBER(10)".to_string();
+ id.is_nullable = false;
+ id.is_primary_key = true;
+ let mut name = column("NAME");
+ name.data_type = "VARCHAR2(100)".to_string();
+ name.is_nullable = false;
+ name.comment = "name col".to_string();
+
+ let result = build_create_table_sql(TableStructureSqlOptions {
+ database_type: Some(DatabaseType::Oscar),
+ schema: Some("SYSDBA".to_string()),
+ table_name: "USERS".to_string(),
+ columns: vec![id, name],
+ indexes: Vec::new(),
+ foreign_keys: Vec::new(),
+ triggers: Vec::new(),
+ table_comment: Some("user table".to_string()),
+ original_table_comment: None,
+ });
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert!(result.statements[0].contains("CREATE TABLE \"SYSDBA\".\"USERS\""), "ddl: {}", result.statements[0]);
+ // Oracle 风格:PK 在表定义末尾单独声明;PK 列省略 NOT NULL(主键隐含),非 PK 非空列显式 NOT NULL。
+ assert!(result.statements[0].contains("\"ID\" NUMBER(10),"), "ddl: {}", result.statements[0]);
+ assert!(result.statements[0].contains("\"NAME\" VARCHAR2(100) NOT NULL,"), "ddl: {}", result.statements[0]);
+ assert!(result.statements[0].contains("PRIMARY KEY (\"ID\")"), "ddl: {}", result.statements[0]);
+ assert!(
+ result.statements.iter().any(|s| s == "COMMENT ON TABLE \"SYSDBA\".\"USERS\" IS 'user table';"),
+ "comments: {:?}",
+ result.statements
+ );
+ assert!(
+ result.statements.iter().any(|s| s == "COMMENT ON COLUMN \"SYSDBA\".\"USERS\".\"NAME\" IS 'name col';"),
+ "comments: {:?}",
+ result.statements
+ );
+}
+
+#[test]
+fn oscar_add_column_with_comment() {
+ let mut age = column("AGE");
+ age.data_type = "NUMBER(3)".to_string();
+ age.is_nullable = true;
+ age.comment = "age col".to_string();
+
+ let result = build_table_structure_change_sql(structure_change_options(
+ DatabaseType::Oscar,
+ Some("SYSDBA"),
+ "users",
+ vec![age],
+ ));
+
+ assert_eq!(result.warnings, Vec::::new());
+ // Oracle 风格:ADD 用圆括号包裹列定义,可空列省略 NULL 关键字。
+ assert_eq!(
+ result.statements,
+ vec![
+ "ALTER TABLE \"SYSDBA\".\"users\" ADD (\"AGE\" NUMBER(3));",
+ "COMMENT ON COLUMN \"SYSDBA\".\"users\".\"AGE\" IS 'age col';",
+ ]
+ );
+}
+
+#[test]
+fn oscar_alter_existing_column_modify_type_and_nullability() {
+ let mut name = column("NAME");
+ name.data_type = "VARCHAR2(100)".to_string();
+ name.is_nullable = false;
+ name.original = Some(ColumnInfo {
+ name: "NAME".to_string(),
+ data_type: "VARCHAR2(50)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: None,
+ ..Default::default()
+ });
+
+ let result = build_table_structure_change_sql(structure_change_options(
+ DatabaseType::Oscar,
+ Some("SYSDBA"),
+ "users",
+ vec![name],
+ ));
+
+ assert_eq!(result.warnings, Vec::::new());
+ // 神通 MODIFY 语法差异:类型变更与可空性变更需拆成两条(带括号的 MODIFY 不允许 NULL/NOT NULL)。
+ assert_eq!(
+ result.statements,
+ vec![
+ "ALTER TABLE \"SYSDBA\".\"users\" MODIFY (\"NAME\" VARCHAR2(100));",
+ "ALTER TABLE \"SYSDBA\".\"users\" MODIFY \"NAME\" NOT NULL;",
+ ]
+ );
+}
+
+#[test]
+fn oscar_alter_only_nullability_emits_single_unparenthesized_modify() {
+ // 只改可空性(类型不变):应只生成一条不带括号的 MODIFY col NOT NULL,不重复发类型变更。
+ let mut name = column("NAME");
+ name.data_type = "VARCHAR2(100)".to_string();
+ name.is_nullable = false;
+ name.original = Some(ColumnInfo {
+ name: "NAME".to_string(),
+ data_type: "VARCHAR2(100)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: None,
+ ..Default::default()
+ });
+
+ let result = build_table_structure_change_sql(structure_change_options(
+ DatabaseType::Oscar,
+ Some("SYSDBA"),
+ "users",
+ vec![name],
+ ));
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert_eq!(result.statements, vec!["ALTER TABLE \"SYSDBA\".\"users\" MODIFY \"NAME\" NOT NULL;"]);
+}
+
+#[test]
+fn oscar_alter_only_default_keeps_parenthesized_modify() {
+ // 只改默认值(类型与可空性不变):带括号的 MODIFY 允许 DEFAULT,不触发可空性单独语句。
+ let mut name = column("NAME");
+ name.data_type = "VARCHAR2(100)".to_string();
+ name.is_nullable = true;
+ name.default_value = "'guest'".to_string();
+ name.original = Some(ColumnInfo {
+ name: "NAME".to_string(),
+ data_type: "VARCHAR2(100)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: None,
+ ..Default::default()
+ });
+
+ let result = build_table_structure_change_sql(structure_change_options(
+ DatabaseType::Oscar,
+ Some("SYSDBA"),
+ "users",
+ vec![name],
+ ));
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert_eq!(
+ result.statements,
+ vec!["ALTER TABLE \"SYSDBA\".\"users\" MODIFY (\"NAME\" VARCHAR2(100) DEFAULT 'guest');"]
+ );
+}
+
+#[test]
+fn oscar_drop_and_readd_primary_key() {
+ // 神通实测支持 ALTER TABLE DROP/ADD PRIMARY KEY(与 Dameng 一致)。
+ let mut old_pk = existing_pk_column("id", "INT", true, false);
+ old_pk.id = "old_id".to_string();
+ let mut new_pk = existing_pk_column("code", "VARCHAR(50)", false, true);
+ new_pk.id = "new_code".to_string();
+
+ let result = build_table_structure_change_sql(structure_change_options(
+ DatabaseType::Oscar,
+ Some("SYSDBA"),
+ "users",
+ vec![old_pk, new_pk],
+ ));
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert_eq!(
+ result.statements,
+ vec![
+ "ALTER TABLE \"SYSDBA\".\"users\" DROP PRIMARY KEY;",
+ "ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"code\");",
+ ]
+ );
+}
+
+#[test]
+fn oscar_drop_index_with_schema_qualifier() {
+ let mut idx = index("DBX_PROBE_IDX", &["NAME"]);
+ idx.marked_for_drop = true;
+ idx.original = Some(IndexInfo {
+ name: "DBX_PROBE_IDX".to_string(),
+ columns: vec!["NAME".to_string()],
+ is_unique: false,
+ is_primary: false,
+ filter: None,
+ index_type: None,
+ included_columns: None,
+ comment: None,
+ });
+
+ let result = build_table_structure_change_sql(TableStructureSqlOptions {
+ database_type: Some(DatabaseType::Oscar),
+ schema: Some("SYSDBA".to_string()),
+ table_name: "users".to_string(),
+ columns: Vec::new(),
+ indexes: vec![idx],
+ foreign_keys: Vec::new(),
+ triggers: Vec::new(),
+ table_comment: None,
+ original_table_comment: None,
+ });
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert_eq!(result.statements, vec!["DROP INDEX \"SYSDBA\".\"DBX_PROBE_IDX\";"]);
+}
+
+#[test]
+fn oscar_table_comment_uses_comment_on_table() {
+ let result = build_table_structure_change_sql(TableStructureSqlOptions {
+ database_type: Some(DatabaseType::Oscar),
+ schema: Some("SYSDBA".to_string()),
+ table_name: "users".to_string(),
+ columns: Vec::new(),
+ indexes: Vec::new(),
+ foreign_keys: Vec::new(),
+ triggers: Vec::new(),
+ table_comment: Some("new comment".to_string()),
+ original_table_comment: Some("old comment".to_string()),
+ });
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert_eq!(result.statements, vec!["COMMENT ON TABLE \"SYSDBA\".\"users\" IS 'new comment';"]);
+}
diff --git a/crates/dbx-core/src/table_structure_sql/util.rs b/crates/dbx-core/src/table_structure_sql/util.rs
index f13b0c72e..2a097aafa 100644
--- a/crates/dbx-core/src/table_structure_sql/util.rs
+++ b/crates/dbx-core/src/table_structure_sql/util.rs
@@ -7,6 +7,7 @@ pub(super) fn qualified_table(dialect: StructureDialect, schema: Option<&str>, t
StructureDialect::Postgres
| StructureDialect::Oracle
| StructureDialect::Dameng
+ | StructureDialect::Oscar
| StructureDialect::SqlServer
| StructureDialect::H2
| StructureDialect::Informix
@@ -144,7 +145,7 @@ pub(super) fn is_temporal_type_for_default(dialect: StructureDialect, base_type:
normalized.as_str(),
"date" | "time" | "datetime" | "datetime2" | "smalldatetime" | "datetimeoffset"
),
- StructureDialect::Oracle | StructureDialect::Dameng => matches!(
+ StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::Oscar => matches!(
normalized.as_str(),
"date"
| "timestamp"
@@ -230,7 +231,7 @@ pub(super) fn is_string_type_for_default(dialect: StructureDialect, base_type: &
normalized.as_str(),
"char" | "varchar" | "nchar" | "nvarchar" | "text" | "ntext" | "xml" | "uniqueidentifier" | "sysname"
),
- StructureDialect::Oracle | StructureDialect::Dameng => matches!(
+ StructureDialect::Oracle | StructureDialect::Dameng | StructureDialect::Oscar => matches!(
normalized.as_str(),
"char" | "nchar" | "varchar2" | "nvarchar2" | "clob" | "nclob" | "long" | "raw" | "long raw" | "bfile"
),