mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(opengauss): support A-compatibility packages
This commit is contained in:
@@ -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>();
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "\
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user