feat(oscar): enable table structure editing and view source for Shentong
This commit is contained in:
parent
dd5d74fa4f
commit
ae13ca9c1a
|
|
@ -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.TEXT、ALL_SOURCE.TEXT、ALL_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 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 源码。
|
||||
*
|
||||
* <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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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, ¤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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue