@secam/pgsql-ast-parser
Version:
Fork of pgsql-ast-parser Simple Postgres SQL parser/modifier for pg-mem
333 lines (289 loc) • 12.2 kB
text/typescript
import { Parser, Grammar } from 'nearley';
import { expect, assert } from 'chai';
import grammar from '../syntax/main.ne';
import { trimNullish } from '../utils';
import { Expr, SelectStatement, CreateTableStatement, CreateIndexStatement, Statement, InsertStatement, UpdateStatement, AlterTableStatement, DeleteStatement, CreateExtensionStatement, CreateSequenceStatement, AlterSequenceStatement, SelectedColumn, Interval, BinaryOperator, ExprBinary, Name, ExprInteger, FromTable, QName, AlterIndexStatement, ExprNumeric } from './ast';
import { astMapper, IAstMapper } from '../ast-mapper';
import { toSql, IAstToSql } from '../to-sql';
import { parseIntervalLiteral } from '../parser';
import { normalizeInterval } from '../literal-syntaxes/interval-builder';
import { tracking } from '../lexer';
export function checkSelect(value: string | string[], expected: SelectStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkCreateSequence(value: string | string[], expected: CreateSequenceStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkCreateTable(value: string | string[], expected: CreateTableStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkCreateTableLoc(value: string | string[], expected: CreateTableStatement) {
checkTree(value, expected, (p, m) => m.statement(p), undefined, true);
}
export function checkCreateIndex(value: string | string[], expected: CreateIndexStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkCreateIndexLoc(value: string | string[], expected: CreateIndexStatement) {
checkTree(value, expected, (p, m) => m.statement(p), undefined, true);
}
export function checkAlterSequence(value: string | string[], expected: AlterSequenceStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkCreateExtension(value: string | string[], expected: CreateExtensionStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkInsert(value: string | string[], expected: InsertStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkInsertLoc(value: string | string[], expected: InsertStatement) {
checkTree(value, expected, (p, m) => m.statement(p), undefined, true);
}
export function checkDelete(value: string | string[], expected: DeleteStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkAlterTable(value: string | string[], expected: AlterTableStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkAlterIndex(value: string | string[], expected: AlterIndexStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkAlterTableLoc(value: string | string[], expected: AlterTableStatement) {
checkTree(value, expected, (p, m) => m.statement(p), undefined, true);
}
export function checkUpdate(value: string | string[], expected: UpdateStatement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
export function checkStatement(value: string | string[], expected: Statement) {
checkTree(value, expected, (p, m) => m.statement(p));
}
function hideLocs(val: any): any {
if (!val) {
return val;
}
if (typeof val !== 'object') {
return val;
}
if (Array.isArray(val)) {
return val.map(hideLocs);
}
const ret = {} as any;
for (const [k, v] of Object.entries(val)) {
ret[k] = hideLocs(v);
}
delete ret._location;
return ret;
}
function deepEqual<T>(a: T, b: T, strict?: boolean, depth = 10, numberDelta = 0.0001) {
if (depth < 0) {
throw new Error('Comparing too deep entities');
}
if (a === b) {
return true;
}
if (!strict) {
// should not use '==' because it could call .toString() on objects when compared to strings.
// ... which is not ok. Especially when working with translatable objects, which .toString() returns a transaltion (a string, thus)
if (!a && !b) {
return true;
}
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i], strict, depth - 1, numberDelta))
return false;
}
return true;
}
// handle dates
if (a instanceof Date || b instanceof Date) {
return a === b;
}
const fa = Number.isFinite(<any>a);
const fb = Number.isFinite(<any>b);
if (fa || fb) {
return fa && fb && Math.abs(<any>a - <any>b) <= numberDelta;
}
// handle plain objects
if (typeof a !== 'object' || typeof a !== typeof b)
return false;
if (!a || !b) {
return false;
}
const ak = Object.keys(a);
const bk = Object.keys(b);
if (strict && ak.length !== bk.length)
return false;
const set: Iterable<string> = strict
? Object.keys(a)
: new Set([...Object.keys(a), ...Object.keys(b)]);
for (const k of set) {
if (!deepEqual((a as any)[k], (b as any)[k], strict, depth - 1, numberDelta))
return false;
}
return true;
}
declare var __non_webpack_require__: any;
function inspect(elt: any) {
return __non_webpack_require__('util').inspect(elt);
}
function checkTree<T>(value: string | string[], expected: T, mapper: (parsed: T, m: IAstMapper | IAstToSql) => any, start?: string, checkLocations?: boolean) {
if (typeof value === 'string') {
value = [value];
}
for (const sql of value) {
it('parses ' + sql, () => {
const gram = Grammar.fromCompiled(grammar);
if (start) {
gram.start = start
}
function doParse(psql: string) {
const parser = new Parser(gram);
parser.feed(psql);
const ret = parser.finish();
if (!ret.length) {
assert.fail('Unexpected end of input');
}
if (ret.length !== 1) {
const noLocs = ret.map(hideLocs);
if (noLocs.slice(1).every(p => deepEqual(p, noLocs[0]))) {
assert.fail(`${noLocs.length} ambiguous syntaxes, but they yielded the same ASTs : ` + inspect(noLocs[0]));
} else {
assert.fail(`${noLocs.length} ambiguous syntaxes, AND THEY HAVE YIELDED DIFFERENT ASTs : \n` + noLocs
.map(inspect)
.join('\n\n ====================== \n\n'));
}
}
return trimNullish(ret[0]);
}
const parsedWithLocations = tracking(() => doParse(sql));
const parsedWithoutTracking = doParse(sql);
const parsed =
checkLocations
? parsedWithLocations
: parsedWithoutTracking;
// check that it is what we expected
expect(parsed)
.to.deep.equal(expected, 'Parser has not returned the expected AST');
// check that top-level statements always have at least some kind of basic position
assert.exists(parsedWithLocations._location, 'Top level statements must have a location');
// check that it generates the same with/without location tracking
expect(hideLocs(parsedWithLocations)).to.deep.equal(hideLocs(parsedWithoutTracking), 'Parser did not return the same thing with and without location tracking enabled');
// check that it is stable through ast modifier
const modified = mapper(parsed, astMapper(() => ({})));
expect(modified).to.equal(parsed, 'It is not stable when passing through a neutral AST mapper -> Should return THE SAME REFERENCE to avoid copying stuff when nothing changed.');
// check that it procuces sql
let newSql: string;
try {
newSql = mapper(parsed, toSql);
assert.isString(newSql);
} catch (e) {
(e as any).message = `⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔
Failed to generate SQL from the parsed AST
=> There should be something wrong in to-sql.ts
⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔
${(e as any).message}`;
throw e;
}
// reparse the generated sql...
let reparsed: any;
try {
assert.isString(newSql);
reparsed = checkLocations
? tracking(() => doParse(newSql))
: doParse(newSql);
} catch (e) {
(e as any).message = `⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔
The parsed AST converted-back to SQL generated invalid SQL.
=> There should be something wrong in to-sql.ts
⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔
${(e as any).message}`;
throw e;
}
// ...and check it still produces the same ast.
expect(hideLocs(reparsed)).to.deep.equal(hideLocs(expected), `⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔
SQL -> AST -> SQL transformation is not stable !
=> This means that the parser is OK, but you might have forgotten to implement something in to-sql.ts
⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ ⛔ `);
});
}
}
export function checkInvalid(sql: string, start?: string) {
it('does not parses ' + sql, () => {
const gram = Grammar.fromCompiled(grammar);
if (start) {
gram.start = start
}
assert.throws(() => {
const parser = new Parser(gram);
parser.feed(sql);
expect(parser.results).not.to.deep.equal([]);
});
});
}
export function checkValid(sql: string, start?: string) {
it('parses ' + sql, () => {
const gram = Grammar.fromCompiled(grammar);
if (start) {
gram.start = start
}
const parser = new Parser(gram);
parser.feed(sql);
expect(parser.results).not.to.deep.equal([]);
});
}
export function checkInvalidExpr(sql: string) {
return checkInvalid(sql, 'expr');
}
export function checkTreeExpr(value: string | string[], expected: Expr) {
checkTree(value, expected, (p, m) => m.expr(p), 'expr');
}
export function checkTreeExprLoc(value: string | string[], expected: Expr) {
checkTree(value, expected, (p, m) => m.expr(p), 'expr', true);
}
export function columns(...vals: (Expr | string)[]): SelectedColumn[] {
return vals.map<SelectedColumn>(expr => typeof expr === 'string'
? { expr: { type: 'ref', name: expr } }
: { expr });
}
export function checkInterval(input: string | string[], expected: Interval) {
for (const v of Array.isArray(input) ? input : [input]) {
it('parses interval "' + v + '"', () => {
expect(normalizeInterval(parseIntervalLiteral(v)))
.to.deep.equal(expected);
})
}
}
export const star: Expr = { type: 'ref', name: '*' };
export const starCol: SelectedColumn = { expr: star };
export function col(name: string, alias?: string): SelectedColumn {
return {
expr: ref(name),
... alias ? { name: alias } : undefined,
};
}
export function ref(name: string): Expr {
return { type: 'ref', name };
}
export function binary(left: Expr, op: BinaryOperator, right: Expr): ExprBinary {
return { type: 'binary', left, op, right };
}
export function name(name: string): Name {
return { name };
}
export function qname(name: string, schema?: string): QName {
return { name, schema };
}
export function int(value: number): ExprInteger {
return { type: 'integer', value };
}
export function tbl(nm: string): FromTable {
return {
type: 'table',
name: name(nm),
};
}
export function numeric(value: number): ExprNumeric {
return { type: 'numeric', value };
}