fix(sql): keep required identifier quotes per dialect

This commit is contained in:
zipg
2026-09-14 17:53:28 +08:00
committed by GitHub
parent 608633edff
commit 28e2568a8f
6 changed files with 132 additions and 15 deletions
@@ -3121,6 +3121,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
objectType: sqlObjectNavigationSourceKind(table),
};
let sqlContent: string | undefined;
const formatDialect = props.formatDialect ?? sqlFormatDialectForDbType(props.databaseType);
let metadataLoadFailed = false;
// The persisted display DDL is canonical across the full-page and hover
@@ -3135,7 +3136,6 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
// (the same one the sidebar/object-source viewers use). Tables keep
// the aligned column layout from reformatHoverDdl.
const isViewObject = objectMetadataRequest.objectType === "VIEW" || objectMetadataRequest.objectType === "MATERIALIZED_VIEW";
const formatDialect = props.formatDialect ?? sqlFormatDialectForDbType(props.databaseType);
const formatted = isViewObject ? await formatSqlForDisplay(rawDdl, formatDialect, settingsStore.editorSettings.sqlFormatter) : reformatHoverDdl(rawDdl, quoteQualifiedName(hoverQualifiedName));
sqlContent = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
}
@@ -3170,6 +3170,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
}
if (fullColumns.length > 0) {
sqlContent = buildHoverTableSql(quoteQualifiedName(hoverQualifiedName), fullColumns, fullIndexes, tableComment);
if (!settingsStore.editorSettings.generateSqlQuoteIdentifiers) sqlContent = omitDdlIdentifierQuotes(sqlContent, formatDialect);
metadataLoadFailed = false;
}
}
@@ -27,7 +27,7 @@ import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect
import { useToast } from "@/composables/useToast";
import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sql/sqlHighlighter";
import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript";
import { formatGeneratedDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { formatGeneratedDdlIdentifierQuotes, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { copyToClipboard } from "@/lib/common/clipboard";
import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
@@ -346,7 +346,9 @@ async function fetchDdl(force = false) {
ddlLoading.value = true;
try {
const { ddl } = await loadObjectDdl(ddlRequest(), { force });
ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter);
const dialect = sqlFormatDialectForDbType(databaseType.value);
const formatted = await formatSqlForDisplay(ddl, dialect, settingsStore.editorSettings.sqlFormatter);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
ddlFetched.value = true;
} catch (e: any) {
ddlContent.value = `-- Error: ${e?.message || e}`;
@@ -1500,7 +1502,9 @@ async function hydrateRestoredDraftFromDatabase() {
if (databaseType.value === "manticoresearch" && tableMetadataCapabilities.value.ddl) {
try {
const { ddl } = await loadObjectDdl({ connectionId, database, schema, tableName, catalog });
ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter);
const dialect = sqlFormatDialectForDbType(databaseType.value);
const formatted = await formatSqlForDisplay(ddl, dialect, settingsStore.editorSettings.sqlFormatter);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
ddlFetched.value = true;
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
} catch {
@@ -2158,7 +2162,9 @@ async function loadStructure(
if (databaseType.value === "manticoresearch" && tableMetadataCapabilities.value.ddl) {
try {
const { ddl } = await loadObjectDdl({ connectionId, database, schema, tableName, catalog }, { force: options.forceDdl });
ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter);
const dialect = sqlFormatDialectForDbType(databaseType.value);
const formatted = await formatSqlForDisplay(ddl, dialect, settingsStore.editorSettings.sqlFormatter);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
ddlFetched.value = true;
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
} catch {
+27 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { qualifiedTableName, qualifyTableReferencesInSql, quoteTableDataIdentifier, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { encodeSqlServerLinkedSchema } from "@/lib/database/sqlServerLinkedServers";
import { qualifiedTableName, qualifyTableReferencesInSql, quoteTableDataIdentifier, quoteTableIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/tableSelectSql";
describe("qualifiedTableName — Doris/StarRocks multi-catalog", () => {
it("prefixes external catalog for Doris (no schema)", () => {
@@ -119,6 +120,13 @@ describe("qualifiedTableName — SQLite attached databases", () => {
});
});
describe("qualifiedTableName — SQL Server linked servers", () => {
it("retains required brackets when optional quoting is disabled", () => {
const schema = encodeSqlServerLinkedSchema({ server: "REMOTE SERVER", catalog: "sales", schema: "dbo" });
expect(qualifiedTableName({ databaseType: "sqlserver", schema, tableName: "order", quoteIdentifiers: false })).toBe("[REMOTE SERVER].sales.dbo.[order]");
});
});
describe("qualifiedTableName — schema-aware JDBC profiles", () => {
it("qualifies Phoenix tables without forcing identifier quotes", () => {
expect(qualifiedTableName({ databaseType: "jdbc", driverProfile: "phoenix", schema: "DEMO", tableName: "STUDENT" })).toBe("DEMO.STUDENT");
@@ -145,6 +153,24 @@ describe("qualifiedTableName — GBase 8s", () => {
});
describe("quoteTableIdentifier", () => {
it("keeps only required quotes when identifier quoting is disabled", () => {
expect(quoteTableIdentifierIfNeeded("oracle", "DBX_TEST")).toBe("DBX_TEST");
expect(quoteTableIdentifierIfNeeded("oracle", "Order")).toBe('"Order"');
expect(quoteTableIdentifierIfNeeded("oracle", "ORDER")).toBe('"ORDER"');
expect(quoteTableIdentifierIfNeeded("dameng", "DBX_TEST")).toBe("DBX_TEST");
expect(quoteTableIdentifierIfNeeded("dameng", "order detail")).toBe('"order detail"');
expect(quoteTableIdentifierIfNeeded("postgres", "dbx_test")).toBe("dbx_test");
expect(quoteTableIdentifierIfNeeded("postgres", "Order")).toBe('"Order"');
expect(quoteTableIdentifierIfNeeded("mysql", "dbx_test")).toBe("dbx_test");
expect(quoteTableIdentifierIfNeeded("mysql", "order")).toBe("`order`");
});
it("uses a JDBC driver's reported quote for required identifiers", () => {
expect(quoteTableIdentifierIfNeeded("jdbc", "student", '"')).toBe("student");
expect(quoteTableIdentifierIfNeeded("jdbc", "Order", '"')).toBe('"Order"');
expect(quoteTableIdentifierIfNeeded("jdbc", "order detail", "`")).toBe("`order detail`");
});
it("backtick-quotes mysql identifiers", () => {
expect(quoteTableIdentifier("mysql", "orders")).toBe("`orders`");
});
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { buildTableSelectTemplate } from "@/lib/table/tableSqlTemplates";
import { buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
describe("buildTableSelectTemplate", () => {
it("can omit identifier quotes for a new-query table reference", () => {
@@ -13,4 +13,32 @@ describe("buildTableSelectTemplate", () => {
}),
).toBe("SELECT *\nFROM public.dbx_smoke;");
});
it("keeps required quotes for columns when identifier quoting is disabled", () => {
expect(
buildTableSelectTemplate({
databaseType: "dameng",
tableName: "DBX_TEST",
columns: [
{ name: "ORDER", data_type: "VARCHAR" },
{ name: "CUSTOMER_NAME", data_type: "VARCHAR" },
],
quoteIdentifiers: false,
}),
).toBe('SELECT "ORDER", CUSTOMER_NAME\nFROM DBX_TEST;');
});
it("applies the same identifier policy to update templates", () => {
expect(
buildTableUpdateTemplate({
databaseType: "oracle",
tableName: "DBX_TEST",
columns: [
{ name: "ID", data_type: "NUMBER", is_primary_key: true },
{ name: "ORDER", data_type: "VARCHAR" },
],
quoteIdentifiers: false,
}),
).toBe("UPDATE DBX_TEST\nSET \"ORDER\" = 'ORDER_value'\nWHERE ID = 0;");
});
});
+61 -5
View File
@@ -3,7 +3,7 @@ import { isSchemaAware, usesDatabaseObjectTreeMode } from "@/lib/database/databa
import { jdbcDriverProfileUsesSchemaQualification } from "@/lib/database/jdbcDialect";
import * as api from "@/lib/backend/api.ts";
import { parseSqlServerLinkedSchema, sqlServerLinkedTableName } from "@/lib/database/sqlServerLinkedServers.ts";
import { isExplicitlyQuotedSqlIdentifier, quoteGaussDbJdbcIdentifier } from "@/lib/sql/sqlIdentifier.ts";
import { isExplicitlyQuotedSqlIdentifier, quoteGaussDbJdbcIdentifier, requiresDamengIdentifierQuote, requiresMysqlIdentifierQuote, requiresOracleIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier.ts";
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
import { sqlSemanticTableNameSpans } from "@/lib/sql/semantic/model";
import { tokenIsIdentifier, tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens";
@@ -33,7 +33,7 @@ export interface BuildTableSelectSqlOptions {
database?: string;
/** Include the active database when this dialect supports `database.table` references. */
includeDatabaseName?: boolean;
/** Emit bare identifiers instead of dialect-specific identifier quotes. */
/** Omit optional identifier quotes while retaining quotes required by the dialect. */
quoteIdentifiers?: boolean;
}
@@ -105,14 +105,68 @@ export function quoteTableDataIdentifier(databaseType: DatabaseType | undefined,
return quoteTableIdentifier(databaseType, name);
}
function quoteWithDelimiter(name: string, delimiter: string): string {
if (delimiter === "[") return `[${name.replace(/\]/g, "]]")}]`;
return `${delimiter}${name.replaceAll(delimiter, delimiter + delimiter)}${delimiter}`;
}
function requiresIdentifierQuote(databaseType: DatabaseType | undefined, name: string, identifierQuote?: string): boolean {
if (isExplicitlyQuotedSqlIdentifier(name)) return false;
switch (databaseType) {
case "mysql":
case "clickhouse":
case "hive":
case "argo":
case "kyuubi":
case "impala":
case "spark":
case "databricks":
case "databend":
case "tdengine":
case "access":
case "doris":
case "starrocks":
case "goldendb":
return requiresMysqlIdentifierQuote(name);
case "oracle":
return requiresOracleIdentifierQuote(name);
case "dameng":
return requiresDamengIdentifierQuote(name);
case "postgres":
case "gaussdb":
case "opengauss":
case "kingbase":
return identifierQuote === "`" ? requiresMysqlIdentifierQuote(name) : requiresPostgresIdentifierQuote(name);
case "sqlserver":
return requiresMysqlIdentifierQuote(name);
case "jdbc":
return identifierQuote === "`" ? requiresMysqlIdentifierQuote(name) : requiresPostgresIdentifierQuote(name);
default:
return requiresMysqlIdentifierQuote(name);
}
}
/**
* Omits optional identifier quotes while preserving quotes needed for reserved,
* mixed-case, or otherwise non-bare identifiers.
*/
export function quoteTableIdentifierIfNeeded(databaseType: DatabaseType | undefined, name: string, identifierQuote?: string): string {
if (isExplicitlyQuotedSqlIdentifier(name) || !requiresIdentifierQuote(databaseType, name, identifierQuote)) return name;
if (databaseType === "jdbc" && identifierQuote) return quoteWithDelimiter(name, identifierQuote);
if ((databaseType === "postgres" || databaseType === "gaussdb" || databaseType === "opengauss" || databaseType === "kingbase") && identifierQuote) {
return quoteGaussDbJdbcIdentifier(name, identifierQuote);
}
return quoteTableIdentifier(databaseType, name);
}
function quoteCypherIdentifier(name: string): string {
return `\`${name.replace(/`/g, "``")}\``;
}
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "driverProfile" | "identifierQuote" | "schema" | "tableName" | "catalog" | "database" | "includeDatabaseName" | "quoteIdentifiers">): string {
const { databaseType, driverProfile, identifierQuote, schema, tableName, catalog, database, includeDatabaseName, quoteIdentifiers } = options;
const quoteTable = (name: string) => (quoteIdentifiers === false ? name : quoteTableIdentifier(databaseType, name));
const quoteTableData = (name: string) => (quoteIdentifiers === false ? name : quoteTableDataIdentifier(databaseType, name, identifierQuote));
const quoteTable = (name: string) => (quoteIdentifiers === false ? quoteTableIdentifierIfNeeded(databaseType, name, identifierQuote) : quoteTableIdentifier(databaseType, name));
const quoteTableData = (name: string) => (quoteIdentifiers === false ? quoteTableIdentifierIfNeeded(databaseType, name, identifierQuote) : quoteTableDataIdentifier(databaseType, name, identifierQuote));
if (databaseType === "informix" && driverProfile?.trim().toLowerCase() === "gbase8s") {
return quoteTableData(tableName);
}
@@ -175,7 +229,9 @@ export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "da
if ((isSchemaAware(databaseType) || databaseType === "sqlite") && !usesDatabaseObjectTreeMode(databaseType) && schema) {
if (databaseType === "sqlserver") {
const linked = parseSqlServerLinkedSchema(schema);
if (linked) return quoteIdentifiers === false ? [linked.server, linked.catalog, linked.schema, tableName].join(".") : sqlServerLinkedTableName(linked, tableName);
if (linked) {
return quoteIdentifiers === false ? [linked.server, linked.catalog, linked.schema, tableName].map((name) => quoteTableIdentifierIfNeeded(databaseType, name)).join(".") : sqlServerLinkedTableName(linked, tableName);
}
}
return `${quoteTable(schema)}.${quoteTable(tableName)}`;
}
@@ -1,5 +1,5 @@
import type { ColumnInfo, DatabaseType } from "@/types/database";
import { metricRangeQuery, qualifiedTableName, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { metricRangeQuery, qualifiedTableName, quoteTableDataIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/tableSelectSql";
export interface TableSqlTemplateOptions {
databaseType?: DatabaseType;
@@ -9,7 +9,7 @@ export interface TableSqlTemplateOptions {
catalog?: string;
database?: string;
includeDatabaseName?: boolean;
/** Emit bare table identifiers instead of dialect-specific quotes. */
/** Omit optional identifier quotes while retaining quotes required by the dialect. */
quoteIdentifiers?: boolean;
tableName: string;
columns?: ColumnInfo[];
@@ -116,7 +116,7 @@ function templateTableName(options: TableSqlTemplateOptions): string {
}
function templateIdentifier(options: TableSqlTemplateOptions, name: string): string {
return quoteTableDataIdentifier(options.databaseType, name, options.identifierQuote);
return options.quoteIdentifiers === false ? quoteTableIdentifierIfNeeded(options.databaseType, name, options.identifierQuote) : quoteTableDataIdentifier(options.databaseType, name, options.identifierQuote);
}
function buildWhereClause(options: TableSqlTemplateOptions, primaryKeys: ColumnInfo[]): string {