From 05bd98f64068df097fa2137aaf510fb69f0b3c22 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Mon, 7 Sep 2020 19:32:21 +0200 Subject: [PATCH] WIP --- bin/zap | 32 +++ experiments/db-test.js | 43 +++ experiments/forum-queries.js | 173 ++++++++++++ experiments/raqb-concepts.js | 79 +++++- experiments/todo.js | 67 ++++- notes.txt | 59 +++- package.json | 13 +- src/ast-to-query.js | 62 +++-- src/client/index.js | 171 ++++++++++++ src/internal-operations/array-of.js | 12 +- src/internal-operations/condition.js | 4 +- src/operations/add-columns.js | 19 -- src/operations/add-fields.js | 19 ++ src/operations/alias.js | 13 +- src/operations/all-of.js | 7 +- src/operations/any-of.js | 7 +- src/operations/collapse-by.js | 16 +- src/operations/compute.js | 3 +- src/operations/count.js | 19 +- src/operations/equals.js | 4 +- src/operations/expression.js | 6 +- src/operations/{column.js => field.js} | 8 +- src/operations/foreign-column.js | 30 --- src/operations/functions/now.js | 14 + src/operations/hierarchical.js | 10 +- src/operations/index.js | 200 +++----------- src/operations/indexes/index-where.js | 21 ++ src/operations/indexes/index.js | 11 + src/operations/indexes/primary-key.js | 11 + src/operations/indexes/unique-where.js | 21 ++ src/operations/indexes/unique.js | 11 + src/operations/less-than.js | 4 +- src/operations/more-than.js | 4 +- src/operations/only-columns.js | 19 -- src/operations/only-fields.js | 19 ++ src/operations/parameter.js | 3 +- src/operations/relations/belongs-to.js | 28 ++ src/operations/relations/has.js | 28 ++ src/operations/relations/through.js | 31 +++ src/operations/remote-field.js | 31 +++ src/operations/result-exists.js | 24 ++ src/operations/schema/change-collection.js | 36 +++ src/operations/schema/create-collection.js | 36 +++ src/operations/schema/default-to.js | 21 ++ src/operations/schema/delete-collection.js | 21 ++ src/operations/schema/fields.js | 21 ++ src/operations/schema/indexes.js | 22 ++ src/operations/select.js | 12 +- src/operations/sum.js | 10 +- src/operations/table.js | 6 +- src/operations/types/auto-id.js | 12 + src/operations/types/boolean.js | 11 + src/operations/types/string.js | 12 + src/operations/types/timestamp.js | 12 + src/operations/types/uuid.js | 11 + src/optimizers/arrayify-predicate-lists.js | 55 +++- src/optimizers/collapse-where.js | 4 +- src/optimizers/conditions-to-expressions.js | 32 ++- src/optimizers/flatten-predicate-lists.js | 53 ++-- src/optimizers/index.js | 3 +- src/optimizers/set-collapse-by-columns.js | 157 ----------- src/optimizers/set-collapse-by-fields.js | 185 +++++++++++++ src/optimizers/test-context.js | 50 ++-- src/optimizers/values-to-conditions.js | 47 ++++ ...is-table-name.js => is-collection-name.js} | 0 src/validators/is-foreign-column-string.js | 16 -- src/validators/is-literal-value.js | 5 +- ...olumn-string.js => is-local-field-name.js} | 3 +- src/validators/is-remote-field-name.js | 23 ++ ...olumn-object.js => is-any-field-object.js} | 4 +- ...ible-column.js => is-collapsible-field.js} | 5 +- .../{is-table.js => is-collection.js} | 6 +- src/validators/operations/is-computable.js | 4 +- .../operations/is-condition-value.js | 20 ++ src/validators/operations/is-condition.js | 4 +- src/validators/operations/is-field.js | 15 ++ .../operations/is-local-value-expression.js | 19 ++ .../operations/is-possibly-foreign-column.js | 15 -- .../operations/is-possibly-remote-field.js | 15 ++ .../operations/is-predicate-list.js | 16 +- .../operations/is-relation-clause.js | 8 + src/validators/operations/is-remote-field.js | 15 ++ src/validators/operations/is-select-clause.js | 4 +- ...ction-column.js => is-selectable-field.js} | 4 +- .../operations/is-value-expression.js | 6 +- src/validators/operations/is-where-object.js | 6 +- .../schema/is-composite-index-type.js | 17 ++ .../operations/schema/is-field-type.js | 16 ++ .../operations/schema/is-fields-object.js | 27 ++ .../operations/schema/is-index-type.js | 15 ++ .../wrap-possibly-foreign-column-name.js | 11 - .../wrap-possibly-remote-field-name.js | 11 + .../operations/wrap-with-operation.js | 2 +- yarn.lock | 255 +++++++++++++++++- 94 files changed, 2110 insertions(+), 642 deletions(-) create mode 100755 bin/zap create mode 100644 experiments/db-test.js create mode 100644 experiments/forum-queries.js create mode 100644 src/client/index.js delete mode 100644 src/operations/add-columns.js create mode 100644 src/operations/add-fields.js rename src/operations/{column.js => field.js} (54%) delete mode 100644 src/operations/foreign-column.js create mode 100644 src/operations/functions/now.js create mode 100644 src/operations/indexes/index-where.js create mode 100644 src/operations/indexes/index.js create mode 100644 src/operations/indexes/primary-key.js create mode 100644 src/operations/indexes/unique-where.js create mode 100644 src/operations/indexes/unique.js delete mode 100644 src/operations/only-columns.js create mode 100644 src/operations/only-fields.js create mode 100644 src/operations/relations/belongs-to.js create mode 100644 src/operations/relations/has.js create mode 100644 src/operations/relations/through.js create mode 100644 src/operations/remote-field.js create mode 100644 src/operations/result-exists.js create mode 100644 src/operations/schema/change-collection.js create mode 100644 src/operations/schema/create-collection.js create mode 100644 src/operations/schema/default-to.js create mode 100644 src/operations/schema/delete-collection.js create mode 100644 src/operations/schema/fields.js create mode 100644 src/operations/schema/indexes.js create mode 100644 src/operations/types/auto-id.js create mode 100644 src/operations/types/boolean.js create mode 100644 src/operations/types/string.js create mode 100644 src/operations/types/timestamp.js create mode 100644 src/operations/types/uuid.js delete mode 100644 src/optimizers/set-collapse-by-columns.js create mode 100644 src/optimizers/set-collapse-by-fields.js create mode 100644 src/optimizers/values-to-conditions.js rename src/validators/{is-table-name.js => is-collection-name.js} (100%) delete mode 100644 src/validators/is-foreign-column-string.js rename src/validators/{is-local-column-string.js => is-local-field-name.js} (53%) create mode 100644 src/validators/is-remote-field-name.js rename src/validators/operations/{is-column-object.js => is-any-field-object.js} (81%) rename src/validators/operations/{is-collapsible-column.js => is-collapsible-field.js} (54%) rename src/validators/operations/{is-table.js => is-collection.js} (70%) create mode 100644 src/validators/operations/is-condition-value.js create mode 100644 src/validators/operations/is-field.js create mode 100644 src/validators/operations/is-local-value-expression.js delete mode 100644 src/validators/operations/is-possibly-foreign-column.js create mode 100644 src/validators/operations/is-possibly-remote-field.js create mode 100644 src/validators/operations/is-relation-clause.js create mode 100644 src/validators/operations/is-remote-field.js rename src/validators/operations/{is-selection-column.js => is-selectable-field.js} (50%) create mode 100644 src/validators/operations/schema/is-composite-index-type.js create mode 100644 src/validators/operations/schema/is-field-type.js create mode 100644 src/validators/operations/schema/is-fields-object.js create mode 100644 src/validators/operations/schema/is-index-type.js delete mode 100644 src/validators/operations/wrap-possibly-foreign-column-name.js create mode 100644 src/validators/operations/wrap-possibly-remote-field-name.js diff --git a/bin/zap b/bin/zap new file mode 100755 index 0000000..c21d369 --- /dev/null +++ b/bin/zap @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +"use strict"; + +const yargs = require("yargs"); +const matchValue = require("match-value"); + +let argv = yargs + .command("change", "Create a new schema revision") + .command("upgrade ", "Upgrade the database to a new schema revision", { + revision: { describe: "The number of the revision to upgrade to (or 'latest')" } + }) + .command("undo", "Undo the most recent schema revision upgrade") + .command("show ", "Show the full database schema at a given revision") + .argv; + +console.log(argv); + +matchValue(argv._[0], { + change: () => { + console.log("change"); + }, + upgrade: () => { + console.log("upgrade"); + }, + undo: () => { + console.log("undo"); + }, + show: () => { + console.log("show"); + } +}); diff --git a/experiments/db-test.js b/experiments/db-test.js new file mode 100644 index 0000000..dc847f4 --- /dev/null +++ b/experiments/db-test.js @@ -0,0 +1,43 @@ +"use strict"; + +const Promise = require("bluebird"); +const asTable = require("as-table"); + +const createClient = require("../src/client"); +const { select, collapseBy, hierarchical, compute, count, sum, where, anyOf, allOf, parameter, unsafeSQL, moreThan } = require("../src/operations"); + +// let query = select("sales", [ +// // where({ size: moreThan(anyOf(parameter("sizes"))) }), // should work +// // where({ size: moreThan(anyOf([1, 2, 3])) }), // should work +// where({ size: moreThan(anyOf([1, allOf([2, 3]) ])) }), // should work +// // where({ size: anyOf(unsafeSQL("")) }), +// collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [ +// compute({ +// total_sold: count(), +// total_revenue: sum("price") +// }) +// ]) +// ]); + +let query = select("sales", [ + where({ size: anyOf(parameter("sizes")) }), + collapseBy(["size"], [ + compute({ total_revenue: sum("price") }) + ]) +]); + +let client = createClient({ + socket: "/var/run/postgresql", + database: "movietest" +}); + +return Promise.try(() => { + let sizes = [ "S", "M", "L", "XL" ]; + + return client.query(query, { sizes }); +}).then((results) => { + console.log(asTable(results)); + + return client.destroy(); +}); + diff --git a/experiments/forum-queries.js b/experiments/forum-queries.js new file mode 100644 index 0000000..8a62299 --- /dev/null +++ b/experiments/forum-queries.js @@ -0,0 +1,173 @@ +/* eslint-disable no-undef */ +"use strict"; + +// FIXME: Think about whether to do type(clauses) or [ type(), ... clauses ] +// FIXME: Defaults via defaultTo! And make sure to document that these can *only* consist of query objects (literals or expressions), not arbitrary JS +// FIXME: Think about field validation API; both schema-time CHECK constraints and runtime validation +// FIXME: Think about some way to integrate partial cacheing of queries (eg. for determining permissions for roles) +// FIXME: Document that due to how schema creation works, only belongsTo(...) is possible and has(...) isn't +// FIXME: last() in addition to first()? Would need to find a way to combine that with sorting, and think about how to handle that when no sorting criterium is given. +// FIXME: duration() and time parsing +// FIXME: ensure that relations work on collapsed queries, but *only* if the FK column appears in the permitted output columns +// FIXME: Separate batch-insert API so that data(...) calls are composable without implying multiple items +// FIXME: Have a short-hand (non-composable) API for create("foo", {obj}) and select("foo", {whereObj}) type usage? Especially for eg. `select("posts", { id: postID })` +// FIXME: Also for createTable? So that you can do eg. createTable("posts", { title: string() }) instead of the `columns` indirection, when you don't have stuff like composite indexes +// FIXME: Allow passing a custom type to autoID? + +let timestamps = { + created_at: [ timestamp(), defaultTo(now()) ], + updated_at: [ timestamp(), optional() ] +}; + +createTable("permissions", { + role: belongsTo("roles.id"), + permission: string() // defined in the application code +}); + +createTable("roles", { + name: string(), + color: string() +}); + +createTable("users", { + username: string(), + password_hash: string(), + email_address: string(), + is_banned: [ boolean(), defaultTo(false) ], + ban_reason: [ string(), optional() ], + last_activity: [ timestamp(), defaultTo(now()), index() ], + activation_key: [ uuid(), optional(), index() ], + activation_expiry: [ timestamp(), optional() ], + password_reset_key: [ uuid(), optional(), index() ], + password_reset_expiry: [ timestamp(), optional() ] +}); + +createTable("categories", { + name: string(), + created_by: belongsTo("users.id"), + visible_on_frontpage: [ boolean(), defaultTo(true) ] +}); + +createTable("threads", { + title: string(), + category: belongsTo("categories.id"), + user: belongsTo("users.id"), // FIXME: Figure out how to auto-detect the column type for relations + visible: [ boolean(), defaultTo(true) ], + ... timestamps +}); + +createTable("posts", { + thread: belongsTo("threads.id"), + user: belongsTo("users.id"), + body: string(), + visible: [ boolean(), defaultTo(true) ], + ... timestamps +}); + +//////////////////////////////////////////////////////////// + +// List active users and include their role information for highlighting moderators etc. + +function timeAgo(time) { + return subtract(now(), duration(time)); +} + +select("users", [ + where({ last_activity: lessThan(timeAgo("1h")) }), + withRelations({ role: belongsTo("role") }) +]); + +// Count the active users by role +// NOTE: This returns an object { role, count } where `role` is the actual data from the `roles` table + +select("users", [ + where({ last_activity: moreThan(timeAgo("1h")) }), + withRelations({ role: belongsTo("role") }), + collapseBy("role", [ + compute({ count: count() }) + ]) +]); + +// Update a user's last activity + +update("users", [ + where({ id: userID }), + set({ last_activity: now() }) +]); + +// Show latest threads in all categories except hidden threads and frontpage-hidden categories + +function mostRecent(field) { + return [ + first(), + sortedBy(descending(field)) + ]; +} + +select("threads", [ + define("category", belongsTo("category")), + define("last_post", has("posts.thread", [ + mostRecent("created_at") + ])), + where({ + visible: true, + category: { visible_on_frontpage: true } + }), + sortedBy(descending("last_post.created_at")) +]); + +// Get a thread with all its posts + +select("threads", [ + first(), + where({ id: threadID }), + withRelations({ + posts: has("posts.thread", [ + where({ visible: true }), + startAt(offset), + first(10) + ]) + }) +]); + +// Create a new thread + +create("threads", [ + withRelations({ posts: has("posts.thread") }), + set({ + title: title, + user: userID, + posts: [{ + user: userID, + body: body + }] + }) +]); + +// Update the thread title + +update("threads", [ + where({ id: threadID }), + set({ + title: newTitle, + updated_at: now() + }) +]); + +// Create a new post + +create("posts", { + thread: threadID, + user: userID, + body: body +}); + +// Edit a post body + +update("posts", [ + where({ id: postID }), + set({ + body: newBody, + updated_at: now() + }) +]); diff --git a/experiments/raqb-concepts.js b/experiments/raqb-concepts.js index a6b4483..d25481d 100644 --- a/experiments/raqb-concepts.js +++ b/experiments/raqb-concepts.js @@ -5,7 +5,7 @@ Error.stackTraceLimit = 100; const util = require("util"); 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, collapseBy, compute, hierarchical, sum, count, addColumns } = require("../src/operations"); +let { select, onlyFields, where, withRelations, withDerived, field, through, inValues, unsafeSQL, postProcess, belongsTo, has, value, parameter, not, anyOf, allOf, lessThan, moreThan, alias, foreignColumn, collection, expression, equals, collapseBy, compute, hierarchical, sum, count, addFields } = require("../src/operations"); const astToQuery = require("../src/ast-to-query"); const optimizeAST = require("../src/ast/optimize"); const measureTime = require("../src/measure-time"); @@ -34,8 +34,8 @@ try { // Edit me! /* Available functions: - select, onlyColumns, addColumns, alias, - where, column, foreignColumn, table, + select, onlyFields, addFields, alias, + where, field, foreignColumn, collection, value, parameter, not, anyOf, allOf, lessThan, moreThan, equals, @@ -46,13 +46,13 @@ try { // let niceNumbers = anyOf([ 1, 2, 3 ]); // query = select("projects", [ - // onlyColumns([ "foo" ]), + // onlyFields([ "foo" ]), // where({ // number_one: niceNumbers, // number_two: niceNumbers // }), // where({ - // number_three: anyOf([ 42, column("number_one") ]), + // number_three: anyOf([ 42, field("number_one") ]), // number_four: moreThan(1337) // }) // ]); @@ -78,14 +78,65 @@ try { */ // SELECT country_id, store_id, color, size, COUNT(*) AS total_sold, SUM(price) AS total_revenue FROM sales GROUP BY color, size, ROLLUP (country_id, store_id); - query = select("sales", [ - collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [ - compute({ - total_sold: count(), - total_revenue: sum("price") - }) - ]) - ]); + // query = select("sales", [ + // collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [ + // compute({ + // total_sold: count(), + // total_revenue: sum("price") + // }) + // ]) + // ]); + + // FIXME: Test with nested any/all + + // query = select("sales", [ + // where({ size: moreThan(anyOf(parameter("sizes"))) }), // should work + // // where({ size: moreThan(anyOf([1, unsafeSQL("foo"), allOf([2, 3]) ])) }), // should work + // // where({ size: anyOf(unsafeSQL("")) }), + // collapseBy([ "color", "size", hierarchical([ "country_id", "store_id" ]) ], [ + // compute({ + // total_sold: count(), + // total_revenue: sum("price") + // }) + // ]) + // ]); + + query = createTable("actors", { + fields: { + imdb_id: string(), + name: [ required(), string() ], + date_of_birth: date({ withTimezone: true }), + place_of_birth: string(), + imdb_metadata_scraped: json() + } + }); + + query = createTable("movies", { + fields: { + imdb_id: string(), + name: string(), + imdb_metadata_scraped: json() + } + }); + + query = createTable("appearances", { + fields: { + actor: belongsTo("actors"), + movie: belongsTo("movie") + } + }); + + updateTable("appearances", { + fields: { + + } + }) + + // FIXME: For query generation, ensure we are generating correct queries for TRUE/FALSE/NULL/NOTNULL (in particular, moreThan/lessThan should not allow these!) + // FIXME: Partial indexes + // FIXME: not(anyOf(...)) + // FIXME: anyOfValues rendering without arrayify optimizer + // FIXME: index, indexWhere(...), unique }); console.log(util.inspect(query, { depth: null, colors: true })); @@ -164,7 +215,7 @@ try { // FIXME: Pre-processing (eg. inverse case-mapping for inserted objects or WHERE clauses) - maybe pre-processing that subscribes to particular operations? Something along the lines of axios interceptors perhaps // FIXME: `either`/`all` for OR/AND representation respectively? -// FIXME: I guess `withDerived` could be implemented externally, as something that (depending on value type) either a) adds a column selector or b) adds a post-processing hook +// FIXME: I guess `withDerived` could be implemented externally, as something that (depending on value type) either a) adds a field selector or b) adds a post-processing hook // FIXME: Aggregrates (GROUP BY) // return db.execute(query); diff --git a/experiments/todo.js b/experiments/todo.js index 64a7c72..a131de1 100644 --- a/experiments/todo.js +++ b/experiments/todo.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ "use strict"; // JOIN @@ -64,8 +65,10 @@ query = select("reviews", [ movie: { title: includes("Movie") } }), collapseBy("movie_id", [ - compute({ positive_review_count: count() }), - renameColumn("movie.title", "title") + compute({ + positive_review_count: count(), + title: "movie.title" + }), ]) ]); @@ -432,3 +435,63 @@ combine(/* [ clause ], allOf([ clause ]), anyOf([ clause]) */); // }), // mapCase({ from: "snake", to: "camel" }) // ]); + +query = select("bandwidth_measurements", [ + collapseBy("customer_id", [ + compute({ usage: nthPercentile(95, "usage") }) + ]) +]); + + +SELECT expirations.ban_id, max(expire_id) FROM expirations +WHERE expire < ? AND removed_at IS NULL +GROUP BY expirations.ban_id + +query = select("expirations", [ + where({ + expire: lessThan(currentTime()), + removedAt: isNull() + }), + collapseBy("ban_id", [ + compute({ latest_expiry_id: max("expire_id") }) + ]) +]); + +let waypointsForTrip = select("waypoints", [ + onlyColumns([ "position" ]), + compute({ + distance: postgis.distanceBetween( + column("position"), + fromPreviousRow("position", sortedBy("generated_at")) + ) + }), + where({ + user_id: foreignColumn("trips.user_id"), + generated_at: between(column("trips.started_at"), column("trips.ended_at")) + }), + sortedBy("generated_at") +]); + +select("trips", [ + define({ + user: belongsTo("user_id"), + waypoints: waypointsForTrip, + computed_trip: select("waypoints", [ + compute({ + distance: sum("distance"), + route: postgis.makeLine("waypoints") + }) + ]), + }), + where({ user: { team_id: 42 } }), + addColumns([ "user.*" ]), + compute({ + average_distance_per_ms: divide("computed_trip.distance", subtract("ended_at", "started_at")), + route_as_geojson: postgis.asGeoJSON("computed_trip.route"), + route_as_polyline: postgis.asEncodedPolyline("computed_trip.route"), + distance: "computed_trip.distance", + route: "computed_trip.route" + }), + sortedBy(descending("ended_at")) +]) + diff --git a/notes.txt b/notes.txt index 9129b14..9bbe939 100644 --- a/notes.txt +++ b/notes.txt @@ -11,12 +11,37 @@ Todo: - Boolean logic x anyOf(...) x allOf(...) +- Annotate placeholder/parameter nodes with the expected parameter type in their position, for validation of input parameters at query time +- Rename table -> collection everywhere, also column -> field? +- Update wrapError API Docs: +- Emphasize that users should feel free to experiment; the library will tell them (safely) if they are trying to do something invalid - need to help the user overcome the "but what if I get it wrong" fear (also emphasize the flexibility of schema updates to help with this, "it can always be fixed later" or so) - Instruct users to report it if raqb generates invalid SQL, as this is considered a bug in raqb, regardless of the input (except when it's caused by `unsafeSQL`) +- Explain the base mental model for relations; the relationship between things is always that one Thing 'owns' many other things. Therefore, has(...) means all the items in a collection that reference the current item in their (remote) column, whereas belongsTo(...) means an item in another collection that's referenced in one of the current item's local columns +- Explain that aggregrates (collapseBy) are always applied in reference to the being-queried table; eg. if you want to collapse user counts by their role_id, you should base-query the users table, not the roles table +- Demonstrate the usefulness of normalization through examples: + - Deduplication of data (eg. user.friends containing duplicate user records) + - Recursion is impossible (eg. user -> posts -> replyUsers or something) Considerations: - Disallow non-lowercase column/table names entirely due to PostgreSQL weirdness? Or just cast them to lowercase? +- For rollback of NOT NULL column deletions in migrations: require each column deletion change to include an initializer function for recreating it afterwards, otherwise it cannot be rolled back +- Before applying or rolling back a migration, all the migrations should be evaluated in-memory, so that the state at any given migration can be determined + - Instead of applying the delta in the migration directly, always generate the "actual delta" from the evaluated state at the previous point vs. that at the new point? This can skip intermediate steps when eg. one migration adds a column that the next migration removes, and should generally result in a much more consistent outcome (similar to deterministic package management) + +FAQ: +- Why have you changed all the names of things, like table and column and GROUP BY? + - One of the primary goals of zapdb is to get more people using relational databases who were previously intimidated by them. Unfortunately, typical RDMBSes are full of jargon that isn't intuitively understandable to many new users. Because of this, zapdb uses more intuitive terms that users can understand without having to memorize them - it's much easier for experienced RDBMS users to adapt to intuitive terms, than it is for inexpected users to learn the jargon. + +Terminology: +- Collection: table +- Field: column + - Local + - Remote: foreign column +- Operation: any kind of item in a query; eg. select(...), but also where(...) and collapseBy(...) -- need to emphasize that this does *not* imply immediate execution +- Clause: any kind of operation that modifies the behaviour of its parent operation in some way; eg. a where(...) inside of a select(...) or inside of a `has(...)` +- Predicate: condition, like moreThan(3) ---- @@ -37,6 +62,7 @@ Recommendations to make: Ideas: - Make placeholders typed so that we can warn the user when they try to pass in invalid things? +- Safe client API that returns a disposer instead of a client upon client creation? ---- @@ -174,7 +200,7 @@ MARKER: 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) +FIXME: Document that `DEBUG=raqb:ast:optimize:*` can be used for tracking down unstable optimizers (symptom = error telling you so, should not be a stack overflow anymore) Pitch: "Easier than MongoDB", no more if(foo.bar) checks, no more data format mismatches, the database ensures for you that all data looks like expected, and it's easy to change the format if needed. @@ -320,3 +346,34 @@ for each country_id for (each store_id + null) for each color for each size + +NTILE(3) OVER ( ORDER BY amount ) + order by `amount`, then divide into 3 equally-sized buckets, and for each record return its bucket ID + +NTILE(3) OVER ( PARTITION BY YEAR ORDER BY amount ) + same as above, but produce a record for each distinct year (like a GROUP BY) + +SELECT foo, bar, OVER ( ) + +Windowing function + like GROUP BY, but the rows are not actually collapsed in the output result; they are just collapsed behind the scenes, so that aggregrate functions can be used over their virtual groups + + SELECT employee_id, avg(salary) OVER (PARTITION BY department_id) AS average_department_salary FROM salary; + SELECT department_id, avg(salary) FROM salary GROUP BY department_id; + + + + +--------- + +# Relation operations + +through([ item, item, ... ]) + item: columName [belongsTo] | remoteColumnName [has] | belongsTo | has + +has(remoteColumnName, [ operation, operation, ... ]) + operation: any operation that is valid in a SELECT? + +belongsTo(columnName) + +linkTo(remoteColumnName) diff --git a/package.json b/package.json index 490c9b1..26a7ab4 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "raqb", - "version": "1.0.0", + "name": "zapdb", + "version": "0.1.0", "main": "index.js", "repository": "git@git.cryto.net:joepie91/raqb.git", "author": "Sven Slootweg ", - "license": "MIT", + "license": "WTFPL OR CC0-1.0", "dependencies": { "@validatem/allow-extra-properties": "^0.1.0", "@validatem/any-property": "^0.1.3", @@ -18,6 +18,7 @@ "@validatem/is-boolean": "^0.1.1", "@validatem/is-date": "^0.1.0", "@validatem/is-function": "^0.1.0", + "@validatem/is-integer": "^0.1.0", "@validatem/is-number": "^0.1.2", "@validatem/is-plain-object": "^0.1.1", "@validatem/is-string": "^0.1.1", @@ -25,6 +26,7 @@ "@validatem/matches-format": "^0.1.0", "@validatem/nested-array-of": "^0.1.0", "@validatem/one-of": "^0.1.1", + "@validatem/require-either": "^0.1.0", "@validatem/required": "^0.1.1", "@validatem/wrap-error": "^0.1.2", "acorn": "^6.0.5", @@ -42,17 +44,20 @@ "flatten": "^1.0.3", "map-obj": "^4.1.0", "match-value": "^1.1.0", + "pg": "^8.3.3", "prismjs": "^1.20.0", "scope-analyzer": "^2.0.5", "split-filter": "^1.1.3", "split-filter-n": "^1.1.2", - "syncpipe": "^1.0.0" + "syncpipe": "^1.0.0", + "yargs": "^15.4.1" }, "devDependencies": { "@babel/core": "^7.10.4", "@babel/preset-env": "^7.10.4", "@babel/preset-react": "^7.10.4", "@joepie91/eslint-config": "^1.1.0", + "as-table": "^1.0.55", "babelify": "^10.0.0", "benchmark": "^2.1.4", "budo-express": "^1.0.2", diff --git a/src/ast-to-query.js b/src/ast-to-query.js index 0e42cce..e165787 100644 --- a/src/ast-to-query.js +++ b/src/ast-to-query.js @@ -30,13 +30,15 @@ const required = require("@validatem/required"); const either = require("@validatem/either"); const arrayOf = require("@validatem/array-of"); const isString = require("@validatem/is-string"); +const isBoolean = require("@validatem/is-boolean"); const isValue = require("@validatem/is-value"); const allowExtraProperties = require("@validatem/allow-extra-properties"); let isPlaceholder = { __raqbASTNode: isValue(true), type: isValue("placeholder"), - name: isString + name: isString, + parameterizable: isBoolean }; // FIXME: Do we want a nested array here? Probably not, since PostgreSQL might not support arbitrarily nested arrays @@ -164,10 +166,10 @@ function $combine(_strings, ... _nodes) { return $object({ query, params, placeholders }); } -function columnList({ onlyColumns, addColumns }) { +function $fieldList({ onlyColumns, addColumns }) { let primaryColumns = (onlyColumns.length > 0) ? onlyColumns - : [ { type: "allColumns" } ]; + : [ { type: "allFields" } ]; return $join(", ", $handleAll(concat([ primaryColumns, addColumns ]))); } @@ -181,7 +183,7 @@ let simpleColumnNameRegex = /^[a-z_]+(?:\.[a-z_]+)?$/; function $maybeParenthesize($query) { if ($query.query === "?" || simpleColumnNameRegex.test($query.query)) { - // We don't want to bloat the generated query with unnecessary parentheses, so we leave them out for things that only evaluated to placeholders or column names anyway. Other simple cases may be added here in the future, though we're limited in what we can *safely and reliably* analyze from already-generated SQL output. + // We don't want to bloat the generated query with unnecessary parentheses, so we leave them out for things that only evaluated to placeholders or field names anyway. Other simple cases may be added here in the future, though we're limited in what we can *safely and reliably* analyze from already-generated SQL output. return $query; } else { return $parenthesize($query); @@ -200,7 +202,7 @@ function $arrayFrom(items, canBeParameterized) { if (typeOf(items) === "placeholder") { return $handle(items); } else if (!canBeParameterized) { - // If the list contains SQL expressions, we cannot pass it in as a param at query time; we need to explicitly compile the expressions into the query + // If the list contains eg. SQL expressions, we cannot pass it in as a param at query time; we need to explicitly compile the expressions into the query let $items = items.map((item) => $maybeParenthesize($handle(item))); return $combine`ARRAY[${$join(", ", $items)}]`; @@ -224,14 +226,14 @@ function $arrayFrom(items, canBeParameterized) { // FIXME: createQueryObject/$ wrapper, including properties like relation mappings and dependent query queue let process = { - select: function ({ table, clauses }) { - let $table = $handle(table); + select: function ({ collection, clauses }) { + let $collection = $handle(collection); - let expectedClauseTypes = [ "where", "addColumns", "onlyColumns" ]; + let expectedClauseTypes = [ "where", "addFields", "onlyFields" ]; let clausesByType = splitFilterN(clauses, expectedClauseTypes, (clause) => clause.type); - let onlyColumns = clausesByType.onlyColumns.map((node) => node.columns).flat(); - let addColumns = clausesByType.addColumns.map((node) => node.columns).flat(); + let onlyFields = clausesByType.onlyFields.map((node) => node.fields).flat(); + let addFields = clausesByType.addFields.map((node) => node.fields).flat(); let whereExpressions = clausesByType.where.map((node) => node.expression); // FIXME: Fold relations @@ -245,56 +247,60 @@ let process = { }); let $groupByClause = asExpression(() => { - if (clausesByType.collapseBy.length > 0) { + if (clausesByType.collapseBy.length === 1) { // NOTE: We currently only support a single collapseBy clause - let collapseColumns = clausesByType.collapseBy[0].columns.columns; + let collapseFields = clausesByType.collapseBy[0].fields.fields; - return $combine`GROUP BY ${$join(", ", $handleAll(collapseColumns))}`; + return $combine`GROUP BY ${$join(", ", $handleAll(collapseFields))}`; + } else if (clausesByType.collapseBy.length > 1) { + // FIXME: Is this the correct place to check this? + throw new Error(`Encountered multiple collapseBy clauses in the same query; this is not currently supported`); } else { return NoQuery; } }); - let $columnSelection = columnList({ onlyColumns, addColumns }); + let $fieldSelection = $fieldList({ onlyColumns: onlyFields, addColumns: addFields }); let $orderedClauses = [ - $table, + $collection, $whereClause, $groupByClause ]; - return $combine`SELECT ${$columnSelection} FROM ${$join(" ", $orderedClauses)}`; + return $combine`SELECT ${$fieldSelection} FROM ${$join(" ", $orderedClauses)}`; }, - tableName: function ({ name }) { + collection: function ({ name }) { // FIXME: escape return $object({ query: name, params: [] }); }, - allColumns: function () { - // Special value used in column selection, to signify any/all columns + allFields: function () { + // Special value used in field selection, to signify any/all field return $object({ query: "*", params: [] }); }, - columnName: function ({ name }) { + field: function ({ name }) { // FIXME: escape return $object({ query: name, params: [] }); }, - hierarchical: function ({ columns }) { - return $combine`ROLLUP (${$join(", ", $handleAll(columns))})`; + // FIXME: Shouldn't there by some remoteField/foreignColumn handling here? + hierarchical: function ({ fields }) { + return $combine`ROLLUP (${$join(", ", $handleAll(fields))})`; }, - alias: function ({ column, expression }) { + alias: function ({ field, expression }) { // FIXME: escape - let $column = $handle(column); + let $field = $handle(field); let $expression = $handle(expression); - return $combine`${$expression} AS ${$column}`; + return $combine`${$expression} AS ${$field}`; }, sqlExpression: function ({ sql, parameters }) { return $object({ @@ -303,7 +309,7 @@ let process = { }); }, literalValue: function ({ value }) { - // FIXME: Check if this is valid in column selections / aliases + // FIXME: Check if this is valid in field selections / aliases return $object({ query: "?", params: [ value ] @@ -391,8 +397,10 @@ function $handle(node) { } } -// FIXME: Disallow stringifying things that are not top-level queries! Eg. `columnName` +// FIXME: Disallow stringifying things that are not top-level queries! Eg. `field` module.exports = function astToQuery(ast) { + // console.log(require("util").inspect({ast}, {colors: true,depth:null})); + let result = $handle(ast); return $object({ diff --git a/src/client/index.js b/src/client/index.js new file mode 100644 index 0000000..15f3715 --- /dev/null +++ b/src/client/index.js @@ -0,0 +1,171 @@ +"use strict"; + +const Promise = require("bluebird"); +const defaultValue = require("default-value"); +const syncpipe = require("syncpipe"); +const pg = require("pg"); + +const { validateOptions } = require("@validatem/core"); +const isString = require("@validatem/is-string"); +const isInteger = require("@validatem/is-integer"); +const required = require("@validatem/required"); +const requireEither = require("@validatem/require-either"); + +const astToQuery = require("../ast-to-query"); +const optimizeAST = require("../ast/optimize"); +const optimizers = require("../optimizers"); +const typeOf = require("../type-of"); + +function numberPlaceholders(query) { + // FIXME: This is a hack. Need to find a better way to assemble queries that doesn't rely on fragile regex afterwards to produce correctly-numbered placeholders. + let i = 0; + + return query.replace(/\?/g, () => { + i += 1; + return `$${i}`; + }); +} + +module.exports = function createClient(_options) { + let options = validateOptions(arguments, { + hostname: [ isString ], + port: [ isInteger ], // FIXME: isPortNumber + socket: [ isString ], + username: [ isString ], + password: [ isString ], + database: [ required, isString ] + }, requireEither([ "hostname", "socket" ])); + + let pool = new pg.Pool({ + // This is a `pg` weird-ism, it expects you to provide a socket path like it is a hostname + host: defaultValue(options.host, options.socket), + port: options.port, + user: options.username, + password: options.password, + database: options.database + }); + + function claimConnection() { + return Promise.try(() => { + return pool.connect(); + }).disposer((connection) => { + connection.release(); + }); + } + + function createClientInstance(options) { + return { + // FIXME: Rename to something that is clearly a verb, eg. `execute` or `do` + query: function (ast, parameters = {}) { + // FIXME/NOTE: `parameters` is an object, not an array! it fills in the placeholders in the built query + let pgClient = defaultValue(options.pgClient, pool); + + // FIXME: Allow passing in a precomputed query + let queryObject = syncpipe(ast, [ + (_) => optimizeAST(_, optimizers), + (_) => astToQuery(_.ast) + ]); + + // FIXME: Switch this over to proper typed Validatem validation + for (let key of queryObject.placeholders) { + if (!(key in parameters)) { + throw new Error(`Query requires a parameter '${key}', but it was not specified`); + } + } + + let numberedQuery = numberPlaceholders(queryObject.query); + + let processedParams = queryObject.params.map((param) => { + if (typeof param === "object" && typeOf(param) === "placeholder") { + return parameters[param.name]; + } else { + return param; + } + }); + + console.log({queryObject, numberedQuery, processedParams}); + + + // FIXME: Process placeholders! + return Promise.try(() => { + return pgClient.query(numberedQuery, processedParams); + }).then((result) => { + return result.rows; + }); + }, + tryQuery: function (query, parameters) { + // This automagically wraps a query in a conceptual transaction, so that if it fails, we can roll back to the point before the query. This is useful for eg. cases where you want to run some custom logic (that attempts a fallback query) on a UNIQUE violation *within a transaction*. Normally, in this situation the transaction would be 'locked' until a rollback is carried out, but if we don't have a savepoint right before the expected-to-fail query, we can't automatically roll back to the correct point. + // FIXME: Check whether an outer transaction is necessary for this to work correctly! + // TODO: Document this as "opportunistically try out a query, so that if it fails, it will be like the query never happened (but it will still throw the original error)". Make sure to explain why the regular query behaviour is different from this; automatically creating a savepoint for every single query could have undesirable performance implications. + return this.transaction((tx) => { + return tx.query(query, parameters); + }); + }, + transaction: function (callback) { + if (options.pgClient == null) { + // Not inside a transaction yet + return Promise.try(() => { + return claimConnection(); + }).then((disposableClient) => { + return Promise.using(disposableClient, (pgClient) => { + let instance = createClientInstance({ pgClient: pgClient }); + + return Promise.try(() => { + return callback(instance); + }).then(() => { + return pgClient.query("COMMIT"); + }).catch((error) => { + return Promise.try(() => { + return pgClient.query("ROLLBACK"); + }).then(() => { + throw error; + }); + }); + }); + }); + } else { + // Inside a transaction already + let currentSavepointID = defaultValue(options.savepointID, 0); + let newSavepointID = currentSavepointID + 1; + let newSavepointName = `ZAP_${newSavepointID}`; + + let instance = createClientInstance({ + pgClient: options.pgClient, + savepointID: newSavepointID + }); + + return Promise.try(() => { + // FIXME: Can we parameterize this? + return options.pgClient.query(`SAVEPOINT ${newSavepointName}`); + }).then(() => { + // Nested to ensure that we only try to rollback if it's actually the *user query itself* that fails, not the SAVEPOINT command + return Promise.try(() => { + return callback(instance); + }).catch((error) => { + return Promise.try(() => { + // FIXME: Can we parameterize this? + return options.pgClient.query(`ROLLBACK TO SAVEPOINT ${newSavepointName}`); + }).then(() => { + throw error; + }); + }); + }); + } + }, + destroy: function () { + if (options.pgClient == null) { + return pool.end(); + } else { + // FIXME: Add an explicit rollback function to the API, for cases where there's no thrown error but the transaction needs to be rolled back anyway due to some external condition? Or just expect the user to throw an error? Maybe a special error type that gets absorbed after applying the rollback? Maybe that should be named 'unsafe rollback' as it could wrongly represent a failure as a success from a Promises perspective? + throw new Error(`You can only destroy the ZapDB client outside of a transaction`); + } + } + }; + } + + return createClientInstance({}); +}; + + +// SELECT color, size, country_id, store_id, COUNT(*) AS total_sold, SUM(price) AS total_revenue FROM sales GROUP BY color, size, ROLLUP (country_id, store_id); +// SELECT * FROM sales GROUP BY color, size, ROLLUP (country_id, store_id); diff --git a/src/internal-operations/array-of.js b/src/internal-operations/array-of.js index 138e63f..3e7cc76 100644 --- a/src/internal-operations/array-of.js +++ b/src/internal-operations/array-of.js @@ -1,6 +1,7 @@ "use strict"; const { validateOptions } = require("@validatem/core"); +const either = require("@validatem/either"); const required = require("@validatem/required"); const oneOf = require("@validatem/one-of"); const arrayOf = require("@validatem/array-of"); @@ -9,13 +10,20 @@ const isBoolean = require("@validatem/is-boolean"); const node = require("../ast-node"); module.exports = function (operations) { - const isValueExpression = require("../validators/operations/is-value-expression")(operations); + const isConditionValue = require("../validators/operations/is-condition-value")(operations); + const isObjectType = require("../validators/operations/is-object-type")(operations); return function _arrayOf(_options) { let { type, items, canBeParameterized } = validateOptions(arguments, { type: [ required, oneOf([ "anyOf", "allOf" ]) ], canBeParameterized: [ required, isBoolean ], - items: [ required, arrayOf(isValueExpression) ] + items: [ required, either([ + arrayOf(either([ + isConditionValue, + isObjectType("_arrayOf") + ])), + isObjectType("placeholder") + ])] }); return node({ diff --git a/src/internal-operations/condition.js b/src/internal-operations/condition.js index 0001873..76b1186 100644 --- a/src/internal-operations/condition.js +++ b/src/internal-operations/condition.js @@ -9,12 +9,12 @@ 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); + const isConditionValue = require("../validators/operations/is-condition-value")(operations); return function _condition(_options) { let { type, expression } = validateOptions(arguments, { type: [ required, oneOf([ "lessThan", "moreThan", "equals" ]) ], - expression: [ required, either([ isValueExpression, isInternalArrayType ]) ] + expression: [ required, either([ isConditionValue, isInternalArrayType ]) ] }); return node({ diff --git a/src/operations/add-columns.js b/src/operations/add-columns.js deleted file mode 100644 index abdb951..0000000 --- a/src/operations/add-columns.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; - -const { validateArguments } = require("@validatem/core"); -const required = require("@validatem/required"); -const arrayOf = require("@validatem/array-of"); - -const node = require("../ast-node"); - -module.exports = function (operations) { - const isSelectionColumn = require("../validators/operations/is-selection-column")(operations); - - return function addColumns(_columns) { - let [ columns ] = validateArguments(arguments, { - columns: [ required, arrayOf([ required, isSelectionColumn ]) ] - }); - - return node({ type: "addColumns", columns: columns }); - }; -}; diff --git a/src/operations/add-fields.js b/src/operations/add-fields.js new file mode 100644 index 0000000..687e743 --- /dev/null +++ b/src/operations/add-fields.js @@ -0,0 +1,19 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const arrayOf = require("@validatem/array-of"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isSelectionField = require("../validators/operations/is-selection-field")(operations); // FIXME: This needs a more descriptive name. Selectable field? + + return function addFields(_fields) { + let [ fields ] = validateArguments(arguments, { + fields: [ required, arrayOf([ required, isSelectionField ]) ] + }); + + return node({ type: "addFields", fields: fields }); + }; +}; diff --git a/src/operations/alias.js b/src/operations/alias.js index 3298349..ad73ed0 100644 --- a/src/operations/alias.js +++ b/src/operations/alias.js @@ -2,25 +2,20 @@ const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); -const either = require("@validatem/either"); -const isString = require("@validatem/is-string"); const node = require("../ast-node"); module.exports = function (operations) { const isValueExpression = require("../validators/operations/is-value-expression")(operations); - const isObjectType = require("../validators/operations/is-object-type")(operations); - const wrapWithOperation = require("../validators/operations/wrap-with-operation")(operations); + const isField = require("../validators/operations/is-field"); return function alias(_name, _expression) { let [ name, expression ] = validateArguments(arguments, { - name: [ required, either([ - [ isObjectType("columnName") ], - [ isString, wrapWithOperation("column") ] - ])], + name: [ required, isField ], + // FIXME: Make more strict to only accept column references, and expect the user to use compute for the rest (which should internally use an _expression type or so to pass into alias, during set-collapse-by-field) expression: [ required, isValueExpression ] }); - return node({ type: "alias", column: name, expression: expression }); + return node({ type: "alias", field: name, expression: expression }); }; }; diff --git a/src/operations/all-of.js b/src/operations/all-of.js index 7b3c37d..dff0097 100644 --- a/src/operations/all-of.js +++ b/src/operations/all-of.js @@ -14,7 +14,12 @@ module.exports = function (operations) { items: [ required, isPredicateList ] }); - if (items.type === "conditions") { + if (items.type === "values") { + return node({ + type: "allOfValues", + items: items.value + }); + } else if (items.type === "conditions") { return node({ type: "allOfConditions", items: items.value diff --git a/src/operations/any-of.js b/src/operations/any-of.js index dee9d31..385df4e 100644 --- a/src/operations/any-of.js +++ b/src/operations/any-of.js @@ -14,7 +14,12 @@ module.exports = function (operations) { items: [ required, isPredicateList ] }); - if (items.type === "conditions") { + if (items.type === "values") { + return node({ + type: "anyOfValues", + items: items.value + }); + } else if (items.type === "conditions") { return node({ type: "anyOfConditions", items: items.value diff --git a/src/operations/collapse-by.js b/src/operations/collapse-by.js index 039b3b8..876db4a 100644 --- a/src/operations/collapse-by.js +++ b/src/operations/collapse-by.js @@ -6,21 +6,23 @@ const arrayOf = require("@validatem/array-of"); const node = require("../ast-node"); +// FIXME: Allow single field that gets auto-array-wrapped? + module.exports = function (operations) { - const isCollapsibleColumn = require("../validators/operations/is-collapsible-column")(operations); + const isCollapsibleField = require("../validators/operations/is-collapsible-field")(operations); const isCollapseByClause = require("../validators/operations/is-collapse-by-clause")(operations); - return function collapseBy(_columns) { - let [ columns, clauses ] = validateArguments(arguments, { - columns: [ required, arrayOf([ required, isCollapsibleColumn ]) ], + return function collapseBy(_fields) { + let [ fields, clauses ] = validateArguments(arguments, { + fields: [ required, arrayOf([ required, isCollapsibleField ]) ], clauses: [ arrayOf([ isCollapseByClause ]) ] }); return node({ type: "collapseBy", - columns: node({ - type: "collapseByColumns", - columns: columns + fields: node({ + type: "collapseByFields", + fields: fields }), clauses: clauses }); diff --git a/src/operations/compute.js b/src/operations/compute.js index 426df26..1281576 100644 --- a/src/operations/compute.js +++ b/src/operations/compute.js @@ -1,7 +1,6 @@ "use strict"; const { validateArguments } = require("@validatem/core"); -const either = require("@validatem/either"); const required = require("@validatem/required"); const anyProperty = require("@validatem/any-property"); const isString = require("@validatem/is-string"); @@ -35,7 +34,7 @@ module.exports = function (operations) { items: Object.entries(items.value).map(([ key, value ]) => { return node({ type: "compute", - column: operations.column(key), + field: operations.field(key), expression: value }); }) diff --git a/src/operations/count.js b/src/operations/count.js index 22b52e1..f14f153 100644 --- a/src/operations/count.js +++ b/src/operations/count.js @@ -1,27 +1,26 @@ "use strict"; const { validateArguments } = require("@validatem/core"); -const required = require("@validatem/required"); const node = require("../ast-node"); module.exports = function count(operations) { - const isTable = require("../validators/operations/is-table")(operations); + const isCollection = require("../validators/operations/is-collection")(operations); - return function count(_table) { - let [ table ] = validateArguments(arguments, { - table: [ isTable ] + return function count(_collection) { + let [ collection ] = validateArguments(arguments, { + collection: [ isCollection ] }); - // TODO: Investigate whether this can be made more performant by counting a specific column rather than *. That would probably require stateful knowledge of the database schema, though. - let columnReference = (table != null) - ? operations.foreignColumn({ table: table, column: "*" }) // FIXME: Make sure not to break this (internally) when making column name checks more strict - : operations.column("*"); + // TODO: Investigate whether this can be made more performant by counting a specific field rather than *. That would probably require stateful knowledge of the database schema, though. + let fieldReference = (collection != null) + ? operations.remoteField({ collection: collection, field: "*" }) // FIXME: Make sure not to break this (internally) when making field name checks more strict + : operations.field("*"); return node({ type: "aggregrateFunction", functionName: "count", - args: [ columnReference ] + args: [ fieldReference ] }); }; }; diff --git a/src/operations/equals.js b/src/operations/equals.js index f53a271..5fa5f04 100644 --- a/src/operations/equals.js +++ b/src/operations/equals.js @@ -6,11 +6,11 @@ const required = require("@validatem/required"); const node = require("../ast-node"); module.exports = function (operations) { - const isValueExpression = require("../validators/operations/is-value-expression")(operations); + const isConditionValue = require("../validators/operations/is-condition-value")(operations); return function equals(_name, _expression) { let [ expression ] = validateArguments(arguments, { - expression: [ required, isValueExpression ] + expression: [ required, isConditionValue ] }); return node({ type: "condition", conditionType: "equals", expression: expression }); diff --git a/src/operations/expression.js b/src/operations/expression.js index 309fb94..0135b39 100644 --- a/src/operations/expression.js +++ b/src/operations/expression.js @@ -7,15 +7,15 @@ const node = require("../ast-node"); module.exports = function (operations) { const isCondition = require("../validators/operations/is-condition")(operations); - const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(operations); + const isPossibleRemoteField = require("../validators/operations/is-possibly-remote-field")(operations); return function expression(_options) { let { left, condition } = validateOptions(arguments, { - left: [ required, isPossiblyForeignColumn ], // FIXME: allow sqlExpression and such + left: [ required, isPossibleRemoteField ], // FIXME: allow sqlExpression and such condition: [ required, isCondition ] }); - // FIXME/MARKER: Rename to 'assert'? + // FIXME/MARKER: Rename to 'assert' or something like that? return node({ type: "expression", diff --git a/src/operations/column.js b/src/operations/field.js similarity index 54% rename from src/operations/column.js rename to src/operations/field.js index dd4b44a..7fffadf 100644 --- a/src/operations/column.js +++ b/src/operations/field.js @@ -3,16 +3,16 @@ const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); -const isLocalColumnName = require("../validators/is-local-column-string"); +const isLocalFieldName = require("../validators/is-local-field-name"); const node = require("../ast-node"); -module.exports = function column(_operations) { +module.exports = function field(_operations) { return function (_name) { let [ name ] = validateArguments(arguments, { - name: [ required, isLocalColumnName ] + name: [ required, isLocalFieldName ] }); - return node({ type: "columnName", name }); + return node({ type: "field", name }); }; }; diff --git a/src/operations/foreign-column.js b/src/operations/foreign-column.js deleted file mode 100644 index b5b7e12..0000000 --- a/src/operations/foreign-column.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; - -const { validateArguments } = require("@validatem/core"); -const required = require("@validatem/required"); -const either = require("@validatem/either"); -const isString = require("@validatem/is-string"); - -const isForeignColumnString = require("../validators/is-foreign-column-string"); -const node = require("../ast-node"); - -module.exports = function (_operations) { - return function foreignColumn(_descriptor) { - let [ descriptor ] = validateArguments(arguments, { - descriptor: [ required, either([ - [ isForeignColumnString ], - // FIXME: Instruct users to use the below format specifically when dynamically specifying a foreign column, rather than string-concatenating - [{ - table: [ required, isString ], - column: [ required, isString ] - }] - ])] - }); - - return node({ - type: "foreignColumnName", - tableName: descriptor.table, - columnName: descriptor.column - }); - }; -}; diff --git a/src/operations/functions/now.js b/src/operations/functions/now.js new file mode 100644 index 0000000..26a427b --- /dev/null +++ b/src/operations/functions/now.js @@ -0,0 +1,14 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function now() { + // FIXME: Allow selecting statement_timestamp/clock_timestamp instead (as either a separate operation, or an option to this one) + return function now() { + return node({ + type: "sqlFunction", + functionName: "transaction_timestamp", // Equivalent to now(), but more clearly named + args: [] + }); + }; +}; diff --git a/src/operations/hierarchical.js b/src/operations/hierarchical.js index b2324fd..d94e2b1 100644 --- a/src/operations/hierarchical.js +++ b/src/operations/hierarchical.js @@ -7,16 +7,16 @@ const arrayOf = require("@validatem/array-of"); const node = require("../ast-node"); module.exports = function (operations) { - const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(operations); + const isPossiblyRemoteField = require("../validators/operations/is-possibly-remote-field")(operations); - return function hierarchical(_columns) { - let [ columns ] = validateArguments(arguments, { - columns: [ required, arrayOf([ required, isPossiblyForeignColumn ]) ] // FIXME: Require minimum 2, probably + return function hierarchical(_fields) { + let [ fields ] = validateArguments(arguments, { + fields: [ required, arrayOf([ required, isPossiblyRemoteField ]) ] // FIXME: Require minimum 2, probably }); return node({ type: "hierarchical", - columns: columns + fields: fields }); }; }; diff --git a/src/operations/index.js b/src/operations/index.js index 7e8ed01..9e1d01c 100644 --- a/src/operations/index.js +++ b/src/operations/index.js @@ -7,35 +7,11 @@ // FIXME: Verify that all wrapWith calls have a corresponding isObjectType arm require("array.prototype.flat").shim(); -const splitFilter = require("split-filter"); -const asExpression = require("as-expression"); - -const { validateArguments } = require("@validatem/core"); -const required = require("@validatem/required"); -const nestedArrayOf = require("@validatem/nested-array-of"); -const either = require("@validatem/either"); -const isString = require("@validatem/is-string"); -const isFunction = require("@validatem/is-function"); -const defaultTo = require("@validatem/default-to"); -const anyProperty = require("@validatem/any-property"); - const node = require("../ast-node"); -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); -const isSelectClause = require("../validators/operations/is-select-clause")(module.exports); -const isObjectType = require("../validators/operations/is-object-type")(module.exports); - -function normalizeClauses(clauses) { - if (clauses != null) { - return clauses.flat(Infinity); - } else { - return clauses; - } -} +// NOTE: withDerived has been replaced with compute // FIXME: All of the below need to be refactored, and moved into operation modules let operations = { @@ -56,82 +32,6 @@ let operations = { }; }) }); - }, - withDerived: function (_derivations) { - let [ derivations ] = validateArguments(arguments, { - derivations: [ required, anyProperty({ - key: [ required, isString ], // FIXME: Verify that this is not a foreign column name? - value: [ required, either([ - [ isFunction ], - [ isObjectType("sqlExpression") ] - ])] - })] - }); - - let derivationEntries = Object.entries(derivations); - - let [ functionTransforms, sqlTransforms ] = splitFilter(derivationEntries, ([ _key, value ]) => { - return (typeof value === "function"); - }); - - let postProcessClauses = asExpression(() => { - return functionTransforms.map(([ key, handler ]) => { - return module.exports.postProcess((results) => { - return results.map((result) => { - return { - ... result, - [key]: handler(result) - }; - }); - }); - }); - }); - - let columnClause = asExpression(() => { - if (sqlTransforms.length > 0) { - return module.exports.addColumns(sqlTransforms.map(([ key, expression ]) => { - return module.exports.alias(key, expression); - })); - } - }); - - return [ - postProcessClauses, - columnClause - ].filter((clause) => clause != null); - }, - has: function (_column, _options) { - let [ column, { query }] = validateArguments(arguments, { - column: [ required, isPossiblyForeignColumn ], - options: [ defaultTo({}), { - query: [ defaultTo([]), nestedArrayOf(isSelectClause), flatten ] - }] - }); - - // column: string or columnName or foreignColumnName - // query: array of clauses - return node({ - type: "has", - column: normalizePossiblyRemoteColumnName(column), - clauses: normalizeClauses(query) - }); - }, - belongsTo: function (column, { query } = {}) { - // column: string or columnName or foreignColumnName - // query: array of clauses - return node({ - type: "belongsTo", - column: normalizePossiblyRemoteColumnName(column), - clauses: normalizeClauses(query) - }); - }, - // FIXME: Refactor below - through: function (relations) { - // relations: array of has/belongsTo or string or columnName or foreignColumnName - return node({ - type: "through", - relations: relations.map(normalizeRelation) - }); } }; @@ -139,15 +39,15 @@ let operationModules = { // Base operations select: require("./select"), - // Column selection - addColumns: require("./add-columns"), - onlyColumns: require("./only-columns"), + // Field selection + addFields: require("./add-fields"), + onlyFields: require("./only-fields"), alias: require("./alias"), // Reference/scalar types - column: require("./column"), - foreignColumn: require("./column"), - table: require("./table"), + field: require("./field"), + remoteField: require("./remote-field"), + collection: require("./collection"), value: require("./value"), // Filtering @@ -175,64 +75,48 @@ let operationModules = { count: require("./count"), sum: require("./sum"), + // Relations and subqueries + belongsTo: require("./relations/belongs-to"), + has: require("./relations/has"), + through: require("./relations/through"), + withRelations: require("./relations/with-relations"), + define: require("./relations/define"), + resultExists: require("./relations/result-exists"), // FIXME: Better name? + linkTo: require("./relations/link-to"), + // Misc. parameter: require("./parameter"), postProcess: require("./post-process"), unsafeSQL: require("./unsafe-sql"), + now: require("./functions/now"), + + // Schema definitions + createCollection: require("./schema/create-collection"), + changeCollection: require("./schema/change-collection"), + deleteCollection: require("./schema/delete-collection"), + fields: require("./schema/fields"), + indexes: require("./schema/indexes"), + deleteField: require("./schema/delete-field"), + restoreAs: require("./schema/restore-as"), + optional: require("./schema/optional"), + defaultTo: require("./schema/default-to"), + + // Field types + autoID: require("./types/auto-id"), + boolean: require("./types/boolean"), + string: require("./types/string"), + uuid: require("./types/uuid"), + timestamp: require("./types/timestamp"), + + // Index types + primaryKey: require("./indexes/primary-key"), + index: require("./indexes/index"), + indexWhere: require("./indexes/index-where"), + unique: require("./indexes/unique"), + uniqueWhere: require("./indexes/unique-where"), }; Object.assign(module.exports, operations); evaluateCyclicalModulesOnto(module.exports, operationModules); -// module.exports = { -// ... operations, -// ... evaluateCyclicalModules(operationModules) -// }; - -// function normalizeNotExpression(input) { -// if (input == null || typeOf(input) === "condition") { -// return input; -// } else { -// return module.exports.equals(input); -// } -// } - -// function normalizePossiblyRemoteColumnName(input) { -// // FIXME: Validation - -// if (typeof input === "object") { -// // FIXME: Better check -// return input; -// } else if (input != null) { -// if (input.includes(".")) { -// return module.exports.foreignColumn(input); -// } else { -// return module.exports.column(input); -// } -// } else { -// return input; -// } -// } - -// function normalizeRelation(input) { -// // FIXME: Validation -// // accept columnName, foreignColumnName, string with or without dot - -// if (typeof input === "object" && [ "has", "belongsTo", "through" ].includes(typeOf(input))) { -// return input; -// } else { -// let columnName = (typeof input === "string") -// ? normalizePossiblyRemoteColumnName(input) -// : input; - -// if (typeOf(columnName) === "columnName") { -// return module.exports.belongsTo(input); -// } else if (typeOf(columnName) === "foreignColumnName") { -// return module.exports.has(input); -// } else { -// unreachable(`Invalid type: ${typeOf(columnName)}`); -// } -// } -// } - // NOTE: normalizeExpression should sometimes only accept sql/literal, but sometimes also sql/literal/condition? diff --git a/src/operations/indexes/index-where.js b/src/operations/indexes/index-where.js new file mode 100644 index 0000000..0602913 --- /dev/null +++ b/src/operations/indexes/index-where.js @@ -0,0 +1,21 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); + +const node = require("../../ast-node"); + +module.exports = function (operations) { + return function indexWhere(_expression) { + const isExpression = require("../../validators/operations/is-expression")(operations); + + let [ expression ] = validateArguments(arguments, { + expression: [ required, isExpression ] + }); + + return node({ + type: "index", + expression: expression + }); + }; +}; diff --git a/src/operations/indexes/index.js b/src/operations/indexes/index.js new file mode 100644 index 0000000..f527068 --- /dev/null +++ b/src/operations/indexes/index.js @@ -0,0 +1,11 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + return function index() { + return node({ + type: "index" + }); + }; +}; diff --git a/src/operations/indexes/primary-key.js b/src/operations/indexes/primary-key.js new file mode 100644 index 0000000..ca1c661 --- /dev/null +++ b/src/operations/indexes/primary-key.js @@ -0,0 +1,11 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + return function primaryKey() { + return node({ + type: "primaryKey" + }); + }; +}; diff --git a/src/operations/indexes/unique-where.js b/src/operations/indexes/unique-where.js new file mode 100644 index 0000000..eeb996b --- /dev/null +++ b/src/operations/indexes/unique-where.js @@ -0,0 +1,21 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); + +const node = require("../../ast-node"); + +module.exports = function (operations) { + return function uniqueWhere(_expression) { + const isExpression = require("../../validators/operations/is-expression")(operations); + + let [ expression ] = validateArguments(arguments, { + expression: [ required, isExpression ] + }); + + return node({ + type: "uniqueIndex", + expression: expression + }); + }; +}; diff --git a/src/operations/indexes/unique.js b/src/operations/indexes/unique.js new file mode 100644 index 0000000..433cee3 --- /dev/null +++ b/src/operations/indexes/unique.js @@ -0,0 +1,11 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + return function unique() { + return node({ + type: "uniqueIndex" + }); + }; +}; diff --git a/src/operations/less-than.js b/src/operations/less-than.js index 27c52ab..999e861 100644 --- a/src/operations/less-than.js +++ b/src/operations/less-than.js @@ -6,11 +6,11 @@ const required = require("@validatem/required"); const node = require("../ast-node"); module.exports = function (operations) { - const isValueExpression = require("../validators/operations/is-value-expression")(operations); + const isConditionValue = require("../validators/operations/is-condition-value")(operations); return function lessThan(_name, _expression) { let [ expression ] = validateArguments(arguments, { - expression: [ required, isValueExpression ] + expression: [ required, isConditionValue ] }); // FIXME: Calling it `expression` is super confusing. That needs a better name. diff --git a/src/operations/more-than.js b/src/operations/more-than.js index 95b0eff..2187285 100644 --- a/src/operations/more-than.js +++ b/src/operations/more-than.js @@ -6,11 +6,11 @@ const required = require("@validatem/required"); const node = require("../ast-node"); module.exports = function (operations) { - const isValueExpression = require("../validators/operations/is-value-expression")(operations); + const isConditionValue = require("../validators/operations/is-condition-value")(operations); return function moreThan(_name, _expression) { let [ expression ] = validateArguments(arguments, { - expression: [ required, isValueExpression ] + expression: [ required, isConditionValue ] }); return node({ type: "condition", conditionType: "moreThan", expression: expression }); diff --git a/src/operations/only-columns.js b/src/operations/only-columns.js deleted file mode 100644 index 95c1943..0000000 --- a/src/operations/only-columns.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; - -const { validateArguments } = require("@validatem/core"); -const required = require("@validatem/required"); -const arrayOf = require("@validatem/array-of"); - -const node = require("../ast-node"); - -module.exports = function (operations) { - const isSelectionColumn = require("../validators/operations/is-selection-column")(operations); - - return function onlyColumns(_columns) { - let [ columns ] = validateArguments(arguments, { - columns: [ required, arrayOf([ required, isSelectionColumn ]) ] - }); - - return node({ type: "onlyColumns", columns: columns }); - }; -}; diff --git a/src/operations/only-fields.js b/src/operations/only-fields.js new file mode 100644 index 0000000..0f37019 --- /dev/null +++ b/src/operations/only-fields.js @@ -0,0 +1,19 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const arrayOf = require("@validatem/array-of"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isSelectableField = require("../validators/operations/is-selectable-field")(operations); + + return function onlyFields(_fields) { + let [ fields ] = validateArguments(arguments, { + fields: [ required, arrayOf([ required, isSelectableField ]) ] + }); + + return node({ type: "onlyFields", fields: fields }); + }; +}; diff --git a/src/operations/parameter.js b/src/operations/parameter.js index 6906d09..1ade06d 100644 --- a/src/operations/parameter.js +++ b/src/operations/parameter.js @@ -14,7 +14,8 @@ module.exports = function (_operations) { return node({ type: "placeholder", - name: name + name: name, + parameterizable: true }); }; }; diff --git a/src/operations/relations/belongs-to.js b/src/operations/relations/belongs-to.js new file mode 100644 index 0000000..f7633bc --- /dev/null +++ b/src/operations/relations/belongs-to.js @@ -0,0 +1,28 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const defaultTo = require("@validatem/default-to"); +const nestedArrayOf = require("@validatem/nested-array-of"); +const flatten = require("../../validators/flatten"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isRelationClause = require("../../validators/operations/is-relation-clause")(operations); + const isField = require("../../validators/operations/is-field")(operations); + + return function belongsTo(_field, _clauses) { + let [ field, clauses ] = validateArguments(arguments, { + field: [ required, isField ], + clauses: [ defaultTo([]), nestedArrayOf(isRelationClause), flatten ] + }); + + return node({ + type: "relation", + relationType: "belongsTo", + field: field, + clauses: clauses + }); + }; +}; diff --git a/src/operations/relations/has.js b/src/operations/relations/has.js new file mode 100644 index 0000000..fee234d --- /dev/null +++ b/src/operations/relations/has.js @@ -0,0 +1,28 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const defaultTo = require("@validatem/default-to"); +const nestedArrayOf = require("@validatem/nested-array-of"); +const flatten = require("../../validators/flatten"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isRelationClause = require("../../validators/operations/is-relation-clause")(operations); + const isRemoteField = require("../../validators/operations/is-remote-field")(operations); + + return function has(_field, _clauses) { + let [ field, clauses ] = validateArguments(arguments, { + field: [ required, isRemoteField ], + clauses: [ defaultTo([]), nestedArrayOf(isRelationClause), flatten ] + }); + + return node({ + type: "relation", + relationType: "has", + field: field, + clauses: clauses + }); + }; +}; diff --git a/src/operations/relations/through.js b/src/operations/relations/through.js new file mode 100644 index 0000000..4c1c713 --- /dev/null +++ b/src/operations/relations/through.js @@ -0,0 +1,31 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const arrayOf = require("@validatem/array-of"); +const either = require("@validatem/either"); + +const node = require("../ast-node"); + +module.exports = function (operations) { + const isObjectType = require("../../validators/operations/is-object-type")(operations); + const isPossiblyRemoteField = require("../../validators/operations/is-possibly-remote-field")(operations); + const wrapWithOperation = require("../../validators/operations/wrap-with-operation")(operations); + + return function through(_relations) { + let [ relations ] = validateArguments(arguments, { + relations: [ required, arrayOf(either([ + [ isObjectType("relation") ], + [ isPossiblyRemoteField, either([ + [ isObjectType("field"), wrapWithOperation("belongsTo") ], + [ isObjectType("remoteField"), wrapWithOperation("has") ], + ])] + ]))] + }); + + return node({ + type: "throughRelations", + relations: relations + }); + }; +}; diff --git a/src/operations/remote-field.js b/src/operations/remote-field.js new file mode 100644 index 0000000..7a74c88 --- /dev/null +++ b/src/operations/remote-field.js @@ -0,0 +1,31 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const either = require("@validatem/either"); + +const isRemoteFieldName = require("../validators/is-remote-field-name"); +const isLocalFieldName = require("../validators/is-local-field-name"); +const isCollectionName = require("../validators/is-collection-name"); +const node = require("../ast-node"); + +module.exports = function (_operations) { + return function remoteField(_descriptor) { + let [ descriptor ] = validateArguments(arguments, { + descriptor: [ required, either([ + [ isRemoteFieldName ], // NOTE: This returns a parsed version of the name + // FIXME: Instruct users to use the below format specifically when dynamically specifying a remote field, rather than string-concatenating + [{ + collection: [ required, isCollectionName ], + field: [ required, isLocalFieldName ] + }] + ])] + }); + + return node({ + type: "remoteField", + collectionName: descriptor.collection, + fieldName: descriptor.field + }); + }; +}; diff --git a/src/operations/result-exists.js b/src/operations/result-exists.js new file mode 100644 index 0000000..c47e320 --- /dev/null +++ b/src/operations/result-exists.js @@ -0,0 +1,24 @@ +"use strict"; + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const either = require("@validatem/either"); + +const node = require("../../ast-node"); + +module.exports = function (operations) { + return function resultExists(_expression) { + const isExpression = require("../../validators/operations/is-expression")(operations); + const isRelation = require("../../validators/operations/is-relation")(operations); // FIXME: More specific type? + // MARKER: Finish this + + let [ expression ] = validateArguments(arguments, { + expression: [ required, either([ isExpression, isRelation ]) ] + }); + + return node({ + type: "index", + expression: expression + }); + }; +}; diff --git a/src/operations/schema/change-collection.js b/src/operations/schema/change-collection.js new file mode 100644 index 0000000..8892ced --- /dev/null +++ b/src/operations/schema/change-collection.js @@ -0,0 +1,36 @@ +"use strict"; + +const node = require("../../ast-node"); + +const { validateArguments } = require("@validatem/core"); +const either = require("@validatem/either"); +const required = require("@validatem/required"); +const arrayOf = require("@validatem/array-of"); + +const isCollectionName = require("../../validators/is-collection-name"); + +module.exports = function (operations) { + const isObjectType = require("../../validators/operations/is-object-type")(operations); + const isFieldsObject = require("../../validators/operations/schema/is-fields-object")(operations); + + return function changeCollection(_name, _operations) { + let [ name, collectionOperations ] = validateArguments(arguments, { + name: [ required, isCollectionName ], + operations: [ required, either([ + [ isFieldsObject, (fieldsObject) => { + return [ operations.fields(fieldsObject) ]; + }], + arrayOf(either([ + isObjectType("schemaFields"), + isObjectType("schemaIndexes") + ])) + ]) ] + }); + + return node({ + type: "changeCollectionCommand", + name: name, + operations: collectionOperations + }); + }; +}; diff --git a/src/operations/schema/create-collection.js b/src/operations/schema/create-collection.js new file mode 100644 index 0000000..06a281d --- /dev/null +++ b/src/operations/schema/create-collection.js @@ -0,0 +1,36 @@ +"use strict"; + +const node = require("../../ast-node"); + +const { validateArguments } = require("@validatem/core"); +const either = require("@validatem/either"); +const required = require("@validatem/required"); +const arrayOf = require("@validatem/array-of"); + +const isCollectionName = require("../../validators/is-collection-name"); + +module.exports = function (operations) { + const isObjectType = require("../../validators/operations/is-object-type")(operations); + const isFieldsObject = require("../../validators/operations/schema/is-fields-object")(operations); + + return function createCollection(_name, _operations) { + let [ name, collectionOperations ] = validateArguments(arguments, { + name: [ required, isCollectionName ], + operations: [ required, either([ + [ isFieldsObject, (fieldsObject) => { + return [ operations.fields(fieldsObject) ]; + }], + arrayOf(either([ + isObjectType("schemaFields"), + isObjectType("schemaIndexes") + ])) // FIXME: At least one of schemaFields + ]) ] + }); + + return node({ + type: "createCollectionCommand", + name: name, + operations: collectionOperations + }); + }; +}; diff --git a/src/operations/schema/default-to.js b/src/operations/schema/default-to.js new file mode 100644 index 0000000..b56928a --- /dev/null +++ b/src/operations/schema/default-to.js @@ -0,0 +1,21 @@ +"use strict"; + +const node = require("../../ast-node"); + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); + +module.exports = function (operations) { + const isLocalValueExpression = require("../../validators/operations/is-local-value-expression")(operations); + + return function defaultTo(_value) { + let [ value ] = validateArguments(arguments, { + value: [ required, isLocalValueExpression ] + }); + + return node({ + type: "defaultTo", + value: value + }); + }; +}; diff --git a/src/operations/schema/delete-collection.js b/src/operations/schema/delete-collection.js new file mode 100644 index 0000000..dc5cc8e --- /dev/null +++ b/src/operations/schema/delete-collection.js @@ -0,0 +1,21 @@ +"use strict"; + +const node = require("../../ast-node"); + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); + +const isCollectionName = require("../../validators/is-collection-name"); + +module.exports = function (_operations) { + return function deleteCollection(_name) { + let [ name ] = validateArguments(arguments, { + name: [ required, isCollectionName ] + }); + + return node({ + type: "deleteCollectionCommand", + name: name + }); + }; +}; diff --git a/src/operations/schema/fields.js b/src/operations/schema/fields.js new file mode 100644 index 0000000..3f5c1fc --- /dev/null +++ b/src/operations/schema/fields.js @@ -0,0 +1,21 @@ +"use strict"; + +const node = require("../../ast-node"); + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); + +module.exports = function (operations) { + const isFieldsObject = require("../../validators/operations/schema/is-fields-object")(operations); + + return function fields(_fields) { + let [ fields ] = validateArguments(arguments, { + fields: [ required, isFieldsObject ] + }); + + return node({ + type: "schemaFields", + fields: fields + }); + }; +}; diff --git a/src/operations/schema/indexes.js b/src/operations/schema/indexes.js new file mode 100644 index 0000000..d0d6670 --- /dev/null +++ b/src/operations/schema/indexes.js @@ -0,0 +1,22 @@ +"use strict"; + +const node = require("../../ast-node"); + +const { validateArguments } = require("@validatem/core"); +const required = require("@validatem/required"); +const arrayOf = require("@validatem/array-of"); + +module.exports = function (operations) { + const isCompositeIndexType = require("../../validators/operations/schema/is-composite-index-type")(operations); + + return function indexes(_indexes) { + let [ indexes ] = validateArguments(arguments, { + indexes: [ required, arrayOf(isCompositeIndexType) ] + }); + + return node({ + type: "schemaIndexes", + indexes: indexes + }); + }; +}; diff --git a/src/operations/select.js b/src/operations/select.js index b8104d7..fbd14a7 100644 --- a/src/operations/select.js +++ b/src/operations/select.js @@ -14,15 +14,15 @@ module.exports = function (operations) { const isSelectClause = require("../validators/operations/is-select-clause")(operations); const wrapWithOperation = require("../validators/operations/wrap-with-operation")(operations); - return function select(_table, _clauses) { - let [ table, clauses ] = validateArguments(arguments, { - table: [ required, either([ - [ isObjectType("tableName") ], - [ isString, wrapWithOperation("table") ] + return function select(_collection, _clauses) { + let [ collection, clauses ] = validateArguments(arguments, { + collection: [ required, either([ + [ isObjectType("collection") ], + [ isString, wrapWithOperation("collection") ] ])], clauses: [ required, nestedArrayOf(isSelectClause), flatten ] }); - return node({ type: "select", table: table, clauses: clauses }); + return node({ type: "select", collection: collection, clauses: clauses }); }; }; diff --git a/src/operations/sum.js b/src/operations/sum.js index a7ad588..c90888d 100644 --- a/src/operations/sum.js +++ b/src/operations/sum.js @@ -6,17 +6,17 @@ const required = require("@validatem/required"); const node = require("../ast-node"); module.exports = function count(operations) { - const isPossiblyForeignColumn = require("../validators/operations/is-possibly-foreign-column")(operations); + const isPossiblyRemoteField = require("../validators/operations/is-possibly-remote-field")(operations); - return function sum(_column) { - let [ column ] = validateArguments(arguments, { - column: [ required, isPossiblyForeignColumn ] + return function sum(_field) { + let [ field ] = validateArguments(arguments, { + field: [ required, isPossiblyRemoteField ] }); return node({ type: "aggregrateFunction", functionName: "sum", - args: [ column ] + args: [ field ] }); }; }; diff --git a/src/operations/table.js b/src/operations/table.js index c4ca02e..da611fc 100644 --- a/src/operations/table.js +++ b/src/operations/table.js @@ -3,16 +3,16 @@ const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); -const isTableName = require("../validators/is-table-name"); +const isCollectionName = require("../validators/is-collection-name"); const node = require("../ast-node"); module.exports = function (_operations) { return function (_name) { let [ name ] = validateArguments(arguments, { - name: [ required, isTableName ] + name: [ required, isCollectionName ] }); - return node({ type: "tableName", name }); + return node({ type: "collection", name }); }; }; diff --git a/src/operations/types/auto-id.js b/src/operations/types/auto-id.js new file mode 100644 index 0000000..2c5ff41 --- /dev/null +++ b/src/operations/types/auto-id.js @@ -0,0 +1,12 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + // FIXME: Length options? + return function autoID() { + return node({ + type: "autoIDField" + }); + }; +}; diff --git a/src/operations/types/boolean.js b/src/operations/types/boolean.js new file mode 100644 index 0000000..8555908 --- /dev/null +++ b/src/operations/types/boolean.js @@ -0,0 +1,11 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + return function boolean() { + return node({ + type: "booleanField" + }); + }; +}; diff --git a/src/operations/types/string.js b/src/operations/types/string.js new file mode 100644 index 0000000..2bb3058 --- /dev/null +++ b/src/operations/types/string.js @@ -0,0 +1,12 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + // FIXME: Length options? + return function string() { + return node({ + type: "stringField" + }); + }; +}; diff --git a/src/operations/types/timestamp.js b/src/operations/types/timestamp.js new file mode 100644 index 0000000..2ebcb31 --- /dev/null +++ b/src/operations/types/timestamp.js @@ -0,0 +1,12 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + // FIXME: withTimezone option + return function timestamp() { + return node({ + type: "timestampField" + }); + }; +}; diff --git a/src/operations/types/uuid.js b/src/operations/types/uuid.js new file mode 100644 index 0000000..9e159bd --- /dev/null +++ b/src/operations/types/uuid.js @@ -0,0 +1,11 @@ +"use strict"; + +const node = require("../../ast-node"); + +module.exports = function (_operations) { + return function uuid() { + return node({ + type: "uuidField" + }); + }; +}; diff --git a/src/optimizers/arrayify-predicate-lists.js b/src/optimizers/arrayify-predicate-lists.js index fbfa446..ab55648 100644 --- a/src/optimizers/arrayify-predicate-lists.js +++ b/src/optimizers/arrayify-predicate-lists.js @@ -10,16 +10,17 @@ const concat = require("../concat"); const typeOf = require("../type-of"); const NoChange = require("./util/no-change"); -let parameterizableTypes = [ "literalValue", "placeholder" ]; +let parameterizableTypes = new Set([ "literalValue", "placeholder" ]); +let disallowedNestedTypes = new Set([ "_arrayOf" ]); // 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 ])}`; + if (left.type === "field") { + return `field:${JSON.stringify(left.name)}`; + } else if (left.type === "remoteField") { + return `remoteField:${JSON.stringify([ left.collection.name, left.field.name ])}`; } else if (left.type === "sqlExpression") { return `sqlExpression:${JSON.stringify(left.expression)}`; } else { @@ -61,7 +62,7 @@ function createExpressionTracker() { }; } -function createHandler(type) { +function createExpressionHandler(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, @@ -79,12 +80,16 @@ function createHandler(type) { 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 (item.type === "expression") { // FIXME: typeOf + if (disallowedNestedTypes.has(typeOf(item.condition.expression))) { // FIXME: Check that this full path is always valid + // Abort immediately, we have encountered a previously-processed array which means we cannot nest any further. + return NoChange; + } else { + tracker.addExpression(item); + } } } - + if (tracker.arrayIsPossible()) { let newExpressions = syncpipe(tracker, [ (_) => _.getMapping(), @@ -96,8 +101,8 @@ function createHandler(type) { return expressions[0]; } else { let allValues = expressions.map((expression) => expression.condition.expression); - let canBeParameterized = allValues.every((value) => parameterizableTypes.includes(typeOf(value))); - + let canBeParameterized = allValues.every((value) => parameterizableTypes.has(typeOf(value))); + return operations.expression({ left: expressions[0].left, condition: internalOperations._condition({ @@ -127,11 +132,33 @@ function createHandler(type) { }; } +function createValueHandler(type) { + let internalArrayType = matchValue(type, { + "anyOfValues": "anyOf", + "allOfValues": "allOf" + }); + + return function arrayifyValueList(node) { + if (!Array.isArray(node.items)) { + // We only handle the placeholder / single SQL fragment / etc. special cases here, not the usual list-of-items cases + return internalOperations._arrayOf({ + type: internalArrayType, + canBeParameterized: parameterizableTypes.has(typeOf(node.items)), + items: node.items + }); + } else { + return NoChange; + } + }; +} + module.exports = { name: "arrayify-predicate-lists", category: [ "readability" ], visitors: { - allOfExpressions: createHandler("allOfExpressions"), - anyOfExpressions: createHandler("anyOfExpressions"), + allOfExpressions: createExpressionHandler("allOfExpressions"), + anyOfExpressions: createExpressionHandler("anyOfExpressions"), + allOfValues: createValueHandler("allOfValues"), + anyOfValues: createValueHandler("anyOfValues"), } }; diff --git a/src/optimizers/collapse-where.js b/src/optimizers/collapse-where.js index af39042..3f80a38 100644 --- a/src/optimizers/collapse-where.js +++ b/src/optimizers/collapse-where.js @@ -11,14 +11,14 @@ module.exports = { name: "collapse-where", category: [ "normalization" ], visitors: { - select: ({ table, clauses }) => { + select: ({ collection, 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)); + return operations.select(collection, [ newWhere ].concat(otherClauses)); } else { return NoChange; } diff --git a/src/optimizers/conditions-to-expressions.js b/src/optimizers/conditions-to-expressions.js index 5f228c4..55b3a95 100644 --- a/src/optimizers/conditions-to-expressions.js +++ b/src/optimizers/conditions-to-expressions.js @@ -3,16 +3,22 @@ const matchValue = require("match-value"); const operations = require("../operations"); +const internalOperations = require("../internal-operations"); const typeOf = require("../type-of"); const unreachable = require("../unreachable"); const NoChange = require("./util/no-change"); +// FIXME: anyOf(not(placeholder)) should be disallowed, but probably currently is allowed +// not(anyOf(placeholder)) is fine though + +let unprocessableTypes = new Set([ "sqlExpression", "placeholder" ]); + module.exports = { name: "conditions-to-expressions", category: [ "normalization" ], visitors: { expression: (rootNode) => { - if (rootNode.condition.type === "condition") { + if (rootNode.condition.type === "condition") { // FIXME: typeOf return NoChange; } else { // anyOfConditions, allOfConditions, notCondition @@ -26,7 +32,29 @@ module.exports = { }); if (listOperation != null) { - return listOperation(node.items.map((item) => convertNode(item))); + if (Array.isArray(node.items)) { + return listOperation(node.items.map((item) => convertNode(item))); + } else if (unprocessableTypes.has(typeOf(node.items))) { + // Placeholders / unsafe SQL expressions are not representable as a series of expressions, as we don't know the value of them upfront. Therefore we immediately convert it to an internal ANY/ALL expression here, so that it doesn't accidentally get touched by anything else afterwards. + // FIXME: Check the relevance here of node.items.parameterizable for placeholders + + return operations.expression({ + left: rootNode.left, + condition: internalOperations._condition({ + type: "equals", + expression: internalOperations._arrayOf({ + type: matchValue(typeOf(node), { + anyOfConditions: "anyOf", + allOfConditions: "allOf" + }), + canBeParameterized: true, + items: node.items + }) + }) + }); + } else { + unreachable("Condition item was not an array"); + } } else if (typeOf(node) === "notCondition") { return operations.not(convertNode(node.condition)); } else if (typeOf(node) === "condition") { diff --git a/src/optimizers/flatten-predicate-lists.js b/src/optimizers/flatten-predicate-lists.js index 068da0e..513c931 100644 --- a/src/optimizers/flatten-predicate-lists.js +++ b/src/optimizers/flatten-predicate-lists.js @@ -15,40 +15,47 @@ function createHandler(type) { allOfExpressions: operations.allOf, anyOfConditions: operations.anyOf, allOfConditions: operations.allOf, + anyOfValues: operations.anyOf, + allOfValues: 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 (Array.isArray(list.items)) { + 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 = []; + if (mustFlatten) { + let actualItems = []; - function collectItemsRecursively(node) { - for (let subItem of node.items) { - if (typeOf(subItem) === type) { - collectItemsRecursively(subItem); - } else { - actualItems.push(subItem); + function collectItemsRecursively(node) { + for (let subItem of node.items) { + if (typeOf(subItem) === type) { + collectItemsRecursively(subItem); + } else { + actualItems.push(subItem); + } } } - } - collectItemsRecursively(list); + 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]; + 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 listOperation(actualItems); + return NoChange; } } else { + // Ignore placeholders, SQL fragments, etc. return NoChange; } }; @@ -62,5 +69,7 @@ module.exports = { anyOfExpressions: createHandler("anyOfExpressions"), allOfConditions: createHandler("allOfConditions"), anyOfConditions: createHandler("anyOfConditions"), + allOfValues: createHandler("allOfValues"), + anyOfValues: createHandler("anyOfValues"), } }; diff --git a/src/optimizers/index.js b/src/optimizers/index.js index 6ef3701..d1fc52d 100644 --- a/src/optimizers/index.js +++ b/src/optimizers/index.js @@ -2,10 +2,11 @@ module.exports = [ require("./collapse-where"), + require("./values-to-conditions"), require("./conditions-to-expressions"), require("./flatten-not-predicates"), require("./flatten-predicate-lists"), require("./arrayify-predicate-lists"), - require("./set-collapse-by-columns"), + require("./set-collapse-by-fields"), // require("./test-context"), ]; diff --git a/src/optimizers/set-collapse-by-columns.js b/src/optimizers/set-collapse-by-columns.js deleted file mode 100644 index 1b438b3..0000000 --- a/src/optimizers/set-collapse-by-columns.js +++ /dev/null @@ -1,157 +0,0 @@ -"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 diff --git a/src/optimizers/set-collapse-by-fields.js b/src/optimizers/set-collapse-by-fields.js new file mode 100644 index 0000000..6b74820 --- /dev/null +++ b/src/optimizers/set-collapse-by-fields.js @@ -0,0 +1,185 @@ +"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 + ]) + }); + } + } + }); + }, + } +}; + +// 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 diff --git a/src/optimizers/test-context.js b/src/optimizers/test-context.js index 9ccc8d7..3a8490e 100644 --- a/src/optimizers/test-context.js +++ b/src/optimizers/test-context.js @@ -8,45 +8,45 @@ module.exports = { name: "test-context", category: [ "testing" ], visitors: { - columnName: (node, { setState }) => { - setState("seenColumn", node.name); + field: (node, { setState }) => { + setState("seenField", node.name); return NoChange; }, select: (node, { registerStateHandler, defer }) => { - let seenColumns = new Set(); + let seenFields = new Set(); - registerStateHandler("seenColumnsInWhere", (names) => { + registerStateHandler("seenFieldsInWhere", (names) => { for (let name of names) { - seenColumns = seenColumns.add(name); + seenFields = seenFields.add(name); } }); // FIXME: Definitely need better AST modification/derivation tools... probably some sort of deep-modifying utility, for starters. Maybe merge-by-template can be of use here? With a custom AST node merger? It probably doesn't support non-enumerable properties correctly right now, though... return defer(() => { - console.log("Seen columns in WHERE in SELECT:", seenColumns); + // console.log("Seen fields in WHERE in SELECT:", seenFields); - let onlyColumnsClause = node.clauses.find((clause) => clause.type === "onlyColumns"); + let onlyFieldsClause = node.clauses.find((clause) => clause.type === "onlyFields"); - let columnsAlreadyAdded = onlyColumnsClause != null && Array.from(seenColumns).every((column) => { - return onlyColumnsClause.columns.some((existingColumn) => existingColumn.name === column); + let fieldsAlreadyAdded = onlyFieldsClause != null && Array.from(seenFields).every((field) => { + return onlyFieldsClause.fields.some((existingField) => existingField.name === field); }); - if (!columnsAlreadyAdded) { + if (!fieldsAlreadyAdded) { // NOTE: This is a good test case for optimizer stability! Just returning a derived node in every case. - let newOnlyColumnsClause = (onlyColumnsClause == null) - ? operations.onlyColumns(Array.from(seenColumns)) - : deriveNode(onlyColumnsClause, { - columns: onlyColumnsClause.columns.concat(Array.from(seenColumns).map((columnName) => { - return operations.column(columnName); + let newOnlyFieldsClause = (onlyFieldsClause == null) + ? operations.onlyFields(Array.from(seenFields)) + : deriveNode(onlyFieldsClause, { + fields: onlyFieldsClause.fields.concat(Array.from(seenFields).map((fieldName) => { + return operations.field(fieldName); })) }); return deriveNode(node, { clauses: node.clauses - .filter((clause) => clause.type !== "onlyColumns") - .concat([ newOnlyColumnsClause ]) + .filter((clause) => clause.type !== "onlyFields") + .concat([ newOnlyFieldsClause ]) }); } else { return NoChange; @@ -54,20 +54,20 @@ module.exports = { }); }, where: (node, { registerStateHandler, defer, setState }) => { - let seenColumns = []; + let seenFields = []; - registerStateHandler("seenColumn", (name) => seenColumns.push(name)); + registerStateHandler("seenField", (name) => seenFields.push(name)); return defer(() => { - setState("seenColumnsInWhere", seenColumns); + setState("seenFieldsInWhere", seenFields); return NoChange; }); - // let seenColumns = []; + // let seenFields = []; // let id = Math.random(); - // registerStateHandler("seenColumn", (name) => { - // seenColumns.push(name); + // registerStateHandler("seenField", (name) => { + // seenFields.push(name); // }); // console.log("Scheduling defer", id); @@ -76,8 +76,8 @@ module.exports = { // console.log("Defer called", id); // // MARKER: This gets called twice, but should only be called once! - // // console.log("Seen columns in WHERE:", seenColumns, require("util").inspect(node, {colors:true,depth:null})); - // console.log("Seen columns in WHERE:", seenColumns); + // // console.log("Seen fields in WHERE:", seenFields, require("util").inspect(node, {colors:true,depth:null})); + // console.log("Seen fields in WHERE:", seenFields); // return NoChange; // }); } diff --git a/src/optimizers/values-to-conditions.js b/src/optimizers/values-to-conditions.js new file mode 100644 index 0000000..896327d --- /dev/null +++ b/src/optimizers/values-to-conditions.js @@ -0,0 +1,47 @@ +"use strict"; + +const matchValue = require("match-value"); + +const operations = require("../operations"); +const internalOperations = require("../internal-operations"); +const NoChange = require("./util/no-change"); +const typeOf = require("../type-of"); + +let valueListTypes = new Set([ "anyOfValues", "allOfValues" ]); + +module.exports = { + name: "values-to-conditions", + category: [ "normalization" ], + visitors: { + condition: (rootNode) => { + let isListType = valueListTypes.has(typeOf(rootNode.expression)); + let listContainsArray = isListType && Array.isArray(rootNode.expression.items); // FIXME: Make this `unpackable` metadata on the AST node? + + if (!isListType || !listContainsArray) { + return NoChange; + } else { + function convertListNode(node) { + let listOperation = matchValue.literal(typeOf(node), { + anyOfValues: operations.anyOf, + allOfValues: operations.allOf, + }); + + let conditions = node.items.map((item) => { + if (valueListTypes.has(typeOf(item))) { + return convertListNode(item); + } else { + return internalOperations._condition({ + type: rootNode.conditionType, + expression: item + }); + } + }); + + return listOperation(conditions); + } + + return convertListNode(rootNode.expression); + } + } + } +}; diff --git a/src/validators/is-table-name.js b/src/validators/is-collection-name.js similarity index 100% rename from src/validators/is-table-name.js rename to src/validators/is-collection-name.js diff --git a/src/validators/is-foreign-column-string.js b/src/validators/is-foreign-column-string.js deleted file mode 100644 index f617f07..0000000 --- a/src/validators/is-foreign-column-string.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; - -const matchesFormat = require("@validatem/matches-format"); - -module.exports = [ - // FIXME: Validate format properly - matchesFormat(/^[^.]+\.[^.]+$/), - (string) => { - let [ tableName, columnName ] = string.split("."); - - return { - table: tableName, - column: columnName - }; - } -]; diff --git a/src/validators/is-literal-value.js b/src/validators/is-literal-value.js index c844412..60e5cc4 100644 --- a/src/validators/is-literal-value.js +++ b/src/validators/is-literal-value.js @@ -5,11 +5,12 @@ const isString = require("@validatem/is-string"); const isDate = require("@validatem/is-date"); const isBoolean = require("@validatem/is-boolean"); const isNumber = require("@validatem/is-number"); +const wrapError = require("@validatem/wrap-error"); // FIXME: Add null, but not undefined -module.exports = either([ +module.exports = wrapError("Must be a literal value", either([ isString, isNumber, // FIXME: Disallow Infinity? isBoolean, isDate -]); +])); diff --git a/src/validators/is-local-column-string.js b/src/validators/is-local-field-name.js similarity index 53% rename from src/validators/is-local-column-string.js rename to src/validators/is-local-field-name.js index f8af7fc..b49c37b 100644 --- a/src/validators/is-local-column-string.js +++ b/src/validators/is-local-field-name.js @@ -3,6 +3,7 @@ const matchesFormat = require("@validatem/matches-format"); const wrapError = require("@validatem/wrap-error"); -module.exports = wrapError("Must not include a table name", [ +module.exports = wrapError("Must not include a collection name", [ + // FIXME: Validate format properly (including disallowing uppercase!) matchesFormat(/^[^.]+$/) ]); diff --git a/src/validators/is-remote-field-name.js b/src/validators/is-remote-field-name.js new file mode 100644 index 0000000..834ea01 --- /dev/null +++ b/src/validators/is-remote-field-name.js @@ -0,0 +1,23 @@ +"use strict"; + +const matchesFormat = require("@validatem/matches-format"); + +const isCollectionName = require("./is-collection-name"); +const isLocalFieldName = require("./is-local-field-name"); + +module.exports = [ + // FIXME: Validate format properly + matchesFormat(/^[^.]+\.[^.]+$/), + (string) => { + let [ collectionName, fieldName ] = string.split("."); + + return { + collection: collectionName, + field: fieldName + }; + }, + { // FIXME: Make any validation errors at this point be represented as virtual properties, as it's a parse result + collection: [ isCollectionName ], + field: [ isLocalFieldName ] + } +]; diff --git a/src/validators/operations/is-column-object.js b/src/validators/operations/is-any-field-object.js similarity index 81% rename from src/validators/operations/is-column-object.js rename to src/validators/operations/is-any-field-object.js index b113d9d..423db01 100644 --- a/src/validators/operations/is-column-object.js +++ b/src/validators/operations/is-any-field-object.js @@ -7,8 +7,8 @@ module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); return either([ - [ isObjectType("columnName") ], - [ isObjectType("foreignColumnName") ], + [ isObjectType("field") ], + [ isObjectType("remoteField") ], ]); }; diff --git a/src/validators/operations/is-collapsible-column.js b/src/validators/operations/is-collapsible-field.js similarity index 54% rename from src/validators/operations/is-collapsible-column.js rename to src/validators/operations/is-collapsible-field.js index cfb3dab..3acacdc 100644 --- a/src/validators/operations/is-collapsible-column.js +++ b/src/validators/operations/is-collapsible-field.js @@ -2,12 +2,13 @@ const either = require("@validatem/either"); +// FIXME: Maybe this should actually be called isCollapsibleFieldSpecification? module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); - const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations); + const isPossiblyRemoteField = require("./is-possibly-remote-field")(operations); return either([ - isPossiblyForeignColumn, + isPossiblyRemoteField, isObjectType("hierarchical") ]); }; diff --git a/src/validators/operations/is-table.js b/src/validators/operations/is-collection.js similarity index 70% rename from src/validators/operations/is-table.js rename to src/validators/operations/is-collection.js index 8e63825..be91fec 100644 --- a/src/validators/operations/is-table.js +++ b/src/validators/operations/is-collection.js @@ -8,8 +8,8 @@ module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); const wrapWithOperation = require("./wrap-with-operation")(operations); - return wrapError("Must be a table name or object", either([ - [ isObjectType("tableName") ], - [ isString, wrapWithOperation("table") ] + return wrapError("Must be a collection name or object", either([ + [ isObjectType("collection") ], + [ isString, wrapWithOperation("collection") ] ])); }; diff --git a/src/validators/operations/is-computable.js b/src/validators/operations/is-computable.js index 17a2448..10b543a 100644 --- a/src/validators/operations/is-computable.js +++ b/src/validators/operations/is-computable.js @@ -4,13 +4,13 @@ const either = require("@validatem/either"); module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); - const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations); + const isPossiblyRemoteField = require("./is-possibly-remote-field")(operations); return either([ isObjectType("sqlExpression"), isObjectType("aggregrateFunction"), isObjectType("valueFrom"), isObjectType("literalValue"), - isPossiblyForeignColumn + isPossiblyRemoteField ]); }; diff --git a/src/validators/operations/is-condition-value.js b/src/validators/operations/is-condition-value.js new file mode 100644 index 0000000..111dd08 --- /dev/null +++ b/src/validators/operations/is-condition-value.js @@ -0,0 +1,20 @@ +"use strict"; + +const either = require("@validatem/either"); +const wrapError = require("@validatem/wrap-error"); + +const isLiteralValue = require("../is-literal-value"); + +// NOTE: This validator should typically come last in an `either`, since it will catch various types of inputs (sqlExpression, literal values, etc.) that might need to be interpreted differently in specific contexts. +// FIXME: Rename valueExpression -> value? + +module.exports = function (operations) { + const isValueExpression = require("./is-value-expression")(operations); + const isObjectType = require("./is-object-type")(operations); + + return either([ + isValueExpression, + [ isObjectType("anyOfValues") ], + [ isObjectType("allOfValues") ] + ]); +}; diff --git a/src/validators/operations/is-condition.js b/src/validators/operations/is-condition.js index 661309a..da0f7a8 100644 --- a/src/validators/operations/is-condition.js +++ b/src/validators/operations/is-condition.js @@ -5,7 +5,7 @@ const wrapError = require("@validatem/wrap-error"); module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); - const isValueExpression = require("./is-value-expression")(operations); + const isConditionValue = require("./is-condition-value")(operations); const wrapWithOperation = require("./wrap-with-operation")(operations); return wrapError("Must be a type of condition or value", either([ @@ -13,6 +13,6 @@ module.exports = function (operations) { [ isObjectType("notCondition") ], // not(condition) [ isObjectType("anyOfConditions") ], // anyOf(...) [ isObjectType("allOfConditions") ], // allOf(...) - [ isValueExpression, wrapWithOperation("equals") ] + [ isConditionValue, wrapWithOperation("equals") ] ])); }; diff --git a/src/validators/operations/is-field.js b/src/validators/operations/is-field.js new file mode 100644 index 0000000..a417663 --- /dev/null +++ b/src/validators/operations/is-field.js @@ -0,0 +1,15 @@ +"use strict"; + +const wrapError = require("@validatem/wrap-error"); +const either = require("@validatem/either"); +const isLocalFieldName = require("../is-local-field-name"); + +module.exports = function (operations) { + const wrapWithOperation = require("./wrap-with-operation")(operations); + const isObjectType = require("./is-object-type")(operations); + + return wrapError("Must be a local field name or object", either([ + [ isObjectType("field") ], + [ isLocalFieldName, wrapWithOperation("field") ] + ])); +}; diff --git a/src/validators/operations/is-local-value-expression.js b/src/validators/operations/is-local-value-expression.js new file mode 100644 index 0000000..26822cb --- /dev/null +++ b/src/validators/operations/is-local-value-expression.js @@ -0,0 +1,19 @@ +"use strict"; + +const either = require("@validatem/either"); + +const isLiteralValue = require("../is-literal-value"); + +// NOTE: This validator should typically come last in an `either`, since it will catch various types of inputs (sqlExpression, literal values, etc.) that might need to be interpreted differently in specific contexts. + +module.exports = function (operations) { + const isObjectType = require("./is-object-type")(operations); + const wrapWithOperation = require("./wrap-with-operation")(operations); + + return either([ + [ isObjectType("sqlExpression") ], + [ isObjectType("literalValue") ], + [ isObjectType("field") ], + [ isLiteralValue, wrapWithOperation("value") ] + ]); +}; diff --git a/src/validators/operations/is-possibly-foreign-column.js b/src/validators/operations/is-possibly-foreign-column.js deleted file mode 100644 index cff4f3b..0000000 --- a/src/validators/operations/is-possibly-foreign-column.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; - -const wrapError = require("@validatem/wrap-error"); -const either = require("@validatem/either"); -const isString = require("@validatem/is-string"); - -module.exports = function (operations) { - const isColumnObject = require("./is-column-object")(operations); - const wrapPossiblyForeignColumnName = require("./wrap-possibly-foreign-column-name")(operations); - - return wrapError("Must be a column name or object", either([ - [ isColumnObject ], - [ isString, wrapPossiblyForeignColumnName ] - ])); -}; diff --git a/src/validators/operations/is-possibly-remote-field.js b/src/validators/operations/is-possibly-remote-field.js new file mode 100644 index 0000000..f0d96fa --- /dev/null +++ b/src/validators/operations/is-possibly-remote-field.js @@ -0,0 +1,15 @@ +"use strict"; + +const wrapError = require("@validatem/wrap-error"); +const either = require("@validatem/either"); +const isString = require("@validatem/is-string"); + +module.exports = function (operations) { + const isAnyFieldObject = require("./is-any-field-object")(operations); + const wrapPossiblyRemoteFieldName = require("./wrap-possibly-remote-field-name")(operations); + + return wrapError("Must be a field name or object", either([ + [ isAnyFieldObject ], + [ isString, wrapPossiblyRemoteFieldName ] + ])); +}; diff --git a/src/validators/operations/is-predicate-list.js b/src/validators/operations/is-predicate-list.js index 1a33c1f..431ba17 100644 --- a/src/validators/operations/is-predicate-list.js +++ b/src/validators/operations/is-predicate-list.js @@ -1,6 +1,7 @@ "use strict"; const either = require("@validatem/either"); +const arrayOf = require("@validatem/array-of"); const tagAsType = require("../tag-as-type"); @@ -9,19 +10,22 @@ module.exports = function (operations) { const isExpressionList = require("./is-expression-list")(operations); const isConditionList = require("./is-condition-list")(operations); const isWhereObjectList = require("./is-where-object-list")(operations); + const isConditionValue = require("./is-condition-value")(operations); return either([ // Boolean AND/OR [ isExpressionList, tagAsType("expressions") ], // Combine (JOIN) - // FIXME + // FIXME? // ... + // Value-expression-only conditions (eg. `moreThan(anyOf(...))`) + [ either([ + arrayOf(isConditionValue), + isObjectType("placeholder"), // for dynamically specified array of values + isObjectType("sqlExpression"), // FIXME: Verify that this should be treated as a value, not an expression + ]), tagAsType("values") ], // Multiple-choice conditions [ isWhereObjectList, tagAsType("expressions") ], - [ either([ - [ isObjectType("sqlExpression") ], - [ isObjectType("placeholder") ], // for dynamically specified array of values - [ isConditionList ], - ]), tagAsType("conditions") ], + [ isConditionList, tagAsType("conditions") ], ]); }; diff --git a/src/validators/operations/is-relation-clause.js b/src/validators/operations/is-relation-clause.js new file mode 100644 index 0000000..d410a85 --- /dev/null +++ b/src/validators/operations/is-relation-clause.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = function (operations) { + const isSelectClause = require("./is-select-clause")(operations); + + // FIXME: Check whether certain select clauses are invalid in relations, and/or whether additional clauses need to be made valid + return isSelectClause; +}; diff --git a/src/validators/operations/is-remote-field.js b/src/validators/operations/is-remote-field.js new file mode 100644 index 0000000..feb327c --- /dev/null +++ b/src/validators/operations/is-remote-field.js @@ -0,0 +1,15 @@ +"use strict"; + +const wrapError = require("@validatem/wrap-error"); +const either = require("@validatem/either"); +const isRemoteFieldName = require("../is-remote-field-name"); + +module.exports = function (operations) { + const wrapWithOperation = require("./wrap-with-operation")(operations); + const isObjectType = require("./is-object-type")(operations); + + return wrapError("Must be a remote field name or object", either([ + [ isObjectType("remoteField") ], + [ isRemoteFieldName, wrapWithOperation("remoteField") ] + ])); +}; diff --git a/src/validators/operations/is-select-clause.js b/src/validators/operations/is-select-clause.js index 66a1c20..209fb7f 100644 --- a/src/validators/operations/is-select-clause.js +++ b/src/validators/operations/is-select-clause.js @@ -9,8 +9,8 @@ module.exports = function (operations) { return either([ [ isObjectType("where") ], [ isComputeClause ], - [ isObjectType("addColumns") ], - [ isObjectType("onlyColumns") ], + [ isObjectType("addFields") ], + [ isObjectType("onlyFields") ], [ isObjectType("withRelations") ], [ isObjectType("postProcess") ], [ isObjectType("collapseBy") ], diff --git a/src/validators/operations/is-selection-column.js b/src/validators/operations/is-selectable-field.js similarity index 50% rename from src/validators/operations/is-selection-column.js rename to src/validators/operations/is-selectable-field.js index ccba4dc..29affff 100644 --- a/src/validators/operations/is-selection-column.js +++ b/src/validators/operations/is-selectable-field.js @@ -4,10 +4,10 @@ const either = require("@validatem/either"); module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); - const isPossiblyForeignColumn = require("./is-possibly-foreign-column")(operations); + const isPossiblyRemoteField = require("./is-possibly-remote-field")(operations); return either([ isObjectType("alias"), - isPossiblyForeignColumn, // FIXME: Think about whether we actually want to permit foreign columns here, without an alias. + isPossiblyRemoteField, // FIXME: Think about whether we actually want to permit remote fields here, without an alias. ]); }; diff --git a/src/validators/operations/is-value-expression.js b/src/validators/operations/is-value-expression.js index d2faf77..9852839 100644 --- a/src/validators/operations/is-value-expression.js +++ b/src/validators/operations/is-value-expression.js @@ -10,7 +10,7 @@ const isLiteralValue = require("../is-literal-value"); module.exports = function (operations) { const isObjectType = require("./is-object-type")(operations); - const isColumnObject = require("./is-column-object")(operations); + const isAnyFieldObject = require("./is-any-field-object")(operations); const isAggregrateFunction = require("./is-aggregrate-function")(operations); const wrapWithOperation = require("./wrap-with-operation")(operations); @@ -18,8 +18,8 @@ module.exports = function (operations) { [ isObjectType("sqlExpression") ], [ isObjectType("literalValue") ], [ isObjectType("placeholder") ], // TODO: Verify that this also works for the `alias` method - [ isAggregrateFunction ], - [ isColumnObject ], + [ isAggregrateFunction ], // FIXME: Make sure to check that this is only permitted when collapsing + [ isAnyFieldObject ], [ isLiteralValue, wrapWithOperation("value") ] ])); }; diff --git a/src/validators/operations/is-where-object.js b/src/validators/operations/is-where-object.js index 77345b5..ea2e8da 100644 --- a/src/validators/operations/is-where-object.js +++ b/src/validators/operations/is-where-object.js @@ -10,12 +10,12 @@ const isNotAnASTNode = require("../is-not-an-ast-node"); module.exports = function (operations) { const isCondition = require("./is-condition")(operations); - const wrapPossiblyForeignColumnName = require("./wrap-possibly-foreign-column-name")(operations); + const wrapPossiblyRemoteFieldName = require("./wrap-possibly-remote-field-name")(operations); function wrapWhereObject(fields) { let expressions = Object.entries(fields).map(([ key, value ]) => { return operations.expression({ - left: wrapPossiblyForeignColumnName(key), + left: wrapPossiblyRemoteFieldName(key), condition: value }); }); @@ -24,7 +24,7 @@ module.exports = function (operations) { } let isConditionsMapping = anyProperty({ - // NOTE: We cannot wrap the key as a column name object here, since object keys can only be strings; we do so within wrapWhereObject instead + // NOTE: We cannot wrap the key as a field name object here, since object keys can only be strings; we do so within wrapWhereObject instead key: [ required, isString ], value: [ required, isCondition ] }); diff --git a/src/validators/operations/schema/is-composite-index-type.js b/src/validators/operations/schema/is-composite-index-type.js new file mode 100644 index 0000000..5d85aae --- /dev/null +++ b/src/validators/operations/schema/is-composite-index-type.js @@ -0,0 +1,17 @@ +"use strict"; + +const wrapError = require("@validatem/wrap-error"); +const ValidationError = require("@validatem/error"); + +module.exports = function (operations) { + const isIndexType = require("./is-index-type")(operations); + + return wrapError("Must be a composite index type", [ + isIndexType, + (node) => { + if (node.composite !== true) { + return new ValidationError(`Must be a composite index`); + } + } + ]); +}; diff --git a/src/validators/operations/schema/is-field-type.js b/src/validators/operations/schema/is-field-type.js new file mode 100644 index 0000000..8bfbf7b --- /dev/null +++ b/src/validators/operations/schema/is-field-type.js @@ -0,0 +1,16 @@ +"use strict"; + +const either = require("@validatem/either"); +const wrapError = require("@validatem/wrap-error"); + +module.exports = function (operations) { + const isObjectType = require("../is-object-type")(operations); + + return wrapError("Must be a field type", either([ + isObjectType("autoIDField"), + isObjectType("stringField"), + isObjectType("timestampField"), + isObjectType("booleanField"), + isObjectType("uuidField"), + ])); +}; diff --git a/src/validators/operations/schema/is-fields-object.js b/src/validators/operations/schema/is-fields-object.js new file mode 100644 index 0000000..93dca97 --- /dev/null +++ b/src/validators/operations/schema/is-fields-object.js @@ -0,0 +1,27 @@ +"use strict"; + +const either = require("@validatem/either"); +const wrapError = require("@validatem/wrap-error"); +const anyProperty = require("@validatem/any-property"); +const required = require("@validatem/required"); +const isLocalFieldName = require("../../is-local-field-name"); + +module.exports = function (operations) { + const isObjectType = require("../is-object-type")(operations); + const isFieldType = require("./is-field-type")(operations); + const isIndexType = require("./is-index-type")(operations); + + return wrapError("Must be an object of schema fields", anyProperty({ + key: [ required, isLocalFieldName ], + value: [ required, either([ + isFieldType, + isIndexType, + isObjectType("belongsTo"), // FIXME: Correct type? Need to distinguish between reference and definition usage (local vs. remote field name) + isObjectType("defaultTo"), + isObjectType("optional"), + // FIXME: Only allow deleteField/restoreAs for changeCollection, not createCollection + isObjectType("deleteField"), + isObjectType("restoreAs"), + ])] + }), { preserveOriginalErrors: true }); +}; diff --git a/src/validators/operations/schema/is-index-type.js b/src/validators/operations/schema/is-index-type.js new file mode 100644 index 0000000..921be58 --- /dev/null +++ b/src/validators/operations/schema/is-index-type.js @@ -0,0 +1,15 @@ +"use strict"; + +const either = require("@validatem/either"); +const wrapError = require("@validatem/wrap-error"); + +module.exports = function (operations) { + const isObjectType = require("../is-object-type")(operations); + + return wrapError("Must be an index type", either([ + isObjectType("index"), + isObjectType("indexWhere"), + isObjectType("unique"), + isObjectType("uniqueWhere"), + ])); +}; diff --git a/src/validators/operations/wrap-possibly-foreign-column-name.js b/src/validators/operations/wrap-possibly-foreign-column-name.js deleted file mode 100644 index 7c4f62d..0000000 --- a/src/validators/operations/wrap-possibly-foreign-column-name.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; - -module.exports = function (operations) { - return function wrapPossiblyForeignColumnName(value) { - if (value.includes(".")) { - return operations.foreignColumn(value); - } else { - return operations.column(value); - } - }; -}; diff --git a/src/validators/operations/wrap-possibly-remote-field-name.js b/src/validators/operations/wrap-possibly-remote-field-name.js new file mode 100644 index 0000000..dd47edb --- /dev/null +++ b/src/validators/operations/wrap-possibly-remote-field-name.js @@ -0,0 +1,11 @@ +"use strict"; + +module.exports = function (operations) { + return function wrapPossiblyRemoteFieldName(value) { + if (value.includes(".")) { + return operations.remoteField(value); + } else { + return operations.field(value); + } + }; +}; diff --git a/src/validators/operations/wrap-with-operation.js b/src/validators/operations/wrap-with-operation.js index b5484de..560b93c 100644 --- a/src/validators/operations/wrap-with-operation.js +++ b/src/validators/operations/wrap-with-operation.js @@ -7,5 +7,5 @@ module.exports = function (operations) { return operations[name](value); }; }; -} +}; diff --git a/yarn.lock b/yarn.lock index fdcf29b..ace0595 100644 --- a/yarn.lock +++ b/yarn.lock @@ -973,7 +973,7 @@ dependencies: is-callable "^1.1.5" -"@validatem/either@^0.1.3": +"@validatem/either@^0.1.3", "@validatem/either@^0.1.9": version "0.1.9" resolved "https://registry.yarnpkg.com/@validatem/either/-/either-0.1.9.tgz#0d753ef8fe04486d2b7122de3dd3ac51b3acaacf" integrity sha512-cUqlRjy02qDcZ166/D6duk8lrtqrHynHuSakU0TvMGMBiLzjWpMJ+3beAWHe+kILB5/dlXVyc68ZIjSNhBi8Kw== @@ -1042,6 +1042,14 @@ "@validatem/error" "^1.0.0" is-callable "^1.1.5" +"@validatem/is-integer@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/is-integer/-/is-integer-0.1.0.tgz#52c544063aaeabb630854e1822298f5c043196a0" + integrity sha512-sSp66uxfirIFMqro64DAdfM+UKo+IICmHdy/x3ZJXUM9F4byz/GyFmhR4wfcQswywwF1fqKw9458GE38fozjOQ== + dependencies: + "@validatem/error" "^1.0.0" + "@validatem/is-number" "^0.1.2" + "@validatem/is-number@^0.1.2": version "0.1.3" resolved "https://registry.yarnpkg.com/@validatem/is-number/-/is-number-0.1.3.tgz#0f8ce8c72970dbedbbd04d12942e5ab48a44cda6" @@ -1133,6 +1141,18 @@ dependencies: "@validatem/error" "^1.0.0" +"@validatem/require-either@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@validatem/require-either/-/require-either-0.1.0.tgz#250e35ab06f124ea90f3925d74b5f53a083923b0" + integrity sha512-UyZtJieT3aJhO9tj1OJp47V9jpHCE7RSohue9jg3FyDGwmIBVYXCfASeM19mWg9W0lp6IevsqTmaGQhqQOQYJg== + dependencies: + "@validatem/allow-extra-properties" "^0.1.0" + "@validatem/either" "^0.1.9" + "@validatem/forbidden" "^0.1.0" + "@validatem/required" "^0.1.1" + assure-array "^1.0.0" + flatten "^1.0.3" + "@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" @@ -1284,7 +1304,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -1381,6 +1401,13 @@ as-expression@^1.0.0: resolved "https://registry.yarnpkg.com/as-expression/-/as-expression-1.0.0.tgz#7bc620ca4cb2fe0ee90d86729bd6add33b8fd831" integrity sha512-Iqh4GxNUfxbJdGn6b7/XMzc8m1Dz2ZHouBQ9DDTzyMRO3VPPIAXeoY/sucRxxxXKbUtzwzWZSN6jPR3zfpYHHA== +as-table@^1.0.55: + version "1.0.55" + resolved "https://registry.yarnpkg.com/as-table/-/as-table-1.0.55.tgz#dc984da3937745de902cea1d45843c01bdbbec4f" + integrity sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ== + dependencies: + printable-characters "^1.0.42" + asn1.js@^4.0.0: version "4.10.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" @@ -1790,6 +1817,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1851,7 +1883,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.3.1: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== @@ -1978,6 +2010,15 @@ clipboard@^2.0.0: select "^1.1.2" tiny-emitter "^2.0.0" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + clone-regexp@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" @@ -2255,6 +2296,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -2927,6 +2973,14 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -3037,6 +3091,11 @@ get-assigned-identifiers@^1.1.0, get-assigned-identifiers@^1.2.0: resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-ports@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/get-ports/-/get-ports-1.0.3.tgz#f40bd580aca7ec0efb7b96cbfcbeb03ef894b5e8" @@ -3820,6 +3879,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash.memoize@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" @@ -4291,6 +4357,25 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + package-json@^6.3.0: version "6.5.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" @@ -4301,6 +4386,11 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + pad-left@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994" @@ -4371,6 +4461,11 @@ path-dirname@^1.0.0: resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -4422,6 +4517,58 @@ pem@^1.13.2: os-tmpdir "^1.0.1" which "^2.0.2" +pg-connection-string@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6" + integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0" + integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA== + +pg-protocol@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.2.5.tgz#28a1492cde11646ff2d2d06bdee42a3ba05f126c" + integrity sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.3.3: + version "8.3.3" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.3.3.tgz#0338631ca3c39b4fb425b699d494cab17f5bb7eb" + integrity sha512-wmUyoQM/Xzmo62wgOdQAn5tl7u+IA1ZYK7qbuppi+3E+Gj4hlUxVHjInulieWrd0SfHi/ADriTb5ILJ/lsJrSg== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.3.0" + pg-pool "^3.2.1" + pg-protocol "^1.2.5" + pg-types "^2.1.0" + pgpass "1.x" + semver "4.3.2" + +pgpass@1.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" + integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= + dependencies: + split "^1.0.0" + pick-random-weighted@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/pick-random-weighted/-/pick-random-weighted-1.2.3.tgz#3d337543ff59b53c7aad17aa97560981a4ac0311" @@ -4447,6 +4594,28 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -4471,6 +4640,11 @@ pretty-ms@^2.1.0: parse-ms "^1.0.0" plur "^1.0.0" +printable-characters@^1.0.42: + version "1.0.42" + resolved "https://registry.yarnpkg.com/printable-characters/-/printable-characters-1.0.42.tgz#3f18e977a9bd8eb37fcc4ff5659d7be90868b3d8" + integrity sha1-Pxjpd6m9jrN/zE/1ZZ176Qhos9g= + prismjs@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" @@ -4809,6 +4983,16 @@ repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4926,6 +5110,11 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" +semver@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" + integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= + semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -4975,6 +5164,11 @@ serve-static@1.14.1, serve-static@^1.10.0: parseurl "~1.3.3" send "0.17.1" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -5151,6 +5345,13 @@ split2@^0.2.1: dependencies: through2 "~0.6.1" +split@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -5229,7 +5430,7 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.0.0, string-width@^4.1.0: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -5417,7 +5618,7 @@ through2@~0.6.1: readable-stream ">=1.0.33-1 <1.1.0-0" xtend ">=4.0.0 <4.1.0-0" -"through@>=2.2.7 <3": +through@2, "through@>=2.2.7 <3": version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -5746,6 +5947,11 @@ watchify@^3.11.1: through2 "^2.0.0" xtend "^4.0.0" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -5772,6 +5978,15 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -5810,3 +6025,33 @@ xdg-basedir@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2"