feat(opengauss): support A-compatibility packages

This commit is contained in:
amwps290 authored and GitHub committed 2026-09-14 16:13:44 +08:00
1 parent 760cad6d1b
commit deb47df2eb
28 files changed
+1706 -139

No files matched your search

@@ -294,7 +294,11 @@ const settingsStore = useSettingsStore();
function sqlStatementParameterOptions() {
const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, props.databaseType, settingsStore.editorSettings.sqlVariableSubstitutionEnabled);
return { databaseType: props.databaseType, enabledSyntaxes: enabledSqlParameterSyntaxes(toggles) };
return {
databaseType: props.databaseType,
compatibilityMode: props.databaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(props.connectionId, props.database) : undefined,
enabledSyntaxes: enabledSqlParameterSyntaxes(toggles),
};
}
const { isDark, themePalette, activeCustomUiColors } = useTheme();
const { t } = useI18n();
@@ -3473,7 +3477,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
if (props.databaseType !== "sqlserver") {
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType, sqlStatementParameterOptions());
}
const diagnosticRanges = sqlSemanticDiagnosticRangesForViewport(sql, visibleRanges, props.databaseType, props.databaseType === "sqlserver" ? undefined : executableStatementRangeCache?.ranges);
const diagnosticRanges = sqlSemanticDiagnosticRangesForViewport(sql, visibleRanges, props.databaseType, props.databaseType === "sqlserver" ? undefined : executableStatementRangeCache?.ranges, sqlStatementParameterOptions());
if (diagnosticRanges.length === 0) {
if (!options.preserveOutsideRanges) setSemanticDiagnostics([]);
return;
@@ -5334,9 +5338,31 @@ function shouldLoadCompletionObjects(completionContext: ReturnType<typeof getSql
return routineContext && !isReferencedTableQualifier(completionContext);
}
/**
* Databases whose qualified routine completion treats the first qualifier as a
* package (Oracle) or as a package-or-schema in A compatibility mode
* (openGauss). The backend re-validates: non-package parents fall through to
* the ordinary routine search, so routing is safe even when the compatibility
* map is not yet warm. openGauss keeps the package-aware routing while the
* mode is unknown (cold start, so A-mode completion works immediately after
* connect) and once the mode is known to be A; a known B/PG mode skips the
* extra package-style queries entirely.
*/
function usesPackageAwareRoutineCompletion(): boolean {
if (props.databaseType === "oracle") return true;
if (props.databaseType !== "opengauss") return false;
const mode = connectionStore.databaseCompatibilityMode(props.connectionId, props.database)?.trim().toUpperCase();
return mode === undefined || mode === "A";
}
function oracleRoutineCompletionTargets(completionContext: ReturnType<typeof getSqlCompletionContext>): RoutineCompletionTarget[] {
const parts = (completionContext.qualifierParts?.length ? completionContext.qualifierParts : completionContext.qualifier?.split("."))?.filter(Boolean) ?? [];
if (parts.length === 0) return [{ schema: props.schema, globalSearch: true }];
if (parts.length === 0) {
// Oracle resolves unqualified routines across all schemas (owner semantics).
// openGauss keeps the PG search_path scope for the unqualified case.
if (props.databaseType === "oracle") return [{ schema: props.schema, globalSearch: true }];
return [{ schema: props.schema }];
}
if (parts.length === 1) {
return [{ schema: props.schema, parentName: parts[0] }, { schema: parts[0] }];
}
@@ -5353,14 +5379,14 @@ function routineCompletionTargetForContext(completionContext: ReturnType<typeof
}
function routineCompletionScopeForContext(completionContext: ReturnType<typeof getSqlCompletionContext>, scope: CompletionMetadataScope): CompletionMetadataScope {
if (props.databaseType === "oracle") return scope;
if (usesPackageAwareRoutineCompletion()) return scope;
const target = routineCompletionTargetForContext(completionContext, scope);
return { database: target.database, schema: target.schema };
}
function lookupLocalCompletionObjectsForContext(completionContext: ReturnType<typeof getSqlCompletionContext>, scope: CompletionMetadataScope): SqlCompletionObject[] {
if (!props.connectionId || props.database == null) return [];
if (props.databaseType === "oracle") {
if (usesPackageAwareRoutineCompletion()) {
return connectionStore.lookupLocalCompletionObjects(props.connectionId, scope.database, completionContext.prefix, MAX_COMPLETION_TABLES);
}
const target = routineCompletionTargetForContext(completionContext, scope);
@@ -5370,7 +5396,7 @@ function lookupLocalCompletionObjectsForContext(completionContext: ReturnType<ty
async function listCompletionObjectsForContext(completionContext: ReturnType<typeof getSqlCompletionContext>, scope: CompletionMetadataScope): Promise<SqlCompletionObject[]> {
if (!props.connectionId || props.database == null) return [];
const objectKinds = completionObjectKindsForContext(completionContext);
if (props.databaseType !== "oracle") {
if (!usesPackageAwareRoutineCompletion()) {
const target = routineCompletionTargetForContext(completionContext, scope);
return connectionStore.listCompletionObjects(props.connectionId, target.database, target.mask, MAX_COMPLETION_TABLES, target.schema, undefined, false, scope.schema, objectKinds);
}
@@ -7134,6 +7160,23 @@ watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect,
});
});
// openGauss compatibility mode is loaded asynchronously from the backend into a
// dedicated store map (not the sidebar tree). A restored tab may open before the
// map is warm; when the mode arrives, re-derive statement boundaries and
// diagnostics so package DDL is parsed with the correct PL/SQL rules.
watch(
() => (props.databaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(props.connectionId, props.database) : undefined),
(now, before) => {
if (now === before) return;
executableStatementRangeCache = null;
if (props.databaseType !== "opengauss") return;
if (!view.value) return;
refreshCompletionCache();
setSemanticDiagnostics([]);
scheduleSemanticDiagnostics(0);
},
);
watch(
() => props.forceWordWrap,
() => {
@@ -176,6 +176,7 @@ import type { QueryTab, TableInfoTab, TreeNode, VectorCollectionMeta } from "@/t
import type { SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { productionContextForDatabase } from "@/lib/database/productionSafety";
import { sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
type DataGridHandle = DataGridColumnLayoutHandle & {
@@ -342,6 +343,9 @@ const activeEffectiveDatabaseType = computed(() => effectiveDatabaseTypeForConne
const canOpenTableImport = computed(() => supportsTableImport(activeEffectiveDatabaseType.value));
const activeVectorConnection = computed(() => connectionStore.getConfig(props.activeTab.connectionId) ?? props.activeConnection);
const activeDataTabExecutionDatabase = computed(() => dataTabExecutionDatabase(props.activeConnection, props.activeTab.database, activeDataTabTableMeta.value?.catalog));
const activeSqlStatementParameterOptions = computed(() =>
sqlStatementParameterOptionsForCompatibility(activeEffectiveDatabaseType.value, activeEffectiveDatabaseType.value === "opengauss" ? connectionStore.databaseCompatibilityMode(activeResultConnectionId.value, activeResultDatabase.value) : undefined),
);
const activeProductionContext = computed(() => productionContextForDatabase(props.activeConnection, props.activeTab.database));
const productionWatermarkText = computed(() => (locale.value.startsWith("zh") ? "生产环境" : "PROD"));
const productionSessionDetail = computed(() => {
@@ -474,6 +478,7 @@ const activeStatementExecutionMarkers = computed(() =>
props.activeTab.resultBaseSql || props.activeTab.lastExecutedSql || props.activeTab.sql,
props.activeTab.resultEditorFingerprint ?? "",
props.activeTab.batchSqlExecution,
activeSqlStatementParameterOptions.value,
),
);
const activeElasticsearchJsonResponse = computed(() => elasticsearchJsonResponseForResult(activeEffectiveDatabaseType.value, activeResultSql.value, props.activeTab.result));
@@ -1146,7 +1151,7 @@ function toggleResultAutoSave() {
function selectResultItem(item: (typeof visibleResultItems.value)[number]) {
queryStore.setActiveResultIndex(props.activeTab.id, item.index);
emit("update:activeOutputView", props.activeTab.id, "result");
const range = resultSourceRange(props.activeTab.sql, item.result, item.index, activeEffectiveDatabaseType.value) ?? null;
const range = resultSourceRange(props.activeTab.sql, item.result, item.index, activeEffectiveDatabaseType.value, activeSqlStatementParameterOptions.value) ?? null;
if (queryEditorRef.value) {
nextTick(() => queryEditorRef.value?.previewStatementRange(range));
} else {
@@ -1158,7 +1163,7 @@ function executionSummaryItemRange(item: ExecutionSummaryItem) {
if (typeof item.sourceFrom === "number" && typeof item.sourceTo === "number" && item.sql && props.activeTab.sql.slice(item.sourceFrom, item.sourceTo) === item.sql) {
return { from: item.sourceFrom, to: item.sourceTo };
}
return item.result ? resultSourceRange(props.activeTab.sql, item.result, item.statementIndex, activeEffectiveDatabaseType.value) : undefined;
return item.result ? resultSourceRange(props.activeTab.sql, item.result, item.statementIndex, activeEffectiveDatabaseType.value, activeSqlStatementParameterOptions.value) : undefined;
}
function previewExecutionSummaryItem(item: ExecutionSummaryItem) {
@@ -903,7 +903,8 @@ async function toggle(requestId = beginNavigationRequest()) {
return;
}
if (node.type === "package" && node.connectionId && supportsPackageMemberExpansion(currentDatabaseType())) {
const packageDatabaseType = currentDatabaseType();
if (node.type === "package" && node.connectionId && supportsPackageMemberExpansion(packageDatabaseType, packageDatabaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(node.connectionId, node.database) : undefined)) {
await connectionStore.loadPackageMembers(node);
emitNodeToggled(node, wasExpanded);
return;
@@ -15,7 +15,7 @@ import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { classifyRedisCommandSafety } from "@/lib/redis/redisCommandSafety";
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
import { isElasticsearchRestRequestText, parseElasticsearchRestRequestTarget, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { isElasticsearchRestRequestText, parseElasticsearchRestRequestTarget, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
import { extractSqlParameterDescriptors, type SqlParameterDescriptor, type SqlParameterSyntax } from "@/lib/sql/sqlParameters";
import { expandSqlVariables } from "@/lib/sql/sqlVariables";
import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
@@ -393,7 +393,7 @@ export function useSqlExecution(deps: {
cancelEditorViewportRequest(options.editorViewportRequestId);
return;
}
const statementCount = splitSqlStatementRanges(sql, executionDatabaseType).length;
const statementCount = splitSqlStatementRanges(sql, executionDatabaseType, sqlStatementParameterOptionsForCompatibility(executionDatabaseType, executionDatabaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(tab.connectionId, tab.database) : undefined)).length;
// Output-view switching belongs to the tab the user is looking at — both
// when the query starts and when it finishes.
if (deps.activeTab.value?.id === executionTabId) {
@@ -130,6 +130,15 @@ describe("databaseObjectCapabilities", () => {
}
});
it("exposes packages only for openGauss A compatibility mode", () => {
expect(sidebarObjectKindsForDatabase("opengauss", "A")).toEqual(expect.arrayContaining(["PACKAGE", "PACKAGE_BODY"]));
expect(sidebarObjectKindsForDatabase("opengauss", "a")).toContain("PACKAGE");
for (const mode of ["B", "C", "PG", undefined]) {
expect(sidebarObjectKindsForDatabase("opengauss", mode), String(mode)).not.toContain("PACKAGE");
}
expect(databaseObjectCapabilities("opengauss", "A").sourceReadable).toEqual(expect.arrayContaining(["PACKAGE", "PACKAGE_BODY"]));
});
it("enables custom type details only for verified PG-family databases", () => {
for (const dbType of ["postgres", "opengauss", "gaussdb", "kingbase", "vastbase"] as const) {
expect(customTypeCapabilities(dbType), dbType).toEqual({ details: true, members: true, ddl: true });
@@ -17,6 +17,11 @@ describe("buildDatabaseTreeNodes", () => {
expect(node.id).toBe("conn-1:analytics");
});
it("carries database compatibility mode into database nodes", () => {
const [node] = buildDatabaseTreeNodes("conn-1", [{ name: "oracle_mode", compatibility_mode: "A" }]);
expect(node.compatibilityMode).toBe("A");
});
it("shortens the Cloud Spanner label but keeps the resource path as the node identity", () => {
const [node] = buildDatabaseTreeNodes("conn-1", databases(SPANNER_PATH), { displayLabel: spannerDisplayDatabase });
@@ -25,6 +25,13 @@ describe("queryTimeout", () => {
expect(frontendQueryTimeoutSecsForSql("UPDATE sample_records SET state = 'ready' RETURNING id", "postgres", 30)).toBe(0);
});
it("uses conservative package splitting for an unresolved openGauss mode", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
END pkg_utils;`;
expect(frontendQueryTimeoutSecsForSql(`${packageSpec}\n/\nSELECT 1;`, "opengauss", 30)).toBe(120);
});
it("keeps the frontend guard for non-row PostgreSQL statements", () => {
expect(frontendQueryTimeoutSecsForSql("UPDATE sample_records SET state = 'ready'", "postgres", 30)).toBe(60);
expect(frontendQueryTimeoutSecsForSql("INSERT INTO sample_records(note) VALUES ('RETURNING is text')", "postgres", 30)).toBe(60);
@@ -616,6 +616,27 @@ END pkg_utils_without_replace;`;
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpecWithoutReplace}\nSELECT 1;`, "xugu"))).toEqual([packageSpecWithoutReplace, "SELECT 1"]);
});
it("keeps openGauss packages together while compatibility metadata is unknown", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
PROCEDURE log_message(msg VARCHAR2);
END pkg_utils;`;
const script = `${packageSpec}\n/\nSELECT 1;`;
expect(rangeSqlTexts(splitSqlStatementRanges(script, "opengauss"))).toEqual([packageSpec, "SELECT 1"]);
});
it("splits openGauss A-mode packages without changing PG mode", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
PROCEDURE log_message(msg VARCHAR2);
END pkg_utils;`;
const script = `${packageSpec}\n/\nSELECT 1;`;
expect(rangeSqlTexts(splitSqlStatementRanges(script, "opengauss", { compatibilityMode: "A" }))).toEqual([packageSpec, "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges(script, "opengauss", { compatibilityMode: "PG" }))).not.toEqual([packageSpec, "SELECT 1"]);
});
it("splits a declaration-only Oracle package body before following DML", () => {
const packageBody = `CREATE OR REPLACE PACKAGE BODY packageName IS
null;
@@ -18,6 +18,7 @@ const MYSQL_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCT
// covers user-created types (enum/domain/composite/range/multirange/base);
// relation auto-generated row types stay hidden.
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "TYPE"];
const OPENGAUSS_A_OBJECTS: SidebarObjectKind[] = [...POSTGRES_OBJECTS, "PACKAGE", "PACKAGE_BODY"];
// KWDB is routed through the PostgreSQL pool but its pg_type catalog
// compatibility is not verified yet, so it stays on the pre-TYPE object set.
@@ -114,8 +115,8 @@ function isSourceReadableObjectKind(kind: SidebarObjectKind, dbType?: DatabaseTy
return true;
}
export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjectCapabilities {
const sidebarObjects = sidebarObjectKindsForDatabase(dbType);
export function databaseObjectCapabilities(dbType?: DatabaseType, compatibilityMode?: string): DatabaseObjectCapabilities {
const sidebarObjects = sidebarObjectKindsForDatabase(dbType, compatibilityMode);
return {
sidebarObjects,
sourceReadable: sidebarObjects.filter((kind) => isSourceReadableObjectKind(kind, dbType)),
@@ -166,8 +167,9 @@ export function schemaDiffRoutineObjectTypesIntersection(sourceDbType?: Database
return schemaDiffRoutineObjectTypes(targetDbType).filter((kind) => sourceTypes.has(kind));
}
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObjectKind[] {
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType, compatibilityMode?: string): SidebarObjectKind[] {
if (!dbType) return [...TABLE_VIEW_OBJECTS];
if (dbType === "opengauss" && compatibilityMode?.trim().toUpperCase() === "A") return [...OPENGAUSS_A_OBJECTS];
return DATABASE_TYPE_OBJECTS.get(dbType) ?? [...ROUTINE_OBJECTS];
}
@@ -202,8 +204,8 @@ export function customTypeCapabilities(dbType?: DatabaseType): CustomTypeCapabil
return { details: supported, members: supported, ddl: supported };
}
export function supportsPackageMemberExpansion(dbType?: DatabaseType): boolean {
return !!dbType && PACKAGE_MEMBER_EXPANSION_DATABASES.has(dbType);
export function supportsPackageMemberExpansion(dbType?: DatabaseType, compatibilityMode?: string): boolean {
return !!dbType && (PACKAGE_MEMBER_EXPANSION_DATABASES.has(dbType) || (dbType === "opengauss" && compatibilityMode?.trim().toUpperCase() === "A"));
}
export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
@@ -38,6 +38,7 @@ export function buildDatabaseTreeNodes(
type: "database" as const,
connectionId,
database: name,
compatibilityMode: db.compatibility_mode?.trim() || undefined,
isExpanded: false,
children: [],
},
@@ -87,6 +88,7 @@ export function buildDuckDbConnectionTreeNodes(connectionId: string, databases:
type: "database" as const,
connectionId,
database: name,
compatibilityMode: db.compatibility_mode?.trim() || undefined,
isExpanded: false,
children: [],
},
@@ -25,7 +25,7 @@ export function executableStatementRangeCacheForDoc(
): ExecutableStatementRangeCache {
const parameterOptions = typeof parameterOptionsOrParse === "function" ? undefined : parameterOptionsOrParse;
const parse = typeof parameterOptionsOrParse === "function" ? parameterOptionsOrParse : customParse;
const parameterSyntaxKey = parameterOptions?.enabledSyntaxes ? parameterOptions.enabledSyntaxes.join(",") : "*";
const parameterSyntaxKey = `${parameterOptions?.enabledSyntaxes ? parameterOptions.enabledSyntaxes.join(",") : "*"}|compat=${parameterOptions?.compatibilityMode?.trim().toUpperCase() ?? ""}`;
if (cache?.doc === doc && cache.databaseType === databaseType && cache.parameterSyntaxKey === parameterSyntaxKey) return cache;
const byStart = new Map<number, SqlTextRange>();
+5 -4
View File
@@ -1,4 +1,5 @@
import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
import { DEFAULT_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
import type { ConnectionConfig, DatabaseType } from "@/types/database";
@@ -46,7 +47,7 @@ export function queryTimeoutSecsForConcurrentIndex(configuredTimeoutSecs: number
return Math.max(configuredTimeoutSecs, CONCURRENT_INDEX_QUERY_TIMEOUT_SECS);
}
export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: DatabaseType | undefined, queryTimeoutSecs: number): number {
export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: DatabaseType | undefined, queryTimeoutSecs: number, parameterOptions?: SqlParameterOptions): number {
if (queryTimeoutSecs === 0) return 0;
// PostgreSQL applies the configured timeout while the backend receives rows
@@ -56,7 +57,7 @@ export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: Databa
if (databaseType === "postgres" && postgresQueryMayReturnRows(sql, databaseType)) return 0;
const baseTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
const statementCount = Math.max(splitSqlStatementRanges(sql, databaseType).length, 1);
const statementCount = Math.max(splitSqlStatementRanges(sql, databaseType, parameterOptions ?? sqlStatementParameterOptionsForCompatibility(databaseType)).length, 1);
return baseTimeoutSecs * statementCount;
}
@@ -67,7 +68,7 @@ export function frontendQueryTimeoutDelayMs(timeoutSecs: number): number | undef
}
function postgresQueryMayReturnRows(sql: string, databaseType: DatabaseType): boolean {
return splitSqlStatementRanges(sql, databaseType).some(({ sql: statement }) => {
return splitSqlStatementRanges(sql, databaseType, sqlStatementParameterOptionsForCompatibility(databaseType)).some(({ sql: statement }) => {
const tokens = tokenizeSqlSemantic(statement, "postgres");
const firstKeyword = tokens.find((token) => token.kind === "word" && token.depth === 0)?.normalized;
if (!firstKeyword) return false;
@@ -24,14 +24,14 @@ export interface SqlSemanticDiagnosticVisibleRange {
to: number;
}
export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRanges: readonly SqlSemanticDiagnosticVisibleRange[], databaseType?: DatabaseType, cachedStatements?: readonly SqlTextRange[]): SqlTextRange[] {
const statements = databaseType === "sqlserver" ? sqlServerSemanticDiagnosticRanges(sql) : (cachedStatements ?? executableStatementRanges(sql, databaseType));
export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRanges: readonly SqlSemanticDiagnosticVisibleRange[], databaseType?: DatabaseType, cachedStatements?: readonly SqlTextRange[], parameterOptions?: { compatibilityMode?: string }): SqlTextRange[] {
const statements = databaseType === "sqlserver" ? sqlServerSemanticDiagnosticRanges(sql) : (cachedStatements ?? executableStatementRanges(sql, databaseType, parameterOptions));
if (statements.length === 0 || visibleRanges.length === 0) return [];
const selected: SqlTextRange[] = [];
const seen = new Set<string>();
for (const statement of statements) {
if (isOraclePlSqlStatement(statement.sql, databaseType)) continue;
if (isOraclePlSqlStatement(statement.sql, databaseType, parameterOptions)) continue;
if (!visibleRanges.some((visibleRange) => rangesIntersect(statement, visibleRange))) continue;
const key = `${statement.from}:${statement.to}`;
if (seen.has(key)) continue;
@@ -55,6 +55,8 @@ type TriggerPseudoRecordName = "new" | "old" | "parent" | "eventinfo";
export interface SqlParameterOptions {
databaseType?: DatabaseType;
/** Database-level compatibility mode, for example openGauss A/B/C/PG. */
compatibilityMode?: string;
// Which placeholder syntaxes are recognized. Undefined enables all of them.
enabledSyntaxes?: readonly SqlParameterSyntax[];
}
+23 -8
View File
@@ -499,7 +499,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
continue;
}
}
if (isOracleLikeDatabase(databaseType) && isAtLineStart(sql, i) && isSlashLine(sql, i)) {
if (isOracleLikeDatabase(databaseType, parameterOptions) && isAtLineStart(sql, i) && isSlashLine(sql, i)) {
const lineEnd = findLineEnd(sql, i);
flush(i);
i = nextLineStart(sql, lineEnd);
@@ -579,7 +579,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
const tagMatch = /^\$[A-Za-z_0-9]*\$/.exec(sql.slice(i));
if (tagMatch) {
markContent(i);
if (databaseType === "gaussdb" && statementStart !== -1 && startsWithPostgresDollarQuotedRoutinePrefix(sql.slice(statementStart, i))) {
if ((databaseType === "gaussdb" || isOpenGaussOracleCompatibility(databaseType, parameterOptions)) && statementStart !== -1 && startsWithPostgresDollarQuotedRoutinePrefix(sql.slice(statementStart, i))) {
postgresDollarQuotedRoutine = true;
}
dollarTag = tagMatch[0].slice(1, -1);
@@ -609,7 +609,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
// Internal semicolons remain part of the routine body.
flush();
} else {
if (oraclePlSqlStatementEnd === undefined && isOracleLikeDatabase(databaseType) && !postgresDollarQuotedRoutine && statementStart !== -1) {
if (oraclePlSqlStatementEnd === undefined && isOracleLikeDatabase(databaseType, parameterOptions) && !postgresDollarQuotedRoutine && statementStart !== -1) {
const statementSoFar = sql.slice(statementStart, i);
oraclePlSqlStatementEnd = startsWithOraclePlSqlBlock(statementSoFar) ? statementStart + (oraclePlSqlBlockEnd(sql.slice(statementStart)) ?? sql.length - statementStart) : null;
}
@@ -740,7 +740,7 @@ function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: nu
}
function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
if (isOraclePlSqlStatement(statement.sql, databaseType)) return [statement];
if (isOraclePlSqlStatement(statement.sql, databaseType, parameterOptions)) return [statement];
if (isSapHanaScriptBlockStatement(statement.sql, databaseType)) return [statement];
// Routine bodies contain top-level-looking SET/INSERT/SELECT lines that are not independent statements.
if (isMysqlRoutineBlockDatabase(databaseType) && startsWithMysqlRoutineBlock(statement.sql, parameterOptions)) return [statement];
@@ -1575,12 +1575,27 @@ function isSqlWhitespace(ch: string): boolean {
return ch === " " || ch === "\t" || ch === "\r" || ch === "\n";
}
export function isOracleLikeDatabase(databaseType?: DatabaseType): boolean {
return !!databaseType && ORACLE_LIKE_PL_SQL_DATABASES.has(databaseType);
function isOpenGaussOracleCompatibility(databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
if (databaseType !== "opengauss") return false;
const mode = options?.compatibilityMode?.trim().toUpperCase();
// Unknown mode is handled conservatively so an A-mode package is never
// split into executable fragments before metadata loading finishes.
return mode === undefined || mode === "A";
}
export function isOraclePlSqlStatement(sql: string, databaseType?: DatabaseType): boolean {
return isOracleLikeDatabase(databaseType) && startsWithOraclePlSqlBlock(sql);
export function sqlStatementParameterOptionsForCompatibility(databaseType?: DatabaseType, compatibilityMode?: string): SqlParameterOptions | undefined {
if (databaseType !== "opengauss") return undefined;
// Keep the browser-side splitter aligned with the backend's conservative
// behavior while the compatibility probe is still cold or unavailable.
return { compatibilityMode: compatibilityMode?.trim() || "A" };
}
export function isOracleLikeDatabase(databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
return !!databaseType && (ORACLE_LIKE_PL_SQL_DATABASES.has(databaseType) || isOpenGaussOracleCompatibility(databaseType, options));
}
export function isOraclePlSqlStatement(sql: string, databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
return isOracleLikeDatabase(databaseType, options) && startsWithOraclePlSqlBlock(sql);
}
function isSapHanaScriptBlockDatabase(databaseType?: DatabaseType): boolean {
+17 -8
View File
@@ -4,7 +4,8 @@ import { hexToRgba } from "@/lib/common/color";
import type { CSSProperties } from "vue";
import { findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout";
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
import { executableStatementRanges, splitSqlStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { executableStatementRanges, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database";
@@ -240,14 +241,14 @@ export function resultSqlForGrid(tab: Pick<QueryTab, "result" | "resultBaseSql"
* A stale or ambiguous source is ignored instead of highlighting a different
* statement that happens to have the same text.
*/
export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "sourceStatement" | "sourceFrom" | "sourceTo"> | undefined, resultIndex: number | undefined, databaseType?: DatabaseType): SqlTextRange | undefined {
export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "sourceStatement" | "sourceFrom" | "sourceTo"> | undefined, resultIndex: number | undefined, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange | undefined {
const sourceStatement = result?.sourceStatement;
if (!sourceStatement) return undefined;
if (typeof result.sourceFrom === "number" && typeof result.sourceTo === "number" && editorSql.slice(result.sourceFrom, result.sourceTo) === sourceStatement) {
return { from: result.sourceFrom, to: result.sourceTo, sql: sourceStatement };
}
const statements = statementRanges(editorSql, databaseType);
const statements = statementRanges(editorSql, databaseType, parameterOptions);
const indexed = typeof resultIndex === "number" ? statements[resultIndex] : undefined;
if (indexed?.sql === sourceStatement) {
return { from: indexed.from, to: indexed.to, sql: indexed.sql };
@@ -273,10 +274,10 @@ function lineStartOffset(sql: string, from: number): number {
return sql.lastIndexOf("\n", Math.max(0, from - 1)) + 1;
}
function statementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
function statementRanges(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange[] {
if (databaseType === "redis") return executableStatementRanges(sql, databaseType);
if (databaseType === "mongodb") return splitMongoCommandRanges(sql).map(({ from, to, text }) => ({ from, to, sql: text }));
return splitSqlStatementRanges(sql, databaseType);
return splitSqlStatementRanges(sql, databaseType, parameterOptions ?? sqlStatementParameterOptionsForCompatibility(databaseType));
}
function liveStatementExecutionMarkers(editorSql: string, batch: BatchSqlExecution): StatementExecutionMarker[] {
@@ -301,12 +302,20 @@ function liveStatementExecutionMarkers(editorSql: string, batch: BatchSqlExecuti
}));
}
export function statementExecutionMarkers(editorSql: string, results: QueryResult[] | undefined, databaseType?: DatabaseType, submittedSql = editorSql, executionEditorFingerprint = sqlTextFingerprint(editorSql), batch?: BatchSqlExecution): StatementExecutionMarker[] {
export function statementExecutionMarkers(
editorSql: string,
results: QueryResult[] | undefined,
databaseType?: DatabaseType,
submittedSql = editorSql,
executionEditorFingerprint = sqlTextFingerprint(editorSql),
batch?: BatchSqlExecution,
parameterOptions?: SqlParameterOptions,
): StatementExecutionMarker[] {
if (batch?.items.length) return liveStatementExecutionMarkers(editorSql, batch);
if (!results?.length || sqlTextFingerprint(editorSql) !== executionEditorFingerprint) return [];
const submittedStatements = statementRanges(submittedSql, databaseType);
const submittedStatements = statementRanges(submittedSql, databaseType, parameterOptions);
if (submittedStatements.length <= 1) return [];
const editorStatements = submittedSql === editorSql ? submittedStatements : statementRanges(editorSql, databaseType);
const editorStatements = submittedSql === editorSql ? submittedStatements : statementRanges(editorSql, databaseType, parameterOptions);
const byLine = new Map<number, { success: number; error: number }>();
for (const result of results) {
+156 -19
View File
@@ -461,6 +461,11 @@ export const useConnectionStore = defineStore("connection", () => {
const etcdAccessCapabilityGenerations = new Map<string, number>();
const etcdAccessCapabilityLoads = new Map<string, Promise<EtcdAccessCapabilities>>();
const identifierQuotes = ref<Record<string, string>>({});
/** Per-database compatibility mode (key `${connectionId}\u0000${database}`),
* decoupled from sidebar tree-node lifecycle so editor parsing and sidebar
* capabilities stay correct before/independent of database-tree rendering. */
const databaseCompatibilityModes = ref<Record<string, string>>({});
const databaseCompatibilityRefreshes = new Map<string, Promise<void>>();
const lastConnectionHealthCheckAt = ref<Record<string, number>>({});
const agentDrivers = ref<AgentDriverInstallState[]>([]);
let agentDriversRefreshPromise: Promise<void> | null = null;
@@ -788,13 +793,130 @@ export const useConnectionStore = defineStore("connection", () => {
return identifierQuotes.value[connectionId];
}
function databaseCompatibilityKey(connectionId: string, database: string): string {
return `${connectionId}\u0000${database.trim().toLowerCase()}`;
}
function databaseCompatibilityModeFromTree(connectionId: string, database: string): string | undefined {
const wanted = database.trim().toLowerCase();
const visit = (nodes: readonly TreeNode[]): string | undefined => {
for (const node of nodes) {
if (node.connectionId === connectionId && node.type === "database" && node.database?.trim().toLowerCase() === wanted) {
return node.compatibilityMode?.trim() || undefined;
}
const nested = node.children ? visit(node.children) : undefined;
if (nested) return nested;
}
return undefined;
};
return visit(treeNodes.value);
}
/** Compatibility mode for one database. Live metadata wins over persisted tree data. */
function databaseCompatibilityMode(connectionId: string | undefined, database: string | undefined): string | undefined {
if (!connectionId || database == null) return undefined;
return databaseCompatibilityModes.value[databaseCompatibilityKey(connectionId, database)] ?? databaseCompatibilityModeFromTree(connectionId, database);
}
function setDatabaseCompatibilityModesFromDatabases(connectionId: string, databases: readonly { name: string; compatibility_mode?: string | null }[]) {
if (!connectionId) return;
const prefix = `${connectionId}\u0000`;
const next = { ...databaseCompatibilityModes.value };
// Prune entries for databases that no longer exist on this connection.
let changed = false;
for (const key of Object.keys(next)) {
if (key.startsWith(prefix) && !databases.some((database) => databaseCompatibilityKey(connectionId, database.name.trim()) === key)) {
delete next[key];
changed = true;
}
}
for (const database of databases) {
const name = database.name.trim();
if (!name) continue;
const mode = database.compatibility_mode?.trim() || undefined;
const key = databaseCompatibilityKey(connectionId, name);
if (mode !== undefined && next[key] !== mode) {
next[key] = mode;
changed = true;
} else if (mode === undefined && key in next) {
delete next[key];
changed = true;
}
}
if (changed) databaseCompatibilityModes.value = next;
}
/** Ensure the compatibility map is warm for an openGauss connection even when
* the database tree has not loaded (restored tab, filtered node, …). Runs in
* the background (void caller) and is timeout-bounded so it never blocks or
* hangs the connect flow. The connection-state revision guards against stale
* refreshes overwriting a newer connection's map after a disconnect/reconnect. */
async function refreshConnectionDatabaseModesOnce(connectionId: string, config: ConnectionConfig) {
try {
if (config.db_type !== "opengauss") return;
const revision = connectionStateRevision(connectionId);
const databases = await withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "compatibility modes").catch(() => undefined);
if (!databases || !isCurrentConnectionStateRevision(connectionId, revision)) return;
const before = databaseCompatibilityModes.value;
setDatabaseCompatibilityModesFromDatabases(connectionId, databases);
if (databaseCompatibilityModes.value === before) return;
// The mode map changed; invalidate completion caches for every affected
// database. The completion cache key does not include the compatibility
// mode, so a result cached while the mode was unknown/wrong (e.g. package
// members surfaced as top-level routines) would otherwise be served forever.
for (const database of databases) {
invalidateCompletionCache(connectionId, database.name.trim());
}
// Refresh any expanded openGauss database subtree so PACKAGE groups appear
// without waiting for the next manual expand.
for (const database of databases) {
if (!database.compatibility_mode?.trim()) continue;
if (database.compatibility_mode.trim().toUpperCase() !== "A") continue;
const node = findNode(treeNodes.value, `${connectionId}:${database.name.trim()}`);
if (!node || node.type !== "database" || !node.isExpanded) continue;
void refreshTreeNode(node).catch(() => undefined);
}
} catch {
// Compatibility metadata is optional and must never create an unhandled
// rejection in the background connection lifecycle.
}
}
function refreshConnectionDatabaseModes(connectionId: string, config: ConnectionConfig): Promise<void> {
const existing = databaseCompatibilityRefreshes.get(connectionId);
if (existing) return existing;
const refresh = refreshConnectionDatabaseModesOnce(connectionId, config).finally(() => {
if (databaseCompatibilityRefreshes.get(connectionId) === refresh) databaseCompatibilityRefreshes.delete(connectionId);
});
databaseCompatibilityRefreshes.set(connectionId, refresh);
return refresh;
}
async function ensureDatabaseCompatibilityMode(connectionId: string, database: string | undefined): Promise<string | undefined> {
const config = getConfig(connectionId);
if (config?.db_type !== "opengauss" || database == null) return undefined;
const current = databaseCompatibilityMode(connectionId, database);
if (current !== undefined) return current;
await refreshConnectionDatabaseModes(connectionId, { ...config, id: connectionId });
return databaseCompatibilityMode(connectionId, database);
}
function clearConnectionIdentifierQuote(connectionId: string) {
clearConnectionDatabaseModes(connectionId);
if (!(connectionId in identifierQuotes.value)) return;
const next = { ...identifierQuotes.value };
delete next[connectionId];
identifierQuotes.value = next;
}
function clearConnectionDatabaseModes(connectionId: string) {
const prefix = `${connectionId}\u0000`;
const entries = Object.entries(databaseCompatibilityModes.value).filter(([key]) => !key.startsWith(prefix));
if (entries.length !== Object.keys(databaseCompatibilityModes.value).length) {
databaseCompatibilityModes.value = Object.fromEntries(entries);
}
}
async function refreshConnectionIdentifierQuote(connectionId: string, config: ConnectionConfig) {
clearConnectionIdentifierQuote(connectionId);
if (!connectionShouldLoadIdentifierQuote(config)) return;
@@ -1902,23 +2024,26 @@ export const useConnectionStore = defineStore("connection", () => {
return config?.db_type === "informix" ? `${version}-informix-owner-v2` : version;
}
function supportedSidebarObjectTypes(config?: ConnectionConfig): DatabaseObjectTreeKind[] {
function supportedSidebarObjectTypes(config?: ConnectionConfig, database?: string): DatabaseObjectTreeKind[] {
const dbType = effectiveDatabaseTypeForConnection(config);
return sidebarObjectKindsForDatabase(dbType);
return sidebarObjectKindsForDatabase(dbType, databaseCompatibilityMode(config?.id, database));
}
function sidebarObjectTypesForScope(config: ConnectionConfig | undefined, schema?: string): DatabaseObjectTreeKind[] {
function sidebarObjectTypesForScope(config: ConnectionConfig | undefined, database: string | undefined, schema?: string): DatabaseObjectTreeKind[] {
if (config?.db_type === "xugu" && isXuguPublicSynonymScope(schema)) {
return ["SYNONYM"];
}
if (config?.db_type === "xugu" && isXuguSchedulerJobScope(schema)) {
return ["JOB"];
}
return supportedSidebarObjectTypes(config);
return supportedSidebarObjectTypes(config, database);
}
function objectTreeCacheVersion(config: ConnectionConfig | undefined, schema: string | undefined, baseVersion: string): string {
const scopedVersion = config?.db_type === "xugu" && isXuguPublicSynonymScope(schema) ? `${baseVersion}-public-synonyms` : config?.db_type === "xugu" && isXuguSchedulerJobScope(schema) ? `${baseVersion}-scheduler-jobs` : baseVersion;
function objectTreeCacheVersion(config: ConnectionConfig | undefined, database: string | undefined, schema: string | undefined, baseVersion: string): string {
let scopedVersion = config?.db_type === "xugu" && isXuguPublicSynonymScope(schema) ? `${baseVersion}-public-synonyms` : config?.db_type === "xugu" && isXuguSchedulerJobScope(schema) ? `${baseVersion}-scheduler-jobs` : baseVersion;
if (config?.db_type === "opengauss" && databaseCompatibilityMode(config.id, database)?.trim().toUpperCase() === "A") {
scopedVersion = `${scopedVersion}-a-packages-v1`;
}
return ownerAwareMetadataCacheVersion(config, scopedVersion);
}
@@ -1955,7 +2080,7 @@ export const useConnectionStore = defineStore("connection", () => {
const objectTreeProfileCacheKey = driverProfileObjectTreeProfileForConnection(config)?.cacheKey;
// objects-v8: object-group listing SQL gained a pg_type branch for
// PostgreSQL-family user-defined types; older cached lists miss TYPE nodes.
const baseCacheVersion = objectTreeCacheVersion(config, node.schema, config?.db_type === "oracle" ? "objects-v7" : "objects-v8");
const baseCacheVersion = objectTreeCacheVersion(config, node.database, node.schema, config?.db_type === "oracle" ? "objects-v7" : "objects-v8");
const cacheVersion = objectTreeProfileCacheKey ? `${baseCacheVersion}:${objectTreeProfileCacheKey}` : baseCacheVersion;
return schemaCacheKey(node.connectionId || "", node.database || "", node.schema || "", node.type, cacheVersion);
}
@@ -2194,7 +2319,7 @@ export const useConnectionStore = defineStore("connection", () => {
});
const refreshedGroup = grouped.find((group) => group.type === options.node.type);
const children = refreshedGroup?.children ?? [];
return supportsPackageMemberExpansion(databaseType) ? markPackageNodesExpandable(children) : children;
return supportsPackageMemberExpansion(databaseType, databaseCompatibilityMode(options.node.connectionId, options.node.database)) ? markPackageNodesExpandable(children) : children;
}
function completionTableDetail(comment: string | null | undefined): string | undefined {
@@ -2504,7 +2629,7 @@ export const useConnectionStore = defineStore("connection", () => {
objects: supplementalObjects,
databaseType,
});
if (supportsPackageMemberExpansion(databaseType)) {
if (supportsPackageMemberExpansion(databaseType, databaseCompatibilityMode(options.connectionId, options.database))) {
supplementalChildren = markPackageNodesExpandable(supplementalChildren);
}
if (supplementalChildren.length === 0) return;
@@ -2983,7 +3108,8 @@ export const useConnectionStore = defineStore("connection", () => {
if (connectionUsesVisibleSchemaFilter(config)) {
return schemaCacheKey(connectionId, config.database || "", config.db_type === "oracle" ? "schemas-v2" : "schemas", config.show_system_schemas === true ? "show-system" : "hide-system");
}
return schemaCacheKey(connectionId, "databases-v2");
// v3 includes per-database compatibilityMode used by openGauss A-mode capabilities.
return schemaCacheKey(connectionId, "databases-v3");
}
async function hydrateTreeNodeFromCache(node: TreeNode | null, cacheKey: string | null): Promise<boolean> {
@@ -3969,6 +4095,9 @@ export const useConnectionStore = defineStore("connection", () => {
if (config.db_type !== "plugin") {
void refreshConnectedDatabaseInfo(id, { ...config, id });
await refreshConnectionIdentifierQuote(id, { ...config, id });
// Compatibility modes warm asynchronously; the QueryEditor watcher and the
// backend's own A-mode/EXISTS validation make a cold map safe.
void refreshConnectionDatabaseModes(id, { ...config, id });
}
if (id !== config.id) markSuccessfulLocalConnectionAttempt(config.id, localAttempt);
markSuccessfulLocalConnectionAttempt(id, localAttempt);
@@ -4228,6 +4357,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (config.db_type !== "plugin") {
void refreshConnectedDatabaseInfo(connectionId, config);
await refreshConnectionIdentifierQuote(connectionId, config);
void refreshConnectionDatabaseModes(connectionId, config);
}
markSuccessfulLocalConnectionAttempt(connectionId, localAttempt);
markConnectionHealthChecked(connectionId);
@@ -4479,6 +4609,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
const [databases, schemas] = await Promise.all([withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases"), withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, "main"), "schemas")]);
setDatabaseCompatibilityModesFromDatabases(connectionId, databases);
const databaseNames = databases.map((database) => database.name);
const visibleNames = filterDatabaseNamesForConnection(databaseNames, config);
const visibleNameSet = new Set(visibleNames);
@@ -4561,7 +4692,8 @@ export const useConnectionStore = defineStore("connection", () => {
setChildren(targetNode, children);
await savePersistedConnectionTreeChildren(cacheKey, targetNode.children || children);
} else {
const cacheKey = schemaCacheKey(connectionId, "databases-v2");
// v3 persists per-database compatibilityMode for openGauss A-mode features.
const cacheKey = schemaCacheKey(connectionId, "databases-v3");
if (!options?.force) {
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
if (cached.hit) {
@@ -4571,6 +4703,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
const databases = await withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases");
setDatabaseCompatibilityModesFromDatabases(connectionId, databases);
const visibleNames = filterDatabaseNamesForConnection(
databases.map((database) => database.name),
config,
@@ -5537,7 +5670,7 @@ export const useConnectionStore = defineStore("connection", () => {
const { database, catalog } = node;
const configForScope = getConfig(connectionId);
const simpleObjectDisplayForScope = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
const objectTypesForScope = simpleObjectDisplayForScope ? supportedSidebarObjectTypes(configForScope) : undefined;
const objectTypesForScope = simpleObjectDisplayForScope ? supportedSidebarObjectTypes(configForScope, database) : undefined;
const searchFilterForScope = activeTreeLoadSearchFilter(options);
const pageSizeForScope = sidebarObjectGroupPageSize();
return runTreeMetadataLoad(
@@ -5619,7 +5752,7 @@ export const useConnectionStore = defineStore("connection", () => {
async function loadTables(connectionId: string, database: string, schema?: string, options?: LoadTreeOptions) {
const configForScope = getConfig(connectionId);
const simpleObjectDisplayForScope = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
const objectTypesForScope = simpleObjectDisplayForScope ? sidebarObjectTypesForScope(configForScope, schema) : undefined;
const objectTypesForScope = simpleObjectDisplayForScope ? sidebarObjectTypesForScope(configForScope, database, schema) : undefined;
const searchFilter = activeTreeLoadSearchFilter(options);
const querySchemaForScope = connectionObjectTreeQuerySchema(configForScope, database, schema);
const effectiveSchemaForScope = connectionObjectTreeNodeSchema(configForScope, database, schema);
@@ -5635,7 +5768,7 @@ export const useConnectionStore = defineStore("connection", () => {
// collapses that to the database node id, so the loaded tables were attached to the
// database node instead of the schema node, leaving `(default)` permanently empty.
const nodeId = schema != null ? `${connectionId}:${database}:${schema}` : `${connectionId}:${database}`;
const cacheKey = schemaCacheKey(connectionId, database, schema || "", objectTreeCacheVersion(configForScope, schema, "objects-simple-v8"));
const cacheKey = schemaCacheKey(connectionId, database, schema || "", objectTreeCacheVersion(configForScope, database, schema, "objects-simple-v8"));
if (await hydrateTreeNodeFromCache(findNode(treeNodes.value, nodeId), cacheKey)) {
void loadTables(connectionId, database, schema, { ...options, force: true }).catch(() => undefined);
return;
@@ -5675,7 +5808,7 @@ export const useConnectionStore = defineStore("connection", () => {
const objectTreeProfile = driverProfileObjectTreeProfileForConnection(config);
const isPublicSynonymScope = config?.db_type === "xugu" && isXuguPublicSynonymScope(schema);
const isSchedulerJobScope = config?.db_type === "xugu" && isXuguSchedulerJobScope(schema);
const baseCacheVersion = objectTreeCacheVersion(config, schema, simpleObjectDisplay ? "objects-simple-v8" : "objects-grouped-v8");
const baseCacheVersion = objectTreeCacheVersion(config, database, schema, simpleObjectDisplay ? "objects-simple-v8" : "objects-grouped-v8");
const cacheVersion = !simpleObjectDisplay && objectTreeProfile?.cacheKey ? `${baseCacheVersion}:${objectTreeProfile.cacheKey}` : baseCacheVersion;
const cacheKey = schemaCacheKey(connectionId, database, schema || "", cacheVersion);
const querySchema = connectionObjectTreeQuerySchema(config, database, schema);
@@ -5695,7 +5828,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
const nonTableObjectTypes = simpleObjectDisplay ? sidebarObjectTypesForScope(config, schema).filter((objectType) => objectType !== "TABLE") : [];
const nonTableObjectTypes = simpleObjectDisplay ? sidebarObjectTypesForScope(config, database, schema).filter((objectType) => objectType !== "TABLE") : [];
let children: TreeNode[];
let nextObjectCount: number | undefined;
if (simpleObjectDisplay && !isPublicSynonymScope && !isSchedulerJobScope) {
@@ -5727,7 +5860,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema: effectiveSchema,
objectTypes: sidebarObjectTypesForScope(config, schema),
objectTypes: sidebarObjectTypesForScope(config, database, schema),
groupOverrides: objectTreeProfile?.groupOverrides,
});
if (!schema && isPostgresLikeForExtensions(config?.db_type)) {
@@ -7148,7 +7281,7 @@ export const useConnectionStore = defineStore("connection", () => {
async function loadPackageMembers(node: TreeNode, options?: LoadTreeOptions): Promise<void> {
if (node.type !== "package" || !node.connectionId || !node.database) return;
const databaseType = effectiveDatabaseTypeForConnection(getConfig(node.connectionId));
if (!supportsPackageMemberExpansion(databaseType)) return;
if (!supportsPackageMemberExpansion(databaseType, databaseCompatibilityMode(node.connectionId, node.database))) return;
const connectionId = node.connectionId;
const database = node.database;
const schema = node.schema;
@@ -7278,7 +7411,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
const ORACLE_SYSTEM_COMPLETION_SCHEMAS = new Set(["SYS", "SYSTEM", "SYSMAN", "DBSNMP", "OUTLN", "XDB", "MDSYS", "CTXSYS", "WMSYS"]);
const FILTERED_ROUTINE_COMPLETION_DATABASES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle"]);
const FILTERED_ROUTINE_COMPLETION_DATABASES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle", "opengauss"]);
function completionPreferredSchema(connectionId: string, preferredSchema?: string): string | undefined {
return preferredSchema?.trim() || getConfig(connectionId)?.username?.trim() || undefined;
@@ -7730,6 +7863,7 @@ export const useConnectionStore = defineStore("connection", () => {
await ensureConnected(connectionId);
const config = getConfig(connectionId);
const databases = await api.listDatabases(connectionId);
setDatabaseCompatibilityModesFromDatabases(connectionId, databases);
completionDatabasesCache.value[connectionId] = filterDatabaseNamesForConnection(
databases.map((database) => database.name),
config,
@@ -9062,6 +9196,9 @@ export const useConnectionStore = defineStore("connection", () => {
ensureEtcdAccessCapabilities,
canWriteEtcdKey,
connectionIdentifierQuote,
databaseCompatibilityMode,
ensureDatabaseCompatibilityMode,
databaseCompatibilityModes,
isTreeNodePinned,
orderByPinnedTreeNodes,
toggleTreeNodePin,
+33 -13
View File
@@ -58,7 +58,8 @@ import { appendLargeValueCells, canUseTableDataLargeValuePreview, remapLargeValu
import { simpleDataGridOrderByReferencesMissingColumn, sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { agentProtocolQueryResultMaxRows, capQueryResultTotal, effectiveQueryResultMaxRows, limitQueryPagination, queryResultLimitReached } from "@/lib/dataGrid/queryResultRowLimit";
import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { elasticsearchRestRequestRanges, executableStatementRanges, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
import { replaceSqlServerLeadingUseQuery, sqlServerLeadingUseScript, sqlServerUseDatabaseFromStatement } from "@/lib/sql/sqlCompletionLookupTarget";
import { classifySqlRisk } from "@/lib/sql/sqlRisk";
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
@@ -316,8 +317,8 @@ function preservedResultIndex(results: QueryResult[], currentIndex: number | und
return currentIndex;
}
function annotateQueryResultSources(results: QueryResult[], sql: string, database: string | undefined, databaseType?: DatabaseType, sourceOffset?: number): { results: QueryResult[]; sqlServerUseDatabase?: string } {
const statements = splitSqlStatementRanges(sql, databaseType);
function annotateQueryResultSources(results: QueryResult[], sql: string, database: string | undefined, databaseType?: DatabaseType, sourceOffset?: number, parameterOptions?: SqlParameterOptions): { results: QueryResult[]; sqlServerUseDatabase?: string } {
const statements = splitSqlStatementRanges(sql, databaseType, parameterOptions);
let statementIndex = 0;
let sourceDatabase = database;
let sqlServerUseDatabase: string | undefined;
@@ -356,8 +357,8 @@ function clearLiveBatchSqlExecution(tab: QueryTab, executionId: string) {
if (liveBatchSqlExecutions.get(tab)?.executionId === executionId) liveBatchSqlExecutions.delete(tab);
}
function createBatchSqlExecution(executionId: string, editorSql: string, submittedSql: string, databaseType: DatabaseType | undefined, sourceOffset: number | undefined, executionTarget: MultiDbExecutionTarget): BatchSqlExecution | undefined {
const statements = databaseType === "mongodb" ? splitMongoCommandRanges(submittedSql).map(({ from, to, text }) => ({ from, to, sql: text })) : splitSqlStatementRanges(submittedSql, databaseType);
function createBatchSqlExecution(executionId: string, editorSql: string, submittedSql: string, databaseType: DatabaseType | undefined, sourceOffset: number | undefined, executionTarget: MultiDbExecutionTarget, parameterOptions?: SqlParameterOptions): BatchSqlExecution | undefined {
const statements = databaseType === "mongodb" ? splitMongoCommandRanges(submittedSql).map(({ from, to, text }) => ({ from, to, sql: text })) : splitSqlStatementRanges(submittedSql, databaseType, parameterOptions);
if (statements.length === 0) return undefined;
if (statements.length > 1 && databaseType && NON_STREAMING_BATCH_DATABASE_TYPES.has(databaseType)) return undefined;
const offset = sourceOffset ?? 0;
@@ -5873,19 +5874,31 @@ export const useQueryStore = defineStore("query", () => {
const executionCatalog = resumedExecutionTarget ? resumedExecutionTarget.catalog : targetContext ? (targetContext.scope === "catalog" ? targetContext.catalog : undefined) : (executionTarget?.catalog ?? (tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog));
const contextDatabase = databaseTargetContext?.database;
const targetDatabase = resumedExecutionTarget ? resumedExecutionTarget.database : targetContext?.scope === "connection" ? "" : (contextDatabase ?? executionTarget?.database ?? tab.database);
if (effectiveDbType === "opengauss") {
await connStore.ensureDatabaseCompatibilityMode(executionConnectionId, targetDatabase || conn?.database);
}
const targetSchema = resumedExecutionTarget ? resumedExecutionTarget.schema : targetContext?.scope === "connection" ? undefined : (databaseTargetContext?.schema ?? executionTarget?.schema ?? tab.schema);
const executionDatabase = dataTabExecutionDatabase(conn, targetDatabase, executionCatalog);
const sqlStatementParameterOptions = sqlStatementParameterOptionsForCompatibility(effectiveDbType, effectiveDbType === "opengauss" ? connStore.databaseCompatibilityMode(executionConnectionId, targetDatabase || conn?.database) : undefined);
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type, conn?.driver_profile);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn, settingsStore.editorSettings.globalQueryTimeoutSecs);
if (!batchResume) {
const statementExecution =
tab.mode === "query"
? createBatchSqlExecution(executionId, tab.sql, sql, effectiveDbType, options?.sourceOffset, {
connectionId: executionConnectionId,
catalog: executionCatalog,
database: targetDatabase,
schema: targetSchema,
})
? createBatchSqlExecution(
executionId,
tab.sql,
sql,
effectiveDbType,
options?.sourceOffset,
{
connectionId: executionConnectionId,
catalog: executionCatalog,
database: targetDatabase,
schema: targetSchema,
},
sqlStatementParameterOptions,
)
: undefined;
tab.batchSqlExecution = statementExecution && (tab.autoCommit !== false || statementExecution.total === 1) ? statementExecution : undefined;
if (tab.batchSqlExecution) liveBatchSqlExecutions.set(tab, tab.batchSqlExecution);
@@ -6536,7 +6549,7 @@ export const useQueryStore = defineStore("query", () => {
// could not rewrite the SQL for these engines, so a plain execution
// would return the first page again (#8993).
const isOffsetJumpPage = typeof pageOffset === "number" && pageOffset > 0 && !options?.pagination?.sessionId;
const frontendTimeoutSecs = frontendQueryTimeoutSecsForSql(sqlToExecute, effectiveDbType, queryTimeoutSecs);
const frontendTimeoutSecs = frontendQueryTimeoutSecsForSql(sqlToExecute, effectiveDbType, queryTimeoutSecs, sqlStatementParameterOptions);
const sourceLabelDatabase = targetDatabase || conn?.database;
const executionClientSessionId = options?.pagination?.clientSessionId ?? (tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined);
const currentBeforeDispatch = findExecutionTab(id);
@@ -6695,7 +6708,14 @@ export const useQueryStore = defineStore("query", () => {
} else {
executionPromise = executeWithoutManualTransaction();
}
const annotatedResults = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset);
const annotatedResults = annotateQueryResultSources(
markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))),
queryBaseSql,
sourceLabelDatabase,
effectiveDbType,
options?.sourceOffset,
sqlStatementParameterOptions,
);
const results = offsetBatchQueryResultIndexes(annotatedResults.results, batchResume?.startStatementIndex ?? 0);
reconcileBatchSqlResults(tab, executionId, results);
// Oracle-only sticky state aggregation. Only the initial manual execution
+4
View File
@@ -652,6 +652,8 @@ export interface DatabaseInfo {
comment?: string | null;
default_charset?: string | null;
default_collation?: string | null;
/** Database-level compatibility mode, for example openGauss A/B/C/PG. */
compatibility_mode?: string | null;
}
export interface DatabaseStorageInfo {
@@ -1336,6 +1338,8 @@ export interface TreeNode {
pinned?: boolean;
connectionId?: string;
database?: string;
/** Database-level compatibility mode, for example openGauss A/B/C/PG. */
compatibilityMode?: string;
catalog?: string;
catalogType?: string;
linkedServer?: string;