Ctrl K
ComunidadGuías…

packages/sql/statements.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, tokenize } from './tokenizer'

export interface Statement {
  text: string
  start: number
  end: number
  terminatorEnd: number
  tokens: Token[]
}

// Matched by text: most are not keywords in any dialect's list.
const TRANSACTION_OPENERS = new Set([
  ';',
  'DEFERRABLE',
  'DEFERRED',
  'DISTRIBUTED',
  'EXCLUSIVE',
  'IMMEDIATE',
  'ISOLATION',
  'READ',
  'TRAN',
  'TRANSACTION',
  'WORK',
])
const WHOLE_STATEMENT_ENDINGS = new Set([undefined, 'TRANSACTION', 'WORK'])
const END_QUALIFIERS = ['IF', 'LOOP', 'WHILE', 'REPEAT']
const ACCESS_MODES = ['read only', 'read write'] as const
const ISOLATION_LEVELS = [
  'read uncommitted',
  'read committed',
  'repeatable read',
  'serializable',
  'snapshot',
] as const

const GO_REPEAT_COUNT = /^\s*\d*\s*$/u
const BLANK = /^\s*$/u

const separatorRest = (token: Token, dialect: DialectSpec) => {
  if (dialect.goBatches && isKeyword(token, 'GO')) {
    return GO_REPEAT_COUNT
  }
  return dialect.plsqlUnits && token.text === '/' ? BLANK : null
}

export const separatorLineEnd = (
  text: string,
  tokens: Token[],
  index: number,
  dialect: DialectSpec
) => {
  const token = tokens[index]
  const previous = tokens[index - 1]
  const rest = token && separatorRest(token, dialect)
  if (!(token && rest)) {
    return null
  }
  const lineEnd = text.indexOf('\n', token.end) + 1 || text.length
  const startsLine =
    previous === undefined ||
    text.slice(previous.end, token.start).includes('\n')
  return startsLine && rest.test(text.slice(token.end, lineEnd))
    ? lineEnd
    : null
}

const upperTexts = (tokens: Token[]) =>
  tokens.map((token) => token.text.toUpperCase())

const PLSQL_UNITS = new Set([
  'FUNCTION',
  'PACKAGE',
  'PROCEDURE',
  'TRIGGER',
  'TYPE',
])
const CREATE_MODIFIERS = new Set([
  'OR',
  'REPLACE',
  'EDITIONABLE',
  'NONEDITIONABLE',
])

const startsPlsqlUnit = (tokens: Token[], index: number) => {
  const [word, ...rest] = upperTexts(tokens.slice(index, index + 5))
  if (word === 'DECLARE' || word === 'BEGIN') {
    return true
  }
  const unit = rest.find((next) => !CREATE_MODIFIERS.has(next))
  return word === 'CREATE' && unit !== undefined && PLSQL_UNITS.has(unit)
}

const opensTransactionAt = (tokens: Token[], index: number) => {
  const [word, next, after] = upperTexts(tokens.slice(index, index + 3))
  if (word === 'START') {
    return next === 'TRANSACTION'
  }
  if (next === 'NOT') {
    return word === 'BEGIN' && after === 'DEFERRABLE'
  }
  return (
    word === 'BEGIN' && (next === undefined || TRANSACTION_OPENERS.has(next))
  )
}

const transactionCloser = (tokens: Token[]) => {
  const words = upperTexts(tokens.slice(0, 3))
  const [word, next, after] = words
  if (word === 'COMMIT' || (word === 'ROLLBACK' && !words.includes('TO'))) {
    return word
  }
  if (
    (word === 'END' || word === 'ABORT') &&
    after === undefined &&
    WHOLE_STATEMENT_ENDINGS.has(next)
  ) {
    return word
  }
}

const isColumnPosition = (previous: Token | undefined) =>
  isKeyword(previous, 'SELECT') ||
  isPunctuation(previous, ',') ||
  isPunctuation(previous, '(')

export const leavesTransactionOpen = (text: string, dialect: DialectSpec) => {
  const tokens = tokenize(text, dialect).tokens.filter(
    (token) => token.kind !== 'comment'
  )
  return (
    opensTransactionAt(tokens, 0) &&
    !tokens.some((_, index) => transactionCloser(tokens.slice(index)))
  )
}

