mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-26 08:02:06 +00:00
185 lines
5.4 KiB
TypeScript
185 lines
5.4 KiB
TypeScript
import { isSchemaAware, isSingleDatabase } from "@/lib/databaseFeatureSupport";
|
|
import { isSqlKeyword } from "@/lib/sqlNavigation";
|
|
import type { ActiveTabSidebarTarget } from "@/lib/sidebarActiveTabTarget";
|
|
import type { DatabaseType, QueryTab, TreeNode } from "@/types/database";
|
|
|
|
export interface QueryCursorTableCandidate {
|
|
connectionId: string;
|
|
database: string;
|
|
schema?: string;
|
|
tableName: string;
|
|
}
|
|
|
|
export interface IdentifierPart {
|
|
value: string;
|
|
quoted: boolean;
|
|
}
|
|
|
|
function identifierChar(ch: string | undefined): boolean {
|
|
return !!ch && (/[A-Za-z0-9_$."`-]/.test(ch) || ch === "[" || ch === "]");
|
|
}
|
|
|
|
function readIdentifierWindow(sql: string, pos: number): string {
|
|
const clamped = Math.max(0, Math.min(pos, sql.length));
|
|
let from = clamped;
|
|
let to = clamped;
|
|
|
|
if (!identifierChar(sql[from]) && identifierChar(sql[from - 1])) {
|
|
from -= 1;
|
|
to = from + 1;
|
|
}
|
|
|
|
if (!identifierChar(sql[from])) return "";
|
|
|
|
while (from > 0 && identifierChar(sql[from - 1])) from -= 1;
|
|
while (to < sql.length && identifierChar(sql[to])) to += 1;
|
|
return sql.slice(from, to).replace(/^\.+|\.+$/g, "");
|
|
}
|
|
|
|
function splitQualifiedIdentifier(text: string): IdentifierPart[] {
|
|
const parts: IdentifierPart[] = [];
|
|
let current = "";
|
|
let quoted = false;
|
|
let quote: "`" | '"' | "]" | null = null;
|
|
|
|
function pushPart() {
|
|
const value = current.trim();
|
|
if (value) parts.push({ value, quoted });
|
|
current = "";
|
|
quoted = false;
|
|
}
|
|
|
|
for (let i = 0; i < text.length; i += 1) {
|
|
const ch = text[i];
|
|
|
|
if (quote) {
|
|
if (quote === "]" && ch === "]") {
|
|
if (text[i + 1] === "]") {
|
|
current += "]";
|
|
i += 1;
|
|
} else {
|
|
quote = null;
|
|
}
|
|
continue;
|
|
}
|
|
if ((quote === "`" || quote === '"') && ch === quote) {
|
|
if (text[i + 1] === quote) {
|
|
current += ch;
|
|
i += 1;
|
|
} else {
|
|
quote = null;
|
|
}
|
|
continue;
|
|
}
|
|
current += ch;
|
|
continue;
|
|
}
|
|
|
|
if (ch === ".") {
|
|
pushPart();
|
|
continue;
|
|
}
|
|
|
|
if (ch === "`" || ch === '"') {
|
|
quoted = true;
|
|
quote = ch;
|
|
continue;
|
|
}
|
|
|
|
if (ch === "[") {
|
|
quoted = true;
|
|
quote = "]";
|
|
continue;
|
|
}
|
|
|
|
current += ch;
|
|
}
|
|
|
|
pushPart();
|
|
return parts;
|
|
}
|
|
|
|
export function extractQualifiedIdentifierPartsAt(sql: string, pos: number): IdentifierPart[] {
|
|
const text = readIdentifierWindow(sql, pos);
|
|
if (!text) return [];
|
|
const parts = splitQualifiedIdentifier(text);
|
|
const last = parts[parts.length - 1];
|
|
if (!last || (!last.quoted && isSqlKeyword(last.value))) return [];
|
|
return parts;
|
|
}
|
|
|
|
export function queryCursorTableCandidate(tab: QueryTab | undefined | null, databaseType?: DatabaseType): QueryCursorTableCandidate | null {
|
|
if (!tab || tab.mode !== "query" || !tab.connectionId || !tab.database) return null;
|
|
|
|
const cursor = tab.editorSelection?.head ?? tab.editorSelection?.anchor ?? tab.sql.length;
|
|
const parts = extractQualifiedIdentifierPartsAt(tab.sql, cursor).map((part) => part.value);
|
|
if (parts.length === 0) return null;
|
|
|
|
const tableName = parts[parts.length - 1];
|
|
let database = tab.database;
|
|
let schema = tab.schema;
|
|
|
|
if (parts.length >= 3) {
|
|
database = parts[parts.length - 3];
|
|
schema = parts[parts.length - 2];
|
|
} else if (parts.length === 2) {
|
|
if (databaseType && !isSchemaAware(databaseType) && !isSingleDatabase(databaseType)) {
|
|
database = parts[0];
|
|
schema = undefined;
|
|
} else {
|
|
schema = parts[0];
|
|
}
|
|
}
|
|
|
|
return { connectionId: tab.connectionId, database, schema, tableName };
|
|
}
|
|
|
|
export function qualifiedTableNameAtSqlPosition(sql: string, pos: number): string | null {
|
|
const parts = extractQualifiedIdentifierPartsAt(sql, pos).map((part) => part.value);
|
|
if (parts.length === 0) return null;
|
|
return parts.join(".");
|
|
}
|
|
|
|
export function queryContextTargetFromCandidate(tab: QueryTab | undefined | null, candidate?: QueryCursorTableCandidate | null): ActiveTabSidebarTarget | null {
|
|
if (!tab || tab.mode !== "query" || !tab.connectionId || !tab.database) return null;
|
|
return {
|
|
type: "query-context",
|
|
connectionId: tab.connectionId,
|
|
database: candidate?.database || tab.database,
|
|
schema: candidate?.schema ?? tab.schema,
|
|
};
|
|
}
|
|
|
|
function sameIdentifier(left: string | undefined, right: string | undefined): boolean {
|
|
return (left || "").toLowerCase() === (right || "").toLowerCase();
|
|
}
|
|
|
|
function nodeMatchesCandidate(node: TreeNode, candidate: QueryCursorTableCandidate): boolean {
|
|
if (node.type !== "table" && node.type !== "view") return false;
|
|
if (node.connectionId !== candidate.connectionId) return false;
|
|
if (!sameIdentifier(node.database, candidate.database)) return false;
|
|
if (candidate.schema && !sameIdentifier(node.schema, candidate.schema)) return false;
|
|
return sameIdentifier(node.label, candidate.tableName);
|
|
}
|
|
|
|
export function findLoadedTableTargetForCandidate(nodes: readonly TreeNode[], candidate: QueryCursorTableCandidate): ActiveTabSidebarTarget | null {
|
|
for (const node of nodes) {
|
|
if (nodeMatchesCandidate(node, candidate)) {
|
|
return {
|
|
type: "table",
|
|
connectionId: candidate.connectionId,
|
|
database: node.database || candidate.database,
|
|
schema: node.schema || candidate.schema,
|
|
tableName: node.label,
|
|
};
|
|
}
|
|
|
|
if (node.children) {
|
|
const found = findLoadedTableTargetForCandidate(node.children, candidate);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|