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/set-collapse-by-columns.js

158 lines
5.5 KiB
JavaScript

"use strict";
const NoChange = require("./util/no-change");
const deriveNode = require("../derive-node");
const operations = require("../operations");
const typeOf = require("../type-of");
const unreachable = require("../unreachable");
const concat = require("../concat");
const uniqueByPredicate = require("../unique-by-predicate");
// FIXME: Support for foreign column names
function uniqueColumns(columns) {
return uniqueByPredicate(columns, (column) => column.name);
}
/*
valid columns when collapsing:
- columns that appear in the collapseBy column list, within or without a hierarchical wrapper
- any column that is wrapped in an aggregrate function of some sort
*/
module.exports = {
name: "set-collapse-by-columns",
category: [ "normalization" ],
visitors: {
collapseBy: (node, { setState }) => {
setState("isCollapsing", true);
return NoChange;
},
columnName: (node, { setState }) => {
setState("columnSeen", node);
return NoChange;
},
// FIXME: Think of a generic way to express "only match columns under this specific child property"
collapseByColumns: (node, { registerStateHandler, setState, defer }) => {
let columns = [];
registerStateHandler("columnSeen", (node) => {
columns.push(node);
});
return defer(() => {
setState("setCollapsedColumns", columns);
return NoChange;
});
},
addColumns: (node, { setState }) => {
setState("setAddColumns", node.columns);
return NoChange;
},
onlyColumns: (node, { setState }) => {
setState("setOnlyColumns", node.columns);
return NoChange;
},
aggregrateFunction: (node, { registerStateHandler }) => {
// FIXME: Also report isCollapsing here, due to aggregrate function use, but make sure that the error describes this as the (possible) cause
return NoChange;
},
compute: (node, { setState }) => {
setState("computeSeen", node);
return NoChange;
},
select: (node, { registerStateHandler, defer }) => {
let isCollapsing;
let onlyColumns = [];
let addColumns = [];
let computes = [];
let collapsedColumns;
registerStateHandler("isCollapsing", (value) => {
isCollapsing = isCollapsing || value;
});
registerStateHandler("setCollapsedColumns", (columns) => {
if (collapsedColumns == null) {
collapsedColumns = columns;
} else {
throw new Error(`You can currently only specify a single 'collapseBy' clause. Please file an issue if you have a reason to need more than one!`);
}
});
registerStateHandler("setOnlyColumns", (columns) => {
onlyColumns = onlyColumns.concat(columns);
});
registerStateHandler("setAddColumns", (columns) => {
addColumns = addColumns.concat(columns);
});
registerStateHandler("computeSeen", (node) => {
computes.push(node);
});
return defer(() => {
if (isCollapsing) {
if (addColumns.length > 0) {
let extraColumnNames = addColumns.map((column) => column.name);
throw new Error(`You tried to add extra columns (${extraColumnNames.join(", ")}) in your query, but this is not possible when using collapseBy. See [FIXME: link] for more information, and how to solve this.`);
} else if (onlyColumns.length > 0) {
// NOTE: This can happen either because the user specified an onlyColumns clause, *or* because a previous run of this optimizer did so!
let uniqueSelectedColumns = uniqueColumns(onlyColumns);
let collapsedColumnNames = collapsedColumns.map((column) => column.name);
let invalidColumnSelection = uniqueSelectedColumns.filter((node) => {
let isAggregrateComputation = typeOf(node) === "alias" && typeOf(node.expression) === "aggregrateFunction";
let isCollapsedColumn = typeOf(node) === "columnName" && collapsedColumnNames.includes(node.name);
let isValid = isAggregrateComputation || isCollapsedColumn;
return !isValid;
});
// FIXME: We can probably optimize this by marking the optimizer-created onlyColumns as inherently-valid, via some sort of node metadata mechanism
if (invalidColumnSelection.length > 0) {
let invalidColumnNames = invalidColumnSelection.map((column) => {
let columnType = typeOf(column);
if (columnType === "columnName") {
return column.name;
} else if (columnType === "alias") {
// FIXME: Show alias target instead of column name here?
return column.column.name;
} else {
return unreachable(`Encountered '${columnType}' node in invalid columns`);
}
});
throw new Error(`You tried to include one or more columns in your query (${invalidColumnNames.join(", ")}), that are not used in a collapseBy clause or aggregrate function. See [FIXME: link] for more information.`);
} else {
return NoChange;
}
} else {
let computeAliases = computes.map((node) => {
return operations.alias(node.column, node.expression);
});
return deriveNode(node, {
clauses: node.clauses.concat([
operations.onlyColumns(concat([
collapsedColumns,
computeAliases
]))
])
});
}
}
});
},
}
};
// FIXME: a ConsumeNode marker, like RemoveNode but it does not invalidate that node's state... may need to actually make it a reference, so that a parent node can decide whether to consume that node. Basically passing a "consume this node" function as a state value, that correctly internally triggers the optimizer infrastructure to change the tree as a result.
// FIXME: Consume the compute nodes, and have an optimizer that removes empty computeMultiple nodes