fix(schema-diff): correct Oracle/JDBC column, index and view diffs

This commit is contained in:
zipg authored and GitHub committed 2026-09-25 09:38:29 +08:00
1 parent ba422f96f8
commit 70569e556f
7 files changed
+1095 -95

No files matched your search

@@ -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();
});
});
+20 -1
View File
@@ -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",