fix(sql): allow quoted keyword table navigation
This commit is contained in:
3 files changed
+49
-14
No files matched your search
@@ -37,7 +37,7 @@ import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget }
|
||||
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearch/elasticsearchCompletion";
|
||||
import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
|
||||
import { resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { extractIdentifierAt, isSqlKeyword, matchTable, splitQualifiedIdentifier } from "@/lib/sql/sqlNavigation";
|
||||
import { extractIdentifierDetailsAt, isSqlKeyword, matchTable, splitQualifiedIdentifier } from "@/lib/sql/sqlNavigation";
|
||||
import { lineColumnToOffset, parseSqlErrorLocation } from "@/lib/sql/sqlDiagnostics";
|
||||
import {
|
||||
DBX_TABLE_REFERENCE_MIME,
|
||||
@@ -505,9 +505,9 @@ function tableNavigationIdentifierAt(currentView: EditorViewType, event: MouseEv
|
||||
if (!props.connectionId || props.database == null) return null;
|
||||
const pos = currentView.posAtCoords({ x: event.clientX, y: event.clientY });
|
||||
if (pos == null) return null;
|
||||
const identifier = extractIdentifierAt(currentView.state.doc.toString(), pos);
|
||||
if (!identifier || isSqlKeyword(identifier)) return null;
|
||||
return identifier;
|
||||
const extracted = extractIdentifierDetailsAt(currentView.state.doc.toString(), pos);
|
||||
if (!extracted || (!extracted.quoted && isSqlKeyword(extracted.identifier))) return null;
|
||||
return extracted.identifier;
|
||||
}
|
||||
|
||||
function updateTableNavigationHover(currentView: EditorViewType, event: MouseEvent) {
|
||||
@@ -2889,13 +2889,14 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
const doc = currentView.state.doc.toString();
|
||||
const identifier = extractIdentifierAt(doc, pos);
|
||||
if (!identifier) {
|
||||
const extracted = extractIdentifierDetailsAt(doc, pos);
|
||||
if (!extracted) {
|
||||
return false;
|
||||
}
|
||||
if (isSqlKeyword(identifier)) {
|
||||
if (!extracted.quoted && isSqlKeyword(extracted.identifier)) {
|
||||
return false;
|
||||
}
|
||||
const identifier = extracted.identifier;
|
||||
|
||||
// Prevent default, resolve async
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractIdentifierAt, matchTable, splitQualifiedIdentifier } from "@/lib/sql/sqlNavigation";
|
||||
import { extractIdentifierAt, extractIdentifierDetailsAt, isSqlKeyword, matchTable, splitQualifiedIdentifier } from "@/lib/sql/sqlNavigation";
|
||||
|
||||
describe("extractIdentifierAt", () => {
|
||||
it("extracts unquoted qualified identifiers", () => {
|
||||
@@ -15,6 +15,27 @@ describe("extractIdentifierAt", () => {
|
||||
expect(extractIdentifierAt(sql, sql.indexOf("MAAC00"))).toBe("MAAC00.Accounts");
|
||||
});
|
||||
|
||||
it("preserves quote metadata for quoted keyword identifiers", () => {
|
||||
const sql = "SELECT * FROM `group` LIMIT 100;";
|
||||
|
||||
expect(extractIdentifierDetailsAt(sql, sql.indexOf("group"))).toEqual({
|
||||
identifier: "group",
|
||||
quoted: true,
|
||||
});
|
||||
expect(matchTable(extractIdentifierAt(sql, sql.indexOf("group")) ?? "", [{ name: "group" }])).toEqual({ name: "group" });
|
||||
});
|
||||
|
||||
it("marks unquoted keyword identifiers as unquoted", () => {
|
||||
const sql = "SELECT dept, COUNT(*) FROM users GROUP BY dept;";
|
||||
const extracted = extractIdentifierDetailsAt(sql, sql.indexOf("GROUP"));
|
||||
|
||||
expect(extracted).toEqual({
|
||||
identifier: "GROUP",
|
||||
quoted: false,
|
||||
});
|
||||
expect(extracted && isSqlKeyword(extracted.identifier)).toBe(true);
|
||||
});
|
||||
|
||||
it("extracts double-quoted qualified identifiers", () => {
|
||||
const sql = 'select * from "MAAC00"."Accounts"';
|
||||
|
||||
|
||||
@@ -121,7 +121,12 @@ const SQL_KEYWORDS_SET = new Set([
|
||||
"list_transform",
|
||||
]);
|
||||
|
||||
type IdentifierPart = { value: string; start: number; end: number };
|
||||
type IdentifierPart = { value: string; start: number; end: number; quoted: boolean };
|
||||
|
||||
export interface ExtractedSqlIdentifier {
|
||||
identifier: string;
|
||||
quoted: boolean;
|
||||
}
|
||||
|
||||
function isIdentifierChar(char: string | undefined): boolean {
|
||||
return !!char && /^[A-Za-z0-9_$]$/.test(char);
|
||||
@@ -141,7 +146,7 @@ function readQuotedPart(text: string, start: number): IdentifierPart | null {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
return { value, start, end: i + 1 };
|
||||
return { value, start, end: i + 1, quoted: true };
|
||||
}
|
||||
value += char;
|
||||
}
|
||||
@@ -152,7 +157,7 @@ function readUnquotedPart(text: string, start: number): IdentifierPart | null {
|
||||
if (!isIdentifierChar(text[start])) return null;
|
||||
let end = start + 1;
|
||||
while (end < text.length && isIdentifierChar(text[end])) end += 1;
|
||||
return { value: text.slice(start, end), start, end };
|
||||
return { value: text.slice(start, end), start, end, quoted: false };
|
||||
}
|
||||
|
||||
function readIdentifierPart(text: string, start: number): IdentifierPart | null {
|
||||
@@ -185,8 +190,8 @@ function identifierSearchBounds(doc: string, pos: number): { start: number; end:
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/** Extract identifier at position `pos` in the document. */
|
||||
export function extractIdentifierAt(doc: string, pos: number): string | null {
|
||||
/** Extract identifier and quote metadata at position `pos` in the document. */
|
||||
export function extractIdentifierDetailsAt(doc: string, pos: number): ExtractedSqlIdentifier | null {
|
||||
if (pos < 0 || pos > doc.length) return null;
|
||||
|
||||
const clickPos = pos === doc.length ? pos - 1 : pos;
|
||||
@@ -198,7 +203,10 @@ export function extractIdentifierAt(doc: string, pos: number): string | null {
|
||||
const parsed = parseQualifiedIdentifier(doc, index);
|
||||
if (parsed) {
|
||||
if (clickPos >= parsed.start && clickPos < parsed.end) {
|
||||
return parsed.parts.map((part) => part.value).join(".");
|
||||
return {
|
||||
identifier: parsed.parts.map((part) => part.value).join("."),
|
||||
quoted: parsed.parts.some((part) => part.quoted),
|
||||
};
|
||||
}
|
||||
index = Math.max(parsed.end, index + 1);
|
||||
continue;
|
||||
@@ -209,6 +217,11 @@ export function extractIdentifierAt(doc: string, pos: number): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Extract identifier at position `pos` in the document. */
|
||||
export function extractIdentifierAt(doc: string, pos: number): string | null {
|
||||
return extractIdentifierDetailsAt(doc, pos)?.identifier ?? null;
|
||||
}
|
||||
|
||||
/** Check whether the identifier is a SQL keyword (not a table/column name). */
|
||||
export function isSqlKeyword(identifier: string): boolean {
|
||||
return SQL_KEYWORDS_SET.has(identifier.toLowerCase());
|
||||
|
||||
Reference in new issue
Block a user