import {merge} from './utils'; import {tokenize} from './tokenizer'; import {LEFT, RIGHT} from './operators'; import * as ast from './ast'; export function parseQuery(query) { const {node, rest} = parseSubQuery(query).ifNextToken(isType('semicolon'), curr => curr.then(semicolon) ); if (rest.length > 0) { const restString = rest.map(token => token.string).join(' '); throw SyntaxError(`Parser completed with input remaining: "${restString}"`); } return node; } function parseSubQuery(query) { const tokens = tokenize(query); return parser(tokens) .then(keyword('SELECT')) .bind('select', outputColumns) .bind('from', fromClause) .bind('where', createConditionClause('WHERE')) .map(parseGroupByHaving) .bind('orderBy', orderByClause) .bind('limit', limitClause) .bind('offset', offsetClause) .mapNode(ast.query); } function fromClause(rest) { return parser(rest).ifNextToken(isKeyword('FROM'), curr => curr.then(keyword('FROM')) .just(tableName) ); } function parseGroupByHaving(parser) { return parser .ifNextToken(isKeyword('GROUP'), curr => curr.then(keyword('GROUP')) .then(keyword('BY')) .bind('groupBy', many(expression, {separator: comma})) .bind('having', createConditionClause('HAVING')) ) .mapNode(node => merge({groupBy: null, having: null}, node)); } function createConditionClause(conditionType) { return tokens => parser(tokens).ifNextToken(isKeyword(conditionType), curr => curr.then(keyword(conditionType)) .just(expression) ); } function orderByClause(tokens) { return parser(tokens).ifNextToken(isKeyword('ORDER'), curr => curr.then(keyword('ORDER')) .then(keyword('BY')) .just(many(orderingTerm, {separator: comma})) ); } function orderingTerm(tokens) { return parser(tokens) .bind('expression', expression) .bind('direction', tokens => { const [first, ...rest] = tokens; if (isKeyword('ASC', first) || isKeyword('DESC', first)) { return parser(rest, first.string); } return parser(tokens, 'ASC'); }) .mapNode(node => ast.orderingTerm(node.expression, node.direction)); } function limitClause(tokens) { return parser(tokens).ifNextToken(isKeyword('LIMIT'), curr => curr.then(keyword('LIMIT')).just(number) ); } function offsetClause(tokens) { return parser(tokens).ifNextToken(isKeyword('OFFSET'), curr => curr.then(keyword('OFFSET')).just(number) ); } function atom([first, ...rest]) { // grouped expression if (isType('parOpen', first)) { return expression(rest).then(parClose); } // literal TRUE, FALSE, or NULL else if (isType('keyword', first)) { const s = first.string; if (s === 'TRUE' || s === 'FALSE' || s === 'NULL') { return parser(rest, ast.literal(JSON.parse(s.toLowerCase()))); } else if (first.string === 'CASE') { return caseExpression(rest); } } // function call else if (isType('identifier', first) && isType('parOpen', rest[0])) { const functionName = first.value; if (functionName.toUpperCase() === 'COUNT' && isStar(rest[1])) { return parser(rest.slice(2)) .then(parClose) .mapNode(node => ast.call(functionName, [ast.star()])); } return parser(rest.slice(1)) .bind('arguments', many(expression, {separator: comma, min: 0})) .then(parClose) .mapNode(node => ast.call(functionName, node.arguments)) } // identifier, number, or string else if (isType(['identifier', 'number', 'string'], first)) { return parser(rest, first); } throw SyntaxError( `Expected an expression, found "${first.string || '(end of input)'}"`); } function caseExpression(tokens) { return parser(tokens) .ifNextToken(not(isKeyword('WHEN')), curr => curr.bind('switchExpression', expression) ) .bind('cases', many(whenThen, {min: 1})) .ifNextToken(isKeyword('ELSE'), curr => curr.then(keyword('ELSE')) .bind('elseExpression', expression) ) .then(keyword('END')) .mapNode(({switchExpression, cases, elseExpression}) => switchExpression ? ast.caseSwitch(switchExpression, cases, elseExpression) : ast.caseIf(cases, elseExpression) ); } function whenThen(tokens) { return parser(tokens) .then(keyword('WHEN')) .bind('when', expression) .then(keyword('THEN')) .bind('then', expression) .mapNode(node => ast.whenThen(node.when, node.then)); } function expression(tokens, controlOperator=null) { let parser = atom(tokens); while (true) { const token = parser.rest[0]; const tokenIsLesserOperator = ( token && token.type === 'operator' && ( controlOperator === null || token.precedence > controlOperator.precedence || ( token.precedence === controlOperator.precedence && controlOperator.associativity === RIGHT ) ) ); if (!tokenIsLesserOperator) return parser; parser = parser .mapNode(left => ({left})) .bind('operator', operator) .bind('right', tokens => expression(tokens, token)) .mapNode(node => ast.binaryExpression(node.operator, node.left, node.right)); } } function namedExpression(tokens) { return expression(tokens) .mapNode(expression => ({expression})) .ifNextToken(isKeyword('AS'), curr => curr.then(keyword('AS')) .bind('name', atom) ) .mapNode(({expression, name}) => { return ast.namedExpression(expression, name ? name.value : null) }); } function outputColumns(tokens) { if (isStar(tokens[0])) { return parser(tokens.slice(1), '*'); } return many(namedExpression, {separator: comma})(tokens); } function keyword(word) { return tokens => { const first = tokens[0]; if (isKeyword(word, first)) { return parser(tokens.slice(1), word); } else { throw SyntaxError( `Expected "${word}", found "${first.string || '(end of input)'}"`); } }; } const tableName = parseTokenType('string', {expected: 'a table name'}); const identifier = parseTokenType('identifier', 'an identifier'); const operator = parseTokenType('operator', {expected: 'an operator'}); const comma = parseTokenType('comma'); const number = parseTokenType('number'); const semicolon = parseTokenType('semicolon'); const parOpen = parseTokenType('parOpen', {expected: 'an opening parenthesis'}); const parClose = parseTokenType('parClose', {expected: 'a closing parenthesis'}); function parseTokenType(typeName, {expected}={expected: 'a ' + typeName}) { return ([first, ...rest]) => { if (isType(typeName, first)) { return parser(rest, first.value); } const found = first && first.string || '(end of input)'; throw SyntaxError(`Expected ${expected}, found "${found}"`); }; } function isKeyword(keyword, token) { if (arguments.length < 2) { return token => isKeyword(keyword, token); } return token && token.type === 'keyword' && token.string === keyword; } function isType(types, token) { if (arguments.length < 2) { return token => isType(types, token); } if (!Array.isArray(types)) types = [types]; return token && types.some(type => token.type === type); } function isStar(token) { return token && token.string === '*'; } function many(parseFunc, {separator, min}={}) { if (min === undefined) min = 1; return tokens => { let node, rest = tokens; const parts = []; // TODO refactor this function to provide better error messages for // sequences with a minimum - i.e. don't swallow the errors until // the minimum is reached try { for (let i = 0; rest.length > 0; i ++) { if (separator && i > 0) { ({rest} = separator(rest)); } ({node, rest} = parseFunc(rest)); parts.push(node); } } catch (e) { if (!(e instanceof SyntaxError)) { throw e; } } if (parts.length < min) { throwUnexpected(rest[0]); } return parser(rest, parts); }; } function not(predicate) { return (...args) => !predicate(...args); } function and(predicate1, predicate2) { return (...args) => predicate1(...args) && predicate2(...args); } function throwUnexpected(token) { throw SyntaxError(`Unexpected token: "${token.string}"`); } function printRest(parser) { console.log(parser.rest.map(token => ({[token.type]: token.string}))); return parser; } function parser(rest, node=null) { return { rest, node, bind(key, parseFunc) { const {rest, node} = parseFunc(this.rest); return parser(rest, merge(this.node, {[key]: node})); }, just(parseFunc) { return parseFunc(this.rest); }, then(parseFunc) { const {rest} = parseFunc(this.rest); return parser(rest, this.node); }, ifNextToken(predicate, ifFunc, elseFunc) { if (predicate(this.rest[0])) { return ifFunc(this); } else if (elseFunc) { return elseFunc(this); } else return this; }, mapNode(func) { const mapped = func(this.node); return parser(this.rest, mapped === undefined ? this.node : mapped); }, map(func) { return func(this); }, }; }