feat(oscar): enable table structure editing and view source for Shentong

This commit is contained in:
t8y2 2026-08-07 11:58:19 +08:00
parent dd5d74fa4f
commit ae13ca9c1a
No known key found for this signature in database
13 changed files with 544 additions and 17 deletions

View File

@ -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
*
* <p>神通是 Oracle 兼容的国产数据库系统视图ALL_VIEWS/ALL_SOURCE/ALL_TRIGGERS/ALL_SEQUENCES
* Oracle 基本一致但有两处关键差异实测 v7.0.8 issue #5505
* <ul>
* <li>{@code DBMS_METADATA.GET_DDL} 不可用"Namespace DBMS_METADATA does not exist"
* 故视图/过程/函数/触发器源码改用 ALL_VIEWS.TEXTALL_SOURCE.TEXTALL_TRIGGERS.TRIGGER_BODY 读取</li>
* <li>{@code SET SCHEMA "x"} {@code ALTER SESSION SET CURRENT_SCHEMA} 均不被 parser 接受
* schema 切换改用 PG 风格的 {@code SET SEARCH_PATH TO "x"}已验证可用且不影响 ALL_* 视图的 OWNER 过滤</li>
* </ul>
*
* <p>建表/表结构编辑由 DBX 核心的 Oracle 方言 SQL 生成器负责神通实测支持 ALTER TABLE
* ADD/MODIFY/DROP/RENAME COLUMNDROP/ADD PRIMARY KEYCOMMENT ONCREATE/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"} 切换默认 schemaPG 风格
* 不支持 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 源码
*
* <p>神通不支持 DBMS_METADATA.GET_DDL按对象类型走对应系统视图
* <ul>
* <li>VIEW ALL_VIEWS.TEXT返回完整 SELECT 语句</li>
* <li>PROCEDURE/FUNCTION/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY ALL_SOURCE.TEXT LINE 排序拼接</li>
* <li>TRIGGER ALL_TRIGGERS.TRIGGER_BODY</li>
* <li>SEQUENCE ALL_SEQUENCES 元数据重建 CREATE SEQUENCE 语句</li>
* <li>MATERIALIZED_VIEW 神通无 ALL_MVIEWS返回空源码且不可编辑</li>
* </ul>
*/
@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.8function 也以 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;
}
}
}
}

View File

@ -315,7 +315,7 @@ const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>
starrocks: mysqlCapabilities,
goldendb: mysqlCapabilities,
sundb: mysqlCapabilities,
oscar: unsupportedCapabilities,
oscar: damengCapabilities,
databend: mysqlCapabilities,
gbase: gbaseCapabilities,
postgres: postgresCapabilities,

View File

@ -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,

View File

@ -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));

View File

@ -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<String> {
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, &current_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, &current_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, &current_name)));
}
statements
}
pub(super) fn build_sqlserver_existing_column_sql(
table: &str,
column: &EditableStructureColumn,

View File

@ -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::<u8>() 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()
}

View File

@ -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!(

View File

@ -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 => {

View File

@ -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 {

View File

@ -11,6 +11,9 @@ pub(super) enum StructureDialect {
SqlServer,
Oracle,
Dameng,
// 神通 OscarOracle 兼容方言,且支持 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<DatabaseType>) -> TableStru
alter_primary_key: true,
..base
},
// 神通 Oscar v7Oracle 兼容,实测支持 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<DatabaseType>) -> 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<DatabaseType>) -> 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<Dat
StructureDialect::SqlServer => 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),

View File

@ -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())

View File

@ -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::<String>::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::<String>::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::<String>::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::<String>::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::<String>::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::<String>::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::<String>::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::<String>::new());
assert_eq!(result.statements, vec!["COMMENT ON TABLE \"SYSDBA\".\"users\" IS 'new comment';"]);
}

View File

@ -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"
),