export const endsTransaction = (text: string, dialect: DialectSpec) =>
  !!transactionCloser(
    tokenize(text, dialect).tokens.filter((token) => token.kind !== 'comment')
  )

/** `END CASE` closes the CASE its END already counted. */
const blockDepthChange = (tokens: Token[], index: number) => {
  const token = tokens[index]
  const previous = tokens[index - 1]
  if (
    (isKeyword(token, 'CASE') && !isKeyword(previous, 'END')) ||
    (isKeyword(token, 'BEGIN') && !isColumnPosition(previous))
  ) {
    return 1
  }
  return isKeyword(token, 'END') &&
    !isKeyword(tokens[index + 1], ...END_QUALIFIERS)
    ? -1
    : 0
}

export const statementsFromTokens = (
  text: string,
  tokens: Token[],
  dialect: DialectSpec,
  { groupTransactions = true }: { groupTransactions?: boolean } = {}
): Statement[] => {
  const statements: Statement[] = []
  const significant = tokens.filter((token) => token.kind !== 'comment')
  const groups = groupTransactions && dialect.transactions
  let current: Token[] = []
  let innerStart = 0
  let depth = 0
  let inTransaction = false
  let inPlsqlUnit = false

  const flush = (terminator?: Token) => {
    const [first] = current
    const last = current.at(-1)
    if (inTransaction && !transactionCloser(current.slice(innerStart))) {
      statements.push(
        ...statementsFromTokens(text, current, dialect, {
          groupTransactions: false,
        })
      )
    } else if (first && last) {
      statements.push({
        end: last.end,
        start: first.start,
        terminatorEnd: terminator?.end ?? last.end,
        text: text.slice(first.start, last.end),
        tokens: current,
      })
    }
    current = []
    innerStart = 0
    depth = 0
    inTransaction = false
    inPlsqlUnit = false
  }

  let separatorEnd = 0

  for (const [index, token] of significant.entries()) {
    if (token.start < separatorEnd) {
      continue
    }
    const lineEnd = separatorLineEnd(text, significant, index, dialect)
    if (lineEnd) {
      separatorEnd = lineEnd
      flush()
      continue
    }

    if (
      dialect.plsqlUnits &&
      current.length === 0 &&
      startsPlsqlUnit(significant, index)
    ) {
      inPlsqlUnit = true
    } else if (opensTransactionAt(significant, index)) {
      inTransaction = groups
    } else {
      depth = Math.max(0, depth + blockDepthChange(significant, index))
    }

    if (isPunctuation(token, ';') && depth === 0 && !inPlsqlUnit) {
      if (!inTransaction || transactionCloser(current.slice(innerStart))) {
        flush(token)
        continue
      }
      innerStart = current.length + 1
    }

    current.push(token)
  }

  flush()
  return statements
}

export const splitStatements = (
  text: string,
  dialect: DialectSpec,
  options?: { groupTransactions?: boolean }
) =>
  statementsFromTokens(text, tokenize(text, dialect).tokens, dialect, options)

export const unwrapTransaction = (text: string, dialect: DialectSpec) => {
  if (!dialect.transactions) {
    return null
  }
  const parts = splitStatements(text, dialect, { groupTransactions: false })
  const [first] = parts
  const ending = transactionCloser(parts.at(-1)?.tokens ?? [])
  if (!(first && ending && opensTransactionAt(first.tokens, 0))) {
    return null
  }
  const opener = first.text.toLowerCase().replaceAll(/\s+/gu, ' ')
  return {
    accessMode: ACCESS_MODES.find((mode) => opener.includes(mode)),
    commit: ending === 'COMMIT' || ending === 'END',
    isolationLevel: ISOLATION_LEVELS.find((level) =>
      opener.includes(`isolation level ${level}`)
    ),
    statements: parts.slice(1, -1).map((part) => part.text),
  }
}

export const statementAt = (
  statements: Statement[],
  offset: number,
  text: string
) =>
  statements.findLast(
    ({ start, terminatorEnd }) =>
      offset >= start &&
      (offset <= terminatorEnd ||
        !text.slice(terminatorEnd, offset).includes('\n'))
  )