1 parent
d5f4eadf7a
commit
1d71a5fc23
13 files changed
+473
-67
No files matched your search
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createConcurrencyLimiter, mapWithConcurrency, schemaDiffMetadataConcurrency, schemaDiffMetadataLoadPlan, shouldFetchSchemaDiffDdl } from "@/lib/schema/schemaDiffMetadataLoad";
|
||||
import { createConcurrencyLimiter, loadSchemaDetails, mapWithConcurrency, schemaDiffMetadataConcurrency, schemaDiffMetadataLoadPlan, shouldFetchSchemaDiffDdl, type SchemaDiffMetadataApi, type SchemaDiffMetadataProgress } from "../../schema/schemaDiffMetadataLoad";
|
||||
import { getSchemaDiffNextProgressStep, shouldLoadSchemaDiffExtraObjects } from "../../schema/schemaDiffProgress";
|
||||
import { DEFAULT_MYSQL_OPTIONS, DEFAULT_POSTGRES_OPTIONS } from "../../../types/schemaDiff";
|
||||
import type { TableInfo } from "../../../types/database";
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
@@ -100,6 +103,40 @@ describe("schemaDiffMetadataLoad", () => {
|
||||
expect(maxActive).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("reports each completed table while loading metadata", async () => {
|
||||
const tables = ["orders", "customers", "events"].map((name) => ({ name, table_type: "TABLE" }) as TableInfo);
|
||||
const progress: SchemaDiffMetadataProgress[] = [];
|
||||
const api: SchemaDiffMetadataApi = {
|
||||
getTableDdl: async (_connectionId, _database, _schema, table) => {
|
||||
await wait(table === "orders" ? 10 : table === "customers" ? 1 : 5);
|
||||
return `ddl:${table}`;
|
||||
},
|
||||
getColumns: async () => [],
|
||||
listIndexes: async () => [],
|
||||
listForeignKeys: async () => [],
|
||||
listTriggers: async () => [],
|
||||
};
|
||||
|
||||
const details = await loadSchemaDetails(
|
||||
tables,
|
||||
{
|
||||
connectionId: "source",
|
||||
database: "db",
|
||||
schema: "public",
|
||||
dbType: "mysql",
|
||||
options: { ...DEFAULT_MYSQL_OPTIONS },
|
||||
onProgress: (value) => progress.push(value),
|
||||
},
|
||||
api,
|
||||
);
|
||||
|
||||
expect(details.map((detail) => detail.name)).toEqual(["orders", "customers", "events"]);
|
||||
expect(details.map((detail) => detail.ddl)).toEqual(["ddl:orders", "ddl:customers", "ddl:events"]);
|
||||
expect(progress.map((value) => value.current)).toEqual([1, 2, 3]);
|
||||
expect(progress.map((value) => value.total)).toEqual([3, 3, 3]);
|
||||
expect(new Set(progress.map((value) => value.objectName))).toEqual(new Set(["orders", "customers", "events"]));
|
||||
});
|
||||
|
||||
it("propagates the first worker error and stops scheduling new work", async () => {
|
||||
const started: number[] = [];
|
||||
|
||||
@@ -134,4 +171,31 @@ describe("schemaDiffMetadataLoad", () => {
|
||||
expect(result).toEqual([0, 1, 2, 3]);
|
||||
expect(maxActive).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("only enables extra objects for PostgreSQL-like databases with relevant options", () => {
|
||||
expect(shouldLoadSchemaDiffExtraObjects("mysql", { ...DEFAULT_MYSQL_OPTIONS, functions: true })).toBe(false);
|
||||
expect(shouldLoadSchemaDiffExtraObjects("postgres", DEFAULT_POSTGRES_OPTIONS)).toBe(true);
|
||||
expect(shouldLoadSchemaDiffExtraObjects("opengauss", DEFAULT_POSTGRES_OPTIONS)).toBe(true);
|
||||
expect(
|
||||
shouldLoadSchemaDiffExtraObjects("postgres", {
|
||||
...DEFAULT_POSTGRES_OPTIONS,
|
||||
functions: false,
|
||||
sequences: false,
|
||||
rules: false,
|
||||
owners: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("maps phases to the next step on the actual comparison path", () => {
|
||||
expect(getSchemaDiffNextProgressStep("loading-table-lists", false)).toBe("nextSourceDetails");
|
||||
expect(getSchemaDiffNextProgressStep("loading-source-details", false)).toBe("nextTargetDetails");
|
||||
expect(getSchemaDiffNextProgressStep("loading-target-details", false)).toBe("nextComparing");
|
||||
expect(getSchemaDiffNextProgressStep("loading-target-details", true)).toBe("nextExtraObjects");
|
||||
expect(getSchemaDiffNextProgressStep("loading-extra-objects", true)).toBe("nextComparing");
|
||||
expect(getSchemaDiffNextProgressStep("comparing", false)).toBe("nextGenerating");
|
||||
expect(getSchemaDiffNextProgressStep("generating", false)).toBe("nextComplete");
|
||||
expect(getSchemaDiffNextProgressStep("complete", false)).toBeNull();
|
||||
expect(getSchemaDiffNextProgressStep(undefined, false)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ColumnInfo, ForeignKeyInfo, IndexInfo, ObjectSourceKind, TableInfo, TriggerInfo } from "@/types/database";
|
||||
import { isSchemaDiffView } from "@/lib/schema/schemaDiffTableFilter";
|
||||
import type { SchemaDiffCompareOptions } from "@/types/schemaDiff";
|
||||
import type { TableSchemaDetail } from "@/lib/schema/schemaDiff";
|
||||
|
||||
const MYSQL_LARGE_SCHEMA_DIFF_METADATA_CONCURRENCY = 2;
|
||||
const MYSQL_SMALL_SCHEMA_DIFF_METADATA_CONCURRENCY = 4;
|
||||
@@ -115,3 +118,60 @@ export function createConcurrencyLimiter(limit: number) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface SchemaDiffMetadataApi {
|
||||
getTableDdl(connectionId: string, database: string, schema: string, table: string, objectType?: ObjectSourceKind): Promise<string>;
|
||||
getColumns(connectionId: string, database: string, schema: string, table: string): Promise<ColumnInfo[]>;
|
||||
listIndexes(connectionId: string, database: string, schema: string, table: string): Promise<IndexInfo[]>;
|
||||
listForeignKeys(connectionId: string, database: string, schema: string, table: string): Promise<ForeignKeyInfo[]>;
|
||||
listTriggers(connectionId: string, database: string, schema: string, table: string): Promise<TriggerInfo[]>;
|
||||
}
|
||||
|
||||
export interface SchemaDiffMetadataProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
objectName: string;
|
||||
}
|
||||
|
||||
export interface SchemaDetailLoadContext {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
schema: string;
|
||||
dbType: string;
|
||||
options: SchemaDiffCompareOptions;
|
||||
onProgress?: (progress: SchemaDiffMetadataProgress) => void;
|
||||
}
|
||||
|
||||
function isViewOrMaterializedView(tableType: string): ObjectSourceKind | undefined {
|
||||
switch (tableType.toUpperCase().replace(/\s+/g, "_")) {
|
||||
case "VIEW":
|
||||
return "VIEW";
|
||||
case "MATERIALIZED_VIEW":
|
||||
return "MATERIALIZED_VIEW";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSchemaDetails(tables: TableInfo[], context: SchemaDetailLoadContext, api: SchemaDiffMetadataApi): Promise<TableSchemaDetail[]> {
|
||||
const concurrency = schemaDiffMetadataConcurrency(context.dbType, tables.length);
|
||||
const runMetadataQuery = createConcurrencyLimiter(concurrency);
|
||||
let completed = 0;
|
||||
|
||||
return mapWithConcurrency(tables, concurrency, async (table) => {
|
||||
const objectType = isViewOrMaterializedView(table.table_type);
|
||||
const loadPlan = schemaDiffMetadataLoadPlan(isSchemaDiffView(table), context.options);
|
||||
const ddlPromise = loadPlan.ddl ? runMetadataQuery(() => api.getTableDdl(context.connectionId, context.database, context.schema, table.name, objectType)) : Promise.resolve("");
|
||||
const [columns, indexes, foreignKeys, triggers, ddl] = await Promise.all([
|
||||
loadPlan.columns ? runMetadataQuery(() => api.getColumns(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]),
|
||||
loadPlan.indexes ? runMetadataQuery(() => api.listIndexes(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]),
|
||||
loadPlan.foreignKeys ? runMetadataQuery(() => api.listForeignKeys(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]),
|
||||
loadPlan.triggers ? runMetadataQuery(() => api.listTriggers(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]),
|
||||
ddlPromise,
|
||||
]);
|
||||
|
||||
const detail = { name: table.name, columns, indexes, foreignKeys, triggers, ddl };
|
||||
context.onProgress?.({ current: ++completed, total: tables.length, objectName: table.name });
|
||||
return detail;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { SchemaDiffCompareOptions } from "@/types/schemaDiff";
|
||||
|
||||
export type SchemaDiffProgressPhase = "loading-table-lists" | "loading-source-details" | "loading-target-details" | "loading-extra-objects" | "comparing" | "generating" | "complete";
|
||||
|
||||
export type SchemaDiffNextProgressStep = "nextSourceDetails" | "nextTargetDetails" | "nextExtraObjects" | "nextComparing" | "nextGenerating" | "nextComplete";
|
||||
|
||||
export function isSchemaDiffPostgresLike(dbType: string | null | undefined): boolean {
|
||||
const normalizedDbType = (dbType || "").toLowerCase();
|
||||
return normalizedDbType === "postgres" || normalizedDbType === "opengauss";
|
||||
}
|
||||
|
||||
export function shouldLoadSchemaDiffExtraObjects(dbType: string | null | undefined, options: Pick<SchemaDiffCompareOptions, "functions" | "sequences" | "rules" | "owners">): boolean {
|
||||
return isSchemaDiffPostgresLike(dbType) && (options.functions || options.sequences || options.rules || options.owners);
|
||||
}
|
||||
|
||||
export function getSchemaDiffNextProgressStep(phase: SchemaDiffProgressPhase | undefined, hasExtraObjectPhase: boolean): SchemaDiffNextProgressStep | null {
|
||||
switch (phase) {
|
||||
case "loading-table-lists":
|
||||
return "nextSourceDetails";
|
||||
case "loading-source-details":
|
||||
return "nextTargetDetails";
|
||||
case "loading-target-details":
|
||||
return hasExtraObjectPhase ? "nextExtraObjects" : "nextComparing";
|
||||
case "loading-extra-objects":
|
||||
return "nextComparing";
|
||||
case "comparing":
|
||||
return "nextGenerating";
|
||||
case "generating":
|
||||
return "nextComplete";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user