packages/sql/scope.ts
Rama main · réplica de lectura del repositorio original.
import type { Relation } from './relation'
import { closingParen, cteAt, derivedAlias } from './relation'
import type { Token } from './tokenizer'
import { identifierName, isKeyword, isPunctuation } from './tokenizer'
export interface TableRef {
schema: string | null
name: string
alias: string | null
token: Token
}
export interface StatementScope {
tables: TableRef[]
ctes: string[]
derived: string[]
hasUnknownColumns: boolean
relations: Relation[]
/** The tables a name at the scope's offset can see: enclosing queries' first, its own query's last, so `at(-1)` is the latest join. */
visible: TableRef[]
/** The part of `visible` that belongs to the enclosing queries. */
enclosing: TableRef[]
/** The table an `INSERT INTO` or `UPDATE` at the top level writes. */
target: TableRef | null
}
interface QueryBlock {
parent: QueryBlock | null
refs: TableRef[]
started: boolean
}
export const TABLE_INTRODUCERS = ['FROM', 'JOIN', 'INTO', 'UPDATE', 'TABLE']
const TABLE_MODIFIERS = ['ONLY', 'LATERAL', 'IGNORE', 'IF', 'NOT', 'EXISTS']
const readQualifiedName = (tokens: Token[], from: number) => {
const parts: Token[] = []
let index = from
for (
let token = tokens[index];
token?.kind === 'identifier';
token = tokens[index]
) {
parts.push(token)
if (!isPunctuation(tokens[index + 1], '.')) {
break
}
index += 2
}
return { next: from + parts.length * 2 - 1, parts }
}
const tableRefAt = (
tokens: Token[],
from: number,
parenIsCall: boolean
): { ref: TableRef; next: number } | null => {
const { next, parts } = readQualifiedName(tokens, from)
const nameToken = parts.at(-1)
if (!nameToken || (parenIsCall && isPunctuation(tokens[next], '('))) {
return null
}
const schemaToken = parts.at(-2)
let after = next
let alias: string | null = null
if (isKeyword(tokens[after], 'AS')) {
after += 1
}
const aliasToken = tokens[after]
if (aliasToken?.kind === 'identifier') {
alias = identifierName(aliasToken)
after += 1
}
return {
next: after,
ref: {
alias,
name: identifierName(nameToken),
schema: schemaToken ? identifierName(schemaToken) : null,
token: nameToken,
},
}
}
const introducesTable = (tokens: Token[], index: number) => {
const previous = tokens[index - 1]?.text.toUpperCase() ?? ''
if (isKeyword(tokens[index], 'TABLE')) {
return !['CREATE', 'TEMP', 'TEMPORARY', 'UNLOGGED'].includes(previous)
}
if (isKeyword(tokens[index], 'UPDATE')) {
return previous !== 'FOR' && previous !== 'KEY'
}
return isKeyword(tokens[index], ...TABLE_INTRODUCERS)
}
const sourcesAfter = (tokens: Token[], index: number) => {
const introducer = tokens[index]
const listed = isKeyword(introducer, 'FROM') || isPunctuation(introducer, ',')
const clause = listed || isKeyword(introducer, 'JOIN')
const refs: TableRef[] = []
let cursor = index + 1
while (isKeyword(tokens[cursor], ...TABLE_MODIFIERS)) {
cursor += 1
}
for (;;) {
const found = tableRefAt(tokens, cursor, clause)
if (!found) {
const hasUnknownColumns =
clause &&
(isPunctuation(tokens[cursor], '(') ||
isPunctuation(tokens[cursor + 1], '('))
return { hasUnknownColumns, refs }
}
refs.push(found.ref)
if (!(listed && isPunctuation(tokens[found.next], ','))) {
return { hasUnknownColumns: false, refs }
}
cursor = found.next + 1
}
}
const visibleFrom = (block: QueryBlock | null) => {
const refs: TableRef[] = []
for (let level: QueryBlock | null = block; level; level = level.parent) {
refs.unshift(...level.refs)
}
return refs
}
const openFrame = (tokens: Token[], index: number, block: QueryBlock) => {
const call = ['function', 'identifier', 'type'].includes(
tokens[index - 1]?.kind ?? ''
)
return {
block: call ? block : { parent: block, refs: [], started: false },
call,
open: index,
}
}
/** A derived table is named only outside its parentheses, so a caret inside them never sees it. */
const derivedTable = (
tokens: Token[],
open: number,
close: number,
offset: number,
{ columnNames, name, next }: Exclude<ReturnType<typeof derivedAlias>, null>
) => {
const body = tokens.slice(open + 1, close)
const [first] = body
const closer = tokens[close]
if (!closer || !first || !isKeyword(first, 'SELECT', 'WITH')) {
return null
}
const ref: TableRef = { alias: null, name, schema: null, token: closer }
const holdsOffset = first.start <= offset && offset <= closer.start
const introducer = isKeyword(tokens[open - 1], 'LATERAL')
? tokens[open - 2]
: tokens[open - 1]
return {
listContinuesAt:
isKeyword(introducer, 'FROM', 'JOIN') && isPunctuation(tokens[next], ',')
? next
: null,
ref: holdsOffset ? null : ref,
relation: { body, columnNames, name },
}
}
/** `offset` picks which query's tables `visible` holds: a subquery sees its own and its enclosing queries', never a sibling's or a nested one's. */
export const statementScope = (
tokens: Token[],
offset = Number.POSITIVE_INFINITY
): StatementScope => {
const tables: TableRef[] = []
const ctes: string[] = []
const derived: string[] = []
const relations: Relation[] = []
let hasUnknownColumns = false
let target: TableRef | null = null
const rootBlock: QueryBlock = { parent: null, refs: [], started: false }
const root = { block: rootBlock, call: false, open: -1 }
const frames = [root]
let cursorBlock: QueryBlock | null = null
const readSources = (index: number, block: QueryBlock) => {
const sources = sourcesAfter(tokens, index)
tables.push(...sources.refs)
block.refs.push(...sources.refs)
block.started = true
hasUnknownColumns ||= sources.hasUnknownColumns
if (frames.length === 1 && isKeyword(tokens[index], 'INTO', 'UPDATE')) {
target ??= sources.refs[0] ?? null
}
}
const closeFrame = (index: number) => {
const frame = frames.at(-1) ?? root
frames.length = Math.max(frames.length - 1, 1)
const outer = frames.at(-1) ?? root
const alias = derivedAlias(tokens, index)
if (!alias) {
return
}
derived.push(alias.name)
const table = derivedTable(tokens, frame.open, index, offset, alias)
if (!table) {
return
}
relations.push(table.relation)
if (table.ref) {
outer.block.refs.push(table.ref)
}
if (table.listContinuesAt !== null) {
readSources(table.listContinuesAt, outer.block)
}
}
for (const [index, token] of tokens.entries()) {
const frame = frames.at(-1) ?? root
const cte = cteAt(tokens, index)
if (token.start >= offset) {
cursorBlock ??= frame.block
}
if (isPunctuation(token, '(')) {
frames.push(openFrame(tokens, index, frame.block))
} else if (isPunctuation(token, ')')) {
closeFrame(index)
} else if (isKeyword(token, 'SELECT')) {
if (frame.block.started) {
frame.block = { parent: frame.block.parent, refs: [], started: false }
}
frame.block.started = true
} else if (cte) {
ctes.push(identifierName(token))
relations.push({
body: tokens.slice(cte.open + 1, closingParen(tokens, cte.open)),
columnNames: cte.columnNames,
name: identifierName(token),
})
} else if (introducesTable(tokens, index) && !frame.call) {
readSources(index, frame.block)
}
}
const block = cursorBlock ?? (frames.at(-1) ?? root).block
return {
ctes,
derived,
enclosing: visibleFrom(block.parent),
hasUnknownColumns,
relations,
tables,
target,
visible: visibleFrom(block),
}
}