'use strict';
require('insulin').factory('ndm_ConditionCompiler',
['ndm_ConditionError'], ndm_ConditionCompilerProducer);
function ndm_ConditionCompilerProducer(ConditionError) {
/** A class that compiles a parse tree, as created by a ConditionParser
instance, into a SQL condition. */
class ConditionCompiler {
/**
* Initialize the compiler.
* @param {Escaper} - An instance of an Escaper that matches the database
* type (e.g. MySQLEscaper for a MySQL database).
*/
constructor(escaper) {
this._escaper = escaper;
}
/**
* Compile the parse tree.
* @param {Object} parseTree - A parse tree object, as created by the
* ConditionParser.parse() method.
* @param {Object} params - An object containing key-value pairs that are
* used to replace parameters in the query. The compiler verifies that
* there is a replacement for every parameter, but does not perform the
* actual replacement.
* @return {string} The compiled condition as a SQL string.
*/
compile(parseTree, params) {
const compOps = {
$eq: '=',
$neq: '<>',
$lt: '<',
$lte: '<=',
$gt: '>',
$gte: '>=',
$like: 'LIKE',
$notLike: 'NOT LIKE'
};
const nullOps = {
$is: 'IS',
$isnt: 'IS NOT'
};
const boolOps = {
$and: 'AND',
$or: 'OR'
};
const inOps = {
$in : 'IN',
$notIn: 'NOT IN'
};
params = params || {};
// Function to recursively traverse the parse tree and compile it.
function traverse(tree, escaper, params) {
// Helper to return a <value>, which may be a parameter, column, or number.
// The return is escaped properly.
function getValue(token, escaper) {
// The token could be a column, a parameter, or a number.
if (token.type === 'column')
return escaper.escapeFullyQualifiedColumn(token.value);
else if (token.type === 'parameter') {
// Find the value in the params list (the leading colon is removed).
const paramKey = token.value.substring(1);
const value = params[paramKey];
if (value === undefined)
throw new ConditionError(`Replacement value for parameter "${paramKey}" not present.`);
}
return token.value;
}
switch (tree.token.type) {
case 'comparison-operator': {
// <column> <comparison-operator> <value> (ex. `users`.`name` = :name)
// where value is a parameter, column, or number.
const column = escaper.escapeFullyQualifiedColumn(tree.children[0].token.value);
const op = compOps[tree.token.value];
const value = getValue(tree.children[1].token, escaper);
return `${column} ${op} ${value}`;
}
case 'null-comparison-operator': {
// <column> <null-operator> <nullable> (ex. `j`.`occupation` IS NULL).
// Note that if a parameter is used (e.g. {occupation: null}) it's
// ignored. NULL is blindly inserted since it's the only valid value.
const column = escaper.escapeFullyQualifiedColumn(tree.children[0].token.value);
const op = nullOps[tree.token.value];
return `${column} ${op} NULL`;
}
case 'in-comparison-operator': {
// <column> <in-comparison-operator> (<value> {, <value}) (ex. `shoeSize` IN (10, 10.5, 11)).
const column = escaper.escapeFullyQualifiedColumn(tree.children[0].token.value);
const op = inOps[tree.token.value];
const kids = tree.children
.slice(1)
.map(kid => getValue(kid.token, escaper))
.join(', ');
return `${column} ${op} (${kids})`;
}
case 'boolean-operator': {
// Each of the children is a <condition>. Put each <condition> in an array.
const kids = tree.children
.map(kid => traverse(kid, escaper, params))
.join(` ${boolOps[tree.token.value]} `);
// Explode the conditions on the current boolean operator (AND or OR).
// Boolean conditions must be wrapped in parens for precedence purposes.
return `(${kids})`;
}
default: {
// The only way this can fire is if the input parse tree did not come
// from the ConditionParser. Trees from the ConditionParser are
// guaranteed to be syntactically correct.
throw new Error(`Unknown type: ${tree.token.type}.`);
}
}
}
return traverse(parseTree, this._escaper, params);
}
/**
* Get all the columns referenced in the parse tree and return them as an
* array. The columns will be distinct (that is, if the same column
* appears multiple times in the same condition, it will exist in the
* returned array only once).
* @param parseTree The parse tree, as created by a ConditionParser.
*/
getColumns(parseTree) {
const columns = [];
// Recursively traverse tree.
(function traverse(tree, columns) {
// If the current node is a column and not yet in the list of columns, add it.
if (tree.token.type === 'column' && columns.indexOf(tree.token.value) === -1)
columns.push(tree.token.value);
// Recurse into all children.
for (let i = 0; i < tree.children.length; ++i)
traverse(tree.children[i], columns);
})(parseTree, columns);
return columns;
}
}
return ConditionCompiler;
}