From b5d0ca7c537499545d9b32050eba2bf043526b1d Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Fri, 3 Jul 2020 16:29:20 +0200 Subject: [PATCH] WIP (optimizer infrastructure) --- .eslintrc | 5 +- experiments/benchmark.js | 37 +++ experiments/raqb-concepts.js | 220 ++++++++++-------- notes.txt | 33 ++- package.json | 4 + src/ast-to-query.js | 153 ++---------- src/ast/optimize/index.js | 182 +++++++++++++++ src/evaluate-cyclical-modules-onto.js | 9 + src/filter-type.js | 5 + src/internal-operations/array-of.js | 25 ++ src/internal-operations/condition.js | 26 +++ src/internal-operations/index.js | 14 ++ src/measure-time.js | 12 + src/operations/equals.js | 4 +- src/operations/index.js | 12 +- src/operations/less-than.js | 5 +- src/operations/more-than.js | 4 +- src/operations/not.js | 3 + src/optimizers/arrayify-predicate-lists.js | 133 +++++++++++ src/optimizers/collapse-where.js | 27 +++ src/optimizers/conditions-to-expressions.js | 46 ++++ src/optimizers/flatten-not-predicates.js | 52 +++++ src/optimizers/flatten-predicate-lists.js | 66 ++++++ src/optimizers/util/no-change.js | 3 + src/optimizers/util/remove-node.js | 3 + src/unreachable.js | 2 +- .../operations/is-internal-array-type.js | 7 +- yarn.lock | 69 ++++-- 28 files changed, 870 insertions(+), 291 deletions(-) create mode 100644 experiments/benchmark.js create mode 100644 src/ast/optimize/index.js create mode 100644 src/evaluate-cyclical-modules-onto.js create mode 100644 src/filter-type.js create mode 100644 src/internal-operations/array-of.js create mode 100644 src/internal-operations/condition.js create mode 100644 src/internal-operations/index.js create mode 100644 src/measure-time.js create mode 100644 src/optimizers/arrayify-predicate-lists.js create mode 100644 src/optimizers/collapse-where.js create mode 100644 src/optimizers/conditions-to-expressions.js create mode 100644 src/optimizers/flatten-not-predicates.js create mode 100644 src/optimizers/flatten-predicate-lists.js create mode 100644 src/optimizers/util/no-change.js create mode 100644 src/optimizers/util/remove-node.js diff --git a/.eslintrc b/.eslintrc index b0108ff..59f42a3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "@joepie91/eslint-config" + "extends": "@joepie91/eslint-config", + "parserOptions": { + "ecmaVersion": 2020 + } } diff --git a/experiments/benchmark.js b/experiments/benchmark.js new file mode 100644 index 0000000..c7413c7 --- /dev/null +++ b/experiments/benchmark.js @@ -0,0 +1,37 @@ +"use strict"; + +const benchmark = require("benchmark"); + +const { select, where, anyOf, allOf, lessThan, moreThan, unsafeSQL, parameter } = require("../src/operations"); + +let suite = new benchmark.Suite(); + +suite + .add("Build standard query", () => { + let query = select("projects", [ + where({ + foo: anyOf([ "bar", "baz", anyOf([ "bar2", "baz2" ]), unsafeSQL("TRUE") ]), + qux: anyOf([ 13, moreThan(42) ]), + complex: anyOf([ + 30, + 40, + allOf([ + moreThan(100), + lessThan(200), + lessThan(parameter("max")) + ]) + ]) + }), + where({ second: 2 }) + ]); + }) + .on("cycle", (event) => { + console.log(String(event.target)); + }) + .on("complete", function () { + console.log('Fastest is ' + this.filter('fastest').map('name')); + }) + .run({ + async: true, + minSamples: 500 + }); diff --git a/experiments/raqb-concepts.js b/experiments/raqb-concepts.js index aa390bc..b58c2aa 100644 --- a/experiments/raqb-concepts.js +++ b/experiments/raqb-concepts.js @@ -1,10 +1,23 @@ "use strict"; +Error.stackTraceLimit = 100; + const util = require("util"); -let { select, onlyColumns, where, withRelations, withDerived, column, through, inValues, sql, postProcess, belongsTo, has, value, parameter, not, anyOf, allOf, lessThan, moreThan, alias, foreignColumn, table, expression, equals } = require("../src/operations"); +const chalk = require("chalk"); + +let { select, onlyColumns, where, withRelations, withDerived, column, through, inValues, unsafeSQL, postProcess, belongsTo, has, value, parameter, not, anyOf, allOf, lessThan, moreThan, alias, foreignColumn, table, expression, equals } = require("../src/operations"); const astToQuery = require("../src/ast-to-query"); +const optimizeAST = require("../src/ast/optimize"); +const measureTime = require("../src/measure-time"); + +let optimizers = [ + require("../src/optimizers/collapse-where"), + require("../src/optimizers/conditions-to-expressions"), + require("../src/optimizers/flatten-not-predicates"), + require("../src/optimizers/flatten-predicate-lists"), + require("../src/optimizers/arrayify-predicate-lists"), +]; -Error.stackTraceLimit = Infinity; function withOwner() { return withRelations({ owner: "owner_id" }); @@ -13,8 +26,6 @@ function withOwner() { // FIXME: Mark AST nodes with a special marker -- and disallow these from being interpreted as a WHERE object! // FIXME: Figure out composability for something like connect-session-knex -console.time("queryGen"); - ////// VALIDATION TESTING START /////// let query; @@ -27,102 +38,125 @@ let c = 452; // let flooz = { a: anyOf }; try { - // query = expression({ - // left: "foo", - // condition: equals("bar") - // }); + let buildingResult = measureTime(() => { + // query = expression({ + // left: "foo", + // condition: equals("bar") + // }); + - - query = select("projects", [ - where({ - // foo: anyOf([ "bar", "baz", anyOf([ "bar2", "baz2" ]), sql("TRUE") ]), - // qux: anyOf([ 13, moreThan(42) ]), - complex: anyOf([ - 30, - 40, - allOf([ - moreThan(100), - lessThan(200), - lessThan(parameter("max")) + query = select("projects", [ + where({ + // foo: anyOf([ "bar", not(not("baz")), anyOf([ "bar2", "baz2" ]), unsafeSQL("TRUE") ]), + // qux: anyOf([ 13, moreThan(42) ]), + complex: anyOf([ + 30, + 40, + allOf([ + moreThan(100), + lessThan(200), + lessThan(parameter("max")) + ]) ]) - ]) - }), - where({ second: 2 }) - ]); - - // query = select("projects", [ - // onlyColumns([ - // "foo", - // alias("bar", 42), - // alias("baz", sql("foo")) - // ]), - // where(anyOf([ - // { foo: "bar", qux: anyOf([ "quz", "quy" ]) }, - // { baz: lessThan(42) } - // ])) - // ]); - - /* { - query: 'SELECT foo, ? AS bar, foo AS baz FROM projects WHERE foo = ? OR baz < ?;', - params: [ 42, 'bar', 42 ], - placeholders: [] - } */ - - - - // query = select("projects", [ - // onlyColumns([ "id", "name" ]), - // where({ - // active: true, - // visible: true, - // // primary_category_id: anyOf([ 2, 3, 5, 7, 8 ]) - // // primary_category_id: anyOf(parameter("categoryIDs")) - // primary_category_id: not(anyOf(parameter("categoryIDs"))) // FIXME/MARKER: This gets stringified wrong! - // }), - // // FIXME: where pivot table entry exists for category in that list - // withRelations({ - // primaryCategory: belongsTo("primary_category_id", { query: [ withOwner() ] }), - // categories: through([ - // has("projects_categories.project_id", { query: [ - // // Optional extra clauses for the query on the pivot table, eg. for filtering entries - // where({ adminApproved: true }) - // ]}), - // "category_id" - // ]), - - // // all user groups for a given project ID -> all memberships for the given user group IDs -> for each membership, the record referenced by the given user_id - // users: through([ "user_groups.project_id", "membership.user_group_id", "user_id" ]), - // // ... expands to ... - // // users: through([ - // // has({ column: foreignColumn({ table: "user_groups", column: "project_id" }) }), - // // has({ column: foreignColumn({ table: "memberships", column: "user_group_id" }) }), - // // belongsTo({ column: column("user_id") }), - // // ]), - - // owner: "owner_id", - // // ... expands to - // // owner: belongsTo({ column: "owner_id" }), + }), + // where({ second: 2 }), + // where(not({ + // thirdA: 3, + // thirdB: 3 + // })), + // where(anyOf([ { foo: "bar" } ])) + ]); + + // query = select("projects", [ + // onlyColumns([ + // "foo", + // alias("bar", 42), + // alias("baz", sql("foo")) + // ]), + // where(anyOf([ + // { foo: "bar", qux: anyOf([ "quz", "quy" ]) }, + // { baz: lessThan(42) } + // ])) + // ]); + + /* { + query: 'SELECT foo, ? AS bar, foo AS baz FROM projects WHERE foo = ? OR baz < ?;', + params: [ 42, 'bar', 42 ], + placeholders: [] + } */ + + + + // query = select("projects", [ + // onlyColumns([ "id", "name" ]), + // where({ + // active: true, + // visible: true, + // // primary_category_id: anyOf([ 2, 3, 5, 7, 8 ]) + // // primary_category_id: anyOf(parameter("categoryIDs")) + // primary_category_id: not(anyOf(parameter("categoryIDs"))) // FIXME/MARKER: This gets stringified wrong! + // }), + // // FIXME: where pivot table entry exists for category in that list + // withRelations({ + // primaryCategory: belongsTo("primary_category_id", { query: [ withOwner() ] }), + // categories: through([ + // has("projects_categories.project_id", { query: [ + // // Optional extra clauses for the query on the pivot table, eg. for filtering entries + // where({ adminApproved: true }) + // ]}), + // "category_id" + // ]), + + // // all user groups for a given project ID -> all memberships for the given user group IDs -> for each membership, the record referenced by the given user_id + // users: through([ "user_groups.project_id", "membership.user_group_id", "user_id" ]), + // // ... expands to ... + // // users: through([ + // // has({ column: foreignColumn({ table: "user_groups", column: "project_id" }) }), + // // has({ column: foreignColumn({ table: "memberships", column: "user_group_id" }) }), + // // belongsTo({ column: column("user_id") }), + // // ]), + + // owner: "owner_id", + // // ... expands to + // // owner: belongsTo({ column: "owner_id" }), + + // releases: "releases.project_id", + // // ... expands to ... + // // releases: has({ column: "releases.project_id" }) + // }), + // withDerived({ + // capitalized_name: sql("UPPER(name)"), + // team_count: sql("moderator_count + admin_count"), + // // fourty_two: value(42), // This makes no sense in withDerived! + // name_distance: (project) => wordDistanceAlgorithm(project.name, "someReferenceName") // NOTE: This could have returned a Promise! + // }), + // mapCase({ from: "snake", to: "camel" }) + // ]); + }); + + console.log(util.inspect(query, { depth: null, colors: true })); + console.log(""); + + let optimizerResult = optimizeAST(query, optimizers); - // releases: "releases.project_id", - // // ... expands to ... - // // releases: has({ column: "releases.project_id" }) - // }), - // withDerived({ - // capitalized_name: sql("UPPER(name)"), - // team_count: sql("moderator_count + admin_count"), - // // fourty_two: value(42), // This makes no sense in withDerived! - // name_distance: (project) => wordDistanceAlgorithm(project.name, "someReferenceName") // NOTE: This could have returned a Promise! - // }), - // mapCase({ from: "snake", to: "camel" }) - // ]); + console.log(util.inspect(optimizerResult.ast, { depth: null, colors: true })); + + function toMS(time) { + return (Number(time) / 1e6).toFixed(2); + } + let stringifyResult = measureTime(() => astToQuery(optimizerResult.ast)); + console.log(`\n${chalk.bold("Generation timings:")}`); + console.log(`${chalk.yellow("Building")}: ${chalk.cyan(toMS(buildingResult.time))}ms`); + console.log(`${chalk.yellow("Stringifying")}: ${chalk.cyan(toMS(stringifyResult.time))}ms`); - console.timeEnd("queryGen"); + console.log(`\n${chalk.bold("Optimization timings:")}`); + Object.entries(optimizerResult.timings).forEach(([ name, time ]) => { + console.log(`${chalk.yellow(name)}: ${chalk.cyan(toMS(time))}ms`); + }); - console.log(util.inspect(query, { depth: null, colors: true })); - console.log(""); - console.log(util.inspect(astToQuery(query), { depth: null, colors: true })); + console.log(util.inspect(stringifyResult.value, { depth: null, colors: true })); } catch (error) { // console.error(error.message); // console.error(""); diff --git a/notes.txt b/notes.txt index 5f73275..072ecbe 100644 --- a/notes.txt +++ b/notes.txt @@ -100,7 +100,7 @@ Generic AST optimization infrastructure instead of inline modifications in the s Build AST -> Optimize AST -> Compile to SQL Three AST optimization purposes: -- Normalization (required) +- Normalization/correctness (required) - Readability (optional) - Performance (optional) @@ -123,20 +123,37 @@ Walker design: - Possibly cursors are a useful concept to use here? As mutable pointers to otherwise immutable data. This might cause unexpected behaviour though, with different items being walked than currently (newly) exist in the tree, as the walker would still be operating on the old tree -- unless a change higher-up would trigger a re-walk of that entire subtree, which would require all optimizations to be idempotent. - Make sure to iterate again from whatever new AST subtree was returned from a visitor! So that other visitors get a chance. - How to deal with a case where in notExpression(foo), foo returns a notExpression(bar), and the notExpression deduplicator does not happen anymore because the top-most notExpression was already previously visited? Maybe re-run the whole sequence of visitors repeatedly until the entire tree has stabilized? This also requires idempotence of visitors! And makes persisting/caching processed query ASTs even more important. +- Apply a walking iteration limit to detect trees that will never stabilize +- Change handling strategies: + - After change to a parent marker: abandon current subtree, move back to new subtree + - After completion of walk: re-walk, check if the tree has stabilized +- Allow the walker itself to mutate nodes, but set a "has mutated" flag whenever a visitor returns a new subtree, to know that it has not stabilized yet +- Allow the user to disable certain categories of optimizations (readability, performance) but not critical ones (normalization) + - For this to be possible, select(...) and such cannot auto-optimize! Because there would be no logical place to specify this configuration + - A hacky workaround would be a configuration node wrapped around the clauses, though... + - Probably better to provide an `optimize` wrapper node that goes around the entire query; either the user can specify it themselves with custom optimization configuration, or the query executor will do it by itself automatically, maintaining an internal cache for this in a WeakMap so that the user doesn't have to care about it. +- Optimizers should be provided as a single array of category-tagged items, rather than a separate array for each category; otherwise the correct execution order cannot be ensured between items of different categories that are nevertheless dependent on one of the two having run first. +- Optimizers *must* a) be idempotent and b) have a terminal (NoChange) condition! ------- Planned AST optimization phases (loosely ordered): -- Combine multiple `where(...)` into a single `where(allOf(...))` -- Flatten nested notCondition -- Flatten nested same-typed allOfCondition/anyOfCondition -- Group >1 same-typed conditionTypes into a single condition(_internalArray(...)) -- Invert condition lists into expression lists -- Flatten nested notExpression -- Flatten nested same-typed allOfExpression/anyOfExpression +x Combine multiple `where(...)` into a single `where(allOf(...))` +x Flatten nested notCondition +x Group >1 same-typed conditionTypes into a single condition(_internalArray(...)) +x Invert condition modifiers (not, anyOf, allOf, ...) into expression modifiers +x Flatten nested notExpression +x Flatten nested same-typed allOfCondition/anyOfCondition +x Flatten nested same-typed allOfExpression/anyOfExpression ----- MARKER: - Refactor relation operations to new design - Implement AST optimization infrastructure (incl. solving the immutable reference problem?) + +----- + +NOTE: May need https://www.npmjs.com/package/browser-hrtime for process.hrtime.bigint support in browsers! Need to investigate whether this (or something similar) is already being used by bundlers by default, or whether they use a shim without bigint support. +FIXME: Remove all the .type stuff and replace with typeOf() +FIXME: Document that `DEBUG=raqb:ast:optimize:*` can be used for tracking down unstable optimizers (symptom = stack overflow) diff --git a/package.json b/package.json index 65bdffc..52810f7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@validatem/is-value": "^0.1.0", "@validatem/matches-format": "^0.1.0", "@validatem/nested-array-of": "^0.1.0", + "@validatem/one-of": "^0.1.1", "@validatem/required": "^0.1.1", "@validatem/wrap-error": "^0.1.2", "acorn": "^6.0.5", @@ -31,6 +32,8 @@ "as-expression": "^1.0.0", "assure-array": "^1.0.0", "astw": "^2.2.0", + "chalk": "^4.1.0", + "debug": "^4.1.1", "default-value": "^1.0.0", "estree-assign-parent": "^1.0.0", "flatten": "^1.0.3", @@ -43,6 +46,7 @@ }, "devDependencies": { "@joepie91/eslint-config": "^1.1.0", + "benchmark": "^2.1.4", "eslint": "^7.3.1", "nodemon": "^2.0.4" } diff --git a/src/ast-to-query.js b/src/ast-to-query.js index 105e509..8f2e094 100644 --- a/src/ast-to-query.js +++ b/src/ast-to-query.js @@ -1,6 +1,9 @@ "use strict"; // FIXME: Make sure to account for placeholder nodes *within* arrays passed as single parameters, in query optimization/compilation/execution! +// FIXME: Modularize stringification as well? + +// NOTE: The stringification code *intentionally* does not allow combining strings and $objects in a single template string! All literal strings that need to be inserted into a query, must be wrapped in an $object to explicitly signal that they are considered safe to insert. require("array.prototype.flat").shim(); // FIXME: Use `flatten` method instead const splitFilterN = require("split-filter-n"); @@ -91,8 +94,6 @@ function $join(joiner, items) { function $joinWhereClauses(joiner, clauses) { let $handledClauses = clauses.map((clause) => { - console.log("CONSIDERING CLAUSE", require("util").inspect(clause, { colors: true, depth: null })); - let isSimpleClause = (typeOf(clause) === "expression"); let $handled = $handle(clause); @@ -206,125 +207,6 @@ function $arrayFrom(items) { } } -// TODO: Eventually have some sort of generic Babel-esque AST transformation setup for all of these unpacking functions? - -function unpackNotConditions(expression) { - // This translates `expression(notCondition(condition))` to `notExpression(expression(condition))`, including for multiple nested levels of `notCondition` - let notLevels = 0; - let currentItem = expression.condition; - - while(typeOf(currentItem) === "notCondition") { - notLevels += 1; - currentItem = currentItem.condition; - } - - // This handles double negatives, which may occur if someone is unknowingly nesting `not` conditions; eg. when generating query chunks with a third-party library and inverting the result - // FIXME: Generalize not-deduplication to work for user-provided `notExpression` as well? -> This should be moved to generic AST optimization infrastructure anyway! - let hasNot = (notLevels % 2) === 1; - let unpackedExpression = merge(expression, { condition: currentItem }); - - if (hasNot) { - return operations.not(unpackedExpression); - } else { - return unpackedExpression; - } -} - -function flattenConditionList(conditionList) { - let ownType = typeOf(conditionList); - - // TODO: Replace with Array#flat when Node 10 goes EOL (April 2021) - return flatten(conditionList.items.map((item) => { - let itemType = typeOf(item); - - if (itemType === "condition") { - return item; - } else if (itemType === ownType) { - return flattenConditionList(item); - } else { - // Inverse type (eg. any instead of all); just return it as-is, and let the caller deal with that and the necessary recursion - return item; - } - })); -} - -let allConditionTypes = [ "moreThan", "equals", "lessThan", "list" ]; - -function unpackListConditions(expression) { - // This (recursively) translates `expression(anyOfConditions([ condition1, condition2 ]))` into anyOfExpressions([ expression(condition1), expression(condition2) ]), with some batching of like-typed conditions for more compact queries - // FIXME: Merge multiple `where` clauses, prior to having it processed here, eg. in the `select` handler or an abstraction that can be used in other places (eg. for CHECK constraints) - - function conditionsToExpressions(conditionList) { - let listType = typeOf(conditionList); - - if (listType === "condition") { - // Nothing to unpack here, this is not actually a list. - return expression; - } else if (listType === "anyOfConditions" || listType === "allOfConditions") { - // FIXME: Detect unexpected conditionType - let conditionsByType = syncpipe(conditionList, [ - (_) => flattenConditionList(_), - (_) => splitFilterN(_, allConditionTypes, (condition) => defaultValue(condition.conditionType, "list")) - ]); - - let newExpressions = syncpipe(allConditionTypes, [ - (_) => _.map((conditionType) => { - if (conditionType === "list") { - return conditionsByType.list.map((subList) => { - return conditionsToExpressions(subList); - }); - } else { - let relevantConditions = conditionsByType[conditionType]; - - if (relevantConditions.length === 0) { - return null; - } else if (relevantConditions.length === 1) { - return operations.expression({ - left: expression.left, - condition: relevantConditions[0] - }); - } else { - // [lessThan(a), lessThan(b)] -> lessThan(_internalArray([a, b])) - let internalArrayType = matchValue(listType, { - anyOfConditions: "_internalAnyOfArray", - allOfConditions: "_internalAllOfArray", - }); - - // FIXME: Can this be represented with standard operations alone? Or would that result in an infinite cycle of list condition unpacking? The dynamic access on a public API is also pretty nasty... - return operations.expression({ - left: expression.left, - condition: operations[conditionType]({ - type: internalArrayType, - items: relevantConditions.map((condition) => condition.expression) - }) - }); - } - } - }), - (_) => _.filter((expression) => expression != null), - (_) => flatten(_) - ]); - - if (newExpressions.length === 0) { - unreachable("Left with 0 new expressions after unpacking"); - } else if (newExpressions.length === 1) { - return newExpressions[0]; - } else { - let wrapperType = matchValue.literal(listType, { - anyOfConditions: operations.anyOf, - allOfConditions: operations.allOf - }); - - return wrapperType(newExpressions); - } - } else { - unreachable(`Unexpected condition type '${listType}'`); - } - } - - return conditionsToExpressions(expression.condition); -} - // FIXME: createQueryObject/$ wrapper, including properties like relation mappings and dependent query queue let process = { @@ -394,17 +276,11 @@ let process = { }); }, expression: function (expression) { - // FIXME: wrap nested expression in parens - let unpackedExpression = syncpipe(expression, [ - (_) => unpackNotConditions(expression), - (_) => unpackListConditions(expression) - ]); - - if (typeOf(unpackedExpression) !== "expression") { + if (typeOf(expression) !== "expression") { // We got back an `expression` that was wrapped in a `notExpression`/`anyOfExpression`/`allOfExpression`, so let the respective handler deal with that wrapper first, and we'll come back here later when that handler invokes $handle again with a (now unpackable-condition-less) expression. - return $handle(unpackedExpression); + return $handle(expression); } else { - let { left, condition } = unpackedExpression; + let { left, condition } = expression; return $combine`${$handle(left)} ${$handle(condition)}`; } @@ -412,13 +288,16 @@ let process = { notExpression: function ({ expression }) { return $combine`NOT (${$handle(expression)})`; }, - _internalAnyOfArray: function ({ items }) { - // anyOfConditions: function ({ items }) { - return $combine`ANY(${$maybeParenthesize($arrayFrom(items))})`; - }, - _internalAllOfArray: function ({ items }) { - // allOfConditions: function ({ items }) { - return $combine`ALL(${$maybeParenthesize($arrayFrom(items))})`; + _arrayOf: function ({ listType, items }) { + let $keyword = $object({ + query: matchValue(listType, { + anyOf: "ANY", + allOf: "ALL" + }) + }); + + // return $combine`${keyword}(${$maybeParenthesize($arrayFrom(items))})`; + return $combine`${$keyword}(${$arrayFrom(items)})`; }, anyOfExpressions: function ({ items }) { if (items.length === 1) { diff --git a/src/ast/optimize/index.js b/src/ast/optimize/index.js new file mode 100644 index 0000000..d5b3f71 --- /dev/null +++ b/src/ast/optimize/index.js @@ -0,0 +1,182 @@ +"use strict"; + +const util = require("util"); +const syncpipe = require("syncpipe"); +const debug = require("debug"); + +const NoChange = require("../../optimizers/util/no-change"); +const RemoveNode = require("../../optimizers/util/remove-node"); +const unreachable = require("../../unreachable"); +const typeOf = require("../../type-of"); +const measureTime = require("../../measure-time"); + +// FIXME: Consider deepcopying the tree once, and then mutating that tree, instead of doing everything immutably; this might be significantly faster when a few iterations are needed to stabilize the tree, as that might otherwise result in many copies of the subtree(s) leeding up to the changed node(s), one for each iteration. +// FIXME: Consider whether inverting the evaluation order (deepest-first rather than shallowest-first) can remove the need for multiple optimization passes and stabilization detection. +// FIXME: Verify that changed nodes actually result in a change in where the walker goes! + +function createDebuggers(optimizers) { + let debuggers = {}; + + for (let optimizer of optimizers) { + debuggers[optimizer.name] = debug(`raqb:ast:optimize:${optimizer.name}`); + } + + return debuggers; +} + +function createTimings(optimizers) { + let timings = {}; + + for (let optimizer of optimizers) { + timings[optimizer.name] = 0n; + } + + return timings; +} + +function combineOptimizers(optimizers) { + let allVisitors = {}; + + for (let optimizer of optimizers) { + for (let [ key, visitor ] of Object.entries(optimizer.visitors)) { + if (allVisitors[key] == null) { + allVisitors[key] = []; + } + + allVisitors[key].push({ + name: optimizer.name, + func: visitor + }); + } + } + + return allVisitors; +} + +// FIXME: StopMatching marker to signal that eg. a generic visitor should no longer match after a specific one? +// FIXME: OriginalNode marker to explicitly indicate that any transformations applied by *other* visitors should be thrown out? + +module.exports = function optimizeTree(ast, optimizers) { + // NOTE: Depth-first! + let visitors = combineOptimizers(optimizers); + let timings = createTimings(optimizers); + let debuggers = createDebuggers(optimizers); + // FIXME: Dirty tracking for stabilization detection + + function applyVisitors(node, visitors) { + if (visitors == null) { + // We handle this here to make the `handle` pipeline more readable + return node; + } else { + let lastNode = node; + + for (let visitor of visitors) { + // eslint-disable-next-line no-loop-func + let { value: result, time } = measureTime(() => { + return visitor.func(lastNode); + }); + + timings[visitor.name] += time; + + if (result === NoChange) { + // no-op + } else if (result == null) { + throw new Error(`A visitor is not allowed to return null or undefined; if you intended to leave the node untouched, return a NoChange marker instead`); + } else if (result === RemoveNode) { + debuggers[visitor.name](`Node of type '${typeOf(lastNode)}' removed`); + lastNode = RemoveNode; + break; // Node has gone stale, stop applying visitors to it + } else if (result.__raqbASTNode === true) { + // New subtree to replace the old one + if (result === node) { + // Visitor returned the original node again; but in this case, it should return NoChange instead. We enforce this because after future changes to the optimizer implementation (eg. using an internally-mutable deep copy of the tree), we may no longer be able to *reliably* detect when the original node is returned; so it's best to already get people into the habit of returning a NoChange marker in those cases, by disallowing this. + throw new Error(`Visitor returned original node, but this may not work reliably; if you intended to leave the node untouched, return a NoChange marker instead`); + } else { + debuggers[visitor.name](`Node of type '${typeOf(lastNode)}' replaced by node of type '${typeOf(result)}'`); + lastNode = result; + break; // Node has gone stale, stop applying visitors to it + } + } else { + throw new Error(`Visitor returned an unexpected type of return value: ${util.inspect(result)}`); + } + } + + if (lastNode !== node) { + // We re-evalue the new node before leaving control to the children handler, as the old one has been substituted, and therefore new visitors might be applicable. + return handleSelf(lastNode); + } else { + return lastNode; + } + } + } + + function handleSelf(node) { + return syncpipe(node, [ + (_) => applyVisitors(_, visitors[_.type]), + (_) => applyVisitors(_, visitors["*"]), + ]); + } + + function handleChildren(node) { + // FIXME: Eventually hardcode the available properties for different node types (and them being single/multiple), for improved performance? + let changedProperties = {}; + + for (let [ property, value ] of Object.entries(node)) { + if (value == null) { + continue; + } else if (value.__raqbASTNode === true) { + changedProperties[property] = handle(value); + } else if (Array.isArray(value) && value.length > 0 && value[0].__raqbASTNode === true) { + // NOTE: We assume that if an array in an AST node property contains one AST node, *all* of its items are AST nodes. This should be ensured by the input wrapping in the operations API. + changedProperties[property] = value + .map((item) => handle(item)) + .filter((item) => item !== RemoveNode); + } else { + // Probably some kind of literal value; we don't touch these. + continue; + } + } + + if (Object.keys(changedProperties).length === 0) { + return node; + } else { + let newNode = Object.assign({}, node, changedProperties); + + // FIXME: Think carefully about whether there is *ever* a valid reason to remove a single node! As array items are already taken care of above, and leave an empty array at worst, which can make sense. Possibly we even need to encode this data into node type metadata. + for (let [ key, value ] of Object.entries(newNode)) { + if (value === RemoveNode) { + delete newNode[key]; + } + } + + return newNode; + } + } + + function handle(node) { + // FIXME: Possibly optimize the "node gets returned unchanged" case, somehow? Perhaps by propagating the NoChange marker? But object creation is fast, so that may actually make things slower than just blindly creating new objects... + return syncpipe(node, [ + (_) => handleSelf(_), + (_) => handleChildren(_) + ]); + } + + let { value: rootNode, time } = measureTime(() => { + return handle(ast); + }); + + let timeSpentInOptimizers = Object.values(timings).reduce((sum, n) => sum + n, 0n); + + if (rootNode !== RemoveNode) { + return { + ast: rootNode, + timings: { + "# Total": time, + "# Walker overhead": time - timeSpentInOptimizers, + ... timings, + } + }; + } else { + unreachable("Root node was removed"); + } +}; diff --git a/src/evaluate-cyclical-modules-onto.js b/src/evaluate-cyclical-modules-onto.js new file mode 100644 index 0000000..2801889 --- /dev/null +++ b/src/evaluate-cyclical-modules-onto.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = function evaluateCyclicalModulesOnto(resultObject = {}, moduleMapping) { + for (let [ key, moduleInitializer ] of Object.entries(moduleMapping)) { + resultObject[key] = moduleInitializer(resultObject); + } + + return resultObject; +}; diff --git a/src/filter-type.js b/src/filter-type.js new file mode 100644 index 0000000..c62c30a --- /dev/null +++ b/src/filter-type.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function filterType(type, allNodes) { + return allNodes.filter((node) => node.type === type); +}; diff --git a/src/internal-operations/array-of.js b/src/internal-operations/array-of.js new file mode 100644 index 0000000..fd63011 --- /dev/null +++ b/src/internal-operations/array-of.js @@ -0,0 +1,25 @@ +"use strict"; + +const { validateOptions } = require("@validatem/core"); +const required = require("@validatem/required"); +const oneOf = require("@validatem/one-of"); +const arrayOf = require("@validatem/array-of"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isValueExpression = require("../validators/operations/is-value-expression")(operations); + + return function _arrayOf(_options) { + let { type, items } = validateOptions(arguments, { + type: [ required, oneOf([ "anyOf", "allOf" ]) ], + items: [ required, arrayOf(isValueExpression) ] + }); + + return node({ + type: "_arrayOf", + listType: type, + items: items + }); + }; +}; diff --git a/src/internal-operations/condition.js b/src/internal-operations/condition.js new file mode 100644 index 0000000..0001873 --- /dev/null +++ b/src/internal-operations/condition.js @@ -0,0 +1,26 @@ +"use strict"; + +const { validateOptions } = require("@validatem/core"); +const required = require("@validatem/required"); +const oneOf = require("@validatem/one-of"); +const either = require("@validatem/either"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations); + const isValueExpression = require("../validators/operations/is-value-expression")(operations); + + return function _condition(_options) { + let { type, expression } = validateOptions(arguments, { + type: [ required, oneOf([ "lessThan", "moreThan", "equals" ]) ], + expression: [ required, either([ isValueExpression, isInternalArrayType ]) ] + }); + + return node({ + type: "condition", + conditionType: type, + expression: expression + }); + }; +}; diff --git a/src/internal-operations/index.js b/src/internal-operations/index.js new file mode 100644 index 0000000..067e429 --- /dev/null +++ b/src/internal-operations/index.js @@ -0,0 +1,14 @@ +"use strict"; + +const operations = require("../operations"); +const evaluateCyclicalModulesOnto = require("../evaluate-cyclical-modules-onto"); + +// Shallow clone, so that internal operations can see the public API, but not vice versa +let internalOperations = Object.assign({}, operations); + +evaluateCyclicalModulesOnto(internalOperations, { + _condition: require("./condition"), + _arrayOf: require("./array-of"), +}); + +module.exports = internalOperations; diff --git a/src/measure-time.js b/src/measure-time.js new file mode 100644 index 0000000..e7eaa18 --- /dev/null +++ b/src/measure-time.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = function measureTime(func) { + let startTime = process.hrtime.bigint(); + let result = func(); + let endTime = process.hrtime.bigint(); + + return { + value: result, + time: (endTime - startTime) + }; +}; diff --git a/src/operations/equals.js b/src/operations/equals.js index 5fde67c..f53a271 100644 --- a/src/operations/equals.js +++ b/src/operations/equals.js @@ -2,17 +2,15 @@ const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); -const either = require("@validatem/either"); const node = require("../ast-node"); module.exports = function (operations) { const isValueExpression = require("../validators/operations/is-value-expression")(operations); - const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations); return function equals(_name, _expression) { let [ expression ] = validateArguments(arguments, { - expression: [ required, either([ isValueExpression, isInternalArrayType ]) ] + expression: [ required, isValueExpression ] }); return node({ type: "condition", conditionType: "equals", expression: expression }); diff --git a/src/operations/index.js b/src/operations/index.js index c699cac..9182d94 100644 --- a/src/operations/index.js +++ b/src/operations/index.js @@ -21,8 +21,8 @@ const anyProperty = require("@validatem/any-property"); const node = require("../ast-node"); -// MARKER: Modularizing operations (and eventually also AST stringification?) const flatten = require("../validators/flatten"); +const evaluateCyclicalModulesOnto = require("../evaluate-cyclical-modules-onto"); // FIXME: Hack until all operations have been moved over to modules const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(module.exports); @@ -157,16 +157,6 @@ let operationModules = { where: require("./where"), }; -function evaluateCyclicalModulesOnto(resultObject, moduleMapping) { - // let resultObject = {}; // FIXME: Uncomment after refactoring is complete - - for (let [ key, moduleInitializer ] of Object.entries(moduleMapping)) { - resultObject[key] = moduleInitializer(resultObject); - } - - return resultObject; -} - Object.assign(module.exports, operations); evaluateCyclicalModulesOnto(module.exports, operationModules); diff --git a/src/operations/less-than.js b/src/operations/less-than.js index 5b2ef2a..27c52ab 100644 --- a/src/operations/less-than.js +++ b/src/operations/less-than.js @@ -2,19 +2,18 @@ const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); -const either = require("@validatem/either"); const node = require("../ast-node"); module.exports = function (operations) { const isValueExpression = require("../validators/operations/is-value-expression")(operations); - const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations); return function lessThan(_name, _expression) { let [ expression ] = validateArguments(arguments, { - expression: [ required, either([ isValueExpression, isInternalArrayType ]) ] + expression: [ required, isValueExpression ] }); + // FIXME: Calling it `expression` is super confusing. That needs a better name. return node({ type: "condition", conditionType: "lessThan", expression: expression }); }; }; diff --git a/src/operations/more-than.js b/src/operations/more-than.js index 911134f..95b0eff 100644 --- a/src/operations/more-than.js +++ b/src/operations/more-than.js @@ -2,17 +2,15 @@ const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); -const either = require("@validatem/either"); const node = require("../ast-node"); module.exports = function (operations) { const isValueExpression = require("../validators/operations/is-value-expression")(operations); - const isInternalArrayType = require("../validators/operations/is-internal-array-type")(operations); return function moreThan(_name, _expression) { let [ expression ] = validateArguments(arguments, { - expression: [ required, either([ isValueExpression, isInternalArrayType ]) ] + expression: [ required, isValueExpression ] }); return node({ type: "condition", conditionType: "moreThan", expression: expression }); diff --git a/src/operations/not.js b/src/operations/not.js index 7eb6adb..c23e9e0 100644 --- a/src/operations/not.js +++ b/src/operations/not.js @@ -11,12 +11,15 @@ const tagAsType = require("../validators/tag-as-type"); module.exports = function (operations) { const isExpression = require("../validators/operations/is-expression")(operations); const isCondition = require("../validators/operations/is-condition")(operations); + const isWhereObject = require("../validators/operations/is-where-object")(operations); return function not(_expression) { let [ expression ] = validateArguments(arguments, { expression: [ required, either([ [ isExpression, tagAsType("expression") ], [ isCondition, tagAsType("condition") ], + // FIXME: Clearly document that the below means not(allOf(...)), rather than not(anyOf(...))! This makes sense both logically and pragmatically (eg. it is analogous to "does not match composite key", and the behaviour is consistent with `where({ ... })`), but it may mismatch initial expectations. For not(anyOf(...)), suggest explicit use of `anyOf` and multiple objects. + [ isWhereObject, tagAsType("expression") ], ])] }); diff --git a/src/optimizers/arrayify-predicate-lists.js b/src/optimizers/arrayify-predicate-lists.js new file mode 100644 index 0000000..1c8c7a3 --- /dev/null +++ b/src/optimizers/arrayify-predicate-lists.js @@ -0,0 +1,133 @@ +"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"), + } +}; diff --git a/src/optimizers/collapse-where.js b/src/optimizers/collapse-where.js new file mode 100644 index 0000000..af39042 --- /dev/null +++ b/src/optimizers/collapse-where.js @@ -0,0 +1,27 @@ +"use strict"; + +const splitFilter = require("split-filter"); + +const operations = require("../operations"); +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... + +module.exports = { + name: "collapse-where", + category: [ "normalization" ], + visitors: { + select: ({ table, clauses }) => { + let [ whereClauses, otherClauses ] = splitFilter(clauses, (clause) => clause.type === "where"); + + if (whereClauses.length > 1) { + let whereExpressions = whereClauses.map((clause) => clause.expression); + let newWhere = operations.where(operations.allOf(whereExpressions)); + + return operations.select(table, [ newWhere ].concat(otherClauses)); + } else { + return NoChange; + } + } + } +}; diff --git a/src/optimizers/conditions-to-expressions.js b/src/optimizers/conditions-to-expressions.js new file mode 100644 index 0000000..8409300 --- /dev/null +++ b/src/optimizers/conditions-to-expressions.js @@ -0,0 +1,46 @@ +"use strict"; + +const matchValue = require("match-value"); + +const operations = require("../operations"); +const typeOf = require("../type-of"); +const unreachable = require("../unreachable"); +const NoChange = require("./util/no-change"); + +module.exports = { + name: "conditions-to-expressions", + category: [ "normalization" ], + visitors: { + expression: (rootNode) => { + if (rootNode.condition.type === "condition") { + return NoChange; + } else { + // anyOfConditions, allOfConditions, notCondition + + function convertNode(node) { + let listOperation = matchValue.literal(typeOf(node), { + anyOfConditions: operations.anyOf, + allOfConditions: operations.allOf, + notCondition: null, + condition: null + }); + + if (listOperation != null) { + return listOperation(node.items.map((item) => convertNode(item))); + } else if (typeOf(node) === "notCondition") { + return operations.notExpression(convertNode(node.condition)); + } else if (typeOf(node) === "condition") { + return operations.expression({ + left: rootNode.left, + condition: node + }); + } else { + unreachable(`Encountered node type '${node.type}' within condition modifier`); + } + } + + return convertNode(rootNode.condition); + } + } + } +}; diff --git a/src/optimizers/flatten-not-predicates.js b/src/optimizers/flatten-not-predicates.js new file mode 100644 index 0000000..69640c7 --- /dev/null +++ b/src/optimizers/flatten-not-predicates.js @@ -0,0 +1,52 @@ +"use strict"; + +const matchValue = require("match-value"); + +const operations = require("../operations"); +const typeOf = require("../type-of"); +const NoChange = require("./util/no-change"); + +// FIXME: Generalize to all predicate lists? + +function createHandler(type) { + let subItemProperty = matchValue(type, { + notCondition: "condition", + notExpression: "expression" + }); + + return function flattenNotPredicates(expression) { + // Flattens multiple levels of like-typed not(...) wrappers, ending up with a logically equivalent subtree + // `notCondition(condition)` -> `notCondition(condition)` + // `notCondition(notCondition(condition))` -> `condition` + // `notCondition(notCondition(notCondition(condition)))` -> `notCondition(condition)` + // etc. + let notLevels = 0; + let currentItem = expression; + + while(typeOf(currentItem) === type) { + notLevels += 1; + currentItem = currentItem[subItemProperty]; + } + + if (notLevels === 1) { + return NoChange; + } else { + let hasNot = (notLevels % 2) === 1; + + if (hasNot) { + return operations.not(currentItem); + } else { + return currentItem; + } + } + }; +} + +module.exports = { + name: "flatten-not-predicates", + category: [ "readability" ], + visitors: { + notExpression: createHandler("notExpression"), + notCondition: createHandler("notCondition") + } +}; diff --git a/src/optimizers/flatten-predicate-lists.js b/src/optimizers/flatten-predicate-lists.js new file mode 100644 index 0000000..068da0e --- /dev/null +++ b/src/optimizers/flatten-predicate-lists.js @@ -0,0 +1,66 @@ +"use strict"; + +const matchValue = require("match-value"); + +const operations = require("../operations"); +const typeOf = require("../type-of"); +const NoChange = require("./util/no-change"); +const RemoveNode = require("./util/remove-node"); + +// FIXME: Generalize to all predicate lists? + +function createHandler(type) { + let listOperation = matchValue.literal(type, { + anyOfExpressions: operations.anyOf, + allOfExpressions: operations.allOf, + anyOfConditions: operations.anyOf, + allOfConditions: operations.allOf, + }); + + return function flattenPredicateList(list) { + let hasNestedPredicates = list.items.some((item) => typeOf(item) === type); + let hasSingleItem = (list.items.length === 1); // For unnecessary anyOf/allOf wrapping, which is also handled by this optimizer + let mustFlatten = hasNestedPredicates || hasSingleItem; + + if (mustFlatten) { + let actualItems = []; + + function collectItemsRecursively(node) { + for (let subItem of node.items) { + if (typeOf(subItem) === type) { + collectItemsRecursively(subItem); + } else { + actualItems.push(subItem); + } + } + } + + collectItemsRecursively(list); + + if (actualItems.length === 0) { + // FIXME: Do we want to log this as a warning? It *could* occur when items get eliminated by another optimizer, but it could also be the result of a bug... + console.warn("Encountered 0 actual items in predicate list"); + + return RemoveNode; + } else if (actualItems.length === 1) { + // Wrapping is pointless here. + return actualItems[0]; + } else { + return listOperation(actualItems); + } + } else { + return NoChange; + } + }; +} + +module.exports = { + name: "flatten-predicate-lists", + category: [ "readability" ], + visitors: { + allOfExpressions: createHandler("allOfExpressions"), + anyOfExpressions: createHandler("anyOfExpressions"), + allOfConditions: createHandler("allOfConditions"), + anyOfConditions: createHandler("anyOfConditions"), + } +}; diff --git a/src/optimizers/util/no-change.js b/src/optimizers/util/no-change.js new file mode 100644 index 0000000..8091f1a --- /dev/null +++ b/src/optimizers/util/no-change.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = Symbol("NoChange"); diff --git a/src/optimizers/util/remove-node.js b/src/optimizers/util/remove-node.js new file mode 100644 index 0000000..17907f4 --- /dev/null +++ b/src/optimizers/util/remove-node.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = Symbol("RemoveNode"); diff --git a/src/unreachable.js b/src/unreachable.js index c1c0b5d..82f6f31 100644 --- a/src/unreachable.js +++ b/src/unreachable.js @@ -1,5 +1,5 @@ "use strict"; module.exports = function unreachable(reason) { - throw new Error(`This code should never be run: ${reason}; this is a bug in raqb, please report it!`); + throw new Error(`${reason}; this is a bug in raqb, please report it!`); }; diff --git a/src/validators/operations/is-internal-array-type.js b/src/validators/operations/is-internal-array-type.js index 38c4741..7916298 100644 --- a/src/validators/operations/is-internal-array-type.js +++ b/src/validators/operations/is-internal-array-type.js @@ -1,12 +1,7 @@ "use strict"; -const either = require("@validatem/either"); - module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); - return either([ - [ isObjectType("_internalAnyOfArray") ], - [ isObjectType("_internalAllOfArray") ], - ]); + return [ isObjectType("_arrayOf") ]; }; diff --git a/yarn.lock b/yarn.lock index 8465819..2591b1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,9 +87,9 @@ integrity sha512-vE8t1tNXknmN62FlN6LxQmA2c6TwVKZ+fl/Wit3H2unFdOhu7SZj2kRPGjAXdK/ARh/3svYfUBeD75pea0j1Sw== "@validatem/core@^0.3.3": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@validatem/core/-/core-0.3.9.tgz#229c8327b1cb64bf26e946ae22390e3219d01182" - integrity sha512-sFUhoirMLcY/5oeY3yqB+O0T488MysKyxq7UtnS7vqWatjAKifRtzOdV9GJygSQiYOXhdFJVm59W7olmd9vu9w== + version "0.3.10" + resolved "https://registry.yarnpkg.com/@validatem/core/-/core-0.3.10.tgz#fa568565376c14196d54be15720c53715956e394" + integrity sha512-PPMd2M27WKN2MPSdL6iRIYHKBQ74JsYmwruIWUIwzVOVVE5g9LzpVO43057np+IyI3AyPZOD27m2VhmnKaQgsQ== dependencies: "@validatem/annotate-errors" "^0.1.2" "@validatem/any-property" "^0.1.0" @@ -121,21 +121,20 @@ is-callable "^1.1.5" "@validatem/either@^0.1.3": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@validatem/either/-/either-0.1.8.tgz#e10ecb79c257a8db4589e695db7032c10871015e" - integrity sha512-qEitMr5nAvAI3EKSoCYcDPABWPk2mNEc8ByNxCuOTSyzvHJf+3fNGtWuhNMn4PZ+fGoHYFVhWGXVdT8MNPGzTA== + version "0.1.9" + resolved "https://registry.yarnpkg.com/@validatem/either/-/either-0.1.9.tgz#0d753ef8fe04486d2b7122de3dd3ac51b3acaacf" + integrity sha512-cUqlRjy02qDcZ166/D6duk8lrtqrHynHuSakU0TvMGMBiLzjWpMJ+3beAWHe+kILB5/dlXVyc68ZIjSNhBi8Kw== dependencies: "@validatem/combinator" "^0.1.1" "@validatem/error" "^1.0.0" + "@validatem/match-validation-error" "^0.1.0" "@validatem/validation-result" "^0.1.2" flatten "^1.0.3" "@validatem/error@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@validatem/error/-/error-1.0.0.tgz#a975904aa4c3e7618d89088a393567a5e1778340" - integrity sha512-7M3tV4DhCuimuCRdC2L/topBByDjhzspzeQGNU0S4/mdn2aDNtESYE43K/2Kh/utCAhqXh2gyw89WYxy//t3fQ== - dependencies: - create-error "^0.3.1" + version "1.1.0" + resolved "https://registry.yarnpkg.com/@validatem/error/-/error-1.1.0.tgz#bef46e7066c39761b494ebe3eec2ecdc7348f4ed" + integrity sha512-gZJEoZq1COi/8/5v0fVKQ9uX54x5lb5HbV7mzIOhY6dqjmLNfxdQmpECZPQrCAOpcRkRMJ7zaFhq4UTslpY9yA== "@validatem/forbidden@^0.1.0": version "0.1.0" @@ -274,6 +273,13 @@ flatten "^1.0.3" is-plain-obj "^2.1.0" +"@validatem/one-of@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@validatem/one-of/-/one-of-0.1.1.tgz#df40f6d2780021b8557b640b99c7b217bda10b95" + integrity sha512-lIgxnkNRouPx5Ydddi8OaAxmzp1ox44OJnrJPRrJkU4ccz9Yb7GSJ+wQJNVkAZCar+DGTDMoXoy51NwDnsf4sw== + dependencies: + "@validatem/error" "^1.0.0" + "@validatem/required@^0.1.0", "@validatem/required@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@validatem/required/-/required-0.1.1.tgz#64f4a87333fc5955511634036b7f8948ed269170" @@ -299,9 +305,9 @@ "@validatem/combinator" "^0.1.1" "@validatem/wrap-error@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@validatem/wrap-error/-/wrap-error-0.1.2.tgz#146394ef8c9f78df0bb249a03e14b760f6c749fc" - integrity sha512-UgXDcBUEyo0cHRbUWi8Wl0Bf88L/QLNVKIlxGhiyQwsatk9aDVsNC3H7NgLev1mjjST0idxic8+zGAP4UcYRuA== + version "0.1.3" + resolved "https://registry.yarnpkg.com/@validatem/wrap-error/-/wrap-error-0.1.3.tgz#2470d24c17325ad97d852a21be6c0227da908d3c" + integrity sha512-86ANJACPGbH8jD/C/tUTZNgQh9xCePUKq4wf5ZRcwOvtIDaZO98FI9cdoT2/zS1CzQCp3VWlwz16YT6FNjJJJA== dependencies: "@validatem/combinator" "^0.1.1" "@validatem/error" "^1.0.0" @@ -353,10 +359,10 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@^3.2.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-regex@^4.1.0: version "4.1.0" @@ -443,6 +449,14 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +benchmark@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + binary-extensions@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" @@ -517,7 +531,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -651,7 +665,7 @@ debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.0.1: +debug@^4.0.1, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -731,11 +745,11 @@ end-of-stream@^1.1.0: once "^1.4.0" enquirer@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" - integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== dependencies: - ansi-colors "^3.2.1" + ansi-colors "^4.1.1" es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: version "1.17.6" @@ -1382,7 +1396,7 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lodash@^4.17.14: +lodash@^4.17.14, lodash@^4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -1572,6 +1586,11 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +platform@^1.3.3: + version "1.3.5" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" + integrity sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"