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

"use strict";
Error.stackTraceLimit = 100;
const util = require("util");
const chalk = require("chalk");
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");
const optimizers = require("../src/optimizers");
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(() => {
// Edit me!
/* Available functions:
select, onlyFields, addFields, alias,
where, field, foreignColumn, collection,
value, parameter,
not, anyOf, allOf,
lessThan, moreThan, equals,
expression, unsafeSQL
*/
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)
})
]);
/*
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);
// 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 }));
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);
}
let stringifyResult = measureTime(() => astToQuery(optimizerResult.ast));
console.log(`\n${chalk.bold("Generation timings:")}`);
console.log(`${chalk.yellow("Building")}: ${chalk.cyan(toMS(buildingResult.time))}ms`);
console.log(`${chalk.yellow("Stringifying")}: ${chalk.cyan(toMS(stringifyResult.time))}ms`);
console.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 }));
} 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?
// 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);
/* 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));
});
}