refactor(desktop): organize lib modules by domain

This commit is contained in:
t8y2 committed 2026-07-04 13:53:52 +08:00
1 parent 90f0594e93
commit 7e7c4944e9
700 files changed
+1292 -1264

No files matched your search

@@ -0,0 +1,13 @@
# SQL Semantic
This directory owns the pure frontend SQL semantic layer used by editor completion, diagnostics, and navigation.
- `tokens.ts`: span-preserving SQL tokenization and active statement detection.
- `dialect.ts`: dialect adapters for identifier normalization, quoting, qualifier roles, and projection alias visibility.
- `model.ts`: active-statement semantic model, row-source parsing, cursor intent, and confidence/fallback output.
- `completion.ts`: compatibility adapter from semantic intent to the existing SQL completion item builder.
- `references.ts`: shared row-source references for completion, diagnostics, and navigation.
- `diagnostics.ts`: SQL semantic diagnostic helpers and viewport range selection.
- `fixtures.ts` and `types.ts`: test fixtures and shared semantic types.
The older `sqlCompletion.ts` and `sqlNavigation.ts` files remain at `lib/` for now because they are broad public editor utilities. Move them only with a separate behavior-preserving migration.
@@ -0,0 +1,208 @@
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionReferencedTable } from "@/lib/sql/sqlCompletion";
import { SQL_SEMANTIC_DIALECTS } from "@/lib/sql/semantic/dialect";
import type { SqlSemanticModel, SqlSemanticRowSource } from "@/lib/sql/semantic/types";
export type SqlSemanticCompletionScopeKind = "keyword" | "table" | "schema" | "catalog" | "routine" | "columns" | "local";
export interface SqlSemanticCompletionScope {
kind: SqlSemanticCompletionScopeKind;
prefix: string;
qualifierParts: string[];
targetSource?: SqlSemanticRowSource;
useRemoteMetadata: boolean;
fallbackReason?: string;
}
export function sqlSemanticReferencedTables(model: SqlSemanticModel): SqlCompletionReferencedTable[] {
return model.rowSources
.filter((source) => source.kind !== "unknown")
.map((source) => ({
name: source.name,
schema: source.qualifierParts[source.qualifierParts.length - 1],
alias: source.alias,
columns: source.columns,
}));
}
export function sqlSemanticLocalColumnsByTable(model: SqlSemanticModel): Map<string, SqlCompletionColumn[]> {
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
for (const source of model.rowSources) {
if (!source.columns?.length) continue;
columnsByTable.set(
source.name,
source.columns.map((name) => ({
name,
table: source.name,
schema: source.qualifierParts[source.qualifierParts.length - 1],
})),
);
}
return columnsByTable;
}
function activeProjectionAliasClause(model: SqlSemanticModel): "where" | "groupBy" | "having" | "orderBy" | null {
const words = model.tokens.filter((token) => token.span.end <= model.cursor && token.kind === "word").map((token) => token.normalized);
for (let index = words.length - 1; index >= 0; index -= 1) {
const word = words[index];
const previous = words[index - 1];
if (word === "by" && previous === "order") return "orderBy";
if (word === "by" && previous === "group") return "groupBy";
if (word === "having") return "having";
if (word === "where") return "where";
if (word === "from" || word === "join" || word === "select") return null;
}
return null;
}
export function sqlSemanticProjectionAliasColumns(model: SqlSemanticModel): SqlCompletionColumn[] {
const clause = activeProjectionAliasClause(model);
if (!clause) return [];
const adapter = SQL_SEMANTIC_DIALECTS[model.dialectId] ?? SQL_SEMANTIC_DIALECTS.generic;
if (!adapter.projectionAliasVisibility[clause]) return [];
return model.projections
.filter((projection) => projection.name)
.map((projection) => ({
name: projection.name,
table: "__projection__",
comment: "Projection alias",
}));
}
export function sqlSemanticCompletionScope(model: SqlSemanticModel): SqlSemanticCompletionScope {
const intent = model.cursorIntent;
const targetSource = intent.targetSourceId ? model.rowSources.find((source) => source.id === intent.targetSourceId) : undefined;
switch (intent.kind) {
case "table":
case "delete_target":
return {
kind: "table",
prefix: intent.prefix,
qualifierParts: intent.qualifierParts,
useRemoteMetadata: intent.confidence !== "low",
fallbackReason: intent.fallbackReason,
};
case "schema":
return {
kind: "schema",
prefix: intent.prefix,
qualifierParts: intent.qualifierParts,
useRemoteMetadata: intent.confidence !== "low",
fallbackReason: intent.fallbackReason,
};
case "catalog":
return {
kind: "catalog",
prefix: intent.prefix,
qualifierParts: intent.qualifierParts,
useRemoteMetadata: intent.confidence !== "low",
fallbackReason: intent.fallbackReason,
};
case "routine":
return {
kind: "routine",
prefix: intent.prefix,
qualifierParts: intent.qualifierParts,
useRemoteMetadata: intent.confidence !== "low",
fallbackReason: intent.fallbackReason,
};
case "column":
case "alias_column":
case "insert_column":
case "update_column":
case "join_condition":
case "star":
return {
kind: "columns",
prefix: intent.prefix,
qualifierParts: intent.qualifierParts,
targetSource,
useRemoteMetadata: intent.confidence !== "low" && (!!targetSource || model.rowSources.length > 0),
fallbackReason: intent.fallbackReason,
};
case "suppressed":
return {
kind: "local",
prefix: intent.prefix,
qualifierParts: [],
useRemoteMetadata: false,
fallbackReason: intent.fallbackReason ?? "suppressed",
};
case "keyword":
return {
kind: "keyword",
prefix: intent.prefix,
qualifierParts: intent.qualifierParts,
useRemoteMetadata: false,
fallbackReason: intent.fallbackReason,
};
}
}
function semanticContextKind(model: SqlSemanticModel): SqlCompletionContext["contextKind"] {
switch (model.cursorIntent.kind) {
case "table":
case "schema":
case "catalog":
case "delete_target":
return "table";
case "routine":
return "routine";
case "alias_column":
return "alias_column";
case "insert_column":
case "update_column":
case "column":
case "star":
return "column";
case "join_condition":
return "join";
case "keyword":
case "suppressed":
return "keyword";
}
}
function semanticMutationTarget(model: SqlSemanticModel): SqlSemanticRowSource | undefined {
const targetId = model.cursorIntent.targetSourceId;
return targetId ? model.rowSources.find((source) => source.id === targetId) : model.rowSources.find((source) => source.kind === "mutation_target");
}
export function sqlCompletionContextFromSemantic(model: SqlSemanticModel, base: SqlCompletionContext): SqlCompletionContext {
if (model.cursorIntent.confidence === "low" || model.cursorIntent.kind === "suppressed") {
return base;
}
const scope = sqlSemanticCompletionScope(model);
const qualifier = model.cursorIntent.qualifierParts.length > 0 ? model.cursorIntent.qualifierParts.join(".") : undefined;
const referencedTables = sqlSemanticReferencedTables(model);
const mutationTarget = semanticMutationTarget(model);
const mutationSchema = mutationTarget?.qualifierParts[mutationTarget.qualifierParts.length - 1];
const suggestTables = scope.kind === "table" || scope.kind === "schema" || scope.kind === "catalog";
const suggestColumns = scope.kind === "columns";
const suggestRoutines = scope.kind === "routine";
const projectionAliases = sqlSemanticProjectionAliasColumns(model).map((column) => column.name);
return {
...base,
prefix: model.cursorIntent.prefix,
qualifier,
qualifierParts: model.cursorIntent.qualifierParts.length > 0 ? [...model.cursorIntent.qualifierParts] : undefined,
suggestTables,
suggestColumns,
suggestKeywords: scope.kind === "keyword" || (!suggestTables && !suggestColumns && !suggestRoutines),
suggestRoutines,
suggestJoinConditions: model.cursorIntent.kind === "join_condition",
exclusiveTableSuggestions: suggestTables,
exclusiveColumnSuggestions: model.cursorIntent.kind === "alias_column" || model.cursorIntent.kind === "insert_column" || model.cursorIntent.kind === "update_column",
exclusiveRoutineSuggestions: suggestRoutines,
prioritizeSelectAliases: base.prioritizeSelectAliases || projectionAliases.length > 0,
selectAliases: projectionAliases.length > 0 ? projectionAliases : base.selectAliases,
referencedTables: referencedTables.length > 0 ? referencedTables : base.referencedTables,
insertTable: model.cursorIntent.kind === "insert_column" ? mutationTarget?.name : base.insertTable,
insertSchema: model.cursorIntent.kind === "insert_column" ? mutationSchema : base.insertSchema,
updateTarget: model.cursorIntent.kind === "update_column" && mutationTarget ? { table: mutationTarget.name, schema: mutationSchema } : base.updateTarget,
deleteTarget: model.cursorIntent.kind === "delete_target" && mutationTarget ? { table: mutationTarget.name, schema: mutationSchema } : base.deleteTarget,
onStar: model.cursorIntent.kind === "star" || base.onStar,
contextKind: semanticContextKind(model),
};
}
@@ -0,0 +1,330 @@
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion";
import { executableStatementRanges, isOraclePlSqlStatement, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import type { DatabaseType, SqlColumnReference, SqlReferenceAnalysis, SqlReferenceScope, SqlTableReference, SqlTextSpan } from "@/types/database";
export interface SqlSemanticDiagnostic {
span: SqlTextSpan;
message: string;
severity: "error" | "warning";
}
export interface SqlSemanticDiagnosticSchema {
tables: SqlCompletionTable[];
columnsByTable: Map<string, SqlCompletionColumn[]>;
missingTables?: Set<string>;
loadedColumnTables?: Set<string>;
sql?: string;
}
export interface SqlSemanticDiagnosticVisibleRange {
from: number;
to: number;
}
export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRanges: readonly SqlSemanticDiagnosticVisibleRange[], databaseType?: DatabaseType): SqlTextRange[] {
const statements = executableStatementRanges(sql, databaseType);
if (statements.length === 0 || visibleRanges.length === 0) return [];
const selected: SqlTextRange[] = [];
const seen = new Set<string>();
for (const statement of statements) {
if (isOraclePlSqlStatement(statement.sql, databaseType)) continue;
if (!visibleRanges.some((visibleRange) => rangesIntersect(statement, visibleRange))) continue;
const key = `${statement.from}:${statement.to}`;
if (seen.has(key)) continue;
seen.add(key);
selected.push({ from: statement.from, to: statement.to, sql: sql.slice(statement.from, statement.to) });
}
return selected;
}
export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, schema: SqlSemanticDiagnosticSchema): SqlSemanticDiagnostic[] {
const diagnostics: SqlSemanticDiagnostic[] = [];
const tables = analysis.tables.filter((table) => table.name.trim());
const knownTables = new Map<string, SqlTableReference>();
const scopesById = scopesByIdMap(analysis.scopes);
for (const table of tables) {
knownTables.set(normalizeName(table.name), table);
if (table.alias) knownTables.set(normalizeName(table.alias), table);
if (table.schema) knownTables.set(normalizeName(`${table.schema}.${table.name}`), table);
}
for (const table of tables) {
if (!schema.missingTables?.has(tableReferenceKey(table))) continue;
diagnostics.push({
span: trimSqlTextSpanWhitespace(schema.sql, table.span),
message: `Unknown table ${displayTableName(table)}`,
severity: "error",
});
}
for (const column of analysis.columns) {
const table = resolveColumnTable(column, tables, knownTables, schema.sql, scopesById);
if (!table) continue;
if (schema.missingTables?.has(tableReferenceKey(table))) continue;
const columns = columnsForTable(table, schema.columnsByTable, schema.loadedColumnTables);
if (!columns) continue;
const columnNames = new Set(columns.map((item) => normalizeName(item.name)));
if (columnNames.has(normalizeName(column.name))) continue;
const displayName = column.qualifier ? `${column.qualifier}.${column.name}` : column.name;
diagnostics.push({
span: trimSqlTextSpanWhitespace(schema.sql, column.span),
message: `Unknown column ${displayName}`,
severity: "error",
});
}
return diagnostics;
}
function trimSqlTextSpanWhitespace(sql: string | undefined, span: SqlTextSpan): SqlTextSpan {
if (!sql) return span;
const range = sqlTextSpanToOffsetRange(sql, span);
if (!range) return span;
let from = range.from;
let to = range.to;
while (from < to && /\s/.test(sql[from] ?? "")) from += 1;
while (to > from && /\s/.test(sql[to - 1] ?? "")) to -= 1;
if (from === range.from && to === range.to) return span;
const start = offsetToSqlTextStartPosition(sql, from);
const end = offsetToSqlTextEndPosition(sql, to);
if (!start || !end) return span;
return {
start_line: start.line,
start_column: start.column,
end_line: end.line,
end_column: Math.max(end.column, start.column),
};
}
function sqlTextSpanToOffsetRange(sql: string, span: SqlTextSpan): { from: number; to: number } | null {
if (!span.start_line || !span.start_column) return null;
const from = sqlTextPositionToOffset(sql, span.start_line, span.start_column - 1);
const to = sqlTextPositionToOffset(sql, Math.max(span.end_line, span.start_line), Math.max(span.end_column, span.start_column));
if (from == null || to == null || to <= from) return null;
return { from, to };
}
function sqlTextPositionToOffset(sql: string, line: number, column: number): number | null {
const lines = sql.split(/\r?\n/);
if (line < 1 || line > lines.length) return null;
let offset = 0;
for (let index = 0; index < line - 1; index += 1) {
offset += lines[index].length + 1;
}
return Math.min(offset + Math.max(column, 0), offset + lines[line - 1].length);
}
function offsetToSqlTextStartPosition(sql: string, offset: number): { line: number; column: number } | null {
const position = offsetToLineColumn(sql, offset);
return position ? { line: position.line, column: position.column + 1 } : null;
}
function offsetToSqlTextEndPosition(sql: string, offset: number): { line: number; column: number } | null {
return offsetToLineColumn(sql, offset);
}
function offsetToLineColumn(sql: string, offset: number): { line: number; column: number } | null {
if (offset < 0 || offset > sql.length) return null;
const lines = sql.split(/\r?\n/);
let remaining = offset;
for (let index = 0; index < lines.length; index += 1) {
const lineLength = lines[index].length;
if (remaining <= lineLength) {
return { line: index + 1, column: remaining };
}
remaining -= lineLength + 1;
}
const lastLine = lines[lines.length - 1] ?? "";
return { line: lines.length, column: lastLine.length };
}
export function buildSqlParserErrorDiagnostic(error: unknown, sql: string): SqlSemanticDiagnostic | null {
const message = errorMessage(error);
const location = /\bat Line:\s*(\d+),\s*Column:\s*(\d+)\b/i.exec(message);
if (!location) return null;
const startLine = Number.parseInt(location[1], 10);
const startColumn = Number.parseInt(location[2], 10);
if (!Number.isFinite(startLine) || !Number.isFinite(startColumn) || startLine < 1 || startColumn < 1) return null;
const lineText = sql.split(/\r?\n/)[startLine - 1] ?? "";
const startIndex = Math.max(startColumn - 1, 0);
const token = /^[\w$]+/.exec(lineText.slice(startIndex))?.[0];
const tokenLength = Math.max(token?.length ?? 1, 1);
return {
span: {
start_line: startLine,
start_column: startColumn,
end_line: startLine,
end_column: startColumn + tokenLength - 1,
},
message,
severity: "error",
};
}
export function areSqlSemanticDiagnosticsEqual(left: readonly SqlSemanticDiagnostic[], right: readonly SqlSemanticDiagnostic[]): boolean {
if (left.length !== right.length) return false;
return left.every((item, index) => {
const other = right[index];
return !!other && item.message === other.message && item.severity === other.severity && item.span.start_line === other.span.start_line && item.span.start_column === other.span.start_column && item.span.end_line === other.span.end_line && item.span.end_column === other.span.end_column;
});
}
export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, options: { databaseType?: DatabaseType } = {}): boolean {
if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "qdrant" || options.databaseType === "milvus" || options.databaseType === "weaviate" || options.databaseType === "chromadb" || options.databaseType === "redis") return false;
const context = getSqlCompletionContext(sql, cursor);
if (context.exclusiveColumnSuggestions) return false;
if (context.qualifier) return false;
if ((context.suggestTables || context.exclusiveTableSuggestions) && isCursorAfterTableTrigger(sql, cursor)) return false;
return true;
}
export function isSqlSemanticDiagnosticInputContext(sql: string, cursor: number, options: { databaseType?: DatabaseType } = {}): boolean {
if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "qdrant" || options.databaseType === "milvus" || options.databaseType === "weaviate" || options.databaseType === "chromadb" || options.databaseType === "redis") return false;
const context = getSqlCompletionContext(sql, cursor);
return context.exclusiveColumnSuggestions || !!context.qualifier || ((context.suggestTables || context.exclusiveTableSuggestions) && isCursorAfterTableTrigger(sql, cursor));
}
function resolveColumnTable(column: SqlColumnReference, tables: SqlTableReference[], knownTables: Map<string, SqlTableReference>, sql?: string, scopesById?: Map<number, SqlReferenceScope>): SqlTableReference | null {
const candidateTables = candidateTablesForColumn(tables, column, sql, scopesById);
if (column.qualifier) {
return tableLookupFor(candidateTables).get(normalizeName(column.qualifier)) ?? (sql ? null : (knownTables.get(normalizeName(column.qualifier)) ?? null));
}
if (candidateTables.length !== 1) return null;
return candidateTables[0];
}
function candidateTablesForColumn(tables: SqlTableReference[], column: SqlColumnReference, sql?: string, scopesById?: Map<number, SqlReferenceScope>): SqlTableReference[] {
const scoped = tablesInVisibleScopes(tables, column, scopesById);
if (scoped) return scoped;
return sql ? tablesInSameStatement(tables, column, sql) : tables;
}
function tablesInVisibleScopes(tables: SqlTableReference[], column: SqlColumnReference, scopesById?: Map<number, SqlReferenceScope>): SqlTableReference[] | null {
if (column.scope_id == null || !scopesById || scopesById.size === 0) return null;
if (!column.qualifier) {
const currentScopeTables = tables.filter((table) => table.scope_id === column.scope_id);
if (currentScopeTables.length > 0) return currentScopeTables;
}
const visibleScopeIds = scopeAndParents(column.scope_id, scopesById);
if (visibleScopeIds.size === 0) return null;
return tables.filter((table) => table.scope_id != null && visibleScopeIds.has(table.scope_id));
}
function scopeAndParents(scopeId: number, scopesById: Map<number, SqlReferenceScope>): Set<number> {
const ids = new Set<number>();
let current: number | undefined = scopeId;
while (current != null && !ids.has(current)) {
ids.add(current);
const scope = scopesById.get(current);
current = scope?.parent_id ?? undefined;
}
return ids;
}
function tableLookupFor(tables: SqlTableReference[]): Map<string, SqlTableReference> {
const lookup = new Map<string, SqlTableReference>();
for (const table of tables) {
lookup.set(normalizeName(table.name), table);
if (table.alias) lookup.set(normalizeName(table.alias), table);
if (table.schema) lookup.set(normalizeName(`${table.schema}.${table.name}`), table);
}
return lookup;
}
function tablesInSameStatement(tables: SqlTableReference[], column: SqlColumnReference, sql: string): SqlTableReference[] {
const columnOffset = spanStartOffset(sql, column.span);
if (columnOffset == null) return tables;
return tables.filter((table) => {
const tableOffset = spanStartOffset(sql, table.span);
return tableOffset != null && statementIndexAt(sql, tableOffset) === statementIndexAt(sql, columnOffset);
});
}
function scopesByIdMap(scopes: readonly SqlReferenceScope[] | undefined): Map<number, SqlReferenceScope> {
const map = new Map<number, SqlReferenceScope>();
for (const scope of scopes ?? []) map.set(scope.id, scope);
return map;
}
function spanStartOffset(sql: string, span: SqlTextSpan): number | null {
if (!span.start_line || !span.start_column) return null;
const lines = sql.split(/\r?\n/);
const lineIndex = span.start_line - 1;
if (lineIndex < 0 || lineIndex >= lines.length) return null;
let offset = 0;
for (let index = 0; index < lineIndex; index++) offset += lines[index].length + 1;
return Math.min(offset + span.start_column - 1, offset + lines[lineIndex].length);
}
function statementIndexAt(sql: string, offset: number): number {
let statementIndex = 0;
for (let index = 0; index < Math.min(offset, sql.length); index++) {
if (sql[index] === ";") statementIndex++;
}
return statementIndex;
}
function columnsForTable(table: SqlTableReference, columnsByTable: Map<string, SqlCompletionColumn[]>, loadedColumnTables?: Set<string>): SqlCompletionColumn[] | null {
const inlineColumns = (table as SqlTableReference & { columns?: string[] }).columns;
if (inlineColumns && inlineColumns.length > 0) {
return inlineColumns.map((name) => ({
name,
table: table.name,
schema: table.schema ?? undefined,
}));
}
const keys = table.schema ? [`${table.schema}.${table.name}`, table.name] : [table.name, ...keysWithTableName(columnsByTable, table.name)];
for (const key of keys) {
const normalizedKey = normalizeName(key);
const columns = columnsByTable.get(key) ?? columnsByTable.get(normalizedKey);
if (!columns) continue;
if (columns.length > 0 || loadedColumnTables?.has(normalizedKey)) return columns;
}
if (loadedColumnTables?.has(tableReferenceKey(table))) return [];
return null;
}
function keysWithTableName(columnsByTable: Map<string, SqlCompletionColumn[]>, tableName: string): string[] {
const suffix = `.${normalizeName(tableName)}`;
return [...columnsByTable.keys()].filter((key) => normalizeName(key).endsWith(suffix));
}
function rangesIntersect(left: SqlSemanticDiagnosticVisibleRange, right: SqlSemanticDiagnosticVisibleRange): boolean {
return left.from < right.to && right.from < left.to;
}
export function tableReferenceKey(table: Pick<SqlTableReference, "name" | "schema">): string {
return normalizeName(table.schema ? `${table.schema}.${table.name}` : table.name);
}
function displayTableName(table: SqlTableReference): string {
return table.schema ? `${table.schema}.${table.name}` : table.name;
}
function isCursorAfterTableTrigger(sql: string, cursor: number): boolean {
const beforeCursor = sql.slice(0, cursor).trimEnd();
return /\b(from|join|update|into|table)(?:\s+[\w$`"'[\].]*)?$/i.test(beforeCursor);
}
function normalizeName(value: string): string {
let normalized = value;
while (normalized && `"'\`[]`.includes(normalized[0])) normalized = normalized.slice(1);
while (normalized && `"'\`[]`.includes(normalized[normalized.length - 1])) normalized = normalized.slice(0, -1);
return normalized.toLowerCase();
}
function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}
@@ -0,0 +1,172 @@
import type { DatabaseType } from "@/types/database";
export interface SqlSemanticProjectionAliasVisibility {
where: boolean;
groupBy: boolean;
having: boolean;
orderBy: boolean;
}
export interface SqlSemanticDialectAdapter {
id: string;
identifierQuotes: Array<{ open: string; close: string }>;
supportsAsForTableAlias: boolean;
projectionAliasVisibility: SqlSemanticProjectionAliasVisibility;
normalizeIdentifier(identifier: string, quoted?: boolean): string;
quoteIdentifier(identifier: string): string;
qualifierRole(parts: string[], context: "table" | "column" | "routine"): "catalog" | "schema" | "table" | "package" | "unknown";
}
function quoteWith(identifier: string, quote: string): string {
return `${quote}${identifier.replaceAll(quote, quote + quote)}${quote}`;
}
function defaultNormalize(identifier: string): string {
return identifier;
}
function lowerUnquoted(identifier: string, quoted?: boolean): string {
return quoted ? identifier : identifier.toLowerCase();
}
function upperUnquoted(identifier: string, quoted?: boolean): string {
return quoted ? identifier : identifier.toUpperCase();
}
const defaultProjectionAliasVisibility: SqlSemanticProjectionAliasVisibility = {
where: false,
groupBy: false,
having: false,
orderBy: true,
};
function roleForGenericQualifier(parts: string[], context: "table" | "column" | "routine"): "catalog" | "schema" | "table" | "package" | "unknown" {
if (parts.length <= 0) return "unknown";
if (context === "column") return parts.length >= 2 ? "table" : "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
if (parts.length >= 2) return "schema";
return "schema";
}
export const SQL_SEMANTIC_DIALECTS: Record<string, SqlSemanticDialectAdapter> = {
generic: {
id: "generic",
identifierQuotes: [{ open: '"', close: '"' }],
supportsAsForTableAlias: true,
projectionAliasVisibility: defaultProjectionAliasVisibility,
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, '"'),
qualifierRole: roleForGenericQualifier,
},
postgres: {
id: "postgres",
identifierQuotes: [{ open: '"', close: '"' }],
supportsAsForTableAlias: true,
projectionAliasVisibility: defaultProjectionAliasVisibility,
normalizeIdentifier: lowerUnquoted,
quoteIdentifier: (identifier) => quoteWith(identifier, '"'),
qualifierRole: roleForGenericQualifier,
},
mysql: {
id: "mysql",
identifierQuotes: [
{ open: "`", close: "`" },
{ open: '"', close: '"' },
],
supportsAsForTableAlias: true,
projectionAliasVisibility: { where: false, groupBy: true, having: true, orderBy: true },
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, "`"),
qualifierRole(parts, context) {
if (context === "column") return parts.length >= 2 ? "table" : "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
return parts.length >= 1 ? "schema" : "unknown";
},
},
sqlserver: {
id: "sqlserver",
identifierQuotes: [
{ open: "[", close: "]" },
{ open: '"', close: '"' },
],
supportsAsForTableAlias: true,
projectionAliasVisibility: defaultProjectionAliasVisibility,
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => `[${identifier.replaceAll("]", "]]")}]`,
qualifierRole(parts, context) {
if (context === "column") return parts.length >= 2 ? "table" : "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
if (parts.length >= 2) return "schema";
return "schema";
},
},
sqlite: {
id: "sqlite",
identifierQuotes: [
{ open: '"', close: '"' },
{ open: "`", close: "`" },
{ open: "[", close: "]" },
],
supportsAsForTableAlias: true,
projectionAliasVisibility: { where: false, groupBy: true, having: true, orderBy: true },
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, '"'),
qualifierRole: roleForGenericQualifier,
},
duckdb: {
id: "duckdb",
identifierQuotes: [{ open: '"', close: '"' }],
supportsAsForTableAlias: true,
projectionAliasVisibility: defaultProjectionAliasVisibility,
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, '"'),
qualifierRole: roleForGenericQualifier,
},
oracle: {
id: "oracle",
identifierQuotes: [{ open: '"', close: '"' }],
supportsAsForTableAlias: false,
projectionAliasVisibility: defaultProjectionAliasVisibility,
normalizeIdentifier: upperUnquoted,
quoteIdentifier: (identifier) => quoteWith(identifier, '"'),
qualifierRole(parts, context) {
if (context === "column") return parts.length >= 2 ? "table" : "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
return parts.length >= 1 ? "schema" : "unknown";
},
},
};
export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" }): SqlSemanticDialectAdapter {
if (options.dialect && SQL_SEMANTIC_DIALECTS[options.dialect]) return SQL_SEMANTIC_DIALECTS[options.dialect];
switch (options.databaseType) {
case "postgres":
case "redshift":
case "opengauss":
case "gaussdb":
case "highgo":
return SQL_SEMANTIC_DIALECTS.postgres;
case "mysql":
case "doris":
case "starrocks":
return SQL_SEMANTIC_DIALECTS.mysql;
case "sqlserver":
return SQL_SEMANTIC_DIALECTS.sqlserver;
case "sqlite":
case "rqlite":
case "turso":
return SQL_SEMANTIC_DIALECTS.sqlite;
case "duckdb":
return SQL_SEMANTIC_DIALECTS.duckdb;
case "oracle":
case "oceanbase-oracle":
case "dameng":
case "kingbase":
case "vastbase":
case "goldendb":
case "yashandb":
return SQL_SEMANTIC_DIALECTS.oracle;
default:
return SQL_SEMANTIC_DIALECTS.generic;
}
}
@@ -0,0 +1,168 @@
import type { SqlSemanticCompletionScopeKind } from "@/lib/sql/semantic/completion";
import type { SqlSemanticConfidence, SqlSemanticCursorKind, SqlSemanticStatementKind } from "@/lib/sql/semantic/types";
export interface SqlSemanticFixture {
name: string;
sql: string;
databaseType?: "postgres" | "mysql" | "sqlserver" | "sqlite" | "duckdb" | "oracle";
expected: {
statementKind: SqlSemanticStatementKind;
cursorKind: SqlSemanticCursorKind;
completionScope: SqlSemanticCompletionScopeKind;
prefix: string;
qualifierParts?: string[];
confidence: SqlSemanticConfidence;
rowSources?: Array<{ name: string; alias?: string; kind?: string; columns?: string[] }>;
completionLabels?: string[];
};
}
export function sqlFixtureCursor(input: string): { sql: string; cursor: number } {
const cursor = input.indexOf("|");
if (cursor < 0) {
throw new Error("SQL semantic fixture is missing a | cursor marker");
}
return {
sql: input.slice(0, cursor) + input.slice(cursor + 1),
cursor,
};
}
export const SQL_SEMANTIC_BASELINE_FIXTURES: SqlSemanticFixture[] = [
{
name: "select alias column",
sql: "SELECT * FROM users u JOIN orders o ON o.user_id = u.id WHERE u.|",
expected: {
statementKind: "select",
cursorKind: "alias_column",
completionScope: "columns",
prefix: "",
qualifierParts: ["u"],
confidence: "high",
rowSources: [
{ name: "users", alias: "u", kind: "table" },
{ name: "orders", alias: "o", kind: "table" },
],
},
},
{
name: "with cte table source",
sql: "WITH recent_orders(id, total) AS (SELECT id, total FROM orders) SELECT * FROM recent_orders ro WHERE ro.|",
expected: {
statementKind: "select",
cursorKind: "alias_column",
completionScope: "columns",
prefix: "",
qualifierParts: ["ro"],
confidence: "high",
rowSources: [{ name: "recent_orders", alias: "ro", kind: "cte", columns: ["id", "total"] }],
completionLabels: ["id", "total"],
},
},
{
name: "subquery projection columns",
sql: "SELECT * FROM (SELECT id, name AS user_name FROM users) sq WHERE sq.|",
expected: {
statementKind: "select",
cursorKind: "alias_column",
completionScope: "columns",
prefix: "",
qualifierParts: ["sq"],
confidence: "high",
rowSources: [{ name: "sq", alias: "sq", kind: "subquery", columns: ["id", "user_name"] }],
completionLabels: ["id", "user_name"],
},
},
{
name: "insert target columns",
sql: "INSERT INTO dbo.Users (|",
databaseType: "sqlserver",
expected: {
statementKind: "insert",
cursorKind: "insert_column",
completionScope: "columns",
prefix: "",
qualifierParts: [],
confidence: "medium",
rowSources: [{ name: "Users", kind: "mutation_target" }],
},
},
{
name: "update set columns",
sql: "UPDATE dbo.Users SET |",
databaseType: "sqlserver",
expected: {
statementKind: "update",
cursorKind: "update_column",
completionScope: "columns",
prefix: "",
qualifierParts: [],
confidence: "medium",
rowSources: [{ name: "Users", kind: "mutation_target" }],
},
},
{
name: "call routine",
sql: "CALL app.refresh_|",
databaseType: "postgres",
expected: {
statementKind: "call",
cursorKind: "routine",
completionScope: "routine",
prefix: "refresh_",
qualifierParts: ["app"],
confidence: "high",
},
},
{
name: "delete target",
sql: "DELETE FROM audit_events ae WHERE ae.|",
expected: {
statementKind: "delete",
cursorKind: "alias_column",
completionScope: "columns",
prefix: "",
qualifierParts: ["ae"],
confidence: "high",
rowSources: [{ name: "audit_events", alias: "ae", kind: "mutation_target" }],
},
},
{
name: "table function alias",
sql: "SELECT * FROM JSON_TABLE(payload, '$' COLUMNS(id INT PATH '$.id')) jt WHERE jt.|",
databaseType: "oracle",
expected: {
statementKind: "select",
cursorKind: "alias_column",
completionScope: "columns",
prefix: "",
qualifierParts: ["JT"],
confidence: "high",
rowSources: [{ name: "JT", alias: "JT", kind: "table_function" }],
},
},
{
name: "schema qualified table",
sql: "SELECT * FROM reporting.|",
databaseType: "postgres",
expected: {
statementKind: "select",
cursorKind: "table",
completionScope: "table",
prefix: "",
qualifierParts: ["reporting"],
confidence: "medium",
},
},
{
name: "comment suppressed",
sql: "SELECT * FROM users -- u.|",
expected: {
statementKind: "select",
cursorKind: "suppressed",
completionScope: "local",
prefix: "",
confidence: "high",
},
},
];
+580
View File
@@ -0,0 +1,580 @@
import { sqlSemanticDialectFor, type SqlSemanticDialectAdapter } from "@/lib/sql/semantic/dialect";
import { findActiveSqlStatementSpan, isSuppressedSqlSemanticContext, tokenIsIdentifier, tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens";
import type {
SqlSemanticBuildOptions,
SqlSemanticClauseSpans,
SqlSemanticCursorIntent,
SqlSemanticIdentifierPart,
SqlSemanticModel,
SqlSemanticProjection,
SqlSemanticQualifiedName,
SqlSemanticRowSource,
SqlSemanticScope,
SqlSemanticSpan,
SqlSemanticStatement,
SqlSemanticStatementKind,
SqlSemanticToken,
} from "@/lib/sql/semantic/types";
const TABLE_INTRODUCERS = new Set(["from", "join", "update", "into", "using", "apply"]);
const JOIN_MODIFIERS = new Set(["left", "right", "inner", "outer", "cross", "full", "natural"]);
const CLAUSE_BOUNDARIES = new Set(["where", "group", "having", "order", "limit", "offset", "union", "intersect", "except", "on", "set", "values", "returning"]);
const ALIAS_BLACKLIST = new Set([...CLAUSE_BOUNDARIES, "join", "left", "right", "inner", "outer", "cross", "full", "natural", "as", "select", "from"]);
interface ParseState {
dialect: SqlSemanticDialectAdapter;
tokens: SqlSemanticToken[];
statement: SqlSemanticStatement;
cteSources: SqlSemanticRowSource[];
}
interface TrailingIdentifier {
prefix: string;
replacementRange: SqlSemanticSpan;
qualifierParts: string[];
}
function significantTokens(tokens: readonly SqlSemanticToken[]): SqlSemanticToken[] {
return tokens.filter((item) => item.kind !== "comment");
}
function tokenTextAt(sql: string, span: SqlSemanticSpan): string {
return sql.slice(span.start, span.end);
}
function firstWord(tokens: readonly SqlSemanticToken[]): string {
return tokens.find((item) => item.kind === "word")?.normalized ?? "";
}
function statementKind(tokens: readonly SqlSemanticToken[]): SqlSemanticStatementKind {
const word = firstWord(tokens);
if (word === "with" || word === "select") return "select";
if (word === "insert") return "insert";
if (word === "update") return "update";
if (word === "delete") return "delete";
if (word === "call" || word === "exec" || word === "execute") return "call";
return "unknown";
}
function identifierPart(tokenValue: SqlSemanticToken, dialect: SqlSemanticDialectAdapter): SqlSemanticIdentifierPart {
const quoted = tokenValue.kind === "quoted_identifier";
const raw = unquoteSqlSemanticIdentifier(tokenValue);
return {
raw: tokenValue.text,
name: dialect.normalizeIdentifier(raw, quoted),
span: tokenValue.span,
quote: tokenValue.quote,
};
}
function readQualifiedName(tokens: readonly SqlSemanticToken[], startIndex: number, dialect: SqlSemanticDialectAdapter): { name: SqlSemanticQualifiedName; nextIndex: number } | null {
const parts: SqlSemanticIdentifierPart[] = [];
let index = startIndex;
while (index < tokens.length) {
const current = tokens[index];
if (!tokenIsIdentifier(current)) break;
parts.push(identifierPart(current, dialect));
if (tokens[index + 1]?.text !== ".") {
index += 1;
break;
}
index += 2;
}
if (parts.length === 0) return null;
return {
name: {
parts,
span: { start: parts[0]?.span.start ?? tokens[startIndex]?.span.start ?? 0, end: parts[parts.length - 1]?.span.end ?? tokens[startIndex]?.span.end ?? 0 },
},
nextIndex: index,
};
}
function sourceNameFromQualifiedName(name: SqlSemanticQualifiedName): { name: string; qualifierParts: string[] } {
const parts = name.parts.map((part) => part.name);
return {
name: parts[parts.length - 1] ?? "",
qualifierParts: parts.slice(0, -1),
};
}
function findMatchingParenToken(tokens: readonly SqlSemanticToken[], openIndex: number): number {
if (tokens[openIndex]?.text !== "(") return -1;
const startDepth = tokens[openIndex]?.depth ?? 0;
for (let index = openIndex + 1; index < tokens.length; index += 1) {
const item = tokens[index];
if (item?.text === ")" && item.depth === startDepth) return index;
}
return -1;
}
function splitTopLevelByComma(tokens: readonly SqlSemanticToken[]): SqlSemanticToken[][] {
const groups: SqlSemanticToken[][] = [];
let current: SqlSemanticToken[] = [];
const baseDepth = tokens.reduce((min, item) => Math.min(min, item.depth), Number.POSITIVE_INFINITY);
for (const item of tokens) {
if (item.text === "," && item.depth === baseDepth) {
groups.push(current);
current = [];
} else {
current.push(item);
}
}
if (current.length > 0) groups.push(current);
return groups;
}
function projectionNameFromTokens(tokens: readonly SqlSemanticToken[], dialect: SqlSemanticDialectAdapter): SqlSemanticProjection | null {
const useful = tokens.filter((item) => item.kind !== "comment");
if (useful.length === 0) return null;
let asIndex = -1;
for (let index = useful.length - 1; index >= 0; index -= 1) {
if (useful[index]?.kind === "word" && useful[index]?.normalized === "as") {
asIndex = index;
break;
}
}
const aliasToken = asIndex >= 0 ? useful[asIndex + 1] : undefined;
if (tokenIsIdentifier(aliasToken)) {
const name = identifierPart(aliasToken, dialect).name;
return {
name,
alias: name,
aliasSpan: aliasToken.span,
sourceExpression: useful.map((item) => item.text).join(" "),
span: { start: useful[0]?.span.start ?? 0, end: useful[useful.length - 1]?.span.end ?? 0 },
};
}
const lastIdentifier = [...useful].reverse().find(tokenIsIdentifier);
if (!lastIdentifier) return null;
const name = identifierPart(lastIdentifier, dialect).name;
return {
name,
sourceExpression: useful.map((item) => item.text).join(" "),
span: { start: useful[0]?.span.start ?? 0, end: useful[useful.length - 1]?.span.end ?? 0 },
};
}
function parseSelectProjections(tokens: readonly SqlSemanticToken[], dialect: SqlSemanticDialectAdapter): SqlSemanticProjection[] {
const baseDepth = tokens.reduce((min, item) => Math.min(min, item.depth), Number.POSITIVE_INFINITY);
const selectDepth = Number.isFinite(baseDepth) ? baseDepth : 0;
const selectIndex = tokens.findIndex((item) => item.depth === selectDepth && item.kind === "word" && item.normalized === "select");
if (selectIndex < 0) return [];
let fromIndex = tokens.findIndex((item, index) => index > selectIndex && item.depth === selectDepth && item.kind === "word" && item.normalized === "from");
if (fromIndex < 0) fromIndex = tokens.length;
const projectionTokens = tokens.slice(selectIndex + 1, fromIndex);
return splitTopLevelByComma(projectionTokens)
.map((group) => projectionNameFromTokens(group, dialect))
.filter((projection): projection is SqlSemanticProjection => projection != null && projection.name !== "*");
}
function parseCteSources(state: ParseState): SqlSemanticRowSource[] {
const tokens = state.tokens;
const first = tokens.findIndex((item) => item.kind === "word" && item.normalized === "with");
if (first < 0) return [];
const sources: SqlSemanticRowSource[] = [];
let index = first + 1;
if (tokens[index]?.normalized === "recursive") index += 1;
while (index < tokens.length) {
while (tokens[index]?.text === ",") index += 1;
const nameToken = tokens[index];
if (!tokenIsIdentifier(nameToken)) break;
const namePart = identifierPart(nameToken, state.dialect);
index += 1;
const explicitColumns: string[] = [];
if (tokens[index]?.text === "(") {
const close = findMatchingParenToken(tokens, index);
if (close > index) {
for (const part of splitTopLevelByComma(tokens.slice(index + 1, close))) {
const identifier = part.find(tokenIsIdentifier);
if (identifier) explicitColumns.push(identifierPart(identifier, state.dialect).name);
}
index = close + 1;
}
}
if (tokens[index]?.normalized === "as") index += 1;
if (tokens[index]?.text !== "(") break;
const bodyOpen = index;
const bodyClose = findMatchingParenToken(tokens, bodyOpen);
const safeBodyClose = bodyClose < 0 ? tokens.length - 1 : bodyClose;
const bodyTokens = tokens.slice(bodyOpen + 1, safeBodyClose);
const bodyColumns = explicitColumns.length > 0 ? explicitColumns : parseSelectProjections(bodyTokens, state.dialect).map((projection) => projection.name);
sources.push({
id: `cte:${namePart.name}:${sources.length}`,
kind: "cte",
name: namePart.name,
qualifierParts: [],
sourceSpan: { start: nameToken.span.start, end: tokens[safeBodyClose]?.span.end ?? nameToken.span.end },
columns: bodyColumns,
});
index = safeBodyClose + 1;
if (tokens[index]?.text !== ",") break;
index += 1;
}
return sources;
}
function aliasAfter(tokens: readonly SqlSemanticToken[], index: number, dialect: SqlSemanticDialectAdapter): { alias?: string; aliasSpan?: SqlSemanticSpan; nextIndex: number } {
let cursor = index;
if (tokens[cursor]?.kind === "word" && tokens[cursor]?.normalized === "as") cursor += 1;
const aliasToken = tokens[cursor];
if (tokenIsIdentifier(aliasToken)) {
const alias = identifierPart(aliasToken, dialect).name;
if (!ALIAS_BLACKLIST.has(alias.toLowerCase())) {
return { alias, aliasSpan: aliasToken.span, nextIndex: cursor + 1 };
}
}
return { nextIndex: index };
}
function parseSubquerySource(state: ParseState, openIndex: number, introducer: string, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null {
const close = findMatchingParenToken(state.tokens, openIndex);
if (close < 0) return null;
const alias = aliasAfter(state.tokens, close + 1, state.dialect);
if (!alias.alias) return null;
const bodyTokens = state.tokens.slice(openIndex + 1, close);
const columns = parseSelectProjections(bodyTokens, state.dialect).map((projection) => projection.name);
return {
source: {
id: `${introducer}:subquery:${sourceIndex}`,
kind: "subquery",
name: alias.alias,
qualifierParts: [],
alias: alias.alias,
aliasSpan: alias.aliasSpan,
sourceSpan: { start: state.tokens[openIndex]?.span.start ?? 0, end: alias.aliasSpan?.end ?? state.tokens[close]?.span.end ?? 0 },
columns,
},
nextIndex: alias.nextIndex,
};
}
function parseTableFunctionSource(state: ParseState, nameIndex: number, introducer: string, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null {
const nameToken = state.tokens[nameIndex];
if (!nameToken || nameToken.kind !== "word" || !["table", "xmltable", "json_table", "the", "read_csv", "read_parquet", "read_json", "unnest"].includes(nameToken.normalized)) return null;
if (state.tokens[nameIndex + 1]?.text !== "(") return null;
const close = findMatchingParenToken(state.tokens, nameIndex + 1);
const safeClose = close < 0 ? nameIndex + 1 : close;
const alias = aliasAfter(state.tokens, safeClose + 1, state.dialect);
const sourceName = alias.alias ?? nameToken.normalized;
return {
source: {
id: `${introducer}:table_function:${sourceIndex}`,
kind: "table_function",
name: sourceName,
qualifierParts: [],
alias: alias.alias,
aliasSpan: alias.aliasSpan,
sourceSpan: { start: nameToken.span.start, end: alias.aliasSpan?.end ?? state.tokens[safeClose]?.span.end ?? nameToken.span.end },
unresolved: close < 0,
},
nextIndex: alias.nextIndex,
};
}
function parseTableSource(state: ParseState, nameIndex: number, introducer: string, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null {
const qualified = readQualifiedName(state.tokens, nameIndex, state.dialect);
if (!qualified) return null;
const { name, qualifierParts } = sourceNameFromQualifiedName(qualified.name);
const alias = aliasAfter(state.tokens, qualified.nextIndex, state.dialect);
const cte = state.cteSources.find((source) => source.name.toLowerCase() === name.toLowerCase());
const kind = cte ? "cte" : introducer === "update" || introducer === "into" || (state.statement.kind === "delete" && introducer === "from") ? "mutation_target" : "table";
const source: SqlSemanticRowSource = {
id: `${introducer}:${name}:${sourceIndex}`,
kind,
name,
qualifiedName: qualified.name,
qualifierParts,
alias: alias.alias,
aliasSpan: alias.aliasSpan,
sourceSpan: { start: qualified.name.span.start, end: alias.aliasSpan?.end ?? qualified.name.span.end },
columns: cte?.columns,
metadataTarget: {
schema: qualifierParts[qualifierParts.length - 1],
table: name,
},
};
return { source, nextIndex: alias.nextIndex };
}
function parseRowSources(state: ParseState): SqlSemanticRowSource[] {
const sources: SqlSemanticRowSource[] = [...state.cteSources];
const rootDepth = state.tokens.reduce((min, item) => Math.min(min, item.depth), Number.POSITIVE_INFINITY);
const sourceDepth = Number.isFinite(rootDepth) ? rootDepth : 0;
for (let index = 0; index < state.tokens.length; index += 1) {
const item = state.tokens[index];
if (!item || item.kind !== "word") continue;
if (item.depth !== sourceDepth) continue;
const normalized = item.normalized;
if (!TABLE_INTRODUCERS.has(normalized)) continue;
if (JOIN_MODIFIERS.has(normalized)) continue;
let target = index + 1;
while (JOIN_MODIFIERS.has(state.tokens[target]?.normalized ?? "")) target += 1;
if (state.tokens[target]?.text === "(") {
const subquery = parseSubquerySource(state, target, normalized, sources.length);
if (subquery) {
sources.push(subquery.source);
index = subquery.nextIndex - 1;
}
continue;
}
const tableFunction = parseTableFunctionSource(state, target, normalized, sources.length);
if (tableFunction) {
sources.push(tableFunction.source);
index = tableFunction.nextIndex - 1;
continue;
}
const table = parseTableSource(state, target, normalized, sources.length);
if (table) {
sources.push(table.source);
index = table.nextIndex - 1;
}
}
return dedupeSources(sources);
}
function dedupeSources(sources: SqlSemanticRowSource[]): SqlSemanticRowSource[] {
const seen = new Set<string>();
const result: SqlSemanticRowSource[] = [];
for (const source of sources) {
const key = `${source.kind}:${source.name}:${source.alias ?? ""}:${source.sourceSpan.start}`;
if (seen.has(key)) continue;
seen.add(key);
result.push(source);
}
return result;
}
function clauseSpans(tokens: readonly SqlSemanticToken[]): SqlSemanticClauseSpans {
const spans: SqlSemanticClauseSpans = {};
const depth = tokens[0]?.depth ?? 0;
for (let index = 0; index < tokens.length; index += 1) {
const item = tokens[index];
if (!item || item.depth !== depth || item.kind !== "word") continue;
const next = tokens[index + 1];
const start = item.span.start;
const end = next?.span.start ?? tokens[tokens.length - 1]?.span.end ?? item.span.end;
if (item.normalized === "select") spans.select = { start, end };
if (item.normalized === "from") spans.from = { start, end };
if (item.normalized === "where") spans.where = { start, end };
if (item.normalized === "having") spans.having = { start, end };
if (item.normalized === "limit") spans.limit = { start, end };
if (item.normalized === "group" && next?.normalized === "by") spans.groupBy = { start, end: next.span.end };
if (item.normalized === "order" && next?.normalized === "by") spans.orderBy = { start, end: next.span.end };
if (item.normalized === "set") spans.updateSet = { start, end };
}
return spans;
}
function trailingIdentifier(tokens: readonly SqlSemanticToken[], cursor: number, dialect: SqlSemanticDialectAdapter): TrailingIdentifier {
const before = tokens.filter((item) => item.span.start < cursor && item.kind !== "comment" && item.kind !== "string");
const last = before[before.length - 1];
if (!last) return { prefix: "", replacementRange: { start: cursor, end: cursor }, qualifierParts: [] };
if (last.span.end < cursor && last.text !== ".") {
return { prefix: "", replacementRange: { start: cursor, end: cursor }, qualifierParts: [] };
}
let prefix = "";
let replacementRange: SqlSemanticSpan = { start: cursor, end: cursor };
let index = before.length - 1;
if (tokenIsIdentifier(last) && cursor <= last.span.end) {
const rawPrefix = tokenTextAt(last.text, { start: 0, end: Math.max(0, cursor - last.span.start) });
prefix = last.kind === "quoted_identifier" ? unquoteSqlSemanticIdentifier({ ...last, text: rawPrefix.endsWith(last.quote ?? "") ? rawPrefix : rawPrefix + (last.quote === "[" ? "]" : (last.quote ?? "")) }) : rawPrefix;
replacementRange = { start: last.span.start, end: cursor };
index -= 1;
if (before[index]?.text === ".") index -= 1;
} else if (last.text === ".") {
index -= 1;
}
const qualifierParts: string[] = [];
while (index >= 0) {
const identifier = before[index];
if (!tokenIsIdentifier(identifier)) break;
qualifierParts.unshift(identifierPart(identifier, dialect).name);
if (before[index - 1]?.text !== ".") break;
index -= 2;
}
return { prefix, replacementRange, qualifierParts };
}
function previousWord(tokens: readonly SqlSemanticToken[], cursor: number): string {
const before = tokens.filter((item) => item.span.end <= cursor && item.kind === "word");
return before[before.length - 1]?.normalized ?? "";
}
function wordBeforePosition(tokens: readonly SqlSemanticToken[], position: number): string {
const before = tokens.filter((item) => item.span.end <= position && item.kind === "word");
return before[before.length - 1]?.normalized ?? "";
}
function isTableListContinuation(tokens: readonly SqlSemanticToken[], position: number): boolean {
const before = tokens.filter((item) => item.span.end <= position && item.kind !== "comment");
const commaIndex = before.length - 1;
const comma = before[commaIndex];
if (comma?.text !== ",") return false;
const depth = comma.depth;
for (let index = commaIndex - 1; index >= 0; index -= 1) {
const token = before[index];
if (!token || token.depth !== depth || token.kind !== "word") continue;
if (TABLE_INTRODUCERS.has(token.normalized) || token.normalized === "from" || token.normalized === "join") return true;
if (CLAUSE_BOUNDARIES.has(token.normalized) || token.normalized === "select") return false;
}
return false;
}
function hasWordBefore(tokens: readonly SqlSemanticToken[], cursor: number, word: string): boolean {
return tokens.some((item) => item.span.end <= cursor && item.kind === "word" && item.normalized === word);
}
function isBeforeWord(tokens: readonly SqlSemanticToken[], cursor: number, word: string): boolean {
const target = tokens.find((item) => item.span.start >= cursor && item.kind === "word" && item.normalized === word);
return !!target;
}
function sourceForQualifier(sources: readonly SqlSemanticRowSource[], qualifierParts: readonly string[]): SqlSemanticRowSource | undefined {
const qualifier = qualifierParts[qualifierParts.length - 1]?.toLowerCase();
if (!qualifier) return undefined;
return sources.find((source) => source.alias?.toLowerCase() === qualifier || source.name.toLowerCase() === qualifier);
}
function starQualifierParts(before: readonly SqlSemanticToken[], starIndex: number, dialect: SqlSemanticDialectAdapter): string[] {
let index = starIndex - 1;
if (before[index]?.text === ".") index -= 1;
const qualifierParts: string[] = [];
while (index >= 0) {
const identifier = before[index];
if (!tokenIsIdentifier(identifier)) break;
qualifierParts.unshift(identifierPart(identifier, dialect).name);
if (before[index - 1]?.text !== ".") break;
index -= 2;
}
return qualifierParts;
}
function buildCursorIntent(tokens: readonly SqlSemanticToken[], cursor: number, sources: readonly SqlSemanticRowSource[], dialect: SqlSemanticDialectAdapter, suppressed: boolean, kind: SqlSemanticStatementKind): SqlSemanticCursorIntent {
if (suppressed) {
return { kind: "suppressed", prefix: "", replacementRange: { start: cursor, end: cursor }, qualifierParts: [], expectedObjectKinds: [], confidence: "high", fallbackReason: "comment_or_string" };
}
const trailing = trailingIdentifier(tokens, cursor, dialect);
const previous = previousWord(tokens, cursor);
const targetSource = sourceForQualifier(sources, trailing.qualifierParts);
const before = tokens.filter((item) => item.span.end <= cursor);
const last = before[before.length - 1];
const wordBeforeReplacement = wordBeforePosition(tokens, trailing.replacementRange.start);
const tableListContinuation = isTableListContinuation(tokens, trailing.replacementRange.start);
if (last?.text === "*" || trailing.prefix === "*") {
const starIndex = last?.text === "*" ? before.length - 1 : -1;
const qualifierParts = starIndex >= 0 ? starQualifierParts(before, starIndex, dialect) : trailing.qualifierParts;
const starTarget = sourceForQualifier(sources, qualifierParts);
const replacementRange = last?.text === "*" ? { start: last.span.start, end: Math.min(cursor, last.span.end) } : trailing.replacementRange;
return { kind: "star", prefix: "*", replacementRange, qualifierParts, targetSourceId: starTarget?.id, expectedObjectKinds: ["column"], confidence: "high" };
}
if (kind === "call") {
return { kind: "routine", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["routine", "procedure", "function"], confidence: "high" };
}
if (kind === "insert" && hasWordBefore(tokens, cursor, "into") && !hasWordBefore(tokens, cursor, "values")) {
const mutationTarget = sources.find((source) => source.kind === "mutation_target");
return {
kind: "insert_column",
prefix: trailing.prefix,
replacementRange: trailing.replacementRange,
qualifierParts: trailing.qualifierParts,
targetSourceId: mutationTarget?.id,
expectedObjectKinds: ["column"],
confidence: mutationTarget ? "medium" : "low",
fallbackReason: mutationTarget ? undefined : "unresolved_insert_target",
};
}
if (
trailing.qualifierParts.length > 0 &&
(previous === "from" || previous === "join" || TABLE_INTRODUCERS.has(previous) || TABLE_INTRODUCERS.has(wordBeforeReplacement) || (!!targetSource && !targetSource.alias && TABLE_INTRODUCERS.has(wordBeforePosition(tokens, targetSource.sourceSpan.start))))
) {
const role = dialect.qualifierRole(trailing.qualifierParts, "table");
return { kind: role === "catalog" ? "catalog" : "table", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["table", "view"], confidence: "medium" };
}
if (trailing.qualifierParts.length > 0 && targetSource) {
return { kind: "alias_column", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, targetSourceId: targetSource.id, expectedObjectKinds: ["column"], confidence: "high" };
}
if (TABLE_INTRODUCERS.has(previous) || JOIN_MODIFIERS.has(previous) || previous === "from" || previous === "join" || tableListContinuation) {
return { kind: previous === "join" ? "table" : "table", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["table", "view"], confidence: "high" };
}
if (previous === "call" || previous === "exec" || previous === "execute") {
return { kind: "routine", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["routine", "procedure", "function"], confidence: "high" };
}
if (previous === "set") {
return {
kind: "update_column",
prefix: trailing.prefix,
replacementRange: trailing.replacementRange,
qualifierParts: trailing.qualifierParts,
expectedObjectKinds: ["column"],
confidence: sources.length > 0 ? "medium" : "low",
fallbackReason: sources.length > 0 ? undefined : "unresolved_update_target",
};
}
if (["where", "on", "and", "or", "having", "by", "select"].includes(previous) && sources.length > 0 && !isBeforeWord(tokens, cursor, "from")) {
return { kind: previous === "on" ? "join_condition" : "column", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["column"], confidence: "medium" };
}
return { kind: "keyword", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: [], confidence: "low", fallbackReason: "keyword_context" };
}
function buildScope(statement: SqlSemanticStatement, rowSources: SqlSemanticRowSource[], projections: SqlSemanticProjection[], tokens: SqlSemanticToken[]): SqlSemanticScope {
return {
id: "root",
kind: statement.kind,
span: statement.span,
rowSources,
projections,
clauseSpans: clauseSpans(tokens),
};
}
export function buildSqlSemanticModel(sql: string, cursor: number, options: SqlSemanticBuildOptions = {}): SqlSemanticModel {
const safeCursor = Math.max(0, Math.min(cursor, sql.length));
const dialect = sqlSemanticDialectFor(options);
const allTokens = tokenizeSqlSemantic(sql);
const statementSpan = findActiveSqlStatementSpan(sql, allTokens, safeCursor);
const tokens = significantTokens(allTokens.filter((item) => item.span.end > statementSpan.start && item.span.start < statementSpan.end));
const kind = statementKind(tokens);
const statement: SqlSemanticStatement = {
kind,
span: statementSpan,
text: sql.slice(statementSpan.start, statementSpan.end),
};
const suppressed = isSuppressedSqlSemanticContext(allTokens, safeCursor);
const parseState: ParseState = { dialect, tokens, statement, cteSources: [] };
parseState.cteSources = parseCteSources(parseState);
const rowSources = parseRowSources(parseState);
const projections = parseSelectProjections(tokens, dialect);
const cursorIntent = buildCursorIntent(tokens, safeCursor, rowSources, dialect, suppressed, kind);
const scopes = [buildScope(statement, rowSources, projections, tokens)];
return {
databaseType: options.databaseType,
dialectId: dialect.id,
sql,
cursor: safeCursor,
statement,
tokens: allTokens,
scopes,
rowSources,
projections,
cursorIntent,
diagnostics: [],
};
}
@@ -0,0 +1,154 @@
import type { SqlCompletionReferencedTable } from "@/lib/sql/sqlCompletion";
import type { SqlSemanticModel, SqlSemanticRowSource, SqlSemanticSpan } from "@/lib/sql/semantic/types";
import type { SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
export type SqlSemanticTableReference = SqlTableReference & {
columns?: string[];
semanticSourceId?: string;
semanticSourceKind?: SqlSemanticRowSource["kind"];
};
export interface SqlSemanticNavigationTarget {
name: string;
schema?: string;
alias?: string;
columns?: string[];
source: SqlSemanticRowSource;
}
function offsetToLineColumn(sql: string, offset: number): { line: number; column: number } {
const safeOffset = Math.max(0, Math.min(offset, sql.length));
let line = 1;
let lineStart = 0;
for (let index = 0; index < safeOffset; index += 1) {
if (sql[index] === "\n") {
line += 1;
lineStart = index + 1;
}
}
return { line, column: safeOffset - lineStart };
}
function semanticSpanToSqlTextSpan(sql: string, span: SqlSemanticSpan): SqlTextSpan {
const start = offsetToLineColumn(sql, span.start);
const end = offsetToLineColumn(sql, Math.max(span.end, span.start));
return {
start_line: start.line,
start_column: start.column + 1,
end_line: end.line,
end_column: Math.max(end.column, start.column + 1),
};
}
function normalized(value: string | null | undefined): string {
let result = value ?? "";
while (result && '`"['.includes(result[0])) result = result.slice(1);
while (result && '`"]'.includes(result[result.length - 1])) result = result.slice(0, -1);
return result.toLowerCase();
}
function sourceSchema(source: SqlSemanticRowSource): string | undefined {
return source.metadataTarget?.schema ?? source.qualifierParts[source.qualifierParts.length - 1];
}
function sourceTableName(source: SqlSemanticRowSource): string {
return source.metadataTarget?.table ?? source.name;
}
function sourceReferenceKey(source: SqlSemanticTableReference): string {
return [normalized(source.schema), normalized(source.name), normalized(source.alias), source.span.start_line, source.span.start_column].join(":");
}
function existingScopeId(analysis: SqlReferenceAnalysis): number {
return analysis.tables[0]?.scope_id ?? analysis.columns[0]?.scope_id ?? 0;
}
export function sqlSemanticTableReferences(model: SqlSemanticModel, scopeId = 0): SqlSemanticTableReference[] {
if (model.cursorIntent.kind === "suppressed" || model.cursorIntent.confidence === "low") return [];
return model.rowSources
.filter((source) => source.kind !== "unknown")
.map((source) => {
const span = source.qualifiedName?.span ?? source.aliasSpan ?? source.sourceSpan;
const schema = sourceSchema(source);
return {
name: sourceTableName(source),
schema,
alias: source.alias,
span: semanticSpanToSqlTextSpan(model.sql, span),
scope_id: scopeId,
columns: source.columns?.length ? [...source.columns] : undefined,
semanticSourceId: source.id,
semanticSourceKind: source.kind,
};
});
}
export function mergeSqlSemanticReferenceAnalysis(analysis: SqlReferenceAnalysis, model: SqlSemanticModel): SqlReferenceAnalysis {
const semanticTables = sqlSemanticTableReferences(model, existingScopeId(analysis));
if (semanticTables.length === 0) return analysis;
const merged = new Map<string, SqlSemanticTableReference>();
for (const table of analysis.tables as SqlSemanticTableReference[]) {
merged.set(sourceReferenceKey(table), table);
}
for (const table of semanticTables) {
const existing = [...merged.values()].find((candidate) => {
if (normalized(candidate.name) !== normalized(table.name)) return false;
if (candidate.alias && table.alias && normalized(candidate.alias) !== normalized(table.alias)) return false;
if (candidate.schema && table.schema && normalized(candidate.schema) !== normalized(table.schema)) return false;
return true;
});
if (existing) {
existing.schema = existing.schema ?? table.schema;
existing.alias = existing.alias ?? table.alias;
existing.columns = existing.columns ?? table.columns;
existing.semanticSourceId = existing.semanticSourceId ?? table.semanticSourceId;
} else {
merged.set(sourceReferenceKey(table), table);
}
}
return {
...analysis,
tables: [...merged.values()],
};
}
export function sqlSemanticCompletionReferenceTables(model: SqlSemanticModel): SqlCompletionReferencedTable[] {
return sqlSemanticTableReferences(model).map((table) => ({
name: table.name,
schema: table.schema ?? undefined,
alias: table.alias ?? undefined,
columns: table.columns,
}));
}
export function resolveSqlSemanticNavigationTarget(model: SqlSemanticModel, identifierParts: readonly string[]): SqlSemanticNavigationTarget | null {
if (model.cursorIntent.kind === "suppressed" || model.cursorIntent.confidence === "low" || identifierParts.length === 0) return null;
const normalizedParts = identifierParts.map(normalized).filter(Boolean);
const name = normalizedParts[normalizedParts.length - 1];
const qualifier = normalizedParts.length > 1 ? normalizedParts[normalizedParts.length - 2] : undefined;
const source =
model.rowSources.find((candidate) => {
const candidateName = normalized(sourceTableName(candidate));
const candidateAlias = normalized(candidate.alias);
const candidateSchema = normalized(sourceSchema(candidate));
if (qualifier) {
if (candidateAlias && candidateAlias === qualifier) return true;
return candidateName === name && (!candidateSchema || candidateSchema === qualifier);
}
return candidateName === name || candidateAlias === name;
}) ?? null;
if (!source) return null;
const schema = sourceSchema(source);
return {
name: sourceTableName(source),
schema,
alias: source.alias,
columns: source.columns,
source,
};
}
+163
View File
@@ -0,0 +1,163 @@
import type { SqlSemanticSpan, SqlSemanticToken } from "@/lib/sql/semantic/types";
const WORD_START = /[A-Za-z_@$#]/;
const WORD_PART = /[A-Za-z0-9_@$#]/;
function token(kind: SqlSemanticToken["kind"], text: string, start: number, end: number, depth: number, quote?: string): SqlSemanticToken {
return {
kind,
text,
normalized: kind === "word" ? text.toLowerCase() : text,
span: { start, end },
depth,
quote,
};
}
function readQuoted(input: string, start: number, open: string, close: string): number {
let index = start + open.length;
while (index < input.length) {
if (input.startsWith(close, index)) {
if (input.startsWith(close + close, index)) {
index += close.length * 2;
continue;
}
return index + close.length;
}
index += 1;
}
return input.length;
}
export function tokenizeSqlSemantic(input: string): SqlSemanticToken[] {
const tokens: SqlSemanticToken[] = [];
let index = 0;
let depth = 0;
while (index < input.length) {
const start = index;
const ch = input[index] ?? "";
const next = input[index + 1] ?? "";
if (/\s/.test(ch)) {
index += 1;
continue;
}
if (ch === "-" && next === "-") {
index += 2;
while (index < input.length && input[index] !== "\n" && input[index] !== "\r") index += 1;
tokens.push(token("comment", input.slice(start, index), start, index, depth));
continue;
}
if (ch === "#") {
index += 1;
while (index < input.length && input[index] !== "\n" && input[index] !== "\r") index += 1;
tokens.push(token("comment", input.slice(start, index), start, index, depth));
continue;
}
if (ch === "/" && next === "*") {
index += 2;
while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) index += 1;
index = Math.min(input.length, index + (index < input.length ? 2 : 0));
tokens.push(token("comment", input.slice(start, index), start, index, depth));
continue;
}
if (ch === "'") {
index = readQuoted(input, start, "'", "'");
tokens.push(token("string", input.slice(start, index), start, index, depth, "'"));
continue;
}
if (ch === '"') {
index = readQuoted(input, start, '"', '"');
tokens.push(token("quoted_identifier", input.slice(start, index), start, index, depth, '"'));
continue;
}
if (ch === "`") {
index = readQuoted(input, start, "`", "`");
tokens.push(token("quoted_identifier", input.slice(start, index), start, index, depth, "`"));
continue;
}
if (ch === "[") {
index = readQuoted(input, start, "[", "]");
tokens.push(token("quoted_identifier", input.slice(start, index), start, index, depth, "["));
continue;
}
if (ch === ":" || ch === "?") {
index += 1;
while (index < input.length && WORD_PART.test(input[index] ?? "")) index += 1;
tokens.push(token("parameter", input.slice(start, index), start, index, depth));
continue;
}
if (/[0-9]/.test(ch)) {
index += 1;
while (index < input.length && /[0-9.]/.test(input[index] ?? "")) index += 1;
tokens.push(token("number", input.slice(start, index), start, index, depth));
continue;
}
if (WORD_START.test(ch)) {
index += 1;
while (index < input.length && WORD_PART.test(input[index] ?? "")) index += 1;
tokens.push(token("word", input.slice(start, index), start, index, depth));
continue;
}
if ("(),.;*".includes(ch)) {
if (ch === ")") depth = Math.max(0, depth - 1);
tokens.push(token("punctuation", ch, start, start + 1, depth));
if (ch === "(") depth += 1;
index += 1;
continue;
}
index += 1;
tokens.push(token("operator", ch, start, index, depth));
}
return tokens;
}
export function tokenContainsPosition(tokenValue: SqlSemanticToken, position: number): boolean {
return tokenValue.span.start <= position && position <= tokenValue.span.end;
}
export function isSuppressedSqlSemanticContext(tokens: readonly SqlSemanticToken[], cursor: number): boolean {
return tokens.some((item) => (item.kind === "comment" || item.kind === "string") && item.span.start < cursor && cursor <= item.span.end);
}
export function findActiveSqlStatementSpan(sql: string, tokens: readonly SqlSemanticToken[], cursor: number): SqlSemanticSpan {
let start = 0;
let end = sql.length;
for (const item of tokens) {
if (item.kind !== "punctuation" || item.text !== ";" || item.depth !== 0) continue;
if (item.span.end <= cursor) start = item.span.end;
if (item.span.start >= cursor) {
end = item.span.start;
break;
}
}
while (start < end && /\s/.test(sql[start] ?? "")) start += 1;
while (end > start && /\s/.test(sql[end - 1] ?? "")) end -= 1;
return { start, end };
}
export function unquoteSqlSemanticIdentifier(tokenValue: SqlSemanticToken): string {
if (tokenValue.kind !== "quoted_identifier") return tokenValue.text;
if (tokenValue.quote === "[") return tokenValue.text.slice(1, -1).replaceAll("]]", "]");
const quote = tokenValue.quote ?? tokenValue.text[0] ?? "";
return tokenValue.text.slice(1, -1).replaceAll(quote + quote, quote);
}
export function tokenIsIdentifier(tokenValue: SqlSemanticToken | undefined): tokenValue is SqlSemanticToken {
return !!tokenValue && (tokenValue.kind === "word" || tokenValue.kind === "quoted_identifier");
}
+130
View File
@@ -0,0 +1,130 @@
import type { DatabaseType } from "@/types/database";
export interface SqlSemanticSpan {
start: number;
end: number;
}
export type SqlSemanticConfidence = "high" | "medium" | "low";
export type SqlSemanticStatementKind = "select" | "insert" | "update" | "delete" | "call" | "unknown";
export type SqlSemanticTokenKind = "word" | "quoted_identifier" | "string" | "number" | "comment" | "parameter" | "punctuation" | "operator";
export interface SqlSemanticToken {
kind: SqlSemanticTokenKind;
text: string;
normalized: string;
span: SqlSemanticSpan;
depth: number;
quote?: string;
}
export interface SqlSemanticIdentifierPart {
raw: string;
name: string;
span: SqlSemanticSpan;
quote?: string;
}
export interface SqlSemanticQualifiedName {
parts: SqlSemanticIdentifierPart[];
span: SqlSemanticSpan;
}
export type SqlSemanticRowSourceKind = "table" | "cte" | "subquery" | "table_function" | "mutation_target" | "unknown";
export interface SqlSemanticMetadataTarget {
database?: string;
schema?: string;
table?: string;
packageName?: string;
}
export interface SqlSemanticProjection {
name: string;
sourceExpression: string;
span: SqlSemanticSpan;
alias?: string;
aliasSpan?: SqlSemanticSpan;
}
export interface SqlSemanticRowSource {
id: string;
kind: SqlSemanticRowSourceKind;
name: string;
qualifiedName?: SqlSemanticQualifiedName;
qualifierParts: string[];
alias?: string;
aliasSpan?: SqlSemanticSpan;
sourceSpan: SqlSemanticSpan;
columns?: string[];
metadataTarget?: SqlSemanticMetadataTarget;
unresolved?: boolean;
}
export interface SqlSemanticClauseSpans {
select?: SqlSemanticSpan;
from?: SqlSemanticSpan;
where?: SqlSemanticSpan;
groupBy?: SqlSemanticSpan;
having?: SqlSemanticSpan;
orderBy?: SqlSemanticSpan;
limit?: SqlSemanticSpan;
insertColumns?: SqlSemanticSpan;
updateSet?: SqlSemanticSpan;
}
export interface SqlSemanticScope {
id: string;
kind: SqlSemanticStatementKind | "subquery" | "cte";
span: SqlSemanticSpan;
parentId?: string;
rowSources: SqlSemanticRowSource[];
projections: SqlSemanticProjection[];
clauseSpans: SqlSemanticClauseSpans;
}
export type SqlSemanticCursorKind = "table" | "schema" | "catalog" | "routine" | "column" | "alias_column" | "insert_column" | "update_column" | "delete_target" | "join_condition" | "star" | "keyword" | "suppressed";
export interface SqlSemanticCursorIntent {
kind: SqlSemanticCursorKind;
prefix: string;
replacementRange: SqlSemanticSpan;
qualifierParts: string[];
targetSourceId?: string;
expectedObjectKinds: Array<"database" | "schema" | "table" | "view" | "routine" | "procedure" | "function" | "column">;
confidence: SqlSemanticConfidence;
fallbackReason?: string;
}
export interface SqlSemanticStatement {
kind: SqlSemanticStatementKind;
span: SqlSemanticSpan;
text: string;
}
export interface SqlSemanticDiagnostic {
message: string;
span: SqlSemanticSpan;
severity: "info" | "warning" | "error";
}
export interface SqlSemanticModel {
databaseType?: DatabaseType;
dialectId: string;
sql: string;
cursor: number;
statement: SqlSemanticStatement;
tokens: SqlSemanticToken[];
scopes: SqlSemanticScope[];
rowSources: SqlSemanticRowSource[];
projections: SqlSemanticProjection[];
cursorIntent: SqlSemanticCursorIntent;
diagnostics: SqlSemanticDiagnostic[];
}
export interface SqlSemanticBuildOptions {
databaseType?: DatabaseType;
dialect?: "mysql" | "postgres" | "sqlserver";
}