285 lines
12 KiB
TypeScript
285 lines
12 KiB
TypeScript
import { reactive, shallowReactive } from "vue";
|
|
import { uuid } from "@/lib/common/utils";
|
|
import * as api from "@/lib/backend/api";
|
|
import { openSchemaDiffSession } from "@/composables/useDialogSources";
|
|
import { useExportTracker } from "@/composables/useExportTracker";
|
|
import { filterSchemaDiffTables } from "@/lib/schema/schemaDiffTableFilter";
|
|
import { compileSchemaDiffTableFilter } from "@/lib/schema/schemaDiffTableFilter";
|
|
import { loadSchemaDetails } from "@/lib/schema/schemaDiffMetadataLoad";
|
|
import type { SchemaDiffTableIdentity, SchemaDiffTableListLoader } from "@/lib/schema/schemaDiffTableList";
|
|
import { getSchemaDiffNextProgressStep, isSchemaDiffPostgresLike, shouldLoadSchemaDiffExtraObjects, type SchemaDiffProgressPhase } from "@/lib/schema/schemaDiffProgress";
|
|
import { convertToSchemaDiffObjects, databaseTypeToDialectKind, normalizeDialectKind, schemaDiffDeployTargetSchema, type SchemaDiffObject } from "@/lib/schema/schemaDiff";
|
|
import { normalizeSchemaDiffCompareOptions, type SchemaDiffCompareOptions } from "@/types/schemaDiff";
|
|
import type { DatabaseType } from "@/types/database";
|
|
import type { SchemaDiffPreparation } from "@/lib/schema/schemaDiff";
|
|
|
|
export interface SchemaDiffSessionProgress {
|
|
phase: SchemaDiffProgressPhase;
|
|
current?: number;
|
|
total?: number;
|
|
objectName?: string;
|
|
}
|
|
|
|
export interface SchemaDiffSessionConfig {
|
|
sourceConnectionId: string;
|
|
sourceDatabase: string;
|
|
sourceSchema: string;
|
|
targetConnectionId: string;
|
|
targetDatabase: string;
|
|
targetSchema: string;
|
|
sourceDbType: string;
|
|
targetDbType: DatabaseType;
|
|
options: SchemaDiffCompareOptions;
|
|
ignoreComments: boolean;
|
|
label: string;
|
|
}
|
|
|
|
export type SchemaDiffSessionStatus = "running" | "completed" | "failed";
|
|
|
|
export interface SchemaDiffSession {
|
|
id: string;
|
|
version: number;
|
|
status: SchemaDiffSessionStatus;
|
|
config: SchemaDiffSessionConfig;
|
|
progress: SchemaDiffSessionProgress | null;
|
|
result: SchemaDiffPreparation | null;
|
|
error: string | null;
|
|
startedAt: number;
|
|
finishedAt?: number;
|
|
}
|
|
|
|
export interface SchemaDiffSessionDependencies {
|
|
tableListLoader: SchemaDiffTableListLoader;
|
|
}
|
|
|
|
const sessions = reactive(new Map<string, SchemaDiffSession>());
|
|
|
|
function cloneOptions(options: SchemaDiffCompareOptions): SchemaDiffCompareOptions {
|
|
return {
|
|
...options,
|
|
selectedTables: options.selectedTables ? [...options.selectedTables] : options.selectedTables,
|
|
tableMappings: options.tableMappings?.map((mapping) => ({ ...mapping })),
|
|
fieldMappings: options.fieldMappings?.map((mapping) => ({ ...mapping })),
|
|
};
|
|
}
|
|
|
|
function resultObjectCount(result: SchemaDiffPreparation): number {
|
|
return result.diffs.length + (result.functionDiffs?.length ?? 0) + (result.sequenceDiffs?.length ?? 0) + (result.ruleDiffs?.length ?? 0) + (result.ownerDiffs?.length ?? 0);
|
|
}
|
|
|
|
function sessionError(error: unknown): string {
|
|
return error instanceof Error ? error.message : (error as { message?: string } | null)?.message || String(error);
|
|
}
|
|
|
|
function publishProgress(session: SchemaDiffSession, progress: SchemaDiffSessionProgress): void {
|
|
session.version += 1;
|
|
session.progress = progress;
|
|
useExportTracker().updateCompareTask(session.id, {
|
|
status: "Running",
|
|
comparePhase: progress.phase,
|
|
compareCurrent: progress.current,
|
|
compareTotal: progress.total,
|
|
compareCurrentObject: progress.objectName,
|
|
});
|
|
}
|
|
|
|
async function runSchemaDiffSession(session: SchemaDiffSession, dependencies: SchemaDiffSessionDependencies): Promise<void> {
|
|
const input = session.config;
|
|
const tracker = useExportTracker();
|
|
const options = normalizeSchemaDiffCompareOptions(input.options, input.targetDbType);
|
|
const isPostgresLike = isSchemaDiffPostgresLike(input.targetDbType);
|
|
const hasExtraObjectPhase = shouldLoadSchemaDiffExtraObjects(input.targetDbType, options);
|
|
const tableFilter = compileSchemaDiffTableFilter(options);
|
|
|
|
try {
|
|
publishProgress(session, { phase: "loading-table-lists" });
|
|
|
|
const sourceTableIdentity: SchemaDiffTableIdentity = {
|
|
connectionId: input.sourceConnectionId,
|
|
database: input.sourceDatabase,
|
|
schema: input.sourceSchema,
|
|
};
|
|
const targetTableIdentity: SchemaDiffTableIdentity = {
|
|
connectionId: input.targetConnectionId,
|
|
database: input.targetDatabase,
|
|
schema: input.targetSchema,
|
|
};
|
|
const [sourceTableList, targetTableList] = await Promise.all([dependencies.tableListLoader.load(sourceTableIdentity, { refresh: true }), dependencies.tableListLoader.load(targetTableIdentity, { refresh: true })]);
|
|
const { sourceTables, targetTables } = filterSchemaDiffTables(sourceTableList, targetTableList, tableFilter, options, options.selectedTables);
|
|
|
|
publishProgress(session, { phase: "loading-source-details", current: 0, total: sourceTables.length });
|
|
const sourceDetails = await loadSchemaDetails(
|
|
sourceTables,
|
|
{
|
|
connectionId: input.sourceConnectionId,
|
|
database: input.sourceDatabase,
|
|
schema: input.sourceSchema,
|
|
dbType: input.sourceDbType,
|
|
options,
|
|
onProgress: (progress) => publishProgress(session, { phase: "loading-source-details", ...progress }),
|
|
},
|
|
api,
|
|
);
|
|
|
|
publishProgress(session, { phase: "loading-target-details", current: 0, total: targetTables.length });
|
|
const targetDetails = await loadSchemaDetails(
|
|
targetTables,
|
|
{
|
|
connectionId: input.targetConnectionId,
|
|
database: input.targetDatabase,
|
|
schema: input.targetSchema,
|
|
dbType: input.targetDbType,
|
|
options,
|
|
onProgress: (progress) => publishProgress(session, { phase: "loading-target-details", ...progress }),
|
|
},
|
|
api,
|
|
);
|
|
|
|
const promises: Promise<unknown>[] = [];
|
|
if (isPostgresLike && options.functions) {
|
|
promises.push(api.listFunctions(input.sourceConnectionId, input.sourceDatabase, input.sourceSchema));
|
|
promises.push(api.listFunctions(input.targetConnectionId, input.targetDatabase, input.targetSchema));
|
|
}
|
|
if (isPostgresLike && options.sequences) {
|
|
promises.push(api.listSequences(input.sourceConnectionId, input.sourceDatabase, input.sourceSchema, !!options.sequenceLastValues));
|
|
promises.push(api.listSequences(input.targetConnectionId, input.targetDatabase, input.targetSchema, !!options.sequenceLastValues));
|
|
}
|
|
if (isPostgresLike && options.rules) {
|
|
promises.push(api.listRules(input.sourceConnectionId, input.sourceDatabase, input.sourceSchema));
|
|
promises.push(api.listRules(input.targetConnectionId, input.targetDatabase, input.targetSchema));
|
|
}
|
|
if (isPostgresLike && options.owners) {
|
|
promises.push(api.listOwners(input.sourceConnectionId, input.sourceDatabase, input.sourceSchema));
|
|
promises.push(api.listOwners(input.targetConnectionId, input.targetDatabase, input.targetSchema));
|
|
}
|
|
|
|
if (hasExtraObjectPhase) publishProgress(session, { phase: "loading-extra-objects" });
|
|
const extraObjects = await Promise.all(promises);
|
|
let index = 0;
|
|
const sourceFunctions = options.functions && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listFunctions>>) : [];
|
|
const targetFunctions = options.functions && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listFunctions>>) : [];
|
|
const sourceSequences = options.sequences && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listSequences>>) : [];
|
|
const targetSequences = options.sequences && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listSequences>>) : [];
|
|
const sourceRules = options.rules && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listRules>>) : [];
|
|
const targetRules = options.rules && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listRules>>) : [];
|
|
const sourceOwners = options.owners && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listOwners>>) : [];
|
|
const targetOwners = options.owners && isPostgresLike ? (extraObjects[index++] as Awaited<ReturnType<typeof api.listOwners>>) : [];
|
|
|
|
publishProgress(session, { phase: "comparing" });
|
|
const result = await api.prepareSchemaDiff({
|
|
sourceTables,
|
|
targetTables,
|
|
sourceDetails,
|
|
targetDetails,
|
|
sourceFunctions,
|
|
targetFunctions,
|
|
sourceSequences,
|
|
targetSequences,
|
|
sourceRules,
|
|
targetRules,
|
|
sourceOwners,
|
|
targetOwners,
|
|
tableMappings: options.selectedTables === undefined ? undefined : options.tableMappings,
|
|
databaseType: input.targetDbType,
|
|
targetSchema: schemaDiffDeployTargetSchema(input.targetDbType, input.targetDatabase, input.targetSchema),
|
|
ignoreComments: input.ignoreComments,
|
|
cascadeDelete: options.cascadeDelete ?? false,
|
|
compareColumnOrder: options.compareColumnOrder,
|
|
ignoreTableNameCase: options.ignoreTableNameCase,
|
|
ignoreColumnNameCase: options.ignoreColumnNameCase,
|
|
detectRenames: options.detectRenames ?? false,
|
|
detectTableRenames: options.detectTableRenames ?? false,
|
|
renameThreshold: options.renameThreshold ?? 0.5,
|
|
enableRollback: options.enableRollback ?? false,
|
|
batchPatterns: options.batchPatterns
|
|
? options.batchPatterns
|
|
.split(",")
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
: undefined,
|
|
sourceDialect: options.sourceDialect ? normalizeDialectKind(options.sourceDialect) : input.sourceDbType ? databaseTypeToDialectKind(input.sourceDbType as DatabaseType) : undefined,
|
|
targetDialect: options.targetDialect ? normalizeDialectKind(options.targetDialect) : input.targetDbType ? databaseTypeToDialectKind(input.targetDbType) : undefined,
|
|
compatibilityThreshold: options.compatibilityThreshold ?? 0.5,
|
|
fieldMappings:
|
|
options.fieldMappings?.map((mapping) => ({
|
|
sourceType: mapping.sourceType,
|
|
targetType: mapping.targetType,
|
|
paramStrategy: mapping.paramStrategy ?? "preserve",
|
|
customParams: mapping.customParams,
|
|
})) || [],
|
|
});
|
|
|
|
publishProgress(session, { phase: "generating" });
|
|
session.result = result;
|
|
session.progress = { phase: "complete" };
|
|
session.version += 1;
|
|
session.status = "completed";
|
|
session.finishedAt = Date.now();
|
|
tracker.updateCompareTask(session.id, {
|
|
status: "Done",
|
|
comparePhase: "complete",
|
|
compareCurrent: 1,
|
|
compareTotal: 1,
|
|
compareResultCount: resultObjectCount(result),
|
|
});
|
|
} catch (error: unknown) {
|
|
const message = sessionError(error);
|
|
session.error = message;
|
|
session.progress = null;
|
|
session.version += 1;
|
|
session.status = "failed";
|
|
session.finishedAt = Date.now();
|
|
tracker.updateCompareTask(session.id, {
|
|
status: "Error",
|
|
errorMessage: message,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function startSchemaDiffSession(input: Omit<SchemaDiffSessionConfig, "options"> & { options: SchemaDiffCompareOptions }, dependencies: SchemaDiffSessionDependencies): SchemaDiffSession {
|
|
const id = uuid();
|
|
const session = shallowReactive<SchemaDiffSession>({
|
|
id,
|
|
version: 0,
|
|
status: "running",
|
|
config: {
|
|
...input,
|
|
options: cloneOptions(input.options),
|
|
},
|
|
progress: null,
|
|
result: null,
|
|
error: null,
|
|
startedAt: Date.now(),
|
|
});
|
|
sessions.set(id, session);
|
|
|
|
const tracker = useExportTracker();
|
|
tracker.addSchemaDiffTask(
|
|
id,
|
|
input.label,
|
|
() => openSchemaDiffSession(id),
|
|
() => removeSchemaDiffSession(id),
|
|
);
|
|
void runSchemaDiffSession(session, dependencies);
|
|
return session;
|
|
}
|
|
|
|
export function getSchemaDiffSession(id: string | null | undefined): SchemaDiffSession | undefined {
|
|
return id ? sessions.get(id) : undefined;
|
|
}
|
|
|
|
export function removeSchemaDiffSession(id: string): boolean {
|
|
const session = sessions.get(id);
|
|
if (session?.status === "running") return false;
|
|
return sessions.delete(id);
|
|
}
|
|
|
|
export function schemaDiffSessionNextProgressStep(session: SchemaDiffSession): string | null {
|
|
return getSchemaDiffNextProgressStep(session.progress?.phase, shouldLoadSchemaDiffExtraObjects(session.config.targetDbType, session.config.options));
|
|
}
|
|
|
|
export function schemaDiffSessionObjects(result: SchemaDiffPreparation | null): SchemaDiffObject[] {
|
|
if (!result) return [];
|
|
return convertToSchemaDiffObjects(result.diffs, result.functionDiffs, result.sequenceDiffs, result.ruleDiffs, result.ownerDiffs, result.renameCandidates);
|
|
}
|