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

This commit is contained in:
t8y2 committed 2026-08-07 11:58:52 +08:00
1 parent dd5d74fa4f
commit ae13ca9c1a
13 files changed
+544 -17

No files matched your search

@@ -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,
@@ -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()
}
@@ -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!(
@@ -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 => {
@@ -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 {
@@ -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<DatabaseType>) -> 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<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),
@@ -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())
@@ -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';"]);
}
@@ -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"
),