You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
raqb/src/optimizers/arrayify-predicate-lists.js

134 lines
4.4 KiB
JavaScript

"use strict";
const matchValue = require("match-value");
const flatten = require("flatten");
const syncpipe = require("syncpipe");
const operations = require("../operations");
const internalOperations = require("../internal-operations");
const concat = require("../concat");
const NoChange = require("./util/no-change");
// FIXME: Have some sort of internally-cacheable way to find nodes of a certain type? So that different optimizer visitors don't need to filter the list of clauses over and over again...
function leftIdentity(left) {
// NOTE: This uses JSON.stringify, since that gives us fast escaping for free; which is important to prevent bugs and/or injection-related security issues in the serialized names
if (left.type === "columnName") {
return `column:${JSON.stringify(left.name)}`;
} else if (left.type === "foreignColumnName") {
return `foreignColumn:${JSON.stringify([ left.table.name, left.column.name ])}`;
} else if (left.type === "sqlExpression") {
return `sqlExpression:${JSON.stringify(left.expression)}`;
} else {
return null;
}
}
function createExpressionTracker() {
let leftMapping = new Map();
let hasSeenPossibleArray = false;
return {
addExpression: function (expression) {
let identity = leftIdentity(expression.left);
let conditionType = expression.condition.conditionType;
if (!leftMapping.has(identity)) {
leftMapping.set(identity, new Map());
}
let conditionTypeMapping = leftMapping.get(identity);
if (!conditionTypeMapping.has(conditionType)) {
conditionTypeMapping.set(conditionType, []);
} else {
// Both the left identity and conditionType match, so this can be turned into an array
hasSeenPossibleArray = true;
}
// We store the entire original expression object, so that the new-node-generation code can pick out the expression metadata later. Since everything is grouped by identity and condition type, that code can just assume that the metadata of the first item in the list (if there's more than one) applies to *all* of the items in that list.
conditionTypeMapping.get(conditionType).push(expression);
},
getMapping: function () {
return leftMapping;
},
arrayIsPossible: function () {
return hasSeenPossibleArray;
}
};
}
function createHandler(type) {
// FIXME: Improve matchValue to distinguish between "arm not specified at all" and "arm holds undefined as a specified value", to deal with things like accidental operations.anyOfExpressions
let expressionOperation = matchValue.literal(type, {
"anyOfExpressions": operations.anyOf,
"allOfExpressions": operations.allOf
});
let internalArrayType = matchValue(type, {
"anyOfExpressions": "anyOf",
"allOfExpressions": "allOf"
});
return function arrayifyPredicateList(node) {
// FIXME: Also detect non-parameterizable cases like raw SQL!
let tracker = createExpressionTracker();
console.log(node);
for (let item of node.items) {
// Only regular expressions can be arrayified, not {all,any}OfExpressions, which will get visited by this optimizer later on anyway
// FIXME: Also ignore already-processed arrays
if (item.type === "expression") {
tracker.addExpression(item);
}
}
if (tracker.arrayIsPossible()) {
let newExpressions = syncpipe(tracker, [
(_) => _.getMapping(),
(_) => Array.from(_.values()),
(_) => _.map((conditionMapping) => syncpipe(conditionMapping, [
(_) => Array.from(_.entries()),
(_) => _.map(([ conditionType, expressions ]) => {
if (expressions.length === 1) {
return expressions[0];
} else {
let allValues = expressions.map((expression) => expression.condition.expression);
return operations.expression({
left: expressions[0].left,
condition: internalOperations._condition({
type: conditionType,
expression: internalOperations._arrayOf({
type: internalArrayType,
items: allValues
})
})
});
}
})
])),
(_) => flatten(_)
]);
let untouchedExpressions = node.items.filter((item) => item.type !== "expression");
return expressionOperation(concat([
newExpressions,
untouchedExpressions
]));
} else {
return NoChange;
}
};
}
module.exports = {
name: "arrayify-predicate-lists",
category: [ "readability" ],
visitors: {
allOfExpressions: createHandler("allOfExpressions"),
anyOfExpressions: createHandler("anyOfExpressions"),
}
};