fix(structure): keep Oracle case-sensitive identifiers quoted when quoting off

This commit is contained in:
zipg authored and GitHub committed 2026-09-21 12:52:44 +08:00
1 parent f8ee34f937
commit f19d842b8f
8 files changed
+140 -12

No files matched your search

@@ -248,7 +248,7 @@ import TableStructureEditor from "@/components/structure/TableStructureEditor.vu
const mountedApps: App[] = [];
function draft(isPrimaryKey = false, identity?: { seed: number; increment: number }) {
function draft(isPrimaryKey = false, identity?: { seed: number; increment: number }, columnName = "id", foreignKeyRefTable?: string) {
const isNullable = identity ? false : !isPrimaryKey;
return {
initialized: true,
@@ -259,7 +259,7 @@ function draft(isPrimaryKey = false, identity?: { seed: number; increment: numbe
columns: [
{
id: "existing:id",
name: "id",
name: columnName,
dataType: "INT",
isNullable,
defaultValue: "",
@@ -267,7 +267,7 @@ function draft(isPrimaryKey = false, identity?: { seed: number; increment: numbe
isPrimaryKey,
extra: identity ? { autoIncrement: true, identity: { ...identity } } : {},
original: {
name: "id",
name: columnName,
data_type: "INT",
is_nullable: isNullable,
column_default: null,
@@ -280,12 +280,39 @@ function draft(isPrimaryKey = false, identity?: { seed: number; increment: numbe
},
],
indexes: [],
foreignKeys: [],
foreignKeys: foreignKeyRefTable
? [
{
id: "existing:fk",
name: "FK_T_9649",
column: columnName,
refSchema: "",
refTable: foreignKeyRefTable,
refColumn: "ID",
onUpdate: "",
onDelete: "",
original: {
name: "FK_T_9649",
column: columnName,
ref_schema: null,
ref_table: foreignKeyRefTable,
ref_column: "ID",
on_update: null,
on_delete: null,
},
markedForDrop: false,
},
]
: [],
triggers: [],
};
}
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 } } = {}) {
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 }; tableName?: string; columnName?: string; foreignKeyRefTable?: string } = {},
) {
mocks.connection.db_type = databaseType;
mocks.connection.name = databaseType;
mocks.connection.driver_label = databaseType;
@@ -299,8 +326,8 @@ async function mountEditor(databaseType: "sqlserver" | "postgres" | "sqlite" | "
connectionId: mocks.connection.id,
database: options.database ?? "test",
schema: "SYSDBA",
tableName: "users",
draft: draft(isPrimaryKey, options.identity),
tableName: options.tableName ?? "users",
draft: draft(isPrimaryKey, options.identity, options.columnName, options.foreignKeyRefTable),
});
mountedApps.push(app);
app.mount(root);
@@ -597,6 +624,58 @@ describe("TableStructureEditor primary key editing", () => {
expect(root.textContent).not.toContain(`"${schema}"."demo_table"`);
});
it("keeps case-sensitive Oracle identifiers quoted when quoting is disabled (#9649)", async () => {
mocks.editorSettings.generateSqlQuoteIdentifiers = false;
const root = await mountEditor("oracle", false, { tableName: "T_9649", columnName: "cName" });
mocks.buildTableStructureChangeSql.mockResolvedValueOnce({
statements: ['ALTER TABLE "DBX_TEST"."T_9649" MODIFY ("cName" VARCHAR2(120 BYTE))', 'ALTER TABLE "DBX_TEST"."T_9649" ADD ("cNabcs" clob)', 'COMMENT ON COLUMN "DBX_TEST"."T_9649"."cName" IS \'中文名\''],
warnings: [],
});
buttonWithText(root, "structureEditor.addColumn").click();
await vi.waitFor(() => expect(root.textContent).toContain('MODIFY ("cName" VARCHAR2(120 BYTE))'));
expect(root.textContent).toContain('ADD ("cNabcs" clob)');
expect(root.textContent).toContain("COMMENT ON COLUMN DBX_TEST.T_9649.\"cName\" IS '中文名'");
expect(root.textContent).not.toContain("CNAME");
});
it("keeps a quoted Oracle table name when quoting is disabled (#9649)", async () => {
mocks.editorSettings.generateSqlQuoteIdentifiers = false;
const root = await mountEditor("oracle", false, { tableName: "t_9649_lower", columnName: "ID" });
mocks.buildTableStructureChangeSql.mockResolvedValueOnce({ statements: ['ALTER TABLE "DBX_TEST"."t_9649_lower" MODIFY ("ID" NUMBER(12))'], warnings: [] });
buttonWithText(root, "structureEditor.addColumn").click();
await vi.waitFor(() => expect(root.textContent).toContain('ALTER TABLE DBX_TEST."t_9649_lower" MODIFY (ID NUMBER(12))'));
expect(root.textContent).not.toContain("T_9649_LOWER");
});
it("keeps case-sensitive foreign key referenced names quoted when quoting is disabled (#9649)", async () => {
mocks.editorSettings.generateSqlQuoteIdentifiers = false;
const root = await mountEditor("oracle", false, { tableName: "T_9649", columnName: "ID", foreignKeyRefTable: "cRefTab" });
mocks.buildTableStructureChangeSql.mockResolvedValueOnce({
statements: ['ALTER TABLE "DBX_TEST"."T_9649" ADD CONSTRAINT "FK_T_9649" FOREIGN KEY ("ID") REFERENCES "DBX_TEST"."cRefTab"("ID")'],
warnings: [],
});
buttonWithText(root, "structureEditor.addColumn").click();
await vi.waitFor(() => expect(root.textContent).toContain('REFERENCES DBX_TEST."cRefTab"(ID)'));
expect(root.textContent).not.toContain("CREFTAB");
});
it("still folds plain Oracle identifiers when quoting is disabled (#8997)", async () => {
mocks.editorSettings.generateSqlQuoteIdentifiers = false;
const root = await mountEditor("oracle", false, { tableName: "TEST", columnName: "ID" });
mocks.buildTableStructureChangeSql.mockResolvedValueOnce({ statements: ['ALTER TABLE "SYSTEM"."TEST" ADD ("NEW_COL" VARCHAR2(20))'], warnings: [] });
buttonWithText(root, "structureEditor.addColumn").click();
await vi.waitFor(() => expect(root.textContent).toContain("ALTER TABLE SYSTEM.TEST ADD (NEW_COL VARCHAR2(20))"));
expect(root.textContent).not.toContain('"NEW_COL"');
});
it("keeps SQLite rebuild SQL aligned with the guarded apply plan", async () => {
mocks.editorSettings.generateSqlQuoteIdentifiers = false;
const root = await mountEditor("sqlite");
@@ -373,7 +373,7 @@ function scheduleDdlEditorInit() {
function formatDdlForDisplay(sql: string, dialect: SqlFormatDialect, generated = false): string {
const unqualified = applyDdlDatabaseQualifier(sql, dialect, databaseType.value, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.database, props.catalog);
if (settingsStore.editorSettings.generateSqlQuoteIdentifiers) return unqualified;
return generated ? formatGeneratedDdlIdentifierQuotes(unqualified, dialect, false) : omitDdlIdentifierQuotes(unqualified, dialect);
return generated ? formatGeneratedDdlIdentifierQuotes(unqualified, dialect, false, { preserveCaseSensitiveIdentifiers: tableStoresCaseSensitiveIdentifiers.value }) : omitDdlIdentifierQuotes(unqualified, dialect);
}
function ddlRequest() {
@@ -1165,6 +1165,26 @@ const triggerEventOptions = ["INSERT", "UPDATE", "DELETE"];
const metadataSchema = computed(() => connectionObjectTreeQuerySchema(connection.value, props.database, props.schema));
const refreshVersion = computed(() => (props.connectionId && props.tableName ? queryStore.tableStructureRefreshVersion(props.connectionId, props.database, props.schema, props.tableName) : 0));
const isCreateMode = computed(() => !props.tableName);
/**
* Whether the edited table already stores a lowercase or mixed-case identifier.
* Oracle folds bare identifiers to uppercase, so dequoting such a name points at
* a column that does not exist (`CNAME` instead of `"cName"`, ORA-00904) or
* silently creates a differently-cased column for a newly added field (#9649).
* Tables whose names are all in the dialect's default case keep the
* PL/SQL-Developer-style folding requested in #8997.
*/
const tableStoresCaseSensitiveIdentifiers = computed(() => {
if (isCreateMode.value) return false;
const databaseInfo = connection.value?.database_info;
const requiresQuotesForIdentity = (name: string | null | undefined) => !!name && tableStructureIdentifierComparisonKey(name, databaseType.value, databaseInfo).startsWith("quoted:");
// 外键的引用侧(被引用 schema/表/列)与约束名一样进入生成的 REFERENCES
// 子句,同样需要纳入大小写敏感扫描,否则会被折叠改写身份。
const foreignKeyIdentifiers = foreignKeys.value.flatMap((foreignKey) => {
const original = foreignKey.original;
return original ? [original.name, original.ref_schema, original.ref_table, original.ref_column] : [];
});
return [props.tableName, ...columns.value.map((column) => column.original?.name), ...indexes.value.map((index) => index.original?.name), ...foreignKeyIdentifiers, ...triggers.value.map((trigger) => trigger.original?.name)].some(requiresQuotesForIdentity);
});
const usesSqliteRebuildStrategy = computed(() => !isCreateMode.value && structureCapabilities.value.alterStrategy === "sqlite-rebuild");
const hasSqliteTypeChange = computed(() => usesSqliteRebuildStrategy.value && hasExistingColumnTypeChange(columns.value));
const canAddColumn = computed(() => canAddTableStructureColumn(databaseType.value, isCreateMode.value));