fix(gaussdb): quote identifiers by compatibility mode
This commit is contained in:
28 files changed
+1207
-134
No files matched your search
@@ -98,6 +98,7 @@ import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import { applyHiveKerberosSubmitConfig, hiveKerberosFormConfig, type HiveKerberosAuthMode } from "@/lib/database/hiveKerberosOptions";
|
||||
import { hasCloudflareD1Credentials, isCloudflareD1Connection, normalizeCloudflareD1Connection } from "@/lib/connection/cloudflareD1";
|
||||
import { buildElasticsearchExternalConfig, elasticsearchConnectionModeFromConfig, elasticsearchConnectivityCheckPathFromConfig, elasticsearchKibanaBasePathFromConfig, type ElasticsearchConnectionMode } from "@/lib/connection/elasticsearchKibanaProxy";
|
||||
import { gaussdbIdentifierQuoteStyle, setGaussdbIdentifierQuoteStyle, supportsGaussdbIdentifierQuoteStyle, type GaussdbIdentifierQuoteStyle } from "@/lib/database/jdbcDialect";
|
||||
|
||||
type DbOption = { value: string; label: string };
|
||||
type DbCategoryKey = "sql" | "analytics" | "domestic" | "lightweight" | "document" | "graph_ai" | "timeseries" | "mq" | "registry_config";
|
||||
@@ -450,6 +451,14 @@ function sshLayersForConfig(config: LegacyConnectionConfig): SshTunnelConfig[] {
|
||||
|
||||
const form = ref(defaultForm());
|
||||
const noteTextareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const showGaussdbIdentifierQuoteStyle = computed(() => supportsGaussdbIdentifierQuoteStyle(form.value));
|
||||
const gaussdbQuoteStyle = computed<GaussdbIdentifierQuoteStyle>({
|
||||
get: () => gaussdbIdentifierQuoteStyle(form.value),
|
||||
set: (style) => {
|
||||
setGaussdbIdentifierQuoteStyle(form.value, style);
|
||||
resetTestState();
|
||||
},
|
||||
});
|
||||
|
||||
function resizeNoteTextarea() {
|
||||
const textarea = noteTextareaRef.value;
|
||||
@@ -3220,6 +3229,10 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
config.external_config = buildElasticsearchExternalConfig(elasticsearchConnectionMode.value, elasticsearchKibanaBasePath.value, elasticsearchConnectivityCheckPath.value);
|
||||
} else if (config.db_type === "sqlserver") {
|
||||
config.external_config = sqlServerPortExplicitFromConfig(config) ? { portExplicit: true } : undefined;
|
||||
} else if (supportsGaussdbIdentifierQuoteStyle(config)) {
|
||||
const style = gaussdbIdentifierQuoteStyle(config);
|
||||
config.external_config = undefined;
|
||||
setGaussdbIdentifierQuoteStyle(config, style);
|
||||
} else {
|
||||
config.external_config = undefined;
|
||||
}
|
||||
@@ -6535,6 +6548,22 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<TabsContent value="advanced" class="m-0 min-h-0 flex-1 overflow-hidden">
|
||||
<div class="connection-form-body grid h-full min-h-0 gap-4 overflow-y-auto pt-4 pr-2">
|
||||
<div v-if="showGaussdbIdentifierQuoteStyle" class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.gaussdbIdentifierQuoteStyle") }}</Label>
|
||||
<div class="col-span-3 grid gap-1">
|
||||
<Select v-model="gaussdbQuoteStyle">
|
||||
<SelectTrigger class="h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto">{{ t("connection.gaussdbIdentifierQuoteAuto") }}</SelectItem>
|
||||
<SelectItem value="double">{{ t("connection.gaussdbIdentifierQuoteDouble") }}</SelectItem>
|
||||
<SelectItem value="backtick">{{ t("connection.gaussdbIdentifierQuoteBacktick") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.gaussdbIdentifierQuoteHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.connectTimeout") }}</Label>
|
||||
<Input v-model.number="form.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-3" />
|
||||
|
||||
@@ -503,6 +503,11 @@ export default {
|
||||
sqlServerLegacyCompatibilityModeEnable: "Enable",
|
||||
sqlServerLegacyCompatibilityModeHint: "For SQL Server instances limited to TLS 1.0, login-only encryption, or no encryption.\nSome features may be limited; use only on trusted networks.",
|
||||
sqlServerLegacyCompatibilityComponent: "SQL Server legacy compatibility component",
|
||||
gaussdbIdentifierQuoteStyle: "Identifier compatibility",
|
||||
gaussdbIdentifierQuoteAuto: "Auto (driver/default)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL (double quotes ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M (backticks `)",
|
||||
gaussdbIdentifierQuoteHint: "Auto uses JDBC driver metadata or the native GaussDB default. Select a mode manually only when generated SQL uses the wrong identifier quotes.",
|
||||
saveAndConnect: "Save & Connect",
|
||||
save: "Save",
|
||||
editTitle: "Edit Connection",
|
||||
|
||||
@@ -404,6 +404,11 @@ export default withEnglishFallback({
|
||||
sqlServerLegacyCompatibilityModeEnable: "Activar",
|
||||
sqlServerLegacyCompatibilityModeHint: "Para instancias de SQL Server limitadas a TLS 1.0, cifrado solo al iniciar sesión o sin cifrado.\nAlgunas funciones pueden estar limitadas; úsalo solo en redes de confianza.",
|
||||
sqlServerLegacyCompatibilityComponent: "Componente de compatibilidad heredada de SQL Server",
|
||||
gaussdbIdentifierQuoteStyle: "Compatibilidad de identificadores",
|
||||
gaussdbIdentifierQuoteAuto: "Automático (controlador/predeterminado)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL (comillas dobles ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M (acentos graves `)",
|
||||
gaussdbIdentifierQuoteHint: "El modo automático usa los metadatos del controlador JDBC o el valor predeterminado de GaussDB nativo. Seleccione un modo manualmente solo si el SQL generado usa comillas de identificador incorrectas.",
|
||||
saveAndConnect: "Guardar y conectar",
|
||||
save: "Guardar",
|
||||
editTitle: "Editar conexión",
|
||||
|
||||
@@ -403,6 +403,11 @@ export default withEnglishFallback({
|
||||
sqlServerLegacyCompatibilityModeEnable: "Abilita",
|
||||
sqlServerLegacyCompatibilityModeHint: "Per istanze di SQL Server limitate a TLS 1.0, crittografia solo all'accesso o nessuna crittografia.\nAlcune funzionalità potrebbero essere limitate; usala solo su reti attendibili.",
|
||||
sqlServerLegacyCompatibilityComponent: "Componente di compatibilità legacy SQL Server",
|
||||
gaussdbIdentifierQuoteStyle: "Compatibilità identificatori",
|
||||
gaussdbIdentifierQuoteAuto: "Automatico (driver/predefinito)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL (virgolette doppie ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M (accenti gravi `)",
|
||||
gaussdbIdentifierQuoteHint: "La modalità automatica usa i metadati del driver JDBC o il valore predefinito di GaussDB nativo. Seleziona manualmente una modalità solo se l'SQL generato usa virgolette errate per gli identificatori.",
|
||||
saveAndConnect: "Salva & Connetti",
|
||||
save: "Salva",
|
||||
editTitle: "Modifica Connessione",
|
||||
|
||||
@@ -397,6 +397,11 @@ export default withEnglishFallback({
|
||||
sqlServerLegacyCompatibilityModeEnable: "有効にする",
|
||||
sqlServerLegacyCompatibilityModeHint: "TLS 1.0、ログイン時のみの暗号化、または暗号化なしに限定された SQL Server への接続に使用します。\n一部の機能が制限される場合があるため、信頼できるネットワークでのみ使用してください。",
|
||||
sqlServerLegacyCompatibilityComponent: "SQL Server レガシー互換コンポーネント",
|
||||
gaussdbIdentifierQuoteStyle: "識別子互換モード",
|
||||
gaussdbIdentifierQuoteAuto: "自動(ドライバー/既定値)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL(二重引用符 ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M(バッククォート `)",
|
||||
gaussdbIdentifierQuoteHint: "自動モードでは JDBC ドライバーのメタデータまたはネイティブ GaussDB の既定値を使用します。生成された SQL の識別子引用符が誤っている場合のみ手動で選択してください。",
|
||||
saveAndConnect: "保存して接続",
|
||||
save: "保存",
|
||||
editTitle: "接続を編集",
|
||||
|
||||
@@ -404,6 +404,11 @@ export default withEnglishFallback({
|
||||
sqlServerLegacyCompatibilityModeEnable: "Ativar",
|
||||
sqlServerLegacyCompatibilityModeHint: "Para instâncias do SQL Server limitadas a TLS 1.0, criptografia apenas no login ou sem criptografia.\nAlguns recursos podem ser limitados; use apenas em redes confiáveis.",
|
||||
sqlServerLegacyCompatibilityComponent: "Componente de compatibilidade legada do SQL Server",
|
||||
gaussdbIdentifierQuoteStyle: "Compatibilidade de identificadores",
|
||||
gaussdbIdentifierQuoteAuto: "Automático (driver/padrão)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL (aspas duplas ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M (crases `)",
|
||||
gaussdbIdentifierQuoteHint: "O modo automático usa os metadados do driver JDBC ou o padrão do GaussDB nativo. Selecione um modo manualmente apenas quando o SQL gerado usar aspas de identificador incorretas.",
|
||||
saveAndConnect: "Salvar e Conectar",
|
||||
save: "Salvar",
|
||||
editTitle: "Editar Conexão",
|
||||
|
||||
@@ -504,6 +504,11 @@ export default withEnglishFallback({
|
||||
sqlServerLegacyCompatibilityModeEnable: "启用",
|
||||
sqlServerLegacyCompatibilityModeHint: "用于连接仅支持 TLS 1.0、仅登录阶段加密或不加密的 SQL Server;\n部分功能可能受限,请仅在可信网络中使用",
|
||||
sqlServerLegacyCompatibilityComponent: "SQL Server 旧版兼容组件",
|
||||
gaussdbIdentifierQuoteStyle: "标识符兼容模式",
|
||||
gaussdbIdentifierQuoteAuto: "自动(驱动/默认)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL(双引号 ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M(反引号 `)",
|
||||
gaussdbIdentifierQuoteHint: "自动模式使用 JDBC 驱动元数据或原生 GaussDB 默认值。仅当生成的 SQL 使用了错误的标识符引号时手动选择。",
|
||||
saveAndConnect: "保存并连接",
|
||||
save: "保存",
|
||||
editTitle: "编辑连接",
|
||||
|
||||
@@ -403,6 +403,11 @@ export default withEnglishFallback({
|
||||
sqlServerLegacyCompatibilityModeEnable: "啟用",
|
||||
sqlServerLegacyCompatibilityModeHint: "用於連線僅支援 TLS 1.0、僅登入階段加密或未加密的 SQL Server;\n部分功能可能受限,請僅在可信網路中使用",
|
||||
sqlServerLegacyCompatibilityComponent: "SQL Server 舊版相容元件",
|
||||
gaussdbIdentifierQuoteStyle: "識別字相容模式",
|
||||
gaussdbIdentifierQuoteAuto: "自動(驅動程式/預設)",
|
||||
gaussdbIdentifierQuoteDouble: 'Oracle/PostgreSQL(雙引號 ")',
|
||||
gaussdbIdentifierQuoteBacktick: "MySQL/M(反引號 `)",
|
||||
gaussdbIdentifierQuoteHint: "自動模式使用 JDBC 驅動程式中繼資料或原生 GaussDB 預設值。僅在產生的 SQL 使用錯誤的識別字引號時手動選擇。",
|
||||
saveAndConnect: "儲存並連線",
|
||||
save: "儲存",
|
||||
editTitle: "編輯連線",
|
||||
|
||||
+62
-1
@@ -1,5 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, inferJdbcDialect } from "@/lib/database/jdbcDialect";
|
||||
import {
|
||||
connectionObjectTreeNodeSchema,
|
||||
connectionQueryExecutionSchema,
|
||||
connectionShouldLoadIdentifierQuote,
|
||||
connectionUsesDatabaseObjectTreeMode,
|
||||
effectiveDatabaseTypeForConnection,
|
||||
gaussdbIdentifierQuoteOverride,
|
||||
gaussdbIdentifierQuoteStyle,
|
||||
inferJdbcDialect,
|
||||
setGaussdbIdentifierQuoteStyle,
|
||||
supportsGaussdbIdentifierQuoteStyle,
|
||||
} from "@/lib/database/jdbcDialect";
|
||||
|
||||
describe("jdbc dialect inference", () => {
|
||||
it("detects InterSystems IRIS and Caché JDBC connections", () => {
|
||||
@@ -65,6 +76,56 @@ describe("jdbc dialect inference", () => {
|
||||
expect(connectionUsesDatabaseObjectTreeMode(opengaussConnection)).toBe(false);
|
||||
});
|
||||
|
||||
it("loads driver-reported identifier quotes for compatible JDBC connections", () => {
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "jdbc", jdbc_driver_paths: ["/drivers/gaussdb.jar"] })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "jdbc", jdbc_driver_class: "org.opengauss.Driver" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "jdbc", jdbc_driver_class: "org.postgresql.Driver" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "kingbase" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "gaussdb" })).toBe(true);
|
||||
expect(
|
||||
connectionShouldLoadIdentifierQuote({
|
||||
db_type: "jdbc",
|
||||
jdbc_driver_class: "org.postgresql.Driver",
|
||||
external_config: { gaussdbIdentifierQuoteStyle: "backtick" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("recognizes GaussDB reached through PostgreSQL-compatible JDBC drivers", () => {
|
||||
const connection = {
|
||||
db_type: "jdbc" as const,
|
||||
connection_string: "jdbc:postgresql://localhost:5432/postgres",
|
||||
jdbc_driver_class: "org.postgresql.Driver",
|
||||
database_info: { productName: "GaussDB Kernel", driverName: "PostgreSQL JDBC Driver" },
|
||||
};
|
||||
|
||||
expect(inferJdbcDialect(connection)).toBe("gaussdb");
|
||||
expect(effectiveDatabaseTypeForConnection(connection)).toBe("gaussdb");
|
||||
expect(supportsGaussdbIdentifierQuoteStyle(connection)).toBe(true);
|
||||
});
|
||||
|
||||
it("supports persisted GaussDB identifier quote overrides", () => {
|
||||
const native = { db_type: "gaussdb" as const, external_config: undefined as unknown };
|
||||
const jdbc = { db_type: "jdbc" as const, jdbc_driver_paths: ["/drivers/gaussdb.jar"], external_config: { retained: true } as unknown };
|
||||
|
||||
expect(supportsGaussdbIdentifierQuoteStyle(native)).toBe(true);
|
||||
expect(gaussdbIdentifierQuoteStyle(native)).toBe("auto");
|
||||
expect(gaussdbIdentifierQuoteOverride(native)).toBeUndefined();
|
||||
|
||||
setGaussdbIdentifierQuoteStyle(native, "backtick");
|
||||
expect(gaussdbIdentifierQuoteStyle(native)).toBe("backtick");
|
||||
expect(gaussdbIdentifierQuoteOverride(native)).toBe("`");
|
||||
|
||||
setGaussdbIdentifierQuoteStyle(jdbc, "double");
|
||||
expect(jdbc.external_config).toEqual({ retained: true, gaussdbIdentifierQuoteStyle: "double" });
|
||||
expect(gaussdbIdentifierQuoteOverride(jdbc)).toBe('"');
|
||||
expect(connectionShouldLoadIdentifierQuote(jdbc)).toBe(false);
|
||||
|
||||
setGaussdbIdentifierQuoteStyle(jdbc, "auto");
|
||||
expect(jdbc.external_config).toEqual({ retained: true });
|
||||
expect(connectionShouldLoadIdentifierQuote(jdbc)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects Dameng JDBC connections", () => {
|
||||
const damengConnection = {
|
||||
db_type: "jdbc" as const,
|
||||
|
||||
@@ -54,6 +54,42 @@ describe("quoteTableIdentifier", () => {
|
||||
expect(quoteTableDataIdentifier("kingbase", "order detail", "`")).toBe("`order detail`");
|
||||
});
|
||||
|
||||
it("selectively quotes GaussDB JDBC identifiers with the driver-reported quote", () => {
|
||||
expect(quoteTableDataIdentifier("gaussdb", "table_01", '"')).toBe("table_01");
|
||||
expect(quoteTableDataIdentifier("gaussdb", "MixedCase", '"')).toBe('"MixedCase"');
|
||||
expect(quoteTableDataIdentifier("gaussdb", "order", '"')).toBe('"order"');
|
||||
expect(quoteTableDataIdentifier("gaussdb", "order detail", '"')).toBe('"order detail"');
|
||||
expect(quoteTableDataIdentifier("gaussdb", 'already"quoted', '"')).toBe('"already""quoted"');
|
||||
expect(quoteTableDataIdentifier("gaussdb", '"AlreadyQuoted"', '"')).toBe('"AlreadyQuoted"');
|
||||
|
||||
expect(quoteTableDataIdentifier("gaussdb", "table_01", "`")).toBe("table_01");
|
||||
expect(quoteTableDataIdentifier("gaussdb", "MixedCase", "`")).toBe("`MixedCase`");
|
||||
expect(quoteTableDataIdentifier("gaussdb", "order", "`")).toBe("`order`");
|
||||
expect(quoteTableDataIdentifier("gaussdb", "order detail", "`")).toBe("`order detail`");
|
||||
expect(quoteTableDataIdentifier("gaussdb", "already`quoted", "`")).toBe("`already``quoted`");
|
||||
expect(quoteTableDataIdentifier("gaussdb", "`AlreadyQuoted`", "`")).toBe("`AlreadyQuoted`");
|
||||
});
|
||||
|
||||
it("uses detected GaussDB compatibility quotes through PostgreSQL-compatible JDBC dialects", () => {
|
||||
for (const databaseType of ["postgres", "opengauss"] as const) {
|
||||
expect(quoteTableDataIdentifier(databaseType, "table_01", "`")).toBe("table_01");
|
||||
expect(quoteTableDataIdentifier(databaseType, "MixedCase", "`")).toBe("`MixedCase`");
|
||||
expect(quoteTableDataIdentifier(databaseType, "order", "`")).toBe("`order`");
|
||||
expect(quoteTableDataIdentifier(databaseType, "order detail", "`")).toBe("`order detail`");
|
||||
expect(quoteTableDataIdentifier(databaseType, "`AlreadyQuoted`", "`")).toBe("`AlreadyQuoted`");
|
||||
expect(quoteTableDataIdentifier(databaseType, "MixedCase", '"')).toBe('"MixedCase"');
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves native GaussDB and openGauss quoting behavior", () => {
|
||||
expect(quoteTableDataIdentifier("gaussdb", "table_01")).toBe('"table_01"');
|
||||
expect(quoteTableDataIdentifier("gaussdb", "MixedCase")).toBe('"MixedCase"');
|
||||
expect(quoteTableDataIdentifier("gaussdb", '"AlreadyQuoted"')).toBe('"AlreadyQuoted"');
|
||||
expect(quoteTableDataIdentifier("opengauss", "table_01")).toBe('"table_01"');
|
||||
expect(quoteTableDataIdentifier("opengauss", "MixedCase")).toBe('"MixedCase"');
|
||||
expect(quoteTableDataIdentifier("opengauss", '"AlreadyQuoted"')).toBe('"AlreadyQuoted"');
|
||||
});
|
||||
|
||||
it("escapes Kingbase identifiers without maintaining a reserved-word list", () => {
|
||||
expect(quoteTableDataIdentifier("kingbase", "ANALYZE", "`")).toBe("`ANALYZE`");
|
||||
expect(quoteTableDataIdentifier("kingbase", "AUTHORIZATION", '"')).toBe('"AUTHORIZATION"');
|
||||
|
||||
@@ -2,7 +2,11 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database";
|
||||
import { isSchemaAware, usesDatabaseObjectTreeMode, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
|
||||
|
||||
type JdbcDialectConnection = Pick<ConnectionConfig, "db_type"> & Partial<Pick<ConnectionConfig, "driver_profile" | "driver_label" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info">>;
|
||||
type JdbcDialectConnection = Pick<ConnectionConfig, "db_type"> & Partial<Pick<ConnectionConfig, "driver_profile" | "driver_label" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config">>;
|
||||
|
||||
export type GaussdbIdentifierQuoteStyle = "auto" | "double" | "backtick";
|
||||
|
||||
const GAUSSDB_IDENTIFIER_QUOTE_STYLE_KEY = "gaussdbIdentifierQuoteStyle";
|
||||
|
||||
const DATABASE_AS_EXECUTION_SCHEMA_TYPES = new Set<DatabaseType>(["hive", "spark"]);
|
||||
|
||||
@@ -34,7 +38,9 @@ const JDBC_ASE_PROFILE_PATTERNS = [/(?:^|[\s_-])ase(?:$|[\s_-])/i, /\bsap[\s_-]+
|
||||
|
||||
export function inferJdbcDialect(connection?: JdbcDialectConnection): DatabaseType | undefined {
|
||||
if (!connection || connection.db_type !== "jdbc") return undefined;
|
||||
const haystack = [connection.driver_profile, connection.connection_string, connection.jdbc_driver_class, ...(connection.jdbc_driver_paths ?? [])].filter(Boolean).join("\n");
|
||||
const haystack = [connection.driver_profile, connection.driver_label, connection.connection_string, connection.jdbc_driver_class, ...(connection.jdbc_driver_paths ?? []), connection.database_info?.productName, connection.database_info?.serverComment, connection.database_info?.driverName]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (!haystack) return undefined;
|
||||
return JDBC_DIALECT_MATCHERS.find((matcher) => matcher.patterns.some((pattern) => pattern.test(haystack)))?.type;
|
||||
}
|
||||
@@ -57,6 +63,45 @@ export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConne
|
||||
return inferJdbcDialect(connection) ?? "jdbc";
|
||||
}
|
||||
|
||||
export function connectionShouldLoadIdentifierQuote(connection: JdbcDialectConnection | undefined): boolean {
|
||||
if (!connection) return false;
|
||||
if (connection.db_type === "kingbase") return true;
|
||||
if (gaussdbIdentifierQuoteStyle(connection) !== "auto") return false;
|
||||
if (connection.db_type === "gaussdb") return true;
|
||||
if (connection.db_type !== "jdbc") return false;
|
||||
return ["gaussdb", "opengauss", "postgres"].includes(inferJdbcDialect(connection) ?? "");
|
||||
}
|
||||
|
||||
export function supportsGaussdbIdentifierQuoteStyle(connection: JdbcDialectConnection | undefined): boolean {
|
||||
return effectiveDatabaseTypeForConnection(connection) === "gaussdb";
|
||||
}
|
||||
|
||||
export function gaussdbIdentifierQuoteStyle(connection: JdbcDialectConnection | undefined): GaussdbIdentifierQuoteStyle {
|
||||
const external = externalConfigRecord(connection?.external_config);
|
||||
const style = external[GAUSSDB_IDENTIFIER_QUOTE_STYLE_KEY];
|
||||
return style === "double" || style === "backtick" ? style : "auto";
|
||||
}
|
||||
|
||||
export function gaussdbIdentifierQuoteOverride(connection: JdbcDialectConnection | undefined): string | undefined {
|
||||
const style = gaussdbIdentifierQuoteStyle(connection);
|
||||
if (style === "double") return '"';
|
||||
if (style === "backtick") return "`";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setGaussdbIdentifierQuoteStyle(
|
||||
connection: Pick<ConnectionConfig, "db_type"> & Partial<Pick<ConnectionConfig, "driver_profile" | "driver_label" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config">>,
|
||||
style: GaussdbIdentifierQuoteStyle,
|
||||
) {
|
||||
const external = externalConfigRecord(connection.external_config);
|
||||
if (style === "auto") {
|
||||
delete external[GAUSSDB_IDENTIFIER_QUOTE_STYLE_KEY];
|
||||
} else {
|
||||
external[GAUSSDB_IDENTIFIER_QUOTE_STYLE_KEY] = style;
|
||||
}
|
||||
connection.external_config = Object.keys(external).length > 0 ? external : undefined;
|
||||
}
|
||||
|
||||
export function sqlSnippetDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
|
||||
// ASE uses T-SQL snippets, but mapping it globally to SQL Server would also
|
||||
// enable incompatible SQL Server metadata and pagination behavior.
|
||||
@@ -142,3 +187,7 @@ function isJdbcAseProfile(connection?: JdbcDialectConnection): boolean {
|
||||
function isGbase8sProfile(driverProfile?: string): boolean {
|
||||
return driverProfile === "gbase8s";
|
||||
}
|
||||
|
||||
function externalConfigRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record<string, unknown>) } : {};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import { findActiveSqlStatementSpan, tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
|
||||
import type { SqlSemanticBuildOptions, SqlSemanticSpan } from "@/lib/sql/semantic/types";
|
||||
import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
import { requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
|
||||
|
||||
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
|
||||
@@ -2781,124 +2782,11 @@ function unquoteIdentifier(value: string): string {
|
||||
}
|
||||
|
||||
export function quoteSqlIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver"): string {
|
||||
if (dialect !== "postgres" || !requiresPostgresIdentifierQuote(identifier)) return identifier;
|
||||
if (dialect !== "postgres" || !requiresPostgresIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier;
|
||||
return `"${identifier.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
function requiresPostgresIdentifierQuote(identifier: string): boolean {
|
||||
if (!/^[a-z_][a-z0-9_$]*$/.test(identifier)) return true;
|
||||
return POSTGRES_IDENTIFIER_KEYWORDS.has(identifier);
|
||||
}
|
||||
|
||||
// PostgreSQL reserved_keyword + type_func_name_keyword categories (pg_get_keywords()
|
||||
// catcodes R and T) — identifiers matching these cannot appear as a bare column
|
||||
// reference and must be quoted. SQL_KEYWORDS alone misses most of them because it is
|
||||
// a completion phrase list ("ORDER BY", not "order").
|
||||
const POSTGRES_RESERVED_KEYWORDS = [
|
||||
"all",
|
||||
"analyse",
|
||||
"analyze",
|
||||
"and",
|
||||
"any",
|
||||
"array",
|
||||
"as",
|
||||
"asc",
|
||||
"asymmetric",
|
||||
"authorization",
|
||||
"binary",
|
||||
"both",
|
||||
"case",
|
||||
"cast",
|
||||
"check",
|
||||
"collate",
|
||||
"collation",
|
||||
"column",
|
||||
"concurrently",
|
||||
"constraint",
|
||||
"create",
|
||||
"cross",
|
||||
"current_catalog",
|
||||
"current_date",
|
||||
"current_role",
|
||||
"current_schema",
|
||||
"current_time",
|
||||
"current_timestamp",
|
||||
"current_user",
|
||||
"default",
|
||||
"deferrable",
|
||||
"desc",
|
||||
"distinct",
|
||||
"do",
|
||||
"else",
|
||||
"end",
|
||||
"except",
|
||||
"false",
|
||||
"fetch",
|
||||
"for",
|
||||
"foreign",
|
||||
"freeze",
|
||||
"from",
|
||||
"full",
|
||||
"grant",
|
||||
"group",
|
||||
"having",
|
||||
"ilike",
|
||||
"in",
|
||||
"initially",
|
||||
"inner",
|
||||
"intersect",
|
||||
"into",
|
||||
"is",
|
||||
"isnull",
|
||||
"join",
|
||||
"lateral",
|
||||
"leading",
|
||||
"left",
|
||||
"like",
|
||||
"limit",
|
||||
"localtime",
|
||||
"localtimestamp",
|
||||
"natural",
|
||||
"not",
|
||||
"notnull",
|
||||
"null",
|
||||
"offset",
|
||||
"on",
|
||||
"only",
|
||||
"or",
|
||||
"order",
|
||||
"outer",
|
||||
"overlaps",
|
||||
"placing",
|
||||
"primary",
|
||||
"references",
|
||||
"returning",
|
||||
"right",
|
||||
"select",
|
||||
"session_user",
|
||||
"similar",
|
||||
"some",
|
||||
"symmetric",
|
||||
"system_user",
|
||||
"table",
|
||||
"tablesample",
|
||||
"then",
|
||||
"to",
|
||||
"trailing",
|
||||
"true",
|
||||
"union",
|
||||
"unique",
|
||||
"user",
|
||||
"using",
|
||||
"variadic",
|
||||
"verbose",
|
||||
"when",
|
||||
"where",
|
||||
"window",
|
||||
"with",
|
||||
];
|
||||
|
||||
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()).concat(POSTGRES_RESERVED_KEYWORDS));
|
||||
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()));
|
||||
|
||||
function buildTableItems(
|
||||
context: Pick<SqlCompletionContext, "prefix" | "qualifier">,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
const SIMPLE_LOWER_IDENTIFIER = /^[a-z_][a-z0-9_$]*$/;
|
||||
|
||||
// PostgreSQL pg_get_keywords() categories R and T. GaussDB's PostgreSQL-
|
||||
// compatible modes use the same identifier constraints for bare names.
|
||||
const POSTGRES_RESERVED_IDENTIFIER_KEYWORDS = new Set([
|
||||
"all",
|
||||
"analyse",
|
||||
"analyze",
|
||||
"and",
|
||||
"any",
|
||||
"array",
|
||||
"as",
|
||||
"asc",
|
||||
"asymmetric",
|
||||
"authorization",
|
||||
"binary",
|
||||
"both",
|
||||
"case",
|
||||
"cast",
|
||||
"check",
|
||||
"collate",
|
||||
"collation",
|
||||
"column",
|
||||
"concurrently",
|
||||
"constraint",
|
||||
"create",
|
||||
"cross",
|
||||
"current_catalog",
|
||||
"current_date",
|
||||
"current_role",
|
||||
"current_schema",
|
||||
"current_time",
|
||||
"current_timestamp",
|
||||
"current_user",
|
||||
"default",
|
||||
"deferrable",
|
||||
"desc",
|
||||
"distinct",
|
||||
"do",
|
||||
"else",
|
||||
"end",
|
||||
"except",
|
||||
"false",
|
||||
"fetch",
|
||||
"for",
|
||||
"foreign",
|
||||
"freeze",
|
||||
"from",
|
||||
"full",
|
||||
"grant",
|
||||
"group",
|
||||
"having",
|
||||
"ilike",
|
||||
"in",
|
||||
"initially",
|
||||
"inner",
|
||||
"intersect",
|
||||
"into",
|
||||
"is",
|
||||
"isnull",
|
||||
"join",
|
||||
"lateral",
|
||||
"leading",
|
||||
"left",
|
||||
"like",
|
||||
"limit",
|
||||
"localtime",
|
||||
"localtimestamp",
|
||||
"natural",
|
||||
"not",
|
||||
"notnull",
|
||||
"null",
|
||||
"offset",
|
||||
"on",
|
||||
"only",
|
||||
"or",
|
||||
"order",
|
||||
"outer",
|
||||
"overlaps",
|
||||
"placing",
|
||||
"primary",
|
||||
"references",
|
||||
"returning",
|
||||
"right",
|
||||
"select",
|
||||
"session_user",
|
||||
"similar",
|
||||
"some",
|
||||
"symmetric",
|
||||
"system_user",
|
||||
"table",
|
||||
"tablesample",
|
||||
"then",
|
||||
"to",
|
||||
"trailing",
|
||||
"true",
|
||||
"union",
|
||||
"unique",
|
||||
"user",
|
||||
"using",
|
||||
"variadic",
|
||||
"verbose",
|
||||
"when",
|
||||
"where",
|
||||
"window",
|
||||
"with",
|
||||
]);
|
||||
|
||||
// GaussDB MySQL-compatible drivers report a backtick. These additional words
|
||||
// cover MySQL-only names that cannot safely be emitted as bare identifiers.
|
||||
const MYSQL_ONLY_RESERVED_IDENTIFIER_KEYWORDS = new Set([
|
||||
"accessible",
|
||||
"auto_increment",
|
||||
"change",
|
||||
"database",
|
||||
"databases",
|
||||
"delayed",
|
||||
"describe",
|
||||
"div",
|
||||
"dual",
|
||||
"enclosed",
|
||||
"escaped",
|
||||
"explain",
|
||||
"force",
|
||||
"fulltext",
|
||||
"high_priority",
|
||||
"ignore",
|
||||
"index",
|
||||
"infile",
|
||||
"key",
|
||||
"keys",
|
||||
"kill",
|
||||
"linear",
|
||||
"lines",
|
||||
"load",
|
||||
"lock",
|
||||
"low_priority",
|
||||
"master_ssl_verify_server_cert",
|
||||
"maxvalue",
|
||||
"mediumint",
|
||||
"mod",
|
||||
"no_write_to_binlog",
|
||||
"optimize",
|
||||
"optionally",
|
||||
"outfile",
|
||||
"partition",
|
||||
"purge",
|
||||
"range",
|
||||
"read_write",
|
||||
"regexp",
|
||||
"release",
|
||||
"rename",
|
||||
"replace",
|
||||
"require",
|
||||
"rlike",
|
||||
"schema",
|
||||
"schemas",
|
||||
"separator",
|
||||
"show",
|
||||
"spatial",
|
||||
"sql_big_result",
|
||||
"sql_calc_found_rows",
|
||||
"sql_small_result",
|
||||
"ssl",
|
||||
"starting",
|
||||
"straight_join",
|
||||
"terminated",
|
||||
"tinyint",
|
||||
"unlock",
|
||||
"unsigned",
|
||||
"use",
|
||||
"utc_date",
|
||||
"utc_time",
|
||||
"utc_timestamp",
|
||||
"values",
|
||||
"varbinary",
|
||||
"varchar",
|
||||
"write",
|
||||
"xor",
|
||||
"zerofill",
|
||||
]);
|
||||
|
||||
export function isExplicitlyQuotedSqlIdentifier(value: string): boolean {
|
||||
if (value.length < 2) return false;
|
||||
return (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("`") && value.endsWith("`")) || (value.startsWith("[") && value.endsWith("]"));
|
||||
}
|
||||
|
||||
export function requiresPostgresIdentifierQuote(identifier: string, additionalKeywords?: ReadonlySet<string>): boolean {
|
||||
if (!SIMPLE_LOWER_IDENTIFIER.test(identifier)) return true;
|
||||
return POSTGRES_RESERVED_IDENTIFIER_KEYWORDS.has(identifier) || additionalKeywords?.has(identifier) === true;
|
||||
}
|
||||
|
||||
export function quoteGaussDbJdbcIdentifier(identifier: string, identifierQuote: string): string {
|
||||
if (isExplicitlyQuotedSqlIdentifier(identifier)) return identifier;
|
||||
const quote = identifierQuote.trim();
|
||||
if (!quote) return identifier;
|
||||
const requiresQuote = !SIMPLE_LOWER_IDENTIFIER.test(identifier) || POSTGRES_RESERVED_IDENTIFIER_KEYWORDS.has(identifier) || (quote === "`" && MYSQL_ONLY_RESERVED_IDENTIFIER_KEYWORDS.has(identifier));
|
||||
if (!requiresQuote) return identifier;
|
||||
return `${quote}${identifier.replaceAll(quote, quote + quote)}${quote}`;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { DatabaseType } from "@/types/database.ts";
|
||||
import { isSchemaAware, usesDatabaseObjectTreeMode } from "@/lib/database/databaseCapabilities.ts";
|
||||
import * as api from "@/lib/backend/api.ts";
|
||||
import { parseSqlServerLinkedSchema, sqlServerLinkedTableName } from "@/lib/database/sqlServerLinkedServers.ts";
|
||||
import { isExplicitlyQuotedSqlIdentifier, quoteGaussDbJdbcIdentifier } from "@/lib/sql/sqlIdentifier.ts";
|
||||
|
||||
export interface BuildTableSelectSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
@@ -22,6 +23,7 @@ export interface BuildTableSelectSqlOptions {
|
||||
}
|
||||
|
||||
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
|
||||
if ((databaseType === "gaussdb" || databaseType === "opengauss") && isExplicitlyQuotedSqlIdentifier(name)) return name;
|
||||
if (databaseType === "iotdb") return name;
|
||||
// JDBC connections use the driver-reported identifier quote string
|
||||
// (DatabaseMetaData.getIdentifierQuoteString()) — pass through unquoted.
|
||||
@@ -36,6 +38,7 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
|
||||
}
|
||||
|
||||
export function quoteTableDataIdentifier(databaseType: DatabaseType | undefined, name: string, identifierQuote?: string): string {
|
||||
if ((databaseType === "gaussdb" || databaseType === "opengauss" || databaseType === "postgres") && identifierQuote != null) return quoteGaussDbJdbcIdentifier(name, identifierQuote);
|
||||
if (databaseType === "kingbase" && identifierQuote != null) {
|
||||
if (!identifierQuote) return name;
|
||||
return `${identifierQuote}${name.replaceAll(identifierQuote, identifierQuote + identifierQuote)}${identifierQuote}`;
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
function gaussdbConnection(style: "double" | "backtick"): ConnectionConfig {
|
||||
return {
|
||||
id: `gaussdb-${style}`,
|
||||
name: "GaussDB",
|
||||
db_type: "gaussdb",
|
||||
host: "127.0.0.1",
|
||||
port: 5432,
|
||||
username: "gaussdb",
|
||||
password: "",
|
||||
database: "postgres",
|
||||
external_config: { gaussdbIdentifierQuoteStyle: style },
|
||||
};
|
||||
}
|
||||
|
||||
describe("connectionStore GaussDB identifier quote override", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("resolves the persisted compatibility mode before runtime driver metadata", async () => {
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const double = gaussdbConnection("double");
|
||||
const backtick = gaussdbConnection("backtick");
|
||||
store.connections = [double, backtick];
|
||||
|
||||
expect(store.connectionIdentifierQuote(double.id)).toBe('"');
|
||||
expect(store.connectionIdentifierQuote(backtick.id)).toBe("`");
|
||||
});
|
||||
|
||||
it("loads and caches auto compatibility quotes once per connection", async () => {
|
||||
const config: ConnectionConfig = {
|
||||
...gaussdbConnection("double"),
|
||||
id: "gaussdb-auto",
|
||||
external_config: undefined,
|
||||
};
|
||||
const connectionIdentifierQuote = vi.fn().mockResolvedValue("`");
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
connectDb: vi.fn().mockResolvedValue(config.id),
|
||||
connectionDatabaseInfo: vi.fn().mockRejectedValue(new Error("metadata unavailable")),
|
||||
connectionIdentifierQuote,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
await store.addConnection(config);
|
||||
await store.connect(config);
|
||||
|
||||
expect(connectionIdentifierQuote).toHaveBeenCalledOnce();
|
||||
expect(connectionIdentifierQuote).toHaveBeenCalledWith(config.id);
|
||||
expect(store.connectionIdentifierQuote(config.id)).toBe("`");
|
||||
expect(store.connectionIdentifierQuote(config.id)).toBe("`");
|
||||
expect(connectionIdentifierQuote).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -61,7 +61,7 @@ import * as api from "@/lib/backend/api";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
|
||||
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionShouldDiscoverJdbcSchemas, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionShouldDiscoverJdbcSchemas, connectionShouldLoadIdentifierQuote, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbIdentifierQuoteOverride } from "@/lib/database/jdbcDialect";
|
||||
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, compareSidebarNames, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
|
||||
import { buildSqlServerDatabaseTreeNodes } from "@/lib/database/sqlServerTree";
|
||||
import { collapseExpandedTreeNodes } from "@/lib/sidebar/sidebarTreeCollapse";
|
||||
@@ -499,6 +499,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function connectionIdentifierQuote(connectionId?: string): string | undefined {
|
||||
if (!connectionId) return undefined;
|
||||
const override = gaussdbIdentifierQuoteOverride(getConfig(connectionId));
|
||||
if (override != null) return override;
|
||||
return identifierQuotes.value[connectionId];
|
||||
}
|
||||
|
||||
@@ -511,7 +513,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
async function refreshConnectionIdentifierQuote(connectionId: string, config: ConnectionConfig) {
|
||||
clearConnectionIdentifierQuote(connectionId);
|
||||
if (config.db_type !== "kingbase") return;
|
||||
if (!connectionShouldLoadIdentifierQuote(config)) return;
|
||||
const quote = await api.connectionIdentifierQuote(connectionId).catch(() => undefined);
|
||||
if (quote != null) identifierQuotes.value = { ...identifierQuotes.value, [connectionId]: quote };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user