mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:30:24 +00:00
fix(schema-diff): correct Oracle/JDBC column, index and view diffs
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
schemaDiffDeployTargetSchema,
|
||||
normalizeDialectKind,
|
||||
databaseTypeToDialectKind,
|
||||
schemaDiffEngineDatabaseType,
|
||||
findSchemaDiffObject,
|
||||
flattenSchemaDiffObjects,
|
||||
schemaDiffSelectionTargets,
|
||||
@@ -107,6 +108,10 @@ const showFieldMappingDialog = ref(false);
|
||||
const showConfigSelector = ref(false);
|
||||
const sourceDbType = computed(() => store.getConfig(sourceConnectionId.value)?.db_type ?? "");
|
||||
const targetDbType = computed(() => store.getConfig(targetConnectionId.value)?.db_type ?? "");
|
||||
// A JDBC connection stores `jdbc` in db_type while its product lives in the driver
|
||||
// profile; the diff engine needs the product for clause shapes and view comparison.
|
||||
const sourceEngineDbType = computed(() => schemaDiffEngineDatabaseType(store.getConfig(sourceConnectionId.value)));
|
||||
const targetEngineDbType = computed(() => schemaDiffEngineDatabaseType(store.getConfig(targetConnectionId.value)));
|
||||
|
||||
// Clear stale field mappings when source and target are the same type
|
||||
watch([sourceDbType, targetDbType], ([src, tgt]) => {
|
||||
@@ -742,6 +747,8 @@ function handleCompare(): void {
|
||||
targetSchema: targetSchema.value,
|
||||
sourceDbType,
|
||||
targetDbType,
|
||||
sourceEngineDbType: sourceEngineDbType.value,
|
||||
targetEngineDbType: targetEngineDbType.value,
|
||||
options,
|
||||
ignoreComments: ignoreComments.value,
|
||||
label: `${comparisonEndpointLabel(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value)} → ${comparisonEndpointLabel(targetConnectionId.value, targetDatabase.value, targetSchema.value)}`,
|
||||
@@ -802,11 +809,12 @@ function rebuildDiffGroups() {
|
||||
}
|
||||
|
||||
function buildSchemaSyncPlanOptions(options: SchemaDiffCompareOptions) {
|
||||
const engineDbType = targetEngineDbType.value ?? getDbType();
|
||||
return {
|
||||
databaseType: getDbType(),
|
||||
databaseType: engineDbType,
|
||||
targetSchema: schemaDiffDeployTargetSchema(getDbType(), targetDatabase.value, targetSchema.value),
|
||||
cascadeDelete: options.cascadeDelete,
|
||||
sourceDialect: options.sourceDialect ? normalizeDialectKind(options.sourceDialect) : sourceDbType.value ? databaseTypeToDialectKind(sourceDbType.value) : undefined,
|
||||
sourceDialect: options.sourceDialect ? normalizeDialectKind(options.sourceDialect) : sourceEngineDbType.value ? databaseTypeToDialectKind(sourceEngineDbType.value) : undefined,
|
||||
fieldMappings: options.fieldMappings,
|
||||
enableRollback: options.enableRollback,
|
||||
};
|
||||
|
||||
@@ -322,3 +322,47 @@ test("skips table list loading for routines-only compares", async () => {
|
||||
assert.equal(apiMock.listFunctions.mock.calls.length, 2);
|
||||
assert.deepEqual(apiMock.prepareSchemaDiff.mock.calls[0]?.[0]?.sourceTables, []);
|
||||
});
|
||||
|
||||
test("resolves the JDBC engine dialect from the connection's product type", async () => {
|
||||
// 「Oracle (JDBC)」这类连接的 db_type 是 jdbc,后端 DialectKind 认不出它:对话框把产品类型
|
||||
// 解析出来传进 sourceEngineDbType/targetEngineDbType 后,方言(以及视图比较)才成立。
|
||||
apiMock.prepareSchemaDiff.mockClear();
|
||||
apiMock.prepareSchemaDiff.mockResolvedValue({
|
||||
diffs: [],
|
||||
functionDiffs: [],
|
||||
sequenceDiffs: [],
|
||||
ruleDiffs: [],
|
||||
ownerDiffs: [],
|
||||
renameCandidates: [],
|
||||
syncSql: "",
|
||||
rollbackSyncSql: "",
|
||||
});
|
||||
|
||||
const session = startSchemaDiffSession(
|
||||
{
|
||||
sourceConnectionId: "jdbc-src",
|
||||
sourceDatabase: "XE",
|
||||
sourceSchema: "DBX_TEST",
|
||||
targetConnectionId: "jdbc-dst",
|
||||
targetDatabase: "XE",
|
||||
targetSchema: "DBX_TGT",
|
||||
sourceDbType: "jdbc",
|
||||
targetDbType: "jdbc",
|
||||
sourceEngineDbType: "oracle",
|
||||
targetEngineDbType: "oracle",
|
||||
options: {},
|
||||
ignoreComments: false,
|
||||
label: "jdbc oracle",
|
||||
},
|
||||
{ tableListLoader: { load: vi.fn().mockResolvedValue([]) } },
|
||||
);
|
||||
|
||||
await waitForSession(session);
|
||||
|
||||
assert.equal(session.status, "completed");
|
||||
const payload = apiMock.prepareSchemaDiff.mock.calls[0]?.[0];
|
||||
assert.equal(payload?.sourceDialect, "oracle");
|
||||
assert.equal(payload?.targetDialect, "oracle");
|
||||
// 部署脚本按目标产品类型(而不是 jdbc)生成。
|
||||
assert.equal(payload?.databaseType, "oracle");
|
||||
});
|
||||
|
||||
@@ -31,6 +31,13 @@ export interface SchemaDiffSessionConfig {
|
||||
targetSchema: string;
|
||||
sourceDbType: string;
|
||||
targetDbType: DatabaseType;
|
||||
/**
|
||||
* Product type behind a JDBC `sourceDbType`/`targetDbType` (see
|
||||
* `schemaDiffEngineDatabaseType`). Only the diff engine's dialect/profile decisions use
|
||||
* it: metadata loading, progress phases and option defaults keep the raw db types.
|
||||
*/
|
||||
sourceEngineDbType?: DatabaseType;
|
||||
targetEngineDbType?: DatabaseType;
|
||||
options: SchemaDiffCompareOptions;
|
||||
ignoreComments: boolean;
|
||||
label: string;
|
||||
@@ -56,6 +63,11 @@ export interface SchemaDiffSessionDependencies {
|
||||
|
||||
const sessions = reactive(new Map<string, SchemaDiffSession>());
|
||||
|
||||
function schemaDiffDialect(dbType: DatabaseType | string | undefined, explicit: string | undefined): string | undefined {
|
||||
if (explicit) return normalizeDialectKind(explicit);
|
||||
return dbType ? databaseTypeToDialectKind(dbType as DatabaseType) : undefined;
|
||||
}
|
||||
|
||||
function cloneOptions(options: SchemaDiffCompareOptions): SchemaDiffCompareOptions {
|
||||
return {
|
||||
...options,
|
||||
@@ -209,7 +221,7 @@ async function runSchemaDiffSession(session: SchemaDiffSession, dependencies: Sc
|
||||
sourceOwners,
|
||||
targetOwners,
|
||||
tableMappings: sessionOptions.selectedTables === undefined ? undefined : sessionOptions.tableMappings,
|
||||
databaseType: input.targetDbType,
|
||||
databaseType: input.targetEngineDbType ?? input.targetDbType,
|
||||
targetSchema: schemaDiffDeployTargetSchema(input.targetDbType, input.targetDatabase, input.targetSchema),
|
||||
ignoreComments: input.ignoreComments,
|
||||
cascadeDelete: sessionOptions.cascadeDelete ?? false,
|
||||
@@ -226,8 +238,8 @@ async function runSchemaDiffSession(session: SchemaDiffSession, dependencies: Sc
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
sourceDialect: sessionOptions.sourceDialect ? normalizeDialectKind(sessionOptions.sourceDialect) : input.sourceDbType ? databaseTypeToDialectKind(input.sourceDbType as DatabaseType) : undefined,
|
||||
targetDialect: sessionOptions.targetDialect ? normalizeDialectKind(sessionOptions.targetDialect) : input.targetDbType ? databaseTypeToDialectKind(input.targetDbType) : undefined,
|
||||
sourceDialect: schemaDiffDialect(input.sourceEngineDbType ?? input.sourceDbType, sessionOptions.sourceDialect),
|
||||
targetDialect: schemaDiffDialect(input.targetEngineDbType ?? input.targetDbType, sessionOptions.targetDialect),
|
||||
compatibilityThreshold: sessionOptions.compatibilityThreshold ?? 0.5,
|
||||
fieldMappings:
|
||||
sessionOptions.fieldMappings?.map((mapping) => ({
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { schemaDiffEngineDatabaseType } from "@/lib/schema/schemaDiff";
|
||||
|
||||
describe("schemaDiffEngineDatabaseType", () => {
|
||||
it("resolves the product type of a JDBC connection from the driver profile", () => {
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "jdbc", driver_profile: "jdbc-oracle" })).toBe("oracle");
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "jdbc", driver_profile: "jdbc-mysql" })).toBe("mysql");
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "jdbc", driver_profile: "jdbc-postgresql" })).toBe("postgres");
|
||||
});
|
||||
|
||||
it("resolves the product type from the JDBC URL, driver class and labels", () => {
|
||||
expect(
|
||||
schemaDiffEngineDatabaseType({
|
||||
db_type: "jdbc",
|
||||
connection_string: "jdbc:oracle:thin:@192.168.58.103:15211:XE",
|
||||
jdbc_driver_class: "oracle.jdbc.OracleDriver",
|
||||
}),
|
||||
).toBe("oracle");
|
||||
expect(
|
||||
schemaDiffEngineDatabaseType({
|
||||
db_type: "jdbc",
|
||||
jdbc_driver_paths: ["/plugins/jdbc/drivers/local/ojdbc8-21.9.0.0.jar"],
|
||||
jdbc_driver_class: "com.mysql.cj.jdbc.Driver",
|
||||
connection_string: "jdbc:mysql://127.0.0.1:3306/db",
|
||||
}),
|
||||
).toBe("mysql");
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "jdbc", driver_label: "Apache Hive" })).toBe("hive");
|
||||
});
|
||||
|
||||
it("keeps generic JDBC connections on the generic type", () => {
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "jdbc" })).toBe("jdbc");
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "jdbc", driver_profile: "jdbcx" })).toBe("jdbc");
|
||||
});
|
||||
|
||||
it("leaves non-JDBC connections untouched", () => {
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "oracle" })).toBe("oracle");
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "mysql", driver_profile: "starrocks" })).toBe("mysql");
|
||||
expect(schemaDiffEngineDatabaseType({ db_type: "postgres" })).toBe("postgres");
|
||||
expect(schemaDiffEngineDatabaseType(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ColumnInfo, IndexInfo, ForeignKeyInfo, TriggerInfo, FunctionInfo, SequenceInfo, RuleInfo, OwnerInfo, DatabaseType, TableInfo } from "@/types/database";
|
||||
import type { ColumnInfo, IndexInfo, ForeignKeyInfo, TriggerInfo, FunctionInfo, SequenceInfo, RuleInfo, OwnerInfo, DatabaseType, TableInfo, ConnectionConfig } from "@/types/database";
|
||||
import type { SchemaDiffTableMapping } from "@/types/schemaDiff";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
const DIALECT_KIND_MAP: Record<string, string> = {
|
||||
@@ -44,6 +45,24 @@ export function databaseTypeToDialectKind(dbType: DatabaseType): string {
|
||||
return DIALECT_KIND_MAP[dbType] ?? "unsupported";
|
||||
}
|
||||
|
||||
/** The connection fields the JDBC product inference reads. */
|
||||
export type SchemaDiffDialectConnection = Partial<Pick<ConnectionConfig, "db_type" | "driver_profile" | "driver_label" | "connection_string" | "url_params" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config" | "username">>;
|
||||
|
||||
/**
|
||||
* Product type the schema-diff engine must treat a connection as.
|
||||
*
|
||||
* A JDBC connection stores the generic `jdbc` in `db_type`, but the engine keys its clause
|
||||
* shapes, index rules and view comparison off the product type — diffing a JDBC Oracle
|
||||
* connection as `jdbc` emits ANSI/MySQL clauses for it and never compares its view text.
|
||||
* The product is resolved from the driver profile/URL exactly like the metadata and query
|
||||
* paths already resolve it; every other connection keeps its own `db_type`.
|
||||
*/
|
||||
export function schemaDiffEngineDatabaseType(connection: SchemaDiffDialectConnection | undefined): DatabaseType | undefined {
|
||||
if (!connection?.db_type) return undefined;
|
||||
if (connection.db_type !== "jdbc") return connection.db_type as DatabaseType;
|
||||
return effectiveDatabaseTypeForConnection(connection) ?? (connection.db_type as DatabaseType);
|
||||
}
|
||||
|
||||
const DIALECT_ALIAS_MAP: Record<string, string> = {
|
||||
access: "sql_server",
|
||||
mssql: "sql_server",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -142,6 +142,17 @@ pub struct DdlDialectProfile {
|
||||
pub column_ddl: ColumnDdlSyntax,
|
||||
/// MySQL `MODIFY COLUMN` vs ANSI `ALTER COLUMN … TYPE/SET`.
|
||||
pub alter_uses_modify_column: bool,
|
||||
/// Spell `ADD COLUMN` (MySQL/Postgres/...) instead of the Oracle-family bare `ADD (...)`.
|
||||
pub add_column_uses_column_keyword: bool,
|
||||
/// Spell `MODIFY COLUMN` (MySQL) instead of the bare `MODIFY (...)` Oracle-family and
|
||||
/// Dameng both use.
|
||||
pub modify_column_uses_column_keyword: bool,
|
||||
/// Wrap the definition in parentheses: Oracle's `ADD (...)` / `MODIFY (...)` clauses.
|
||||
pub parenthesized_alter_column_clause: bool,
|
||||
/// Write `DEFAULT` before `NOT NULL`. Oracle's grammar is
|
||||
/// `datatype [DEFAULT expr] [NOT NULL]` and it rejects the MySQL order outright
|
||||
/// (ORA-00907), so the tail of a column definition is dialect data, not cosmetics.
|
||||
pub column_default_precedes_not_null: bool,
|
||||
/// Batch multiple alter clauses in one `ALTER TABLE` statement.
|
||||
pub alter_batches_clauses: bool,
|
||||
/// Prefer emitting source SHOW CREATE / native DDL when dialects match (MySQL-family).
|
||||
@@ -210,10 +221,33 @@ impl DdlDialectProfile {
|
||||
}
|
||||
|
||||
pub fn alter_modify_keyword(&self) -> &'static str {
|
||||
if self.database_type == DatabaseType::Dameng {
|
||||
"MODIFY"
|
||||
} else {
|
||||
if self.modify_column_uses_column_keyword {
|
||||
"MODIFY COLUMN"
|
||||
} else {
|
||||
"MODIFY"
|
||||
}
|
||||
}
|
||||
|
||||
/// Clause that changes one column's type without restating the whole definition.
|
||||
///
|
||||
/// `ALTER COLUMN x TYPE …` everywhere except the Oracle grammar, where `TYPE` does not
|
||||
/// exist and the change has to be spelled `MODIFY (x TYPE)`.
|
||||
pub fn alter_column_type_clause(&self, name: &str, data_type: &str) -> String {
|
||||
if self.parenthesized_alter_column_clause {
|
||||
format!(" {} ({name} {data_type})", self.alter_modify_keyword())
|
||||
} else {
|
||||
format!(" ALTER COLUMN {name} TYPE {data_type}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Clause that flips one column's nullability, in the dialect's spelling.
|
||||
pub fn alter_column_nullability_clause(&self, name: &str, nullable: bool) -> String {
|
||||
if self.parenthesized_alter_column_clause {
|
||||
format!(" {} ({name} {})", self.alter_modify_keyword(), if nullable { "NULL" } else { "NOT NULL" })
|
||||
} else if nullable {
|
||||
format!(" ALTER COLUMN {name} DROP NOT NULL")
|
||||
} else {
|
||||
format!(" ALTER COLUMN {name} SET NOT NULL")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +395,10 @@ fn mysql_family(db: DatabaseType) -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: true,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: true,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: true,
|
||||
create_table_if_not_exists: true,
|
||||
create_index_if_not_exists: false,
|
||||
@@ -410,6 +448,10 @@ fn postgres_family(db: DatabaseType) -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: false,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: false,
|
||||
create_index_if_not_exists: true,
|
||||
@@ -463,6 +505,16 @@ fn oracle_family(db: DatabaseType) -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: false,
|
||||
// Family defaults keep the spellings the rest of the family had before
|
||||
// (`ADD COLUMN`, bare `MODIFY`); `profile_for` turns on the Oracle-specific
|
||||
// clause shapes for the engines whose grammar was verified on a real instance.
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: false,
|
||||
// `profile_for` turns both of these on for the engines whose Oracle grammar is
|
||||
// exercised by tests and real instances (`Oracle`, `OceanbaseOracle`); the rest
|
||||
// of the family keeps the spellings they had before.
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: false,
|
||||
create_index_if_not_exists: false,
|
||||
@@ -514,6 +566,10 @@ fn sqlserver_family(db: DatabaseType) -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: false,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: false,
|
||||
create_index_if_not_exists: false,
|
||||
@@ -563,6 +619,10 @@ fn sqlite_family(db: DatabaseType) -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: true,
|
||||
alter_uses_modify_column: false,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: true,
|
||||
create_index_if_not_exists: true,
|
||||
@@ -613,6 +673,10 @@ fn access_profile() -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: false,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: false,
|
||||
create_index_if_not_exists: false,
|
||||
@@ -664,6 +728,10 @@ fn h2_profile() -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: false,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: false,
|
||||
create_index_if_not_exists: false,
|
||||
@@ -720,6 +788,10 @@ fn conservative_ansi(db: DatabaseType) -> DdlDialectProfile {
|
||||
grant_uses_mysql_user_syntax: false,
|
||||
warn_fk_needs_table_rebuild: false,
|
||||
alter_uses_modify_column: false,
|
||||
add_column_uses_column_keyword: true,
|
||||
modify_column_uses_column_keyword: true,
|
||||
parenthesized_alter_column_clause: false,
|
||||
column_default_precedes_not_null: false,
|
||||
alter_batches_clauses: false,
|
||||
create_table_if_not_exists: false,
|
||||
create_index_if_not_exists: false,
|
||||
@@ -788,8 +860,19 @@ pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile {
|
||||
profile
|
||||
}
|
||||
|
||||
// Oracle family
|
||||
Oracle | OceanbaseOracle | Yashandb | Xugu | Iris => oracle_family(db_type),
|
||||
// Oracle family. Oracle itself (and OceanBase's Oracle-compatibility mode) needs
|
||||
// the Oracle clause spelling everywhere: `ADD (…)` / `MODIFY (….)`, and `DEFAULT`
|
||||
// ahead of `NOT NULL`. The remaining family members keep the previous spellings
|
||||
// until their own grammar is verified against a real instance.
|
||||
Oracle | OceanbaseOracle => {
|
||||
let mut profile = oracle_family(db_type);
|
||||
profile.add_column_uses_column_keyword = false;
|
||||
profile.parenthesized_alter_column_clause = true;
|
||||
profile.column_default_precedes_not_null = true;
|
||||
profile
|
||||
}
|
||||
|
||||
Yashandb | Xugu | Iris => oracle_family(db_type),
|
||||
|
||||
// SQL Server (not Access)
|
||||
SqlServer => sqlserver_family(db_type),
|
||||
|
||||
Reference in New Issue
Block a user