feat(opengauss): support A-compatibility packages

This commit is contained in:
amwps290
2026-09-14 16:13:44 +08:00
committed by GitHub
parent 760cad6d1b
commit deb47df2eb
28 changed files with 1706 additions and 139 deletions
+1
View File
@@ -1,4 +1,5 @@
# Dependencies
openGauss-server/
node_modules
.claude/
.codex/
@@ -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;
+302
View File
@@ -9830,6 +9830,308 @@ for line in sys.stdin:
.await;
}
#[tokio::test]
#[ignore = "requires a reachable openGauss A-compatibility instance via environment variables"]
async fn live_opengauss_package_feature() {
let host = std::env::var("DBX_TEST_OPENGAUSS_HOST").expect("DBX_TEST_OPENGAUSS_HOST not set");
let port = std::env::var("DBX_TEST_OPENGAUSS_PORT")
.expect("DBX_TEST_OPENGAUSS_PORT not set")
.parse::<u16>()
.expect("DBX_TEST_OPENGAUSS_PORT should be a u16");
let username = std::env::var("DBX_TEST_OPENGAUSS_USER").expect("DBX_TEST_OPENGAUSS_USER not set");
let password = std::env::var("DBX_TEST_OPENGAUSS_PASSWORD").expect("DBX_TEST_OPENGAUSS_PASSWORD not set");
let database = std::env::var("DBX_TEST_OPENGAUSS_DATABASE").unwrap_or_else(|_| "postgres".to_string());
let mut config = live_postgres_like_config(DatabaseType::OpenGauss, &host, port, &username, &password, None);
config.id = "opengauss-live".to_string();
config.database = Some(database.clone());
let (state, dir) = test_app_state().await;
state.configs.write().await.insert(config.id.clone(), config);
let pool_key = state.get_or_create_pool("opengauss-live", Some(&database)).await.unwrap();
let pool = match state.pool_handle(&pool_key).await.expect("openGauss pool should be created") {
PoolKind::Postgres(pool) => pool,
_ => panic!("openGauss should use the PostgreSQL pool path"),
};
let mode = db::postgres::opengauss_compatibility_mode(&pool).await.unwrap();
println!("[live] current database = {database}, compatibility mode = {mode:?}");
let is_a_mode = mode.as_deref().map(str::trim).map(|value| value.eq_ignore_ascii_case("A")) == Some(true);
let databases = schema::list_databases_core(&state, "opengauss-live").await.unwrap();
for item in &databases {
println!("[live] database {} -> {:?}", item.name, item.compatibility_mode);
}
assert!(databases.iter().any(|item| item.name == database), "expected {database} in the database list");
if is_a_mode {
assert_eq!(
databases.iter().find(|item| item.name == database).and_then(|item| item.compatibility_mode.as_deref()),
mode.as_deref(),
"listed compatibility mode should match the probed mode"
);
}
// Statement splitting must keep an A-mode package intact.
let script = "CREATE OR REPLACE PACKAGE p AS\n FUNCTION f RETURN INT;\nEND p;\n/\nSELECT 1;";
let a_split = crate::sql::split_sql_statements_for_database_with_compatibility(
script,
DatabaseType::OpenGauss,
Some("A"),
);
let pg_split = crate::sql::split_sql_statements_for_database_with_compatibility(
script,
DatabaseType::OpenGauss,
Some("PG"),
);
println!("[live] A-mode split = {a_split:?}");
println!("[live] PG-mode split = {pg_split:?}");
assert_eq!(a_split.len(), 2, "A-mode package must stay one statement");
assert!(pg_split.len() > 2, "PG-mode must split the same script at inner semicolons");
// A non-A database on the same instance must keep the package gate closed.
if let Some(other) = databases.iter().find(|item| {
item.name != database
&& item.compatibility_mode.as_deref().map(str::trim).is_some_and(|mode| !mode.eq_ignore_ascii_case("A"))
}) {
let other_pool_key = state.get_or_create_pool("opengauss-live", Some(&other.name)).await.unwrap();
if let Some(PoolKind::Postgres(other_pool)) = state.pool_handle(&other_pool_key).await {
let other_mode = db::postgres::opengauss_compatibility_mode(&other_pool).await.unwrap();
let other_packages =
db::postgres::list_opengauss_packages(&other_pool, "public", true, true).await.unwrap();
println!(
"[live] non-A database {} mode = {:?}, listed packages = {}",
other.name,
other_mode,
other_packages.len()
);
assert!(!db::postgres::opengauss_is_oracle_compatible(&other_pool).await.unwrap());
assert!(other_packages.is_empty(), "non-A database must not list packages");
other_pool.close();
}
}
if !is_a_mode {
println!("[live] database is not A-compatible; skipping package catalog assertions");
pool.close();
let _ = std::fs::remove_dir_all(dir);
return;
}
let schema_name = "public";
let pkg = "dbx_live_pkg";
let _ = db::postgres::execute_query(&pool, &format!("DROP PACKAGE BODY IF EXISTS {schema_name}.{pkg}")).await;
let _ = db::postgres::execute_query(&pool, &format!("DROP PACKAGE IF EXISTS {schema_name}.{pkg}")).await;
let spec = format!(
"CREATE OR REPLACE PACKAGE {schema_name}.{pkg} AS\n g_version VARCHAR2(20) := '1.0';\n PROCEDURE log_message(p_message IN VARCHAR2);\n FUNCTION add_numbers(p_left IN INTEGER, p_right IN INTEGER) RETURN INTEGER;\nEND {pkg};"
);
db::postgres::execute_query(&pool, &spec)
.await
.unwrap_or_else(|error| panic!("create package spec failed: {error}"));
let body = format!(
"CREATE OR REPLACE PACKAGE BODY {schema_name}.{pkg} AS\n PROCEDURE log_message(p_message IN VARCHAR2) IS\n BEGIN\n NULL;\n END;\n FUNCTION add_numbers(p_left IN INTEGER, p_right IN INTEGER) RETURN INTEGER IS\n BEGIN\n RETURN p_left + p_right;\n END;\nBEGIN\n g_version := '1.1';\nEND {pkg};"
);
db::postgres::execute_query(&pool, &body)
.await
.unwrap_or_else(|error| panic!("create package body failed: {error}"));
let package_types = vec!["PACKAGE".to_string(), "PACKAGE_BODY".to_string()];
let package_objects = schema::list_objects_core(
&state,
"opengauss-live",
&database,
schema_name,
None,
None,
None,
Some(package_types.as_slice()),
None,
)
.await
.unwrap();
println!(
"[live] package objects = {:?}",
package_objects
.iter()
.filter(|object| object.name == pkg)
.map(|object| object.object_type.as_str())
.collect::<Vec<_>>()
);
assert!(package_objects.iter().any(|object| object.name == pkg && object.object_type == "PACKAGE"));
assert!(package_objects.iter().any(|object| object.name == pkg && object.object_type == "PACKAGE_BODY"));
let routine_types = vec!["PROCEDURE".to_string(), "FUNCTION".to_string()];
let routine_objects = schema::list_objects_core(
&state,
"opengauss-live",
&database,
schema_name,
None,
None,
None,
Some(routine_types.as_slice()),
None,
)
.await
.unwrap();
println!(
"[live] routine objects (top-level) = {:?}",
routine_objects.iter().map(|object| object.name.as_str()).collect::<Vec<_>>()
);
assert!(
!routine_objects.iter().any(|object| object.name == "log_message" || object.name == "add_numbers"),
"package members must not surface as top-level routines"
);
let package_search = schema::completion_assistant_search_core(
&state,
db::CompletionAssistantRequest {
connection_id: "opengauss-live".to_string(),
database: database.clone(),
schema: Some(schema_name.to_string()),
object_kinds: vec![db::CompletionAssistantObjectKind::Routine],
mask: pkg.to_string(),
case_sensitive: false,
global_search: false,
max_results: Some(50),
search_in_comments: false,
search_in_definitions: false,
parent_schema: None,
parent_name: None,
match_mode: Some(db::CompletionAssistantMatchMode::Prefix),
},
)
.await
.unwrap();
println!(
"[live] unqualified package candidates = {:?}",
package_search
.candidates
.iter()
.map(|candidate| (&candidate.name, &candidate.kind, &candidate.data_type))
.collect::<Vec<_>>()
);
assert!(
package_search.candidates.iter().any(|candidate| {
candidate.name == pkg
&& candidate.kind == db::CompletionAssistantCandidateKind::Object
&& candidate.data_type.as_deref() == Some("PACKAGE")
}),
"package name should be offered as an unqualified completion candidate"
);
let member_search = schema::completion_assistant_search_core(
&state,
db::CompletionAssistantRequest {
connection_id: "opengauss-live".to_string(),
database: database.clone(),
schema: Some(schema_name.to_string()),
object_kinds: vec![db::CompletionAssistantObjectKind::Routine],
mask: String::new(),
case_sensitive: false,
global_search: false,
max_results: Some(50),
search_in_comments: false,
search_in_definitions: false,
parent_schema: Some(schema_name.to_string()),
parent_name: Some(pkg.to_string()),
match_mode: Some(db::CompletionAssistantMatchMode::Prefix),
},
)
.await
.unwrap();
println!(
"[live] package member candidates = {:?}",
member_search
.candidates
.iter()
.map(|candidate| (&candidate.name, &candidate.kind, &candidate.signature))
.collect::<Vec<_>>()
);
assert!(!member_search.fallback_used, "package members should not fall back to top-level routines");
assert!(member_search.candidates.iter().any(|candidate| {
candidate.name == "log_message" && candidate.kind == db::CompletionAssistantCandidateKind::Procedure
}));
assert!(member_search.candidates.iter().any(|candidate| {
candidate.name == "add_numbers" && candidate.kind == db::CompletionAssistantCandidateKind::Function
}));
let spec_source = schema::get_object_source_core(
&state,
"opengauss-live",
&database,
schema_name,
pkg,
db::ObjectSourceKind::Package,
None,
None,
)
.await
.unwrap();
let body_source = schema::get_object_source_core(
&state,
"opengauss-live",
&database,
schema_name,
pkg,
db::ObjectSourceKind::PackageBody,
None,
None,
)
.await
.unwrap();
println!("[live] spec source length = {}", spec_source.source.len());
println!("[live] body source length = {}", body_source.source.len());
println!("[live] spec source =\n{}", spec_source.source);
println!("[live] body source =\n{}", body_source.source);
assert!(spec_source.source.to_uppercase().contains("PACKAGE"));
assert!(body_source.source.to_uppercase().contains("PACKAGE BODY"));
assert_eq!(spec_source.editable, Some(false));
assert_eq!(body_source.editable, Some(false));
// Catalog and gs_source lookups are case-insensitive for unquoted names.
let upper = schema::get_object_source_core(
&state,
"opengauss-live",
&database,
schema_name,
&pkg.to_uppercase(),
db::ObjectSourceKind::Package,
None,
None,
)
.await;
println!("[live] uppercase lookup ok = {}", upper.is_ok());
assert!(upper.is_ok(), "case-insensitive package source lookup failed: {upper:?}");
// Force the catalog-rebuild fallback by removing the gs_source rows, then
// prove the rebuilt spec/body are valid DDL by re-executing them.
let _ =
db::postgres::execute_query(&pool, &format!("DELETE FROM dbe_pldeveloper.gs_source WHERE name = '{pkg}'"))
.await;
let fallback_spec = db::postgres::opengauss_package_source(&pool, schema_name, pkg, false).await.unwrap();
let fallback_body = db::postgres::opengauss_package_source(&pool, schema_name, pkg, true).await.unwrap();
println!("[live] fallback spec =\n{fallback_spec}");
println!("[live] fallback body =\n{fallback_body}");
assert!(fallback_spec.contains("AUTHID"), "rebuilt spec must preserve the authid clause");
assert!(fallback_body.contains("g_version := '1.1'"), "rebuilt body must keep the initialization section");
db::postgres::execute_query(&pool, &fallback_spec)
.await
.unwrap_or_else(|error| panic!("rebuilt spec is not valid DDL: {error}\n{fallback_spec}"));
db::postgres::execute_query(&pool, &fallback_body)
.await
.unwrap_or_else(|error| panic!("rebuilt body is not valid DDL: {error}\n{fallback_body}"));
println!("[live] rebuilt fallback DDL re-executed successfully");
let _ = db::postgres::execute_query(&pool, &format!("DROP PACKAGE BODY IF EXISTS {schema_name}.{pkg}")).await;
let _ = db::postgres::execute_query(&pool, &format!("DROP PACKAGE IF EXISTS {schema_name}.{pkg}")).await;
let remaining = db::postgres::list_opengauss_packages(&pool, schema_name, true, true).await.unwrap();
assert!(!remaining.iter().any(|object| object.name == pkg), "cleanup left the test package behind");
println!("[live] cleanup verified: test package removed");
pool.close();
let _ = std::fs::remove_dir_all(dir);
}
#[tokio::test]
#[ignore = "requires a reachable KWDB instance via environment variables"]
async fn live_kwdb_native_connection_succeeds() {
@@ -244,6 +244,7 @@ pub async fn list_databases(client: &Influxdb3Client) -> Result<Vec<DatabaseInfo
comment: None,
default_charset: None,
default_collation: None,
compatibility_mode: None,
})
})
.collect())
+722 -43
View File
@@ -59,6 +59,16 @@ pub(crate) fn gaussdb_identifier_quote_for_compatibility_mode(compatibility_mode
}
}
pub async fn opengauss_compatibility_mode(pool: &Pool) -> Result<Option<String>, String> {
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let row = postgres_query_one_cached(&client, GAUSSDB_COMPATIBILITY_SQL, &[]).await.map_err(|e| e.to_string())?;
Ok(row.try_get::<_, Option<String>>(0).ok().flatten().filter(|value| !value.trim().is_empty()))
}
pub async fn opengauss_is_oracle_compatible(pool: &Pool) -> Result<bool, String> {
Ok(opengauss_compatibility_mode(pool).await?.is_some_and(|mode| mode.trim().eq_ignore_ascii_case("A")))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PostgresTablePrivilegeInfo {
pub grantor: String,
@@ -3290,6 +3300,13 @@ fn list_databases_sql() -> &'static str {
ORDER BY datname"
}
fn list_opengauss_databases_sql() -> &'static str {
"SELECT datname, datcompatibility::text \
FROM pg_catalog.pg_database \
WHERE datallowconn = true \
ORDER BY datname"
}
fn database_metadata_sql() -> &'static str {
"SELECT d.datname, \
CASE \
@@ -3334,6 +3351,40 @@ pub async fn list_databases(pool: &Pool) -> Result<Vec<DatabaseInfo>, String> {
Ok(rows.iter().map(|row| DatabaseInfo { name: pg_row_try_string(row, 0), ..Default::default() }).collect())
}
pub async fn list_opengauss_databases(pool: &Pool) -> Result<Vec<DatabaseInfo>, String> {
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let rows = match postgres_query_cached(&client, list_opengauss_databases_sql(), &[]).await {
Ok(rows) => rows,
Err(primary_error) => {
let primary_error = primary_error.to_string();
return postgres_query_cached(&client, list_databases_sql(), &[])
.await
.map(|rows| {
rows.iter()
.map(|row| DatabaseInfo { name: pg_row_try_string(row, 0), ..Default::default() })
.collect()
})
.map_err(|fallback_error| {
format!(
"openGauss database compatibility lookup failed: {primary_error}; database list fallback failed: {fallback_error}"
)
});
}
};
Ok(rows
.iter()
.map(|row| DatabaseInfo {
name: pg_row_try_string(row, 0),
compatibility_mode: row
.try_get::<_, Option<String>>(1)
.ok()
.flatten()
.filter(|value| !value.trim().is_empty()),
..Default::default()
})
.collect())
}
pub async fn list_database_metadata(pool: &Pool) -> Result<Vec<DatabaseInfo>, String> {
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let rows = postgres_query_cached(&client, database_metadata_sql(), &[]).await.map_err(|e| e.to_string())?;
@@ -3437,6 +3488,25 @@ async fn list_tables_filtered_by_kind(
pub async fn completion_assistant_search(
pool: &Pool,
request: &CompletionAssistantRequest,
) -> Result<CompletionAssistantResponse, String> {
completion_assistant_search_inner(pool, request, false).await
}
/// openGauss variant of [`completion_assistant_search`]: top-level routines
/// exclude package members (`propackage = true`) and private members
/// (`proisprivate = true`), which are surfaced under their PACKAGE nodes by
/// [`opengauss_package_members`] instead.
pub async fn opengauss_completion_assistant_search(
pool: &Pool,
request: &CompletionAssistantRequest,
) -> Result<CompletionAssistantResponse, String> {
completion_assistant_search_inner(pool, request, true).await
}
async fn completion_assistant_search_inner(
pool: &Pool,
request: &CompletionAssistantRequest,
exclude_package_members: bool,
) -> Result<CompletionAssistantResponse, String> {
let schema = request.schema.as_deref().or(request.parent_schema.as_deref());
let routine_schema = schema.unwrap_or("public");
@@ -3506,13 +3576,49 @@ pub async fn completion_assistant_search(
}
}
if candidates.len() < limit
&& exclude_package_members
&& request.parent_name.as_deref().is_none_or(|name| name.trim().is_empty())
&& kinds.iter().any(CompletionAssistantObjectKind::is_routine_like)
{
let rows = match postgres_query_cached(
&client,
opengauss_completion_packages_sql(),
&[&routine_schema, &pattern, &((limit - candidates.len()) as i64)],
)
.await
{
Ok(rows) => rows,
Err(error) if opengauss_optional_package_catalog_error(&error.to_string()) => Vec::new(),
Err(error) => return Err(error.to_string()),
};
for row in rows {
candidates.push(CompletionAssistantCandidate {
name: pg_row_try_string(&row, 0),
kind: CompletionAssistantCandidateKind::Object,
database: Some(request.database.clone()),
schema: Some(pg_row_try_string(&row, 1)),
parent_schema: None,
parent_name: None,
comment: None,
data_type: Some("PACKAGE".to_string()),
signature: None,
});
}
}
if candidates.len() < limit && kinds.iter().any(CompletionAssistantObjectKind::is_routine_like) {
let has_proc_prokind = postgres_proc_has_prokind(&client).await?;
let rows = if has_proc_prokind {
let prokinds = postgres_completion_prokinds(&kinds);
let sql = if exclude_package_members {
opengauss_completion_routines_sql(true)
} else {
postgres_completion_routines_sql(true).to_string()
};
postgres_query_cached(
&client,
postgres_completion_routines_sql(true),
&sql,
&[&routine_schema, &pattern, &prokinds, &((limit - candidates.len()) as i64)],
)
.await
@@ -3520,13 +3626,14 @@ pub async fn completion_assistant_search(
} else if kinds.iter().any(|kind| {
matches!(kind, CompletionAssistantObjectKind::Function | CompletionAssistantObjectKind::Routine)
}) {
postgres_query_cached(
&client,
postgres_completion_routines_sql(false),
&[&routine_schema, &pattern, &((limit - candidates.len()) as i64)],
)
.await
.map_err(|e| e.to_string())?
let sql = if exclude_package_members {
opengauss_completion_routines_sql(false)
} else {
postgres_completion_routines_sql(false).to_string()
};
postgres_query_cached(&client, &sql, &[&routine_schema, &pattern, &((limit - candidates.len()) as i64)])
.await
.map_err(|e| e.to_string())?
} else {
Vec::new()
};
@@ -3655,6 +3762,30 @@ fn postgres_completion_routines_sql(has_proc_prokind: bool) -> &'static str {
ORDER BY p.proname LIMIT $3"
}
/// openGauss variant of [`postgres_completion_routines_sql`]: package members
/// (`propackage = true`) and private members (`proisprivate = true`) are stored
/// in the same `pg_proc` rows, but must not surface as top-level routines.
/// The package/private columns only exist on openGauss kernels; plain
/// PostgreSQL and the Redshift completion path never use this variant, so the
/// extra predicates can be injected unconditionally here.
fn opengauss_completion_routines_sql(has_proc_prokind: bool) -> String {
postgres_completion_routines_sql(has_proc_prokind).replace(
"WHERE n.nspname = $1",
"WHERE n.nspname = $1 AND COALESCE(p.propackage, false) = false \
AND COALESCE(p.proisprivate, false) = false",
)
}
fn opengauss_completion_packages_sql() -> &'static str {
"SELECT p.pkgname::text, n.nspname::text \
FROM pg_catalog.gs_package p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pkgnamespace \
WHERE n.nspname = $1 \
AND EXISTS (SELECT 1 FROM pg_catalog.pg_database d WHERE d.datname = current_database() AND d.datcompatibility::text = 'A') \
AND ($2 = '%%' OR p.pkgname ILIKE $2 ESCAPE '~') \
ORDER BY p.pkgname LIMIT $3"
}
fn postgres_completion_sequences_sql() -> &'static str {
"SELECT c.relname, n.nspname, obj_description(c.oid) AS sequence_comment \
FROM pg_catalog.pg_class c \
@@ -5344,13 +5475,24 @@ fn list_objects_sql(
include_relations: bool,
include_routines: bool,
include_custom_types: bool,
exclude_package_members: bool,
) -> String {
let mut parts: Vec<String> = Vec::new();
if include_relations {
parts.push(list_object_relations_sql(include_timestamps).to_string());
}
if include_routines {
parts.push(list_object_routines_sql(include_timestamps, has_proc_prokind, has_proc_prosp).to_string());
let mut routine = list_object_routines_sql(include_timestamps, has_proc_prokind, has_proc_prosp).to_string();
// openGauss stores package members in pg_proc (propackage/propackageid).
// They are listed under their PACKAGE nodes by list_opengauss_packages, so
// exclude them from the top-level routine list to avoid duplicate entries.
// The column only exists on openGauss kernels; plain PostgreSQL never
// passes exclude_package_members=true.
if exclude_package_members {
routine = routine
.replace("WHERE n.nspname = $1", "WHERE n.nspname = $1 AND COALESCE(p.propackage, false) = false");
}
parts.push(routine);
}
if include_custom_types {
parts.push(list_object_custom_types_sql().to_string());
@@ -5448,6 +5590,7 @@ async fn list_objects_rows(
include_relations: bool,
include_routines: bool,
include_custom_types: bool,
exclude_package_members: bool,
) -> Result<Vec<Row>, String> {
let sql = list_objects_sql(
include_timestamps,
@@ -5457,6 +5600,7 @@ async fn list_objects_rows(
include_relations,
include_routines,
include_custom_types,
exclude_package_members,
);
if sql.is_empty() {
// No branch was selected (e.g. an object_types filter the catalog
@@ -5501,6 +5645,7 @@ pub async fn list_objects(
include_relations,
include_routines,
include_custom_types,
false,
)
.await
{
@@ -5517,6 +5662,7 @@ pub async fn list_objects(
include_relations,
include_routines,
include_custom_types,
false,
)
.await
{
@@ -5531,6 +5677,424 @@ pub async fn list_objects(
Ok(object_rows_to_infos(&rows, schema))
}
/// openGauss variant of [`list_objects`]: top-level routines exclude package
/// members (`propackage = true`), which are surfaced under their PACKAGE nodes
/// by [`list_opengauss_packages`] instead.
pub async fn list_opengauss_objects(
pool: &Pool,
schema: &str,
include_relations: bool,
include_routines: bool,
include_custom_types: bool,
) -> Result<Vec<ObjectInfo>, String> {
let include_custom_types = include_custom_types && !is_postgres_system_schema(schema);
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let (has_proc_prokind, has_proc_prosp, has_function_identity_arguments) = if include_routines {
let has_proc_prokind = postgres_proc_has_prokind(&client).await?;
let has_proc_prosp = postgres_proc_has_prosp(&client).await?;
let has_function_identity_arguments = postgres_has_function_identity_arguments(&client).await?;
(has_proc_prokind, has_proc_prosp, has_function_identity_arguments)
} else {
(false, false, false)
};
let rows = match list_objects_rows(
&client,
schema,
true,
has_proc_prokind,
has_proc_prosp,
has_function_identity_arguments,
include_relations,
include_routines,
include_custom_types,
true,
)
.await
{
Ok(rows) => rows,
Err(primary_error) => {
log::debug!("[postgres][list_opengauss_objects:timestamp-fallback] primary_error={}", primary_error);
match list_objects_rows(
&client,
schema,
false,
has_proc_prokind,
has_proc_prosp,
has_function_identity_arguments,
include_relations,
include_routines,
include_custom_types,
true,
)
.await
{
Ok(rows) => rows,
Err(fallback_error) => {
return Err(format!("{primary_error}; timestamp fallback failed: {fallback_error}"));
}
}
}
};
Ok(object_rows_to_infos(&rows, schema))
}
pub async fn list_opengauss_packages(
pool: &Pool,
schema: &str,
include_spec: bool,
include_body: bool,
) -> Result<Vec<ObjectInfo>, String> {
if !include_spec && !include_body {
return Ok(Vec::new());
}
match opengauss_is_oracle_compatible(pool).await {
Ok(true) => {}
Ok(false) | Err(_) => return Ok(Vec::new()),
}
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let rows = match postgres_query_cached(
&client,
"SELECT p.pkgname::text, \
(p.pkgbodydeclsrc IS NOT NULL OR p.pkgbodyinitsrc IS NOT NULL) AS has_body \
FROM pg_catalog.gs_package p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pkgnamespace \
WHERE n.nspname = $1 \
ORDER BY p.pkgname",
&[&schema],
)
.await
{
Ok(rows) => rows,
Err(error) if opengauss_optional_package_catalog_error(&error.to_string()) => return Ok(Vec::new()),
Err(error) => return Err(error.to_string()),
};
let mut objects = Vec::with_capacity(rows.len() * 2);
for row in rows {
let name = pg_row_try_string(&row, 0);
let has_body = pg_row_try_bool(&row, 1).unwrap_or(false);
let object = |object_type: &str| ObjectInfo {
name: name.clone(),
object_type: object_type.to_string(),
schema: Some(schema.to_string()),
valid: None,
signature: None,
custom_type_kind: None,
has_members: None,
comment: None,
created_at: None,
updated_at: None,
parent_schema: None,
parent_name: None,
trigger: None,
xugu_type_members_expandable: None,
};
if include_spec {
objects.push(object("PACKAGE"));
}
if include_body && has_body {
objects.push(object("PACKAGE_BODY"));
}
}
Ok(objects)
}
fn opengauss_optional_package_catalog_error(error: &str) -> bool {
let lower = error.to_ascii_lowercase();
lower.contains("permission denied")
|| lower.contains("insufficient privilege")
|| lower.contains("does not exist")
|| lower.contains("undefined table")
|| lower.contains("undefined column")
|| lower.contains("undefined schema")
}
/// Whether an openGauss gs_source lookup error means the catalog is not usable
/// (missing relation/schema, or insufficient privileges) so the gs_package
/// fallback should be attempted instead of surfacing the error. Connection
/// failures and protocol errors are not swallowed.
fn opengauss_gs_source_lookup_should_fallback(error: &str) -> bool {
opengauss_optional_package_catalog_error(error)
}
pub async fn opengauss_package_source(
pool: &Pool,
schema: &str,
name: &str,
package_body: bool,
) -> Result<String, String> {
match opengauss_is_oracle_compatible(pool).await {
Ok(true) => {}
Ok(false) => {
return Err("openGauss packages are only available in A compatibility mode".to_string());
}
Err(_) => {}
}
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
let source_type = if package_body { "package body" } else { "package" };
let rows = match postgres_query_cached(
&client,
"SELECT s.src::text \
FROM dbe_pldeveloper.gs_source s \
JOIN pg_catalog.gs_package p ON p.oid = s.id \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pkgnamespace \
WHERE lower(n.nspname) = lower($1) \
AND lower(p.pkgname::text) = lower($2) \
AND lower(s.type) = $3 \
AND s.status = true \
ORDER BY (p.pkgname::text = $2) DESC, p.oid \
LIMIT 1",
&[&schema, &name, &source_type],
)
.await
{
Ok(rows) => rows,
Err(error) => {
let error = error.to_string();
if opengauss_gs_source_lookup_should_fallback(&error) {
Vec::new()
} else {
return Err(error);
}
}
};
if let Some(source) = rows.first().map(|row| pg_row_try_string(row, 0)).filter(|source| !source.trim().is_empty()) {
return Ok(source);
}
// dbe_pldeveloper.gs_source is written asynchronously and can be absent when
// SKIP_GS_SOURCE is set, autonomous-transaction limits are hit, a compile
// failed, or the current user lacks privileges on the schema. Rebuild the
// source from the gs_package fragment columns so an existing package can
// still be viewed and exported. When the stored shape is unrecognised we
// fail loudly instead of emitting a broken CREATE statement.
let meta_rows = postgres_query_cached(
&client,
"SELECT p.pkgname::text, n.nspname::text, p.pkgsecdef, \
p.pkgspecsrc::text, p.pkgbodydeclsrc::text, p.pkgbodyinitsrc::text \
FROM pg_catalog.gs_package p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pkgnamespace \
WHERE lower(n.nspname) = lower($1) AND lower(p.pkgname::text) = lower($2) \
ORDER BY (p.pkgname::text = $2) DESC, (n.nspname::text = $1) DESC, p.oid \
LIMIT 1",
&[&schema, &name],
)
.await
.map_err(|e| e.to_string())?;
let Some(row) = meta_rows.first() else {
return Err(format!("openGauss {} source not found: {schema}.{name}", source_type));
};
let catalog_name = row
.try_get::<_, Option<String>>(0)
.ok()
.flatten()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| name.to_string());
let catalog_schema = row
.try_get::<_, Option<String>>(1)
.ok()
.flatten()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| schema.to_string());
let pkgsecdef = row.try_get::<_, bool>(2).map_err(|error| {
format!("Cannot read openGauss package security mode for {catalog_schema}.{catalog_name}: {error}")
})?;
let spec = row.try_get::<_, Option<String>>(3).ok().flatten().unwrap_or_default();
let body_decl = row.try_get::<_, Option<String>>(4).ok().flatten().unwrap_or_default();
let body_init = row.try_get::<_, Option<String>>(5).ok().flatten().unwrap_or_default();
if package_body {
opengauss_package_body_source(&catalog_schema, &catalog_name, &body_decl, &body_init).ok_or_else(|| {
format!("Cannot rebuild openGauss package body source from catalog for {catalog_schema}.{catalog_name}")
})
} else {
opengauss_package_spec_source(&catalog_schema, &catalog_name, &spec, pkgsecdef).ok_or_else(|| {
format!("Cannot rebuild openGauss package source from catalog for {catalog_schema}.{catalog_name}")
})
}
}
/// Marker openGauss stores at the start of the declaration fragment
/// (`AS` is rewritten to this during CREATE processing).
const OPENGAUSS_PACKAGE_DECL_MARKER: &str = "PACKAGE DECLARE";
/// Marker at the start of the package initialization fragment.
const OPENGAUSS_PACKAGE_INIT_MARKER: &str = "INSTANTIATION";
/// Strip the openGauss declaration-fragment wrapper: the ` PACKAGE DECLARE `
/// prefix and the trailing bare `END` marker that closes the fragment. Member
/// terminators are always `END;` (with a semicolon) and are therefore kept.
fn trim_opengauss_package_decl_fragment(fragment: &str) -> Option<String> {
let trimmed = fragment.trim();
let body = trimmed.strip_prefix(OPENGAUSS_PACKAGE_DECL_MARKER)?.trim();
let body = body.strip_suffix("END").unwrap_or(body).trim_end();
Some(body.trim().to_string())
}
/// Rebuild a full `CREATE OR REPLACE PACKAGE` statement from the gs_package
/// declaration fragment. Used as the fallback when `dbe_pldeveloper.gs_source`
/// has no row for the package.
#[doc(hidden)]
pub fn opengauss_package_spec_source(schema: &str, name: &str, spec_fragment: &str, pkgsecdef: bool) -> Option<String> {
let decl = trim_opengauss_package_decl_fragment(spec_fragment)?;
let authid = if pkgsecdef { " AUTHID DEFINER" } else { " AUTHID CURRENT_USER" };
Some(format!(
"CREATE OR REPLACE PACKAGE {}.{}{} AS\n{}\nEND {};",
pg_quote_ident(schema),
pg_quote_ident(name),
authid,
decl,
pg_quote_ident(name)
))
}
/// Rebuild a full `CREATE OR REPLACE PACKAGE BODY` statement from the
/// gs_package declaration and initialization fragments.
#[doc(hidden)]
pub fn opengauss_package_body_source(
schema: &str,
name: &str,
decl_fragment: &str,
init_fragment: &str,
) -> Option<String> {
let decl = trim_opengauss_package_decl_fragment(decl_fragment)?;
let mut out = format!("CREATE OR REPLACE PACKAGE BODY {}.{} AS\n", pg_quote_ident(schema), pg_quote_ident(name));
if !decl.is_empty() {
out.push_str(&decl);
out.push('\n');
}
let init = init_fragment.trim();
if init.is_empty() {
// No initialization section: the declaration fragment's own trailing
// END marker was already removed, so close the body here.
out.push_str(&format!("END {};", pg_quote_ident(name)));
return Some(out);
}
// Initialization section is stored as `INSTANTIATION \nBEGIN ... END`.
let init = init.strip_prefix(OPENGAUSS_PACKAGE_INIT_MARKER)?.trim_start();
// The trailing `END` of the init fragment is the body terminator; give it
// the package name so the rebuilt DDL stays self-contained.
let core = init.strip_suffix("END").unwrap_or(init).trim_end();
out.push_str(core);
out.push('\n');
out.push_str(&format!("END {};", pg_quote_ident(name)));
Some(out)
}
/// SQL for the package identity lookup used by [`opengauss_package_members`].
/// Prefer an exact spelling, while retaining a case-insensitive fallback for
/// ordinary unquoted identifiers typed with a different case.
fn opengauss_package_identity_sql() -> &'static str {
"SELECT p.oid::oid, p.pkgname::text, n.nspname::text \
FROM pg_catalog.gs_package p \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pkgnamespace \
WHERE lower(n.nspname) = lower($1) AND lower(p.pkgname::text) = lower($2) \
ORDER BY (p.pkgname::text = $2) DESC, (n.nspname::text = $1) DESC, p.oid \
LIMIT 1"
}
/// SQL for [`opengauss_package_members`]: members selected by the resolved
/// package OID, preventing same-lowercase quoted packages from being merged.
fn opengauss_package_members_sql() -> &'static str {
"SELECT f.proname::text, f.prokind::text, \
pg_catalog.pg_get_function_identity_arguments(f.oid)::text, \
pg_catalog.format_type(f.prorettype, NULL)::text \
FROM pg_catalog.pg_proc f \
JOIN pg_catalog.gs_package p ON p.oid = f.propackageid \
JOIN pg_catalog.pg_namespace n ON n.oid = p.pkgnamespace \
WHERE f.propackageid = $1 \
AND f.prokind IN ('f','p','v','b','i') \
AND COALESCE(f.proisprivate, false) = false \
AND ($2 = '%%' OR f.proname ILIKE $2 ESCAPE '~') \
ORDER BY f.proname, f.oid LIMIT $3"
}
pub async fn opengauss_package_members(
pool: &Pool,
request: &CompletionAssistantRequest,
) -> Result<CompletionAssistantResponse, String> {
match opengauss_is_oracle_compatible(pool).await {
Ok(true) => {}
Ok(false) | Err(_) => {
return Ok(CompletionAssistantResponse { candidates: Vec::new(), incomplete: false, fallback_used: true });
}
}
let package_name = request.parent_name.as_deref().unwrap_or("").trim();
if package_name.is_empty() {
return Ok(CompletionAssistantResponse { candidates: Vec::new(), incomplete: false, fallback_used: true });
}
let schema = request.parent_schema.as_deref().or(request.schema.as_deref()).unwrap_or("public");
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
// Resolve one package identity before looking up members. Lowercase matching
// remains useful for ordinary unquoted names, while the exact-name ordering
// preserves quoted mixed-case package identity when it is available.
let identity_rows =
match postgres_query_cached(&client, opengauss_package_identity_sql(), &[&schema, &package_name]).await {
Ok(rows) => rows,
Err(error) if opengauss_optional_package_catalog_error(&error.to_string()) => {
return Ok(CompletionAssistantResponse {
candidates: Vec::new(),
incomplete: false,
fallback_used: true,
});
}
Err(error) => return Err(error.to_string()),
};
let Some(identity_row) = identity_rows.first() else {
return Ok(CompletionAssistantResponse { candidates: Vec::new(), incomplete: false, fallback_used: true });
};
let Some(package_oid) = pg_row_try_u32(identity_row, 0) else {
return Ok(CompletionAssistantResponse { candidates: Vec::new(), incomplete: false, fallback_used: true });
};
let catalog_name = pg_row_try_string(identity_row, 1);
let catalog_schema = pg_row_try_string(identity_row, 2);
let limit = request.max_results.unwrap_or(100).clamp(1, 1000);
// The member prefix mask is applied on the server before the LIMIT so a
// matching member is never shadowed by the first N rows of the full
// alphabetically-sorted member list (the editor only requests a limited
// slice and then filters locally).
let pattern = postgres_completion_like_pattern(&request.mask, request.match_mode.as_ref());
let rows = match postgres_query_cached(
&client,
opengauss_package_members_sql(),
&[&package_oid, &pattern, &(limit as i64)],
)
.await
{
Ok(rows) => rows,
Err(error) if opengauss_optional_package_catalog_error(&error.to_string()) => {
return Ok(CompletionAssistantResponse { candidates: Vec::new(), incomplete: false, fallback_used: true });
}
Err(error) => return Err(error.to_string()),
};
let candidates = rows
.iter()
.map(|row| {
let prokind = pg_row_try_string(row, 1);
// openGauss catalog reports ordinary f/p members on current releases.
// v (subfunction) / b (subprocedure) are kept defensive for
// variants that tag package members with subkind markers.
let kind = if matches!(prokind.as_str(), "p" | "b") {
CompletionAssistantCandidateKind::Procedure
} else {
CompletionAssistantCandidateKind::Function
};
CompletionAssistantCandidate {
name: pg_row_try_string(row, 0),
kind,
database: Some(request.database.clone()),
schema: Some(catalog_schema.clone()),
parent_schema: Some(catalog_schema.clone()),
parent_name: Some(catalog_name.clone()),
comment: None,
data_type: row.try_get::<_, Option<String>>(3).ok().flatten(),
signature: row.try_get::<_, Option<String>>(2).ok().flatten(),
}
})
.collect::<Vec<_>>();
Ok(CompletionAssistantResponse { incomplete: candidates.len() >= limit, candidates, fallback_used: false })
}
pub async fn list_redshift_objects(
pool: &Pool,
schema: &str,
@@ -5543,7 +6107,7 @@ pub async fn list_redshift_objects(
if include_relations {
// Redshift does not support PostgreSQL's generic file, object-location,
// or transaction-ID helpers, so skip the timestamp variant entirely.
rows = list_objects_rows(&client, schema, false, false, false, false, true, false, false).await?;
rows = list_objects_rows(&client, schema, false, false, false, false, true, false, false, false).await?;
}
if include_routines {
@@ -9357,6 +9921,79 @@ mod tests {
assert_eq!(gaussdb_identifier_quote_for_compatibility_mode(""), None);
}
#[test]
fn opengauss_package_catalog_fallback_rebuilds_spec_ddl() {
// Real shape captured from openGauss-lite 5.0.1 gs_package.pkgspecsrc.
let fragment = " PACKAGE DECLARE g_version VARCHAR2(20) := '1.0';\n PROCEDURE log_message(p_message IN VARCHAR2);\n FUNCTION add_numbers(p_left IN INTEGER, p_right IN INTEGER) RETURN INTEGER;\nEND ";
let ddl = opengauss_package_spec_source("public", "dbx_test_pkg_math", fragment, false).unwrap();
assert!(ddl.starts_with("CREATE OR REPLACE PACKAGE \"public\".\"dbx_test_pkg_math\" AUTHID CURRENT_USER AS\n"));
assert!(ddl.contains("AUTHID CURRENT_USER"));
assert!(ddl.contains("g_version VARCHAR2(20) := '1.0';"));
assert!(ddl.contains("PROCEDURE log_message(p_message IN VARCHAR2);"));
assert!(ddl.ends_with("END \"dbx_test_pkg_math\";"));
assert!(!ddl.contains("END "));
assert_eq!(ddl.matches("END").count(), 1);
}
#[test]
fn opengauss_package_catalog_fallback_preserves_definer_security() {
let fragment = " PACKAGE DECLARE PROCEDURE ping;\nEND ";
let ddl = opengauss_package_spec_source("public", "secure_pkg", fragment, true).unwrap();
assert!(ddl.contains("AUTHID DEFINER"));
assert!(!ddl.contains("AUTHID CURRENT_USER"));
}
#[test]
fn opengauss_package_catalog_fallback_rebuilds_body_with_init_ddl() {
// Real shapes from gs_package.pkgbodydeclsrc / pkgbodyinitsrc.
let decl = " PACKAGE DECLARE PROCEDURE log_message(p_message IN VARCHAR2) IS\n BEGIN\n NULL;\n END;\n\n FUNCTION add_numbers(p_left IN INTEGER, p_right IN INTEGER) RETURN INTEGER IS\n BEGIN\n RETURN p_left + p_right;\n END;\n\nEND\n";
let init = " INSTANTIATION \nBEGIN\n g_version := '1.1';\nEND\n";
let ddl = opengauss_package_body_source("public", "dbx_test_pkg_math", decl, init).unwrap();
assert!(ddl.starts_with("CREATE OR REPLACE PACKAGE BODY \"public\".\"dbx_test_pkg_math\" AS\n"));
assert!(ddl.contains("PROCEDURE log_message(p_message IN VARCHAR2) IS"));
assert!(ddl.contains("FUNCTION add_numbers(p_left IN INTEGER, p_right IN INTEGER) RETURN INTEGER IS"));
assert!(ddl.contains("g_version := '1.1';"));
assert!(ddl.ends_with("END \"dbx_test_pkg_math\";"));
}
#[test]
fn opengauss_package_catalog_fallback_rebuilds_body_without_init_ddl() {
let decl = " PACKAGE DECLARE FUNCTION format_value(p_value IN INTEGER) RETURN VARCHAR2 IS\n BEGIN\n RETURN 'x';\n END;\nEND ";
let ddl = opengauss_package_body_source("public", "dbx_test_pkg_overload", decl, "").unwrap();
assert!(ddl.starts_with("CREATE OR REPLACE PACKAGE BODY \"public\".\"dbx_test_pkg_overload\" AS\n"));
assert!(ddl.contains("FUNCTION format_value(p_value IN INTEGER) RETURN VARCHAR2 IS"));
assert!(ddl.ends_with("END \"dbx_test_pkg_overload\";"));
assert!(!ddl.contains("INSTANTIATION"));
}
#[test]
fn opengauss_package_catalog_fallback_rejects_unrecognized_shape() {
assert!(opengauss_package_spec_source("public", "pkg", "CREATE OR REPLACE PACKAGE pkg AS\nEND pkg;", true)
.is_none());
assert!(opengauss_package_spec_source("public", "pkg", "", true).is_none());
}
#[test]
fn opengauss_gs_source_error_classification_falls_back_only_for_catalog_issues() {
for message in [
"ERROR: permission denied for schema dbe_pldeveloper",
"ERROR: insufficient privilege to query table gs_source",
"ERROR: relation \"gs_source\" does not exist",
"ERROR: schema \"dbe_pldeveloper\" does not exist",
] {
assert!(opengauss_gs_source_lookup_should_fallback(message), "expected fallback for: {message}");
}
for message in [
"connection refused",
"server closed the connection unexpectedly",
"timeout expired",
"canceling statement due to statement timeout",
"syntax error at or near \"SELECT\"",
] {
assert!(!opengauss_gs_source_lookup_should_fallback(message), "expected no fallback for: {message}");
}
}
fn test_query_message(message: &str) -> QueryMessage {
QueryMessage {
severity: "NOTICE".to_string(),
@@ -13706,7 +14343,7 @@ mod tests {
#[test]
fn list_objects_sql_includes_routines() {
let sql = list_objects_sql(true, true, false, true, true, true, false);
let sql = list_objects_sql(true, true, false, true, true, true, false, false);
assert!(sql.contains("pg_catalog.pg_class"));
assert!(sql.contains("pg_catalog.pg_proc"));
assert!(sql.contains("pg_catalog.pg_inherits"));
@@ -13724,7 +14361,7 @@ mod tests {
#[test]
fn list_objects_sql_includes_custom_types_when_enabled() {
let sql = list_objects_sql(true, true, false, true, true, true, true);
let sql = list_objects_sql(true, true, false, true, true, true, true, false);
assert!(sql.contains("pg_catalog.pg_type"));
assert!(sql.contains("pg_catalog.pg_class"));
assert!(sql.contains("pg_catalog.pg_description"));
@@ -13755,14 +14392,14 @@ mod tests {
#[test]
fn list_objects_sql_omits_custom_types_when_disabled() {
let sql = list_objects_sql(true, true, false, true, true, true, false);
let sql = list_objects_sql(true, true, false, true, true, true, false, false);
assert!(!sql.contains("pg_type"));
assert!(!sql.contains("t.typtype"));
}
#[test]
fn list_objects_sql_type_only_skips_relations_and_routines() {
let sql = list_objects_sql(true, true, false, true, false, false, true);
let sql = list_objects_sql(true, true, false, true, false, false, true, false);
assert!(sql.contains("pg_catalog.pg_type"));
assert!(!sql.contains("FROM pg_catalog.pg_class"));
assert!(!sql.contains("pg_catalog.pg_proc"));
@@ -13772,7 +14409,7 @@ mod tests {
#[test]
fn list_objects_sql_table_only_skips_routines_and_custom_types() {
let sql = list_objects_sql(true, true, false, true, true, false, false);
let sql = list_objects_sql(true, true, false, true, true, false, false, false);
assert!(sql.contains("pg_catalog.pg_class"));
assert!(!sql.contains("pg_catalog.pg_proc"));
assert!(!sql.contains("pg_catalog.pg_type"));
@@ -13780,7 +14417,7 @@ mod tests {
#[test]
fn list_objects_sql_routine_only_skips_relations_and_custom_types() {
let sql = list_objects_sql(true, true, false, true, false, true, false);
let sql = list_objects_sql(true, true, false, true, false, true, false, false);
assert!(sql.contains("pg_catalog.pg_proc"));
assert!(!sql.contains("pg_catalog.pg_class"));
assert!(!sql.contains("pg_catalog.pg_type"));
@@ -13788,7 +14425,7 @@ mod tests {
#[test]
fn list_objects_sql_empty_scope_produces_no_sql() {
let sql = list_objects_sql(true, true, false, true, false, false, false);
let sql = list_objects_sql(true, true, false, true, false, false, false, false);
assert!(sql.is_empty());
}
@@ -13797,8 +14434,8 @@ mod tests {
// The pg_type branch carries the same filters whether the timestamp
// query or the timestamp fallback query is used.
for sql in [
list_objects_sql(true, true, false, true, false, false, true),
list_objects_sql(false, true, false, true, false, false, true),
list_objects_sql(true, true, false, true, false, false, true, false),
list_objects_sql(false, true, false, true, false, false, true, false),
] {
assert!(sql.contains("pg_catalog.pg_type"));
assert!(sql.contains("t.typtype IN ('b', 'c', 'd', 'e', 'r', 'm')"));
@@ -13810,7 +14447,7 @@ mod tests {
#[test]
fn list_objects_sql_custom_types_keeps_comment_join_semantics() {
let sql = list_objects_sql(true, true, false, true, false, false, true);
let sql = list_objects_sql(true, true, false, true, false, false, true, false);
assert!(sql.contains("d.objoid = t.oid"));
assert!(sql.contains("d.classoid = 'pg_catalog.pg_type'::regclass"));
assert!(sql.contains("d.objsubid = 0"));
@@ -13819,7 +14456,7 @@ mod tests {
#[test]
fn list_objects_sql_custom_types_uses_null_timestamps() {
let sql = list_objects_sql(true, true, false, true, false, false, true);
let sql = list_objects_sql(true, true, false, true, false, false, true, false);
assert!(sql.contains("NULL::text AS created_at"));
assert!(sql.contains("NULL::text AS updated_at"));
assert!(sql.contains("NULL::text AS parent_schema"));
@@ -13830,7 +14467,7 @@ mod tests {
#[test]
fn list_objects_sql_without_timestamps_omits_stat_file() {
let sql = list_objects_sql(false, true, false, true, true, true, false);
let sql = list_objects_sql(false, true, false, true, true, true, false, false);
assert!(!sql.contains("pg_stat_file"));
assert!(sql.contains("NULL::text AS created_at"));
assert!(sql.contains("NULL::text AS updated_at"));
@@ -13838,7 +14475,7 @@ mod tests {
#[test]
fn redshift_compatible_list_objects_sql_uses_legacy_argument_formatter() {
let sql = list_objects_sql(false, false, false, false, true, true, false);
let sql = list_objects_sql(false, false, false, false, true, true, false, false);
assert!(sql.contains("pg_get_function_arguments(p.oid) AS signature"));
assert!(!sql.contains("pg_get_function_identity_arguments"));
}
@@ -13858,7 +14495,7 @@ mod tests {
#[test]
fn redshift_relation_objects_sql_avoids_unsupported_postgres_helpers() {
let sql = list_objects_sql(false, false, false, false, true, false, false);
let sql = list_objects_sql(false, false, false, false, true, false, false, false);
assert!(sql.contains("pg_catalog.pg_class"));
assert!(!sql.contains("pg_catalog.pg_proc"));
assert!(!sql.contains("pg_stat_file"));
@@ -13932,31 +14569,31 @@ mod tests {
#[test]
fn both_list_objects_sql_variants_use_parameter() {
assert!(list_objects_sql(true, true, true, true, true, true, false).contains("$1"));
assert!(list_objects_sql(false, true, true, true, true, true, false).contains("$1"));
assert!(list_objects_sql(true, true, false, true, true, true, false).contains("$1"));
assert!(list_objects_sql(false, true, false, true, true, true, false).contains("$1"));
assert!(list_objects_sql(true, false, true, true, true, true, false).contains("$1"));
assert!(list_objects_sql(false, false, true, true, true, true, false).contains("$1"));
assert!(list_objects_sql(true, false, false, true, true, true, false).contains("$1"));
assert!(list_objects_sql(false, false, false, true, true, true, false).contains("$1"));
assert!(list_objects_sql(true, true, true, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(false, true, true, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(true, true, false, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(false, true, false, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(true, false, true, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(false, false, true, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(true, false, false, true, true, true, false, false).contains("$1"));
assert!(list_objects_sql(false, false, false, true, true, true, false, false).contains("$1"));
}
#[test]
fn both_list_objects_sql_variants_include_pg_proc() {
assert!(list_objects_sql(true, true, true, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, true, true, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, true, false, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, true, false, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, false, true, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, false, true, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, false, false, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, false, false, true, true, true, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, true, true, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, true, true, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, true, false, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, true, false, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, false, true, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, false, true, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(true, false, false, true, true, true, false, false).contains("pg_catalog.pg_proc"));
assert!(list_objects_sql(false, false, false, true, true, true, false, false).contains("pg_catalog.pg_proc"));
}
#[test]
fn legacy_list_objects_sql_avoids_pg11_proc_kind_column() {
let sql = list_objects_sql(true, false, false, true, true, true, false);
let sql = list_objects_sql(true, false, false, true, true, true, false, false);
assert!(!sql.contains("p.prokind"));
assert!(!sql.contains("p.prosp"));
assert!(sql.contains("NOT p.proisagg"));
@@ -13968,7 +14605,7 @@ mod tests {
#[test]
fn gaussdb_compatible_list_objects_sql_uses_prosp_when_prokind_is_missing() {
let sql = list_objects_sql(true, false, true, true, true, true, false);
let sql = list_objects_sql(true, false, true, true, true, true, false, false);
assert!(!sql.contains("p.prokind"));
assert!(sql.contains("CASE WHEN p.prosp THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type"));
assert!(sql.contains("CASE WHEN p.prosp THEN 2 ELSE 3 END AS sort_order"));
@@ -13979,7 +14616,7 @@ mod tests {
#[test]
fn gaussdb_compatible_list_objects_sql_uses_prosp_with_prokind_when_available() {
let sql = list_objects_sql(true, true, true, true, true, true, false);
let sql = list_objects_sql(true, true, true, true, true, true, false, false);
assert!(
sql.contains("CASE WHEN p.prokind = 'p' OR p.prosp THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type")
);
@@ -14611,6 +15248,48 @@ mod tests {
assert!(postgres_visible_table_schema_sql().contains("pg_catalog.pg_table_is_visible(c.oid)"));
}
#[test]
fn opengauss_completion_packages_sql_filters_a_mode_and_prefix_before_limit() {
let sql = opengauss_completion_packages_sql();
assert!(sql.contains("FROM pg_catalog.gs_package"));
assert!(sql.contains("d.datcompatibility::text = 'A'"));
assert!(sql.contains("p.pkgname ILIKE $2 ESCAPE '~'"));
assert!(sql.contains("ORDER BY p.pkgname LIMIT $3"));
}
#[test]
fn opengauss_completion_routines_sql_excludes_package_and_private_members() {
// openGauss stores package members (propackage=true) and private members
// (proisprivate=true) in pg_proc; the completion variant must not surface
// them as top-level routines. See review P1 #1.
for has_proc_prokind in [true, false] {
let sql = opengauss_completion_routines_sql(has_proc_prokind);
assert!(sql.contains("COALESCE(p.propackage, false) = false"));
assert!(sql.contains("COALESCE(p.proisprivate, false) = false"));
assert!(sql.contains("WHERE n.nspname = $1"));
// The prefix mask still applies before the LIMIT.
assert!(sql.contains("$2 = '%%' OR p.proname ILIKE $2 ESCAPE '~'"));
}
assert!(opengauss_completion_routines_sql(true).contains("p.prokind::text = ANY($3::text[])"));
assert!(!opengauss_completion_routines_sql(false).contains("p.prokind"));
}
#[test]
fn opengauss_package_members_sql_uses_case_insensitive_names_and_prefix_mask() {
// Package names are normalized by the parser, so catalog and
// user-typed spellings must match case-insensitively.
// #3: the member prefix must filter on the server before LIMIT so a
// matching member is not shadowed by the first N alphabetically-sorted
// rows.
let sql = opengauss_package_members_sql();
assert!(sql.contains("WHERE f.propackageid = $1"));
assert!(sql.contains("AND f.prokind IN ('f','p','v','b','i')"));
assert!(sql.contains("COALESCE(f.proisprivate, false) = false"));
let identity = opengauss_package_identity_sql();
assert!(identity.contains("SELECT p.oid::oid, p.pkgname::text, n.nspname::text"));
assert!(identity.contains("ORDER BY (p.pkgname::text = $2) DESC"));
}
#[test]
fn postgres_schema_info_sql_only_filters_system_schemas_when_disabled() {
let hidden_sql = postgres_schema_infos_sql(false);
+72 -4
View File
@@ -793,6 +793,21 @@ async fn connection_database_type(state: &AppState, connection_id: &str) -> Opti
configs.get(connection_id).map(|config| config.db_type)
}
async fn connection_sql_compatibility_mode(
state: &AppState,
pool_key: &str,
db_type: Option<DatabaseType>,
) -> Option<String> {
if db_type != Some(DatabaseType::OpenGauss) {
return None;
}
let pool = match state.pool_handle(pool_key).await {
Some(PoolKind::Postgres(pool)) => pool,
_ => return None,
};
db::postgres::opengauss_compatibility_mode(&pool).await.ok().flatten()
}
async fn connection_mysql_query_dialect(state: &AppState, connection_id: &str) -> db::mysql::MySqlQueryDialect {
let configs = state.configs.read().await;
configs
@@ -3112,7 +3127,9 @@ pub async fn execute_multi_core_with_options_for_client_and_progress_typed(
}
};
let execution_plan = query_execution_plan(sql, db_type, is_sqlserver_agent);
let compatibility_mode = connection_sql_compatibility_mode(state, &pool_key, db_type).await;
let execution_plan =
query_execution_plan_with_compatibility(sql, db_type, is_sqlserver_agent, compatibility_mode.as_deref());
let continue_on_error = options.continue_on_error && !execution_plan.stop_on_error;
let statements = execution_plan.statements;
if statements.is_empty() {
@@ -3304,6 +3321,19 @@ pub fn query_execution_plan(
sql: &str,
db_type: Option<DatabaseType>,
preserve_sqlserver_batches: bool,
) -> crate::sql::SqlExecutionPlan {
query_execution_plan_with_compatibility(sql, db_type, preserve_sqlserver_batches, None)
}
/// Same as [`query_execution_plan`], but lets openGauss callers pass the
/// database compatibility mode so A-mode PL/SQL (package) bodies are never split
/// on inner semicolons. `None` keeps the conservative PL/SQL-capable openGauss
/// profile while the probe is cold or unavailable.
pub fn query_execution_plan_with_compatibility(
sql: &str,
db_type: Option<DatabaseType>,
preserve_sqlserver_batches: bool,
compatibility_mode: Option<&str>,
) -> crate::sql::SqlExecutionPlan {
if preserve_sqlserver_batches && db_type == Some(DatabaseType::SqlServer) {
return crate::sql::SqlExecutionPlan { statements: split_sql_batches(sql), stop_on_error: false };
@@ -3311,7 +3341,7 @@ pub fn query_execution_plan(
db_type.map_or_else(
|| crate::sql::SqlExecutionPlan { statements: split_sql_statements(sql), stop_on_error: false },
|db_type| crate::sql::sql_execution_plan_for_database(sql, db_type),
|db_type| crate::sql::sql_execution_plan_for_database_with_compatibility(sql, db_type, compatibility_mode),
)
}
@@ -4191,10 +4221,41 @@ pub async fn execute_schema_diff_deploy(
let now = chrono::Utc::now().to_rfc3339();
let db_type = connection_database_type(state, connection_id).await;
// openGauss A-mode deploy scripts may contain CREATE PACKAGE … / blocks
// that must stay intact. Probe the database compatibility mode when a pool
// is available; on failure fall back to the plain openGauss splitter.
let compatibility_mode = if db_type == Some(DatabaseType::OpenGauss) {
let pool = match state
.get_or_create_pool(connection_id, if database.is_empty() { None } else { Some(database) })
.await
{
Ok(pool_key) => match state.pool_handle(&pool_key).await {
Some(PoolKind::Postgres(pool)) => Some(pool),
_ => None,
},
Err(_) => None,
};
match pool {
Some(pool) => db::postgres::opengauss_compatibility_mode(&pool).await.ok().flatten(),
None => None,
}
} else {
None
};
let parsed: Vec<String> = statements
.iter()
.flat_map(|s| {
db_type.map_or_else(|| split_sql_statements(s), |dt| crate::sql::split_sql_statements_for_database(s, dt))
db_type.map_or_else(
|| split_sql_statements(s),
|dt| {
crate::sql::split_sql_statements_for_database_with_compatibility(
s,
dt,
compatibility_mode.as_deref(),
)
},
)
})
.map(|s| s.trim().to_string())
.filter(|s| {
@@ -5351,9 +5412,16 @@ pub async fn execute_in_manual_transaction_with_options(
};
let db_type = connection_database_type(state, &connection_id).await;
let compatibility_mode = connection_sql_compatibility_mode(state, &pool_key, db_type).await;
let statements = db_type.map_or_else(
|| split_sql_statements(sql),
|db_type| crate::sql::split_sql_statements_for_database(sql, db_type),
|db_type| {
crate::sql::split_sql_statements_for_database_with_compatibility(
sql,
db_type,
compatibility_mode.as_deref(),
)
},
);
if statements.is_empty() {
// Oracle-only UX marker: the no-op is Core's decision that the script
+96 -6
View File
@@ -766,6 +766,9 @@ async fn list_databases_once(state: &AppState, connection_id: &str) -> Result<Ve
}
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => db::ob_oracle::list_databases(p).await,
PoolKind::Mysql(p, _) => db::mysql::list_databases_with_timeout(p, mysql_database_list_timeout).await,
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss) => {
db::postgres::list_opengauss_databases(p).await
}
PoolKind::Postgres(p) => db::postgres::list_databases(p).await,
PoolKind::Sqlite(p) => db::sqlite::list_databases(p).await,
PoolKind::Rqlite(client) => db::rqlite_driver::list_databases(client).await,
@@ -5816,7 +5819,26 @@ pub async fn completion_assistant_search_core(
PoolKind::Postgres(pool) => Some(pool.clone()),
_ => None,
}) {
return db::postgres::completion_assistant_search(&pool, &request).await;
let db_config = connection_config(state, &request.connection_id).await;
if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss)
&& request.parent_name.as_deref().is_some_and(|name| !name.trim().is_empty())
&& request.object_kinds.iter().any(db::CompletionAssistantObjectKind::is_routine_like)
{
let response = db::postgres::opengauss_package_members(&pool, &request).await?;
// fallback_used marks "parent is not a package": continue with
// the ordinary routine completion instead of returning nothing.
if !response.fallback_used {
return Ok(response);
}
}
return if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss) {
// openGauss variant excludes package/private members from the
// ordinary routine lookup, so the fallback stays consistent
// with the package-aware path above.
db::postgres::opengauss_completion_assistant_search(&pool, &request).await
} else {
db::postgres::completion_assistant_search(&pool, &request).await
};
}
}
@@ -6327,9 +6349,29 @@ async fn list_objects_once(
let include_routines = object_types_include_routines(object_types);
let include_custom_types = db_config.as_ref().is_some_and(supports_pg_custom_type_objects)
&& object_types_include_custom_types(object_types);
db::postgres::list_objects(p, schema, include_relations, include_routines, include_custom_types)
.await
.map(unpaged_object_list)
let mut objects = if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss) {
// openGauss variant excludes package members (propackage=true)
// from the top-level routine list; they are surfaced under
// their PACKAGE nodes below.
db::postgres::list_opengauss_objects(
p,
schema,
include_relations,
include_routines,
include_custom_types,
)
.await?
} else {
db::postgres::list_objects(p, schema, include_relations, include_routines, include_custom_types).await?
};
if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss) {
let (include_package_spec, include_package_body) = object_types_include_packages(object_types);
objects.extend(
db::postgres::list_opengauss_packages(p, schema, include_package_spec, include_package_body)
.await?,
);
}
Ok(unpaged_object_list(objects))
}
_ => Ok(unpaged_object_list(
list_tables_core(state, connection_id, database, schema, None, None, None, None, None)
@@ -6456,6 +6498,11 @@ async fn list_completion_objects_once(
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::Redshift) => {
db::postgres::list_redshift_objects(p, schema, false, true).await.map(filter_completion_objects)
}
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss) => {
let mut objects = db::postgres::list_opengauss_objects(p, schema, false, true, false).await?;
objects.extend(db::postgres::list_opengauss_packages(p, schema, true, true).await?);
Ok(filter_completion_objects_with_packages(objects))
}
PoolKind::Postgres(p) => {
db::postgres::list_objects(p, schema, true, true, false).await.map(filter_completion_objects)
}
@@ -6478,6 +6525,19 @@ fn filter_completion_objects(objects: Vec<db::ObjectInfo>) -> Vec<db::ObjectInfo
.collect()
}
fn filter_completion_objects_with_packages(objects: Vec<db::ObjectInfo>) -> Vec<db::ObjectInfo> {
objects
.into_iter()
.filter(|object| {
let object_type = object.object_type.to_ascii_uppercase();
object_type.contains("PROCEDURE")
|| object_type.contains("FUNCTION")
|| object_type.contains("TRIGGER")
|| object_type == "PACKAGE"
})
.collect()
}
fn is_agent_postgres_metadata_fallback_config(config: &ConnectionConfig) -> bool {
// HighGo and Vastbase can use the native PostgreSQL metadata path when their
// agent returns no rows. UXDB is JDBC-only; opening a PostgreSQL fallback
@@ -8294,6 +8354,16 @@ fn object_types_include_custom_types(object_types: Option<&[String]>) -> bool {
.is_none_or(|types| types.iter().any(|t| t.eq_ignore_ascii_case("TYPE") || t.eq_ignore_ascii_case("TYPE_BODY")))
}
fn object_types_include_packages(object_types: Option<&[String]>) -> (bool, bool) {
match object_types {
None => (true, true),
Some(types) => (
types.iter().any(|value| value.eq_ignore_ascii_case("PACKAGE")),
types.iter().any(|value| value.eq_ignore_ascii_case("PACKAGE_BODY")),
),
}
}
/// Whether the object-type filter exclusively asks for user-defined types.
///
/// Used to keep agent errors visible: the native PostgreSQL fallback never
@@ -9473,6 +9543,18 @@ async fn get_object_source_once(
mysql_object_source(pool, mysql_table_metadata_catalog(database, schema), name, &object_type)
.await?
}
PoolKind::Postgres(pool)
if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss)
&& matches!(object_type, db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody) =>
{
db::postgres::opengauss_package_source(
pool,
schema,
name,
matches!(object_type, db::ObjectSourceKind::PackageBody),
)
.await?
}
PoolKind::Postgres(pool) if db_config.as_ref().is_some_and(is_questdb_config) => {
// only view
db::questdb::questdb_object_source(pool, name).await?
@@ -9527,7 +9609,14 @@ async fn get_object_source_once(
}
};
let editable = if matches!(object_type, db::ObjectSourceKind::Trigger)
let editable = if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::OpenGauss)
&& matches!(object_type, db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody)
{
// gs_source returns the original CREATE text. Re-executing CREATE for an
// existing package is not a safe edit operation unless the user changes
// it to CREATE OR REPLACE explicitly, so keep the initial implementation read-only.
Some(false)
} else if matches!(object_type, db::ObjectSourceKind::Trigger)
&& db_config.as_ref().is_some_and(|config| {
matches!(
config.db_type,
@@ -9542,7 +9631,8 @@ async fn get_object_source_once(
| DatabaseType::Uxdb
| DatabaseType::Vastbase
)
}) {
})
{
Some(false)
} else {
None
+60 -1
View File
@@ -246,6 +246,26 @@ impl SqlParsingOptions {
Self::from_profile(SqlDialectProfile::for_database_type(db_type))
}
pub fn for_database_type_and_compatibility(db_type: DatabaseType, compatibility_mode: Option<&str>) -> Self {
if db_type == DatabaseType::OpenGauss {
return match compatibility_mode.map(str::trim) {
// openGauss stores package specs/bodies as PL/SQL regardless of
// mode, and A mode is the only mode where the catalog reports
// them as packages. Any mode other than A keeps the PostgreSQL
// statement splitter.
Some(mode) if mode.eq_ignore_ascii_case("A") => Self::from_profile(SqlDialectProfile::gaussdb()),
Some(_) => Self::for_database_type(db_type),
// Unknown mode: the compatibility probe failed or the pool was
// unavailable. Falling back to the plain PostgreSQL profile would
// split an A-mode package body on its inner semicolons into
// fragments that the caller may then execute individually, so the
// conservative PL/SQL-capable profile is used instead.
None => Self::from_profile(SqlDialectProfile::gaussdb()),
};
}
Self::for_database_type(db_type)
}
pub fn mysql_compatible() -> Self {
Self::from_profile(SqlDialectProfile::mysql_compatible())
}
@@ -677,6 +697,14 @@ pub fn split_sql_statements_for_database(sql: &str, db_type: DatabaseType) -> Ve
sql_execution_plan_for_database(sql, db_type).statements
}
pub fn split_sql_statements_for_database_with_compatibility(
sql: &str,
db_type: DatabaseType,
compatibility_mode: Option<&str>,
) -> Vec<String> {
sql_execution_plan_for_database_with_compatibility(sql, db_type, compatibility_mode).statements
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SqlExecutionPlan {
pub statements: Vec<String>,
@@ -684,7 +712,21 @@ pub struct SqlExecutionPlan {
}
pub fn sql_execution_plan_for_database(sql: &str, db_type: DatabaseType) -> SqlExecutionPlan {
let options = SqlParsingOptions::for_database_type(db_type);
sql_execution_plan_with_options(sql, SqlParsingOptions::for_database_type(db_type))
}
pub fn sql_execution_plan_for_database_with_compatibility(
sql: &str,
db_type: DatabaseType,
compatibility_mode: Option<&str>,
) -> SqlExecutionPlan {
sql_execution_plan_with_options(
sql,
SqlParsingOptions::for_database_type_and_compatibility(db_type, compatibility_mode),
)
}
fn sql_execution_plan_with_options(sql: &str, options: SqlParsingOptions) -> SqlExecutionPlan {
if !options.profile.supports_psql_control_commands {
return SqlExecutionPlan { statements: split_sql_statements_with_options(sql, options), stop_on_error: false };
}
@@ -4338,6 +4380,23 @@ SELECT 1;";
);
}
#[test]
fn opengauss_a_mode_split_keeps_create_package_together() {
let sql = "CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\n PROCEDURE log_message(msg VARCHAR2);\nEND pkg_utils;\n/\nSELECT 1;";
assert_eq!(
super::split_sql_statements_for_database_with_compatibility(sql, DatabaseType::OpenGauss, Some("A")),
vec![
"CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\n PROCEDURE log_message(msg VARCHAR2);\nEND pkg_utils;",
"SELECT 1"
]
);
assert_ne!(
super::split_sql_statements_for_database_with_compatibility(sql, DatabaseType::OpenGauss, Some("PG")),
super::split_sql_statements_for_database_with_compatibility(sql, DatabaseType::OpenGauss, Some("A"))
);
}
#[test]
fn xugu_split_keeps_create_package_body_together() {
let sql = "\
+93 -13
View File
@@ -31,10 +31,43 @@ mod table_restore;
pub use table_restore::SqlFileTable;
use table_restore::TableRestoreFilter;
/// How the database compatibility mode used for SQL statement splitting was
/// resolved. `Option<String>` cannot express the difference between "this
/// database has no compatibility mode concept" and "the probe failed", and
/// conflating those silently downgraded openGauss A-mode PL/SQL splitting to
/// the plain PostgreSQL splitter.
#[derive(Debug, Clone, PartialEq, Eq)]
enum SqlCompatibilityMode {
/// The database type has no compatibility-mode concept (non-openGauss).
NotApplicable,
/// The database is openGauss but the probe failed or the pool was
/// unavailable. The parser must not fall back to PostgreSQL semantics;
/// it uses the PL/SQL-capable openGauss profile so an A-mode package body
/// is never split on its inner semicolons.
Unknown,
/// Probe succeeded; carries the catalog-reported mode (may be B/C/PG/...).
Resolved(String),
}
impl SqlCompatibilityMode {
/// The mode string for [`SqlParsingOptions::for_database_type_and_compatibility`],
/// or `None` when the database has no mode concept or the mode is unknown.
///
/// `None` is interpreted as "unknown openGauss" by the parser (see that
/// function), which selects the conservative PL/SQL-capable profile.
fn as_mode_str(&self) -> Option<&str> {
match self {
Self::Resolved(mode) => Some(mode.as_str()),
Self::Unknown | Self::NotApplicable => None,
}
}
}
#[derive(Debug, Clone)]
struct SqlFileImportTarget {
db_type: DatabaseType,
driver_profile: Option<String>,
compatibility_mode: SqlCompatibilityMode,
}
#[derive(Debug)]
@@ -343,11 +376,12 @@ pub async fn execute_sql_file_content(
started_at: Instant,
mut emit: impl FnMut(SqlFileProgress),
) -> Result<(), String> {
let import_target = sql_file_import_target(state, &request.connection_id).await;
let import_target = sql_file_import_target(state, &request.connection_id, &request.database).await;
validate_table_restore_target(request, import_target.as_ref(), 1)?;
let mut statements = split_sql_file_import_statements_with_control(
file_content,
import_target.as_ref().map(|target| target.db_type),
import_target.as_ref().and_then(|target| target.compatibility_mode.as_mode_str()),
);
if let Some(selected) = &request.selected_tables {
@@ -413,9 +447,16 @@ pub async fn execute_sql_file_paths(
return Err(error);
}
let import_target = sql_file_import_target(state, &request.connection_id).await;
let options =
import_target.as_ref().map(|target| SqlParsingOptions::for_database_type(target.db_type)).unwrap_or_default();
let import_target = sql_file_import_target(state, &request.connection_id, &request.database).await;
let options = import_target
.as_ref()
.map(|target| {
SqlParsingOptions::for_database_type_and_compatibility(
target.db_type,
target.compatibility_mode.as_mode_str(),
)
})
.unwrap_or_default();
let database_type = import_target.as_ref().map(|target| target.db_type);
let mut progress = SqlFileExecutionProgress::new();
// Validate the entire dump before any SQL reaches the database. This also
@@ -1300,7 +1341,7 @@ fn emit_sql_file_terminal_progress(
#[cfg(test)]
fn split_sql_file_import_statements(file_content: &str, db_type: Option<DatabaseType>) -> Vec<String> {
split_sql_file_import_statements_with_control(file_content, db_type)
split_sql_file_import_statements_with_control(file_content, db_type, None)
.into_iter()
.map(|statement| statement.sql)
.collect()
@@ -1309,6 +1350,7 @@ fn split_sql_file_import_statements(file_content: &str, db_type: Option<Database
fn split_sql_file_import_statements_with_control(
file_content: &str,
db_type: Option<DatabaseType>,
compatibility_mode: Option<&str>,
) -> Vec<SqlStatementWithControl> {
if db_type == Some(DatabaseType::SqlServer) {
// GO is a client-side batch delimiter, not T-SQL. SQL Server module DDL
@@ -1319,7 +1361,9 @@ fn split_sql_file_import_statements_with_control(
.collect();
}
let options = db_type.map(SqlParsingOptions::for_database_type).unwrap_or_default();
let options = db_type
.map(|db_type| SqlParsingOptions::for_database_type_and_compatibility(db_type, compatibility_mode))
.unwrap_or_default();
let mut splitter = SqlStatementSplitter::with_options(options);
let mut statements = splitter.push_chunk_with_control(file_content);
statements.extend(splitter.finish_with_control());
@@ -1400,11 +1444,39 @@ fn sql_file_execution_error_progress(
)
}
async fn sql_file_import_target(state: &AppState, connection_id: &str) -> Option<SqlFileImportTarget> {
let configs = state.configs.read().await;
configs
.get(connection_id)
.map(|config| SqlFileImportTarget { db_type: config.db_type, driver_profile: config.driver_profile.clone() })
async fn sql_file_import_target(state: &AppState, connection_id: &str, database: &str) -> Option<SqlFileImportTarget> {
let config = state.configs.read().await.get(connection_id).cloned()?;
// Probing must never drop db_type: even when the pool is unavailable the file
// still uses the openGauss splitter. A failed probe is recorded as `Unknown`
// rather than `None` so the splitter keeps PL/SQL package bodies intact
// instead of silently falling back to PostgreSQL statement semantics.
let compatibility_mode = if config.db_type == DatabaseType::OpenGauss {
let pool = match state.get_or_create_pool(connection_id, Some(database)).await {
Ok(pool_key) => match state.pool_handle(&pool_key).await {
Some(PoolKind::Postgres(pool)) => Some(pool),
_ => None,
},
Err(_) => None,
};
match pool {
Some(pool) => match db::postgres::opengauss_compatibility_mode(&pool).await {
Ok(Some(mode)) => SqlCompatibilityMode::Resolved(mode),
// Catalog returned no row, or the probe errored (permissions,
// timeout, older kernel without pg_database.datcompatibility).
Ok(None) | Err(_) => {
log::warn!(
"[sql_file_import] openGauss compatibility mode probe failed for connection {connection_id}; \
splitting with the conservative PL/SQL profile"
);
SqlCompatibilityMode::Unknown
}
},
None => SqlCompatibilityMode::Unknown,
}
} else {
SqlCompatibilityMode::NotApplicable
};
Some(SqlFileImportTarget { db_type: config.db_type, driver_profile: config.driver_profile, compatibility_mode })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2109,13 +2181,21 @@ mod tests {
request.selected_tables = Some(vec![SqlFileTable { database: None, name: "a".to_string() }]);
assert!(validate_table_restore_target(
&request,
Some(&SqlFileImportTarget { db_type: DatabaseType::Postgres, driver_profile: None }),
Some(&SqlFileImportTarget {
db_type: DatabaseType::Postgres,
driver_profile: None,
compatibility_mode: SqlCompatibilityMode::NotApplicable
}),
1
)
.is_err());
assert!(validate_table_restore_target(
&request,
Some(&SqlFileImportTarget { db_type: DatabaseType::Mysql, driver_profile: None }),
Some(&SqlFileImportTarget {
db_type: DatabaseType::Mysql,
driver_profile: None,
compatibility_mode: SqlCompatibilityMode::NotApplicable
}),
2
)
.is_err());
+4
View File
@@ -17,6 +17,10 @@ pub struct DatabaseInfo {
pub default_charset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_collation: Option<String>,
/// Database-level compatibility mode reported by compatible engines.
/// openGauss uses values such as A, B, C, and PG.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compatibility_mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]