UNPKG

@secam/pgsql-ast-parser

Version:

Fork of pgsql-ast-parser Simple Postgres SQL parser/modifier for pg-mem

142 lines (125 loc) 5.4 kB
import { Statement, Expr, QName, GeometricLiteral, Point, Line, Segment, Box, Path, Polygon, Circle, Interval, PGComment } from './syntax/ast'; import { Parser, Grammar } from 'nearley'; import sqlGrammar from './syntax/main.ne'; import arrayGrammar from './literal-syntaxes/array.ne'; import geometricGrammar from './literal-syntaxes/geometric.ne'; import intervalTextGrammar from './literal-syntaxes/interval.ne'; import intervalIsoGrammar from './literal-syntaxes/interval-iso.ne'; import { buildInterval } from './literal-syntaxes/interval-builder'; import { tracking, trackingComments } from './lexer'; let sqlCompiled: Grammar; let arrayCompiled: Grammar; let geometricCompiled: Grammar; let intervalTextCompiled: Grammar; let intervalIsoCompiled: Grammar; /** Parse the first SQL statement in the given text (discards the rest), and return its AST */ export function parseFirst(sql: string): Statement { const first = parse(sql); return first[0]; } export interface ParseOptions { /** * [Advanced usage only] This allows to parse sub-expressions, not necessarily full valid statements. * * For instance, `parse('2+2', {entry: 'expr'})` will return the AST of the given expression (which is not a valid statement) */ entry?: string; /** If true, then a detailed location will be available on each node */ locationTracking?: boolean; } /** Parse an AST from SQL, and get the comments */ export function parseWithComments(sql: string, options?: ParseOptions): { ast: Statement[]; comments: PGComment[] } { return trackingComments(() => parse(sql, options)); } /** Parse an AST from SQL */ export function parse(sql: string): Statement[]; export function parse(sql: string, entry: 'expr'): Expr; export function parse(sql: string, entry: 'qualified_name'): QName; export function parse(sql: string, options?: ParseOptions): Statement[]; export function parse(sql: string, optEntry?: string | ParseOptions): any { if (!sqlCompiled) { sqlCompiled = Grammar.fromCompiled(sqlGrammar); } const entry = typeof optEntry === 'string' ? optEntry : optEntry?.entry; const opts = typeof optEntry === 'string' ? null : optEntry; // parse sql const doParse = () => _parse(sql, sqlCompiled, entry); let parsed = opts?.locationTracking ? tracking(doParse) : doParse(); // always return an array of statements. if (typeof optEntry !== 'string' && !Array.isArray(parsed)) { parsed = [parsed] } return parsed; } export function parseArrayLiteral(sql: string): string[] { if (!arrayCompiled) { arrayCompiled = Grammar.fromCompiled(arrayGrammar); } return _parse(sql, arrayCompiled); } export function parseIntervalLiteral(literal: string): Interval { if (literal.startsWith('P')) { if (!intervalIsoCompiled) { intervalIsoCompiled = Grammar.fromCompiled(intervalIsoGrammar); } return buildInterval(literal, _parse(literal, intervalIsoCompiled)); } else { if (!intervalTextCompiled) { intervalTextCompiled = Grammar.fromCompiled(intervalTextGrammar); } const low = literal.toLowerCase(); // full text syntax is case insensitive return buildInterval(literal, _parse(low, intervalTextCompiled)); } } export function parseGeometricLiteral(sql: string, type: 'point'): Point; export function parseGeometricLiteral(sql: string, type: 'line'): Line; export function parseGeometricLiteral(sql: string, type: 'lseg'): Segment; export function parseGeometricLiteral(sql: string, type: 'box'): Box; export function parseGeometricLiteral(sql: string, type: 'path'): Path; export function parseGeometricLiteral(sql: string, type: 'polygon'): Polygon; export function parseGeometricLiteral(sql: string, type: 'circle'): Circle; export function parseGeometricLiteral(sql: string, type: 'point' | 'line' | 'lseg' | 'box' | 'path' | 'polygon' | 'circle'): GeometricLiteral { if (!geometricCompiled) { geometricCompiled = Grammar.fromCompiled(geometricGrammar); } return _parse(sql, geometricCompiled, type); } function _parse(sql: string, grammar: Grammar, entry?: string): any { try { grammar.start = entry ?? 'main'; const parser = new Parser(grammar); parser.feed(sql); const asts = parser.finish(); if (!asts.length) { throw new Error('Unexpected end of input'); } else if (asts.length !== 1) { throw new Error(`💀 Ambiguous SQL syntax: Please file an issue stating the request that has failed at https://github.com/oguimbal/pgsql-ast-parser: ${sql} `); } return asts[0]; } catch (e) { if (typeof (e as any)?.message !== 'string') { throw e; } let msg: string = (e as any).message; // remove all the stack crap of nearley parser let begin: string | null = null; const parts: string[] = []; const reg = /A (.+) token based on:/g; let m: RegExpExecArray | null; while (m = reg.exec(msg)) { begin = begin ?? msg.substr(0, m.index); parts.push(` - A "${m[1]}" token`); } if (begin) { msg = begin + parts.join('\n') + '\n\n'; } (e as any).message = msg; throw e; } }