packages/sql/cursor.ts
Rama main · réplica de lectura del repositorio original.
import { identifierName, isKeyword, isPunctuation } from './tokenizer'
import type { Token } from './tokenizer'
export interface ColumnRef {
qualifier: string | null
name: string
}
const WORD_KINDS = new Set(['identifier', 'keyword', 'function', 'type'])
export const columnRefAt = (
tokens: Token[],
index: number
): ColumnRef | null => {
const token = tokens[index]
if (token?.kind !== 'identifier') {
return null
}
const qualifier = tokens[index - 2]
return {
name: identifierName(token),
qualifier:
isPunctuation(tokens[index - 1], '.') && qualifier?.kind === 'identifier'
? identifierName(qualifier)
: null,
}
}
export const columnBeforeValue = (before: Token[], cursor: number) => {
const previous = before[cursor]
if (previous?.kind === 'operator') {
return columnRefAt(before, cursor - 1)
}
if (isPunctuation(previous, '(') && isKeyword(before[cursor - 1], 'IN')) {
return columnRefAt(
before,
isKeyword(before[cursor - 2], 'NOT') ? cursor - 3 : cursor - 2
)
}
if (isKeyword(previous, 'BETWEEN', 'LIKE', 'ILIKE')) {
return columnRefAt(before, cursor - 1)
}
return null
}
const topLevel = (tokens: Token[]) => {
let depth = 0
return tokens.map((token) => {
if (isPunctuation(token, ')')) {
depth -= 1
}
const level = depth
if (isPunctuation(token, '(')) {
depth += 1
}
return level === 0
})
}
export const selectList = (body: Token[]) => {
const outside = topLevel(body)
const select = body.findIndex(
(token, index) => outside[index] && isKeyword(token, 'SELECT')
)
if (select === -1) {
return { at: undefined, segments: [] }
}
const segments: Token[][] = [[]]
let index = select + 1
while (isKeyword(body[index], 'DISTINCT', 'ALL')) {
index += 1
}
if (isKeyword(body[index], 'ON') && isPunctuation(body[index + 1], '(')) {
index += 2
while (body[index] && !outside[index]) {
index += 1
}
}
for (; body[index]; index += 1) {
const token = body[index]
if (outside[index] && isKeyword(token, 'FROM', 'UNION', 'INTO')) {
break
}
if (outside[index] && isPunctuation(token, ',')) {
segments.push([])
} else if (token) {
segments.at(-1)?.push(token)
}
}
return { at: body[select], segments }
}
export const selectedColumns = (statementTokens: Token[]) =>
selectList(statementTokens).segments.flatMap((segment) =>
segment.length > 0 &&
segment.every(
(token) => token.kind === 'identifier' || isPunctuation(token, '.')
)
? [segment.map((token) => token.text).join('')]
: []
)
export const wordAt = (tokens: Token[], offset: number) =>
tokens.find(
(token) =>
WORD_KINDS.has(token.kind) && token.start < offset && offset <= token.end
)
export const literalAt = (tokens: Token[], offset: number) =>
tokens.find(
(token) =>
(token.kind === 'string' || token.kind === 'comment') &&
token.start < offset &&
offset <= token.end &&
(offset < token.end ||
token.unclosed === true ||
(token.kind === 'comment' && !token.text.startsWith('/*')))
)
export const qualifierBefore = (before: Token[]) => {
const qualifier: string[] = []
let cursor = before.length - 1
for (
let part = before[cursor - 1];
isPunctuation(before[cursor], '.') && part?.kind === 'identifier';
part = before[cursor - 1]
) {
qualifier.unshift(identifierName(part))
cursor -= 2
}
return { cursor, qualifier }
}
export const stringContentRange = (literal: Token | undefined) =>
literal?.kind === 'string' && literal.text.startsWith("'")
? {
end: literal.end - (literal.unclosed ? 0 : 1),
start: literal.start + 1,
}
: undefined