mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(sql): keep required identifier quotes per dialect
This commit is contained in:
@@ -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
@@ -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;");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user