You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

240 lines
7.2 KiB
JavaScript

4 years ago
"use strict";
Error.stackTraceLimit = 100;
4 years ago
const util = require("util");
const chalk = require("chalk");
4 years ago
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");
4 years ago
const astToQuery = require("../src/ast-to-query");
const optimizeAST = require("../src/ast/optimize");
const measureTime = require("../src/measure-time");
4 years ago
const optimizers = require("../src/optimizers");
4 years ago
function withOwner() {
return withRelations({ owner: "owner_id" });
}
// FIXME: Mark AST nodes with a special marker -- and disallow these from being interpreted as a WHERE object!
// FIXME: Figure out composability for something like connect-session-knex
////// VALIDATION TESTING START ///////
let query;
// Assume that the below numbers were generated by some external code somehow, and we don't know upfront which one is going to be lowest
let a = 523;
let b = 62;
let c = 452;
// let flooz = { a: anyOf };
try {
let buildingResult = measureTime(() => {
4 years ago
// Edit me!
/* Available functions:
4 years ago
select, onlyFields, addFields, alias,
where, field, foreignColumn, collection,
4 years ago
value, parameter,
not, anyOf, allOf,
lessThan, moreThan, equals,
expression, unsafeSQL
*/
4 years ago
let niceNumbers = anyOf([ 1, 2, 3 ]);
query = select("projects", [
onlyFields([ "foo" ]),
where({
number_one: niceNumbers,
number_two: niceNumbers
}),
where({
number_three: anyOf([ 42, field("number_one") ]),
number_four: moreThan(1337)
})
]);
4 years ago
/*
Generation timings:
Building: 7.79ms
Stringifying: 13.38ms
Optimization timings:
# Total: 6.07ms
# Walker overhead: 1.16ms
collapse-where: 0.50ms
conditions-to-expressions: 1.08ms
flatten-not-predicates: 0.00ms
flatten-predicate-lists: 0.21ms
arrayify-predicate-lists: 3.12ms
{
query: 'SELECT * FROM projects WHERE number_one = ANY(?) AND number_two = ANY(?) AND number_three = ANY(ARRAY[?, number_one]) AND number_four > ?;',
params: [ [ 1, 2, 3 ], [ 1, 2, 3 ], 42, 1337 ],
placeholders: []
}
*/
// 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);
4 years ago
// 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")
// })
// ])
// ]);
4 years ago
// 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: {
4 years ago
4 years ago
// }
// })
4 years ago
// 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
});
4 years ago
console.log(util.inspect(query, { depth: null, colors: true }));
console.log("");
let optimizerResult = optimizeAST(query, optimizers);
console.log(util.inspect(optimizerResult.ast, { depth: null, colors: true }));
function toMS(time) {
return (Number(time) / 1e6).toFixed(2);
}
4 years ago
let stringifyResult = measureTime(() => astToQuery(optimizerResult.ast));
4 years ago
console.log(`\n${chalk.bold("Generation timings:")}`);
console.log(`${chalk.yellow("Building")}: ${chalk.cyan(toMS(buildingResult.time))}ms`);
console.log(`${chalk.yellow("Stringifying")}: ${chalk.cyan(toMS(stringifyResult.time))}ms`);
4 years ago
console.log(`\n${chalk.bold("Optimization timings:")}`);
Object.entries(optimizerResult.timings).forEach(([ name, time ]) => {
console.log(`${chalk.yellow(name)}: ${chalk.cyan(toMS(time))}ms`);
});
console.log(util.inspect(stringifyResult.value, { depth: null, colors: true }));
4 years ago
} catch (error) {
// console.error(error.message);
// console.error("");
if (error.name === "AggregrateValidationError") {
console.error(error.stack);
console.error("# Inputs:", util.inspect(error.errors.map((error) => {
let stringifiedPath = error.path.join(" -> ");
return [ stringifiedPath + ":", error.value ];
}), { colors: true, depth: null }));
process.exit(1);
} else {
throw error;
}
}
////// VALIDATION TESTING END ///////
// TODO: Allow specifying placeholders that can be filled in later, for truly reusable query builders (so that eg. query planning only needs to be done once, this could even use prepared statements under the hood)
// let query = select("projects", [
// where(allOf([
// { score: not(lessThan(anyOf([ a, b, parameter("score") ]))) },
// anyOf([
// { active: true },
// { always_show: true }
// ])
// ]))
// ]);
// let query = select("projects", [
// where({
// active: true,
// // score: not(lessThan(anyOf([ a, b, c ])))
// // MARKER: Implement parameter tracking, so that the below can be made to work (needs a `placeholder` query gen handler that just returns parameter metadata or something, and this should be used elsewhere as well, and the anyOf/allOf code should be updated to also check for placeholders, not just SQL expressions)
// score: not(lessThan(anyOf([
// a,
// b,
// sql("44 + 33"),
// parameter("foo")
// ])))
// })
// ]);
// 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?
4 years ago
// 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
4 years ago
// FIXME: Aggregrates (GROUP BY)
// return db.execute(query);
/* Hypothetical `mapCase` implementation */
function mapObjectCase(object, from, to) {
return mapObject(object, (key, value) => {
return [
caseMapper(key, from, to),
value
];
});
}
function mapCase({ from, to }) {
return postProcess((results) => {
// NOTE: This could have returned a Promise!
return results.map((result) => mapObjectCase(result, from, to));
});
}