fix(oracle): support sequence and pseudo-column completion

This commit is contained in:
lxk955
2026-10-10 09:46:46 +08:00
committed by GitHub
parent e6ae58e989
commit eda4d3e2d5
7 changed files with 352 additions and 8 deletions
+44 -3
View File
@@ -2004,7 +2004,7 @@ func completionRequestHasTableLikeKind(kinds []string) bool {
func completionRequestHasRoutineKind(kinds []string) bool {
for _, kind := range kinds {
switch strings.ToLower(strings.TrimSpace(kind)) {
case "routine", "procedure", "function":
case "routine", "procedure", "function", "sequence":
return true
}
}
@@ -2450,13 +2450,54 @@ func oracleCompletionSynonymTargetsQuery(targets []oracleCompletionSynonymTarget
}
}
func oracleCompletionRoutineObjectTypes(kinds []string) []string {
if len(kinds) == 0 {
return []string{"'FUNCTION'", "'PROCEDURE'", "'PACKAGE'", "'SEQUENCE'"}
}
hasRoutine := false
hasProc := false
hasFunc := false
hasSeq := false
for _, kind := range kinds {
switch strings.ToLower(strings.TrimSpace(kind)) {
case "routine":
hasRoutine = true
case "procedure":
hasProc = true
case "function":
hasFunc = true
case "sequence":
hasSeq = true
}
}
types := make([]string, 0, 4)
if hasRoutine {
types = append(types, "'FUNCTION'", "'PROCEDURE'", "'PACKAGE'")
} else {
if hasProc {
types = append(types, "'PROCEDURE'")
}
if hasFunc {
types = append(types, "'FUNCTION'")
}
}
if hasSeq {
types = append(types, "'SEQUENCE'")
}
if len(types) == 0 {
return []string{"'FUNCTION'", "'PROCEDURE'", "'PACKAGE'"}
}
return types
}
func oracleCompletionRoutinesQuery(request completionAssistantRequest, preferredSchema string, limit int) oracleMetadataListQuery {
pattern := oracleCompletionLikePattern(request.Mask, request.MatchMode)
args := make([]any, 0, 5)
baseSQL := `
objectTypes := oracleCompletionRoutineObjectTypes(request.ObjectKinds)
baseSQL := fmt.Sprintf(`
SELECT o.OWNER, o.OBJECT_NAME, o.OBJECT_TYPE, CAST(NULL AS VARCHAR2(128)) AS PARENT_NAME
FROM ALL_OBJECTS o
WHERE o.OBJECT_TYPE IN ('FUNCTION', 'PROCEDURE', 'PACKAGE')`
WHERE o.OBJECT_TYPE IN (%s)`, strings.Join(objectTypes, ", "))
args = append(args, pattern)
nameParam := len(args)
+24
View File
@@ -2347,6 +2347,30 @@ func TestOracleCompletionRoutinesQueryUsesPublicPackageMetadata(t *testing.T) {
}
}
func TestOracleCompletionRoutinesQueryIncludesSequences(t *testing.T) {
query := oracleCompletionRoutinesQuery(completionAssistantRequest{
Schema: "HR",
ObjectKinds: []string{"routine", "sequence"},
Mask: "SEQ",
GlobalSearch: false,
}, "HR", 100)
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "'SEQUENCE'") {
t.Fatalf("routines query should include SEQUENCE when requested: %s", query.SQL)
}
seqOnlyQuery := oracleCompletionRoutinesQuery(completionAssistantRequest{
Schema: "HR",
ObjectKinds: []string{"sequence"},
Mask: "SEQ",
GlobalSearch: false,
}, "HR", 100)
seqSQL := strings.ToUpper(seqOnlyQuery.SQL)
if !strings.Contains(seqSQL, "IN ('SEQUENCE')") {
t.Fatalf("sequence-only query should only include SEQUENCE: %s", seqOnlyQuery.SQL)
}
}
func TestOracleCompletionPackageCandidatesPreserveOverloadsAndReturnTypes(t *testing.T) {
rows := []oraclePackageRoutineRow{
{
@@ -1411,6 +1411,12 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
function completionObjectKindsForContext(completionContext: ReturnType<typeof getSqlCompletionContext>): CompletionAssistantObjectKind[] {
if (completionContext.contextKind === "exec") return ["procedure"];
if (isOracleCompletionDatabase(props.databaseType)) {
if (completionContext.suggestColumns && completionContext.referencedTables.length > 0 && !completionContext.qualifier) {
return ["function", "sequence"];
}
return ["routine", "sequence"];
}
if (completionContext.suggestColumns && completionContext.referencedTables.length > 0 && !completionContext.qualifier) return ["function"];
return ["routine"];
}
@@ -484,6 +484,163 @@ describe("PostgreSQL sequence literal completion", () => {
expect(items.some((item) => item.label === "order_seq")).toBe(false);
});
it("suggests Oracle sequences in SQL expressions and select lists", () => {
const sql = "SELECT ord";
const unqualifiedItems = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map(),
databaseType: "oracle",
dialect: "oracle",
currentSchema: "APP",
});
expect(unqualifiedItems).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "ORDER_SEQ",
type: "variable",
detail: "sequence in APP",
apply: "ORDER_SEQ",
}),
]),
);
const qualifiedItems = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map(),
databaseType: "oracle",
dialect: "oracle",
});
expect(qualifiedItems).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "ORDER_SEQ",
type: "variable",
detail: "sequence in APP",
apply: "APP.ORDER_SEQ",
}),
]),
);
});
it("suggests Oracle sequences under schema qualifier", () => {
const sql = "SELECT app.ord";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map(),
databaseType: "oracle",
dialect: "oracle",
});
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "ORDER_SEQ",
type: "variable",
apply: "ORDER_SEQ",
}),
]),
);
});
it("suggests NEXTVAL and CURRVAL pseudo-columns for Oracle sequences", () => {
const sql = "SELECT ORDER_SEQ.";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map(),
databaseType: "oracle",
dialect: "oracle",
});
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "NEXTVAL",
type: "column",
detail: "sequence pseudo-column",
}),
expect.objectContaining({
label: "CURRVAL",
type: "column",
detail: "sequence pseudo-column",
}),
]),
);
});
it("suggests NEXTVAL and CURRVAL pseudo-columns for schema-qualified Oracle sequences", () => {
const sql = "SELECT APP.ORDER_SEQ.";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map(),
databaseType: "oracle",
dialect: "oracle",
});
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "NEXTVAL",
type: "column",
detail: "sequence pseudo-column",
}),
expect.objectContaining({
label: "CURRVAL",
type: "column",
detail: "sequence pseudo-column",
}),
]),
);
});
it("respects keyword case for Oracle sequence pseudo-columns", () => {
const sql = "SELECT ORDER_SEQ.n";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map(),
databaseType: "oracle",
dialect: "oracle",
keywordCase: "lower",
});
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "nextval",
apply: "nextval",
}),
]),
);
});
it("suggests Oracle sequence pseudo-columns in INSERT VALUES clause", () => {
const sql = "INSERT INTO orders (id, name) VALUES (ORDER_SEQ.";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [{ name: "orders", schema: "APP" }],
objects: [{ name: "ORDER_SEQ", schema: "APP", type: "sequence" }],
columnsByTable: new Map([["APP.orders", [{ name: "id" }, { name: "name" }]]]),
databaseType: "oracle",
dialect: "oracle",
});
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "NEXTVAL",
type: "column",
detail: "sequence pseudo-column",
}),
]),
);
});
it.each([
["SELECT 'order_", "postgres"],
["SELECT nextval('order_", "mysql"],
+74 -3
View File
@@ -1832,6 +1832,14 @@ class SqlCompletionProvider {
this.items.push(...buildColumnItems(context, this.input.columnsByTable, this.dialect, this.databaseType, this.input.quoteIdentifiers));
this.items.push(...buildSelectAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect, this.databaseType));
this.items.push(...buildInsertAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect, this.input.keywordCase));
if (isOracleLikeDatabase(this.databaseType) && context.qualifier && !completionQualifierIsReferencedTable(context)) {
const profileObjects = driverProfileCompletionObjects(this.input.driverProfile, context);
const allObjects = [...(this.input.objects ?? []), ...profileObjects];
const hasKnownSequence = allObjects.some((object) => sequenceMatchesQualifier(object, context));
if (hasKnownSequence || (!completionQualifierIsKnownTableOrSchema(context, completionTables, this.input.schemas) && (matchesPrefix("NEXTVAL", context.prefix) || matchesPrefix("CURRVAL", context.prefix)))) {
this.items.push(...buildOracleSequencePseudoColumnItems(context.prefix, this.input.keywordCase, hasKnownSequence));
}
}
}
const emptyTableNameCompletion = !context.prefix && (context.suggestTables || context.exclusiveTableSuggestions);
@@ -4453,8 +4461,14 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
const onlyProcedures = context.contextKind === "exec";
const onlyFunctions = context.suggestColumns && context.referencedTables.length > 0 && !context.qualifier;
const prioritizeOracleFunctions = isOracleCompletionDatabase(databaseType) && context.statementKind === "select";
const allowOracleSequences = isOracleCompletionDatabase(databaseType) && !onlyProcedures;
return objects
.filter((object) => object.type !== "sequence" && (!onlyProcedures || object.type === "procedure") && (!onlyFunctions || (object.type === "function" && object.name.toLowerCase().startsWith(context.prefix.toLowerCase()))) && objectMatchesCompletionContext(object, context))
.filter((object) => {
if (object.type === "sequence") {
return allowOracleSequences && objectMatchesCompletionContext(object, context);
}
return (!onlyProcedures || object.type === "procedure") && (!onlyFunctions || (object.type === "function" && object.name.toLowerCase().startsWith(context.prefix.toLowerCase()))) && objectMatchesCompletionContext(object, context);
})
.map((object) => {
const qualifiedByContext = objectIsQualifiedByContext(object, context);
const objectInCurrentSchema = !!currentSchema && !!object.schema && normalizeIdentifierPart(object.schema) === normalizeIdentifierPart(currentSchema);
@@ -4471,10 +4485,10 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
const baseDedupeKey = object.applyName || (isOracleCompletionDatabase(databaseType) && object.schema) ? applyName : undefined;
return {
label: object.name,
type: "function" as const,
type: object.type === "sequence" ? ("variable" as const) : ("function" as const),
detail,
info: buildRoutineInfo(object),
apply: object.type === "trigger" || object.type === "package" ? applyName : buildRoutineApply(applyName, object.signature, includeParams),
apply: object.type === "trigger" || object.type === "package" || object.type === "sequence" ? applyName : buildRoutineApply(applyName, object.signature, includeParams),
boost: computeBoost(object.name, context.prefix) + typeBoost + schemaBoost,
dedupeKey: signature ? `${baseDedupeKey ?? object.name}(${signature})` : baseDedupeKey,
// Preserve exact routine matches before the capped candidate list is truncated.
@@ -4539,6 +4553,7 @@ function buildRoutineInfo(object: SqlCompletionObject): string | undefined {
function routineTypeBoost(type: SqlCompletionObject["type"], prioritizeFunctions: boolean): number {
if (type === "package") return 1600;
if (type === "sequence") return 1500;
if (type === "function") return prioritizeFunctions ? 1800 : 900;
return prioritizeFunctions ? 900 : 1800;
}
@@ -4551,6 +4566,55 @@ function completionQualifierIsReferencedTable(context: SqlCompletionContext): bo
return context.referencedTables.some((table) => referencedTableMatchesColumnQualifier(table, qualifier, qualifierLower, qualifiedTarget));
}
function sequenceMatchesQualifier(object: SqlCompletionObject, context: SqlCompletionContext): boolean {
if (object.type !== "sequence") return false;
if (!context.qualifier) return false;
const parts = context.qualifierParts?.length ? context.qualifierParts : context.qualifier.split(".").filter(Boolean);
if (parts.length === 1) {
return normalizeIdentifierPart(object.name) === normalizeIdentifierPart(parts[0]);
}
if (parts.length === 2) {
const [schema, name] = parts;
const schemaMatches = !object.schema || normalizeIdentifierPart(object.schema) === normalizeIdentifierPart(schema);
return schemaMatches && normalizeIdentifierPart(object.name) === normalizeIdentifierPart(name);
}
return false;
}
function completionQualifierIsKnownTableOrSchema(context: SqlCompletionContext, tables: SqlCompletionTable[], schemas?: string[]): boolean {
if (!context.qualifier) return false;
const parts = context.qualifierParts?.length ? context.qualifierParts : context.qualifier.split(".").filter(Boolean);
if (parts.length === 1) {
const name = normalizeIdentifierPart(parts[0]);
if (schemas?.some((s) => normalizeIdentifierPart(s) === name)) return true;
if (tables.some((t) => normalizeIdentifierPart(t.name) === name)) return true;
return false;
}
if (parts.length === 2) {
const [schema, table] = parts;
if (tables.some((t) => normalizeIdentifierPart(t.name) === normalizeIdentifierPart(table) && (!t.schema || normalizeIdentifierPart(t.schema) === normalizeIdentifierPart(schema)))) return true;
return false;
}
return false;
}
function buildOracleSequencePseudoColumnItems(prefix: string, keywordCase?: SqlKeywordCase, hasKnownSequence = false): SqlCompletionItem[] {
const pseudoColumns = ["NEXTVAL", "CURRVAL"] as const;
return pseudoColumns
.filter((col) => matchesPrefix(col, prefix))
.map((col) => {
const label = applySqlKeywordCase(col, keywordCase);
const isNextVal = col === "NEXTVAL";
return {
label,
type: "column" as const,
detail: "sequence pseudo-column",
apply: label,
boost: computeBoost(col, prefix) + (hasKnownSequence ? (isNextVal ? 2200 : 2100) : isNextVal ? 1200 : 1100),
};
});
}
function objectIsQualifiedByContext(object: SqlCompletionObject, context: SqlCompletionContext): boolean {
if (!context.qualifier || !object.parentName) return false;
const qualifier = context.qualifier.toLowerCase();
@@ -4562,6 +4626,13 @@ function objectIsQualifiedByContext(object: SqlCompletionObject, context: SqlCom
function objectMatchesCompletionContext(object: SqlCompletionObject, context: SqlCompletionContext): boolean {
if (context.oracleTableFunctionContext && object.type !== "function") return false;
if (object.type === "sequence") {
if (context.qualifier) {
const qualifier = context.qualifier.toLowerCase();
return !!object.schema && object.schema.toLowerCase() === qualifier && matchesPrefix(object.name, context.prefix);
}
return matchesPrefix(object.name, context.prefix);
}
if (context.qualifier) {
const qualifier = context.qualifier.toLowerCase();
const qualifierParts = qualifier.split(".").filter(Boolean);
@@ -1625,6 +1625,51 @@ describe("connectionStore completion assistant", () => {
expect(objects).toEqual([expect.objectContaining({ name: "CALCULATE_BONUS", schema: "HR", type: "function", parentSchema: "HR", parentName: "PAYROLL", dataType: undefined, applyName: "HR.CALCULATE_BONUS", boost: 0 })]);
});
it("maps Oracle sequence objects from completion assistant", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "ORDER_SEQ", kind: "sequence", schema: "HR", data_type: "SEQUENCE" }],
incomplete: false,
fallback_used: false,
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
listObjects: vi.fn().mockResolvedValue([]),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [oracleConnection()];
store.connectedIds.add("oracle-1");
const objects = await store.listCompletionObjects("oracle-1", "ORCL", "ORDER", 20, "HR", undefined, false, "APP", ["routine", "sequence"]);
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ object_kinds: ["routine", "sequence"], mask: "ORDER", schema: "APP" }));
expect(objects).toEqual([expect.objectContaining({ name: "ORDER_SEQ", schema: "HR", type: "sequence", dataType: undefined, applyName: "HR.ORDER_SEQ" })]);
});
it("maps Oracle sequence objects from listObjects in fallback path", async () => {
const listCompletionObjects = vi.fn().mockResolvedValue([{ name: "ORDER_SEQ", object_type: "SEQUENCE", schema: "HR" }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch: vi.fn().mockRejectedValue(new Error("assistant disabled")),
listCompletionObjects,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [oracleConnection()];
store.connectedIds.add("oracle-1");
const objects = await store.listCompletionObjects("oracle-1", "ORCL", "ORDER", 20, "HR", undefined, false, "APP", ["routine", "sequence"]);
expect(objects).toEqual([expect.objectContaining({ name: "ORDER_SEQ", schema: "HR", type: "sequence" })]);
});
it("loads PostgreSQL routines by prefix and preserves return metadata", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "st_area", kind: "function", schema: "public", data_type: "double precision", comment: "Returns an area" }],
+2 -2
View File
@@ -8263,7 +8263,7 @@ export const useConnectionStore = defineStore("connection", () => {
const candidateType = candidate.data_type?.toUpperCase();
const type = candidate.kind === "procedure" ? "procedure" : candidate.kind === "function" ? "function" : candidate.kind === "sequence" ? "sequence" : candidate.kind === "object" && candidateType === "PACKAGE" ? "package" : null;
if (!type) return null;
const dataType = candidate.data_type && !["FUNCTION", "PROCEDURE", "PACKAGE"].includes(candidateType ?? "") ? candidate.data_type : undefined;
const dataType = candidate.data_type && !["FUNCTION", "PROCEDURE", "PACKAGE", "SEQUENCE"].includes(candidateType ?? "") ? candidate.data_type : undefined;
return {
name: candidate.name,
schema: candidate.schema ?? undefined,
@@ -9204,7 +9204,7 @@ export const useConnectionStore = defineStore("connection", () => {
function toSqlCompletionObject(object: ObjectInfo): SqlCompletionObject | null {
const objectType = object.object_type.toUpperCase();
const type = objectType.includes("PROCEDURE") ? "procedure" : objectType.includes("FUNCTION") ? "function" : objectType.includes("TRIGGER") ? "trigger" : objectType.includes("PACKAGE") ? "package" : null;
const type = objectType.includes("PROCEDURE") ? "procedure" : objectType.includes("FUNCTION") ? "function" : objectType.includes("TRIGGER") ? "trigger" : objectType.includes("PACKAGE") ? "package" : objectType.includes("SEQUENCE") ? "sequence" : null;
if (!type) return null;
return {
name: object.name,