fix(oceanbase-oracle): complete tables across schemas
This commit is contained in:
1 parent
53184faf87
commit
a73bc4df69
10 files changed
+574
-22
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
import { appendFileSync } from "node:fs";
|
import { appendFileSync, existsSync, readdirSync } from "node:fs";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
@@ -12,7 +12,21 @@ export function changedPaths(base, root) {
|
|||||||
}).split("\0").filter(Boolean);
|
}).split("\0").filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function planCi({ files, metadata, root, eventName = "pull_request", rustChanged = false, agentsChanged = false }) {
|
export function knownJavaDrivers(root, nativeDrivers) {
|
||||||
|
try {
|
||||||
|
const driversRoot = path.join(root, "agents/drivers");
|
||||||
|
return readdirSync(driversRoot, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory()
|
||||||
|
&& existsSync(path.join(driversRoot, entry.name, "build.gradle"))
|
||||||
|
&& !nativeDrivers.includes(entry.name))
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function planCi({ files, metadata, root, eventName = "pull_request", rustChanged = false, agentsChanged = false, javaDrivers }) {
|
||||||
const unknownDiff = files === null;
|
const unknownDiff = files === null;
|
||||||
files ??= [];
|
files ??= [];
|
||||||
const packages = metadata.packages.filter((pkg) => metadata.workspace_members.includes(pkg.id));
|
const packages = metadata.packages.filter((pkg) => metadata.workspace_members.includes(pkg.id));
|
||||||
@@ -49,12 +63,17 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
|
|||||||
.filter(([, names]) => names.some((name) => affected.has(name)))
|
.filter(([, names]) => names.some((name) => affected.has(name)))
|
||||||
.map(([group]) => ({ group }));
|
.map(([group]) => ({ group }));
|
||||||
const nativeDrivers = [...goAgents.map((agent) => agent.driver), ...rustAgents];
|
const nativeDrivers = [...goAgents.map((agent) => agent.driver), ...rustAgents];
|
||||||
|
const jdbcDrivers = javaDrivers ?? knownJavaDrivers(root, nativeDrivers);
|
||||||
const nativeChanges = new Set(nativeDrivers.filter((driver) => files.some((file) => file.startsWith(`agents/drivers/${driver}/`))));
|
const nativeChanges = new Set(nativeDrivers.filter((driver) => files.some((file) => file.startsWith(`agents/drivers/${driver}/`))));
|
||||||
|
const javaDriverChanges = jdbcDrivers.some((driver) => files.some((file) => file.startsWith(`agents/drivers/${driver}/`)));
|
||||||
|
// Driver-scoped JDBC/Go/Rust trees are not "shared Agent inputs". Unknown
|
||||||
|
// agents/drivers/<name>/ paths still fail open to full coverage below.
|
||||||
const sharedAgents = ciChanged || sharedRust || files.some((file) => file.startsWith("agents/")
|
const sharedAgents = ciChanged || sharedRust || files.some((file) => file.startsWith("agents/")
|
||||||
&& !nativeDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`)))
|
&& !nativeDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`))
|
||||||
|
&& !jdbcDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`)))
|
||||||
|| files.some((file) => file.startsWith(".github/scripts/bump-agent-versions.") || file === ".github/workflows/agents-release.yml"
|
|| files.some((file) => file.startsWith(".github/scripts/bump-agent-versions.") || file === ".github/workflows/agents-release.yml"
|
||||||
|| file === "crates/dbx-drivers/assets/agent-protocol-v2.json");
|
|| file === "crates/dbx-drivers/assets/agent-protocol-v2.json");
|
||||||
const allAgents = sharedAgents || (agentsChanged && nativeChanges.size === 0);
|
const allAgents = sharedAgents || (agentsChanged && nativeChanges.size === 0 && !javaDriverChanges);
|
||||||
if (allAgents) {
|
if (allAgents) {
|
||||||
for (const driver of nativeDrivers) nativeChanges.add(driver);
|
for (const driver of nativeDrivers) nativeChanges.add(driver);
|
||||||
}
|
}
|
||||||
@@ -62,8 +81,8 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
|
|||||||
const goMatrix = goAgents.filter((agent) => nativeChanges.has(agent.driver));
|
const goMatrix = goAgents.filter((agent) => nativeChanges.has(agent.driver));
|
||||||
const nativeRustMatrix = rustAgents.filter((driver) => nativeChanges.has(driver)).map((driver) => ({ driver }));
|
const nativeRustMatrix = rustAgents.filter((driver) => nativeChanges.has(driver)).map((driver) => ({ driver }));
|
||||||
const liveMatrix = integrationCases.filter((entry) => nativeChanges.has(entry.driver));
|
const liveMatrix = integrationCases.filter((entry) => nativeChanges.has(entry.driver));
|
||||||
const java = allAgents;
|
const java = allAgents || javaDriverChanges;
|
||||||
const agents = agentsChanged || sharedAgents || nativeChanges.size > 0;
|
const agents = agentsChanged || sharedAgents || nativeChanges.size > 0 || javaDriverChanges;
|
||||||
return {
|
return {
|
||||||
rust, rust_full: full, rust_matrix: { include: rustMatrix }, rust_groups_known: !unknownMember,
|
rust, rust_full: full, rust_matrix: { include: rustMatrix }, rust_groups_known: !unknownMember,
|
||||||
affected_packages: [...affected].sort(),
|
affected_packages: [...affected].sort(),
|
||||||
|
|||||||
@@ -106,6 +106,21 @@ for (const driver of ["duckdb", "tdengine", ...goAgents.map((entry) => entry.dri
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("known JDBC driver changes only select Java agent tests", () => {
|
||||||
|
const result = plan(["agents/drivers/oceanbase-oracle/src/main/java/Agent.java"], {
|
||||||
|
agentsChanged: true,
|
||||||
|
javaDrivers: ["oceanbase-oracle", "dameng"],
|
||||||
|
});
|
||||||
|
assert.equal(result.rust, false);
|
||||||
|
assert.equal(result.agents, true);
|
||||||
|
assert.equal(result.agent_java, true);
|
||||||
|
assert.equal(result.agent_go.include.length, 0);
|
||||||
|
assert.equal(result.agent_rust.include.length, 0);
|
||||||
|
assert.equal(result.agent_integration.include.length, 0);
|
||||||
|
assert.equal(result.duckdb_changed, false);
|
||||||
|
assert.equal(result.fast, true);
|
||||||
|
});
|
||||||
|
|
||||||
test("shared Agent inputs and unknown native modules never silently lose coverage", () => {
|
test("shared Agent inputs and unknown native modules never silently lose coverage", () => {
|
||||||
for (const file of ["agents/common/src/main/java/Protocol.java", "agents/scripts/validate_agents.py", "agents/build.gradle",
|
for (const file of ["agents/common/src/main/java/Protocol.java", "agents/scripts/validate_agents.py", "agents/build.gradle",
|
||||||
"agents/drivers/new-driver/main.go", "crates/dbx-drivers/assets/agent-protocol-v2.json", ".github/workflows/agents-release.yml"]) {
|
"agents/drivers/new-driver/main.go", "crates/dbx-drivers/assets/agent-protocol-v2.json", ".github/workflows/agents-release.yml"]) {
|
||||||
|
|||||||
+332
@@ -2,6 +2,12 @@ package com.dbx.agent.oceanbaseoracle;
|
|||||||
|
|
||||||
import com.dbx.agent.ColumnInfo;
|
import com.dbx.agent.ColumnInfo;
|
||||||
import com.dbx.agent.AgentProtocol;
|
import com.dbx.agent.AgentProtocol;
|
||||||
|
import com.dbx.agent.CompletionAssistantCandidate;
|
||||||
|
import com.dbx.agent.CompletionAssistantCandidateKind;
|
||||||
|
import com.dbx.agent.CompletionAssistantMatchMode;
|
||||||
|
import com.dbx.agent.CompletionAssistantObjectKind;
|
||||||
|
import com.dbx.agent.CompletionAssistantRequest;
|
||||||
|
import com.dbx.agent.CompletionAssistantResponse;
|
||||||
import com.dbx.agent.ConfiguredJdbcAgent;
|
import com.dbx.agent.ConfiguredJdbcAgent;
|
||||||
import com.dbx.agent.ConnectParams;
|
import com.dbx.agent.ConnectParams;
|
||||||
import com.dbx.agent.DatabaseInfo;
|
import com.dbx.agent.DatabaseInfo;
|
||||||
@@ -34,8 +40,10 @@ import java.util.ArrayDeque;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -388,6 +396,330 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionAssistantResponse completionAssistantSearch(CompletionAssistantRequest request) {
|
||||||
|
if (hasTableLikeCompletionKind(request.getObject_kinds())) {
|
||||||
|
return unchecked(() -> completionAssistantTables(request));
|
||||||
|
}
|
||||||
|
return super.completionAssistantSearch(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletionAssistantResponse completionAssistantTables(CompletionAssistantRequest request) throws SQLException {
|
||||||
|
int limit = boundedCompletionLimit(request.getMax_results());
|
||||||
|
int scanLimit = Math.min(1000, Math.max(limit * 3, limit + 1));
|
||||||
|
String preferredSchema = preferredCompletionSchema(request);
|
||||||
|
CompletionTablesQuery query = buildCompletionTablesQuery(request, preferredSchema, scanLimit + 1);
|
||||||
|
List<CompletionTableRow> rows = new ArrayList<>();
|
||||||
|
try (PreparedStatement stmt = requireConnection().prepareStatement(query.sql)) {
|
||||||
|
bindCompletionArgs(stmt, query.args);
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
rows.add(new CompletionTableRow(
|
||||||
|
rs.getString(1),
|
||||||
|
rs.getString(2),
|
||||||
|
rs.getString(3),
|
||||||
|
rs.getString(4),
|
||||||
|
rs.getString(5)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<CompletionSynonymTarget> validTargets = validCompletionSynonymTargets(
|
||||||
|
rows,
|
||||||
|
completionTableObjectTypes(request.getObject_kinds())
|
||||||
|
);
|
||||||
|
List<CompletionAssistantCandidate> candidates = new ArrayList<>();
|
||||||
|
for (CompletionTableRow row : rows) {
|
||||||
|
if (row.name == null || row.name.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ("SYNONYM".equalsIgnoreCase(row.objectType)
|
||||||
|
&& (row.targetOwner == null || row.targetName == null
|
||||||
|
|| !validTargets.contains(new CompletionSynonymTarget(row.targetOwner, row.targetName)))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CompletionAssistantCandidateKind kind = "VIEW".equalsIgnoreCase(row.objectType)
|
||||||
|
? CompletionAssistantCandidateKind.VIEW
|
||||||
|
: CompletionAssistantCandidateKind.TABLE;
|
||||||
|
candidates.add(new CompletionAssistantCandidate(
|
||||||
|
row.name,
|
||||||
|
kind,
|
||||||
|
blankToNull(request.getDatabase()),
|
||||||
|
row.owner,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
row.objectType
|
||||||
|
));
|
||||||
|
}
|
||||||
|
boolean incomplete = candidates.size() > limit;
|
||||||
|
if (incomplete) {
|
||||||
|
candidates = new ArrayList<>(candidates.subList(0, limit));
|
||||||
|
}
|
||||||
|
return new CompletionAssistantResponse(candidates, incomplete, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletionTablesQuery buildCompletionTablesQuery(
|
||||||
|
CompletionAssistantRequest request,
|
||||||
|
String preferredSchema,
|
||||||
|
int limit
|
||||||
|
) {
|
||||||
|
List<String> objectTypes = completionTableObjectTypes(request.getObject_kinds());
|
||||||
|
boolean caseSensitive = request.getCase_sensitive();
|
||||||
|
String pattern = completionLikePattern(request.getMask(), request.getMatch_mode());
|
||||||
|
// OceanBase Oracle matches metadata the same way as listTables: fold the
|
||||||
|
// bind value in Java and compare UPPER(column) LIKE ?. UPPER(?) on binds
|
||||||
|
// is unreliable for fuzzy/lowercase masks in this driver.
|
||||||
|
if (!caseSensitive) {
|
||||||
|
pattern = pattern.toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
List<Object> args = new ArrayList<>();
|
||||||
|
String namePredicate = completionNamePredicate("o.OBJECT_NAME", caseSensitive);
|
||||||
|
String synonymNamePredicate = completionNamePredicate("s.SYNONYM_NAME", caseSensitive);
|
||||||
|
args.add(pattern);
|
||||||
|
|
||||||
|
String ownerPredicate = "";
|
||||||
|
String synonymOwnerPredicate = "";
|
||||||
|
String owner = "";
|
||||||
|
if (!request.getGlobal_search()) {
|
||||||
|
owner = firstNonBlank(request.getParent_schema(), request.getSchema(), preferredSchema);
|
||||||
|
if (owner == null) {
|
||||||
|
owner = "";
|
||||||
|
}
|
||||||
|
owner = owner.toUpperCase(Locale.ROOT);
|
||||||
|
args.add(owner);
|
||||||
|
ownerPredicate = " AND UPPER(o.OWNER) = ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
args.add(pattern);
|
||||||
|
if (!owner.isEmpty()) {
|
||||||
|
args.add(owner);
|
||||||
|
synonymOwnerPredicate = " AND UPPER(s.OWNER) = ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
String preferred = preferredSchema == null ? "" : preferredSchema.trim();
|
||||||
|
String preferredFolded = caseSensitive ? preferred : preferred.toUpperCase(Locale.ROOT);
|
||||||
|
String exactMask = request.getMask() == null ? "" : request.getMask().trim();
|
||||||
|
String exactFolded = caseSensitive ? exactMask : exactMask.toUpperCase(Locale.ROOT);
|
||||||
|
args.add(preferredFolded);
|
||||||
|
args.add(exactFolded);
|
||||||
|
args.add(limit);
|
||||||
|
|
||||||
|
String typeList = String.join(", ", objectTypes);
|
||||||
|
String baseSql = """
|
||||||
|
SELECT o.OWNER,
|
||||||
|
o.OBJECT_NAME,
|
||||||
|
o.OBJECT_TYPE,
|
||||||
|
CAST(NULL AS VARCHAR2(128)) AS TARGET_OWNER,
|
||||||
|
CAST(NULL AS VARCHAR2(128)) AS TARGET_NAME
|
||||||
|
FROM ALL_OBJECTS o
|
||||||
|
WHERE o.OBJECT_TYPE IN (%s)
|
||||||
|
AND %s%s
|
||||||
|
UNION ALL
|
||||||
|
SELECT s.OWNER,
|
||||||
|
s.SYNONYM_NAME AS OBJECT_NAME,
|
||||||
|
'SYNONYM' AS OBJECT_TYPE,
|
||||||
|
s.TABLE_OWNER AS TARGET_OWNER,
|
||||||
|
s.TABLE_NAME AS TARGET_NAME
|
||||||
|
FROM ALL_SYNONYMS s
|
||||||
|
WHERE s.DB_LINK IS NULL
|
||||||
|
AND %s%s
|
||||||
|
""".formatted(typeList, namePredicate, ownerPredicate, synonymNamePredicate, synonymOwnerPredicate)
|
||||||
|
.stripIndent()
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
String preferredOwnerPredicate = caseSensitive ? "OWNER = ?" : "UPPER(OWNER) = ?";
|
||||||
|
String exactNamePredicate = caseSensitive
|
||||||
|
? "OBJECT_NAME = ?"
|
||||||
|
: "UPPER(OBJECT_NAME) = ?";
|
||||||
|
String orderedSql = """
|
||||||
|
SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, TARGET_OWNER, TARGET_NAME
|
||||||
|
FROM (
|
||||||
|
%s
|
||||||
|
)
|
||||||
|
ORDER BY CASE
|
||||||
|
WHEN %s THEN 0
|
||||||
|
WHEN OWNER = 'PUBLIC' THEN 1
|
||||||
|
WHEN OWNER IN ('SYS','SYSTEM','SYSMAN','DBSNMP','OUTLN','XDB','MDSYS','CTXSYS','WMSYS') THEN 3
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
|
CASE WHEN %s THEN 0 ELSE 1 END,
|
||||||
|
CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 WHEN 'SYNONYM' THEN 3 ELSE 4 END,
|
||||||
|
OBJECT_NAME,
|
||||||
|
OWNER
|
||||||
|
""".formatted(baseSql, preferredOwnerPredicate, exactNamePredicate).stripIndent().trim();
|
||||||
|
|
||||||
|
String sql = """
|
||||||
|
SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, TARGET_OWNER, TARGET_NAME
|
||||||
|
FROM (
|
||||||
|
%s
|
||||||
|
)
|
||||||
|
WHERE ROWNUM <= ?
|
||||||
|
""".formatted(orderedSql).stripIndent().trim();
|
||||||
|
|
||||||
|
return new CompletionTablesQuery(sql, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> completionTableObjectTypes(List<CompletionAssistantObjectKind> kinds) {
|
||||||
|
List<String> objectTypes = new ArrayList<>();
|
||||||
|
boolean any = kinds == null || kinds.isEmpty();
|
||||||
|
if (any || kinds.contains(CompletionAssistantObjectKind.TABLE)) {
|
||||||
|
objectTypes.add("'TABLE'");
|
||||||
|
}
|
||||||
|
if (any || kinds.contains(CompletionAssistantObjectKind.VIEW)) {
|
||||||
|
objectTypes.add("'VIEW'");
|
||||||
|
}
|
||||||
|
if (objectTypes.isEmpty()) {
|
||||||
|
objectTypes.add("'TABLE'");
|
||||||
|
objectTypes.add("'VIEW'");
|
||||||
|
}
|
||||||
|
return objectTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasTableLikeCompletionKind(List<CompletionAssistantObjectKind> kinds) {
|
||||||
|
if (kinds == null || kinds.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (CompletionAssistantObjectKind kind : kinds) {
|
||||||
|
if (kind == CompletionAssistantObjectKind.TABLE || kind == CompletionAssistantObjectKind.VIEW) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String preferredCompletionSchema(CompletionAssistantRequest request) throws SQLException {
|
||||||
|
String preferred = firstNonBlank(request.getSchema());
|
||||||
|
if (preferred != null && !preferred.isBlank()) {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
String current = currentSchema();
|
||||||
|
return current == null ? "" : current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String completionLikePattern(String mask, CompletionAssistantMatchMode matchMode) {
|
||||||
|
String escaped = escapeLikePattern(mask == null ? "" : mask.trim());
|
||||||
|
if (matchMode == CompletionAssistantMatchMode.CONTAINS) {
|
||||||
|
return "%" + escaped + "%";
|
||||||
|
}
|
||||||
|
return escaped + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeLikePattern(String mask) {
|
||||||
|
return mask.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String completionNamePredicate(String column, boolean caseSensitive) {
|
||||||
|
if (caseSensitive) {
|
||||||
|
return column + " LIKE ? ESCAPE '\\'";
|
||||||
|
}
|
||||||
|
return "UPPER(" + column + ") LIKE ? ESCAPE '\\'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int boundedCompletionLimit(Integer requested) {
|
||||||
|
if (requested == null || requested <= 0) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return Math.min(requested, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String value) {
|
||||||
|
return value == null || value.isBlank() ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindCompletionArgs(PreparedStatement stmt, List<Object> args) throws SQLException {
|
||||||
|
for (int i = 0; i < args.size(); i++) {
|
||||||
|
Object arg = args.get(i);
|
||||||
|
if (arg instanceof Integer integer) {
|
||||||
|
stmt.setInt(i + 1, integer);
|
||||||
|
} else {
|
||||||
|
stmt.setString(i + 1, arg == null ? null : String.valueOf(arg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<CompletionSynonymTarget> validCompletionSynonymTargets(
|
||||||
|
List<CompletionTableRow> rows,
|
||||||
|
List<String> objectTypes
|
||||||
|
) throws SQLException {
|
||||||
|
Set<CompletionSynonymTarget> targets = new LinkedHashSet<>();
|
||||||
|
for (CompletionTableRow row : rows) {
|
||||||
|
if ("SYNONYM".equalsIgnoreCase(row.objectType) && row.targetOwner != null && row.targetName != null) {
|
||||||
|
targets.add(new CompletionSynonymTarget(row.targetOwner, row.targetName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<CompletionSynonymTarget> valid = new HashSet<>();
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
String typeList = String.join(", ", objectTypes);
|
||||||
|
List<CompletionSynonymTarget> ordered = new ArrayList<>(targets);
|
||||||
|
for (int start = 0; start < ordered.size(); start += 100) {
|
||||||
|
List<CompletionSynonymTarget> batch = ordered.subList(start, Math.min(start + 100, ordered.size()));
|
||||||
|
List<Object> args = new ArrayList<>();
|
||||||
|
List<String> predicates = new ArrayList<>();
|
||||||
|
for (CompletionSynonymTarget target : batch) {
|
||||||
|
args.add(target.owner());
|
||||||
|
args.add(target.name());
|
||||||
|
predicates.add("(o.OWNER = ? AND o.OBJECT_NAME = ?)");
|
||||||
|
}
|
||||||
|
String sql = "SELECT DISTINCT o.OWNER, o.OBJECT_NAME"
|
||||||
|
+ " FROM ALL_OBJECTS o"
|
||||||
|
+ " WHERE o.OBJECT_TYPE IN (" + typeList + ")"
|
||||||
|
+ " AND (" + String.join(" OR ", predicates) + ")";
|
||||||
|
try (PreparedStatement stmt = requireConnection().prepareStatement(sql)) {
|
||||||
|
bindCompletionArgs(stmt, args);
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
valid.add(new CompletionSynonymTarget(rs.getString(1), rs.getString(2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
record CompletionSynonymTarget(String owner, String name) {
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class CompletionTableRow {
|
||||||
|
final String owner;
|
||||||
|
final String name;
|
||||||
|
final String objectType;
|
||||||
|
final String targetOwner;
|
||||||
|
final String targetName;
|
||||||
|
|
||||||
|
CompletionTableRow(String owner, String name, String objectType, String targetOwner, String targetName) {
|
||||||
|
this.owner = owner;
|
||||||
|
this.name = name;
|
||||||
|
this.objectType = objectType;
|
||||||
|
this.targetOwner = targetOwner;
|
||||||
|
this.targetName = targetName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class CompletionTablesQuery {
|
||||||
|
final String sql;
|
||||||
|
final List<Object> args;
|
||||||
|
|
||||||
|
CompletionTablesQuery(String sql, List<Object> args) {
|
||||||
|
this.sql = sql;
|
||||||
|
this.args = List.copyOf(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static MetadataSql oceanBaseMetadataSql(
|
private static MetadataSql oceanBaseMetadataSql(
|
||||||
String baseSql,
|
String baseSql,
|
||||||
String selectList,
|
String selectList,
|
||||||
|
|||||||
+89
@@ -2,6 +2,10 @@ package com.dbx.agent.oceanbaseoracle;
|
|||||||
|
|
||||||
import com.dbx.agent.AgentProtocol;
|
import com.dbx.agent.AgentProtocol;
|
||||||
import com.dbx.agent.ColumnInfo;
|
import com.dbx.agent.ColumnInfo;
|
||||||
|
import com.dbx.agent.CompletionAssistantMatchMode;
|
||||||
|
import com.dbx.agent.CompletionAssistantObjectKind;
|
||||||
|
import com.dbx.agent.CompletionAssistantRequest;
|
||||||
|
import com.dbx.agent.CompletionAssistantResponse;
|
||||||
import com.dbx.agent.ConnectParams;
|
import com.dbx.agent.ConnectParams;
|
||||||
import com.dbx.agent.ExecuteQueryOptions;
|
import com.dbx.agent.ExecuteQueryOptions;
|
||||||
import com.dbx.agent.MetadataListConstraints;
|
import com.dbx.agent.MetadataListConstraints;
|
||||||
@@ -460,6 +464,64 @@ class OceanBaseOracleAgentTest {
|
|||||||
Assertions.assertTrue(sql.get(0).contains("ROWNUM <= ?"), sql.get(0));
|
Assertions.assertTrue(sql.get(0).contains("ROWNUM <= ?"), sql.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void globalCompletionTableSearchOmitsOwnerFilter() {
|
||||||
|
CompletionAssistantRequest request = completionRequest("DWD", null, "STAG", true);
|
||||||
|
OceanBaseOracleAgent.CompletionTablesQuery query = OceanBaseOracleAgent.buildCompletionTablesQuery(request, "DWD", 21);
|
||||||
|
|
||||||
|
Assertions.assertTrue(query.sql.contains("FROM ALL_OBJECTS"), query.sql);
|
||||||
|
Assertions.assertTrue(query.sql.contains("FROM ALL_SYNONYMS"), query.sql);
|
||||||
|
Assertions.assertTrue(query.sql.contains("UPPER(o.OBJECT_NAME) LIKE ?"), query.sql);
|
||||||
|
Assertions.assertFalse(query.sql.contains("UPPER(o.OWNER) = ?"), query.sql);
|
||||||
|
Assertions.assertFalse(query.sql.contains("UPPER(s.OWNER) = ?"), query.sql);
|
||||||
|
Assertions.assertTrue(query.sql.contains("ROWNUM <= ?"), query.sql);
|
||||||
|
Assertions.assertEquals(List.of("STAG%", "STAG%", "DWD", "STAG", 21), query.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completionTableSearchFoldsLowercaseMasksForFuzzyMatch() {
|
||||||
|
CompletionAssistantRequest request = completionRequest("dwd", null, "ord", true);
|
||||||
|
setField(request, "match_mode", CompletionAssistantMatchMode.CONTAINS);
|
||||||
|
OceanBaseOracleAgent.CompletionTablesQuery query = OceanBaseOracleAgent.buildCompletionTablesQuery(request, "dwd", 21);
|
||||||
|
|
||||||
|
Assertions.assertTrue(query.sql.contains("UPPER(o.OBJECT_NAME) LIKE ?"), query.sql);
|
||||||
|
Assertions.assertTrue(query.sql.contains("UPPER(OWNER) = ?"), query.sql);
|
||||||
|
Assertions.assertTrue(query.sql.contains("UPPER(OBJECT_NAME) = ?"), query.sql);
|
||||||
|
Assertions.assertFalse(query.sql.contains("LIKE UPPER(?)"), query.sql);
|
||||||
|
Assertions.assertEquals(List.of("%ORD%", "%ORD%", "DWD", "ORD", 21), query.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scopedCompletionTableSearchFiltersOwnerCaseInsensitively() {
|
||||||
|
CompletionAssistantRequest request = completionRequest("dwd", "staging", "ord", false);
|
||||||
|
OceanBaseOracleAgent.CompletionTablesQuery query = OceanBaseOracleAgent.buildCompletionTablesQuery(request, "dwd", 21);
|
||||||
|
|
||||||
|
Assertions.assertTrue(query.sql.contains("UPPER(o.OWNER) = ?"), query.sql);
|
||||||
|
Assertions.assertTrue(query.sql.contains("UPPER(s.OWNER) = ?"), query.sql);
|
||||||
|
Assertions.assertEquals(List.of("ORD%", "STAGING", "ORD%", "STAGING", "DWD", "ORD", 21), query.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completionAssistantSearchReturnsGlobalTableCandidates() {
|
||||||
|
List<String> sql = new ArrayList<>();
|
||||||
|
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||||
|
TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet(
|
||||||
|
new String[]{"OWNER", "OBJECT_NAME", "OBJECT_TYPE", "TARGET_OWNER", "TARGET_NAME"},
|
||||||
|
new Object[][]{
|
||||||
|
{"DWD", "ORDERS", "TABLE", null, null},
|
||||||
|
{"STAGING", "ORDERS", "TABLE", null, null}
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
|
||||||
|
CompletionAssistantResponse response = agent.completionAssistantSearch(completionRequest("DWD", null, "ORD", true));
|
||||||
|
|
||||||
|
Assertions.assertEquals(2, response.getCandidates().size());
|
||||||
|
Assertions.assertEquals("ORDERS", response.getCandidates().get(0).getName());
|
||||||
|
Assertions.assertEquals("DWD", response.getCandidates().get(0).getSchema());
|
||||||
|
Assertions.assertEquals("STAGING", response.getCandidates().get(1).getSchema());
|
||||||
|
Assertions.assertFalse(sql.get(0).contains("UPPER(o.OWNER) = ?"), sql.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void readsViewDdlWithDbmsMetadataForSchemaCompare() {
|
void readsViewDdlWithDbmsMetadataForSchemaCompare() {
|
||||||
List<String> sql = new ArrayList<>();
|
List<String> sql = new ArrayList<>();
|
||||||
@@ -1467,6 +1529,33 @@ class OceanBaseOracleAgentTest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CompletionAssistantRequest completionRequest(
|
||||||
|
String schema,
|
||||||
|
String parentSchema,
|
||||||
|
String mask,
|
||||||
|
boolean globalSearch
|
||||||
|
) {
|
||||||
|
CompletionAssistantRequest request = new CompletionAssistantRequest();
|
||||||
|
setField(request, "database", "OBORCL");
|
||||||
|
setField(request, "schema", schema);
|
||||||
|
setField(request, "parent_schema", parentSchema);
|
||||||
|
setField(request, "mask", mask);
|
||||||
|
setField(request, "global_search", globalSearch);
|
||||||
|
setField(request, "max_results", 20);
|
||||||
|
setField(request, "object_kinds", List.of(CompletionAssistantObjectKind.TABLE, CompletionAssistantObjectKind.VIEW));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String name, Object value) {
|
||||||
|
try {
|
||||||
|
var field = target.getClass().getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static <T> T proxy(Class<T> type, MethodHandler handler) {
|
private static <T> T proxy(Class<T> type, MethodHandler handler) {
|
||||||
InvocationHandler invocationHandler = new InvocationHandler() {
|
InvocationHandler invocationHandler = new InvocationHandler() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentat
|
|||||||
import { supportsDatabaseNameCompletion } from "@/lib/database/databaseFeatureSupport";
|
import { supportsDatabaseNameCompletion } from "@/lib/database/databaseFeatureSupport";
|
||||||
import { sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
import { sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||||
import { oracleDatabaseLinkCompletionContext, oracleDatabaseLinkCompletionItems } from "@/lib/sql/oracleDatabaseLinkCompletion";
|
import { oracleDatabaseLinkCompletionContext, oracleDatabaseLinkCompletionItems } from "@/lib/sql/oracleDatabaseLinkCompletion";
|
||||||
|
import { isOracleCompletionDatabase } from "@/lib/sql/oracleCompletionSession";
|
||||||
import { buildRedisCompletionItemsFromContext, getRedisCompletionContext, getRedisCompletionResultValidFor, shouldAutoOpenRedisCompletion, takesKeyArgument } from "@/lib/redis/redisCompletion";
|
import { buildRedisCompletionItemsFromContext, getRedisCompletionContext, getRedisCompletionResultValidFor, shouldAutoOpenRedisCompletion, takesKeyArgument } from "@/lib/redis/redisCompletion";
|
||||||
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject } from "@/lib/sql/sqlCompletion";
|
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject } from "@/lib/sql/sqlCompletion";
|
||||||
import type { CompletionAssistantObjectKind, SqlServerCompletionContext } from "@/types/database";
|
import type { CompletionAssistantObjectKind, SqlServerCompletionContext } from "@/types/database";
|
||||||
@@ -995,7 +996,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
|||||||
completionContext,
|
completionContext,
|
||||||
knownDatabases: databaseNames,
|
knownDatabases: databaseNames,
|
||||||
});
|
});
|
||||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
const globalOracleTableSearch = isOracleCompletionDatabase(props.databaseType) && completionContext.suggestTables && !completionContext.qualifier;
|
||||||
const tables = schemaLookupDatabase
|
const tables = schemaLookupDatabase
|
||||||
? []
|
? []
|
||||||
: shouldLoadTables
|
: shouldLoadTables
|
||||||
@@ -1157,7 +1158,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
|||||||
knownDatabases: databaseNames,
|
knownDatabases: databaseNames,
|
||||||
});
|
});
|
||||||
if (!localOnlyMetadata && !schemaLookupDatabase && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
if (!localOnlyMetadata && !schemaLookupDatabase && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
||||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
const globalOracleTableSearch = isOracleCompletionDatabase(props.databaseType) && completionContext.suggestTables && !completionContext.qualifier;
|
||||||
const refreshEpoch = completionEpoch;
|
const refreshEpoch = completionEpoch;
|
||||||
queueTableCompletionRefresh(async () => {
|
queueTableCompletionRefresh(async () => {
|
||||||
if (refreshEpoch !== completionEpoch) return;
|
if (refreshEpoch !== completionEpoch) return;
|
||||||
@@ -1270,7 +1271,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
|||||||
if (parts.length === 0) {
|
if (parts.length === 0) {
|
||||||
// Oracle resolves unqualified routines across all schemas (owner semantics).
|
// Oracle resolves unqualified routines across all schemas (owner semantics).
|
||||||
// openGauss keeps the PG search_path scope for the unqualified case.
|
// openGauss keeps the PG search_path scope for the unqualified case.
|
||||||
if (props.databaseType === "oracle") return [{ schema: props.schema, globalSearch: true }];
|
if (isOracleCompletionDatabase(props.databaseType)) return [{ schema: props.schema, globalSearch: true }];
|
||||||
return [{ schema: props.schema }];
|
return [{ schema: props.schema }];
|
||||||
}
|
}
|
||||||
if (parts.length === 1) {
|
if (parts.length === 1) {
|
||||||
@@ -1366,7 +1367,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
|||||||
completionContext,
|
completionContext,
|
||||||
knownDatabases: databaseNames,
|
knownDatabases: databaseNames,
|
||||||
});
|
});
|
||||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
const globalOracleTableSearch = isOracleCompletionDatabase(props.databaseType) && completionContext.suggestTables && !completionContext.qualifier;
|
||||||
let tables = schemaLookupDatabase
|
let tables = schemaLookupDatabase
|
||||||
? []
|
? []
|
||||||
: shouldLoadTables
|
: shouldLoadTables
|
||||||
@@ -1385,7 +1386,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
|||||||
let completionObjects = shouldLoadObjects ? (localOnlyMetadata ? lookupLocalCompletionObjectsForContext(completionContext, scope) : await listCompletionObjectsForContext(completionContext, scope)) : scopedCachedCompletionObjects;
|
let completionObjects = shouldLoadObjects ? (localOnlyMetadata ? lookupLocalCompletionObjectsForContext(completionContext, scope) : await listCompletionObjectsForContext(completionContext, scope)) : scopedCachedCompletionObjects;
|
||||||
if (epoch !== completionEpoch) return null;
|
if (epoch !== completionEpoch) return null;
|
||||||
|
|
||||||
if (!props.catalog && props.databaseType !== "oracle" && !localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
|
if (!props.catalog && !isOracleCompletionDatabase(props.databaseType) && !localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
|
||||||
const target = routineCompletionTargetForContext(completionContext, scope);
|
const target = routineCompletionTargetForContext(completionContext, scope);
|
||||||
const schemaObjects = await connectionStore.listCompletionObjects(props.connectionId!, target.database, target.mask, MAX_COMPLETION_TABLES, target.schema, undefined, false, scope.schema);
|
const schemaObjects = await connectionStore.listCompletionObjects(props.connectionId!, target.database, target.mask, MAX_COMPLETION_TABLES, target.schema, undefined, false, scope.schema);
|
||||||
if (schemaObjects.length > 0) {
|
if (schemaObjects.length > 0) {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { DatabaseType } from "@/types/database";
|
import type { DatabaseType } from "@/types/database";
|
||||||
|
|
||||||
|
/** Native Oracle and OceanBase Oracle share multi-schema completion semantics. */
|
||||||
|
export function isOracleCompletionDatabase(databaseType?: DatabaseType): boolean {
|
||||||
|
return databaseType === "oracle" || databaseType === "oceanbase-oracle";
|
||||||
|
}
|
||||||
|
|
||||||
export function usesOracleCurrentSchemaCompletion(databaseType?: DatabaseType, schema?: string | null): boolean {
|
export function usesOracleCurrentSchemaCompletion(databaseType?: DatabaseType, schema?: string | null): boolean {
|
||||||
return (databaseType === "oracle" || databaseType === "oceanbase-oracle") && !schema;
|
return isOracleCompletionDatabase(databaseType) && !schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usesOracleSessionCompletionColumns(options: { databaseType?: DatabaseType; selectedSchema?: string; referenceSchema?: string | null; clientSessionId?: string }): boolean {
|
export function usesOracleSessionCompletionColumns(options: { databaseType?: DatabaseType; selectedSchema?: string; referenceSchema?: string | null; clientSessionId?: string }): boolean {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { quoteTableIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/
|
|||||||
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions";
|
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions";
|
||||||
import { DORIS_FUNCTION_DOCS, DORIS_FUNCTION_SIGNATURES } from "@/lib/sql/doris/functions";
|
import { DORIS_FUNCTION_DOCS, DORIS_FUNCTION_SIGNATURES } from "@/lib/sql/doris/functions";
|
||||||
import { rejectsAliasReferenceInHaving } from "@/lib/database/databaseFeatureSupport";
|
import { rejectsAliasReferenceInHaving } from "@/lib/database/databaseFeatureSupport";
|
||||||
|
import { isOracleCompletionDatabase } from "@/lib/sql/oracleCompletionSession";
|
||||||
|
|
||||||
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||||
|
|
||||||
@@ -3907,10 +3908,10 @@ function resolveTableSchemaQualification(
|
|||||||
currentSchema: string | undefined,
|
currentSchema: string | undefined,
|
||||||
schemasByTableName: Map<string, Set<string>>,
|
schemasByTableName: Map<string, Set<string>>,
|
||||||
): { ambiguousTableName: boolean; schemaQualification: boolean; defaultApplyName: string } {
|
): { ambiguousTableName: boolean; schemaQualification: boolean; defaultApplyName: string } {
|
||||||
const oracleSchemaQualification = databaseType === "oracle" && table.schema && table.schema.toUpperCase() !== "PUBLIC" && (!currentSchema || normalizeIdentifierPart(table.schema) !== normalizeIdentifierPart(currentSchema));
|
const oracleSchemaQualification = isOracleCompletionDatabase(databaseType) && table.schema && table.schema.toUpperCase() !== "PUBLIC" && (!currentSchema || normalizeIdentifierPart(table.schema) !== normalizeIdentifierPart(currentSchema));
|
||||||
// A bare table name is ambiguous when metadata contains the same name in multiple schemas.
|
// A bare table name is ambiguous when metadata contains the same name in multiple schemas.
|
||||||
// Keep Oracle's current-schema behavior, but qualify the generic/PostgreSQL/SQL Server paths.
|
// Keep Oracle's current-schema behavior, but qualify the generic/PostgreSQL/SQL Server paths.
|
||||||
const ambiguousTableName = databaseType !== "oracle" && (schemasByTableName.get(normalizeIdentifierPart(table.name))?.size ?? 0) > 1;
|
const ambiguousTableName = !isOracleCompletionDatabase(databaseType) && (schemasByTableName.get(normalizeIdentifierPart(table.name))?.size ?? 0) > 1;
|
||||||
const schemaQualification = !!table.schema && (oracleSchemaQualification || ambiguousTableName);
|
const schemaQualification = !!table.schema && (oracleSchemaQualification || ambiguousTableName);
|
||||||
const defaultApplyName = schemaQualification ? `${quoteCompletionApplyIdentifier(table.schema!, dialect)}.${quoteCompletionApplyIdentifier(table.name, dialect)}` : quoteCompletionApplyIdentifier(table.name, dialect);
|
const defaultApplyName = schemaQualification ? `${quoteCompletionApplyIdentifier(table.schema!, dialect)}.${quoteCompletionApplyIdentifier(table.name, dialect)}` : quoteCompletionApplyIdentifier(table.name, dialect);
|
||||||
return { ambiguousTableName, schemaQualification, defaultApplyName };
|
return { ambiguousTableName, schemaQualification, defaultApplyName };
|
||||||
@@ -3948,7 +3949,7 @@ function buildTableItems(
|
|||||||
detail,
|
detail,
|
||||||
apply: formatTableAliasApply(applyName, alias),
|
apply: formatTableAliasApply(applyName, alias),
|
||||||
boost: computeBoost(table.name, prefix) + 1000 + (table.boost ?? 0),
|
boost: computeBoost(table.name, prefix) + 1000 + (table.boost ?? 0),
|
||||||
dedupeKey: table.applyName || ambiguousTableName || (databaseType === "oracle" && table.schema) ? applyName : undefined,
|
dedupeKey: table.applyName || ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort(compareCompletionItems)
|
.sort(compareCompletionItems)
|
||||||
@@ -4004,7 +4005,7 @@ function buildForeignKeyRelatedTableItems(
|
|||||||
boost: computeBoost(table.name, context.prefix) + 3600,
|
boost: computeBoost(table.name, context.prefix) + 3600,
|
||||||
// Mirror buildTableItems' dedupeKey so an FK candidate and the regular
|
// Mirror buildTableItems' dedupeKey so an FK candidate and the regular
|
||||||
// candidate for the same schema-qualified table collapse to one entry.
|
// candidate for the same schema-qualified table collapse to one entry.
|
||||||
dedupeKey: ambiguousTableName || (databaseType === "oracle" && table.schema) ? applyName : undefined,
|
dedupeKey: ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort(compareCompletionItems);
|
.sort(compareCompletionItems);
|
||||||
@@ -4044,7 +4045,7 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
|
|||||||
if (completionQualifierIsReferencedTable(context)) return [];
|
if (completionQualifierIsReferencedTable(context)) return [];
|
||||||
const onlyProcedures = context.contextKind === "exec";
|
const onlyProcedures = context.contextKind === "exec";
|
||||||
const onlyFunctions = context.suggestColumns && context.referencedTables.length > 0 && !context.qualifier;
|
const onlyFunctions = context.suggestColumns && context.referencedTables.length > 0 && !context.qualifier;
|
||||||
const prioritizeOracleFunctions = databaseType === "oracle" && context.statementKind === "select";
|
const prioritizeOracleFunctions = isOracleCompletionDatabase(databaseType) && context.statementKind === "select";
|
||||||
return objects
|
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) => object.type !== "sequence" && (!onlyProcedures || object.type === "procedure") && (!onlyFunctions || (object.type === "function" && object.name.toLowerCase().startsWith(context.prefix.toLowerCase()))) && objectMatchesCompletionContext(object, context))
|
||||||
.map((object) => {
|
.map((object) => {
|
||||||
@@ -4060,7 +4061,7 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
|
|||||||
const detail = [locationDetail, signature ? `(${signature})` : undefined, object.dataType ? `[${object.dataType}]` : undefined].filter(Boolean).join(" ");
|
const detail = [locationDetail, signature ? `(${signature})` : undefined, object.dataType ? `[${object.dataType}]` : undefined].filter(Boolean).join(" ");
|
||||||
const schemaBoost = onlyFunctions ? Math.min(object.boost ?? 0, 1000) : (object.boost ?? 0);
|
const schemaBoost = onlyFunctions ? Math.min(object.boost ?? 0, 1000) : (object.boost ?? 0);
|
||||||
const typeBoost = routineTypeBoost(object.type, prioritizeOracleFunctions && !onlyFunctions);
|
const typeBoost = routineTypeBoost(object.type, prioritizeOracleFunctions && !onlyFunctions);
|
||||||
const baseDedupeKey = object.applyName || (databaseType === "oracle" && object.schema) ? applyName : undefined;
|
const baseDedupeKey = object.applyName || (isOracleCompletionDatabase(databaseType) && object.schema) ? applyName : undefined;
|
||||||
return {
|
return {
|
||||||
label: object.name,
|
label: object.name,
|
||||||
type: "function" as const,
|
type: "function" as const,
|
||||||
@@ -5470,7 +5471,7 @@ function activeSqlKeywords(databaseType?: DatabaseType): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isOracleLikeDatabase(databaseType?: DatabaseType): boolean {
|
function isOracleLikeDatabase(databaseType?: DatabaseType): boolean {
|
||||||
return databaseType === "oracle" || databaseType === "oceanbase-oracle";
|
return isOracleCompletionDatabase(databaseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CQL has no table alias syntax, so Cassandra must never receive `table alias`
|
// CQL has no table alias syntax, so Cassandra must never receive `table alias`
|
||||||
|
|||||||
@@ -678,6 +678,59 @@ describe("connectionStore completion assistant", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps global OceanBase Oracle tables with safe qualification and schema priority", async () => {
|
||||||
|
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||||
|
candidates: [
|
||||||
|
{ name: "ORDERS", kind: "table", schema: "DWD", data_type: "TABLE" },
|
||||||
|
{ name: "ORDERS", kind: "table", schema: "STAGING", data_type: "TABLE" },
|
||||||
|
],
|
||||||
|
incomplete: false,
|
||||||
|
fallback_used: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({
|
||||||
|
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||||
|
completionAssistantSearch,
|
||||||
|
listTables: vi.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [oceanBaseOracleConnection()];
|
||||||
|
store.connectedIds.add("oceanbase-oracle-1");
|
||||||
|
|
||||||
|
const tables = await store.listCompletionTables("oceanbase-oracle-1", "OBORCL", "ORD", 20, "DWD", true, "DWD");
|
||||||
|
|
||||||
|
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "DWD", parent_schema: null, global_search: true, mask: "ORD" }));
|
||||||
|
expect(tables).toEqual([expect.objectContaining({ name: "ORDERS", schema: "DWD", applyName: "ORDERS", boost: 2400 }), expect.objectContaining({ name: "ORDERS", schema: "STAGING", applyName: "STAGING.ORDERS", boost: 0 })]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes OceanBase Oracle table completion when a schema qualifier is present", async () => {
|
||||||
|
const completionAssistantSearch = vi.fn(async (request: { schema?: string | null; parent_schema?: string | null }) => ({
|
||||||
|
candidates: request.parent_schema?.toLowerCase() === "staging" ? [{ name: "ORDERS", kind: "table", schema: "STAGING", data_type: "TABLE" }] : [],
|
||||||
|
incomplete: false,
|
||||||
|
fallback_used: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({
|
||||||
|
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||||
|
completionAssistantSearch,
|
||||||
|
listTables: vi.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [oceanBaseOracleConnection()];
|
||||||
|
store.connectedIds.add("oceanbase-oracle-1");
|
||||||
|
|
||||||
|
const tables = await store.listCompletionTables("oceanbase-oracle-1", "OBORCL", "", 20, "staging", false, "DWD");
|
||||||
|
|
||||||
|
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "staging", parent_schema: "staging", global_search: false, mask: "" }));
|
||||||
|
expect(tables).toEqual([expect.objectContaining({ name: "ORDERS", schema: "STAGING", applyName: "ORDERS", boost: 2400 })]);
|
||||||
|
});
|
||||||
|
|
||||||
it("lets Oracle resolve CURRENT_SCHEMA for unqualified column completion", async () => {
|
it("lets Oracle resolve CURRENT_SCHEMA for unqualified column completion", async () => {
|
||||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||||
candidates: [],
|
candidates: [],
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ import {
|
|||||||
type ConnectionExportProtection,
|
type ConnectionExportProtection,
|
||||||
} from "@/lib/connection/connectionConfigTransfer";
|
} from "@/lib/connection/connectionConfigTransfer";
|
||||||
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||||
import { usesOracleCurrentSchemaCompletion } from "@/lib/sql/oracleCompletionSession";
|
import { usesOracleCurrentSchemaCompletion, isOracleCompletionDatabase } from "@/lib/sql/oracleCompletionSession";
|
||||||
import { mergeSqlObjectNavigationType, sqlObjectNavigationTypeFromTableType } from "@/lib/sql/sqlNavigation";
|
import { mergeSqlObjectNavigationType, sqlObjectNavigationTypeFromTableType } from "@/lib/sql/sqlNavigation";
|
||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import { oracleDatabaseLinksFromResult, oracleDatabaseLinksSql, supportsOracleDatabaseLinks } from "@/lib/database/oracleDatabaseLinks";
|
import { oracleDatabaseLinksFromResult, oracleDatabaseLinksSql, supportsOracleDatabaseLinks } from "@/lib/database/oracleDatabaseLinks";
|
||||||
@@ -7997,7 +7997,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
requestRevision = completionCacheRevision(connectionId, database),
|
requestRevision = completionCacheRevision(connectionId, database),
|
||||||
matchMode: CompletionAssistantMatchMode = "prefix",
|
matchMode: CompletionAssistantMatchMode = "prefix",
|
||||||
): Promise<SqlCompletionTable[]> {
|
): Promise<SqlCompletionTable[]> {
|
||||||
const oracleAssistant = getConfig(connectionId)?.db_type === "oracle";
|
const oracleAssistant = isOracleCompletionDatabase(getConfig(connectionId)?.db_type);
|
||||||
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, globalSearch ? currentSchema : (schema ?? currentSchema)) : schema?.trim() || undefined;
|
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, globalSearch ? currentSchema : (schema ?? currentSchema)) : schema?.trim() || undefined;
|
||||||
const objectKinds: CompletionAssistantObjectKind[] = ["table", "view"];
|
const objectKinds: CompletionAssistantObjectKind[] = ["table", "view"];
|
||||||
const response = await completionAssistantSearch(
|
const response = await completionAssistantSearch(
|
||||||
@@ -8033,7 +8033,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
matchMode: CompletionAssistantMatchMode = "prefix",
|
matchMode: CompletionAssistantMatchMode = "prefix",
|
||||||
): Promise<SqlCompletionObject[]> {
|
): Promise<SqlCompletionObject[]> {
|
||||||
const databaseType = getConfig(connectionId)?.db_type;
|
const databaseType = getConfig(connectionId)?.db_type;
|
||||||
const oracleAssistant = databaseType === "oracle";
|
const oracleAssistant = isOracleCompletionDatabase(databaseType);
|
||||||
const requestedSchema = schema?.trim() || currentSchema?.trim() || undefined;
|
const requestedSchema = schema?.trim() || currentSchema?.trim() || undefined;
|
||||||
const sequenceOnly = objectKinds.length === 1 && objectKinds[0] === "sequence";
|
const sequenceOnly = objectKinds.length === 1 && objectKinds[0] === "sequence";
|
||||||
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (!sequenceOnly && databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
|
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (!sequenceOnly && databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
|
||||||
|
|||||||
+37
@@ -2821,6 +2821,43 @@ test("prioritizes current Oracle schema tables and safely qualifies other schema
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prioritizes current OceanBase Oracle schema tables and safely qualifies other schemas", () => {
|
||||||
|
const sql = "select * from orders";
|
||||||
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||||
|
tables: [
|
||||||
|
{ name: "ORDERS", schema: "STAGING", type: "table", applyName: "STAGING.ORDERS", boost: 0 },
|
||||||
|
{ name: "ORDERS", schema: "DWD", type: "table", applyName: "ORDERS", boost: 2400 },
|
||||||
|
],
|
||||||
|
columnsByTable,
|
||||||
|
databaseType: "oceanbase-oracle",
|
||||||
|
currentSchema: "DWD",
|
||||||
|
});
|
||||||
|
const matches = items.filter((item) => item.label === "ORDERS");
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
matches.map((item) => item.apply),
|
||||||
|
["ORDERS", "STAGING.ORDERS"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("qualifies OceanBase Oracle tables outside the current schema without applyName", () => {
|
||||||
|
const sql = "select * from orders";
|
||||||
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||||
|
tables: [
|
||||||
|
{ name: "ORDERS", schema: "STAGING", type: "table", boost: 0 },
|
||||||
|
{ name: "ORDERS", schema: "DWD", type: "table", boost: 2400 },
|
||||||
|
],
|
||||||
|
columnsByTable,
|
||||||
|
databaseType: "oceanbase-oracle",
|
||||||
|
currentSchema: "DWD",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
items.filter((item) => item.label === "ORDERS").map((item) => item.apply),
|
||||||
|
["ORDERS", "STAGING.ORDERS"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("does not duplicate an Oracle schema qualifier when applying a scoped table", () => {
|
test("does not duplicate an Oracle schema qualifier when applying a scoped table", () => {
|
||||||
const sql = "select * from COMM.DEPT_D";
|
const sql = "select * from COMM.DEPT_D";
|
||||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||||
|
|||||||
Reference in new issue
Block a user