feat(oracle): support adding primary keys to existing tables

Closes #6922
This commit is contained in:
t8y2 committed 2026-08-22 12:24:02 +00:00
1 parent 946677a579
commit a1e94d8228
8 files changed
+252 -23

No files matched your search

@@ -263,7 +263,7 @@ function draft(isPrimaryKey = false, identity?: { seed: number; increment: numbe
};
}
async function mountEditor(databaseType: "sqlserver" | "postgres" | "sqlite" | "oracle" | "dameng" | "duckdb" | "informix", isPrimaryKey = false, options: { database?: string; dynamicTypes?: string[]; identity?: { seed: number; increment: number } } = {}) {
async function mountEditor(databaseType: "sqlserver" | "postgres" | "sqlite" | "oracle" | "oceanbase-oracle" | "iris" | "dameng" | "duckdb" | "informix", isPrimaryKey = false, options: { database?: string; dynamicTypes?: string[]; identity?: { seed: number; increment: number } } = {}) {
mocks.connection.db_type = databaseType;
mocks.connection.name = databaseType;
mocks.connection.driver_label = databaseType;
@@ -315,10 +315,10 @@ async function mountLoadingEditor(initialTab: "columns" | "indexes" | "foreignKe
return root;
}
function columnCheckbox(root: HTMLElement, header: string): HTMLInputElement {
function columnCheckbox(root: HTMLElement, header: string, rowIndex = 0): HTMLInputElement {
const headerIndex = Array.from(root.querySelectorAll("thead th")).findIndex((cell) => cell.textContent?.trim() === header);
if (headerIndex < 0) throw new Error(`Missing ${header} column`);
const row = root.querySelector<HTMLElement>('[data-column-row-index="0"]');
const row = root.querySelector<HTMLElement>(`[data-column-row-index="${rowIndex}"]`);
const cell = row?.querySelectorAll("td")[headerIndex];
const checkbox = cell?.querySelector<HTMLInputElement>('input[type="checkbox"]');
if (!checkbox) throw new Error(`Missing ${header} checkbox`);
@@ -428,8 +428,67 @@ describe("TableStructureEditor primary key editing", () => {
);
});
it("keeps the primary-key checkbox disabled for an existing Oracle column", async () => {
it("enables a primary key on an existing keyless Oracle column and makes it not null", async () => {
const root = await mountEditor("oracle");
const primaryKey = columnCheckbox(root, "structureEditor.primaryKey");
const nullable = columnCheckbox(root, "structureEditor.nullable");
expect(primaryKey.disabled).toBe(false);
expect(nullable.checked).toBe(true);
primaryKey.checked = true;
primaryKey.dispatchEvent(new Event("change", { bubbles: true }));
await nextTick();
expect(primaryKey.checked).toBe(true);
expect(nullable.checked).toBe(false);
await vi.waitFor(() => expect(mocks.buildTableStructureChangeSql).toHaveBeenCalled());
expect(mocks.buildTableStructureChangeSql).toHaveBeenLastCalledWith(
expect.objectContaining({
columns: [expect.objectContaining({ isPrimaryKey: true, isNullable: false })],
}),
);
});
it("keeps all primary-key checkboxes enabled while composing a new Oracle key", async () => {
const root = await mountEditor("oracle");
buttonWithText(root, "structureEditor.addColumn").click();
await nextTick();
const firstPrimaryKey = columnCheckbox(root, "structureEditor.primaryKey", 0);
const secondPrimaryKey = columnCheckbox(root, "structureEditor.primaryKey", 1);
expect(firstPrimaryKey.disabled).toBe(false);
expect(secondPrimaryKey.disabled).toBe(false);
firstPrimaryKey.checked = true;
firstPrimaryKey.dispatchEvent(new Event("change", { bubbles: true }));
await nextTick();
expect(secondPrimaryKey.disabled).toBe(false);
secondPrimaryKey.checked = true;
secondPrimaryKey.dispatchEvent(new Event("change", { bubbles: true }));
await nextTick();
await vi.waitFor(() =>
expect(mocks.buildTableStructureChangeSql).toHaveBeenLastCalledWith(
expect.objectContaining({
columns: [expect.objectContaining({ isPrimaryKey: true }), expect.objectContaining({ isPrimaryKey: true })],
}),
),
);
});
it("keeps an existing Oracle primary key and all replacement choices disabled", async () => {
const root = await mountEditor("oracle", true);
buttonWithText(root, "structureEditor.addColumn").click();
await nextTick();
expect(columnCheckbox(root, "structureEditor.primaryKey", 0).disabled).toBe(true);
expect(columnCheckbox(root, "structureEditor.primaryKey", 1).disabled).toBe(true);
});
it.each(["oceanbase-oracle", "iris"] as const)("keeps primary-key creation disabled for existing %s tables", async (databaseType) => {
const root = await mountEditor(databaseType);
expect(columnCheckbox(root, "structureEditor.primaryKey").disabled).toBe(true);
});
@@ -2433,8 +2433,9 @@ function isColumnCharsetDisabled(column: EditableStructureColumn): boolean {
function isPrimaryKeyDisabled(column: EditableStructureColumn): boolean {
if (column.markedForDrop) return true;
if (!column.original) return false;
return !structureCapabilities.value.alterPrimaryKey;
if (isCreateMode.value || structureCapabilities.value.alterPrimaryKey) return false;
if (!structureCapabilities.value.addPrimaryKey) return true;
return columns.value.some((candidate) => candidate.original?.is_primary_key);
}
function canDropColumn(column: EditableStructureColumn): boolean {
@@ -93,10 +93,21 @@ describe("tableStructureCapabilities", () => {
expect(getTableStructureCapabilities("dameng", "dameng").comment).toBe(true);
});
it("enables alter primary key for Dameng without enabling it for Oracle", () => {
expect(getTableStructureCapabilities("dameng", "dameng").alterPrimaryKey).toBe(true);
expect(getTableStructureCapabilities("oracle", "oracle").alterPrimaryKey).toBe(false);
expect(getTableStructureCapabilities("oceanbase-oracle", "oceanbase-oracle").alterPrimaryKey).toBe(false);
it("separates adding a primary key from replacing an existing one", () => {
expect(getTableStructureCapabilities("dameng", "dameng")).toMatchObject({
addPrimaryKey: true,
alterPrimaryKey: true,
});
expect(getTableStructureCapabilities("oracle", "oracle")).toMatchObject({
addPrimaryKey: true,
alterPrimaryKey: false,
});
for (const databaseType of ["oceanbase-oracle", "iris"] as const) {
expect(getTableStructureCapabilities(databaseType, databaseType)).toMatchObject({
addPrimaryKey: false,
alterPrimaryKey: false,
});
}
});
it("uses local-only column reordering for editable databases without physical reorder support", () => {
@@ -15,6 +15,7 @@ export interface TableStructureCapabilities {
alterType: boolean;
alterNullability: boolean;
alterDefault: boolean;
addPrimaryKey: boolean;
alterPrimaryKey: boolean;
reorderColumn: boolean;
comment: boolean;
@@ -40,6 +41,7 @@ const unsupportedCapabilities: TableStructureCapabilities = {
alterType: false,
alterNullability: false,
alterDefault: false,
addPrimaryKey: false,
alterPrimaryKey: false,
reorderColumn: false,
comment: false,
@@ -59,6 +61,9 @@ function capabilities(overrides: Partial<TableStructureCapabilities>): TableStru
if (overrides.alterStrategy === undefined && resolved.alterExistingColumn) {
resolved.alterStrategy = "direct";
}
if (resolved.alterPrimaryKey) {
resolved.addPrimaryKey = true;
}
return resolved;
}
@@ -129,6 +134,7 @@ const redshiftCapabilities = capabilities({
indexInclude: false,
indexFilter: false,
indexComment: false,
addPrimaryKey: false,
alterPrimaryKey: false,
});
@@ -182,7 +188,7 @@ const sqlserverCapabilities = capabilities({
indexComment: true,
});
const oracleCapabilities = capabilities({
const oracleCompatibleCapabilities = capabilities({
dialect: "oracle",
createTable: true,
addColumn: true,
@@ -199,15 +205,20 @@ const oracleCapabilities = capabilities({
indexType: true,
});
const oracleCapabilities = capabilities({
...oracleCompatibleCapabilities,
addPrimaryKey: true,
});
// Dameng (DM8): ALTER TABLE ... DROP PRIMARY KEY / ADD PRIMARY KEY is official DDL.
// Keep separate from oracleCapabilities so UI cannot enable PK edit without BE drop SQL.
// Keep separate from Oracle-compatible engines so UI cannot enable PK edits without verified DDL.
const damengCapabilities = capabilities({
...oracleCapabilities,
...oracleCompatibleCapabilities,
alterPrimaryKey: true,
});
const irisCapabilities = capabilities({
...oracleCapabilities,
...oracleCompatibleCapabilities,
// IRIS exposes %DESCRIPTION at definition time but cannot alter persisted descriptions.
comment: false,
});
@@ -340,10 +351,10 @@ const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>
sqlserver: sqlserverCapabilities,
oracle: oracleCapabilities,
dameng: damengCapabilities,
"oceanbase-oracle": oracleCapabilities,
"oceanbase-oracle": oracleCompatibleCapabilities,
iris: irisCapabilities,
yashandb: oracleCapabilities,
xugu: oracleCapabilities,
yashandb: oracleCompatibleCapabilities,
xugu: oracleCompatibleCapabilities,
h2: h2Capabilities,
access: accessCapabilities,
clickhouse: clickhouseCapabilities,