import { sqlSemanticDialectFor, type SqlSemanticDialectAdapter } from "@/lib/sql/semantic/dialect"; import { findActiveSqlStatementSpan, isSuppressedSqlSemanticContext, tokenIsIdentifier, tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens"; import { resolveSqlStatementWindow } from "@/lib/sql/sqlSyntaxTreeWindow"; import type { SqlSemanticBuildOptions, SqlSemanticClauseSpans, SqlSemanticCteOutputColumn, SqlSemanticCteStar, SqlSemanticCursorIntent, SqlSemanticIdentifierPart, SqlSemanticModel, SqlSemanticProjection, SqlSemanticQualifiedName, SqlSemanticRowSource, SqlSemanticScope, SqlSemanticSpan, SqlSemanticStatement, SqlSemanticStatementKind, SqlSemanticToken, } from "@/lib/sql/semantic/types"; const TABLE_INTRODUCERS = new Set(["from", "join", "straight_join", "update", "into", "using", "apply"]); const TABLE_FUNCTION_NAMES = new Set(["table", "xmltable", "json_table", "the", "read_csv", "read_parquet", "read_json", "unnest"]); 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 FROM_CLAUSE_BOUNDARIES = new Set([...CLAUSE_BOUNDARIES, "window", "qualify", "fetch", "for", "connect", "start", "model"].filter((item) => item !== "on")); const ALIAS_BLACKLIST = new Set([...FROM_CLAUSE_BOUNDARIES, "on", "join", "straight_join", "left", "right", "inner", "outer", "cross", "full", "natural", "as", "select", "from", "with"]); const TABLE_TARGET_MODIFIERS = new Set(["lateral", "only"]); const TABLE_FUNCTION_INTRODUCERS = new Set(["from", "join", "straight_join", "apply"]); const TOP_LEVEL_STATEMENT_WORDS = new Set(["select", "insert", "delete", "merge", "create", "alter", "drop", "truncate", "call", "exec", "execute", "grant", "revoke"]); const SQLSERVER_DEFAULT_SCHEMA = "dbo"; const SQLSERVER_UPDATE_STATISTICS_SCOPES = new Set(["all", "index", "table"]); // CTE navigation enrichment (body projection origins + body row sources) is best-effort; on very // large statements it is skipped entirely so click/hover parsing stays cheap. const CTE_ENRICHMENT_STATEMENT_TOKEN_LIMIT = 20_000; const CTE_ENRICHMENT_BODY_TOKEN_LIMIT = 5_000; interface ParseState { dialect: SqlSemanticDialectAdapter; tokens: SqlSemanticToken[]; statement: SqlSemanticStatement; cteSources: SqlSemanticRowSource[]; expandGroupedSources?: boolean; groupedSourceScopes?: SqlSemanticGroupedSourceScope[]; /** Enrich derived-table row sources parsed from this state (one level, CTE bodies only). */ enrichDerivedTables?: boolean; } export interface SqlSemanticGroupedSourceScope { span: SqlSemanticSpan; depth: number; sources: SqlSemanticRowSource[]; } interface QuerySourceRange { depth: number; startIndex: number; endIndex: number; } 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 (dialect.id === "sqlserver" && tokens[index]?.text === ".") { const omittedSchema = tokens[index]; parts.push({ raw: "", name: SQLSERVER_DEFAULT_SCHEMA, span: omittedSchema.span }); while (tokens[index]?.text === ".") index += 1; } if (!tokenIsIdentifier(tokens[index])) return null; } 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 sqlServerMaintenanceTableTarget(tokens: readonly SqlSemanticToken[], target: number, introducer: string, dialect: SqlSemanticDialectAdapter): number { if (dialect.id !== "sqlserver" || introducer !== "update") return target; if (tokens[target]?.normalized === "statistics") return target + 1; // ASE accepts UPDATE {ALL | INDEX | TABLE} STATISTICS; these scope words // describe the maintenance operation and must never become table targets. if (SQLSERVER_UPDATE_STATISTICS_SCOPES.has(tokens[target]?.normalized ?? "") && tokens[target + 1]?.normalized === "statistics") return target + 2; return target; } function updateIntroducesMutationTarget(tokens: readonly SqlSemanticToken[], updateIndex: number): boolean { const update = tokens[updateIndex]; if (update?.kind !== "word" || update.normalized !== "update") return false; for (let index = updateIndex - 1; index >= 0; index -= 1) { const item = tokens[index]; if (!item || item.depth !== update.depth) continue; if (item.text === ";") break; if (item.kind === "word" && (item.normalized === "update" || TOP_LEVEL_STATEMENT_WORDS.has(item.normalized))) return false; } return true; } /** * Finds concrete table-name tokens for visual highlighting without consulting * metadata. Only the final identifier in a qualified name is returned, so * schemas/catalogs and aliases keep the regular identifier color. */ export function sqlSemanticTableNameSpans(sql: string, options: SqlSemanticBuildOptions = {}): SqlSemanticSpan[] { const dialect = sqlSemanticDialectFor(options); const tokens = significantTokens(tokenizeSqlSemantic(sql, dialect.id)); const spans: SqlSemanticSpan[] = []; const seen = new Set(); const commaContinuesTableListByDepth = new Map(); for (let index = 0; index < tokens.length; index += 1) { const item = tokens[index]; if (!item) continue; const commaContinuesTableList = item.text === "," && commaContinuesTableListByDepth.get(item.depth) === true; if (item.kind === "word") { if (item.normalized === "from") commaContinuesTableListByDepth.set(item.depth, true); else if (item.normalized === "select" || item.normalized === "join" || TABLE_INTRODUCERS.has(item.normalized) || CLAUSE_BOUNDARIES.has(item.normalized)) commaContinuesTableListByDepth.set(item.depth, false); } const introduced = item?.kind === "word" && TABLE_INTRODUCERS.has(item.normalized) && (item.normalized !== "update" || updateIntroducesMutationTarget(tokens, index)); if (!introduced && !commaContinuesTableList) continue; let target = index + 1; while (TABLE_TARGET_MODIFIERS.has(tokens[target]?.normalized ?? "")) target += 1; target = sqlServerMaintenanceTableTarget(tokens, target, item?.normalized ?? "", dialect); if (tokens[target]?.text === "(") continue; const qualified = readQualifiedName(tokens, target, dialect); const followedByParenthesis = qualified && tokens[qualified.nextIndex]?.text === "("; if (!qualified || tokens[target]?.depth !== item?.depth || (followedByParenthesis && (!introduced || TABLE_FUNCTION_INTRODUCERS.has(item.normalized)))) continue; const tablePart = qualified.name.parts[qualified.name.parts.length - 1]; if (!tablePart) continue; const key = `${tablePart.span.start}:${tablePart.span.end}`; if (seen.has(key)) continue; seen.add(key); spans.push(tablePart.span); } return spans; } 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 !== "*"); } /** * Analyzes one comma-separated CTE body SELECT projection for navigation metadata: * a bare/qualified star or a named output with an optional plain-column origin. */ function cteBodyProjectionShape(state: ParseState, group: readonly SqlSemanticToken[]): { output?: SqlSemanticCteOutputColumn; star?: SqlSemanticCteStar } | null { const useful = group.filter((item) => item.kind !== "comment"); if (useful.length === 0) return null; const dialect = state.dialect; // Bare `*` if (useful.length === 1 && useful[0]?.text === "*") { return { star: { starSpan: useful[0].span, qualifierParts: [] } }; } // Qualified `q.*` (possibly a multi-part `a.b.*`) — the whole group must be the star chain. const last = useful[useful.length - 1]; if (last?.text === "*" && useful[useful.length - 2]?.text === ".") { const qualifierParts: string[] = []; let cursor = useful.length - 3; let valid = cursor >= 0; while (valid && cursor >= 0) { const identifier = useful[cursor]; if (!identifier || !tokenIsIdentifier(identifier)) { valid = false; break; } qualifierParts.unshift(identifierPart(identifier, dialect).name); cursor -= 1; if (cursor < 0) break; if (useful[cursor]?.text !== ".") { valid = false; break; } cursor -= 1; } if (valid && qualifierParts.length > 0) { return { star: { starSpan: last.span, qualifierParts } }; } } const projection = projectionNameFromTokens(useful, dialect); if (!projection || projection.name === "*") return null; // `expression AS alias` — navigate to the alias token; the expression itself is not traceable. if (projection.aliasSpan) { return { output: { name: projection.name, jumpSpan: projection.aliasSpan } }; } // Plain `col` / `q.col` chain (identifiers separated only by dots) — traceable to a body source. let isIdentifierChain = useful.length % 2 === 1; for (let index = 0; isIdentifierChain && index < useful.length; index += 1) { const token = useful[index]; if (!token) { isIdentifierChain = false; break; } if (index % 2 === 0) { if (!tokenIsIdentifier(token)) isIdentifierChain = false; } else if (token.text !== ".") { isIdentifierChain = false; } } if (isIdentifierChain) { const identifiers = useful.filter((_, index) => index % 2 === 0); const columnToken = identifiers[identifiers.length - 1]; if (columnToken) { return { output: { name: identifierPart(columnToken, dialect).name, jumpSpan: columnToken.span, origin: { qualifierParts: identifiers.slice(0, -1).map((token) => identifierPart(token, dialect).name), column: identifierPart(columnToken, dialect).name, }, }, }; } } // Any other expression without an alias — select the whole projection span, no origin. return { output: { name: projection.name, jumpSpan: projection.span } }; } /** * CTE-body counterpart of parseSelectProjections that keeps stars and per-projection spans * instead of flattening everything to bare column names. */ function parseCteBodyProjectionShapes(state: ParseState, bodyTokens: readonly SqlSemanticToken[]): { outputs: SqlSemanticCteOutputColumn[]; stars: SqlSemanticCteStar[] } { const outputs: SqlSemanticCteOutputColumn[] = []; const stars: SqlSemanticCteStar[] = []; const baseDepth = bodyTokens.reduce((min, item) => Math.min(min, item.depth), Number.POSITIVE_INFINITY); if (!Number.isFinite(baseDepth)) return { outputs, stars }; const selectDepth = baseDepth; const selectIndex = bodyTokens.findIndex((item) => item.depth === selectDepth && item.kind === "word" && item.normalized === "select"); if (selectIndex < 0) return { outputs, stars }; let fromIndex = bodyTokens.findIndex((item, index) => index > selectIndex && item.depth === selectDepth && item.kind === "word" && item.normalized === "from"); if (fromIndex < 0) fromIndex = bodyTokens.length; const projectionTokens = bodyTokens.slice(selectIndex + 1, fromIndex); for (const group of splitTopLevelByComma(projectionTokens)) { const shape = cteBodyProjectionShape(state, group); if (shape?.output) outputs.push(shape.output); if (shape?.star) stars.push(shape.star); } return { outputs, stars }; } /** Parses row sources visible inside one CTE body; preceding CTEs are in scope for chaining. */ function parseCteBodySources(state: ParseState, bodyTokens: readonly SqlSemanticToken[], precedingCtes: readonly SqlSemanticRowSource[]): SqlSemanticRowSource[] { if (bodyTokens.length === 0) return []; let bodyDepth = Number.POSITIVE_INFINITY; for (const token of bodyTokens) bodyDepth = Math.min(bodyDepth, token.depth); if (!Number.isFinite(bodyDepth)) return []; const first = bodyTokens.find((token) => token.depth === bodyDepth); const last = bodyTokens[bodyTokens.length - 1]; if (!first || !last) return []; const bodyState: ParseState = { ...state, tokens: bodyTokens as SqlSemanticToken[], statement: { kind: "select", span: { start: first.span.start, end: last.span.end }, text: state.statement.text, }, cteSources: precedingCtes as SqlSemanticRowSource[], // Body parsing must not leak grouped-join scopes into the outer statement parse. groupedSourceScopes: state.groupedSourceScopes ? [] : undefined, }; return parseRowSourcesAtDepth(bodyState, bodyDepth); } 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; const enrich = tokens.length <= CTE_ENRICHMENT_STATEMENT_TOKEN_LIMIT; 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: Array<{ name: string; span: SqlSemanticSpan }> = []; 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({ name: identifierPart(identifier, state.dialect).name, span: identifier.span }); } 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); // columns: unchanged flat name list consumed by completion/intention logic. const bodyColumns = explicitColumns.length > 0 ? explicitColumns.map((column) => column.name) : parseSelectProjections(bodyTokens, state.dialect).map((projection) => projection.name); let cteOutputs: SqlSemanticCteOutputColumn[] | undefined; let cteStars: SqlSemanticCteStar[] | undefined; let bodySources: SqlSemanticRowSource[] | undefined; if (enrich && bodyTokens.length <= CTE_ENRICHMENT_BODY_TOKEN_LIMIT) { const shapes = parseCteBodyProjectionShapes(state, bodyTokens); cteStars = shapes.stars; if (explicitColumns.length > 0) { // Explicit `WITH c(a,b)` names win; body projection origins are inherited positionally // so hover comments can still trace through the list, while navigation lands on list items. cteOutputs = explicitColumns.map((column, columnIndex) => ({ name: column.name, jumpSpan: column.span, ...(shapes.outputs[columnIndex]?.origin ? { origin: shapes.outputs[columnIndex]!.origin } : {}), })); } else { cteOutputs = shapes.outputs; } // Derived tables in the body (12-branch `FROM (SELECT ...) X` bodies and friends) receive one // level of enrichment so CTE lineage can keep tracing down to the physical tables inside them. bodySources = parseCteBodySources({ ...state, enrichDerivedTables: true }, bodyTokens, sources); } 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, nameSpan: nameToken.span, bodySpan: { start: tokens[bodyOpen]?.span.start ?? nameToken.span.start, end: tokens[safeBodyClose]?.span.end ?? nameToken.span.end }, ...(cteOutputs ? { cteOutputs } : {}), ...(cteStars ? { cteStars } : {}), ...(bodySources ? { bodySources } : {}), }); index = safeBodyClose + 1; if (tokens[index]?.text !== ",") break; index += 1; } return sources; } function correlationColumnsAfter(tokens: readonly SqlSemanticToken[], index: number, dialect: SqlSemanticDialectAdapter): { columns: string[]; nextIndex: number } | null { if (tokens[index]?.text !== "(") return null; const close = findMatchingParenToken(tokens, index); if (close < 0) return null; const columns = splitTopLevelByComma(tokens.slice(index + 1, close)) .map((group) => group.find(tokenIsIdentifier)) .filter((item): item is SqlSemanticToken => item != null) .map((item) => identifierPart(item, dialect).name); return { columns, nextIndex: close + 1 }; } function aliasAfter(tokens: readonly SqlSemanticToken[], index: number, dialect: SqlSemanticDialectAdapter, options: { allowCorrelationColumns?: boolean } = {}): { alias?: string; aliasSpan?: SqlSemanticSpan; columns?: string[]; 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())) { const columns = options.allowCorrelationColumns === false ? null : correlationColumnsAfter(tokens, cursor + 1, dialect); return { alias, aliasSpan: aliasToken.span, columns: columns?.columns, nextIndex: columns?.nextIndex ?? cursor + 1 }; } } return { nextIndex: index }; } function mergeColumnAliases(columns: readonly string[], aliases: readonly string[] | undefined): string[] { if (!aliases?.length) return [...columns]; if (columns.length === 0) return [...aliases]; return columns.map((column, index) => aliases[index] ?? column); } /** * Derived tables can rename the inline-view outputs (`FROM (SELECT id FROM t) x(a)`): the outer * query sees `x.a` while the body still projects `id`. The rename list is positional, so it can * only be applied when the body projects names one-for-one — no stars and equal lengths. Otherwise * the outputs are returned untouched, because a positional rename against a star or a shorter list * would point `x.a` at an unrelated column. `origin` / `jumpSpan` are intentionally kept: both * still describe the body side, which is where tracing and Ctrl+click must continue. */ function applyDerivedTableColumnAliases(outputs: SqlSemanticCteOutputColumn[], stars: readonly SqlSemanticCteStar[], aliases: readonly string[] | undefined): SqlSemanticCteOutputColumn[] { if (!aliases?.length || stars.length > 0 || aliases.length !== outputs.length) return outputs; return outputs.map((output, index) => ({ ...output, name: aliases[index] ?? output.name })); } function consumeSqlServerTableHint(tokens: readonly SqlSemanticToken[], index: number, dialect: SqlSemanticDialectAdapter): number { if (dialect.id !== "sqlserver") return index; const openIndex = tokens[index]?.normalized === "with" && tokens[index + 1]?.text === "(" ? index + 1 : index; if (tokens[openIndex]?.text !== "(") return index; const close = findMatchingParenToken(tokens, openIndex); return close < 0 ? index : close + 1; } 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 = mergeColumnAliases( parseSelectProjections(bodyTokens, state.dialect).map((projection) => projection.name), alias.columns, ); // A derived table reached from a CTE body carries the same body metadata a CTE definition does, // so `followBodySource` can resolve its projections/stars and trace one level into the inline // view. Derived tables outside CTE bodies stay metadata-free (the common path stays cheap). let body: Pick = {}; if (state.enrichDerivedTables && bodyTokens.length <= CTE_ENRICHMENT_BODY_TOKEN_LIMIT) { const shapes = parseCteBodyProjectionShapes(state, bodyTokens); body = { // Outer column aliases rename the outputs so `x.a` resolves; tracing stays on the body side. cteOutputs: applyDerivedTableColumnAliases(shapes.outputs, shapes.stars, alias.columns), cteStars: shapes.stars, // Enrichment stops here: derived tables nested inside this inline view are not parsed again, // so depth can never re-parse arbitrarily nested views on every model build. bodySources: parseCteBodySources({ ...state, enrichDerivedTables: false }, bodyTokens, state.cteSources), }; } 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: state.tokens[alias.nextIndex - 1]?.span.end ?? alias.aliasSpan?.end ?? state.tokens[close]?.span.end ?? 0 }, columns, ...body, }, nextIndex: alias.nextIndex, }; } function parseTableFunctionSource(state: ParseState, nameIndex: number, introducer: string, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null { const isMutationTarget = introducer === "update" || introducer === "into" || (state.statement.kind === "delete" && introducer === "from"); if (isMutationTarget) return null; const qualified = readQualifiedName(state.tokens, nameIndex, state.dialect); if (!qualified || state.tokens[qualified.nextIndex]?.text !== "(") return null; const { name, qualifierParts } = sourceNameFromQualifiedName(qualified.name); if (state.dialect.id !== "postgres" && !TABLE_FUNCTION_NAMES.has(name.toLowerCase())) return null; const close = findMatchingParenToken(state.tokens, qualified.nextIndex); const safeClose = close < 0 ? qualified.nextIndex : close; let aliasIndex = safeClose + 1; if (state.dialect.id === "postgres" && state.tokens[aliasIndex]?.normalized === "with" && state.tokens[aliasIndex + 1]?.normalized === "ordinality") aliasIndex += 2; const alias = aliasAfter(state.tokens, aliasIndex, state.dialect); const sourceName = alias.alias ?? name; return { source: { id: `${introducer}:table_function:${sourceIndex}`, kind: "table_function", name: sourceName, qualifiedName: qualified.name, qualifierParts, alias: alias.alias, aliasSpan: alias.aliasSpan, sourceSpan: { start: qualified.name.span.start, end: state.tokens[alias.nextIndex - 1]?.span.end ?? alias.aliasSpan?.end ?? state.tokens[safeClose]?.span.end ?? qualified.name.span.end }, columns: alias.columns, 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, { allowCorrelationColumns: state.dialect.id !== "sqlserver" }); const nextIndex = consumeSqlServerTableHint(state.tokens, alias.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: state.tokens[nextIndex - 1]?.span.end ?? alias.aliasSpan?.end ?? qualified.name.span.end }, columns: cte?.columns ? mergeColumnAliases(cte.columns, alias.columns) : undefined, columnAliases: alias.columns, metadataTarget: { database: qualifierParts.length >= 2 ? qualifierParts[qualifierParts.length - 2] : undefined, schema: qualifierParts[qualifierParts.length - 1], table: name, }, }; return { source, nextIndex }; } function isPostgresLateralSource(state: ParseState, target: number, introducer: string): boolean { if (state.dialect.id !== "postgres" || state.tokens[target]?.normalized !== "lateral" || (introducer !== "from" && introducer !== "join")) return false; const sourceIndex = target + 1; if (state.tokens[sourceIndex]?.text === "(") return true; const qualified = readQualifiedName(state.tokens, sourceIndex, state.dialect); return !!qualified && state.tokens[qualified.nextIndex]?.text === "("; } function parseRowSource(state: ParseState, target: number, introducer: string, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null { if (isPostgresLateralSource(state, target, introducer)) target += 1; if (state.tokens[target]?.text === "(") return parseSubquerySource(state, target, introducer, sourceIndex); return parseTableFunctionSource(state, target, introducer, sourceIndex) ?? parseTableSource(state, target, introducer, sourceIndex); } function parseRowSourceList(state: ParseState, target: number, introducer: string, sourceIndex: number): { sources: SqlSemanticRowSource[]; nextIndex: number } | null { const open = state.tokens[target]; if (state.expandGroupedSources && open?.text === "(" && open.depth < 128) { const close = findMatchingParenToken(state.tokens, target); const first = state.tokens[target + 1]; const isQuery = first?.kind === "word" && (first.normalized === "select" || first.normalized === "with"); if (close >= 0 && !isQuery) { // An unaliased parenthesized join is transparent to the surrounding query scope. // A SELECT body or an explicitly aliased group must retain its own boundary. const from: SqlSemanticToken = { ...open, kind: "word", text: "from", normalized: "from", depth: open.depth + 1 }; const sources = parseRowSourcesAtDepth({ ...state, tokens: [from, ...state.tokens.slice(target + 1, close)] }, from.depth, sourceIndex); if (!aliasAfter(state.tokens, close + 1, state.dialect).alias) return { sources, nextIndex: close + 1 }; state.groupedSourceScopes?.push({ span: { start: open.span.end, end: state.tokens[close].span.start }, depth: from.depth, sources }); } } const parsed = parseRowSource(state, target, introducer, sourceIndex); return parsed ? { sources: [parsed.source], nextIndex: parsed.nextIndex } : null; } function parseDorisLateralView(state: ParseState, index: number, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null { if (state.dialect.id !== "doris" || state.tokens[index]?.normalized !== "lateral" || state.tokens[index + 1]?.normalized !== "view") return null; // Doris's `LATERAL VIEW [OUTER] fn(...) alias AS col` -- skip the optional OUTER marker so the // OUTER form models the same function columns as the plain form. let functionIndex = index + 2; if (state.tokens[functionIndex]?.normalized === "outer") functionIndex += 1; const functionName = readQualifiedName(state.tokens, functionIndex, state.dialect); if (!functionName || state.tokens[functionName.nextIndex]?.text !== "(") return null; const close = findMatchingParenToken(state.tokens, functionName.nextIndex); if (close < 0) return null; let aliasIndex = close + 1; if (state.tokens[aliasIndex]?.normalized === "as") aliasIndex += 1; const alias = state.tokens[aliasIndex]; if (!alias || alias.kind !== "word") return null; let columnIndex = aliasIndex + 1; if (state.tokens[columnIndex]?.normalized === "as") columnIndex += 1; const columns: string[] = []; while (state.tokens[columnIndex]?.kind === "word") { columns.push(state.tokens[columnIndex].text); columnIndex += 1; if (state.tokens[columnIndex]?.text !== ",") break; columnIndex += 1; } const endToken = state.tokens[Math.max(aliasIndex, columnIndex - 1)] ?? alias; const name = alias.text; return { source: { id: `table-function:${sourceIndex}:${name}`, kind: "table_function", name, alias: name, qualifierParts: [name], qualifiedName: functionName.name, sourceSpan: { start: state.tokens[index].span.start, end: endToken.span.end }, columns: columns.length ? columns : undefined, }, nextIndex: columnIndex, }; } function parseRowSourcesAtDepth(state: ParseState, sourceDepth: number, sourceIndexOffset = 0): SqlSemanticRowSource[] { const sources: SqlSemanticRowSource[] = []; let inSelectFromClause = false; for (let index = 0; index < state.tokens.length; index += 1) { const item = state.tokens[index]; if (!item) continue; if (item.depth !== sourceDepth) continue; if (item.kind === "word") { if (state.statement.kind === "select" && item.normalized === "from") inSelectFromClause = true; else if (inSelectFromClause && FROM_CLAUSE_BOUNDARIES.has(item.normalized)) inSelectFromClause = false; } if (inSelectFromClause && item.text === ",") { const parsed = parseRowSourceList(state, index + 1, "from", sourceIndexOffset + sources.length); if (parsed) { sources.push(...parsed.sources); index = parsed.nextIndex - 1; } continue; } if (item.kind !== "word") continue; const normalized = item.normalized; if (!TABLE_INTRODUCERS.has(normalized)) continue; if (normalized === "update" && !updateIntroducesMutationTarget(state.tokens, index)) continue; if (JOIN_MODIFIERS.has(normalized)) continue; let target = index + 1; while (JOIN_MODIFIERS.has(state.tokens[target]?.normalized ?? "")) target += 1; target = sqlServerMaintenanceTableTarget(state.tokens, target, normalized, state.dialect); for (;;) { const parsed = parseRowSourceList(state, target, normalized, sourceIndexOffset + sources.length); if (!parsed) break; sources.push(...parsed.sources); index = parsed.nextIndex - 1; // Track the position past any LATERAL VIEW clauses so the FROM-list separator check sees // the real comma after them (parsed.nextIndex points at the first "lateral" token itself). let afterSources = parsed.nextIndex; let lateral = parseDorisLateralView(state, afterSources, sourceIndexOffset + sources.length); while (lateral) { sources.push(lateral.source); index = lateral.nextIndex - 1; afterSources = lateral.nextIndex; lateral = parseDorisLateralView(state, afterSources, sourceIndexOffset + sources.length); } const separator = state.tokens[afterSources]; if (normalized !== "from" || separator?.text !== "," || separator.depth !== sourceDepth) break; target = afterSources + 1; } } return dedupeSources(sources); } /** Parse declarations in one query block without merging outer or sibling sources. */ export function sqlSemanticQueryBlockSources(tokens: SqlSemanticToken[], options: SqlSemanticBuildOptions = {}, groupedSourceScopes?: SqlSemanticGroupedSourceScope[]): SqlSemanticRowSource[] { if (!tokens.length) return []; const statement: SqlSemanticStatement = { kind: statementKind(tokens), span: { start: tokens[0].span.start, end: tokens[tokens.length - 1].span.end }, text: "", }; return parseRowSourcesAtDepth({ dialect: sqlSemanticDialectFor(options), tokens, statement, cteSources: [], expandGroupedSources: true, groupedSourceScopes }, tokens[0].depth); } function querySourceRanges(tokens: readonly SqlSemanticToken[], cursor: number): QuerySourceRange[] { const rootDepth = tokens.reduce((min, item) => Math.min(min, item.depth), Number.POSITIVE_INFINITY); const fallbackDepth = Number.isFinite(rootDepth) ? rootDepth : 0; const cursorDepth = [...tokens].reverse().find((item) => item.span.start < cursor)?.depth ?? fallbackDepth; const starts = new Map(); for (let index = 0; index < tokens.length; index += 1) { const item = tokens[index]; if (!item || item.span.start >= cursor) break; if (item.kind === "word" && item.normalized === "select" && item.depth <= cursorDepth) { starts.set(item.depth, index); } } if (!starts.has(fallbackDepth)) starts.set(fallbackDepth, 0); return [...starts.entries()] .sort(([left], [right]) => right - left) .map(([depth, startIndex]) => { let endIndex = tokens.length; if (depth > fallbackDepth) { for (let index = startIndex + 1; index < tokens.length; index += 1) { if ((tokens[index]?.depth ?? depth) < depth) { endIndex = index; break; } } } return { depth, startIndex, endIndex }; }); } function parseRowSources(state: ParseState, cursor: number): SqlSemanticRowSource[] { const ranges = querySourceRanges(state.tokens, cursor); const sources: SqlSemanticRowSource[] = []; for (const range of ranges) { const scopedState = { ...state, tokens: state.tokens.slice(range.startIndex, range.endIndex), }; sources.push(...parseRowSourcesAtDepth(scopedState, range.depth, sources.length)); } return dedupeSources([...sources, ...state.cteSources]); } function dedupeSources(sources: SqlSemanticRowSource[]): SqlSemanticRowSource[] { const seen = new Set(); 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; let hasQualifier = false; 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 === ".") { hasQualifier = true; index -= 1; } } else if (last.text === ".") { hasQualifier = true; index -= 1; } const qualifierParts: string[] = []; if (hasQualifier) { 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 !== "comment"); return nearestPreviousSyntaxWord(before); } function wordBeforePosition(tokens: readonly SqlSemanticToken[], position: number): string { const before = tokens.filter((item) => item.span.end <= position && item.kind !== "comment"); return nearestPreviousSyntaxWord(before); } function wordBeforeTrailingIdentifier(tokens: readonly SqlSemanticToken[], cursor: number, trailing: TrailingIdentifier): string { const before = tokens.filter((item) => item.span.end <= cursor && item.kind !== "comment"); let index = before.length - 1; let identifiersToSkip = trailing.qualifierParts.length + (trailing.prefix ? 1 : 0); while (index >= 0 && identifiersToSkip > 0) { if (before[index]?.text === ".") { index -= 1; continue; } if (!tokenIsIdentifier(before[index])) break; identifiersToSkip -= 1; index -= 1; } return nearestPreviousSyntaxWord(before.slice(0, index + 1)); } /** * Scans backward from the end of `before` for the nearest unquoted syntax word. * A quoted identifier is a barrier: its object name must not participate in * keyword comparisons even when it is named `from`, `join`, or `update`. */ function nearestPreviousSyntaxWord(tokens: readonly SqlSemanticToken[]): string { for (let index = tokens.length - 1; index >= 0; index -= 1) { const item = tokens[index]; if (!item) continue; if (item.kind === "word") return item.normalized; if (item.kind === "quoted_identifier") return ""; if (item.text === "." || item.kind === "comment") continue; break; } return ""; } function isTableListContinuation(tokens: readonly SqlSemanticToken[], position: number): boolean { const before = tokens.filter((item) => item.span.end <= position && item.kind !== "comment"); let commaIndex = before.length - 1; while (commaIndex >= 0 && (tokenIsIdentifier(before[commaIndex]) || before[commaIndex]?.text === ".")) commaIndex -= 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 !== ".") return []; 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 wordBeforeTrailing = wordBeforeTrailingIdentifier(tokens, cursor, trailing); 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) || TABLE_INTRODUCERS.has(wordBeforeTrailing) || (!!targetSource && !targetSource.alias && trailing.replacementRange.start >= targetSource.sourceSpan.start && trailing.replacementRange.start <= targetSource.sourceSpan.end + 1 && 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) || TABLE_INTRODUCERS.has(wordBeforeReplacement) || TABLE_INTRODUCERS.has(wordBeforeTrailing) || previous === "from" || previous === "join" || wordBeforeReplacement === "from" || wordBeforeReplacement === "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 window = resolveSqlStatementWindow(sql, safeCursor, options.editorState, dialect.id); const windowSql = sql.slice(window.from, window.to); const windowCursor = safeCursor - window.from; const localTokens = tokenizeSqlSemantic(windowSql, dialect.id); const localStatementSpan = findActiveSqlStatementSpan(windowSql, localTokens, windowCursor); const allTokens = localTokens.map((token) => ({ ...token, span: { start: token.span.start + window.from, end: token.span.end + window.from }, })); const statementSpan = { start: localStatementSpan.start + window.from, end: localStatementSpan.end + window.from }; 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, safeCursor); 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: [], }; }