fix(sqlserver): cap and de-duplicate the clone PK constraint name
This commit is contained in:
1 parent
933decc98b
commit
ac7fbaeb43
3 files changed
+114
-2
No files matched your search
+45
-1
@@ -23,6 +23,8 @@ import {
|
||||
damengDuplicateTableCreateOptions,
|
||||
duplicateTableStructureRequiresScript,
|
||||
oracleDuplicateTableCreateOptions,
|
||||
sqlServerClonePrimaryKeyConstraintName,
|
||||
SQLSERVER_IDENTIFIER_MAX_LENGTH,
|
||||
supportsNativeMysqlAutoIncrement,
|
||||
} from "@/lib/database/dbAdminSql";
|
||||
|
||||
@@ -442,10 +444,52 @@ describe("buildDuplicateTableStructurePlan", () => {
|
||||
});
|
||||
|
||||
expect(apiMock.listIndexes).toHaveBeenCalledWith("mssql-1", "app", "dbo", "orders", undefined);
|
||||
expect(apiMock.buildDuplicateTableStructureSql).toHaveBeenCalledWith(expect.objectContaining({ databaseType: "sqlserver", primaryKeyColumns: ["ID", "SEQ"] }));
|
||||
expect(apiMock.buildDuplicateTableStructureSql).toHaveBeenCalledWith(expect.objectContaining({ databaseType: "sqlserver", primaryKeyColumns: ["ID", "SEQ"], primaryKeyConstraintName: "PK_orders_copy" }));
|
||||
expect(plan.executeAsScript).toBe(true);
|
||||
});
|
||||
|
||||
it("renames the SQL Server clone PK when it would collide with a source index", async () => {
|
||||
apiMock.listIndexes.mockResolvedValue([
|
||||
{ name: "PK_ORDERS", columns: ["ID"], is_unique: true, is_primary: true },
|
||||
{ name: "PK_orders_copy", columns: ["FLAG"], is_unique: false, is_primary: false },
|
||||
]);
|
||||
apiMock.buildDuplicateTableStructureSql.mockResolvedValue("");
|
||||
|
||||
await buildDuplicateTableStructurePlan({
|
||||
connectionId: "mssql-1",
|
||||
database: "app",
|
||||
databaseType: "sqlserver",
|
||||
schema: "dbo",
|
||||
sourceName: "orders",
|
||||
targetName: "orders_copy",
|
||||
});
|
||||
|
||||
expect(apiMock.buildDuplicateTableStructureSql).toHaveBeenCalledWith(expect.objectContaining({ primaryKeyConstraintName: "PK_orders_copy_2" }));
|
||||
});
|
||||
|
||||
it("caps the SQL Server clone PK name at the 128-character identifier limit", () => {
|
||||
const indexes = [{ name: "IDX", columns: ["ID"], is_unique: false, is_primary: false }];
|
||||
const longTarget = `orders_${"x".repeat(200)}`;
|
||||
const name = sqlServerClonePrimaryKeyConstraintName(indexes, longTarget);
|
||||
|
||||
expect(name.length).toBeLessThanOrEqual(SQLSERVER_IDENTIFIER_MAX_LENGTH);
|
||||
expect(name.startsWith("PK_orders_")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the dedup suffix within the identifier limit for long targets", () => {
|
||||
const longTarget = `orders_${"x".repeat(200)}`;
|
||||
const colliding = `PK_${longTarget}`.slice(0, SQLSERVER_IDENTIFIER_MAX_LENGTH);
|
||||
const indexes = [
|
||||
{ name: "IDX", columns: ["ID"], is_unique: false, is_primary: false },
|
||||
{ name: colliding, columns: ["FLAG"], is_unique: false, is_primary: false },
|
||||
];
|
||||
|
||||
const name = sqlServerClonePrimaryKeyConstraintName(indexes as never, longTarget);
|
||||
|
||||
expect(name.length).toBeLessThanOrEqual(SQLSERVER_IDENTIFIER_MAX_LENGTH);
|
||||
expect(name.endsWith("_2")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps primary-key-free SQL Server clones on a single statement", async () => {
|
||||
apiMock.listIndexes.mockResolvedValue([{ name: "IDX_ORDERS_CUSTOMER", columns: ["CUSTOMER"], is_unique: false, is_primary: false }]);
|
||||
apiMock.buildDuplicateTableStructureSql.mockResolvedValue("SELECT TOP 0 * INTO [orders_copy] FROM [orders];");
|
||||
|
||||
@@ -86,6 +86,9 @@ export interface DuplicateTableStructureSqlOptions {
|
||||
/** SQL Server only: source primary-key columns recreated on the clone, because
|
||||
* `SELECT ... INTO` copies columns (and IDENTITY) but drops constraints. */
|
||||
primaryKeyColumns?: string[];
|
||||
/** SQL Server only: pre-computed PK constraint name for the clone (respecting
|
||||
* the 128-character identifier limit and source-table index names). */
|
||||
primaryKeyConstraintName?: string;
|
||||
/** Quote character reported by the connected server, for types whose quote is not fixed by the
|
||||
* database type alone (Cloud Spanner's two dialects differ). Mirrors `identifierQuote` on the
|
||||
* table-data SQL options. */
|
||||
@@ -215,6 +218,29 @@ export function damengDuplicateTableCreateOptions(options: { schema?: string | n
|
||||
};
|
||||
}
|
||||
|
||||
/** SQL Server caps identifiers at 128 characters. */
|
||||
export const SQLSERVER_IDENTIFIER_MAX_LENGTH = 128;
|
||||
|
||||
/**
|
||||
* Derives the clone's primary-key constraint name from `PK_{target}`, capped at
|
||||
* SQL Server's 128-character identifier limit and de-duplicated against the
|
||||
* source table's index names (schema-wide constraint names cannot be listed
|
||||
* through the table-level index metadata, so a name taken by a *different*
|
||||
* table still surfaces as a server-side error the user can act on).
|
||||
*/
|
||||
export function sqlServerClonePrimaryKeyConstraintName(indexes: IndexInfo[], targetName: string): string {
|
||||
const base = `PK_${targetName}`;
|
||||
const taken = new Set(indexes.map((index) => index.name.toLowerCase()));
|
||||
let name = base.slice(0, SQLSERVER_IDENTIFIER_MAX_LENGTH);
|
||||
let suffix = 2;
|
||||
while (taken.has(name.toLowerCase())) {
|
||||
const tail = `_${suffix}`;
|
||||
name = `${base.slice(0, SQLSERVER_IDENTIFIER_MAX_LENGTH - tail.length)}${tail}`;
|
||||
suffix += 1;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
export async function buildDuplicateTableStructurePlan(options: DuplicateTableStructurePlanOptions): Promise<DuplicateTableStructurePlan> {
|
||||
if (options.databaseType === "oracle") {
|
||||
const columnsPromise = options.sourceColumns ? Promise.resolve(options.sourceColumns) : api.getColumns(options.connectionId, options.database, options.schema || "", options.sourceName, options.catalog);
|
||||
@@ -273,6 +299,7 @@ export async function buildDuplicateTableStructurePlan(options: DuplicateTableSt
|
||||
if (options.databaseType === "sqlserver") {
|
||||
const indexes = await api.listIndexes(options.connectionId, options.database, options.schema || "", options.sourceName, options.catalog);
|
||||
const primaryKeyColumns = indexes.find((index) => index.is_primary && index.columns.length > 0)?.columns ?? [];
|
||||
const primaryKeyConstraintName = sqlServerClonePrimaryKeyConstraintName(indexes, options.targetName);
|
||||
const sql = await buildDuplicateTableStructureSql({
|
||||
databaseType: options.databaseType,
|
||||
schema: options.schema,
|
||||
@@ -281,6 +308,7 @@ export async function buildDuplicateTableStructurePlan(options: DuplicateTableSt
|
||||
tableComment: options.tableComment,
|
||||
columnComments: [],
|
||||
primaryKeyColumns,
|
||||
primaryKeyConstraintName,
|
||||
identifierQuote: options.identifierQuote,
|
||||
});
|
||||
return { sql, sourceColumns: options.sourceColumns, executeAsScript: primaryKeyColumns.length > 0 || duplicateTableStructureRequiresScript(sql) };
|
||||
|
||||
Reference in new issue
Block a user