Ctrl K
ComunidadGuías…

packages/sql/clauses.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { DialectSpec } from './dialect'
import type { Token } from './tokenizer'
import { isKeyword, isPunctuation } from './tokenizer'

const SELECT_TAIL = ['GROUP BY', 'ORDER BY', 'LIMIT']
const JOINS = [
  'JOIN',
  'LEFT JOIN',
  'INNER JOIN',
  'RIGHT JOIN',
  'FULL JOIN',
  'CROSS JOIN',
]
const ORDERING_TAIL = ['NULLS FIRST', 'NULLS LAST', 'LIMIT', 'OFFSET']
const CONFLICT_ACTIONS = ['DO NOTHING', 'DO UPDATE SET']

const NEXT_CLAUSES: Record<string, string[]> = {
  AND: ['AND', 'OR', ...SELECT_TAIL],
  ASC: ORDERING_TAIL,
  CONFLICT: CONFLICT_ACTIONS,
  DESC: ORDERING_TAIL,
  FIRST: ['LIMIT', 'OFFSET'],
  FROM: ['WHERE', ...JOINS, ...SELECT_TAIL],
  'GROUP BY': ['HAVING', 'ORDER BY', 'LIMIT'],
  HAVING: ['AND', 'OR', 'ORDER BY', 'LIMIT'],
  INTO: ['VALUES', 'SELECT'],
  JOIN: ['ON', 'AS'],
  LAST: ['LIMIT', 'OFFSET'],
  LIMIT: ['OFFSET'],
  NOTHING: ['RETURNING'],
  ON: ['AND', 'OR', 'WHERE', 'JOIN', 'LEFT JOIN', ...SELECT_TAIL],
  OR: ['AND', 'OR', ...SELECT_TAIL],
  'ORDER BY': ['ASC', 'DESC', ...ORDERING_TAIL],
  SELECT: ['FROM', 'AS'],
  SET: ['WHERE', 'RETURNING'],
  UPDATE: ['SET'],
  VALUES: ['ON CONFLICT', 'ON DUPLICATE KEY UPDATE', 'RETURNING'],
  WHERE: ['AND', 'OR', ...SELECT_TAIL],
}

const FOLLOWERS: Record<string, string[]> = {
  CONFLICT: CONFLICT_ACTIONS,
  DO: ['NOTHING', 'UPDATE SET'],
  IS: ['NULL', 'NOT NULL', 'TRUE', 'FALSE'],
  'IS NOT': ['NULL', 'TRUE', 'FALSE'],
  NOT: ['IN (', 'LIKE', 'ILIKE', 'BETWEEN'],
  NULLS: ['FIRST', 'LAST'],
}

const SELECT_ONLY = new Set([...JOINS, 'HAVING', ...SELECT_TAIL])
const ENDS_IN_RETURNING = new Set(['AND', 'FROM', 'OR', 'WHERE'])
export const CONDITION_CLAUSES = new Set(['AND', 'HAVING', 'ON', 'OR', 'WHERE'])
const COMPARISON_KEYWORDS = [
  'BETWEEN',
  'EXISTS',
  'ILIKE',
  'IN',
  'IS',
  'LIKE',
  'NOT',
]
const CONDITION_STARTERS = [...CONDITION_CLAUSES, 'NOT']
const TERM_KEYWORDS = [
  'ASC',
  'DESC',
  'FALSE',
  'FIRST',
  'LAST',
  'NOTHING',
  'NULL',
  'TRUE',
]
const WORD = /^\w+$/u

export const isTerm = (token: Token | undefined) =>
  token !== undefined &&
  (['identifier', 'number', 'string', 'variable'].includes(token.kind) ||
    isPunctuation(token, ')') ||
    isKeyword(token, ...TERM_KEYWORDS))

/** `flat` has every closed parenthesis group emptied, so a group reads `(` `)`. */
const termBefore = (flat: Token[], end = flat.length) => {
  let index = end - 1
  const closesGroup = isPunctuation(flat[index], ')')
  if (closesGroup) {
    index -= 1
  }
  const call =
    closesGroup &&
    ['function', 'identifier', 'type'].includes(flat[index - 1]?.kind ?? '')
  if (!closesGroup || call) {
    index -= closesGroup ? 1 : 0
    while (
      isPunctuation(flat[index - 1], '.') &&
      flat[index - 2]?.kind === 'identifier'
    ) {
      index -= 2
    }
  }
  return { at: index - 1, group: closesGroup && !call }
}

export const completesCondition = (flat: Token[]) => {
  const term = termBefore(flat)
  const before = flat[term.at]
  if (isKeyword(before, 'AND')) {
    return isKeyword(flat[termBefore(flat, term.at).at], 'BETWEEN')
  }
  return (
    before?.kind === 'operator' ||
    (isKeyword(before, ...COMPARISON_KEYWORDS) &&
      !isKeyword(before, 'BETWEEN')) ||
    (term.group &&
      (isKeyword(before, ...CONDITION_STARTERS) || isPunctuation(before, '(')))
  )
}

const spoken = (clause: string, dialect: DialectSpec) =>
  clause
    .split(' ')
    .every((word) => !WORD.test(word) || dialect.keywords.has(word))

/** `nested`: the caret sits in a parenthesized group opened after the clause, where only `AND`/`OR` can continue it. */
export const nextClauses = (
  flat: Token[],
  clause: string,
  verb: string,
  nested: boolean,
  dialect: DialectSpec
) => {
  const [beforePrevious, previous] = flat.slice(-2)
  const followed =
    isKeyword(previous, 'NOT') && isKeyword(beforePrevious, 'IS')
      ? 'IS NOT'
      : (previous?.text.toUpperCase() ?? '')
  if (
    previous?.kind === 'keyword' &&
    FOLLOWERS[followed] &&
    (followed !== 'NOT' || isTerm(beforePrevious))
  ) {
    return FOLLOWERS[followed].filter((next) => spoken(next, dialect))
  }
  const selectsAll =
    previous?.text === '*' && isKeyword(beforePrevious, 'SELECT')
  if (!isTerm(previous) && !selectsAll) {
    return []
  }
  const term = termBefore(flat)
  if (isKeyword(flat[term.at], 'BETWEEN')) {
    return ['AND']
  }
  if (CONDITION_CLAUSES.has(clause) && !completesCondition(flat)) {
    return []
  }
  const next = (NEXT_CLAUSES[clause] ?? []).filter(
    (item) => !nested || item === 'AND' || item === 'OR'
  )
  const clauses =
    verb === 'SELECT'
      ? next
      : [
          ...next.filter((item) => !SELECT_ONLY.has(item)),
          ...(ENDS_IN_RETURNING.has(clause) ? ['RETURNING'] : []),
        ]
  return clauses.filter((item) => spoken(item, dialect))
}