fix(sql): support Oracle system value completion
This commit is contained in:
parent
f4ce0dc435
commit
79f8c7284a
|
|
@ -1,5 +1,5 @@
|
|||
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion";
|
||||
import { getSqlCompletionContext, isOracleSystemValueName } from "@/lib/sql/sqlCompletion";
|
||||
import { executableStatementRanges, isOraclePlSqlStatement, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import type { DatabaseType, SqlColumnReference, SqlReferenceAnalysis, SqlReferenceScope, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
|
||||
|
|
@ -63,6 +63,7 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
|
|||
}
|
||||
|
||||
for (const column of analysis.columns) {
|
||||
if (isUnquotedOracleSystemValueReference(column, schema)) continue;
|
||||
const table = resolveColumnTable(column, tables, knownTables, schema.sql, scopesById);
|
||||
if (!table) continue;
|
||||
if (schema.missingTables?.has(tableReferenceKey(table))) continue;
|
||||
|
|
@ -84,6 +85,16 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
|
|||
return diagnostics;
|
||||
}
|
||||
|
||||
function isUnquotedOracleSystemValueReference(column: SqlColumnReference, schema: SqlSemanticDiagnosticSchema): boolean {
|
||||
if (column.qualifier || !isOracleSystemValueName(column.name, schema.databaseType)) return false;
|
||||
if (!schema.sql) return false;
|
||||
|
||||
const range = sqlTextSpanToOffsetRange(schema.sql, column.span);
|
||||
if (!range) return false;
|
||||
const firstCharacter = schema.sql.slice(range.from, range.to).trimStart()[0];
|
||||
return firstCharacter !== '"' && firstCharacter !== "'" && firstCharacter !== "`" && firstCharacter !== "[";
|
||||
}
|
||||
|
||||
export function isSqlVirtualTableReference(table: { name: string; schema?: string | null }, databaseType?: DatabaseType): boolean {
|
||||
return databaseType === "mysql" && !table.schema && normalizeName(table.name) === "dual";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,6 +509,14 @@ const ORACLE_SQL_TYPES = [
|
|||
"XMLTYPE",
|
||||
];
|
||||
|
||||
const ORACLE_SYSTEM_VALUE_NAMES = ["SYSDATE", "SYSTIMESTAMP", "CURRENT_DATE", "CURRENT_TIMESTAMP", "LOCALTIMESTAMP", "SESSIONTIMEZONE", "DBTIMEZONE", "USER", "UID"] as const;
|
||||
|
||||
const ORACLE_SYSTEM_VALUE_NAME_SET = new Set<string>(ORACLE_SYSTEM_VALUE_NAMES);
|
||||
|
||||
export function isOracleSystemValueName(name: string, databaseType?: DatabaseType): boolean {
|
||||
return isOracleLikeDatabase(databaseType) && ORACLE_SYSTEM_VALUE_NAME_SET.has(name.toUpperCase());
|
||||
}
|
||||
|
||||
const NON_ORACLE_COMPLETION_WORDS = new Set(["BIGSERIAL", "BOOLEAN", "ELSEIF", "LIMIT", "LOCALTIME", "SERIAL", "STRING", "TEXT", "TIME", "USE"]);
|
||||
|
||||
const ORACLE_SQL_KEYWORDS = Array.from(
|
||||
|
|
@ -1350,6 +1358,9 @@ class SqlCompletionProvider {
|
|||
if (!preferReferencedColumns || context.suggestRoutines) {
|
||||
const functionItems = buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType);
|
||||
this.items.push(...(preferReferencedColumns ? functionItems.filter((item) => item.label.toLowerCase().startsWith(context.prefix.toLowerCase())) : functionItems));
|
||||
if (isOracleLikeDatabase(this.databaseType)) {
|
||||
this.items.push(...buildOracleSystemValueItems(context.prefix, this.input.keywordCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4025,6 +4036,19 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<str
|
|||
return items;
|
||||
}
|
||||
|
||||
function buildOracleSystemValueItems(prefix: string, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
|
||||
return ORACLE_SYSTEM_VALUE_NAMES.filter((name) => matchesPrefix(name, prefix)).map((name) => {
|
||||
const label = applySqlKeywordCase(name, keywordCase);
|
||||
return {
|
||||
label,
|
||||
type: "function" as const,
|
||||
detail: "Oracle system value",
|
||||
apply: label,
|
||||
boost: computeBoost(name, prefix) + 300,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mongoCompletionItemToSqlCompletionItem(item: MongoCompletionItem): SqlCompletionItem {
|
||||
return {
|
||||
label: item.label,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { buildSqlCompletionItems } from "../../apps/desktop/src/lib/sql/sqlCompletion.ts";
|
||||
import { buildSqlSemanticDiagnostics } from "../../apps/desktop/src/lib/sql/semantic/diagnostics.ts";
|
||||
import type { SqlReferenceAnalysis } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
const ORACLE_SYSTEM_VALUES = ["SYSDATE", "SYSTIMESTAMP", "CURRENT_DATE", "CURRENT_TIMESTAMP", "LOCALTIMESTAMP", "SESSIONTIMEZONE", "DBTIMEZONE", "USER", "UID"];
|
||||
|
||||
const span = (startColumn: number, endColumn: number) => ({
|
||||
start_line: 1,
|
||||
start_column: startColumn,
|
||||
end_line: 1,
|
||||
end_column: endColumn,
|
||||
});
|
||||
|
||||
test("suggests Oracle system values without function-call parentheses", () => {
|
||||
for (const name of ORACLE_SYSTEM_VALUES) {
|
||||
const prefix = name.slice(0, Math.min(5, name.length));
|
||||
const sql = `SELECT * FROM orders WHERE created_at > ${prefix}`;
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables: [{ name: "orders", type: "table" }],
|
||||
columnsByTable: new Map([["orders", [{ name: "created_at", table: "orders" }]]]),
|
||||
databaseType: "oracle",
|
||||
});
|
||||
|
||||
const systemValue = items.find((item) => item.label === name);
|
||||
assert.equal(systemValue?.type, "function", name);
|
||||
assert.equal(systemValue?.apply, name, name);
|
||||
assert.equal(systemValue?.detail, "Oracle system value", name);
|
||||
}
|
||||
});
|
||||
|
||||
test("does not flag unquoted Oracle system values as table columns", () => {
|
||||
for (const name of ORACLE_SYSTEM_VALUES) {
|
||||
const sql = `SELECT id FROM orders WHERE ${name} IS NOT NULL`;
|
||||
const startColumn = sql.indexOf(name) + 1;
|
||||
const diagnostics = buildSqlSemanticDiagnostics(
|
||||
{
|
||||
tables: [{ name: "orders", span: span(16, 21), scope_id: 0 }],
|
||||
columns: [
|
||||
{ name: "id", span: span(8, 9), scope_id: 0 },
|
||||
{ name, span: span(startColumn, startColumn + name.length - 1), scope_id: 0 },
|
||||
],
|
||||
scopes: [{ id: 0, parent_id: null }],
|
||||
},
|
||||
{
|
||||
tables: [{ name: "orders", type: "table" }],
|
||||
columnsByTable: new Map([["orders", [{ name: "id", table: "orders" }]]]),
|
||||
databaseType: "oracle",
|
||||
sql,
|
||||
},
|
||||
);
|
||||
assert.deepEqual(diagnostics, [], name);
|
||||
}
|
||||
});
|
||||
|
||||
test("continues validating qualified and quoted Oracle system-value names as columns", () => {
|
||||
const sql = 'SELECT o.id FROM orders o WHERE missing > 0 AND o.SYSDATE > 0 AND "SYSDATE" > 0';
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [{ name: "orders", alias: "o", span: span(18, 23), scope_id: 0 }],
|
||||
columns: [
|
||||
{ name: "id", qualifier: "o", span: span(10, 11), scope_id: 0 },
|
||||
{ name: "missing", span: span(33, 39), scope_id: 0 },
|
||||
{ name: "SYSDATE", qualifier: "o", span: span(51, 57), scope_id: 0 },
|
||||
{ name: "SYSDATE", span: span(67, 75), scope_id: 0 },
|
||||
],
|
||||
scopes: [{ id: 0, parent_id: null }],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
|
||||
tables: [{ name: "orders", type: "table" }],
|
||||
columnsByTable: new Map([["orders", [{ name: "id", table: "orders" }]]]),
|
||||
databaseType: "oracle",
|
||||
sql,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
diagnostics.map((diagnostic) => diagnostic.message),
|
||||
["Unknown column missing", "Unknown column o.SYSDATE", "Unknown column SYSDATE"],
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue