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.
188 lines
6.5 KiB
JavaScript
188 lines
6.5 KiB
JavaScript
"use strict";
|
|
|
|
const syncpipe = require("syncpipe");
|
|
const splitFilter = require("split-filter");
|
|
const asExpression = require("as-expression");
|
|
|
|
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 merge = require("../merge");
|
|
|
|
const uniqueByPredicate = require("../unique-by-predicate");
|
|
|
|
// FIXME: Support for remote field names
|
|
|
|
function uniqueFields(fields) {
|
|
return uniqueByPredicate(fields, (field) => field.name);
|
|
}
|
|
|
|
/*
|
|
valid fields when collapsing:
|
|
- fields that appear in the collapseBy field list, within or without a hierarchical wrapper
|
|
- any field that is wrapped in an aggregrate function of some sort
|
|
*/
|
|
|
|
module.exports = {
|
|
name: "set-collapse-by-fields",
|
|
category: [ "normalization" ],
|
|
visitors: {
|
|
collapseBy: (_node, { setState }) => {
|
|
setState("isCollapsing", true);
|
|
return NoChange;
|
|
},
|
|
field: (node, { setState }) => {
|
|
setState("fieldSeen", node);
|
|
return NoChange;
|
|
},
|
|
// FIXME: Think of a generic way to express "only match fields under this specific child property" (+ direct descendants also?) -- maybe (node, property, parentNode) signature?
|
|
collapseByFields: (node, { registerStateHandler, setState, defer }) => {
|
|
let fields = [];
|
|
|
|
registerStateHandler("fieldSeen", (node) => {
|
|
fields.push(node);
|
|
});
|
|
|
|
return defer(() => {
|
|
setState("setCollapsedFields", fields);
|
|
return NoChange;
|
|
});
|
|
},
|
|
addFields: (node, { setState }) => {
|
|
setState("setAddFields", node.fields);
|
|
return NoChange;
|
|
},
|
|
onlyFields: (node, { setState }) => {
|
|
setState("setOnlyFields", node.fields);
|
|
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 onlyFields = [];
|
|
let addFields = [];
|
|
let computes = [];
|
|
let collapsedFields;
|
|
|
|
registerStateHandler("isCollapsing", (value) => {
|
|
isCollapsing = isCollapsing || value;
|
|
});
|
|
|
|
registerStateHandler("setCollapsedFields", (fields) => {
|
|
if (collapsedFields == null) {
|
|
collapsedFields = fields;
|
|
} 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("setOnlyFields", (fields) => {
|
|
onlyFields = onlyFields.concat(fields);
|
|
});
|
|
|
|
registerStateHandler("setAddFields", (fields) => {
|
|
addFields = addFields.concat(fields);
|
|
});
|
|
|
|
registerStateHandler("computeSeen", (node) => {
|
|
computes.push(node);
|
|
});
|
|
|
|
return defer(() => {
|
|
if (isCollapsing) {
|
|
if (addFields.length > 0) {
|
|
let extraFieldNames = addFields.map((field) => field.name);
|
|
|
|
throw new Error(`You tried to add extra fields (${extraFieldNames.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 (onlyFields.length > 0) {
|
|
// NOTE: This can happen either because the user specified an onlyFields clause, *or* because a previous run of this optimizer did so!
|
|
let uniqueSelectedFields = uniqueFields(onlyFields);
|
|
let collapsedFieldNames = collapsedFields.map((field) => field.name);
|
|
|
|
let invalidFieldSelection = uniqueSelectedFields.filter((node) => {
|
|
let isAggregrateComputation = typeOf(node) === "alias" && typeOf(node.expression) === "aggregrateFunction";
|
|
let isCollapsedField = typeOf(node) === "field" && collapsedFieldNames.includes(node.name);
|
|
|
|
let isValid = isAggregrateComputation || isCollapsedField;
|
|
|
|
return !isValid;
|
|
});
|
|
|
|
// FIXME: We can probably optimize this by marking the optimizer-created onlyFields as inherently-valid, via some sort of node metadata mechanism
|
|
|
|
if (invalidFieldSelection.length > 0) {
|
|
let invalidFieldNames = invalidFieldSelection.map((item) => {
|
|
let fieldType = typeOf(item);
|
|
|
|
if (fieldType === "field") {
|
|
return item.name;
|
|
} else if (fieldType === "alias") {
|
|
// FIXME: Show alias target instead of field name here?
|
|
return item.field.name;
|
|
} else {
|
|
return unreachable(`Encountered '${fieldType}' node in invalid fields`);
|
|
}
|
|
});
|
|
|
|
throw new Error(`You tried to include one or more field in your query (${invalidFieldNames.join(", ")}), that are not used in a collapseBy clause or aggregrate function. See [FIXME: link] for more information.`);
|
|
} else {
|
|
return NoChange;
|
|
}
|
|
} else {
|
|
// TODO: Move compute -> alias/withResult conversion out into its own optimizer eventually, as it's a separate responsibility
|
|
let [ withResultComputes, aliasComputes ] = splitFilter(computes, (node) => typeof node.expression === "function");
|
|
|
|
let computeAliases = aliasComputes.map((node) => {
|
|
return operations.alias(node.field, node.expression);
|
|
});
|
|
|
|
let withResultsOperations = asExpression(() => {
|
|
if (withResultComputes.length > 0) {
|
|
return operations.withResult((item) => {
|
|
// FIXME: Remote fields support
|
|
let computedFields = syncpipe(withResultComputes, [
|
|
(_) => _.map((compute) => {
|
|
let fieldName = compute.field.name;
|
|
return [ fieldName, compute.expression(item[fieldName]) ];
|
|
}),
|
|
(_) => Object.fromEntries(_)
|
|
]);
|
|
|
|
return merge(item, computedFields);
|
|
});
|
|
} else {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
return deriveNode(node, {
|
|
clauses: node.clauses.concat([
|
|
operations.onlyFields(concat([
|
|
collapsedFields,
|
|
computeAliases
|
|
])),
|
|
withResultsOperations
|
|
])
|
|
});
|
|
}
|
|
} else {
|
|
return NoChange;
|
|
}
|
|
});
|
|
},
|
|
}
|
|
};
|
|
|
|
// 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
|