feat(opengauss): support A-compatibility packages

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

No files matched your search

@@ -130,6 +130,15 @@ describe("databaseObjectCapabilities", () => {
}
});
it("exposes packages only for openGauss A compatibility mode", () => {
expect(sidebarObjectKindsForDatabase("opengauss", "A")).toEqual(expect.arrayContaining(["PACKAGE", "PACKAGE_BODY"]));
expect(sidebarObjectKindsForDatabase("opengauss", "a")).toContain("PACKAGE");
for (const mode of ["B", "C", "PG", undefined]) {
expect(sidebarObjectKindsForDatabase("opengauss", mode), String(mode)).not.toContain("PACKAGE");
}
expect(databaseObjectCapabilities("opengauss", "A").sourceReadable).toEqual(expect.arrayContaining(["PACKAGE", "PACKAGE_BODY"]));
});
it("enables custom type details only for verified PG-family databases", () => {
for (const dbType of ["postgres", "opengauss", "gaussdb", "kingbase", "vastbase"] as const) {
expect(customTypeCapabilities(dbType), dbType).toEqual({ details: true, members: true, ddl: true });
@@ -17,6 +17,11 @@ describe("buildDatabaseTreeNodes", () => {
expect(node.id).toBe("conn-1:analytics");
});
it("carries database compatibility mode into database nodes", () => {
const [node] = buildDatabaseTreeNodes("conn-1", [{ name: "oracle_mode", compatibility_mode: "A" }]);
expect(node.compatibilityMode).toBe("A");
});
it("shortens the Cloud Spanner label but keeps the resource path as the node identity", () => {
const [node] = buildDatabaseTreeNodes("conn-1", databases(SPANNER_PATH), { displayLabel: spannerDisplayDatabase });
@@ -25,6 +25,13 @@ describe("queryTimeout", () => {
expect(frontendQueryTimeoutSecsForSql("UPDATE sample_records SET state = 'ready' RETURNING id", "postgres", 30)).toBe(0);
});
it("uses conservative package splitting for an unresolved openGauss mode", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
END pkg_utils;`;
expect(frontendQueryTimeoutSecsForSql(`${packageSpec}\n/\nSELECT 1;`, "opengauss", 30)).toBe(120);
});
it("keeps the frontend guard for non-row PostgreSQL statements", () => {
expect(frontendQueryTimeoutSecsForSql("UPDATE sample_records SET state = 'ready'", "postgres", 30)).toBe(60);
expect(frontendQueryTimeoutSecsForSql("INSERT INTO sample_records(note) VALUES ('RETURNING is text')", "postgres", 30)).toBe(60);
@@ -616,6 +616,27 @@ END pkg_utils_without_replace;`;
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpecWithoutReplace}\nSELECT 1;`, "xugu"))).toEqual([packageSpecWithoutReplace, "SELECT 1"]);
});
it("keeps openGauss packages together while compatibility metadata is unknown", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
PROCEDURE log_message(msg VARCHAR2);
END pkg_utils;`;
const script = `${packageSpec}\n/\nSELECT 1;`;
expect(rangeSqlTexts(splitSqlStatementRanges(script, "opengauss"))).toEqual([packageSpec, "SELECT 1"]);
});
it("splits openGauss A-mode packages without changing PG mode", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
PROCEDURE log_message(msg VARCHAR2);
END pkg_utils;`;
const script = `${packageSpec}\n/\nSELECT 1;`;
expect(rangeSqlTexts(splitSqlStatementRanges(script, "opengauss", { compatibilityMode: "A" }))).toEqual([packageSpec, "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges(script, "opengauss", { compatibilityMode: "PG" }))).not.toEqual([packageSpec, "SELECT 1"]);
});
it("splits a declaration-only Oracle package body before following DML", () => {
const packageBody = `CREATE OR REPLACE PACKAGE BODY packageName IS
null;
@@ -18,6 +18,7 @@ const MYSQL_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCT
// covers user-created types (enum/domain/composite/range/multirange/base);
// relation auto-generated row types stay hidden.
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "TYPE"];
const OPENGAUSS_A_OBJECTS: SidebarObjectKind[] = [...POSTGRES_OBJECTS, "PACKAGE", "PACKAGE_BODY"];
// KWDB is routed through the PostgreSQL pool but its pg_type catalog
// compatibility is not verified yet, so it stays on the pre-TYPE object set.
@@ -114,8 +115,8 @@ function isSourceReadableObjectKind(kind: SidebarObjectKind, dbType?: DatabaseTy
return true;
}
export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjectCapabilities {
const sidebarObjects = sidebarObjectKindsForDatabase(dbType);
export function databaseObjectCapabilities(dbType?: DatabaseType, compatibilityMode?: string): DatabaseObjectCapabilities {
const sidebarObjects = sidebarObjectKindsForDatabase(dbType, compatibilityMode);
return {
sidebarObjects,
sourceReadable: sidebarObjects.filter((kind) => isSourceReadableObjectKind(kind, dbType)),
@@ -166,8 +167,9 @@ export function schemaDiffRoutineObjectTypesIntersection(sourceDbType?: Database
return schemaDiffRoutineObjectTypes(targetDbType).filter((kind) => sourceTypes.has(kind));
}
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObjectKind[] {
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType, compatibilityMode?: string): SidebarObjectKind[] {
if (!dbType) return [...TABLE_VIEW_OBJECTS];
if (dbType === "opengauss" && compatibilityMode?.trim().toUpperCase() === "A") return [...OPENGAUSS_A_OBJECTS];
return DATABASE_TYPE_OBJECTS.get(dbType) ?? [...ROUTINE_OBJECTS];
}
@@ -202,8 +204,8 @@ export function customTypeCapabilities(dbType?: DatabaseType): CustomTypeCapabil
return { details: supported, members: supported, ddl: supported };
}
export function supportsPackageMemberExpansion(dbType?: DatabaseType): boolean {
return !!dbType && PACKAGE_MEMBER_EXPANSION_DATABASES.has(dbType);
export function supportsPackageMemberExpansion(dbType?: DatabaseType, compatibilityMode?: string): boolean {
return !!dbType && (PACKAGE_MEMBER_EXPANSION_DATABASES.has(dbType) || (dbType === "opengauss" && compatibilityMode?.trim().toUpperCase() === "A"));
}
export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
@@ -38,6 +38,7 @@ export function buildDatabaseTreeNodes(
type: "database" as const,
connectionId,
database: name,
compatibilityMode: db.compatibility_mode?.trim() || undefined,
isExpanded: false,
children: [],
},
@@ -87,6 +88,7 @@ export function buildDuckDbConnectionTreeNodes(connectionId: string, databases:
type: "database" as const,
connectionId,
database: name,
compatibilityMode: db.compatibility_mode?.trim() || undefined,
isExpanded: false,
children: [],
},
@@ -25,7 +25,7 @@ export function executableStatementRangeCacheForDoc(
): ExecutableStatementRangeCache {
const parameterOptions = typeof parameterOptionsOrParse === "function" ? undefined : parameterOptionsOrParse;
const parse = typeof parameterOptionsOrParse === "function" ? parameterOptionsOrParse : customParse;
const parameterSyntaxKey = parameterOptions?.enabledSyntaxes ? parameterOptions.enabledSyntaxes.join(",") : "*";
const parameterSyntaxKey = `${parameterOptions?.enabledSyntaxes ? parameterOptions.enabledSyntaxes.join(",") : "*"}|compat=${parameterOptions?.compatibilityMode?.trim().toUpperCase() ?? ""}`;
if (cache?.doc === doc && cache.databaseType === databaseType && cache.parameterSyntaxKey === parameterSyntaxKey) return cache;
const byStart = new Map<number, SqlTextRange>();
+5 -4
View File
@@ -1,4 +1,5 @@
import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
import { DEFAULT_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
import type { ConnectionConfig, DatabaseType } from "@/types/database";
@@ -46,7 +47,7 @@ export function queryTimeoutSecsForConcurrentIndex(configuredTimeoutSecs: number
return Math.max(configuredTimeoutSecs, CONCURRENT_INDEX_QUERY_TIMEOUT_SECS);
}
export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: DatabaseType | undefined, queryTimeoutSecs: number): number {
export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: DatabaseType | undefined, queryTimeoutSecs: number, parameterOptions?: SqlParameterOptions): number {
if (queryTimeoutSecs === 0) return 0;
// PostgreSQL applies the configured timeout while the backend receives rows
@@ -56,7 +57,7 @@ export function frontendQueryTimeoutSecsForSql(sql: string, databaseType: Databa
if (databaseType === "postgres" && postgresQueryMayReturnRows(sql, databaseType)) return 0;
const baseTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
const statementCount = Math.max(splitSqlStatementRanges(sql, databaseType).length, 1);
const statementCount = Math.max(splitSqlStatementRanges(sql, databaseType, parameterOptions ?? sqlStatementParameterOptionsForCompatibility(databaseType)).length, 1);
return baseTimeoutSecs * statementCount;
}
@@ -67,7 +68,7 @@ export function frontendQueryTimeoutDelayMs(timeoutSecs: number): number | undef
}
function postgresQueryMayReturnRows(sql: string, databaseType: DatabaseType): boolean {
return splitSqlStatementRanges(sql, databaseType).some(({ sql: statement }) => {
return splitSqlStatementRanges(sql, databaseType, sqlStatementParameterOptionsForCompatibility(databaseType)).some(({ sql: statement }) => {
const tokens = tokenizeSqlSemantic(statement, "postgres");
const firstKeyword = tokens.find((token) => token.kind === "word" && token.depth === 0)?.normalized;
if (!firstKeyword) return false;
@@ -24,14 +24,14 @@ export interface SqlSemanticDiagnosticVisibleRange {
to: number;
}
export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRanges: readonly SqlSemanticDiagnosticVisibleRange[], databaseType?: DatabaseType, cachedStatements?: readonly SqlTextRange[]): SqlTextRange[] {
const statements = databaseType === "sqlserver" ? sqlServerSemanticDiagnosticRanges(sql) : (cachedStatements ?? executableStatementRanges(sql, databaseType));
export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRanges: readonly SqlSemanticDiagnosticVisibleRange[], databaseType?: DatabaseType, cachedStatements?: readonly SqlTextRange[], parameterOptions?: { compatibilityMode?: string }): SqlTextRange[] {
const statements = databaseType === "sqlserver" ? sqlServerSemanticDiagnosticRanges(sql) : (cachedStatements ?? executableStatementRanges(sql, databaseType, parameterOptions));
if (statements.length === 0 || visibleRanges.length === 0) return [];
const selected: SqlTextRange[] = [];
const seen = new Set<string>();
for (const statement of statements) {
if (isOraclePlSqlStatement(statement.sql, databaseType)) continue;
if (isOraclePlSqlStatement(statement.sql, databaseType, parameterOptions)) continue;
if (!visibleRanges.some((visibleRange) => rangesIntersect(statement, visibleRange))) continue;
const key = `${statement.from}:${statement.to}`;
if (seen.has(key)) continue;
@@ -55,6 +55,8 @@ type TriggerPseudoRecordName = "new" | "old" | "parent" | "eventinfo";
export interface SqlParameterOptions {
databaseType?: DatabaseType;
/** Database-level compatibility mode, for example openGauss A/B/C/PG. */
compatibilityMode?: string;
// Which placeholder syntaxes are recognized. Undefined enables all of them.
enabledSyntaxes?: readonly SqlParameterSyntax[];
}
+23 -8
View File
@@ -499,7 +499,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
continue;
}
}
if (isOracleLikeDatabase(databaseType) && isAtLineStart(sql, i) && isSlashLine(sql, i)) {
if (isOracleLikeDatabase(databaseType, parameterOptions) && isAtLineStart(sql, i) && isSlashLine(sql, i)) {
const lineEnd = findLineEnd(sql, i);
flush(i);
i = nextLineStart(sql, lineEnd);
@@ -579,7 +579,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
const tagMatch = /^\$[A-Za-z_0-9]*\$/.exec(sql.slice(i));
if (tagMatch) {
markContent(i);
if (databaseType === "gaussdb" && statementStart !== -1 && startsWithPostgresDollarQuotedRoutinePrefix(sql.slice(statementStart, i))) {
if ((databaseType === "gaussdb" || isOpenGaussOracleCompatibility(databaseType, parameterOptions)) && statementStart !== -1 && startsWithPostgresDollarQuotedRoutinePrefix(sql.slice(statementStart, i))) {
postgresDollarQuotedRoutine = true;
}
dollarTag = tagMatch[0].slice(1, -1);
@@ -609,7 +609,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
// Internal semicolons remain part of the routine body.
flush();
} else {
if (oraclePlSqlStatementEnd === undefined && isOracleLikeDatabase(databaseType) && !postgresDollarQuotedRoutine && statementStart !== -1) {
if (oraclePlSqlStatementEnd === undefined && isOracleLikeDatabase(databaseType, parameterOptions) && !postgresDollarQuotedRoutine && statementStart !== -1) {
const statementSoFar = sql.slice(statementStart, i);
oraclePlSqlStatementEnd = startsWithOraclePlSqlBlock(statementSoFar) ? statementStart + (oraclePlSqlBlockEnd(sql.slice(statementStart)) ?? sql.length - statementStart) : null;
}
@@ -740,7 +740,7 @@ function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: nu
}
function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
if (isOraclePlSqlStatement(statement.sql, databaseType)) return [statement];
if (isOraclePlSqlStatement(statement.sql, databaseType, parameterOptions)) return [statement];
if (isSapHanaScriptBlockStatement(statement.sql, databaseType)) return [statement];
// Routine bodies contain top-level-looking SET/INSERT/SELECT lines that are not independent statements.
if (isMysqlRoutineBlockDatabase(databaseType) && startsWithMysqlRoutineBlock(statement.sql, parameterOptions)) return [statement];
@@ -1575,12 +1575,27 @@ function isSqlWhitespace(ch: string): boolean {
return ch === " " || ch === "\t" || ch === "\r" || ch === "\n";
}
export function isOracleLikeDatabase(databaseType?: DatabaseType): boolean {
return !!databaseType && ORACLE_LIKE_PL_SQL_DATABASES.has(databaseType);
function isOpenGaussOracleCompatibility(databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
if (databaseType !== "opengauss") return false;
const mode = options?.compatibilityMode?.trim().toUpperCase();
// Unknown mode is handled conservatively so an A-mode package is never
// split into executable fragments before metadata loading finishes.
return mode === undefined || mode === "A";
}
export function isOraclePlSqlStatement(sql: string, databaseType?: DatabaseType): boolean {
return isOracleLikeDatabase(databaseType) && startsWithOraclePlSqlBlock(sql);
export function sqlStatementParameterOptionsForCompatibility(databaseType?: DatabaseType, compatibilityMode?: string): SqlParameterOptions | undefined {
if (databaseType !== "opengauss") return undefined;
// Keep the browser-side splitter aligned with the backend's conservative
// behavior while the compatibility probe is still cold or unavailable.
return { compatibilityMode: compatibilityMode?.trim() || "A" };
}
export function isOracleLikeDatabase(databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
return !!databaseType && (ORACLE_LIKE_PL_SQL_DATABASES.has(databaseType) || isOpenGaussOracleCompatibility(databaseType, options));
}
export function isOraclePlSqlStatement(sql: string, databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
return isOracleLikeDatabase(databaseType, options) && startsWithOraclePlSqlBlock(sql);
}
function isSapHanaScriptBlockDatabase(databaseType?: DatabaseType): boolean {
+17 -8
View File
@@ -4,7 +4,8 @@ import { hexToRgba } from "@/lib/common/color";
import type { CSSProperties } from "vue";
import { findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout";
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
import { executableStatementRanges, splitSqlStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { executableStatementRanges, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import type { SqlParameterOptions } from "@/lib/sql/sqlParameters";
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database";
@@ -240,14 +241,14 @@ export function resultSqlForGrid(tab: Pick<QueryTab, "result" | "resultBaseSql"
* A stale or ambiguous source is ignored instead of highlighting a different
* statement that happens to have the same text.
*/
export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "sourceStatement" | "sourceFrom" | "sourceTo"> | undefined, resultIndex: number | undefined, databaseType?: DatabaseType): SqlTextRange | undefined {
export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "sourceStatement" | "sourceFrom" | "sourceTo"> | undefined, resultIndex: number | undefined, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange | undefined {
const sourceStatement = result?.sourceStatement;
if (!sourceStatement) return undefined;
if (typeof result.sourceFrom === "number" && typeof result.sourceTo === "number" && editorSql.slice(result.sourceFrom, result.sourceTo) === sourceStatement) {
return { from: result.sourceFrom, to: result.sourceTo, sql: sourceStatement };
}
const statements = statementRanges(editorSql, databaseType);
const statements = statementRanges(editorSql, databaseType, parameterOptions);
const indexed = typeof resultIndex === "number" ? statements[resultIndex] : undefined;
if (indexed?.sql === sourceStatement) {
return { from: indexed.from, to: indexed.to, sql: indexed.sql };
@@ -273,10 +274,10 @@ function lineStartOffset(sql: string, from: number): number {
return sql.lastIndexOf("\n", Math.max(0, from - 1)) + 1;
}
function statementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
function statementRanges(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): SqlTextRange[] {
if (databaseType === "redis") return executableStatementRanges(sql, databaseType);
if (databaseType === "mongodb") return splitMongoCommandRanges(sql).map(({ from, to, text }) => ({ from, to, sql: text }));
return splitSqlStatementRanges(sql, databaseType);
return splitSqlStatementRanges(sql, databaseType, parameterOptions ?? sqlStatementParameterOptionsForCompatibility(databaseType));
}
function liveStatementExecutionMarkers(editorSql: string, batch: BatchSqlExecution): StatementExecutionMarker[] {
@@ -301,12 +302,20 @@ function liveStatementExecutionMarkers(editorSql: string, batch: BatchSqlExecuti
}));
}
export function statementExecutionMarkers(editorSql: string, results: QueryResult[] | undefined, databaseType?: DatabaseType, submittedSql = editorSql, executionEditorFingerprint = sqlTextFingerprint(editorSql), batch?: BatchSqlExecution): StatementExecutionMarker[] {
export function statementExecutionMarkers(
editorSql: string,
results: QueryResult[] | undefined,
databaseType?: DatabaseType,
submittedSql = editorSql,
executionEditorFingerprint = sqlTextFingerprint(editorSql),
batch?: BatchSqlExecution,
parameterOptions?: SqlParameterOptions,
): StatementExecutionMarker[] {
if (batch?.items.length) return liveStatementExecutionMarkers(editorSql, batch);
if (!results?.length || sqlTextFingerprint(editorSql) !== executionEditorFingerprint) return [];
const submittedStatements = statementRanges(submittedSql, databaseType);
const submittedStatements = statementRanges(submittedSql, databaseType, parameterOptions);
if (submittedStatements.length <= 1) return [];
const editorStatements = submittedSql === editorSql ? submittedStatements : statementRanges(editorSql, databaseType);
const editorStatements = submittedSql === editorSql ? submittedStatements : statementRanges(editorSql, databaseType, parameterOptions);
const byLine = new Map<number, { success: number; error: number }>();
for (const result of results) {