feat(completion): suggest sequences in OceanBase-Oracle SQL

This commit is contained in:
lxk955
2026-10-13 23:48:28 +08:00
committed by GitHub
parent e78244f4d3
commit de1824274c
9 changed files with 249 additions and 26 deletions
@@ -106,3 +106,33 @@ it("does not convert two failed interpretations into successful completion", asy
api.completionAssistantSearch.mockRejectedValue(new Error("Network connection lost"));
expect(await createEditor()()).toBeNull();
});
it("suggests sequence pseudo-columns for schema-qualified OceanBase sequences when package probe reports not visible", async () => {
api.completionAssistantSearch.mockImplementation(async (request: CompletionAssistantRequest) => {
if (request.parent_name) throw new Error("Package is not visible: APP.ORDER_SEQ");
return {
candidates: [{ name: "ORDER_SEQ", schema: "APP", kind: "sequence" }],
incomplete: false,
fallback_used: false,
};
});
const provide = createEditor("SELECT APP.ORDER_SEQ.");
const result = await provide();
expect(result?.options.some((option) => option.label === "NEXTVAL")).toBe(true);
expect(result?.options.some((option) => option.label === "CURRVAL")).toBe(true);
});
it("suggests sequence pseudo-columns for unqualified sequence in current schema", async () => {
api.completionAssistantSearch.mockImplementation(async (request: CompletionAssistantRequest) => {
if (request.parent_name) throw new Error("Package is not visible: CURRENT.ORDER_SEQ");
return {
candidates: [{ name: "ORDER_SEQ", schema: "CURRENT", kind: "sequence" }],
incomplete: false,
fallback_used: false,
};
});
const provide = createEditor("SELECT ORDER_SEQ.");
const result = await provide();
expect(result?.options.some((option) => option.label === "NEXTVAL")).toBe(true);
expect(result?.options.some((option) => option.label === "CURRVAL")).toBe(true);
});
@@ -11,6 +11,7 @@ import {
getSqlCompletionContext,
getSqlCompletionResultValidFor,
isSqlLikeCompletionStatement,
normalizeIdentifierPart,
prepareSqlCompletionReplacement,
recordCompletionSelection,
shouldAutoOpenSqlCompletion,
@@ -1420,11 +1421,28 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
const list = (schema: string | undefined, parentName?: string, globalSearch = false) =>
connectionStore.listCompletionObjects(props.connectionId!, scope.database, completionContext.prefix, MAX_COMPLETION_TABLES, schema, parentName, globalSearch, scope.schema, objectKinds, !!completionContext.prefixQuoted);
if (parts.length === 0) return list(scope.schema, undefined, true);
if (parts.length === 2) return list(parts[0], parts[1]);
if (parts.length === 2) {
let packageMembers: SqlCompletionObject[] = [];
try {
packageMembers = await list(parts[0], parts[1]);
} catch (error) {
if (/Package is not visible:/i.test(String(error)) && objectKinds.includes("sequence")) {
const schemaObjects = await list(parts[0]);
return schemaObjects.filter((obj) => obj.type === "sequence");
}
throw error;
}
if (packageMembers.length > 0) return packageMembers;
if (objectKinds.includes("sequence")) {
const schemaObjects = await list(parts[0]);
return schemaObjects.filter((obj) => obj.type === "sequence");
}
return [];
}
if (parts.length > 2) return [];
// One qualifier can name a schema or a package in the current schema.
const groups = await Promise.allSettled([list(parts[0]), list(scope.schema, parts[0])]);
if (groups[0].status === "rejected" && groups[1].status === "rejected") throw groups[0].reason;
const groups = await Promise.allSettled([list(parts[0]), list(scope.schema, parts[0]), ...(objectKinds.includes("sequence") && scope.schema && normalizeIdentifierPart(scope.schema) !== normalizeIdentifierPart(parts[0]) ? [list(scope.schema)] : [])]);
if (groups.every((g) => g.status === "rejected")) throw groups[0].status === "rejected" ? groups[0].reason : undefined;
let objects: SqlCompletionObject[] = [];
for (const group of groups) {
if (group.status === "fulfilled") objects = mergeCompletionObjects(objects, group.value);
@@ -675,6 +675,163 @@ describe("PostgreSQL sequence literal completion", () => {
);
});
it("suggests OceanBase-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: "oceanbase-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: "oceanbase-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 OceanBase-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: "oceanbase-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 OceanBase-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: "oceanbase-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 OceanBase-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: "oceanbase-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 OceanBase-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: "oceanbase-oracle",
dialect: "oracle",
keywordCase: "lower",
});
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "nextval",
apply: "nextval",
}),
]),
);
});
it("suggests OceanBase-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: "oceanbase-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"],
+4 -4
View File
@@ -4837,11 +4837,11 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
})
.map((object) => {
const qualifiedByContext = objectIsQualifiedByContext(object, context, databaseType, currentSchema);
const objectInCurrentSchema = !!currentSchema && !!object.schema && (databaseType === "oceanbase-oracle" ? object.schema === currentSchema : normalizeIdentifierPart(object.schema) === normalizeIdentifierPart(currentSchema));
const objectInCurrentSchema = !!currentSchema && !!object.schema && normalizeIdentifierPart(object.schema) === normalizeIdentifierPart(currentSchema);
const suppliedApplyName = object.applyName ? quoteCompletionRoutineName(object.applyName, dialect) : undefined;
const oceanBaseApplyName = databaseType === "oceanbase-oracle" ? [...(object.schema && !objectInCurrentSchema ? [object.schema] : []), ...(object.parentName ? [object.parentName] : []), object.name].map((part) => quoteCompletionRoutineIdentifier(part, dialect)).join(".") : undefined;
const applyName =
qualifiedByContext || (context.qualifier && (databaseType === "oceanbase-oracle" ? object.schema === oceanBaseRoutineQualifierParts(context)[0] : object.schema?.toLowerCase() === context.qualifier.toLowerCase()))
qualifiedByContext || (context.qualifier && (databaseType === "oceanbase-oracle" ? normalizeIdentifierPart(object.schema ?? "") === normalizeIdentifierPart(oceanBaseRoutineQualifierParts(context)[0] ?? "") : object.schema?.toLowerCase() === context.qualifier.toLowerCase()))
? quoteCompletionRoutineIdentifier(object.name, dialect)
: (oceanBaseApplyName ?? suppliedApplyName ?? (object.schema && !objectInCurrentSchema ? `${quoteCompletionRoutineIdentifier(object.schema, dialect)}.${quoteCompletionRoutineIdentifier(object.name, dialect)}` : quoteCompletionRoutineIdentifier(object.name, dialect)));
const locationDetail = object.type === "trigger" && object.parentName ? `trigger on ${object.parentName}` : object.parentName ? `${object.type} in ${object.parentName}` : object.schema ? `${object.type} in ${object.schema}` : object.type;
@@ -5006,7 +5006,7 @@ function objectMatchesCompletionContext(object: SqlCompletionObject, context: Sq
const prefixMatches = context.prefixQuoted ? object.name.startsWith(context.prefix) : matchesPrefix(object.name, context.prefix);
if (!context.qualifier) return prefixMatches;
const parts = oceanBaseRoutineQualifierParts(context);
return prefixMatches && (object.parentName ? objectIsQualifiedByContext(object, context, databaseType, currentSchema) : parts.length === 1 && object.schema === parts[0]);
return prefixMatches && (object.parentName ? objectIsQualifiedByContext(object, context, databaseType, currentSchema) : parts.length === 1 && normalizeIdentifierPart(object.schema ?? "") === normalizeIdentifierPart(parts[0] ?? ""));
}
if (object.type === "sequence") {
if (context.qualifier) {
@@ -5942,7 +5942,7 @@ function normalizeTableName(name: string): string {
.toLowerCase();
}
function normalizeIdentifierPart(name: string): string {
export function normalizeIdentifierPart(name: string): string {
return name.replace(/^["`[]|["`\]]$/g, "").toLowerCase();
}
@@ -1893,9 +1893,9 @@ describe("connectionStore completion assistant", () => {
});
it.each([
["empty", vi.fn().mockResolvedValue({ candidates: [], incomplete: false, fallback_used: false })],
["permission error", vi.fn().mockRejectedValue(new Error("permission denied for sequence"))],
])("returns an empty sequence fallback for %s metadata", async (_caseName, completionAssistantSearch) => {
["empty", vi.fn().mockResolvedValue({ candidates: [], incomplete: false, fallback_used: false }), false],
["permission error", vi.fn().mockRejectedValue(new Error("permission denied for sequence")), true],
])("returns an empty sequence fallback for %s metadata", async (_caseName, completionAssistantSearch, expectMetadataCall) => {
const listCompletionObjects = vi.fn().mockResolvedValue([{ name: "not_a_sequence", object_type: "FUNCTION" }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
@@ -1903,6 +1903,7 @@ describe("connectionStore completion assistant", () => {
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
listCompletionObjects,
listSchemas: vi.fn().mockResolvedValue(["public"]),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
@@ -1911,7 +1912,11 @@ describe("connectionStore completion assistant", () => {
store.connectedIds.add("pg-1");
await expect(store.listCompletionObjects("pg-1", "app", "missing", 20, undefined, undefined, false, undefined, ["sequence"])).resolves.toEqual([]);
expect(listCompletionObjects).not.toHaveBeenCalled();
// The sequence-only error fallback loads schema-level objects (filtered to
// sequences), so the metadata call goes through on permission errors; the
// empty-metadata case returns [] without touching the metadata API.
if (expectMetadataCall) expect(listCompletionObjects).toHaveBeenCalled();
else expect(listCompletionObjects).not.toHaveBeenCalled();
});
it("searches the server-reported SQL Server default schema without treating the username as a schema", async () => {
+13 -3
View File
@@ -9256,6 +9256,15 @@ export const useConnectionStore = defineStore("connection", () => {
return deduped;
}
function normalizeCompletionIdentifier(name?: string | null): string {
if (!name) return "";
const trimmed = name.trim();
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("`") && trimmed.endsWith("`")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
return trimmed.slice(1, -1);
}
return trimmed.toLowerCase();
}
async function listCompletionObjects(
connectionId: string,
database: string,
@@ -9302,7 +9311,8 @@ export const useConnectionStore = defineStore("connection", () => {
if (databaseType === "oceanbase-oracle" && parentName) throw error;
if (databaseType === "oceanbase-oracle" && !/does not support filtered routine completion|unknown method|method not found|not supported/i.test(String(error))) throw error;
if (objectKinds.length === 1 && objectKinds[0] === "sequence") {
loadedObjects = [];
const objects = isSchemaAwareDatabase(connectionId) ? await listSchemaAwareCompletionObjects(connectionId, database, schema) : await api.listCompletionObjects(connectionId, database, connectionDatabaseMetadataSchema(getConfig(connectionId), database, schema));
loadedObjects = dedupeCompletionObjects(objects.map(toSqlCompletionObject).filter((object): object is SqlCompletionObject => object?.type === "sequence"));
} else {
const objects = isSchemaAwareDatabase(connectionId) ? await listSchemaAwareCompletionObjects(connectionId, database, schema) : await api.listCompletionObjects(connectionId, database, connectionDatabaseMetadataSchema(getConfig(connectionId), database, schema));
loadedObjects = dedupeCompletionObjects(objects.map(toSqlCompletionObject).filter((object): object is SqlCompletionObject => object != null));
@@ -9328,8 +9338,8 @@ export const useConnectionStore = defineStore("connection", () => {
if (!objectKinds.includes("sequence")) return false;
} else if (object.type !== "procedure" && object.type !== "function") return false;
else if (!objectKinds.includes("routine") && !objectKinds.includes(object.type)) return false;
if (schema && !globalSearch && object.schema !== schema) return false;
if (parentName && object.parentName !== parentName) return false;
if (schema && !globalSearch && (!object.schema || normalizeCompletionIdentifier(object.schema) !== normalizeCompletionIdentifier(schema))) return false;
if (parentName && (!object.parentName || normalizeCompletionIdentifier(object.parentName) !== normalizeCompletionIdentifier(parentName))) return false;
if (caseSensitive) return object.name.includes(filter.trim());
}
return !normalizedFilter || fuzzyCompletionObjectMatch(object, normalizedFilter);
+6 -11
View File
@@ -7659,22 +7659,17 @@ fn filter_completion_objects(objects: Vec<db::ObjectInfo>) -> Vec<db::ObjectInfo
.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.contains("PROCEDURE")
|| object_type.contains("FUNCTION")
|| object_type.contains("TRIGGER")
|| object_type.contains("PACKAGE")
|| object_type.contains("SEQUENCE")
})
.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()
filter_completion_objects(objects)
}
fn is_agent_postgres_metadata_fallback_config(config: &ConnectionConfig) -> bool {
+6 -1
View File
@@ -102,8 +102,11 @@ fn list_objects_sql(schema: &str) -> String {
FROM ALL_PROCEDURES \
WHERE OWNER = {s} AND OBJECT_TYPE IN ('PROCEDURE', 'FUNCTION') AND PROCEDURE_NAME IS NULL \
UNION ALL \
SELECT SEQUENCE_NAME AS OBJECT_NAME, 'SEQUENCE' AS OBJECT_TYPE, SEQUENCE_OWNER AS OBJECT_SCHEMA, 4 AS SORT_ORDER \
FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = {s} \
UNION ALL \
SELECT OBJECT_NAME, CASE OBJECT_TYPE WHEN 'PACKAGE BODY' THEN 'PACKAGE_BODY' ELSE OBJECT_TYPE END AS OBJECT_TYPE, \
OWNER AS OBJECT_SCHEMA, CASE WHEN OBJECT_TYPE = 'PACKAGE' THEN 4 ELSE 5 END AS SORT_ORDER \
OWNER AS OBJECT_SCHEMA, CASE WHEN OBJECT_TYPE = 'PACKAGE' THEN 5 ELSE 6 END AS SORT_ORDER \
FROM ALL_OBJECTS \
WHERE OWNER = {s} AND OBJECT_TYPE IN ('PACKAGE', 'PACKAGE BODY') \
ORDER BY SORT_ORDER, OBJECT_NAME",
@@ -349,6 +352,8 @@ mod tests {
assert!(sql.contains("ALL_PROCEDURES"));
assert!(sql.contains("'PROCEDURE'"));
assert!(sql.contains("'FUNCTION'"));
assert!(sql.contains("ALL_SEQUENCES"));
assert!(sql.contains("'SEQUENCE'"));
assert!(sql.contains("ALL_OBJECTS"));
assert!(sql.contains("'PACKAGE'"));
assert!(sql.contains("'PACKAGE BODY'"));
+3
View File
@@ -159,6 +159,9 @@ metadata_queries:
list_triggers:
sql: "SELECT TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT FROM ALL_TRIGGERS WHERE OWNER = '{schema}' AND TABLE_NAME = '{table}'"
performance_profile: "MEDIUM"
list_sequences:
sql: "SELECT SEQUENCE_NAME FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '{schema}'"
performance_profile: "LIGHT"
dependencies:
sql: "SELECT a.TABLE_NAME, c_pk.TABLE_NAME AS R_TABLE_NAME FROM ALL_CONS_COLUMNS a JOIN ALL_CONSTRAINTS c ON a.CONSTRAINT_NAME = c.CONSTRAINT_NAME JOIN ALL_CONSTRAINTS c_pk ON c.R_CONSTRAINT_NAME = c_pk.CONSTRAINT_NAME WHERE c.CONSTRAINT_TYPE = 'R' AND a.OWNER = '{schema}'"
depth_support: 2