fix(dameng): support primary key editing
This commit is contained in:
1 parent
66f94f23f1
commit
f80fdcdfa7
6 files changed
+778
-168
No files matched your search
+324
@@ -0,0 +1,324 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { createApp, nextTick, type App } from "vue";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
connection: {
|
||||||
|
id: "structure-test",
|
||||||
|
name: "Dameng",
|
||||||
|
db_type: "dameng",
|
||||||
|
driver_label: "Dameng",
|
||||||
|
},
|
||||||
|
ensureConnected: vi.fn(),
|
||||||
|
listDataTypes: vi.fn(),
|
||||||
|
buildTableStructureChangeSql: vi.fn(),
|
||||||
|
updateEditorSettings: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
|
||||||
|
|
||||||
|
vi.mock("@lucide/vue", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Icon = defineComponent({ name: "Icon", setup: () => () => h("span") });
|
||||||
|
return {
|
||||||
|
AlertTriangle: Icon,
|
||||||
|
Check: Icon,
|
||||||
|
ChevronDown: Icon,
|
||||||
|
ChevronUp: Icon,
|
||||||
|
Copy: Icon,
|
||||||
|
Database: Icon,
|
||||||
|
Info: Icon,
|
||||||
|
KeyRound: Icon,
|
||||||
|
ListChevronsUpDown: Icon,
|
||||||
|
Loader2: Icon,
|
||||||
|
Maximize2: Icon,
|
||||||
|
Plus: Icon,
|
||||||
|
RefreshCw: Icon,
|
||||||
|
Save: Icon,
|
||||||
|
Search: Icon,
|
||||||
|
Settings: Icon,
|
||||||
|
SlidersHorizontal: Icon,
|
||||||
|
Trash2: Icon,
|
||||||
|
X: Icon,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/button", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
return {
|
||||||
|
Button: defineComponent({
|
||||||
|
name: "Button",
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("button", attrs, slots.default?.()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/input", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
return {
|
||||||
|
Input: defineComponent({
|
||||||
|
name: "Input",
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs }) =>
|
||||||
|
() =>
|
||||||
|
h("input", attrs),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/badge", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
return {
|
||||||
|
Badge: defineComponent({
|
||||||
|
name: "Badge",
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("span", attrs, slots.default?.()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/tabs", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Div = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("div", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
const Button = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("button", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
return { Tabs: Div, TabsContent: Div, TabsList: Div, TabsTrigger: Button };
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/dropdown-menu", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Div = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("div", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
const Button = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("button", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
return { DropdownMenu: Div, DropdownMenuCheckboxItem: Div, DropdownMenuContent: Div, DropdownMenuItem: Button, DropdownMenuTrigger: Div };
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/popover", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Div = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("div", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
return { Popover: Div, PopoverContent: Div, PopoverTrigger: Div };
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/tooltip", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Div = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("div", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
return { Tooltip: Div, TooltipContent: Div, TooltipTrigger: Div };
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/searchable-select", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
return {
|
||||||
|
SearchableSelect: defineComponent({
|
||||||
|
name: "SearchableSelect",
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs }) =>
|
||||||
|
() =>
|
||||||
|
h("div", attrs),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
vi.mock("@/components/ui/select", async () => {
|
||||||
|
const { defineComponent, h } = await import("vue");
|
||||||
|
const Div = defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup:
|
||||||
|
(_props, { attrs, slots }) =>
|
||||||
|
() =>
|
||||||
|
h("div", attrs, slots.default?.()),
|
||||||
|
});
|
||||||
|
return { Select: Div, SelectContent: Div, SelectItem: Div, SelectTrigger: Div, SelectValue: Div };
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/stores/connectionStore", () => ({
|
||||||
|
useConnectionStore: () => ({
|
||||||
|
ensureConnected: mocks.ensureConnected,
|
||||||
|
getConfig: (connectionId: string) => (connectionId === mocks.connection.id ? mocks.connection : undefined),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.mock("@/stores/productionSafetyStore", () => ({ useProductionSafetyStore: () => ({ requestConfirmation: vi.fn() }) }));
|
||||||
|
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({ tableStructureRefreshVersion: () => 0 }) }));
|
||||||
|
vi.mock("@/stores/historyStore", () => ({ useHistoryStore: () => ({ add: vi.fn() }) }));
|
||||||
|
vi.mock("@/stores/settingsStore", () => ({
|
||||||
|
useSettingsStore: () => ({
|
||||||
|
editorSettings: { structureEditorDensity: "compact", sqlFormatter: {}, tableColumnTemplateFields: [] },
|
||||||
|
updateEditorSettings: mocks.updateEditorSettings,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.mock("@/composables/useTheme", () => ({ useTheme: () => ({ isDark: { value: false } }) }));
|
||||||
|
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||||
|
vi.mock("@/lib/sql/sqlHighlighter", () => ({ createShikiSqlHighlighter: vi.fn(async () => (sql: string) => sql) }));
|
||||||
|
vi.mock("@/lib/backend/api", () => ({
|
||||||
|
listDataTypes: mocks.listDataTypes,
|
||||||
|
buildTableStructureChangeSql: mocks.buildTableStructureChangeSql,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import TableStructureEditor from "@/components/structure/TableStructureEditor.vue";
|
||||||
|
|
||||||
|
const mountedApps: App[] = [];
|
||||||
|
|
||||||
|
function draft(isPrimaryKey = false) {
|
||||||
|
return {
|
||||||
|
initialized: true,
|
||||||
|
activeTab: "columns" as const,
|
||||||
|
newTableName: "",
|
||||||
|
tableComment: "",
|
||||||
|
originalTableComment: "",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: "existing:id",
|
||||||
|
name: "id",
|
||||||
|
dataType: "INT",
|
||||||
|
isNullable: !isPrimaryKey,
|
||||||
|
defaultValue: "",
|
||||||
|
comment: "",
|
||||||
|
isPrimaryKey,
|
||||||
|
extra: {},
|
||||||
|
original: {
|
||||||
|
name: "id",
|
||||||
|
data_type: "INT",
|
||||||
|
is_nullable: !isPrimaryKey,
|
||||||
|
column_default: null,
|
||||||
|
is_primary_key: isPrimaryKey,
|
||||||
|
extra: null,
|
||||||
|
comment: null,
|
||||||
|
},
|
||||||
|
originalPosition: 0,
|
||||||
|
markedForDrop: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indexes: [],
|
||||||
|
foreignKeys: [],
|
||||||
|
triggers: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountEditor(databaseType: "dameng" | "oracle", isPrimaryKey = false) {
|
||||||
|
mocks.connection.db_type = databaseType;
|
||||||
|
mocks.connection.name = databaseType;
|
||||||
|
mocks.connection.driver_label = databaseType;
|
||||||
|
mocks.ensureConnected.mockResolvedValue(undefined);
|
||||||
|
mocks.listDataTypes.mockResolvedValue([]);
|
||||||
|
mocks.buildTableStructureChangeSql.mockResolvedValue({ statements: [], warnings: [] });
|
||||||
|
|
||||||
|
const root = document.createElement("div");
|
||||||
|
document.body.append(root);
|
||||||
|
const app = createApp(TableStructureEditor, {
|
||||||
|
connectionId: mocks.connection.id,
|
||||||
|
database: "test",
|
||||||
|
schema: "SYSDBA",
|
||||||
|
tableName: "users",
|
||||||
|
draft: draft(isPrimaryKey),
|
||||||
|
});
|
||||||
|
mountedApps.push(app);
|
||||||
|
app.mount(root);
|
||||||
|
await nextTick();
|
||||||
|
await Promise.resolve();
|
||||||
|
await nextTick();
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function columnCheckbox(root: HTMLElement, header: string): 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 cell = row?.querySelectorAll("td")[headerIndex];
|
||||||
|
const checkbox = cell?.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||||
|
if (!checkbox) throw new Error(`Missing ${header} checkbox`);
|
||||||
|
return checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const app of mountedApps.splice(0)) app.unmount();
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("TableStructureEditor primary key editing", () => {
|
||||||
|
it("enables the primary-key checkbox for an existing Dameng column and makes it not null", async () => {
|
||||||
|
const root = await mountEditor("dameng");
|
||||||
|
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 the primary-key checkbox disabled for an existing Oracle column", async () => {
|
||||||
|
const root = await mountEditor("oracle");
|
||||||
|
|
||||||
|
expect(columnCheckbox(root, "structureEditor.primaryKey").disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows an existing Dameng primary key to be cleared", async () => {
|
||||||
|
const root = await mountEditor("dameng", true);
|
||||||
|
const primaryKey = columnCheckbox(root, "structureEditor.primaryKey");
|
||||||
|
|
||||||
|
expect(primaryKey.disabled).toBe(false);
|
||||||
|
expect(primaryKey.checked).toBe(true);
|
||||||
|
|
||||||
|
primaryKey.checked = false;
|
||||||
|
primaryKey.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(primaryKey.checked).toBe(false);
|
||||||
|
await vi.waitFor(() => expect(mocks.buildTableStructureChangeSql).toHaveBeenCalled());
|
||||||
|
expect(mocks.buildTableStructureChangeSql).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
columns: [expect.objectContaining({ isPrimaryKey: false })],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -43,6 +43,12 @@ describe("tableStructureCapabilities", () => {
|
|||||||
expect(getTableStructureCapabilities("dameng", "dameng").comment).toBe(true);
|
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("uses local-only column reordering for editable databases without physical reorder support", () => {
|
it("uses local-only column reordering for editable databases without physical reorder support", () => {
|
||||||
for (const databaseType of ["sqlserver", "postgres", "sqlite", "oracle", "dameng", "duckdb", "informix"] as const) {
|
for (const databaseType of ["sqlserver", "postgres", "sqlite", "oracle", "dameng", "duckdb", "informix"] as const) {
|
||||||
expect(supportsLocalTableColumnReorder(databaseType, databaseType)).toBe(true);
|
expect(supportsLocalTableColumnReorder(databaseType, databaseType)).toBe(true);
|
||||||
|
|||||||
@@ -190,6 +190,13 @@ const oracleCapabilities = capabilities({
|
|||||||
indexType: true,
|
indexType: 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.
|
||||||
|
const damengCapabilities = capabilities({
|
||||||
|
...oracleCapabilities,
|
||||||
|
alterPrimaryKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
const irisCapabilities = capabilities({
|
const irisCapabilities = capabilities({
|
||||||
...oracleCapabilities,
|
...oracleCapabilities,
|
||||||
// IRIS exposes %DESCRIPTION at definition time but cannot alter persisted descriptions.
|
// IRIS exposes %DESCRIPTION at definition time but cannot alter persisted descriptions.
|
||||||
@@ -323,7 +330,7 @@ const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>
|
|||||||
duckdb: duckdbCapabilities,
|
duckdb: duckdbCapabilities,
|
||||||
sqlserver: sqlserverCapabilities,
|
sqlserver: sqlserverCapabilities,
|
||||||
oracle: oracleCapabilities,
|
oracle: oracleCapabilities,
|
||||||
dameng: oracleCapabilities,
|
dameng: damengCapabilities,
|
||||||
"oceanbase-oracle": oracleCapabilities,
|
"oceanbase-oracle": oracleCapabilities,
|
||||||
iris: irisCapabilities,
|
iris: irisCapabilities,
|
||||||
yashandb: oracleCapabilities,
|
yashandb: oracleCapabilities,
|
||||||
|
|||||||
@@ -209,12 +209,22 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit primary key constraint changes after individual column changes
|
// Keep the existing key while column DDL validates. This avoids leaving a table
|
||||||
|
// without a key when an incoming key column cannot be made valid.
|
||||||
statements.extend(build_primary_key_sql(options, dialect, &table, warnings));
|
statements.extend(build_primary_key_sql(options, dialect, &table, warnings));
|
||||||
|
|
||||||
statements
|
statements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn was_primary_key_column(column: &EditableStructureColumn) -> bool {
|
||||||
|
column.original.as_ref().is_some_and(|original| original.is_primary_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Columns that should appear in `ADD PRIMARY KEY (...)` (must remain on the table).
|
||||||
|
fn appears_in_add_primary_key(column: &EditableStructureColumn) -> bool {
|
||||||
|
column.is_primary_key && !column.marked_for_drop
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn build_primary_key_sql(
|
pub(super) fn build_primary_key_sql(
|
||||||
options: &TableStructureSqlOptions,
|
options: &TableStructureSqlOptions,
|
||||||
dialect: StructureDialect,
|
dialect: StructureDialect,
|
||||||
@@ -223,17 +233,21 @@ pub(super) fn build_primary_key_sql(
|
|||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let capabilities = capabilities_for(options.database_type);
|
let capabilities = capabilities_for(options.database_type);
|
||||||
|
|
||||||
let old_pk_names: Vec<&str> = options
|
// A draft cannot drop a primary-key column. The column pass emits the user-facing
|
||||||
.columns
|
// warning; keep the entire PK change empty so another checked column cannot
|
||||||
.iter()
|
// turn that invalid draft into a partial DROP/ADD constraint change.
|
||||||
.filter(|c| c.original.as_ref().is_some_and(|o| o.is_primary_key))
|
if options.columns.iter().any(|column| column.marked_for_drop && was_primary_key_column(column)) {
|
||||||
.map(|c| c.name.as_str())
|
return Vec::new();
|
||||||
.collect();
|
}
|
||||||
|
|
||||||
let new_pk_names: Vec<&str> =
|
// Membership by draft id (set equality): pure rename / local reorder of the same key
|
||||||
options.columns.iter().filter(|c| !c.marked_for_drop && c.is_primary_key).map(|c| c.name.as_str()).collect();
|
// columns is not a PK change. ADD still lists columns in table order.
|
||||||
|
let old_pk_ids: HashSet<&str> =
|
||||||
|
options.columns.iter().filter(|c| was_primary_key_column(c)).map(|c| c.id.as_str()).collect();
|
||||||
|
let new_pk_ids: HashSet<&str> =
|
||||||
|
options.columns.iter().filter(|c| appears_in_add_primary_key(c)).map(|c| c.id.as_str()).collect();
|
||||||
|
|
||||||
if old_pk_names == new_pk_names {
|
if old_pk_ids == new_pk_ids {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,29 +260,52 @@ pub(super) fn build_primary_key_sql(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut statements = Vec::new();
|
let mut statements = Vec::new();
|
||||||
|
if !old_pk_ids.is_empty() {
|
||||||
if !old_pk_names.is_empty() {
|
let Some(drop_sql) = drop_primary_key_statement(dialect, table, options) else {
|
||||||
match dialect {
|
warnings.push(format!(
|
||||||
StructureDialect::Postgres => {
|
"Changing primary keys is not supported for {} from this editor.",
|
||||||
let raw_table = options.table_name.split('.').next_back().unwrap_or(&options.table_name);
|
database_label(options.database_type)
|
||||||
let pk_name = format!("{}_pkey", clean(raw_table));
|
));
|
||||||
statements.push(format!("ALTER TABLE {table} DROP CONSTRAINT {};", quote_ident(dialect, &pk_name)));
|
return Vec::new();
|
||||||
}
|
};
|
||||||
StructureDialect::Mysql => {
|
statements.push(drop_sql);
|
||||||
statements.push(format!("ALTER TABLE {table} DROP PRIMARY KEY;"));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let new_pk_names: Vec<&str> =
|
||||||
|
options.columns.iter().filter(|c| appears_in_add_primary_key(c)).map(|c| c.name.as_str()).collect();
|
||||||
if !new_pk_names.is_empty() {
|
if !new_pk_names.is_empty() {
|
||||||
let pk_list = new_pk_names.iter().map(|n| quote_ident(dialect, n)).collect::<Vec<_>>().join(", ");
|
let pk_list = new_pk_names.iter().map(|n| quote_ident(dialect, n)).collect::<Vec<_>>().join(", ");
|
||||||
|
// DM8: ADD [CONSTRAINT name] PRIMARY KEY; anonymous form matches Navicat/DBeaver/MySQL editors.
|
||||||
statements.push(format!("ALTER TABLE {table} ADD PRIMARY KEY ({pk_list});"));
|
statements.push(format!("ALTER TABLE {table} ADD PRIMARY KEY ({pk_list});"));
|
||||||
}
|
}
|
||||||
|
|
||||||
statements
|
statements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dialect-specific DROP for an existing primary key.
|
||||||
|
///
|
||||||
|
/// - MySQL: `DROP PRIMARY KEY`
|
||||||
|
/// - Dameng (DM8): official `DROP PRIMARY KEY [RESTRICT|CASCADE]`; default RESTRICT
|
||||||
|
/// (no CASCADE — dependent FKs should not be silently removed).
|
||||||
|
/// System names (`CONS…`) are not stable; name-based DROP is avoided.
|
||||||
|
/// Cluster primary keys cannot use this path (DM8 restriction) — left to the server.
|
||||||
|
/// - Postgres: `DROP CONSTRAINT {table}_pkey` (default naming convention)
|
||||||
|
fn drop_primary_key_statement(
|
||||||
|
dialect: StructureDialect,
|
||||||
|
table: &str,
|
||||||
|
options: &TableStructureSqlOptions,
|
||||||
|
) -> Option<String> {
|
||||||
|
match dialect {
|
||||||
|
StructureDialect::Postgres => {
|
||||||
|
let raw_table = options.table_name.split('.').next_back().unwrap_or(&options.table_name);
|
||||||
|
let pk_name = format!("{}_pkey", clean(raw_table));
|
||||||
|
Some(format!("ALTER TABLE {table} DROP CONSTRAINT {};", quote_ident(dialect, &pk_name)))
|
||||||
|
}
|
||||||
|
StructureDialect::Mysql | StructureDialect::Dameng => Some(format!("ALTER TABLE {table} DROP PRIMARY KEY;")),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn has_sqlserver_identity(column: &EditableStructureColumn) -> bool {
|
fn has_sqlserver_identity(column: &EditableStructureColumn) -> bool {
|
||||||
column.extra.as_ref().is_some_and(|extra| extra.auto_increment.unwrap_or(false) || extra.identity.is_some())
|
column.extra.as_ref().is_some_and(|extra| extra.auto_increment.unwrap_or(false) || extra.identity.is_some())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
|
|||||||
drop_index: true,
|
drop_index: true,
|
||||||
rebuild_index: true,
|
rebuild_index: true,
|
||||||
index_type: true,
|
index_type: true,
|
||||||
|
alter_primary_key: true,
|
||||||
..base
|
..base
|
||||||
},
|
},
|
||||||
Some(DatabaseType::Iris) => TableStructureCapabilities {
|
Some(DatabaseType::Iris) => TableStructureCapabilities {
|
||||||
|
|||||||
@@ -19,6 +19,49 @@ fn column(name: &str) -> EditableStructureColumn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Existing column draft with optional primary-key membership change.
|
||||||
|
fn existing_pk_column(
|
||||||
|
name: &str,
|
||||||
|
data_type: &str,
|
||||||
|
was_primary_key: bool,
|
||||||
|
is_primary_key: bool,
|
||||||
|
) -> EditableStructureColumn {
|
||||||
|
let mut col = column(name);
|
||||||
|
col.data_type = data_type.to_string();
|
||||||
|
col.is_nullable = false;
|
||||||
|
col.is_primary_key = is_primary_key;
|
||||||
|
col.original = Some(ColumnInfo {
|
||||||
|
name: name.to_string(),
|
||||||
|
data_type: data_type.to_string(),
|
||||||
|
is_nullable: false,
|
||||||
|
column_default: None,
|
||||||
|
is_primary_key: was_primary_key,
|
||||||
|
extra: None,
|
||||||
|
comment: None,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
col
|
||||||
|
}
|
||||||
|
|
||||||
|
fn structure_change_options(
|
||||||
|
database_type: DatabaseType,
|
||||||
|
schema: Option<&str>,
|
||||||
|
table_name: &str,
|
||||||
|
columns: Vec<EditableStructureColumn>,
|
||||||
|
) -> TableStructureSqlOptions {
|
||||||
|
TableStructureSqlOptions {
|
||||||
|
database_type: Some(database_type),
|
||||||
|
schema: schema.map(str::to_string),
|
||||||
|
table_name: table_name.to_string(),
|
||||||
|
columns,
|
||||||
|
indexes: Vec::new(),
|
||||||
|
foreign_keys: Vec::new(),
|
||||||
|
triggers: Vec::new(),
|
||||||
|
table_comment: None,
|
||||||
|
original_table_comment: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn index(name: &str, columns: &[&str]) -> EditableStructureIndex {
|
fn index(name: &str, columns: &[&str]) -> EditableStructureIndex {
|
||||||
EditableStructureIndex {
|
EditableStructureIndex {
|
||||||
id: name.to_string(),
|
id: name.to_string(),
|
||||||
@@ -2547,65 +2590,297 @@ fn builds_h2_schema_qualified_existing_column_statements() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_postgres_alter_table_add_primary_key() {
|
fn builds_postgres_alter_table_add_primary_key() {
|
||||||
let mut id = column("id");
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
id.data_type = "integer".to_string();
|
DatabaseType::Postgres,
|
||||||
id.is_nullable = false;
|
Some("public"),
|
||||||
id.is_primary_key = true;
|
"users",
|
||||||
id.original = Some(ColumnInfo {
|
vec![existing_pk_column("id", "integer", false, true)],
|
||||||
name: "id".to_string(),
|
));
|
||||||
data_type: "integer".to_string(),
|
|
||||||
is_nullable: false,
|
|
||||||
column_default: None,
|
|
||||||
is_primary_key: false,
|
|
||||||
extra: None,
|
|
||||||
comment: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
|
||||||
database_type: Some(DatabaseType::Postgres),
|
|
||||||
schema: Some("public".to_string()),
|
|
||||||
table_name: "users".to_string(),
|
|
||||||
columns: vec![id],
|
|
||||||
indexes: Vec::new(),
|
|
||||||
foreign_keys: Vec::new(),
|
|
||||||
triggers: Vec::new(),
|
|
||||||
table_comment: None,
|
|
||||||
original_table_comment: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(result.warnings, Vec::<String>::new());
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
assert_eq!(result.statements, vec!["ALTER TABLE \"public\".\"users\" ADD PRIMARY KEY (\"id\");"]);
|
assert_eq!(result.statements, vec!["ALTER TABLE \"public\".\"users\" ADD PRIMARY KEY (\"id\");"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_postgres_alter_table_drop_primary_key() {
|
fn builds_dameng_alter_table_add_primary_key() {
|
||||||
let mut id = column("id");
|
// DM8: ADD [CONSTRAINT name] PRIMARY KEY — anonymous form matches DBeaver/MySQL-style editors.
|
||||||
id.data_type = "integer".to_string();
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
id.is_nullable = false;
|
DatabaseType::Dameng,
|
||||||
id.is_primary_key = false;
|
Some("SYSDBA"),
|
||||||
id.original = Some(ColumnInfo {
|
"users",
|
||||||
name: "id".to_string(),
|
vec![existing_pk_column("id", "INT", false, true)],
|
||||||
data_type: "integer".to_string(),
|
));
|
||||||
is_nullable: false,
|
|
||||||
column_default: None,
|
|
||||||
is_primary_key: true,
|
|
||||||
extra: None,
|
|
||||||
comment: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
database_type: Some(DatabaseType::Postgres),
|
assert_eq!(result.statements, vec!["ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"id\");"]);
|
||||||
schema: Some("public".to_string()),
|
}
|
||||||
table_name: "users".to_string(),
|
|
||||||
columns: vec![id],
|
#[test]
|
||||||
indexes: Vec::new(),
|
fn builds_dameng_composite_primary_key_in_draft_order() {
|
||||||
foreign_keys: Vec::new(),
|
let mut tenant_id = existing_pk_column("tenant_id", "INT", false, true);
|
||||||
triggers: Vec::new(),
|
tenant_id.id = "tenant_id".to_string();
|
||||||
table_comment: None,
|
let mut code = existing_pk_column("code", "VARCHAR(50)", false, true);
|
||||||
original_table_comment: None,
|
code.id = "code".to_string();
|
||||||
});
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![tenant_id, code],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert_eq!(result.statements, vec!["ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"tenant_id\", \"code\");"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_reordering_unchanged_composite_primary_key_does_not_emit_primary_key_ddl() {
|
||||||
|
let mut tenant_id = existing_pk_column("tenant_id", "INT", true, true);
|
||||||
|
tenant_id.id = "tenant_id".to_string();
|
||||||
|
tenant_id.original_position = Some(0);
|
||||||
|
let mut code = existing_pk_column("code", "VARCHAR(50)", true, true);
|
||||||
|
code.id = "code".to_string();
|
||||||
|
code.original_position = Some(1);
|
||||||
|
|
||||||
|
// Dameng reordering is local-only. Moving these columns must not recreate the key
|
||||||
|
// merely because the draft order changed.
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![code, tenant_id],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert!(result.statements.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_adds_new_primary_key_column_before_adding_constraint() {
|
||||||
|
let mut code = column("code");
|
||||||
|
code.data_type = "VARCHAR(50)".to_string();
|
||||||
|
code.is_nullable = false;
|
||||||
|
code.is_primary_key = true;
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![code],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert_eq!(
|
||||||
|
result.statements,
|
||||||
|
vec![
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" ADD (\"code\" VARCHAR(50));",
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"code\");",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_dameng_alter_table_drop_primary_key() {
|
||||||
|
// DM8 official: DROP PRIMARY KEY [RESTRICT|CASCADE]; default RESTRICT (no CASCADE from editor).
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![existing_pk_column("id", "INT", true, false)],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert_eq!(result.statements, vec!["ALTER TABLE \"SYSDBA\".\"users\" DROP PRIMARY KEY;"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_dameng_alter_table_change_primary_key() {
|
||||||
|
// DBeaver/Navicat-style modify: drop existing key then add the new one (never ADD without DROP).
|
||||||
|
let mut old_pk = existing_pk_column("id", "INT", true, false);
|
||||||
|
old_pk.id = "old_id".to_string();
|
||||||
|
let mut new_pk = existing_pk_column("code", "VARCHAR(50)", false, true);
|
||||||
|
new_pk.id = "new_code".to_string();
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![old_pk, new_pk],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert_eq!(
|
||||||
|
result.statements,
|
||||||
|
vec![
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" DROP PRIMARY KEY;",
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"code\");",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_validates_new_primary_key_column_before_replacing_existing_key() {
|
||||||
|
let mut old_pk = existing_pk_column("id", "INT", true, false);
|
||||||
|
old_pk.id = "old_id".to_string();
|
||||||
|
let mut code = existing_pk_column("code", "VARCHAR(50)", false, true);
|
||||||
|
code.id = "new_code".to_string();
|
||||||
|
code.original.as_mut().unwrap().is_nullable = true;
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![old_pk, code],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert_eq!(
|
||||||
|
result.statements,
|
||||||
|
vec![
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" MODIFY (\"code\" VARCHAR(50) NOT NULL);",
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" DROP PRIMARY KEY;",
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"code\");",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_blocks_dropping_former_primary_key_column() {
|
||||||
|
let mut id = existing_pk_column("id", "INT", true, false);
|
||||||
|
id.marked_for_drop = true;
|
||||||
|
let name = existing_pk_column("name", "VARCHAR(50)", false, false);
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![id, name],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(result.statements.is_empty());
|
||||||
|
assert!(result.warnings.iter().any(|warning| warning.contains("Primary key column")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oracle_uncheck_primary_key_and_drop_column_does_not_emit_drop_column() {
|
||||||
|
// alter_primary_key is false for Oracle: unchecking PK must not unlock DROP COLUMN without a PK drop.
|
||||||
|
let mut id = existing_pk_column("id", "NUMBER", true, false);
|
||||||
|
id.marked_for_drop = true;
|
||||||
|
let name = existing_pk_column("name", "VARCHAR2(50)", false, false);
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Oracle,
|
||||||
|
Some("HR"),
|
||||||
|
"users",
|
||||||
|
vec![id, name],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!result.statements.iter().any(|sql| sql.to_ascii_uppercase().contains("DROP COLUMN")),
|
||||||
|
"must not DROP COLUMN former PK without DROP PRIMARY KEY; got {:?}",
|
||||||
|
result.statements
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!result.statements.iter().any(|sql| sql.to_ascii_uppercase().contains("PRIMARY KEY")),
|
||||||
|
"Oracle must not emit partial PK DDL; got {:?}",
|
||||||
|
result.statements
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.warnings.iter().any(|w| w.contains("primary key") || w.contains("Primary key")),
|
||||||
|
"expected primary-key related warning; got {:?}",
|
||||||
|
result.warnings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sqlserver_uncheck_primary_key_and_drop_column_does_not_emit_drop_column() {
|
||||||
|
let mut id = existing_pk_column("id", "int", true, false);
|
||||||
|
id.marked_for_drop = true;
|
||||||
|
let name = existing_pk_column("name", "nvarchar(50)", false, false);
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::SqlServer,
|
||||||
|
Some("dbo"),
|
||||||
|
"users",
|
||||||
|
vec![id, name],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!result.statements.iter().any(|sql| sql.to_ascii_uppercase().contains("DROP COLUMN")),
|
||||||
|
"must not DROP COLUMN former PK without PK drop; got {:?}",
|
||||||
|
result.statements
|
||||||
|
);
|
||||||
|
assert!(result.warnings.iter().any(|w| w.contains("primary key") || w.contains("Primary key")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_set_not_null_before_add_primary_key() {
|
||||||
|
// DM8: PK columns must be NOT NULL; DM auto-adds NOT NULL but clients still MODIFY first.
|
||||||
|
// Order: column MODIFY NOT NULL, then ADD PRIMARY KEY.
|
||||||
|
let mut id = existing_pk_column("id", "INT", false, true);
|
||||||
|
id.original.as_mut().unwrap().is_nullable = true;
|
||||||
|
// is_nullable stays false (set when marking PK) so MODIFY ... NOT NULL is emitted.
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![id],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
|
assert_eq!(
|
||||||
|
result.statements,
|
||||||
|
vec![
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" MODIFY (\"id\" INT NOT NULL);",
|
||||||
|
"ALTER TABLE \"SYSDBA\".\"users\" ADD PRIMARY KEY (\"id\");",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_blocks_dropping_active_primary_key_column() {
|
||||||
|
// Keep a non-PK column so we do not hit the "cannot drop all columns" guard first.
|
||||||
|
let mut id = existing_pk_column("id", "INT", true, true);
|
||||||
|
id.marked_for_drop = true;
|
||||||
|
let name = existing_pk_column("name", "VARCHAR(50)", false, false);
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![id, name],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(!result.statements.iter().any(|sql| sql.contains("DROP COLUMN")));
|
||||||
|
assert!(result.warnings.iter().any(|w| w.contains("Primary key column")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dameng_does_not_mutate_primary_key_when_active_key_column_is_marked_for_drop() {
|
||||||
|
let mut id = existing_pk_column("id", "INT", true, true);
|
||||||
|
id.marked_for_drop = true;
|
||||||
|
let code = existing_pk_column("code", "VARCHAR(50)", false, true);
|
||||||
|
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Dameng,
|
||||||
|
Some("SYSDBA"),
|
||||||
|
"users",
|
||||||
|
vec![id, code],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(result.statements.is_empty(), "invalid draft must not emit partial PK DDL: {:?}", result.statements);
|
||||||
|
assert!(result.warnings.iter().any(|warning| warning.contains("Primary key column")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_postgres_alter_table_drop_primary_key() {
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Postgres,
|
||||||
|
Some("public"),
|
||||||
|
"users",
|
||||||
|
vec![existing_pk_column("id", "integer", true, false)],
|
||||||
|
));
|
||||||
|
|
||||||
assert_eq!(result.warnings, Vec::<String>::new());
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
assert_eq!(result.statements, vec!["ALTER TABLE \"public\".\"users\" DROP CONSTRAINT \"users_pkey\";"]);
|
assert_eq!(result.statements, vec!["ALTER TABLE \"public\".\"users\" DROP CONSTRAINT \"users_pkey\";"]);
|
||||||
@@ -2613,49 +2888,17 @@ fn builds_postgres_alter_table_drop_primary_key() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_mysql_alter_table_change_primary_key() {
|
fn builds_mysql_alter_table_change_primary_key() {
|
||||||
let mut old_pk = column("id");
|
let mut old_pk = existing_pk_column("id", "int", true, false);
|
||||||
old_pk.id = "old_id".to_string();
|
old_pk.id = "old_id".to_string();
|
||||||
old_pk.data_type = "int".to_string();
|
let mut new_pk = existing_pk_column("uuid", "varchar(36)", false, true);
|
||||||
old_pk.is_nullable = false;
|
|
||||||
old_pk.is_primary_key = false;
|
|
||||||
old_pk.original = Some(ColumnInfo {
|
|
||||||
name: "id".to_string(),
|
|
||||||
data_type: "int".to_string(),
|
|
||||||
is_nullable: false,
|
|
||||||
column_default: None,
|
|
||||||
is_primary_key: true,
|
|
||||||
extra: None,
|
|
||||||
comment: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut new_pk = column("uuid");
|
|
||||||
new_pk.id = "new_uuid".to_string();
|
new_pk.id = "new_uuid".to_string();
|
||||||
new_pk.data_type = "varchar(36)".to_string();
|
|
||||||
new_pk.is_nullable = false;
|
|
||||||
new_pk.is_primary_key = true;
|
|
||||||
new_pk.original = Some(ColumnInfo {
|
|
||||||
name: "uuid".to_string(),
|
|
||||||
data_type: "varchar(36)".to_string(),
|
|
||||||
is_nullable: false,
|
|
||||||
column_default: None,
|
|
||||||
is_primary_key: false,
|
|
||||||
extra: None,
|
|
||||||
comment: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
database_type: Some(DatabaseType::Mysql),
|
DatabaseType::Mysql,
|
||||||
schema: None,
|
None,
|
||||||
table_name: "users".to_string(),
|
"users",
|
||||||
columns: vec![old_pk, new_pk],
|
vec![old_pk, new_pk],
|
||||||
indexes: Vec::new(),
|
));
|
||||||
foreign_keys: Vec::new(),
|
|
||||||
triggers: Vec::new(),
|
|
||||||
table_comment: None,
|
|
||||||
original_table_comment: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(result.warnings, Vec::<String>::new());
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2666,65 +2909,57 @@ fn builds_mysql_alter_table_change_primary_key() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_no_statements_when_primary_key_unchanged() {
|
fn builds_no_statements_when_primary_key_unchanged() {
|
||||||
let mut id = column("id");
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
id.data_type = "integer".to_string();
|
DatabaseType::Postgres,
|
||||||
id.is_nullable = false;
|
None,
|
||||||
id.is_primary_key = true;
|
"users",
|
||||||
id.original = Some(ColumnInfo {
|
vec![existing_pk_column("id", "integer", true, true)],
|
||||||
name: "id".to_string(),
|
));
|
||||||
data_type: "integer".to_string(),
|
|
||||||
is_nullable: false,
|
|
||||||
column_default: None,
|
|
||||||
is_primary_key: true,
|
|
||||||
extra: None,
|
|
||||||
comment: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
|
||||||
database_type: Some(DatabaseType::Postgres),
|
|
||||||
schema: None,
|
|
||||||
table_name: "users".to_string(),
|
|
||||||
columns: vec![id],
|
|
||||||
indexes: Vec::new(),
|
|
||||||
foreign_keys: Vec::new(),
|
|
||||||
triggers: Vec::new(),
|
|
||||||
table_comment: None,
|
|
||||||
original_table_comment: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(result.warnings, Vec::<String>::new());
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
assert!(result.statements.is_empty());
|
assert!(result.statements.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warns_sqlite_cannot_alter_primary_key() {
|
fn rename_only_primary_key_column_does_not_emit_primary_key_ddl() {
|
||||||
let mut id = column("id");
|
let mut id = existing_pk_column("id_new", "integer", true, true);
|
||||||
id.data_type = "integer".to_string();
|
id.original.as_mut().unwrap().name = "id".to_string();
|
||||||
id.is_nullable = false;
|
|
||||||
id.is_primary_key = true;
|
|
||||||
id.original = Some(ColumnInfo {
|
|
||||||
name: "id".to_string(),
|
|
||||||
data_type: "integer".to_string(),
|
|
||||||
is_nullable: false,
|
|
||||||
column_default: None,
|
|
||||||
is_primary_key: false,
|
|
||||||
extra: None,
|
|
||||||
comment: None,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
database_type: Some(DatabaseType::Sqlite),
|
DatabaseType::Dameng,
|
||||||
schema: None,
|
Some("SYSDBA"),
|
||||||
table_name: "users".to_string(),
|
"users",
|
||||||
columns: vec![id],
|
vec![id],
|
||||||
indexes: Vec::new(),
|
));
|
||||||
foreign_keys: Vec::new(),
|
|
||||||
triggers: Vec::new(),
|
// Membership is tracked by draft id, so rename alone is not a PK change.
|
||||||
table_comment: None,
|
assert_eq!(result.warnings, Vec::<String>::new());
|
||||||
original_table_comment: None,
|
assert!(!result.statements.iter().any(|sql| sql.contains("PRIMARY KEY")));
|
||||||
});
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warns_sqlite_cannot_alter_primary_key() {
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::Sqlite,
|
||||||
|
None,
|
||||||
|
"users",
|
||||||
|
vec![existing_pk_column("id", "integer", false, true)],
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(result.statements, Vec::<String>::new());
|
||||||
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
assert!(result.warnings[0].contains("primary key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warns_sqlserver_cannot_alter_primary_key_without_drop_strategy() {
|
||||||
|
// alter_primary_key is false for SQL Server; fail closed (no partial ADD-only SQL).
|
||||||
|
let result = build_table_structure_change_sql(structure_change_options(
|
||||||
|
DatabaseType::SqlServer,
|
||||||
|
Some("dbo"),
|
||||||
|
"users",
|
||||||
|
vec![existing_pk_column("id", "int", true, false)],
|
||||||
|
));
|
||||||
|
|
||||||
assert_eq!(result.statements, Vec::<String>::new());
|
assert_eq!(result.statements, Vec::<String>::new());
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
|||||||
Reference in new issue
Block a user