diff --git a/lib/ExprCompiler.d.ts b/lib/ExprCompiler.d.ts index c49b437..d5c4453 100644 --- a/lib/ExprCompiler.d.ts +++ b/lib/ExprCompiler.d.ts @@ -1,10 +1,65 @@ import { JsonQLExpr, JsonQLFrom } from "jsonql"; import Schema from "./Schema"; -import { Expr, Variable } from "./types"; - +import { BuildEnumsetExpr, CaseExpr, Column, Expr, FieldExpr, LegacyComparisonExpr, LegacyLogicalExpr, OpExpr, ScalarExpr, ScoreExpr, Variable, VariableExpr } from "./types"; +/** Compiles expressions to JsonQL. Assumes that geometry is in Webmercator (3857) */ export default class ExprCompiler { - constructor(schema: Schema, variables?: Variable[], variableValues?: { [variableId: string]: Expr }) - - compileExpr(options: { expr: Expr, tableAlias: string }): JsonQLExpr - compileTable(table: string, alias: string): JsonQLFrom + schema: Schema; + variables: Variable[]; + variableValues: { + [variableId: string]: Expr; + }; + constructor(schema: Schema, variables?: Variable[], variableValues?: { + [variableId: string]: Expr; + }); + /** Compile an expression. Pass expr and tableAlias. */ + compileExpr(options: { + expr: Expr; + tableAlias: string; + }): JsonQLExpr; + /** Compile a field expressions */ + compileFieldExpr(options: { + expr: FieldExpr; + tableAlias: string; + }): JsonQLExpr; + compileScalarExpr(options: { + expr: ScalarExpr; + tableAlias: string; + }): JsonQLExpr; + /** Compile a join into an on or where clause + * fromTableID: column definition + * joinColumn: column definition + * fromAlias: alias of from table + * toAlias: alias of to table + */ + compileJoin(fromTableId: string, joinColumn: Column, fromAlias: string, toAlias: string): any; + compileOpExpr(options: { + expr: OpExpr; + tableAlias: string; + }): JsonQLExpr; + compileCaseExpr(options: { + expr: CaseExpr; + tableAlias: string; + }): JsonQLExpr; + compileScoreExpr(options: { + expr: ScoreExpr; + tableAlias: string; + }): JsonQLExpr; + compileBuildEnumsetExpr(options: { + expr: BuildEnumsetExpr; + tableAlias: string; + }): JsonQLExpr; + compileComparisonExpr(options: { + expr: LegacyComparisonExpr; + tableAlias: string; + }): JsonQLExpr; + compileLogicalExpr(options: { + expr: LegacyLogicalExpr; + tableAlias: string; + }): JsonQLExpr; + compileColumnRef(column: any, tableAlias: string): JsonQLExpr; + compileTable(tableId: string, alias: string): JsonQLFrom; + compileVariableExpr(options: { + expr: VariableExpr; + tableAlias: string; + }): JsonQLExpr; } diff --git a/lib/ExprCompiler.js b/lib/ExprCompiler.js index 1d57e48..a489e80 100644 --- a/lib/ExprCompiler.js +++ b/lib/ExprCompiler.js @@ -1,3037 +1,2068 @@ "use strict"; - -var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); - -var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); - -var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); - -var ColumnNotFoundException, ExprCompiler, ExprUtils, _, convertToJsonB, getExprExtension, injectTableAlias, injectTableAliases, moment, nowExpr, nowMinus24HoursExpr; - -_ = require('lodash'); -injectTableAlias = require('./injectTableAliases').injectTableAlias; -injectTableAliases = require('./injectTableAliases').injectTableAliases; -ExprUtils = require('./ExprUtils')["default"]; -moment = require('moment'); -ColumnNotFoundException = require('./ColumnNotFoundException')["default"]; -getExprExtension = require('./extensions').getExprExtension; // now expression: (to_json(now() at time zone 'UTC')#>>'{}') - -nowExpr = { - type: "op", - op: "#>>", - exprs: [{ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var lodash_1 = __importDefault(require("lodash")); +var moment_1 = __importDefault(require("moment")); +var ColumnNotFoundException_1 = __importDefault(require("./ColumnNotFoundException")); +var ExprUtils_1 = __importDefault(require("./ExprUtils")); +var extensions_1 = require("./extensions"); +var injectTableAliases_1 = require("./injectTableAliases"); +// now expression: (to_json(now() at time zone 'UTC')#>>'{}') +var nowExpr = { type: "op", - op: "to_json", - exprs: [{ - type: "op", - op: "at time zone", - exprs: [{ - type: "op", - op: "now", - exprs: [] - }, "UTC"] - }] - }, "{}"] -}; // now 24 hours ago: (to_json((now() - interval '24 hour') at time zone 'UTC')#>>'{}') - -nowMinus24HoursExpr = { - type: "op", - op: "#>>", - exprs: [{ + op: "#>>", + exprs: [ + { type: "op", op: "to_json", exprs: [ + { type: "op", op: "at time zone", exprs: [ + { type: "op", op: "now", exprs: [] }, + "UTC" + ] } + ] }, + "{}" + ] +}; +// now 24 hours ago: (to_json((now() - interval '24 hour') at time zone 'UTC')#>>'{}') +var nowMinus24HoursExpr = { type: "op", - op: "to_json", - exprs: [{ - type: "op", - op: "at time zone", - exprs: [{ - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "now", - exprs: [] - }, { - type: "op", - op: "interval", - exprs: [{ - type: "literal", - value: "24 hour" - }] - }] - }, "UTC"] - }] - }, "{}"] -}; // Compiles expressions to JsonQL. Assumes that geometry is in Webmercator (3857) - -module.exports = ExprCompiler = /*#__PURE__*/function () { - // Variable values are lookup of id to variable value, which is always an expression - function ExprCompiler(schema) { - var variables = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; - var variableValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - (0, _classCallCheck2["default"])(this, ExprCompiler); - // Compile an expression. Pass expr and tableAlias. - this.compileExpr = this.compileExpr.bind(this); - this.schema = schema; - this.variables = variables; - this.variableValues = variableValues; - } - - (0, _createClass2["default"])(ExprCompiler, [{ - key: "compileExpr", - value: function compileExpr(options) { - var compiledExpr, expr; - expr = options.expr; // Handle null - - if (!expr) { - return null; - } - - switch (expr.type) { - case "id": - compiledExpr = this.compileColumnRef(this.schema.getTable(expr.table).primaryKey, options.tableAlias); - break; - - case "field": - compiledExpr = this.compileFieldExpr(options); - break; - - case "scalar": - compiledExpr = this.compileScalarExpr(options); - break; - - case "literal": - if (expr.value != null) { - compiledExpr = { - type: "literal", - value: expr.value - }; - } else { - compiledExpr = null; - } - - break; - - case "op": - compiledExpr = this.compileOpExpr(options); - break; - - case "case": - compiledExpr = this.compileCaseExpr(options); - break; - - case "score": - compiledExpr = this.compileScoreExpr(options); - break; - - case "build enumset": - compiledExpr = this.compileBuildEnumsetExpr(options); - break; - - case "variable": - compiledExpr = this.compileVariableExpr(options); - break; - - case "extension": - compiledExpr = getExprExtension(expr.extension).compileExpr(expr, options.tableAlias, this.schema, this.variables, this.variableValues); - break; - - case "count": - // DEPRECATED - compiledExpr = null; - break; - - case "comparison": - // DEPRECATED - compiledExpr = this.compileComparisonExpr(options); - break; - - case "logical": - // DEPRECATED - compiledExpr = this.compileLogicalExpr(options); - break; - - default: - throw new Error("Expr type ".concat(expr.type, " not supported")); - } - - return compiledExpr; + op: "#>>", + exprs: [ + { type: "op", op: "to_json", exprs: [ + { type: "op", op: "at time zone", exprs: [ + { type: "op", op: "-", exprs: [{ type: "op", op: "now", exprs: [] }, { type: "op", op: "interval", exprs: [{ type: "literal", value: "24 hour" }] }] }, + "UTC" + ] } + ] }, + "{}" + ] +}; +/** Compiles expressions to JsonQL. Assumes that geometry is in Webmercator (3857) */ +var ExprCompiler = /** @class */ (function () { + // Variable values are lookup of id to variable value, which is always an expression + function ExprCompiler(schema, variables, variableValues) { + this.schema = schema; + this.variables = variables || []; + this.variableValues = variableValues || {}; } - }, { - key: "compileFieldExpr", - value: function compileFieldExpr(options) { - var column, expr, ref; - expr = options.expr; - column = this.schema.getColumn(expr.table, expr.column); - - if (!column) { - throw new ColumnNotFoundException("Column ".concat(expr.table, ".").concat(expr.column, " not found")); - } // Handle joins specially - - - if (column.type === "join") { - // If id is result - if ((ref = column.join.type) === '1-1' || ref === 'n-1') { - // Use scalar to create - return this.compileScalarExpr({ - expr: { - type: "scalar", - table: expr.table, - joins: [column.id], - expr: { - type: "id", - table: column.join.toTable - } - }, - tableAlias: options.tableAlias - }); - } else { - return { - type: "scalar", - expr: { - type: "op", - op: "to_jsonb", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [this.compileColumnRef(this.schema.getTable(column.join.toTable).primaryKey, "inner")] - }] - }, - from: this.compileTable(column.join.toTable, "inner"), - where: this.compileJoin(expr.table, column, options.tableAlias, "inner"), - limit: 1 // Limit 1 to be safe - - }; + /** Compile an expression. Pass expr and tableAlias. */ + ExprCompiler.prototype.compileExpr = function (options) { + var expr = options.expr, tableAlias = options.tableAlias; + // Handle null + if (!expr) { + return null; } - } // Handle if has expr - - - if (column.expr) { - return this.compileExpr({ - expr: column.expr, - tableAlias: options.tableAlias - }); - } // If column has custom jsonql, use that instead of id - - - return this.compileColumnRef(column.jsonql || column.id, options.tableAlias); - } - }, { - key: "compileScalarExpr", - value: function compileScalarExpr(options) { - var alias, expr, extraWhere, from, fromColumn, generateAlias, i, j, joinColumn, nextAlias, onClause, orderBy, ordering, ref, scalar, scalarExpr, table, tableAlias, toTable, where; - expr = options.expr; - where = null; - from = null; - orderBy = null; // Null expr is null - - if (!expr.expr) { - return null; - } // Simplify if a join to an id field where the join uses the primary key of the to table - - - if (!expr.aggr && !expr.where && expr.joins.length === 1 && expr.expr.type === "id") { - fromColumn = this.schema.getColumn(expr.table, expr.joins[0]); - - if (fromColumn.type === "id") { - return this.compileColumnRef(fromColumn.id, options.tableAlias); + switch (expr.type) { + case "id": + return this.compileColumnRef(this.schema.getTable(expr.table).primaryKey, options.tableAlias); + case "field": + return this.compileFieldExpr({ expr: expr, tableAlias: tableAlias }); + case "scalar": + return this.compileScalarExpr({ expr: expr, tableAlias: tableAlias }); + case "literal": + if (expr.value != null) { + return { type: "literal", value: expr.value }; + } + else { + return null; + } + case "op": + return this.compileOpExpr({ expr: expr, tableAlias: tableAlias }); + case "case": + return this.compileCaseExpr({ expr: expr, tableAlias: tableAlias }); + case "score": + return this.compileScoreExpr({ expr: expr, tableAlias: tableAlias }); + case "build enumset": + return this.compileBuildEnumsetExpr({ expr: expr, tableAlias: tableAlias }); + case "variable": + return this.compileVariableExpr({ expr: expr, tableAlias: tableAlias }); + case "extension": + return extensions_1.getExprExtension(expr.extension).compileExpr(expr, tableAlias, this.schema, this.variables, this.variableValues); + case "count": // DEPRECATED + return null; + case "comparison": // DEPRECATED + return this.compileComparisonExpr({ expr: expr, tableAlias: tableAlias }); + case "logical": // DEPRECATED + return this.compileLogicalExpr({ expr: expr, tableAlias: tableAlias }); + default: + throw new Error("Expr type " + expr.type + " not supported"); + } + }; + /** Compile a field expressions */ + ExprCompiler.prototype.compileFieldExpr = function (options) { + var expr = options.expr; + var column = this.schema.getColumn(expr.table, expr.column); + if (!column) { + throw new ColumnNotFoundException_1.default("Column " + expr.table + "." + expr.column + " not found"); } - - if (fromColumn.join && fromColumn.join.toColumn === this.schema.getTable(expr.expr.table).primaryKey) { - return this.compileColumnRef(fromColumn.join.fromColumn, options.tableAlias); + // Handle joins specially + if (column.type === "join") { + // If id is result + if (['1-1', 'n-1'].includes(column.join.type)) { + // Use scalar to create + return this.compileScalarExpr({ expr: { type: "scalar", table: expr.table, joins: [column.id], expr: { type: "id", table: column.join.toTable } }, tableAlias: options.tableAlias }); + } + else { + return { + type: "scalar", + expr: { + type: "op", + op: "to_jsonb", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [ + this.compileColumnRef(this.schema.getTable(column.join.toTable).primaryKey, "inner") + ] + } + ] + }, + from: this.compileTable(column.join.toTable, "inner"), + where: this.compileJoin(expr.table, column, options.tableAlias, "inner"), + limit: 1 // Limit 1 to be safe + }; + } } - } // Generate a consistent, semi-unique alias - - - generateAlias = function generateAlias(expr, joinIndex) { - // Make alias-friendly (replace all symbols with _) - return expr.joins[joinIndex].replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); - }; // Perform joins - - - table = expr.table; - tableAlias = options.tableAlias; // First join is in where clause - - if (expr.joins && expr.joins.length > 0) { - joinColumn = this.schema.getColumn(expr.table, expr.joins[0]); - - if (!joinColumn) { - throw new ColumnNotFoundException("Join column ".concat(expr.table, ":").concat(expr.joins[0], " not found")); - } // Determine which column join is to - - - toTable = joinColumn.type === "join" ? joinColumn.join.toTable : joinColumn.idTable; // Generate a consistent, semi-unique alias - - alias = generateAlias(expr, 0); - where = this.compileJoin(table, joinColumn, tableAlias, alias); - from = this.compileTable(toTable, alias); // We are now at j1, which is the to of the first join - - table = toTable; - tableAlias = alias; - } // Perform remaining joins - - - if (expr.joins.length > 1) { - for (i = j = 1, ref = expr.joins.length; 1 <= ref ? j < ref : j > ref; i = 1 <= ref ? ++j : --j) { - joinColumn = this.schema.getColumn(table, expr.joins[i]); - - if (!joinColumn) { - throw new ColumnNotFoundException("Join column ".concat(table, ":").concat(expr.joins[i], " not found")); - } // Determine which column join is to - - - toTable = joinColumn.type === "join" ? joinColumn.join.toTable : joinColumn.idTable; // Generate a consistent, semi-unique alias - - nextAlias = generateAlias(expr, i); - onClause = this.compileJoin(table, joinColumn, tableAlias, nextAlias); - from = { - type: "join", - left: from, - right: this.compileTable(toTable, nextAlias), - kind: "inner", - on: onClause - }; // We are now at jn - - table = toTable; - tableAlias = nextAlias; + // Handle if has expr + if (column.expr) { + return this.compileExpr({ expr: column.expr, tableAlias: options.tableAlias }); } - } // Compile where clause - - - if (expr.where) { - extraWhere = this.compileExpr({ - expr: expr.where, - tableAlias: tableAlias - }); // Add to existing - - if (where) { - where = { - type: "op", - op: "and", - exprs: [where, extraWhere] - }; - } else { - where = extraWhere; + // If column has custom jsonql, use that instead of id + return this.compileColumnRef(column.jsonql || column.id, options.tableAlias); + }; + ExprCompiler.prototype.compileScalarExpr = function (options) { + var joinColumn, toTable; + var expr = options.expr; + var where = null; + var from = undefined; + var orderBy = undefined; + // Null expr is null + if (!expr.expr) { + return null; } - } - - scalarExpr = this.compileExpr({ - expr: expr.expr, - tableAlias: tableAlias - }); // Aggregate DEPRECATED - - if (expr.aggr) { - switch (expr.aggr) { - case "last": - // Get ordering - ordering = this.schema.getTable(table).ordering; - - if (!ordering) { - throw new Error("No ordering defined"); - } // order descending - - - orderBy = [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: table, - column: ordering - }, - tableAlias: tableAlias - }), - direction: "desc" - }]; - break; - - case "sum": - case "count": - case "avg": - case "max": - case "min": - case "stdev": - case "stdevp": - // Don't include scalarExpr if null - if (!scalarExpr) { - scalarExpr = { - type: "op", - op: expr.aggr, - exprs: [] - }; - } else { - scalarExpr = { - type: "op", - op: expr.aggr, - exprs: [scalarExpr] - }; + // Simplify if a join to an id field where the join uses the primary key of the to table + if (!expr.aggr && !expr.where && (expr.joins.length === 1) && (expr.expr.type === "id")) { + var fromColumn = this.schema.getColumn(expr.table, expr.joins[0]); + if (fromColumn.type === "id") { + return this.compileColumnRef(fromColumn.id, options.tableAlias); + } + if (fromColumn.join && (fromColumn.join.toColumn === this.schema.getTable(expr.expr.table).primaryKey)) { + return this.compileColumnRef(fromColumn.join.fromColumn, options.tableAlias); } - - break; - - default: - throw new Error("Unknown aggregation ".concat(expr.aggr)); - } - } // If no expr, return null - - - if (!scalarExpr) { - return null; - } // If no where, from, orderBy or limit, just return expr for simplicity - - - if (!from && !where && !orderBy) { - return scalarExpr; - } // Create scalar - - - scalar = { - type: "scalar", - expr: scalarExpr, - limit: 1 - }; - - if (from) { - scalar.from = from; - } - - if (where) { - scalar.where = where; - } - - if (orderBy) { - scalar.orderBy = orderBy; - } - - return scalar; - } // Compile a join into an on or where clause - // fromTableID: column definition - // joinColumn: column definition - // fromAlias: alias of from table - // toAlias: alias of to table - - }, { - key: "compileJoin", - value: function compileJoin(fromTableId, joinColumn, fromAlias, toAlias) { - var toTable; // For join columns - - if (joinColumn.type === "join") { - if (joinColumn.join.jsonql) { - return injectTableAliases(joinColumn.join.jsonql, { - "{from}": fromAlias, - "{to}": toAlias - }); - } else { - return { - // Use manual columns - type: "op", - op: "=", - exprs: [this.compileColumnRef(joinColumn.join.toColumn, toAlias), this.compileColumnRef(joinColumn.join.fromColumn, fromAlias)] - }; } - } else if (joinColumn.type === "id") { - // Get to table - toTable = this.schema.getTable(joinColumn.idTable); - return { - // Create equal - type: "op", - op: "=", - exprs: [this.compileFieldExpr({ - expr: { - type: "field", - table: fromTableId, - column: joinColumn.id - }, - tableAlias: fromAlias - }), { - type: "field", - tableAlias: toAlias, - column: toTable.primaryKey - }] + // Generate a consistent, semi-unique alias. Make alias-friendly (replace all symbols with _) + var generateAlias = function (expr, joinIndex) { + return expr.joins[joinIndex].replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); }; - } else if (joinColumn.type === "id[]") { - // Get to table - toTable = this.schema.getTable(joinColumn.idTable); - return { - // Create equal - type: "op", - op: "=", - modifier: "any", - exprs: [{ - type: "field", - tableAlias: toAlias, - column: toTable.primaryKey - }, { - type: "scalar", - expr: { - type: "op", - op: "unnest", - exprs: [this.compileFieldExpr({ - expr: { - type: "field", - table: fromTableId, - column: joinColumn.id - }, - tableAlias: fromAlias - })] + // Perform joins + var table = expr.table; + var tableAlias = options.tableAlias; + // First join is in where clause + if (expr.joins && (expr.joins.length > 0)) { + joinColumn = this.schema.getColumn(expr.table, expr.joins[0]); + if (!joinColumn) { + throw new ColumnNotFoundException_1.default("Join column " + expr.table + ":" + expr.joins[0] + " not found"); } - }] - }; - } else { - throw new Error("Invalid join column type ".concat(joinColumn.type)); - } - } // Compile an expression. Pass expr and tableAlias. - - }, { - key: "compileOpExpr", - value: function compileOpExpr(options) { - var _this = this; - - var compiledExprs, enumValues, expr, expr0Type, exprUtils, filterCompiled, idTable, innerrnQuery, lhsCompiled, orderBy, ordering, outerrnQuery, ref, ref1, ref2, ref3, ref4; - exprUtils = new ExprUtils(this.schema); - expr = options.expr; - compiledExprs = _.map(expr.exprs, function (e) { - return _this.compileExpr({ - expr: e, - tableAlias: options.tableAlias - }); - }); // Get type of expr 0 - - expr0Type = exprUtils.getExprType(expr.exprs[0]); // Handle multi - - switch (expr.op) { - case "and": - case "or": - // Strip nulls - compiledExprs = _.compact(compiledExprs); - - if (compiledExprs.length === 0) { - return null; - } - - return { - type: "op", - op: expr.op, - exprs: compiledExprs - }; - - case "*": - // Strip nulls - compiledExprs = _.compact(compiledExprs); - - if (compiledExprs.length === 0) { - return null; - } - - return { - // Cast to decimal before multiplying to prevent integer overflow - type: "op", - op: expr.op, - exprs: _.map(compiledExprs, function (e) { - return { - type: "op", - op: "::decimal", - exprs: [e] - }; - }) - }; - - case "+": - // Strip nulls - compiledExprs = _.compact(compiledExprs); - - if (compiledExprs.length === 0) { - return null; - } - - return { - // Cast to decimal before adding to prevent integer overflow. Do cast on internal expr to prevent coalesce mismatch - type: "op", - op: expr.op, - exprs: _.map(compiledExprs, function (e) { - return { - type: "op", - op: "coalesce", - exprs: [{ - type: "op", - op: "::decimal", - exprs: [e] - }, 0] - }; - }) - }; - - case "-": - // Null if any not present - if (_.any(compiledExprs, function (ce) { - return ce == null; - })) { - return null; - } - - return { - // Cast to decimal before subtracting to prevent integer overflow - type: "op", - op: expr.op, - exprs: _.map(compiledExprs, function (e) { - return { - type: "op", - op: "::decimal", - exprs: [e] - }; - }) - }; - - case ">": - case "<": - case ">=": - case "<=": - case "<>": - case "=": - case "~*": - case "round": - case "floor": - case "ceiling": - case "sum": - case "avg": - case "min": - case "max": - case "count": - case "stdev": - case "stdevp": - case "var": - case "varp": - case "array_agg": - // Null if any not present - if (_.any(compiledExprs, function (ce) { - return ce == null; - })) { - return null; - } - - return { - type: "op", - op: expr.op, - exprs: compiledExprs - }; - - case "least": - case "greatest": - return { - type: "op", - op: expr.op, - exprs: compiledExprs - }; - - case "/": - // Null if any not present - if (_.any(compiledExprs, function (ce) { - return ce == null; - })) { - return null; - } - - return { - // Cast to decimal before dividing to prevent integer math - type: "op", - op: expr.op, - exprs: [compiledExprs[0], { - type: "op", - op: "::decimal", - exprs: [{ - type: "op", - op: "nullif", - exprs: [compiledExprs[1], 0] - }] - }] - }; - - case "last": - // Null if not present - if (!compiledExprs[0]) { - return null; - } // Get ordering - - - ordering = (ref = this.schema.getTable(expr.table)) != null ? ref.ordering : void 0; - - if (!ordering) { - throw new Error("Table ".concat(expr.table, " must be ordered to use last()")); - } - - return { - // (array_agg(xyz order by theordering desc nulls last))[1] - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [compiledExprs[0]], - orderBy: [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "desc", - nulls: "last" - }] - }, 1] - }; - - case "last where": - // Null if not value present - if (!compiledExprs[0]) { - return null; - } // Get ordering - - - ordering = (ref1 = this.schema.getTable(expr.table)) != null ? ref1.ordering : void 0; - - if (!ordering) { - throw new Error("Table ".concat(expr.table, " must be ordered to use last()")); - } // Simple last if not condition present - - - if (!compiledExprs[1]) { - return { - // (array_agg(xyz order by theordering desc nulls last))[1] - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [compiledExprs[0]], - orderBy: [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "desc", - nulls: "last" - }] - }, 1] - }; - } - - return { - // Compiles to: - // (array_agg((case when then else null end) order by (case when then 0 else 1 end), desc nulls last))[1] - // which prevents non-matching from appearing - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1], - then: compiledExprs[0] - }], - "else": null - }], - orderBy: [{ - expr: { - type: "case", - cases: [{ - when: compiledExprs[1], - then: 0 - }], - "else": 1 - } - }, { - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "desc", - nulls: "last" - }] - }, 1] - }; - - case "previous": - // Null if not present - if (!compiledExprs[0]) { - return null; - } // Get ordering - - - ordering = (ref2 = this.schema.getTable(expr.table)) != null ? ref2.ordering : void 0; - - if (!ordering) { - throw new Error("Table ".concat(expr.table, " must be ordered to use previous()")); - } - - return { - // (array_agg(xyz order by theordering desc nulls last))[2] - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [compiledExprs[0]], - orderBy: [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "desc", - nulls: "last" - }] - }, 2] - }; - - case "first": - // Null if not present - if (!compiledExprs[0]) { - return null; - } // Get ordering - - - ordering = (ref3 = this.schema.getTable(expr.table)) != null ? ref3.ordering : void 0; - - if (!ordering) { - throw new Error("Table ".concat(expr.table, " must be ordered to use first()")); - } - - return { - // (array_agg(xyz order by theordering asc nulls last))[1] - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [compiledExprs[0]], - orderBy: [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "asc", - nulls: "last" - }] - }, 1] - }; - - case "first where": - // Null if not value present - if (!compiledExprs[0]) { - return null; - } // Get ordering - - - ordering = (ref4 = this.schema.getTable(expr.table)) != null ? ref4.ordering : void 0; - - if (!ordering) { - throw new Error("Table ".concat(expr.table, " must be ordered to use first where()")); - } // Simple first if not condition present - - - if (!compiledExprs[1]) { - return { - // (array_agg(xyz order by theordering asc nulls last))[1] - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [compiledExprs[0]], - orderBy: [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "asc", - nulls: "last" - }] - }, 1] - }; - } - - return { - // Compiles to: - // (array_agg((case when then else null end) order by (case when then 0 else 1 end), asc nulls last))[1] - // which prevents non-matching from appearing - type: "op", - op: "[]", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1], - then: compiledExprs[0] - }], - "else": null - }], - orderBy: [{ - expr: { - type: "case", - cases: [{ - when: compiledExprs[1], - then: 0 - }], - "else": 1 - } - }, { - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: options.tableAlias - }), - direction: "asc", - nulls: "last" - }] - }, 1] - }; - - case '= any': - // Null if any not present - if (_.any(compiledExprs, function (ce) { - return ce == null; - })) { - return null; - } // False if empty list on rhs - - - if (expr.exprs[1].type === "literal") { - if (!expr.exprs[1].value || _.isArray(expr.exprs[1].value) && expr.exprs[1].value.length === 0) { - return false; + // Determine which column join is to + toTable = joinColumn.type === "join" ? joinColumn.join.toTable : joinColumn.idTable; + // Generate a consistent, semi-unique alias + var alias = generateAlias(expr, 0); + where = this.compileJoin(table, joinColumn, tableAlias, alias); + from = this.compileTable(toTable, alias); + // We are now at j1, which is the to of the first join + table = toTable; + tableAlias = alias; + } + // Perform remaining joins + if (expr.joins.length > 1) { + for (var i = 1, end = expr.joins.length, asc = 1 <= end; asc ? i < end : i > end; asc ? i++ : i--) { + joinColumn = this.schema.getColumn(table, expr.joins[i]); + if (!joinColumn) { + throw new ColumnNotFoundException_1.default("Join column " + table + ":" + expr.joins[i] + " not found"); + } + // Determine which column join is to + toTable = joinColumn.type === "join" ? joinColumn.join.toTable : joinColumn.idTable; + // Generate a consistent, semi-unique alias + var nextAlias = generateAlias(expr, i); + var onClause = this.compileJoin(table, joinColumn, tableAlias, nextAlias); + from = { + type: "join", + left: from, + right: this.compileTable(toTable, nextAlias), + kind: "inner", + on: onClause + }; + // We are now at jn + table = toTable; + tableAlias = nextAlias; } - } - - return { - type: "op", - op: "=", - modifier: "any", - exprs: compiledExprs - }; - - case "between": - // Null if first not present - if (!compiledExprs[0]) { - return null; - } // Null if second and third not present - - - if (!compiledExprs[1] && !compiledExprs[2]) { - return null; - } // >= if third missing - - - if (!compiledExprs[2]) { - return { - type: "op", - op: ">=", - exprs: [compiledExprs[0], compiledExprs[1]] - }; - } // <= if second missing - - - if (!compiledExprs[1]) { - return { - type: "op", - op: "<=", - exprs: [compiledExprs[0], compiledExprs[2]] - }; - } - - return { - // Between - type: "op", - op: "between", - exprs: compiledExprs - }; - - case "not": - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: expr.op, - exprs: [{ - type: "op", - op: "coalesce", - exprs: [compiledExprs[0], false] - }] - }; - - case "is null": - case "is not null": - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: expr.op, - exprs: compiledExprs - }; - - case "contains": - // Null if either not present - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } // Null if no expressions in literal list - - - if (compiledExprs[1].type === "literal" && compiledExprs[1].value.length === 0) { - return null; - } - - return { - // Cast both to jsonb and use @>. Also convert both to json first to handle literal arrays - type: "op", - op: "@>", - exprs: [convertToJsonB(compiledExprs[0]), convertToJsonB(compiledExprs[1])] - }; - - case "intersects": - // Null if either not present - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } // Null if no expressions in literal list - - - if (compiledExprs[1].type === "literal" && compiledExprs[1].value.length === 0) { - return null; - } - - return { - // Cast to jsonb and use ?| Also convert to json first to handle literal arrays - type: "op", - op: "?|", - exprs: [convertToJsonB(compiledExprs[0]), compiledExprs[1]] - }; - - case "length": - // 0 if null - if (compiledExprs[0] == null) { - return 0; - } - - return { - // Cast both to jsonb and use jsonb_array_length. Also convert both to json first to handle literal arrays. Coalesce to 0 so that null is 0 - type: "op", - op: "coalesce", - exprs: [{ - type: "op", - op: "jsonb_array_length", - exprs: [convertToJsonB(compiledExprs[0])] - }, 0] - }; - - case "line length": - // null if null - if (compiledExprs[0] == null) { - return null; - } - - return { - // ST_Length_Spheroid(ST_Transform(location,4326::integer), 'SPHEROID["GRS_1980",6378137,298.257222101]'::spheroid) - type: "op", - op: "ST_LengthSpheroid", - exprs: [{ - type: "op", - op: "ST_Transform", - exprs: [compiledExprs[0], { - type: "op", - op: "::integer", - exprs: [4326] - }] - }, { - type: "op", - op: "::spheroid", - exprs: ['SPHEROID["GRS_1980",6378137,298.257222101]'] - }] - }; - - case "to text": - // Null if not present - if (!compiledExprs[0]) { + } + // Compile where clause + if (expr.where) { + var extraWhere = this.compileExpr({ expr: expr.where, tableAlias: tableAlias }); + // Add to existing + if (where) { + where = { type: "op", op: "and", exprs: [where, extraWhere] }; + } + else { + where = extraWhere; + } + } + var scalarExpr = this.compileExpr({ expr: expr.expr, tableAlias: tableAlias }); + // Aggregate DEPRECATED + if (expr.aggr) { + switch (expr.aggr) { + case "last": + // Get ordering + var ordering = this.schema.getTable(table).ordering; + if (!ordering) { + throw new Error("No ordering defined"); + } + // order descending + orderBy = [{ expr: this.compileFieldExpr({ expr: { type: "field", table: table, column: ordering }, tableAlias: tableAlias }), direction: "desc" }]; + break; + case "sum": + case "count": + case "avg": + case "max": + case "min": + case "stdev": + case "stdevp": + // Don't include scalarExpr if null + if (!scalarExpr) { + scalarExpr = { type: "op", op: expr.aggr, exprs: [] }; + } + else { + scalarExpr = { type: "op", op: expr.aggr, exprs: [scalarExpr] }; + } + break; + default: + throw new Error("Unknown aggregation " + expr.aggr); + } + } + // If no expr, return null + if (!scalarExpr) { + // TODO extend to include null! return null; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "enum") { - // Null if no enum values - enumValues = exprUtils.getExprEnumValues(expr.exprs[0]); - - if (!enumValues) { - return null; + } + // If no where, from, orderBy or limit, just return expr for simplicity + if (!from && !where && !orderBy) { + return scalarExpr; + } + // Create scalar + var scalar = { + type: "scalar", + expr: scalarExpr, + limit: 1 + }; + if (from) { + scalar.from = from; + } + if (where) { + scalar.where = where; + } + if (orderBy) { + scalar.orderBy = orderBy; + } + return scalar; + }; + /** Compile a join into an on or where clause + * fromTableID: column definition + * joinColumn: column definition + * fromAlias: alias of from table + * toAlias: alias of to table + */ + ExprCompiler.prototype.compileJoin = function (fromTableId, joinColumn, fromAlias, toAlias) { + // For join columns + var toTable; + if (joinColumn.type === "join") { + if (joinColumn.join.jsonql) { + return injectTableAliases_1.injectTableAliases(joinColumn.join.jsonql, { "{from}": fromAlias, "{to}": toAlias }); } - - return { - type: "case", - input: compiledExprs[0], - cases: _.map(enumValues, function (ev) { + else { + // Use manual columns return { - when: { - type: "literal", - value: ev.id - }, - then: { - type: "literal", - value: exprUtils.localizeString(ev.name, expr.locale) - } + type: "op", op: "=", + exprs: [ + this.compileColumnRef(joinColumn.join.toColumn, toAlias), + this.compileColumnRef(joinColumn.join.fromColumn, fromAlias) + ] }; - }) - }; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "number") { - return { - type: "op", - op: "::text", - exprs: [compiledExprs[0]] - }; - } - - return null; - - case "to date": - // Null if not present - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "substr", - exprs: [compiledExprs[0], 1, 10] - }; - - case "count where": - // Null if not present - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "coalesce", - exprs: [{ - type: "op", - op: "sum", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[0], - then: 1 - }], - "else": 0 - }] - }, 0] - }; - - case "percent where": - // Null if not present - if (!compiledExprs[0]) { - return null; - } - - return { - // Compiles as sum(case when cond [and basis (if present)] then 100::decimal else 0 end)/sum(1 [or case when basis then 1 else 0 (if present)]) (prevent div by zero) - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "sum", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1] ? { - type: "op", - op: "and", - exprs: [compiledExprs[0], compiledExprs[1]] - } : compiledExprs[0], - then: { - type: "op", - op: "::decimal", - exprs: [100] - } - }], - "else": 0 - }] - }, compiledExprs[1] ? { - type: "op", - op: "nullif", - exprs: [{ - type: "op", - op: "sum", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1], - then: 1 - }], - "else": 0 - }] - }, 0] - } : { - type: "op", - op: "sum", - exprs: [1] - }] - }; - - case "sum where": - // Null if not present - if (!compiledExprs[0]) { - return null; - } // Simple sum if not specified where - - - if (!compiledExprs[1]) { - return { - type: "op", - op: "sum", - exprs: [compiledExprs[0]] - }; - } - - return { - type: "op", - op: "sum", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1], - then: compiledExprs[0] - }], - "else": 0 - }] - }; - - case "min where": - // Null if not present - if (!compiledExprs[0]) { - return null; - } // Simple min if not specified where - - - if (!compiledExprs[1]) { + } + } + else if (joinColumn.type === "id") { + // Get to table + toTable = this.schema.getTable(joinColumn.idTable); + // Create equal return { - type: "op", - op: "min", - exprs: [compiledExprs[0]] + type: "op", op: "=", + exprs: [ + this.compileFieldExpr({ expr: { type: "field", table: fromTableId, column: joinColumn.id }, tableAlias: fromAlias }), + { type: "field", tableAlias: toAlias, column: toTable.primaryKey } + ] }; - } - - return { - type: "op", - op: "min", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1], - then: compiledExprs[0] - }], - "else": null - }] - }; - - case "max where": - // Null if not present - if (!compiledExprs[0]) { - return null; - } // Simple max if not specified where - - - if (!compiledExprs[1]) { + } + else if (joinColumn.type === "id[]") { + // Get to table + toTable = this.schema.getTable(joinColumn.idTable); + // Create equal return { - type: "op", - op: "max", - exprs: [compiledExprs[0]] + type: "op", op: "=", modifier: "any", + exprs: [ + { type: "field", tableAlias: toAlias, column: toTable.primaryKey }, + { + type: "scalar", + expr: { type: "op", op: "unnest", exprs: [ + this.compileFieldExpr({ expr: { type: "field", table: fromTableId, column: joinColumn.id }, tableAlias: fromAlias }) + ] } + } + ] }; - } - - return { - type: "op", - op: "max", - exprs: [{ - type: "case", - cases: [{ - when: compiledExprs[1], - then: compiledExprs[0] - }], - "else": null - }] - }; - - case "count distinct": - // Null if not present - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "count", - exprs: [compiledExprs[0]], - modifier: "distinct" - }; - - case "percent": - return { - // Compiles as count(*) * 100::decimal / sum(count(*)) over() - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "*", - exprs: [{ - type: "op", - op: "count", - exprs: [] - }, { - type: "op", - op: "::decimal", - exprs: [100] - }] - }, { - type: "op", - op: "sum", - exprs: [{ - type: "op", - op: "count", - exprs: [] - }], - over: {} - }] - }; - // Hierarchical test that uses ancestry column - - case "within": - // Null if either not present - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } // Get table being used - - - idTable = exprUtils.getExprIdTable(expr.exprs[0]); // Prefer ancestryTable - - if (this.schema.getTable(idTable).ancestryTable) { - return { - // exists (select null from as subwithin where ancestor = compiledExprs[1] and descendant = compiledExprs[0]) - type: "op", - op: "exists", - exprs: [{ - type: "scalar", - expr: null, - from: { - type: "table", - table: this.schema.getTable(idTable).ancestryTable, - alias: "subwithin" - }, - where: { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: "=", - exprs: [{ - type: "field", - tableAlias: "subwithin", - column: "ancestor" - }, compiledExprs[1]] - }, { + } + else { + throw new Error("Invalid join column type " + joinColumn.type); + } + }; + // Compile an expression. Pass expr and tableAlias. + ExprCompiler.prototype.compileOpExpr = function (options) { + var _this = this; + var ordering; + var exprUtils = new ExprUtils_1.default(this.schema); + var expr = options.expr; + var compiledExprs = lodash_1.default.map(expr.exprs, function (e) { return _this.compileExpr({ expr: e, tableAlias: options.tableAlias }); }); + // Get type of expr 0 + var expr0Type = exprUtils.getExprType(expr.exprs[0]); + // Handle multi + switch (expr.op) { + case "and": + case "or": + // Strip nulls + compiledExprs = lodash_1.default.compact(compiledExprs); + if (compiledExprs.length === 0) { + return null; + } + return { type: "op", - op: "=", - exprs: [{ - type: "field", - tableAlias: "subwithin", - column: "descendant" - }, compiledExprs[0]] - }] - } - }] - }; - } - - return { - type: "op", - op: "in", - exprs: [compiledExprs[0], { - type: "scalar", - expr: this.compileColumnRef(this.schema.getTable(idTable).primaryKey, "subwithin"), - from: { - type: "table", - table: idTable, - alias: "subwithin" - }, - where: { - type: "op", - op: "@>", - exprs: [{ - type: "field", - tableAlias: "subwithin", - column: this.schema.getTable(idTable).ancestry - }, { - type: "op", - op: "::jsonb", - exprs: [{ + op: expr.op, + exprs: compiledExprs + }; + case "*": + // Strip nulls + compiledExprs = lodash_1.default.compact(compiledExprs); + if (compiledExprs.length === 0) { + return null; + } + // Cast to decimal before multiplying to prevent integer overflow + return { type: "op", - op: "json_build_array", - exprs: [compiledExprs[1]] - }] - }] - } - }] - }; - // Hierarchical test that uses ancestry column - - case "within any": - // Null if either not present - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } // Get table being used - - - idTable = exprUtils.getExprIdTable(expr.exprs[0]); // Prefer ancestryTable - - if (this.schema.getTable(idTable).ancestryTable) { - return { - // exists (select null from as subwithin where ancestor = any(compiledExprs[1]) and descendant = compiledExprs[0]) - type: "op", - op: "exists", - exprs: [{ - type: "scalar", - expr: null, - from: { - type: "table", - table: this.schema.getTable(idTable).ancestryTable, - alias: "subwithin" - }, - where: { - type: "op", - op: "and", - exprs: [{ + op: expr.op, + exprs: lodash_1.default.map(compiledExprs, function (e) { return ({ + type: "op", + op: "::decimal", + exprs: [e] + }); }) + }; + case "+": + // Strip nulls + compiledExprs = lodash_1.default.compact(compiledExprs); + if (compiledExprs.length === 0) { + return null; + } + // Cast to decimal before adding to prevent integer overflow. Do cast on internal expr to prevent coalesce mismatch + return { type: "op", - op: "=", - modifier: "any", - exprs: [{ - type: "field", - tableAlias: "subwithin", - column: "ancestor" - }, compiledExprs[1]] - }, { + op: expr.op, + exprs: lodash_1.default.map(compiledExprs, function (e) { return ({ + type: "op", + op: "coalesce", + exprs: [{ type: "op", op: "::decimal", exprs: [e] }, 0] + }); }) + }; + case "-": + // Null if any not present + if (lodash_1.default.any(compiledExprs, function (ce) { return ce == null; })) { + return null; + } + // Cast to decimal before subtracting to prevent integer overflow + return { type: "op", - op: "=", - exprs: [{ - type: "field", - tableAlias: "subwithin", - column: "descendant" - }, compiledExprs[0]] - }] - } - }] - }; - } // This older code fails now that admin_regions uses integer pk. Replaced with literal-only code - // return { - // type: "op" - // op: "in" - // exprs: [ - // compiledExprs[0] - // { - // type: "scalar" - // expr: @compileColumnRef(@schema.getTable(idTable).primaryKey, "subwithin") - // from: { type: "table", table: idTable, alias: "subwithin" } - // where: { - // type: "op" - // op: "?|" - // exprs: [ - // { type: "field", tableAlias: "subwithin", column: @schema.getTable(idTable).ancestry } - // compiledExprs[1] - // ] - // } - // } - // ] - // } - // If not literal, fail - - - if (compiledExprs[1].type !== "literal") { - throw new Error("Non-literal RHS of within any not supported"); - } - - return { - type: "op", - op: "in", - exprs: [compiledExprs[0], { - type: "scalar", - expr: this.compileColumnRef(this.schema.getTable(idTable).primaryKey, "subwithin"), - from: { - type: "table", - table: idTable, - alias: "subwithin" - }, - where: { - type: "op", - op: "?|", - exprs: [{ - type: "field", - tableAlias: "subwithin", - column: this.schema.getTable(idTable).ancestryText || this.schema.getTable(idTable).ancestry - }, { - type: "literal", - value: _.map(compiledExprs[1].value, function (value) { - if (_.isNumber(value)) { - return "" + value; - } else { - return value; - } - }) - }] - } - }] - }; - - case "latitude": - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "ST_Y", - exprs: [{ - type: "op", - op: "ST_Centroid", - exprs: [{ - type: "op", - op: "ST_Transform", - exprs: [compiledExprs[0], { - type: "op", - op: "::integer", - exprs: [4326] - }] - }] - }] - }; - - case "longitude": - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "ST_X", - exprs: [{ - type: "op", - op: "ST_Centroid", - exprs: [{ - type: "op", - op: "ST_Transform", - exprs: [compiledExprs[0], { - type: "op", - op: "::integer", - exprs: [4326] - }] - }] - }] - }; - - case 'days difference': - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "datetime" || exprUtils.getExprType(expr.exprs[1]) === "datetime") { - return { - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "date_part", - exprs: ['epoch', { + op: expr.op, + exprs: lodash_1.default.map(compiledExprs, function (e) { return ({ + type: "op", + op: "::decimal", + exprs: [e] + }); }) + }; + case ">": + case "<": + case ">=": + case "<=": + case "<>": + case "=": + case "~*": + case "round": + case "floor": + case "ceiling": + case "sum": + case "avg": + case "min": + case "max": + case "count": + case "stdev": + case "stdevp": + case "var": + case "varp": + case "array_agg": + // Null if any not present + if (lodash_1.default.any(compiledExprs, function (ce) { return ce == null; })) { + return null; + } + return { type: "op", - op: "::timestamp", - exprs: [compiledExprs[0]] - }] - }, { - type: "op", - op: "date_part", - exprs: ['epoch', { + op: expr.op, + exprs: compiledExprs + }; + case "least": + case "greatest": + return { type: "op", - op: "::timestamp", - exprs: [compiledExprs[1]] - }] - }] - }, 86400] - }; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "date") { - return { - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }, { - type: "op", - op: "::date", - exprs: [compiledExprs[1]] - }] - }; - } - - return null; - - case 'months difference': - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "datetime" || exprUtils.getExprType(expr.exprs[1]) === "datetime") { - return { - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "date_part", - exprs: ['epoch', { + op: expr.op, + exprs: compiledExprs + }; + case "/": + // Null if any not present + if (lodash_1.default.any(compiledExprs, function (ce) { return ce == null; })) { + return null; + } + // Cast to decimal before dividing to prevent integer math + return { type: "op", - op: "::timestamp", - exprs: [compiledExprs[0]] - }] - }, { - type: "op", - op: "date_part", - exprs: ['epoch', { + op: expr.op, + exprs: [ + compiledExprs[0], + { type: "op", op: "::decimal", exprs: [{ type: "op", op: "nullif", exprs: [compiledExprs[1], 0] }] } + ] + }; + case "last": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Get ordering + ordering = this.schema.getTable(expr.table).ordering; + if (!ordering) { + throw new Error("Table " + expr.table + " must be ordered to use last()"); + } + // (array_agg(xyz order by theordering desc nulls last))[1] + return { type: "op", - op: "::timestamp", - exprs: [compiledExprs[1]] - }] - }] - }, 86400 * 30.5] - }; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "date") { - return { - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }, { - type: "op", - op: "::date", - exprs: [compiledExprs[1]] - }] - }, 30.5] - }; - } - - return null; - - case 'years difference': - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "datetime" || exprUtils.getExprType(expr.exprs[1]) === "datetime") { - return { - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "date_part", - exprs: ['epoch', { + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "desc", nulls: "last" }] }, + 1 + ] + }; + case "last where": + // Null if not value present + if (compiledExprs[0] == null) { + return null; + } + // Get ordering + ordering = this.schema.getTable(expr.table).ordering; + if (!ordering) { + throw new Error("Table " + expr.table + " must be ordered to use last()"); + } + // Simple last if not condition present + if (compiledExprs[1] == null) { + // (array_agg(xyz order by theordering desc nulls last))[1] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "desc", nulls: "last" }] }, + 1 + ] + }; + } + // Compiles to: + // (array_agg((case when then else null end) order by (case when then 0 else 1 end), desc nulls last))[1] + // which prevents non-matching from appearing + return { type: "op", - op: "::timestamp", - exprs: [compiledExprs[0]] - }] - }, { - type: "op", - op: "date_part", - exprs: ['epoch', { + op: "[]", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [ + { type: "case", cases: [{ when: compiledExprs[1], then: compiledExprs[0] }], else: null } + ], + orderBy: [ + { expr: { type: "case", cases: [{ when: compiledExprs[1], then: 0 }], else: 1 } }, + { expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "desc", nulls: "last" } + ] + }, + 1 + ] + }; + case "previous": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Get ordering + ordering = this.schema.getTable(expr.table).ordering; + if (!ordering) { + throw new Error("Table " + expr.table + " must be ordered to use previous()"); + } + // (array_agg(xyz order by theordering desc nulls last))[2] + return { type: "op", - op: "::timestamp", - exprs: [compiledExprs[1]] - }] - }] - }, 86400 * 365] - }; - } - - if (exprUtils.getExprType(expr.exprs[0]) === "date") { - return { - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }, { - type: "op", - op: "::date", - exprs: [compiledExprs[1]] - }] - }, 365] - }; - } - - return null; - - case 'days since': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "-", - exprs: [{ - type: "op", - op: "::date", - exprs: [moment().format("YYYY-MM-DD")] - }, { - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }] - }; - - case "datetime": - return { - type: "op", - op: "/", - exprs: [{ - type: "op", - op: "-", - exprs: [{ + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "desc", nulls: "last" }] }, + 2 + ] + }; + case "first": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Get ordering + ordering = this.schema.getTable(expr.table).ordering; + if (!ordering) { + throw new Error("Table " + expr.table + " must be ordered to use first()"); + } + // (array_agg(xyz order by theordering asc nulls last))[1] + return { type: "op", - op: "date_part", - exprs: ['epoch', { - type: "op", - op: "::timestamp", - exprs: [nowExpr] - }] - }, { + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "asc", nulls: "last" }] }, + 1 + ] + }; + case "first where": + // Null if not value present + if (compiledExprs[0] == null) { + return null; + } + // Get ordering + ordering = this.schema.getTable(expr.table).ordering; + if (!ordering) { + throw new Error("Table " + expr.table + " must be ordered to use first where()"); + } + // Simple first if not condition present + if (compiledExprs[1] == null) { + // (array_agg(xyz order by theordering asc nulls last))[1] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "asc", nulls: "last" }] }, + 1 + ] + }; + } + // Compiles to: + // (array_agg((case when then else null end) order by (case when then 0 else 1 end), asc nulls last))[1] + // which prevents non-matching from appearing + return { type: "op", - op: "date_part", - exprs: ['epoch', { - type: "op", - op: "::timestamp", - exprs: [compiledExprs[0]] - }] - }] - }, 86400] - }; - - default: - return null; - } - - break; - - case 'month': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "substr", - exprs: [compiledExprs[0], 6, 2] - }; - - case 'yearmonth': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "rpad", - exprs: [{ - type: "op", - op: "substr", - exprs: [compiledExprs[0], 1, 7] - }, 10, "-01"] - }; - - case 'yearquarter': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "to_char", - exprs: [{ - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }, "YYYY-Q"] - }; - - case 'yearweek': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "to_char", - exprs: [{ - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }, "IYYY-IW"] - }; - - case 'weekofyear': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "to_char", - exprs: [{ - type: "op", - op: "::date", - exprs: [compiledExprs[0]] - }, "IW"] - }; - - case 'year': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "rpad", - exprs: [{ - type: "op", - op: "substr", - exprs: [compiledExprs[0], 1, 4] - }, 10, "-01-01"] - }; - - case 'weekofmonth': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "to_char", - exprs: [{ - type: "op", - op: "::timestamp", - exprs: [compiledExprs[0]] - }, "W"] - }; - - case 'dayofmonth': - if (!compiledExprs[0]) { - return null; - } - - return { - type: "op", - op: "to_char", - exprs: [{ - type: "op", - op: "::timestamp", - exprs: [compiledExprs[0]] - }, "DD"] - }; - - case 'thisyear': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("year").format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("year").add(1, 'years').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("year").toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("year").add(1, 'years').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'lastyear': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("year").subtract(1, 'years').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("year").format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("year").subtract(1, 'years').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("year").toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'thismonth': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("month").format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("month").add(1, 'months').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("month").toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("month").add(1, 'months').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'lastmonth': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("month").subtract(1, 'months').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("month").format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("month").subtract(1, 'months').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("month").toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'today': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("day").toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'yesterday': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(1, 'days').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("day").subtract(1, 'days').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'last24hours': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(1, 'days').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<=", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], nowMinus24HoursExpr] - }, { - type: "op", - op: "<=", - exprs: [compiledExprs[0], nowExpr] - }] - }; - - default: - return null; - } - - break; - - case 'last7days': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(7, 'days').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("day").subtract(7, 'days').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'last30days': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(30, 'days').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("day").subtract(30, 'days').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'last365days': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(365, 'days').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().startOf("day").subtract(365, 'days').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'last12months': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(11, "months").startOf('month').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(11, "months").startOf('month').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'last6months': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(5, "months").startOf('month').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(5, "months").startOf('month').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'last3months': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(2, "months").startOf('month').format("YYYY-MM-DD")] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD")] - }] - }; - - case "datetime": - return { - type: "op", - op: "and", - exprs: [{ - type: "op", - op: ">=", - exprs: [compiledExprs[0], moment().subtract(2, "months").startOf('month').toISOString()] - }, { - type: "op", - op: "<", - exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString()] - }] - }; - - default: - return null; - } - - break; - - case 'future': - if (!compiledExprs[0]) { - return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: ">", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD")] - }; - - case "datetime": - return { - type: "op", - op: ">", - exprs: [compiledExprs[0], nowExpr] - }; - + op: "[]", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [ + { type: "case", cases: [{ when: compiledExprs[1], then: compiledExprs[0] }], else: null } + ], + orderBy: [ + { expr: { type: "case", cases: [{ when: compiledExprs[1], then: 0 }], else: 1 } }, + { expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias }), direction: "asc", nulls: "last" } + ] + }, + 1 + ] + }; + case '= any': + // Null if any not present + if (lodash_1.default.any(compiledExprs, function (ce) { return ce == null; })) { + return null; + } + // False if empty list on rhs + if (expr.exprs[1].type === "literal") { + var rhsLiteral = expr.exprs[1]; + if (rhsLiteral.value == null || (lodash_1.default.isArray(rhsLiteral.value) && (rhsLiteral.value.length === 0))) { + return false; + } + } + return { type: "op", op: "=", modifier: "any", exprs: compiledExprs }; + case "between": + // Null if first not present + if (compiledExprs[0] == null) { + return null; + } + // Null if second and third not present + if (compiledExprs[1] == null && compiledExprs[2] == null) { + return null; + } + // >= if third missing + if (compiledExprs[2] == null) { + return { + type: "op", + op: ">=", + exprs: [compiledExprs[0], compiledExprs[1]] + }; + } + // <= if second missing + if (compiledExprs[1] == null) { + return { + type: "op", + op: "<=", + exprs: [compiledExprs[0], compiledExprs[2]] + }; + } + // Between + return { + type: "op", + op: "between", + exprs: compiledExprs + }; + case "not": + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: expr.op, + exprs: [ + { type: "op", op: "coalesce", exprs: [compiledExprs[0], false] } + ] + }; + case "is null": + case "is not null": + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: expr.op, + exprs: compiledExprs + }; + case "contains": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + // Null if no expressions in literal list + if ((compiledExprs[1].type === "literal") && (compiledExprs[1].value.length === 0)) { + return null; + } + // Cast both to jsonb and use @>. Also convert both to json first to handle literal arrays + return { + type: "op", + op: "@>", + exprs: [ + convertToJsonB(compiledExprs[0]), + convertToJsonB(compiledExprs[1]) + ] + }; + case "intersects": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + // Null if no expressions in literal list + if ((compiledExprs[1].type === "literal") && (compiledExprs[1].value.length === 0)) { + return null; + } + // Cast to jsonb and use ?| Also convert to json first to handle literal arrays + return { + type: "op", + op: "?|", + exprs: [ + convertToJsonB(compiledExprs[0]), + compiledExprs[1] + ] + }; + case "length": + // 0 if null + if ((compiledExprs[0] == null)) { + return 0; + } + // Cast both to jsonb and use jsonb_array_length. Also convert both to json first to handle literal arrays. Coalesce to 0 so that null is 0 + return { + type: "op", + op: "coalesce", + exprs: [ + { + type: "op", + op: "jsonb_array_length", + exprs: [ + convertToJsonB(compiledExprs[0]) + ] + }, + 0 + ] + }; + case "line length": + // null if null + if ((compiledExprs[0] == null)) { + return null; + } + // ST_Length_Spheroid(ST_Transform(location,4326::integer), 'SPHEROID["GRS_1980",6378137,298.257222101]'::spheroid) + return { + type: "op", + op: "ST_LengthSpheroid", + exprs: [ + { + type: "op", + op: "ST_Transform", + exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] + }, + { type: "op", op: "::spheroid", exprs: ['SPHEROID["GRS_1980",6378137,298.257222101]'] } + ] + }; + case "to text": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + if (exprUtils.getExprType(expr.exprs[0]) === "enum") { + // Null if no enum values + var enumValues = exprUtils.getExprEnumValues(expr.exprs[0]); + if (!enumValues) { + return null; + } + return { + type: "case", + input: compiledExprs[0], + cases: lodash_1.default.map(enumValues, function (ev) { + return { + when: { type: "literal", value: ev.id }, + then: { type: "literal", value: exprUtils.localizeString(ev.name) } + }; + }) + }; + } + if (exprUtils.getExprType(expr.exprs[0]) === "number") { + return { + type: "op", + op: "::text", + exprs: [compiledExprs[0]] + }; + } + return null; + case "to date": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "substr", + exprs: [ + compiledExprs[0], + 1, + 10 + ] + }; + case "count where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "coalesce", + exprs: [ + { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[0], + then: 1 + } + ], + else: 0 + } + ] + }, + 0 + ] + }; + case "percent where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Compiles as sum(case when cond [and basis (if present)] then 100::decimal else 0 end)/sum(1 [or case when basis then 1 else 0 (if present)]) (prevent div by zero) + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1] ? { type: "op", op: "and", exprs: [compiledExprs[0], compiledExprs[1]] } : compiledExprs[0], + then: { type: "op", op: "::decimal", exprs: [100] } + } + ], + else: 0 + } + ] + }, + compiledExprs[1] ? + { + type: "op", + op: "nullif", + exprs: [ + { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: 1 + } + ], + else: 0 + } + ] + }, + 0 + ] + } + : + { type: "op", op: "sum", exprs: [1] } + ] + }; + case "sum where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Simple sum if not specified where + if (compiledExprs[1] == null) { + return { + type: "op", + op: "sum", + exprs: [compiledExprs[0]] + }; + } + return { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: compiledExprs[0] + } + ], + else: 0 + } + ] + }; + case "min where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Simple min if not specified where + if (compiledExprs[1] == null) { + return { + type: "op", + op: "min", + exprs: [compiledExprs[0]] + }; + } + return { + type: "op", + op: "min", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: compiledExprs[0] + } + ], + else: null + } + ] + }; + case "max where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + // Simple max if not specified where + if (compiledExprs[1] == null) { + return { + type: "op", + op: "max", + exprs: [compiledExprs[0]] + }; + } + return { + type: "op", + op: "max", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: compiledExprs[0] + } + ], + else: null + } + ] + }; + case "count distinct": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "count", + exprs: [compiledExprs[0]], + modifier: "distinct" + }; + case "percent": + // Compiles as count(*) * 100::decimal / sum(count(*)) over() + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "*", + exprs: [ + { type: "op", op: "count", exprs: [] }, + { type: "op", op: "::decimal", exprs: [100] } + ] + }, + { + type: "op", + op: "sum", + exprs: [ + { type: "op", op: "count", exprs: [] } + ], + over: {} + } + ] + }; + // Hierarchical test that uses ancestry column + case "within": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + // Get table being used + var idTable = exprUtils.getExprIdTable(expr.exprs[0]); + // Prefer ancestryTable + if (this.schema.getTable(idTable).ancestryTable) { + // exists (select null from as subwithin where ancestor = compiledExprs[1] and descendant = compiledExprs[0]) + return { + type: "op", + op: "exists", + exprs: [ + { + type: "scalar", + expr: null, + from: { type: "table", table: this.schema.getTable(idTable).ancestryTable, alias: "subwithin" }, + where: { + type: "op", + op: "and", + exprs: [ + { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "ancestor" }, compiledExprs[1]] }, + { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "descendant" }, compiledExprs[0]] } + ] + } + } + ] + }; + } + return { + type: "op", + op: "in", + exprs: [ + compiledExprs[0], + { + type: "scalar", + expr: this.compileColumnRef(this.schema.getTable(idTable).primaryKey, "subwithin"), + from: { type: "table", table: idTable, alias: "subwithin" }, + where: { + type: "op", + op: "@>", + exprs: [ + { type: "field", tableAlias: "subwithin", column: this.schema.getTable(idTable).ancestry }, + { type: "op", op: "::jsonb", exprs: [{ type: "op", op: "json_build_array", exprs: [compiledExprs[1]] }] } + ] + } + } + ] + }; + // Hierarchical test that uses ancestry column + case "within any": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + // Get table being used + idTable = exprUtils.getExprIdTable(expr.exprs[0]); + // Prefer ancestryTable + if (this.schema.getTable(idTable).ancestryTable) { + // exists (select null from as subwithin where ancestor = any(compiledExprs[1]) and descendant = compiledExprs[0]) + return { + type: "op", + op: "exists", + exprs: [ + { + type: "scalar", + expr: null, + from: { type: "table", table: this.schema.getTable(idTable).ancestryTable, alias: "subwithin" }, + where: { + type: "op", + op: "and", + exprs: [ + { type: "op", op: "=", modifier: "any", exprs: [{ type: "field", tableAlias: "subwithin", column: "ancestor" }, compiledExprs[1]] }, + { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "descendant" }, compiledExprs[0]] } + ] + } + } + ] + }; + } + // This older code fails now that admin_regions uses integer pk. Replaced with literal-only code + // return { + // type: "op" + // op: "in" + // exprs: [ + // compiledExprs[0] + // { + // type: "scalar" + // expr: @compileColumnRef(@schema.getTable(idTable).primaryKey, "subwithin") + // from: { type: "table", table: idTable, alias: "subwithin" } + // where: { + // type: "op" + // op: "?|" + // exprs: [ + // { type: "field", tableAlias: "subwithin", column: @schema.getTable(idTable).ancestry } + // compiledExprs[1] + // ] + // } + // } + // ] + // } + // If not literal, fail + if (compiledExprs[1].type !== "literal") { + throw new Error("Non-literal RHS of within any not supported"); + } + return { + type: "op", + op: "in", + exprs: [ + compiledExprs[0], + { + type: "scalar", + expr: this.compileColumnRef(this.schema.getTable(idTable).primaryKey, "subwithin"), + from: { type: "table", table: idTable, alias: "subwithin" }, + where: { + type: "op", + op: "?|", + exprs: [ + { type: "field", tableAlias: "subwithin", column: this.schema.getTable(idTable).ancestryText || this.schema.getTable(idTable).ancestry }, + { type: "literal", value: lodash_1.default.map(compiledExprs[1].value, function (value) { + if (lodash_1.default.isNumber(value)) { + return "" + value; + } + else { + return value; + } + }) + } + ] + } + } + ] + }; + case "latitude": + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "ST_Y", + exprs: [ + { type: "op", op: "ST_Centroid", exprs: [ + { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } + ] } + ] + }; + case "longitude": + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "ST_X", + exprs: [ + { type: "op", op: "ST_Centroid", exprs: [ + { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } + ] } + ] + }; + case 'days difference': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + if ((exprUtils.getExprType(expr.exprs[0]) === "datetime") || (exprUtils.getExprType(expr.exprs[1]) === "datetime")) { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } + ] + }, + 86400 + ] + }; + } + if (exprUtils.getExprType(expr.exprs[0]) === "date") { + return { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + { type: "op", op: "::date", exprs: [compiledExprs[1]] } + ] + }; + } + return null; + case 'months difference': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + if ((exprUtils.getExprType(expr.exprs[0]) === "datetime") || (exprUtils.getExprType(expr.exprs[1]) === "datetime")) { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } + ] + }, + 86400 * 30.5 + ] + }; + } + if (exprUtils.getExprType(expr.exprs[0]) === "date") { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + { type: "op", op: "::date", exprs: [compiledExprs[1]] } + ] + }, + 30.5 + ] + }; + } + return null; + case 'years difference': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + if ((exprUtils.getExprType(expr.exprs[0]) === "datetime") || (exprUtils.getExprType(expr.exprs[1]) === "datetime")) { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } + ] + }, + 86400 * 365 + ] + }; + } + if (exprUtils.getExprType(expr.exprs[0]) === "date") { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + { type: "op", op: "::date", exprs: [compiledExprs[1]] } + ] + }, + 365 + ] + }; + } + return null; + case 'days since': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [moment_1.default().format("YYYY-MM-DD")] }, + { type: "op", op: "::date", exprs: [compiledExprs[0]] } + ] + }; + case "datetime": + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [nowExpr] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] } + ] + }, + 86400 + ] + }; + default: + return null; + } + case 'month': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "substr", + exprs: [ + compiledExprs[0], + 6, + 2 + ] + }; + case 'yearmonth': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "rpad", + exprs: [ + { type: "op", op: "substr", exprs: [compiledExprs[0], 1, 7] }, + 10, + "-01" + ] + }; + case 'yearquarter': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + "YYYY-Q" + ] + }; + case 'yearweek': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + "IYYY-IW" + ] + }; + case 'weekofyear': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + "IW" + ] + }; + case 'year': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "rpad", + exprs: [ + { type: "op", op: "substr", exprs: [compiledExprs[0], 1, 4] }, + 10, + "-01-01" + ] + }; + case 'weekofmonth': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }, + "W" + ] + }; + case 'dayofmonth': + if (compiledExprs[0] == null) { + return null; + } + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }, + "DD" + ] + }; + case 'thisyear': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("year").format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("year").add(1, 'years').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("year").toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("year").add(1, 'years').toISOString()] } + ] + }; + default: + return null; + } + case 'lastyear': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("year").subtract(1, 'years').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("year").format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("year").subtract(1, 'years').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("year").toISOString()] } + ] + }; + default: + return null; + } + case 'thismonth': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("month").format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("month").add(1, 'months').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("month").toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("month").add(1, 'months').toISOString()] } + ] + }; + default: + return null; + } + case 'lastmonth': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("month").subtract(1, 'months').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("month").format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("month").subtract(1, 'months').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("month").toISOString()] } + ] + }; + default: + return null; + } + case 'today': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("day").toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'yesterday': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(1, 'days').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("day").subtract(1, 'days').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").toISOString()] } + ] + }; + default: + return null; + } + case 'last24hours': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(1, 'days').format("YYYY-MM-DD")] }, + { type: "op", op: "<=", exprs: [compiledExprs[0], moment_1.default().format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], nowMinus24HoursExpr] }, + { type: "op", op: "<=", exprs: [compiledExprs[0], nowExpr] } + ] + }; + default: + return null; + } + case 'last7days': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(7, 'days').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("day").subtract(7, 'days').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'last30days': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(30, 'days').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("day").subtract(30, 'days').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'last365days': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(365, 'days').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().startOf("day").subtract(365, 'days').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'last12months': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(11, "months").startOf('month').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(11, "months").startOf('month').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'last6months': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(5, "months").startOf('month').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(5, "months").startOf('month').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'last3months': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(2, "months").startOf('month').format("YYYY-MM-DD")] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().add(1, 'days').format("YYYY-MM-DD")] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment_1.default().subtract(2, "months").startOf('month').toISOString()] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment_1.default().startOf("day").add(1, 'days').toISOString()] } + ] + }; + default: + return null; + } + case 'future': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: ">", + exprs: [compiledExprs[0], moment_1.default().format("YYYY-MM-DD")] + }; + case "datetime": + return { + type: "op", + op: ">", + exprs: [compiledExprs[0], nowExpr] + }; + default: + return null; + } + case 'notfuture': + if (compiledExprs[0] == null) { + return null; + } + switch (expr0Type) { + case "date": + return { + type: "op", + op: "<=", + exprs: [compiledExprs[0], moment_1.default().format("YYYY-MM-DD")] + }; + case "datetime": + return { + type: "op", + op: "<=", + exprs: [compiledExprs[0], nowExpr] + }; + default: + return null; + } + case 'current date': + return { type: "literal", value: moment_1.default().format("YYYY-MM-DD") }; + case 'current datetime': + return { type: "literal", value: moment_1.default().toISOString() }; + case 'distance': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + return { + type: "op", + op: "ST_DistanceSphere", + exprs: [ + { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] }, + { type: "op", op: "ST_Transform", exprs: [compiledExprs[1], { type: "op", op: "::integer", exprs: [4326] }] } + ] + }; + case 'is latest': + var lhsCompiled = this.compileExpr({ expr: expr.exprs[0], tableAlias: "innerrn" }); + if (!lhsCompiled) { + return null; + } + var filterCompiled = this.compileExpr({ expr: expr.exprs[1], tableAlias: "innerrn" }); + // Get ordering + ordering = this.schema.getTable(expr.table).ordering; + if (!ordering) { + throw new Error("No ordering defined"); + } + // order descending + var orderBy = [{ expr: this.compileFieldExpr({ expr: { type: "field", table: expr.table, column: ordering }, tableAlias: "innerrn" }), direction: "desc" }]; + // _id in (select outerrn.id from (select innerrn.id, row_number() over (partition by EXPR1 order by ORDERING desc) as rn from the_table as innerrn where filter) as outerrn where outerrn.rn = 1) + // Create innerrn query + var innerrnQuery = { + type: "query", + selects: [ + { type: "select", expr: this.compileExpr({ expr: { type: "id", table: expr.table }, tableAlias: "innerrn" }), alias: "id" }, + { + type: "select", + expr: { + type: "op", + op: "row_number", + exprs: [], + over: { + partitionBy: [lhsCompiled], + orderBy: orderBy + } + }, + alias: "rn" + } + ], + from: { type: "table", table: expr.table, alias: "innerrn" } + }; + if (filterCompiled) { + innerrnQuery.where = filterCompiled; + } + // Wrap in outer query + var outerrnQuery = { + type: "scalar", + expr: { type: "field", tableAlias: "outerrn", column: "id" }, + from: { + type: "subquery", + query: innerrnQuery, + alias: "outerrn" + }, + where: { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "outerrn", column: "rn" }, 1] } + }; + return { + type: "op", + op: "in", + exprs: [ + this.compileExpr({ expr: { type: "id", table: expr.table }, tableAlias: options.tableAlias }), + outerrnQuery + ] + }; default: - return null; - } - - break; - - case 'notfuture': - if (!compiledExprs[0]) { + throw new Error("Unknown op " + expr.op); + } + }; + ExprCompiler.prototype.compileCaseExpr = function (options) { + var _this = this; + var expr = options.expr; + var compiled = { + type: "case", + cases: lodash_1.default.map(expr.cases, function (c) { + return { + when: _this.compileExpr({ expr: c.when, tableAlias: options.tableAlias }), + then: _this.compileExpr({ expr: c.then, tableAlias: options.tableAlias }) + }; + }), + else: this.compileExpr({ expr: expr.else, tableAlias: options.tableAlias }) + }; + // Remove null cases + compiled.cases = lodash_1.default.filter(compiled.cases, function (c) { return c.when != null; }); + // Return null if no cases + if (compiled.cases.length === 0) { return null; - } - - switch (expr0Type) { - case "date": - return { - type: "op", - op: "<=", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD")] - }; - - case "datetime": - return { - type: "op", - op: "<=", - exprs: [compiledExprs[0], nowExpr] - }; - + } + return compiled; + }; + ExprCompiler.prototype.compileScoreExpr = function (options) { + var _this = this; + var expr = options.expr; + var exprUtils = new ExprUtils_1.default(this.schema); + // If empty, literal 0 + if (lodash_1.default.isEmpty(expr.scores)) { + return { type: "literal", value: 0 }; + } + // Get type of input + var inputType = exprUtils.getExprType(expr.input); + switch (inputType) { + case "enum": + return { + type: "case", + input: this.compileExpr({ expr: expr.input, tableAlias: options.tableAlias }), + cases: lodash_1.default.map(lodash_1.default.pairs(expr.scores), function (pair) { + return { + when: { type: "literal", value: pair[0] }, + then: _this.compileExpr({ expr: pair[1], tableAlias: options.tableAlias }) + }; + }), + else: { type: "literal", value: 0 } + }; + case "enumset": + return { + type: "op", + op: "+", + exprs: lodash_1.default.map(lodash_1.default.pairs(expr.scores), function (pair) { + return { + type: "case", + cases: [ + { + when: { + type: "op", + op: "@>", + exprs: [ + convertToJsonB(_this.compileExpr({ expr: expr.input, tableAlias: options.tableAlias })), + convertToJsonB({ type: "literal", value: [pair[0]] }) + ] + }, + then: _this.compileExpr({ expr: pair[1], tableAlias: options.tableAlias }) + } + ], + else: { type: "literal", value: 0 } + }; + }) + }; + // Null if no expression default: - return null; - } - - break; - - case 'current date': - return { - type: "literal", - value: moment().format("YYYY-MM-DD") - }; - - case 'current datetime': - return { - type: "literal", - value: moment().toISOString() - }; - - case 'distance': - if (!compiledExprs[0] || !compiledExprs[1]) { - return null; - } - - return { - type: "op", - op: "ST_DistanceSphere", - exprs: [{ - type: "op", - op: "ST_Transform", - exprs: [compiledExprs[0], { - type: "op", - op: "::integer", - exprs: [4326] - }] - }, { - type: "op", - op: "ST_Transform", - exprs: [compiledExprs[1], { - type: "op", - op: "::integer", - exprs: [4326] - }] - }] - }; - - case 'is latest': - lhsCompiled = this.compileExpr({ - expr: expr.exprs[0], - tableAlias: "innerrn" - }); - - if (!lhsCompiled) { + return null; + } + }; + ExprCompiler.prototype.compileBuildEnumsetExpr = function (options) { + // Create enumset + // select to_jsonb(array_agg(bes.v)) from (select (case when true then 'x' end) as v union all select (case when true then 'y' end) as v ...) as bes where v is not null + var _this = this; + var expr = options.expr; + // Handle empty case + if (lodash_1.default.keys(expr.values).length === 0) { return null; - } - - filterCompiled = this.compileExpr({ - expr: expr.exprs[1], - tableAlias: "innerrn" - }); // Get ordering - - ordering = this.schema.getTable(expr.table).ordering; - - if (!ordering) { - throw new Error("No ordering defined"); - } // order descending - - - orderBy = [{ - expr: this.compileFieldExpr({ - expr: { - type: "field", - table: expr.table, - column: ordering - }, - tableAlias: "innerrn" - }), - direction: "desc" - }]; // _id in (select outerrn.id from (select innerrn.id, row_number() over (partition by EXPR1 order by ORDERING desc) as rn from the_table as innerrn where filter) as outerrn where outerrn.rn = 1) - // Create innerrn query - - innerrnQuery = { - type: "query", - selects: [{ - type: "select", - expr: this.compileExpr({ - expr: { - type: "id", - table: expr.table - }, - tableAlias: "innerrn" - }), - alias: "id" - }, { - type: "select", - expr: { - type: "op", - op: "row_number", - exprs: [], - over: { - partitionBy: [lhsCompiled], - orderBy: orderBy - } - }, - alias: "rn" - }], - from: { - type: "table", - table: expr.table, - alias: "innerrn" - } - }; - - if (filterCompiled) { - innerrnQuery.where = filterCompiled; - } // Wrap in outer query - - - outerrnQuery = { + } + return { type: "scalar", expr: { - type: "field", - tableAlias: "outerrn", - column: "id" + type: "op", + op: "to_jsonb", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [{ type: "field", tableAlias: "bes", column: "v" }] + } + ] }, from: { - type: "subquery", - query: innerrnQuery, - alias: "outerrn" + type: "subquery", + alias: "bes", + query: { + type: "union all", + queries: lodash_1.default.map(lodash_1.default.pairs(expr.values), function (pair) { + return { + type: "query", + selects: [ + { + type: "select", + expr: { + type: "case", + cases: [{ when: _this.compileExpr({ expr: pair[1], tableAlias: options.tableAlias }), then: pair[0] }] + }, + alias: "v" + } + ] + }; + }) + } }, where: { - type: "op", - op: "=", - exprs: [{ - type: "field", - tableAlias: "outerrn", - column: "rn" - }, 1] + type: "op", + op: "is not null", + exprs: [{ type: "field", tableAlias: "bes", column: "v" }] } - }; - return { - type: "op", - op: "in", - exprs: [this.compileExpr({ - expr: { - type: "id", - table: expr.table - }, - tableAlias: options.tableAlias - }), outerrnQuery] - }; - - default: - throw new Error("Unknown op ".concat(expr.op)); - } - } - }, { - key: "compileCaseExpr", - value: function compileCaseExpr(options) { - var _this2 = this; - - var compiled, expr; - expr = options.expr; - compiled = { - type: "case", - cases: _.map(expr.cases, function (c) { - return { - when: _this2.compileExpr({ - expr: c.when, - tableAlias: options.tableAlias - }), - then: _this2.compileExpr({ - expr: c.then, - tableAlias: options.tableAlias - }) - }; - }), - "else": this.compileExpr({ - expr: expr["else"], - tableAlias: options.tableAlias - }) - }; // Remove null cases - - compiled.cases = _.filter(compiled.cases, function (c) { - return c.when != null; - }); // Return null if no cases - - if (compiled.cases.length === 0) { - return null; - } - - return compiled; - } - }, { - key: "compileScoreExpr", - value: function compileScoreExpr(options) { - var _this3 = this; - - var expr, exprUtils, inputType; - expr = options.expr; - exprUtils = new ExprUtils(this.schema); // If empty, literal 0 - - if (_.isEmpty(expr.scores)) { - return { - type: "literal", - value: 0 }; - } // Get type of input - - - inputType = exprUtils.getExprType(expr.input); - - switch (inputType) { - case "enum": - return { - type: "case", - input: this.compileExpr({ - expr: expr.input, - tableAlias: options.tableAlias - }), - cases: _.map(_.pairs(expr.scores), function (pair) { - return { - when: { - type: "literal", - value: pair[0] - }, - then: _this3.compileExpr({ - expr: pair[1], - tableAlias: options.tableAlias - }) - }; - }), - "else": { - type: "literal", - value: 0 - } - }; - - case "enumset": - return { - type: "op", - op: "+", - exprs: _.map(_.pairs(expr.scores), function (pair) { - return { - type: "case", - cases: [{ - when: { + }; + ExprCompiler.prototype.compileComparisonExpr = function (options) { + var exprs; + var expr = options.expr; + var exprUtils = new ExprUtils_1.default(this.schema); + // Missing left-hand side type means null condition + var exprLhsType = exprUtils.getExprType(expr.lhs); + if (!exprLhsType) { + return null; + } + // Missing right-hand side means null condition + if (exprUtils.getComparisonRhsType(exprLhsType, expr.op) && (expr.rhs == null)) { + return null; + } + var lhsExpr = this.compileExpr({ expr: expr.lhs, tableAlias: options.tableAlias }); + if (expr.rhs) { + var rhsExpr = this.compileExpr({ expr: expr.rhs, tableAlias: options.tableAlias }); + exprs = [lhsExpr, rhsExpr]; + } + else { + exprs = [lhsExpr]; + } + // Handle special cases + switch (expr.op) { + case '= true': + return { type: "op", op: "=", exprs: [lhsExpr, { type: "literal", value: true }] }; + case '= false': + return { type: "op", op: "=", exprs: [lhsExpr, { type: "literal", value: false }] }; + case '= any': + return { type: "op", op: "=", modifier: "any", exprs: exprs }; + case 'between': + return { type: "op", op: "between", exprs: [lhsExpr, { type: "literal", value: expr.rhs.value[0] }, { type: "literal", value: expr.rhs.value[1] }] }; + default: + return { type: "op", - op: "@>", - exprs: [convertToJsonB(_this3.compileExpr({ - expr: expr.input, - tableAlias: options.tableAlias - })), convertToJsonB({ - type: "literal", - value: [pair[0]] - })] - }, - then: _this3.compileExpr({ - expr: pair[1], - tableAlias: options.tableAlias - }) - }], - "else": { - type: "literal", - value: 0 - } - }; - }) - }; - - default: - // Null if no expression - return null; - } - } - }, { - key: "compileBuildEnumsetExpr", - value: function compileBuildEnumsetExpr(options) { - var _this4 = this; - - var expr; // Create enumset - // select to_jsonb(array_agg(bes.v)) from (select (case when true then 'x' end) as v union all select (case when true then 'y' end) as v ...) as bes where v is not null - - expr = options.expr; // Handle empty case - - if (_.keys(expr.values).length === 0) { - return null; - } - - return { - type: "scalar", - expr: { - type: "op", - op: "to_jsonb", - exprs: [{ - type: "op", - op: "array_agg", - exprs: [{ - type: "field", - tableAlias: "bes", - column: "v" - }] - }] - }, - from: { - type: "subquery", - alias: "bes", - query: { - type: "union all", - queries: _.map(_.pairs(expr.values), function (pair) { - return { - type: "query", - selects: [{ - type: "select", - expr: { - type: "case", - cases: [{ - when: _this4.compileExpr({ - expr: pair[1], - tableAlias: options.tableAlias - }), - then: pair[0] - }] - }, - alias: "v" - }] - }; - }) - } - }, - where: { - type: "op", - op: "is not null", - exprs: [{ - type: "field", - tableAlias: "bes", - column: "v" - }] + op: expr.op, + exprs: exprs + }; } - }; - } - }, { - key: "compileComparisonExpr", - value: function compileComparisonExpr(options) { - var expr, exprLhsType, exprUtils, exprs, lhsExpr, rhsExpr; - expr = options.expr; - exprUtils = new ExprUtils(this.schema); // Missing left-hand side type means null condition - - exprLhsType = exprUtils.getExprType(expr.lhs); - - if (!exprLhsType) { - return null; - } // Missing right-hand side means null condition - - - if (exprUtils.getComparisonRhsType(exprLhsType, expr.op) && expr.rhs == null) { - return null; - } - - lhsExpr = this.compileExpr({ - expr: expr.lhs, - tableAlias: options.tableAlias - }); - - if (expr.rhs) { - rhsExpr = this.compileExpr({ - expr: expr.rhs, - tableAlias: options.tableAlias - }); - exprs = [lhsExpr, rhsExpr]; - } else { - exprs = [lhsExpr]; - } // Handle special cases - - - switch (expr.op) { - case '= true': - return { - type: "op", - op: "=", - exprs: [lhsExpr, { - type: "literal", - value: true - }] - }; - - case '= false': - return { - type: "op", - op: "=", - exprs: [lhsExpr, { - type: "literal", - value: false - }] - }; - - case '= any': - return { - type: "op", - op: "=", - modifier: "any", - exprs: exprs - }; - - case 'between': - return { - type: "op", - op: "between", - exprs: [lhsExpr, { - type: "literal", - value: expr.rhs.value[0] - }, { - type: "literal", - value: expr.rhs.value[1] - }] - }; - - default: - return { + }; + ExprCompiler.prototype.compileLogicalExpr = function (options) { + var _this = this; + var expr = options.expr; + var compiledExprs = lodash_1.default.map(expr.exprs, function (e) { return _this.compileExpr({ expr: e, tableAlias: options.tableAlias }); }); + // Remove nulls + compiledExprs = lodash_1.default.compact(compiledExprs); + // Simplify + if (compiledExprs.length === 1) { + return compiledExprs[0]; + } + if (compiledExprs.length === 0) { + return null; + } + return { type: "op", op: expr.op, - exprs: exprs - }; - } - } - }, { - key: "compileLogicalExpr", - value: function compileLogicalExpr(options) { - var _this5 = this; - - var compiledExprs, expr; - expr = options.expr; - compiledExprs = _.map(expr.exprs, function (e) { - return _this5.compileExpr({ - expr: e, - tableAlias: options.tableAlias - }); - }); // Remove nulls - - compiledExprs = _.compact(compiledExprs); // Simplify - - if (compiledExprs.length === 1) { - return compiledExprs[0]; - } - - if (compiledExprs.length === 0) { - return null; - } - - return { - type: "op", - op: expr.op, - exprs: compiledExprs - }; - } // Compiles a reference to a column or a JsonQL expression + exprs: compiledExprs + }; + }; + // Compiles a reference to a column or a JsonQL expression // If parameter is a string, create a simple field expression // If parameter is an object, inject tableAlias for `{alias}` - - }, { - key: "compileColumnRef", - value: function compileColumnRef(column, tableAlias) { - if (_.isString(column)) { - return { - type: "field", - tableAlias: tableAlias, - column: column - }; - } - - return injectTableAlias(column, tableAlias); - } // Compiles a table, substituting with custom jsonql if required - - }, { - key: "compileTable", - value: function compileTable(tableId, alias) { - var table; - table = this.schema.getTable(tableId); - - if (!table) { - throw new Error("Table ".concat(tableId, " not found")); - } - - if (!table.jsonql) { - return { - type: "table", - table: tableId, - alias: alias - }; - } else { - return { - type: "subquery", - query: table.jsonql, - alias: alias - }; - } - } - }, { - key: "compileVariableExpr", - value: function compileVariableExpr(options) { - var value, variable; // Get variable - - variable = _.findWhere(this.variables, { - id: options.expr.variableId - }); - - if (!variable) { - throw new Error("Variable ".concat(options.expr.variableId, " not found")); - } // Get value (which is always an expression) - - - value = this.variableValues[variable.id]; // If expression, compile - - if (value != null) { - return this.compileExpr({ - expr: value, - tableAlias: options.tableAlias - }); - } else { - return null; - } - } - }]); - return ExprCompiler; -}(); // Converts a compiled expression to jsonb. Literals cannot use to_jsonb as they will + ExprCompiler.prototype.compileColumnRef = function (column, tableAlias) { + if (lodash_1.default.isString(column)) { + return { type: "field", tableAlias: tableAlias, column: column }; + } + return injectTableAliases_1.injectTableAlias(column, tableAlias); + }; + // Compiles a table, substituting with custom jsonql if required + ExprCompiler.prototype.compileTable = function (tableId, alias) { + var table = this.schema.getTable(tableId); + if (!table) { + throw new Error("Table " + tableId + " not found"); + } + if (!table.jsonql) { + return { type: "table", table: tableId, alias: alias }; + } + else { + return { type: "subquery", query: table.jsonql, alias: alias }; + } + }; + ExprCompiler.prototype.compileVariableExpr = function (options) { + // Get variable + var variable = lodash_1.default.findWhere(this.variables, { id: options.expr.variableId }); + if (!variable) { + throw new Error("Variable " + options.expr.variableId + " not found"); + } + // Get value (which is always an expression) + var value = this.variableValues[variable.id]; + // If expression, compile + if (value != null) { + return this.compileExpr({ expr: value, tableAlias: options.tableAlias }); + } + else { + return null; + } + }; + return ExprCompiler; +}()); +exports.default = ExprCompiler; +// Converts a compiled expression to jsonb. Literals cannot use to_jsonb as they will // trigger "could not determine polymorphic type because input has type unknown" unless the // SQL is inlined - - -convertToJsonB = function convertToJsonB(compiledExpr) { - if (!compiledExpr) { - return compiledExpr; - } // Literals are special and are cast to jsonb from a JSON string - - - if (compiledExpr.type === "literal") { - return { - type: "op", - op: "::jsonb", - exprs: [{ - type: "literal", - value: JSON.stringify(compiledExpr.value) - }] - }; - } - - return { +function convertToJsonB(compiledExpr) { + if (compiledExpr == null) { + return compiledExpr; + } + if (typeof compiledExpr == "number" || typeof compiledExpr == "boolean" || typeof compiledExpr == "string") { + return { type: "op", op: "::jsonb", exprs: [{ type: "literal", value: JSON.stringify(compiledExpr) }] }; + } + // Literals are special and are cast to jsonb from a JSON string + if (compiledExpr.type === "literal") { + return { type: "op", op: "::jsonb", exprs: [{ type: "literal", value: JSON.stringify(compiledExpr.value) }] }; + } // First convert using to_jsonb in case is array - type: "op", - op: "to_jsonb", - exprs: [compiledExpr] - }; -}; \ No newline at end of file + return { type: "op", op: "to_jsonb", exprs: [compiledExpr] }; +} diff --git a/lib/ExprUtils.js b/lib/ExprUtils.js index 4d48bd8..e459bfb 100644 --- a/lib/ExprUtils.js +++ b/lib/ExprUtils.js @@ -1130,6 +1130,7 @@ addOpItem({ op: "within any", name: "is within any of", resultType: "boolean", e addOpItem({ op: "array_agg", name: "Make list of", desc: "Aggregates results into a list", resultType: "text[]", exprTypes: ["text"], prefix: true, aggr: true }); addOpItem({ op: "contains", name: "includes all of", resultType: "boolean", exprTypes: ["id[]", "id[]"] }); addOpItem({ op: "intersects", name: "includes any of", resultType: "boolean", exprTypes: ["id[]", "id[]"] }); +addOpItem({ op: "includes", name: "includes", resultType: "boolean", exprTypes: ["id[]", "id"] }); addOpItem({ op: "count", name: "Total Number", desc: "Get total number of items", resultType: "number", exprTypes: [], prefix: true, aggr: true }); addOpItem({ op: "percent", name: "Percent of Total", desc: "Percent of all items", resultType: "number", exprTypes: [], prefix: true, aggr: true }); addOpItem({ op: "~*", name: "matches", resultType: "boolean", exprTypes: ["text", "text"] }); diff --git a/lib/types.d.ts b/lib/types.d.ts index ef7aa21..39db502 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -1,4 +1,4 @@ -import { JsonQL } from "jsonql"; +import { JsonQL, JsonQLQuery } from "jsonql"; export interface LocalizedString { _base: string; [language: string]: string; @@ -104,6 +104,8 @@ export interface ScalarExpr { table: string; /** @deprecated */ aggr?: string; + /** @deprecated */ + where?: Expr; /** Array of join columns to follow to get to table of expr. All must be `join` type */ joins: string[]; /** Expression from final table to get value */ @@ -170,7 +172,7 @@ export interface Table { /** Optional custom JsonQL expression. This allows a simple table to be translated to an arbitrarily complex JsonQL expression before being sent to the server. * @deprecated This is not enforced everywhere as some queries don't use compileTable */ - jsonql?: JsonQL; + jsonql?: JsonQLQuery; /** sql expression that gets the table. Usually just name of the table. *Note*: this is only for when using a schema file for Water.org's visualization server */ sql?: string; } diff --git a/package-lock.json b/package-lock.json index 6b565b5..598c6d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5617,9 +5617,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -5751,13 +5751,12 @@ "dev": true }, "jsonql": { - "version": "github:mWater/jsonql#b9c21ac49cb1e3b0b77fc3411d85431249b0a003", + "version": "github:mWater/jsonql#f7784950e617785940ad3928b9d7262f13d3a09d", "from": "github:mWater/jsonql", "requires": { "js-yaml": "^3.3.1", "lodash": "^3.9.3", - "minimist": "^1.1.3", - "pg-escape": "^0.1.0" + "minimist": "^1.1.3" } }, "jsprim": { @@ -7417,11 +7416,6 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, - "pg-escape": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/pg-escape/-/pg-escape-0.1.0.tgz", - "integrity": "sha1-9teke3u+UxpQ7qmy6hmjkcCJOgo=" - }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", diff --git a/src/ExprCompiler.coffee b/src/ExprCompiler.coffee deleted file mode 100644 index 9894b00..0000000 --- a/src/ExprCompiler.coffee +++ /dev/null @@ -1,2055 +0,0 @@ -_ = require 'lodash' -injectTableAlias = require('./injectTableAliases').injectTableAlias -injectTableAliases = require('./injectTableAliases').injectTableAliases -ExprUtils = require('./ExprUtils').default -moment = require 'moment' -ColumnNotFoundException = require('./ColumnNotFoundException').default -getExprExtension = require('./extensions').getExprExtension - -# now expression: (to_json(now() at time zone 'UTC')#>>'{}') -nowExpr = { - type: "op" - op: "#>>" - exprs: [ - { type: "op", op: "to_json", exprs: [ - { type: "op", op: "at time zone", exprs: [ - { type: "op", op: "now", exprs: [] } - "UTC" - ]} - ]} - "{}" - ] -} - -# now 24 hours ago: (to_json((now() - interval '24 hour') at time zone 'UTC')#>>'{}') -nowMinus24HoursExpr = { - type: "op" - op: "#>>" - exprs: [ - { type: "op", op: "to_json", exprs: [ - { type: "op", op: "at time zone", exprs: [ - { type: "op", op: "-", exprs: [{ type: "op", op: "now", exprs: [] }, { type: "op", op: "interval", exprs: [{ type: "literal", value: "24 hour" }]}] } - "UTC" - ]} - ]} - "{}" - ] -} - - -# Compiles expressions to JsonQL. Assumes that geometry is in Webmercator (3857) -module.exports = class ExprCompiler - # Variable values are lookup of id to variable value, which is always an expression - constructor: (schema, variables = [], variableValues = {}) -> - @schema = schema - @variables = variables - @variableValues = variableValues - - # Compile an expression. Pass expr and tableAlias. - compileExpr: (options) => - expr = options.expr - - # Handle null - if not expr - return null - - switch expr.type - when "id" - compiledExpr = @compileColumnRef(@schema.getTable(expr.table).primaryKey, options.tableAlias) - when "field" - compiledExpr = @compileFieldExpr(options) - when "scalar" - compiledExpr = @compileScalarExpr(options) - when "literal" - if expr.value? - compiledExpr = { type: "literal", value: expr.value } - else - compiledExpr = null - when "op" - compiledExpr = @compileOpExpr(options) - when "case" - compiledExpr = @compileCaseExpr(options) - when "score" - compiledExpr = @compileScoreExpr(options) - when "build enumset" - compiledExpr = @compileBuildEnumsetExpr(options) - when "variable" - compiledExpr = @compileVariableExpr(options) - when "extension" - compiledExpr = getExprExtension(expr.extension).compileExpr(expr, options.tableAlias, @schema, @variables, @variableValues) - when "count" # DEPRECATED - compiledExpr = null - when "comparison" # DEPRECATED - compiledExpr = @compileComparisonExpr(options) - when "logical" # DEPRECATED - compiledExpr = @compileLogicalExpr(options) - else - throw new Error("Expr type #{expr.type} not supported") - return compiledExpr - - compileFieldExpr: (options) -> - expr = options.expr - - column = @schema.getColumn(expr.table, expr.column) - if not column - throw new ColumnNotFoundException("Column #{expr.table}.#{expr.column} not found") - - # Handle joins specially - if column.type == "join" - # If id is result - if column.join.type in ['1-1', 'n-1'] - # Use scalar to create - return @compileScalarExpr(expr: { type: "scalar", table: expr.table, joins: [column.id], expr: { type: "id", table: column.join.toTable }}, tableAlias: options.tableAlias) - else - return { - type: "scalar" - expr: { - type: "op" - op: "to_jsonb" - exprs: [ - { - type: "op" - op: "array_agg" - exprs: [ - @compileColumnRef(@schema.getTable(column.join.toTable).primaryKey, "inner") - ] - } - ] - } - from: @compileTable(column.join.toTable, "inner") - where: @compileJoin(expr.table, column, options.tableAlias, "inner") - limit: 1 # Limit 1 to be safe - } - - # Handle if has expr - if column.expr - return @compileExpr({ expr: column.expr, tableAlias: options.tableAlias }) - - # If column has custom jsonql, use that instead of id - return @compileColumnRef(column.jsonql or column.id, options.tableAlias) - - compileScalarExpr: (options) -> - expr = options.expr - - where = null - from = null - orderBy = null - - # Null expr is null - if not expr.expr - return null - - # Simplify if a join to an id field where the join uses the primary key of the to table - if not expr.aggr and not expr.where and expr.joins.length == 1 and expr.expr.type == "id" - fromColumn = @schema.getColumn(expr.table, expr.joins[0]) - - if fromColumn.type == "id" - return @compileColumnRef(fromColumn.id, options.tableAlias) - if fromColumn.join and fromColumn.join.toColumn == @schema.getTable(expr.expr.table).primaryKey - return @compileColumnRef(fromColumn.join.fromColumn, options.tableAlias) - - # Generate a consistent, semi-unique alias - generateAlias = (expr, joinIndex) -> - # Make alias-friendly (replace all symbols with _) - return expr.joins[joinIndex].replace(/[^a-zA-Z0-9]/g, "_").toLowerCase() - - # Perform joins - table = expr.table - tableAlias = options.tableAlias - - # First join is in where clause - if expr.joins and expr.joins.length > 0 - joinColumn = @schema.getColumn(expr.table, expr.joins[0]) - if not joinColumn - throw new ColumnNotFoundException("Join column #{expr.table}:#{expr.joins[0]} not found") - - # Determine which column join is to - toTable = if joinColumn.type == "join" then joinColumn.join.toTable else joinColumn.idTable - - # Generate a consistent, semi-unique alias - alias = generateAlias(expr, 0) - - where = @compileJoin(table, joinColumn, tableAlias, alias) - - from = @compileTable(toTable, alias) - - # We are now at j1, which is the to of the first join - table = toTable - tableAlias = alias - - # Perform remaining joins - if expr.joins.length > 1 - for i in [1...expr.joins.length] - joinColumn = @schema.getColumn(table, expr.joins[i]) - if not joinColumn - throw new ColumnNotFoundException("Join column #{table}:#{expr.joins[i]} not found") - - # Determine which column join is to - toTable = if joinColumn.type == "join" then joinColumn.join.toTable else joinColumn.idTable - - # Generate a consistent, semi-unique alias - nextAlias = generateAlias(expr, i) - - onClause = @compileJoin(table, joinColumn, tableAlias, nextAlias) - - from = { - type: "join" - left: from - right: @compileTable(toTable, nextAlias) - kind: "inner" - on: onClause - } - - # We are now at jn - table = toTable - tableAlias = nextAlias - - # Compile where clause - if expr.where - extraWhere = @compileExpr(expr: expr.where, tableAlias: tableAlias) - - # Add to existing - if where - where = { type: "op", op: "and", exprs: [where, extraWhere]} - else - where = extraWhere - - scalarExpr = @compileExpr(expr: expr.expr, tableAlias: tableAlias) - - # Aggregate DEPRECATED - if expr.aggr - switch expr.aggr - when "last" - # Get ordering - ordering = @schema.getTable(table).ordering - if not ordering - throw new Error("No ordering defined") - - # order descending - orderBy = [{ expr: @compileFieldExpr(expr: { type: "field", table: table, column: ordering}, tableAlias: tableAlias), direction: "desc" }] - when "sum", "count", "avg", "max", "min", "stdev", "stdevp" - # Don't include scalarExpr if null - if not scalarExpr - scalarExpr = { type: "op", op: expr.aggr, exprs: [] } - else - scalarExpr = { type: "op", op: expr.aggr, exprs: [scalarExpr] } - else - throw new Error("Unknown aggregation #{expr.aggr}") - - # If no expr, return null - if not scalarExpr - return null - - # If no where, from, orderBy or limit, just return expr for simplicity - if not from and not where and not orderBy - return scalarExpr - - # Create scalar - scalar = { - type: "scalar" - expr: scalarExpr - limit: 1 - } - - if from - scalar.from = from - - if where - scalar.where = where - - if orderBy - scalar.orderBy = orderBy - - return scalar - - # Compile a join into an on or where clause - # fromTableID: column definition - # joinColumn: column definition - # fromAlias: alias of from table - # toAlias: alias of to table - compileJoin: (fromTableId, joinColumn, fromAlias, toAlias) -> - # For join columns - if joinColumn.type == "join" - if joinColumn.join.jsonql - return injectTableAliases(joinColumn.join.jsonql, { "{from}": fromAlias, "{to}": toAlias }) - else - # Use manual columns - return { - type: "op", op: "=" - exprs: [ - @compileColumnRef(joinColumn.join.toColumn, toAlias) - @compileColumnRef(joinColumn.join.fromColumn, fromAlias) - ] - } - else if joinColumn.type == "id" - # Get to table - toTable = @schema.getTable(joinColumn.idTable) - - # Create equal - return { - type: "op", op: "=" - exprs: [ - @compileFieldExpr(expr: { type: "field", table: fromTableId, column: joinColumn.id }, tableAlias: fromAlias) - { type: "field", tableAlias: toAlias, column: toTable.primaryKey } - ] - } - else if joinColumn.type == "id[]" - # Get to table - toTable = @schema.getTable(joinColumn.idTable) - - # Create equal - return { - type: "op", op: "=", modifier: "any", - exprs: [ - { type: "field", tableAlias: toAlias, column: toTable.primaryKey } - { - type: "scalar", - expr: { type: "op", op: "unnest", exprs: [ - @compileFieldExpr(expr: { type: "field", table: fromTableId, column: joinColumn.id }, tableAlias: fromAlias) - ]} - } - ] - } - else - throw new Error("Invalid join column type #{joinColumn.type}") - - # Compile an expression. Pass expr and tableAlias. - compileOpExpr: (options) -> - exprUtils = new ExprUtils(@schema) - - expr = options.expr - - compiledExprs = _.map(expr.exprs, (e) => @compileExpr(expr: e, tableAlias: options.tableAlias)) - - # Get type of expr 0 - expr0Type = exprUtils.getExprType(expr.exprs[0]) - - # Handle multi - switch expr.op - when "and", "or" - # Strip nulls - compiledExprs = _.compact(compiledExprs) - if compiledExprs.length == 0 - return null - - return { - type: "op" - op: expr.op - exprs: compiledExprs - } - when "*" - # Strip nulls - compiledExprs = _.compact(compiledExprs) - if compiledExprs.length == 0 - return null - - # Cast to decimal before multiplying to prevent integer overflow - return { - type: "op" - op: expr.op - exprs: _.map(compiledExprs, (e) -> { type: "op", op: "::decimal", exprs: [e] }) - } - when "+" - # Strip nulls - compiledExprs = _.compact(compiledExprs) - if compiledExprs.length == 0 - return null - - # Cast to decimal before adding to prevent integer overflow. Do cast on internal expr to prevent coalesce mismatch - return { - type: "op" - op: expr.op - exprs: _.map(compiledExprs, (e) -> { type: "op", op: "coalesce", exprs: [{ type: "op", op: "::decimal", exprs: [e] }, 0] }) - } - when "-" - # Null if any not present - if _.any(compiledExprs, (ce) -> not ce?) - return null - - # Cast to decimal before subtracting to prevent integer overflow - return { - type: "op" - op: expr.op - exprs: _.map(compiledExprs, (e) -> { type: "op", op: "::decimal", exprs: [e] }) - } - when ">", "<", ">=", "<=", "<>", "=", "~*", "round", "floor", "ceiling", "sum", "avg", "min", "max", "count", "stdev", "stdevp", "var", "varp", "array_agg" - # Null if any not present - if _.any(compiledExprs, (ce) -> not ce?) - return null - - return { - type: "op" - op: expr.op - exprs: compiledExprs - } - when "least", "greatest" - return { - type: "op" - op: expr.op - exprs: compiledExprs - } - when "/" - # Null if any not present - if _.any(compiledExprs, (ce) -> not ce?) - return null - - # Cast to decimal before dividing to prevent integer math - return { - type: "op" - op: expr.op - exprs: [ - compiledExprs[0] - { type: "op", op: "::decimal", exprs: [{ type: "op", op: "nullif", exprs: [compiledExprs[1], 0] }] } - ] - } - when "last" - # Null if not present - if not compiledExprs[0] - return null - - # Get ordering - ordering = @schema.getTable(expr.table)?.ordering - if not ordering - throw new Error("Table #{expr.table} must be ordered to use last()") - - # (array_agg(xyz order by theordering desc nulls last))[1] - return { - type: "op" - op: "[]" - exprs: [ - { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: options.tableAlias), direction: "desc", nulls: "last" }] } - 1 - ] - } - - when "last where" - # Null if not value present - if not compiledExprs[0] - return null - - # Get ordering - ordering = @schema.getTable(expr.table)?.ordering - if not ordering - throw new Error("Table #{expr.table} must be ordered to use last()") - - # Simple last if not condition present - if not compiledExprs[1] - # (array_agg(xyz order by theordering desc nulls last))[1] - return { - type: "op" - op: "[]" - exprs: [ - { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: options.tableAlias), direction: "desc", nulls: "last" }] } - 1 - ] - } - - # Compiles to: - # (array_agg((case when then else null end) order by (case when then 0 else 1 end), desc nulls last))[1] - # which prevents non-matching from appearing - return { - type: "op" - op: "[]" - exprs: [ - { - type: "op" - op: "array_agg" - exprs: [ - { type: "case", cases: [{ when: compiledExprs[1], then: compiledExprs[0] }], else: null } - ] - orderBy: [ - { expr: { type: "case", cases: [{ when: compiledExprs[1], then: 0 }], else: 1 } } - { expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: options.tableAlias), direction: "desc", nulls: "last" } - ] - } - 1 - ] - } - - when "previous" - # Null if not present - if not compiledExprs[0] - return null - - # Get ordering - ordering = @schema.getTable(expr.table)?.ordering - if not ordering - throw new Error("Table #{expr.table} must be ordered to use previous()") - - # (array_agg(xyz order by theordering desc nulls last))[2] - return { - type: "op" - op: "[]" - exprs: [ - { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering }, tableAlias: options.tableAlias), direction: "desc", nulls: "last" }] } - 2 - ] - } - - when "first" - # Null if not present - if not compiledExprs[0] - return null - - # Get ordering - ordering = @schema.getTable(expr.table)?.ordering - if not ordering - throw new Error("Table #{expr.table} must be ordered to use first()") - - # (array_agg(xyz order by theordering asc nulls last))[1] - return { - type: "op" - op: "[]" - exprs: [ - { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: options.tableAlias), direction: "asc", nulls: "last" }] } - 1 - ] - } - - when "first where" - # Null if not value present - if not compiledExprs[0] - return null - - # Get ordering - ordering = @schema.getTable(expr.table)?.ordering - if not ordering - throw new Error("Table #{expr.table} must be ordered to use first where()") - - # Simple first if not condition present - if not compiledExprs[1] - # (array_agg(xyz order by theordering asc nulls last))[1] - return { - type: "op" - op: "[]" - exprs: [ - { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: options.tableAlias), direction: "asc", nulls: "last" }] } - 1 - ] - } - - # Compiles to: - # (array_agg((case when then else null end) order by (case when then 0 else 1 end), asc nulls last))[1] - # which prevents non-matching from appearing - return { - type: "op" - op: "[]" - exprs: [ - { - type: "op" - op: "array_agg" - exprs: [ - { type: "case", cases: [{ when: compiledExprs[1], then: compiledExprs[0] }], else: null } - ] - orderBy: [ - { expr: { type: "case", cases: [{ when: compiledExprs[1], then: 0 }], else: 1 } } - { expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: options.tableAlias), direction: "asc", nulls: "last" } - ] - } - 1 - ] - } - - when '= any' - # Null if any not present - if _.any(compiledExprs, (ce) -> not ce?) - return null - - # False if empty list on rhs - if expr.exprs[1].type == "literal" - if not expr.exprs[1].value or (_.isArray(expr.exprs[1].value) and expr.exprs[1].value.length == 0) - return false - - return { type: "op", op: "=", modifier: "any", exprs: compiledExprs } - - when "between" - # Null if first not present - if not compiledExprs[0] - return null - - # Null if second and third not present - if not compiledExprs[1] and not compiledExprs[2] - return null - - # >= if third missing - if not compiledExprs[2] - return { - type: "op" - op: ">=" - exprs: [compiledExprs[0], compiledExprs[1]] - } - - # <= if second missing - if not compiledExprs[1] - return { - type: "op" - op: "<=" - exprs: [compiledExprs[0], compiledExprs[2]] - } - - # Between - return { - type: "op" - op: "between" - exprs: compiledExprs - } - - when "not" - if not compiledExprs[0] - return null - - return { - type: "op" - op: expr.op - exprs: [ - { type: "op", op: "coalesce", exprs: [compiledExprs[0], false] } - ] - } - - when "is null", "is not null" - if not compiledExprs[0] - return null - - return { - type: "op" - op: expr.op - exprs: compiledExprs - } - - when "contains" - # Null if either not present - if not compiledExprs[0] or not compiledExprs[1] - return null - - # Null if no expressions in literal list - if compiledExprs[1].type == "literal" and compiledExprs[1].value.length == 0 - return null - - # Cast both to jsonb and use @>. Also convert both to json first to handle literal arrays - return { - type: "op" - op: "@>" - exprs: [ - convertToJsonB(compiledExprs[0]) - convertToJsonB(compiledExprs[1]) - ] - } - - when "intersects" - # Null if either not present - if not compiledExprs[0] or not compiledExprs[1] - return null - - # Null if no expressions in literal list - if compiledExprs[1].type == "literal" and compiledExprs[1].value.length == 0 - return null - - # Cast to jsonb and use ?| Also convert to json first to handle literal arrays - return { - type: "op" - op: "?|" - exprs: [ - convertToJsonB(compiledExprs[0]) - compiledExprs[1] - ] - } - - when "length" - # 0 if null - if not compiledExprs[0]? - return 0 - - # Cast both to jsonb and use jsonb_array_length. Also convert both to json first to handle literal arrays. Coalesce to 0 so that null is 0 - return { - type: "op" - op: "coalesce", - exprs: [ - { - type: "op" - op: "jsonb_array_length" - exprs: [ - convertToJsonB(compiledExprs[0]) - ] - } - 0 - ] - } - - when "line length" - # null if null - if not compiledExprs[0]? - return null - - # ST_Length_Spheroid(ST_Transform(location,4326::integer), 'SPHEROID["GRS_1980",6378137,298.257222101]'::spheroid) - return { - type: "op" - op: "ST_LengthSpheroid", - exprs: [ - { - type: "op" - op: "ST_Transform" - exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] - } - { type: "op", op: "::spheroid", exprs: ['SPHEROID["GRS_1980",6378137,298.257222101]' ]} - ] - } - - when "to text" - # Null if not present - if not compiledExprs[0] - return null - - if exprUtils.getExprType(expr.exprs[0]) == "enum" - # Null if no enum values - enumValues = exprUtils.getExprEnumValues(expr.exprs[0]) - if not enumValues - return null - - return { - type: "case" - input: compiledExprs[0] - cases: _.map(enumValues, (ev) => - { - when: { type: "literal", value: ev.id } - then: { type: "literal", value: exprUtils.localizeString(ev.name, expr.locale) } - }) - } - - if exprUtils.getExprType(expr.exprs[0]) == "number" - return { - type: "op" - op: "::text" - exprs: [compiledExprs[0]] - } - - return null - - when "to date" - # Null if not present - if not compiledExprs[0] - return null - - return { - type: "op" - op: "substr" - exprs: [ - compiledExprs[0] - 1 - 10 - ] - } - - when "count where" - # Null if not present - if not compiledExprs[0] - return null - - return { - type: "op" - op: "coalesce" - exprs: [ - { - type: "op" - op: "sum" - exprs: [ - { - type: "case" - cases: [ - when: compiledExprs[0] - then: 1 - ] - else: 0 - } - ] - } - 0 - ] - } - - when "percent where" - # Null if not present - if not compiledExprs[0] - return null - - # Compiles as sum(case when cond [and basis (if present)] then 100::decimal else 0 end)/sum(1 [or case when basis then 1 else 0 (if present)]) (prevent div by zero) - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "sum" - exprs: [ - { - type: "case" - cases: [ - when: if compiledExprs[1] then { type: "op", op: "and", exprs: [compiledExprs[0], compiledExprs[1]] } else compiledExprs[0] - then: { type: "op", op: "::decimal", exprs: [100] } - ] - else: 0 - } - ] - } - if compiledExprs[1] - { - type: "op" - op: "nullif" - exprs: [ - { - type: "op" - op: "sum" - exprs: [ - { - type: "case" - cases: [ - when: compiledExprs[1] - then: 1 - ] - else: 0 - } - ] - } - 0 - ] - } - else - { type: "op", op: "sum", exprs: [1] } - ] - } - - when "sum where" - # Null if not present - if not compiledExprs[0] - return null - - # Simple sum if not specified where - if not compiledExprs[1] - return { - type: "op" - op: "sum" - exprs: [compiledExprs[0]] - } - - return { - type: "op" - op: "sum" - exprs: [ - { - type: "case" - cases: [ - when: compiledExprs[1] - then: compiledExprs[0] - ] - else: 0 - } - ] - } - - when "min where" - # Null if not present - if not compiledExprs[0] - return null - - # Simple min if not specified where - if not compiledExprs[1] - return { - type: "op" - op: "min" - exprs: [compiledExprs[0]] - } - - return { - type: "op" - op: "min" - exprs: [ - { - type: "case" - cases: [ - when: compiledExprs[1] - then: compiledExprs[0] - ] - else: null - } - ] - } - - when "max where" - # Null if not present - if not compiledExprs[0] - return null - - # Simple max if not specified where - if not compiledExprs[1] - return { - type: "op" - op: "max" - exprs: [compiledExprs[0]] - } - - return { - type: "op" - op: "max" - exprs: [ - { - type: "case" - cases: [ - when: compiledExprs[1] - then: compiledExprs[0] - ] - else: null - } - ] - } - - when "count distinct" - # Null if not present - if not compiledExprs[0] - return null - - return { - type: "op" - op: "count" - exprs: [compiledExprs[0]] - modifier: "distinct" - } - - when "percent" - # Compiles as count(*) * 100::decimal / sum(count(*)) over() - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "*" - exprs: [ - { type: "op", op: "count", exprs: [] } - { type: "op", op: "::decimal", exprs: [100] } - ] - } - { - type: "op" - op: "sum" - exprs: [ - { type: "op", op: "count", exprs: [] } - ] - over: {} - } - ] - } - - # Hierarchical test that uses ancestry column - when "within" - # Null if either not present - if not compiledExprs[0] or not compiledExprs[1] - return null - - # Get table being used - idTable = exprUtils.getExprIdTable(expr.exprs[0]) - - # Prefer ancestryTable - if @schema.getTable(idTable).ancestryTable - # exists (select null from as subwithin where ancestor = compiledExprs[1] and descendant = compiledExprs[0]) - return { - type: "op" - op: "exists" - exprs: [ - { - type: "scalar" - expr: null - from: { type: "table", table: @schema.getTable(idTable).ancestryTable, alias: "subwithin" } - where: { - type: "op" - op: "and" - exprs: [ - { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "ancestor" }, compiledExprs[1]]} - { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "descendant" }, compiledExprs[0]]} - ] - } - } - ] - } - - return { - type: "op" - op: "in" - exprs: [ - compiledExprs[0] - { - type: "scalar" - expr: @compileColumnRef(@schema.getTable(idTable).primaryKey, "subwithin") - from: { type: "table", table: idTable, alias: "subwithin" } - where: { - type: "op" - op: "@>" - exprs: [ - { type: "field", tableAlias: "subwithin", column: @schema.getTable(idTable).ancestry } - { type: "op", op: "::jsonb", exprs: [{ type: "op", op: "json_build_array", exprs: [compiledExprs[1]] }] } - ] - } - } - ] - } - - # Hierarchical test that uses ancestry column - when "within any" - # Null if either not present - if not compiledExprs[0] or not compiledExprs[1] - return null - - # Get table being used - idTable = exprUtils.getExprIdTable(expr.exprs[0]) - - # Prefer ancestryTable - if @schema.getTable(idTable).ancestryTable - # exists (select null from as subwithin where ancestor = any(compiledExprs[1]) and descendant = compiledExprs[0]) - return { - type: "op" - op: "exists" - exprs: [ - { - type: "scalar" - expr: null - from: { type: "table", table: @schema.getTable(idTable).ancestryTable, alias: "subwithin" } - where: { - type: "op" - op: "and" - exprs: [ - { type: "op", op: "=", modifier: "any", exprs: [{ type: "field", tableAlias: "subwithin", column: "ancestor" }, compiledExprs[1]]} - { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "descendant" }, compiledExprs[0]]} - ] - } - } - ] - } - - # This older code fails now that admin_regions uses integer pk. Replaced with literal-only code - # return { - # type: "op" - # op: "in" - # exprs: [ - # compiledExprs[0] - # { - # type: "scalar" - # expr: @compileColumnRef(@schema.getTable(idTable).primaryKey, "subwithin") - # from: { type: "table", table: idTable, alias: "subwithin" } - # where: { - # type: "op" - # op: "?|" - # exprs: [ - # { type: "field", tableAlias: "subwithin", column: @schema.getTable(idTable).ancestry } - # compiledExprs[1] - # ] - # } - # } - # ] - # } - - # If not literal, fail - if compiledExprs[1].type != "literal" - throw new Error("Non-literal RHS of within any not supported") - - return { - type: "op" - op: "in" - exprs: [ - compiledExprs[0] - { - type: "scalar" - expr: @compileColumnRef(@schema.getTable(idTable).primaryKey, "subwithin") - from: { type: "table", table: idTable, alias: "subwithin" } - where: { - type: "op" - op: "?|" - exprs: [ - { type: "field", tableAlias: "subwithin", column: @schema.getTable(idTable).ancestryText or @schema.getTable(idTable).ancestry } - { type: "literal", value: _.map(compiledExprs[1].value, (value) => - if _.isNumber(value) - return "" + value - else - return value - ) - } - ] - } - } - ] - } - - when "latitude" - if not compiledExprs[0] - return null - - return { - type: "op" - op: "ST_Y" - exprs: [ - { type: "op", op: "ST_Centroid", exprs: [ - { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } - ] } - ] - } - - when "longitude" - if not compiledExprs[0] - return null - - return { - type: "op" - op: "ST_X" - exprs: [ - { type: "op", op: "ST_Centroid", exprs: [ - { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } - ] } - ] - } - - when 'days difference' - if not compiledExprs[0] or not compiledExprs[1] - return null - - if exprUtils.getExprType(expr.exprs[0]) == "datetime" or exprUtils.getExprType(expr.exprs[1]) == "datetime" - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] } - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } - ] - } - 86400 - ] - } - - if exprUtils.getExprType(expr.exprs[0]) == "date" - return { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - { type: "op", op: "::date", exprs: [compiledExprs[1]] } - ] - } - - return null - - when 'months difference' - if not compiledExprs[0] or not compiledExprs[1] - return null - - if exprUtils.getExprType(expr.exprs[0]) == "datetime" or exprUtils.getExprType(expr.exprs[1]) == "datetime" - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] } - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } - ] - } - 86400 * 30.5 - ] - } - - if exprUtils.getExprType(expr.exprs[0]) == "date" - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - { type: "op", op: "::date", exprs: [compiledExprs[1]] } - ] - } - 30.5 - ] - } - - return null - - when 'years difference' - if not compiledExprs[0] or not compiledExprs[1] - return null - - if exprUtils.getExprType(expr.exprs[0]) == "datetime" or exprUtils.getExprType(expr.exprs[1]) == "datetime" - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] } - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } - ] - } - 86400 * 365 - ] - } - - if exprUtils.getExprType(expr.exprs[0]) == "date" - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - { type: "op", op: "::date", exprs: [compiledExprs[1]] } - ] - } - 365 - ] - } - - return null - - when 'days since' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "::date", exprs: [moment().format("YYYY-MM-DD")] } - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - ] - } - when "datetime" - return { - type: "op" - op: "/" - exprs: [ - { - type: "op" - op: "-" - exprs: [ - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [nowExpr] }] } - { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] } - ] - } - 86400 - ] - } - else - return null - - when 'month' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "substr" - exprs: [ - compiledExprs[0] - 6 - 2 - ] - } - - when 'yearmonth' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "rpad" - exprs: [ - { type: "op", op: "substr", exprs: [compiledExprs[0], 1, 7] } - 10 - "-01" - ] - } - - when 'yearquarter' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "to_char" - exprs: [ - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - "YYYY-Q" - ] - } - - when 'yearweek' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "to_char" - exprs: [ - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - "IYYY-IW" - ] - } - - when 'weekofyear' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "to_char" - exprs: [ - { type: "op", op: "::date", exprs: [compiledExprs[0]] } - "IW" - ] - } - - when 'year' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "rpad" - exprs: [ - { type: "op", op: "substr", exprs: [compiledExprs[0], 1, 4] } - 10 - "-01-01" - ] - } - - when 'weekofmonth' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "to_char" - exprs: [ - { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] } - "W" - ] - } - - when 'dayofmonth' - if not compiledExprs[0] - return null - - return { - type: "op" - op: "to_char" - exprs: [ - { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] } - "DD" - ] - } - - when 'thisyear' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").add(1, 'years').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").add(1, 'years').toISOString() ] } - ] - } - else - return null - - when 'lastyear' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").subtract(1, 'years').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").subtract(1, 'years').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").toISOString() ] } - ] - } - else - return null - - when 'thismonth' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").add(1, 'months').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").add(1, 'months').toISOString() ] } - ] - } - else - return null - - when 'lastmonth' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").subtract(1, 'months').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").subtract(1, 'months').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").toISOString() ] } - ] - } - else - return null - - when 'today' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'yesterday' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(1, 'days').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(1, 'days').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").toISOString() ] } - ] - } - else - return null - - when 'last24hours' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(1, 'days').format("YYYY-MM-DD") ] } - { type: "op", op: "<=", exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], nowMinus24HoursExpr] } - { type: "op", op: "<=", exprs: [compiledExprs[0], nowExpr] } - ] - } - else - return null - - when 'last7days' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(7, 'days').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(7, 'days').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'last30days' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(30, 'days').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(30, 'days').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'last365days' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(365, 'days').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(365, 'days').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'last12months' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(11, "months").startOf('month').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(11, "months").startOf('month').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'last6months' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(5, "months").startOf('month').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(5, "months").startOf('month').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'last3months' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(2, "months").startOf('month').format("YYYY-MM-DD") ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } - ] - } - when "datetime" - return { - type: "op" - op: "and" - exprs: [ - { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(2, "months").startOf('month').toISOString() ] } - { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } - ] - } - else - return null - - when 'future' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op", - op: ">", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] - } - when "datetime" - return { - type: "op", - op: ">", - exprs: [compiledExprs[0], nowExpr] - } - else - return null - - when 'notfuture' - if not compiledExprs[0] - return null - - switch expr0Type - when "date" - return { - type: "op", - op: "<=", - exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] - } - when "datetime" - return { - type: "op", - op: "<=", - exprs: [compiledExprs[0], nowExpr] - } - else - return null - - when 'current date' - return { type: "literal", value: moment().format("YYYY-MM-DD") } - - when 'current datetime' - return { type: "literal", value: moment().toISOString() } - - when 'distance' - if not compiledExprs[0] or not compiledExprs[1] - return null - - return { - type: "op" - op: "ST_DistanceSphere" - exprs: [ - { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } - { type: "op", op: "ST_Transform", exprs: [compiledExprs[1], { type: "op", op: "::integer", exprs: [4326] }] } - ] - } - - when 'is latest' - lhsCompiled = @compileExpr(expr: expr.exprs[0], tableAlias: "innerrn") - if not lhsCompiled - return null - - filterCompiled = @compileExpr(expr: expr.exprs[1], tableAlias: "innerrn") - - # Get ordering - ordering = @schema.getTable(expr.table).ordering - if not ordering - throw new Error("No ordering defined") - - # order descending - orderBy = [{ expr: @compileFieldExpr(expr: { type: "field", table: expr.table, column: ordering}, tableAlias: "innerrn"), direction: "desc" }] - - # _id in (select outerrn.id from (select innerrn.id, row_number() over (partition by EXPR1 order by ORDERING desc) as rn from the_table as innerrn where filter) as outerrn where outerrn.rn = 1) - - # Create innerrn query - innerrnQuery = { - type: "query" - selects: [ - { type: "select", expr: @compileExpr(expr: { type: "id", table: expr.table }, tableAlias: "innerrn" ), alias: "id" } - { - type: "select" - expr: { - type: "op" - op: "row_number" - exprs: [] - over: { - partitionBy: [lhsCompiled] - orderBy: orderBy - } - } - alias: "rn" - } - ] - from: { type: "table", table: expr.table, alias: "innerrn" } - } - if filterCompiled - innerrnQuery.where = filterCompiled - - # Wrap in outer query - outerrnQuery = { - type: "scalar" - expr: { type: "field", tableAlias: "outerrn", column: "id" } - from: { - type: "subquery" - query: innerrnQuery - alias: "outerrn" - } - where: { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "outerrn", column: "rn" }, 1]} - } - - return { - type: "op" - op: "in" - exprs: [ - @compileExpr(expr: { type: "id", table: expr.table }, tableAlias: options.tableAlias) - outerrnQuery - ] - } - - else - throw new Error("Unknown op #{expr.op}") - - - compileCaseExpr: (options) -> - expr = options.expr - - compiled = { - type: "case" - cases: _.map(expr.cases, (c) => - { - when: @compileExpr(expr: c.when, tableAlias: options.tableAlias) - then: @compileExpr(expr: c.then, tableAlias: options.tableAlias) - }) - else: @compileExpr(expr: expr.else, tableAlias: options.tableAlias) - } - - # Remove null cases - compiled.cases = _.filter(compiled.cases, (c) -> c.when?) - - # Return null if no cases - if compiled.cases.length == 0 - return null - - return compiled - - compileScoreExpr: (options) -> - expr = options.expr - exprUtils = new ExprUtils(@schema) - - # If empty, literal 0 - if _.isEmpty(expr.scores) - return { type: "literal", value: 0 } - - # Get type of input - inputType = exprUtils.getExprType(expr.input) - - switch inputType - when "enum" - return { - type: "case" - input: @compileExpr(expr: expr.input, tableAlias: options.tableAlias) - cases: _.map(_.pairs(expr.scores), (pair) => - { - when: { type: "literal", value: pair[0] } - then: @compileExpr(expr: pair[1], tableAlias: options.tableAlias) - } - ) - else: { type: "literal", value: 0 } - } - when "enumset" - return { - type: "op" - op: "+" - exprs: _.map(_.pairs(expr.scores), (pair) => - { - type: "case" - cases: [ - { - when: { - type: "op" - op: "@>" - exprs: [ - convertToJsonB(@compileExpr(expr: expr.input, tableAlias: options.tableAlias)) - convertToJsonB({ type: "literal", value: [pair[0]] }) - ] - } - then: @compileExpr(expr: pair[1], tableAlias: options.tableAlias) - } - ] - else: { type: "literal", value: 0 } - } - ) - } - - # Null if no expression - else - return null - - compileBuildEnumsetExpr: (options) -> - # Create enumset - # select to_jsonb(array_agg(bes.v)) from (select (case when true then 'x' end) as v union all select (case when true then 'y' end) as v ...) as bes where v is not null - - expr = options.expr - - # Handle empty case - if _.keys(expr.values).length == 0 - return null - - return { - type: "scalar" - expr: { - type: "op" - op: "to_jsonb" - exprs: [ - { - type: "op" - op: "array_agg" - exprs: [{ type: "field", tableAlias: "bes", column: "v" }] - } - ] - } - from: { - type: "subquery" - alias: "bes" - query: { - type: "union all" - queries: _.map _.pairs(expr.values), (pair) => - { - type: "query" - selects: [ - { - type: "select" - expr: { - type: "case" - cases: [{ when: @compileExpr(expr: pair[1], tableAlias: options.tableAlias), then: pair[0] }] - } - alias: "v" - } - ] - } - } - } - - where: { - type: "op" - op: "is not null" - exprs: [{ type: "field", tableAlias: "bes", column: "v" }] - } - } - - compileComparisonExpr: (options) -> - expr = options.expr - exprUtils = new ExprUtils(@schema) - - # Missing left-hand side type means null condition - exprLhsType = exprUtils.getExprType(expr.lhs) - if not exprLhsType - return null - - # Missing right-hand side means null condition - if exprUtils.getComparisonRhsType(exprLhsType, expr.op) and not expr.rhs? - return null - - lhsExpr = @compileExpr(expr: expr.lhs, tableAlias: options.tableAlias) - if expr.rhs - rhsExpr = @compileExpr(expr: expr.rhs, tableAlias: options.tableAlias) - exprs = [lhsExpr, rhsExpr] - else - exprs = [lhsExpr] - - # Handle special cases - switch expr.op - when '= true' - return { type: "op", op: "=", exprs: [lhsExpr, { type: "literal", value: true }]} - when '= false' - return { type: "op", op: "=", exprs: [lhsExpr, { type: "literal", value: false }]} - when '= any' - return { type: "op", op: "=", modifier: "any", exprs: exprs } - when 'between' - return { type: "op", op: "between", exprs: [lhsExpr, { type: "literal", value: expr.rhs.value[0] }, { type: "literal", value: expr.rhs.value[1] }] } - else - return { - type: "op" - op: expr.op - exprs: exprs - } - - compileLogicalExpr: (options) -> - expr = options.expr - - compiledExprs = _.map(expr.exprs, (e) => @compileExpr(expr: e, tableAlias: options.tableAlias)) - - # Remove nulls - compiledExprs = _.compact(compiledExprs) - - # Simplify - if compiledExprs.length == 1 - return compiledExprs[0] - - if compiledExprs.length == 0 - return null - - return { - type: "op" - op: expr.op - exprs: compiledExprs - } - - # Compiles a reference to a column or a JsonQL expression - # If parameter is a string, create a simple field expression - # If parameter is an object, inject tableAlias for `{alias}` - compileColumnRef: (column, tableAlias) -> - if _.isString(column) - return { type: "field", tableAlias: tableAlias, column: column } - - return injectTableAlias(column, tableAlias) - - # Compiles a table, substituting with custom jsonql if required - compileTable: (tableId, alias) -> - table = @schema.getTable(tableId) - if not table - throw new Error("Table #{tableId} not found") - - if not table.jsonql - return { type: "table", table: tableId, alias: alias } - else - return { type: "subquery", query: table.jsonql, alias: alias } - - compileVariableExpr: (options) -> - # Get variable - variable = _.findWhere(@variables, id: options.expr.variableId) - if not variable - throw new Error("Variable #{options.expr.variableId} not found") - - # Get value (which is always an expression) - value = @variableValues[variable.id] - - # If expression, compile - if value? - return @compileExpr({ expr: value, tableAlias: options.tableAlias }) - else - return null - -# Converts a compiled expression to jsonb. Literals cannot use to_jsonb as they will -# trigger "could not determine polymorphic type because input has type unknown" unless the -# SQL is inlined -convertToJsonB = (compiledExpr) -> - if not compiledExpr - return compiledExpr - - # Literals are special and are cast to jsonb from a JSON string - if compiledExpr.type == "literal" - return { type: "op", op: "::jsonb", exprs: [{ type: "literal", value: JSON.stringify(compiledExpr.value) }] } - - # First convert using to_jsonb in case is array - return { type: "op", op: "to_jsonb", exprs: [compiledExpr] } \ No newline at end of file diff --git a/src/ExprCompiler.d.ts b/src/ExprCompiler.d.ts deleted file mode 100644 index c49b437..0000000 --- a/src/ExprCompiler.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { JsonQLExpr, JsonQLFrom } from "jsonql"; -import Schema from "./Schema"; -import { Expr, Variable } from "./types"; - -export default class ExprCompiler { - constructor(schema: Schema, variables?: Variable[], variableValues?: { [variableId: string]: Expr }) - - compileExpr(options: { expr: Expr, tableAlias: string }): JsonQLExpr - compileTable(table: string, alias: string): JsonQLFrom -} diff --git a/src/ExprCompiler.ts b/src/ExprCompiler.ts new file mode 100644 index 0000000..df9feb8 --- /dev/null +++ b/src/ExprCompiler.ts @@ -0,0 +1,2277 @@ +import { JsonQLCase, JsonQLExpr, JsonQLFrom, JsonQLLiteral, JsonQLQuery, JsonQLScalar, JsonQLSelectQuery, JsonQLTableFrom } from "jsonql"; +import _ from "lodash"; +import moment from "moment"; +import ColumnNotFoundException from "./ColumnNotFoundException"; +import ExprUtils from "./ExprUtils"; +import { getExprExtension } from "./extensions"; +import { injectTableAlias, injectTableAliases } from "./injectTableAliases"; +import Schema from "./Schema"; +import { BuildEnumsetExpr, CaseExpr, Column, Expr, FieldExpr, LegacyComparisonExpr, LegacyLogicalExpr, LiteralExpr, OpExpr, ScalarExpr, ScoreExpr, Variable, VariableExpr } from "./types"; + +// now expression: (to_json(now() at time zone 'UTC')#>>'{}') +const nowExpr: JsonQLExpr = { + type: "op", + op: "#>>", + exprs: [ + { type: "op", op: "to_json", exprs: [ + { type: "op", op: "at time zone", exprs: [ + { type: "op", op: "now", exprs: [] }, + "UTC" + ]} + ]}, + "{}" + ] +}; + +// now 24 hours ago: (to_json((now() - interval '24 hour') at time zone 'UTC')#>>'{}') +const nowMinus24HoursExpr: JsonQLExpr = { + type: "op", + op: "#>>", + exprs: [ + { type: "op", op: "to_json", exprs: [ + { type: "op", op: "at time zone", exprs: [ + { type: "op", op: "-", exprs: [{ type: "op", op: "now", exprs: [] }, { type: "op", op: "interval", exprs: [{ type: "literal", value: "24 hour" }]}] }, + "UTC" + ]} + ]}, + "{}" + ] +}; + + +/** Compiles expressions to JsonQL. Assumes that geometry is in Webmercator (3857) */ +export default class ExprCompiler { + schema: Schema + variables: Variable[] + variableValues: { [variableId: string]: Expr } + + // Variable values are lookup of id to variable value, which is always an expression + constructor(schema: Schema, variables?: Variable[], variableValues?: { [variableId: string]: Expr }) { + this.schema = schema + this.variables = variables || [] + this.variableValues = variableValues || {} + } + + /** Compile an expression. Pass expr and tableAlias. */ + compileExpr(options: { expr: Expr, tableAlias: string }): JsonQLExpr { + const { expr, tableAlias } = options + + // Handle null + if (!expr) { + return null + } + + switch (expr.type) { + case "id": + return this.compileColumnRef(this.schema.getTable(expr.table)!.primaryKey, options.tableAlias) + case "field": + return this.compileFieldExpr({ expr, tableAlias }) + case "scalar": + return this.compileScalarExpr({ expr, tableAlias }) + case "literal": + if (expr.value != null) { + return { type: "literal", value: expr.value } + } else { + return null; + } + case "op": + return this.compileOpExpr({ expr, tableAlias }) + case "case": + return this.compileCaseExpr({ expr, tableAlias }) + case "score": + return this.compileScoreExpr({ expr, tableAlias }) + case "build enumset": + return this.compileBuildEnumsetExpr({ expr, tableAlias }) + case "variable": + return this.compileVariableExpr({ expr, tableAlias }) + case "extension": + return getExprExtension(expr.extension).compileExpr(expr, tableAlias, this.schema, this.variables, this.variableValues); + case "count": // DEPRECATED + return null; + case "comparison": // DEPRECATED + return this.compileComparisonExpr({ expr, tableAlias }); + case "logical": // DEPRECATED + return this.compileLogicalExpr({ expr, tableAlias }); + default: + throw new Error(`Expr type ${(expr as any).type} not supported`); + } + } + + /** Compile a field expressions */ + compileFieldExpr(options: { expr: FieldExpr, tableAlias: string }): JsonQLExpr { + const { expr } = options + + const column = this.schema.getColumn(expr.table, expr.column); + if (!column) { + throw new ColumnNotFoundException(`Column ${expr.table}.${expr.column} not found`); + } + + // Handle joins specially + if (column.type === "join") { + // If id is result + if (['1-1', 'n-1'].includes(column.join!.type)) { + // Use scalar to create + return this.compileScalarExpr({expr: { type: "scalar", table: expr.table, joins: [column.id], expr: { type: "id", table: column.join!.toTable }}, tableAlias: options.tableAlias}); + } else { + return { + type: "scalar", + expr: { + type: "op", + op: "to_jsonb", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [ + this.compileColumnRef(this.schema.getTable(column.join!.toTable)!.primaryKey, "inner") + ] + } + ] + }, + from: this.compileTable(column.join!.toTable, "inner"), + where: this.compileJoin(expr.table, column, options.tableAlias, "inner"), + limit: 1 // Limit 1 to be safe + }; + } + } + + // Handle if has expr + if (column.expr) { + return this.compileExpr({ expr: column.expr, tableAlias: options.tableAlias }); + } + + // If column has custom jsonql, use that instead of id + return this.compileColumnRef(column.jsonql || column.id, options.tableAlias); + } + + compileScalarExpr(options: { expr: ScalarExpr, tableAlias: string }): JsonQLExpr { + let joinColumn, toTable; + const { expr } = options; + + let where = null; + let from: JsonQLFrom | undefined = undefined; + let orderBy: { expr: JsonQLExpr, direction: "asc" | "desc" }[] | undefined = undefined; + + // Null expr is null + if (!expr.expr) { + return null + } + + // Simplify if a join to an id field where the join uses the primary key of the to table + if (!expr.aggr && !expr.where && (expr.joins.length === 1) && (expr.expr.type === "id")) { + const fromColumn = this.schema.getColumn(expr.table, expr.joins[0])! + + if (fromColumn.type === "id") { + return this.compileColumnRef(fromColumn.id, options.tableAlias); + } + if (fromColumn.join && (fromColumn.join.toColumn === this.schema.getTable(expr.expr.table)!.primaryKey)) { + return this.compileColumnRef(fromColumn.join.fromColumn, options.tableAlias); + } + } + + // Generate a consistent, semi-unique alias. Make alias-friendly (replace all symbols with _) + const generateAlias = (expr: ScalarExpr, joinIndex: number) => + expr.joins[joinIndex].replace(/[^a-zA-Z0-9]/g, "_").toLowerCase() + + // Perform joins + let { table } = expr + let { tableAlias } = options + + // First join is in where clause + if (expr.joins && (expr.joins.length > 0)) { + joinColumn = this.schema.getColumn(expr.table, expr.joins[0]); + if (!joinColumn) { + throw new ColumnNotFoundException(`Join column ${expr.table}:${expr.joins[0]} not found`); + } + + // Determine which column join is to + toTable = joinColumn.type === "join" ? joinColumn.join!.toTable : joinColumn.idTable! + + // Generate a consistent, semi-unique alias + const alias = generateAlias(expr, 0); + + where = this.compileJoin(table, joinColumn, tableAlias, alias); + + from = this.compileTable(toTable, alias); + + // We are now at j1, which is the to of the first join + table = toTable + tableAlias = alias + } + + // Perform remaining joins + if (expr.joins.length > 1) { + for (let i = 1, end = expr.joins.length, asc = 1 <= end; asc ? i < end : i > end; asc ? i++ : i--) { + joinColumn = this.schema.getColumn(table, expr.joins[i]); + if (!joinColumn) { + throw new ColumnNotFoundException(`Join column ${table}:${expr.joins[i]} not found`); + } + + // Determine which column join is to + toTable = joinColumn.type === "join" ? joinColumn.join!.toTable : joinColumn.idTable! + + // Generate a consistent, semi-unique alias + const nextAlias = generateAlias(expr, i); + + const onClause = this.compileJoin(table, joinColumn, tableAlias, nextAlias); + + from = { + type: "join", + left: from!, + right: this.compileTable(toTable, nextAlias), + kind: "inner", + on: onClause + }; + + // We are now at jn + table = toTable; + tableAlias = nextAlias; + } + } + + // Compile where clause + if (expr.where) { + const extraWhere = this.compileExpr({expr: expr.where, tableAlias}); + + // Add to existing + if (where) { + where = { type: "op", op: "and", exprs: [where, extraWhere]}; + } else { + where = extraWhere; + } + } + + let scalarExpr = this.compileExpr({expr: expr.expr, tableAlias}); + + // Aggregate DEPRECATED + if (expr.aggr) { + switch (expr.aggr) { + case "last": + // Get ordering + var { ordering } = this.schema.getTable(table)! + if (!ordering) { + throw new Error("No ordering defined"); + } + + // order descending + orderBy = [{ expr: this.compileFieldExpr({expr: { type: "field", table, column: ordering}, tableAlias}), direction: "desc" }] + break; + case "sum": case "count": case "avg": case "max": case "min": case "stdev": case "stdevp": + // Don't include scalarExpr if null + if (!scalarExpr) { + scalarExpr = { type: "op", op: expr.aggr, exprs: [] }; + } else { + scalarExpr = { type: "op", op: expr.aggr, exprs: [scalarExpr] }; + } + break; + default: + throw new Error(`Unknown aggregation ${expr.aggr}`); + } + } + + // If no expr, return null + if (!scalarExpr) { + // TODO extend to include null! + return (null as unknown) as JsonQLExpr + } + + // If no where, from, orderBy or limit, just return expr for simplicity + if (!from && !where && !orderBy) { + return scalarExpr; + } + + // Create scalar + const scalar: JsonQLScalar = { + type: "scalar", + expr: scalarExpr, + limit: 1 + }; + + if (from) { + scalar.from = from; + } + + if (where) { + scalar.where = where; + } + + if (orderBy) { + scalar.orderBy = orderBy; + } + + return scalar; + } + + /** Compile a join into an on or where clause + * fromTableID: column definition + * joinColumn: column definition + * fromAlias: alias of from table + * toAlias: alias of to table + */ + compileJoin(fromTableId: string, joinColumn: Column, fromAlias: string, toAlias: string) { + // For join columns + let toTable; + if (joinColumn.type === "join") { + if (joinColumn.join!.jsonql) { + return injectTableAliases(joinColumn.join!.jsonql, { "{from}": fromAlias, "{to}": toAlias }); + } else { + // Use manual columns + return { + type: "op", op: "=", + exprs: [ + this.compileColumnRef(joinColumn.join!.toColumn, toAlias), + this.compileColumnRef(joinColumn.join!.fromColumn, fromAlias) + ] + }; + } + } else if (joinColumn.type === "id") { + // Get to table + toTable = this.schema.getTable(joinColumn.idTable!)! + + // Create equal + return { + type: "op", op: "=", + exprs: [ + this.compileFieldExpr({expr: { type: "field", table: fromTableId, column: joinColumn.id }, tableAlias: fromAlias}), + { type: "field", tableAlias: toAlias, column: toTable.primaryKey } + ] + }; + } else if (joinColumn.type === "id[]") { + // Get to table + toTable = this.schema.getTable(joinColumn.idTable!)! + + // Create equal + return { + type: "op", op: "=", modifier: "any", + exprs: [ + { type: "field", tableAlias: toAlias, column: toTable.primaryKey }, + { + type: "scalar", + expr: { type: "op", op: "unnest", exprs: [ + this.compileFieldExpr({expr: { type: "field", table: fromTableId, column: joinColumn.id }, tableAlias: fromAlias}) + ]} + } + ] + }; + } else { + throw new Error(`Invalid join column type ${joinColumn.type}`); + } + } + + // Compile an expression. Pass expr and tableAlias. + compileOpExpr(options: { expr: OpExpr, tableAlias: string }): JsonQLExpr { + var ordering: string | undefined + const exprUtils = new ExprUtils(this.schema) + + const { + expr + } = options; + + let compiledExprs = _.map(expr.exprs, e => this.compileExpr({expr: e, tableAlias: options.tableAlias})); + + // Get type of expr 0 + const expr0Type = exprUtils.getExprType(expr.exprs[0]); + + // Handle multi + switch (expr.op) { + case "and": case "or": + // Strip nulls + compiledExprs = _.compact(compiledExprs); + if (compiledExprs.length === 0) { + return null; + } + + return { + type: "op", + op: expr.op, + exprs: compiledExprs + }; + case "*": + // Strip nulls + compiledExprs = _.compact(compiledExprs); + if (compiledExprs.length === 0) { + return null; + } + + // Cast to decimal before multiplying to prevent integer overflow + return { + type: "op", + op: expr.op, + exprs: _.map(compiledExprs, e => ({ + type: "op", + op: "::decimal", + exprs: [e] + })) + }; + case "+": + // Strip nulls + compiledExprs = _.compact(compiledExprs); + if (compiledExprs.length === 0) { + return null; + } + + // Cast to decimal before adding to prevent integer overflow. Do cast on internal expr to prevent coalesce mismatch + return { + type: "op", + op: expr.op, + exprs: _.map(compiledExprs, e => ({ + type: "op", + op: "coalesce", + exprs: [{ type: "op", op: "::decimal", exprs: [e] }, 0] + } as JsonQLExpr)) + }; + case "-": + // Null if any not present + if (_.any(compiledExprs, ce => ce == null)) { + return null; + } + + // Cast to decimal before subtracting to prevent integer overflow + return { + type: "op", + op: expr.op, + exprs: _.map(compiledExprs, e => ({ + type: "op", + op: "::decimal", + exprs: [e] + })) + }; + case ">": case "<": case ">=": case "<=": case "<>": case "=": case "~*": case "round": case "floor": case "ceiling": case "sum": case "avg": case "min": case "max": case "count": case "stdev": case "stdevp": case "var": case "varp": case "array_agg": + // Null if any not present + if (_.any(compiledExprs, ce => ce == null)) { + return null; + } + + return { + type: "op", + op: expr.op, + exprs: compiledExprs + }; + case "least": case "greatest": + return { + type: "op", + op: expr.op, + exprs: compiledExprs + }; + case "/": + // Null if any not present + if (_.any(compiledExprs, ce => ce == null)) { + return null; + } + + // Cast to decimal before dividing to prevent integer math + return { + type: "op", + op: expr.op, + exprs: [ + compiledExprs[0], + { type: "op", op: "::decimal", exprs: [{ type: "op", op: "nullif", exprs: [compiledExprs[1], 0] }] } + ] + }; + case "last": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Get ordering + ordering = this.schema.getTable(expr.table!)!.ordering + if (!ordering) { + throw new Error(`Table ${expr.table} must be ordered to use last()`); + } + + // (array_agg(xyz order by theordering desc nulls last))[1] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: options.tableAlias}), direction: "desc", nulls: "last" }] }, + 1 + ] + } + + case "last where": + // Null if not value present + if (compiledExprs[0] == null) { + return null; + } + + // Get ordering + ordering = this.schema.getTable(expr.table!)!.ordering + if (!ordering) { + throw new Error(`Table ${expr.table} must be ordered to use last()`); + } + + // Simple last if not condition present + if (compiledExprs[1] == null) { + // (array_agg(xyz order by theordering desc nulls last))[1] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: options.tableAlias}), direction: "desc", nulls: "last" }] }, + 1 + ] + }; + } + + // Compiles to: + // (array_agg((case when then else null end) order by (case when then 0 else 1 end), desc nulls last))[1] + // which prevents non-matching from appearing + return { + type: "op", + op: "[]", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [ + { type: "case", cases: [{ when: compiledExprs[1], then: compiledExprs[0] }], else: null } + ], + orderBy: [ + { expr: { type: "case", cases: [{ when: compiledExprs[1], then: 0 }], else: 1 } }, + { expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: options.tableAlias}), direction: "desc", nulls: "last" } + ] + }, + 1 + ] + }; + + case "previous": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Get ordering + ordering = this.schema.getTable(expr.table!)!.ordering + if (!ordering) { + throw new Error(`Table ${expr.table} must be ordered to use previous()`); + } + + // (array_agg(xyz order by theordering desc nulls last))[2] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering }, tableAlias: options.tableAlias}), direction: "desc", nulls: "last" }] }, + 2 + ] + }; + + case "first": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Get ordering + ordering = this.schema.getTable(expr.table!)!.ordering + if (!ordering) { + throw new Error(`Table ${expr.table} must be ordered to use first()`); + } + + // (array_agg(xyz order by theordering asc nulls last))[1] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: options.tableAlias}), direction: "asc", nulls: "last" }] }, + 1 + ] + }; + + case "first where": + // Null if not value present + if (compiledExprs[0] == null) { + return null; + } + + // Get ordering + ordering = this.schema.getTable(expr.table!)!.ordering + if (!ordering) { + throw new Error(`Table ${expr.table} must be ordered to use first where()`); + } + + // Simple first if not condition present + if (compiledExprs[1] == null) { + // (array_agg(xyz order by theordering asc nulls last))[1] + return { + type: "op", + op: "[]", + exprs: [ + { type: "op", op: "array_agg", exprs: [compiledExprs[0]], orderBy: [{ expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: options.tableAlias}), direction: "asc", nulls: "last" }] }, + 1 + ] + }; + } + + // Compiles to: + // (array_agg((case when then else null end) order by (case when then 0 else 1 end), asc nulls last))[1] + // which prevents non-matching from appearing + return { + type: "op", + op: "[]", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [ + { type: "case", cases: [{ when: compiledExprs[1], then: compiledExprs[0] }], else: null } + ], + orderBy: [ + { expr: { type: "case", cases: [{ when: compiledExprs[1], then: 0 }], else: 1 } }, + { expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: options.tableAlias}), direction: "asc", nulls: "last" } + ] + }, + 1 + ] + }; + + case '= any': + // Null if any not present + if (_.any(compiledExprs, ce => ce == null)) { + return null; + } + + // False if empty list on rhs + if (expr.exprs[1]!.type === "literal") { + const rhsLiteral = expr.exprs[1] as LiteralExpr + if (rhsLiteral.value == null || (_.isArray(rhsLiteral.value) && (rhsLiteral.value.length === 0))) { + return false + } + } + + return { type: "op", op: "=", modifier: "any", exprs: compiledExprs }; + + case "between": + // Null if first not present + if (compiledExprs[0] == null) { + return null; + } + + // Null if second and third not present + if (compiledExprs[1] == null && compiledExprs[2] == null) { + return null; + } + + // >= if third missing + if (compiledExprs[2] == null) { + return { + type: "op", + op: ">=", + exprs: [compiledExprs[0], compiledExprs[1]] + }; + } + + // <= if second missing + if (compiledExprs[1] == null) { + return { + type: "op", + op: "<=", + exprs: [compiledExprs[0], compiledExprs[2]] + }; + } + + // Between + return { + type: "op", + op: "between", + exprs: compiledExprs + }; + + case "not": + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: expr.op, + exprs: [ + { type: "op", op: "coalesce", exprs: [compiledExprs[0], false] } + ] + }; + + case "is null": case "is not null": + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: expr.op, + exprs: compiledExprs + }; + + case "contains": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + // Null if no expressions in literal list + if (((compiledExprs[1] as any).type === "literal") && ((compiledExprs[1] as any).value.length === 0)) { + return null; + } + + // Cast both to jsonb and use @>. Also convert both to json first to handle literal arrays + return { + type: "op", + op: "@>", + exprs: [ + convertToJsonB(compiledExprs[0]), + convertToJsonB(compiledExprs[1]) + ] + }; + + case "intersects": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + // Null if no expressions in literal list + if (((compiledExprs[1] as any).type === "literal") && ((compiledExprs[1] as any).value.length === 0)) { + return null; + } + + // Cast to jsonb and use ?| Also convert to json first to handle literal arrays + return { + type: "op", + op: "?|", + exprs: [ + convertToJsonB(compiledExprs[0]), + compiledExprs[1] + ] + }; + + case "length": + // 0 if null + if ((compiledExprs[0] == null)) { + return 0; + } + + // Cast both to jsonb and use jsonb_array_length. Also convert both to json first to handle literal arrays. Coalesce to 0 so that null is 0 + return { + type: "op", + op: "coalesce", + exprs: [ + { + type: "op", + op: "jsonb_array_length", + exprs: [ + convertToJsonB(compiledExprs[0]) + ] + }, + 0 + ] + }; + + case "line length": + // null if null + if ((compiledExprs[0] == null)) { + return null; + } + + // ST_Length_Spheroid(ST_Transform(location,4326::integer), 'SPHEROID["GRS_1980",6378137,298.257222101]'::spheroid) + return { + type: "op", + op: "ST_LengthSpheroid", + exprs: [ + { + type: "op", + op: "ST_Transform", + exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] + }, + { type: "op", op: "::spheroid", exprs: ['SPHEROID["GRS_1980",6378137,298.257222101]' ]} + ] + }; + + case "to text": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + if (exprUtils.getExprType(expr.exprs[0]) === "enum") { + // Null if no enum values + const enumValues = exprUtils.getExprEnumValues(expr.exprs[0]); + if (!enumValues) { + return null; + } + + return { + type: "case", + input: compiledExprs[0], + cases: _.map(enumValues, ev => { + return { + when: { type: "literal", value: ev.id }, + then: { type: "literal", value: exprUtils.localizeString(ev.name) } + }; + }) + }; + } + + if (exprUtils.getExprType(expr.exprs[0]) === "number") { + return { + type: "op", + op: "::text", + exprs: [compiledExprs[0]] + }; + } + + return null; + + case "to date": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "substr", + exprs: [ + compiledExprs[0], + 1, + 10 + ] + }; + + case "count where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "coalesce", + exprs: [ + { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[0], + then: 1 + } + ], + else: 0 + } + ] + }, + 0 + ] + }; + + case "percent where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Compiles as sum(case when cond [and basis (if present)] then 100::decimal else 0 end)/sum(1 [or case when basis then 1 else 0 (if present)]) (prevent div by zero) + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1] ? { type: "op", op: "and", exprs: [compiledExprs[0], compiledExprs[1]] } : compiledExprs[0], + then: { type: "op", op: "::decimal", exprs: [100] } + } + ], + else: 0 + } + ] + }, + compiledExprs[1] ? + { + type: "op", + op: "nullif", + exprs: [ + { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: 1 + } + ], + else: 0 + } + ] + }, + 0 + ] + } + : + { type: "op", op: "sum", exprs: [1] } + ] + }; + + case "sum where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Simple sum if not specified where + if (compiledExprs[1] == null) { + return { + type: "op", + op: "sum", + exprs: [compiledExprs[0]] + }; + } + + return { + type: "op", + op: "sum", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: compiledExprs[0] + } + ], + else: 0 + } + ] + }; + + case "min where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Simple min if not specified where + if (compiledExprs[1] == null) { + return { + type: "op", + op: "min", + exprs: [compiledExprs[0]] + }; + } + + return { + type: "op", + op: "min", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: compiledExprs[0] + } + ], + else: null + } + ] + }; + + case "max where": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + // Simple max if not specified where + if (compiledExprs[1] == null) { + return { + type: "op", + op: "max", + exprs: [compiledExprs[0]] + }; + } + + return { + type: "op", + op: "max", + exprs: [ + { + type: "case", + cases: [{ + when: compiledExprs[1], + then: compiledExprs[0] + } + ], + else: null + } + ] + }; + + case "count distinct": + // Null if not present + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "count", + exprs: [compiledExprs[0]], + modifier: "distinct" + }; + + case "percent": + // Compiles as count(*) * 100::decimal / sum(count(*)) over() + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "*", + exprs: [ + { type: "op", op: "count", exprs: [] }, + { type: "op", op: "::decimal", exprs: [100] } + ] + }, + { + type: "op", + op: "sum", + exprs: [ + { type: "op", op: "count", exprs: [] } + ], + over: {} + } + ] + }; + + // Hierarchical test that uses ancestry column + case "within": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + // Get table being used + var idTable = exprUtils.getExprIdTable(expr.exprs[0])! + + // Prefer ancestryTable + if (this.schema.getTable(idTable)!.ancestryTable) { + // exists (select null from as subwithin where ancestor = compiledExprs[1] and descendant = compiledExprs[0]) + return { + type: "op", + op: "exists", + exprs: [ + { + type: "scalar", + expr: null, + from: { type: "table", table: this.schema.getTable(idTable)!.ancestryTable!, alias: "subwithin" }, + where: { + type: "op", + op: "and", + exprs: [ + { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "ancestor" }, compiledExprs[1]]}, + { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "descendant" }, compiledExprs[0]]} + ] + } + } + ] + }; + } + + return { + type: "op", + op: "in", + exprs: [ + compiledExprs[0], + { + type: "scalar", + expr: this.compileColumnRef(this.schema.getTable(idTable)!.primaryKey, "subwithin"), + from: { type: "table", table: idTable, alias: "subwithin" }, + where: { + type: "op", + op: "@>", + exprs: [ + { type: "field", tableAlias: "subwithin", column: this.schema.getTable(idTable)!.ancestry! }, + { type: "op", op: "::jsonb", exprs: [{ type: "op", op: "json_build_array", exprs: [compiledExprs[1]] }] } + ] + } + } + ] + }; + + // Hierarchical test that uses ancestry column + case "within any": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + // Get table being used + idTable = exprUtils.getExprIdTable(expr.exprs[0])! + + // Prefer ancestryTable + if (this.schema.getTable(idTable)!.ancestryTable) { + // exists (select null from as subwithin where ancestor = any(compiledExprs[1]) and descendant = compiledExprs[0]) + return { + type: "op", + op: "exists", + exprs: [ + { + type: "scalar", + expr: null, + from: { type: "table", table: this.schema.getTable(idTable)!.ancestryTable!, alias: "subwithin" }, + where: { + type: "op", + op: "and", + exprs: [ + { type: "op", op: "=", modifier: "any", exprs: [{ type: "field", tableAlias: "subwithin", column: "ancestor" }, compiledExprs[1]]}, + { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "subwithin", column: "descendant" }, compiledExprs[0]]} + ] + } + } + ] + }; + } + + // This older code fails now that admin_regions uses integer pk. Replaced with literal-only code + // return { + // type: "op" + // op: "in" + // exprs: [ + // compiledExprs[0] + // { + // type: "scalar" + // expr: @compileColumnRef(@schema.getTable(idTable).primaryKey, "subwithin") + // from: { type: "table", table: idTable, alias: "subwithin" } + // where: { + // type: "op" + // op: "?|" + // exprs: [ + // { type: "field", tableAlias: "subwithin", column: @schema.getTable(idTable).ancestry } + // compiledExprs[1] + // ] + // } + // } + // ] + // } + + // If not literal, fail + if ((compiledExprs[1] as any).type !== "literal") { + throw new Error("Non-literal RHS of within any not supported"); + } + + return { + type: "op", + op: "in", + exprs: [ + compiledExprs[0], + { + type: "scalar", + expr: this.compileColumnRef(this.schema.getTable(idTable)!.primaryKey, "subwithin"), + from: { type: "table", table: idTable, alias: "subwithin" }, + where: { + type: "op", + op: "?|", + exprs: [ + { type: "field", tableAlias: "subwithin", column: this.schema.getTable(idTable)!.ancestryText || this.schema.getTable(idTable)!.ancestry! }, + { type: "literal", value: _.map((compiledExprs[1] as JsonQLLiteral).value, value => { + if (_.isNumber(value)) { + return "" + value; + } else { + return value; + } + }) + } + ] + } + } + ] + }; + + case "latitude": + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "ST_Y", + exprs: [ + { type: "op", op: "ST_Centroid", exprs: [ + { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } + ] } + ] + }; + + case "longitude": + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "ST_X", + exprs: [ + { type: "op", op: "ST_Centroid", exprs: [ + { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] } + ] } + ] + }; + + case 'days difference': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + if ((exprUtils.getExprType(expr.exprs[0]) === "datetime") || (exprUtils.getExprType(expr.exprs[1]) === "datetime")) { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } + ] + }, + 86400 + ] + }; + } + + if (exprUtils.getExprType(expr.exprs[0]) === "date") { + return { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + { type: "op", op: "::date", exprs: [compiledExprs[1]] } + ] + }; + } + + return null; + + case 'months difference': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + if ((exprUtils.getExprType(expr.exprs[0]) === "datetime") || (exprUtils.getExprType(expr.exprs[1]) === "datetime")) { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } + ] + }, + 86400 * 30.5 + ] + }; + } + + if (exprUtils.getExprType(expr.exprs[0]) === "date") { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + { type: "op", op: "::date", exprs: [compiledExprs[1]] } + ] + }, + 30.5 + ] + }; + } + + return null; + + case 'years difference': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + if ((exprUtils.getExprType(expr.exprs[0]) === "datetime") || (exprUtils.getExprType(expr.exprs[1]) === "datetime")) { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[1]] }] } + ] + }, + 86400 * 365 + ] + }; + } + + if (exprUtils.getExprType(expr.exprs[0]) === "date") { + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + { type: "op", op: "::date", exprs: [compiledExprs[1]] } + ] + }, + 365 + ] + }; + } + + return null; + + case 'days since': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "::date", exprs: [moment().format("YYYY-MM-DD")] }, + { type: "op", op: "::date", exprs: [compiledExprs[0]] } + ] + }; + case "datetime": + return { + type: "op", + op: "/", + exprs: [ + { + type: "op", + op: "-", + exprs: [ + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [nowExpr] }] }, + { type: "op", op: "date_part", exprs: ['epoch', { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }] } + ] + }, + 86400 + ] + }; + default: + return null; + } + + case 'month': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "substr", + exprs: [ + compiledExprs[0], + 6, + 2 + ] + }; + + case 'yearmonth': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "rpad", + exprs: [ + { type: "op", op: "substr", exprs: [compiledExprs[0], 1, 7] }, + 10, + "-01" + ] + }; + + case 'yearquarter': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + "YYYY-Q" + ] + }; + + case 'yearweek': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + "IYYY-IW" + ] + }; + + case 'weekofyear': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::date", exprs: [compiledExprs[0]] }, + "IW" + ] + }; + + case 'year': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "rpad", + exprs: [ + { type: "op", op: "substr", exprs: [compiledExprs[0], 1, 4] }, + 10, + "-01-01" + ] + }; + + case 'weekofmonth': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }, + "W" + ] + }; + + case 'dayofmonth': + if (compiledExprs[0] == null) { + return null; + } + + return { + type: "op", + op: "to_char", + exprs: [ + { type: "op", op: "::timestamp", exprs: [compiledExprs[0]] }, + "DD" + ] + }; + + case 'thisyear': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").add(1, 'years').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").add(1, 'years').toISOString() ] } + ] + }; + default: + return null; + } + + case 'lastyear': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").subtract(1, 'years').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("year").subtract(1, 'years').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("year").toISOString() ] } + ] + }; + default: + return null; + } + + case 'thismonth': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").add(1, 'months').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").add(1, 'months').toISOString() ] } + ] + }; + default: + return null; + } + + case 'lastmonth': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").subtract(1, 'months').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("month").subtract(1, 'months').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("month").toISOString() ] } + ] + }; + default: + return null; + } + + case 'today': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'yesterday': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(1, 'days').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(1, 'days').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").toISOString() ] } + ] + }; + default: + return null; + } + + case 'last24hours': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(1, 'days').format("YYYY-MM-DD") ] }, + { type: "op", op: "<=", exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], nowMinus24HoursExpr] }, + { type: "op", op: "<=", exprs: [compiledExprs[0], nowExpr] } + ] + }; + default: + return null; + } + + case 'last7days': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(7, 'days').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(7, 'days').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'last30days': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(30, 'days').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(30, 'days').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'last365days': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(365, 'days').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().startOf("day").subtract(365, 'days').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'last12months': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(11, "months").startOf('month').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(11, "months").startOf('month').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'last6months': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(5, "months").startOf('month').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(5, "months").startOf('month').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'last3months': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(2, "months").startOf('month').format("YYYY-MM-DD") ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().add(1, 'days').format("YYYY-MM-DD") ] } + ] + }; + case "datetime": + return { + type: "op", + op: "and", + exprs: [ + { type: "op", op: ">=", exprs: [compiledExprs[0], moment().subtract(2, "months").startOf('month').toISOString() ] }, + { type: "op", op: "<", exprs: [compiledExprs[0], moment().startOf("day").add(1, 'days').toISOString() ] } + ] + }; + default: + return null; + } + + case 'future': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: ">", + exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] + }; + case "datetime": + return { + type: "op", + op: ">", + exprs: [compiledExprs[0], nowExpr] + }; + default: + return null; + } + + case 'notfuture': + if (compiledExprs[0] == null) { + return null; + } + + switch (expr0Type) { + case "date": + return { + type: "op", + op: "<=", + exprs: [compiledExprs[0], moment().format("YYYY-MM-DD") ] + }; + case "datetime": + return { + type: "op", + op: "<=", + exprs: [compiledExprs[0], nowExpr] + }; + default: + return null; + } + + case 'current date': + return { type: "literal", value: moment().format("YYYY-MM-DD") }; + + case 'current datetime': + return { type: "literal", value: moment().toISOString() }; + + case 'distance': + if (compiledExprs[0] == null || compiledExprs[1] == null) { + return null; + } + + return { + type: "op", + op: "ST_DistanceSphere", + exprs: [ + { type: "op", op: "ST_Transform", exprs: [compiledExprs[0], { type: "op", op: "::integer", exprs: [4326] }] }, + { type: "op", op: "ST_Transform", exprs: [compiledExprs[1], { type: "op", op: "::integer", exprs: [4326] }] } + ] + }; + + case 'is latest': + var lhsCompiled = this.compileExpr({expr: expr.exprs[0], tableAlias: "innerrn"}); + if (!lhsCompiled) { + return null; + } + + var filterCompiled = this.compileExpr({expr: expr.exprs[1], tableAlias: "innerrn"}); + + // Get ordering + ordering = this.schema.getTable(expr.table!)!.ordering + + if (!ordering) { + throw new Error("No ordering defined"); + } + + // order descending + var orderBy: { expr: JsonQLExpr, direction: "desc" }[] = [{ expr: this.compileFieldExpr({expr: { type: "field", table: expr.table!, column: ordering}, tableAlias: "innerrn"}), direction: "desc" }]; + + // _id in (select outerrn.id from (select innerrn.id, row_number() over (partition by EXPR1 order by ORDERING desc) as rn from the_table as innerrn where filter) as outerrn where outerrn.rn = 1) + + // Create innerrn query + var innerrnQuery: JsonQLQuery = { + type: "query", + selects: [ + { type: "select", expr: this.compileExpr({expr: { type: "id", table: expr.table! }, tableAlias: "innerrn" }), alias: "id" }, + { + type: "select", + expr: { + type: "op", + op: "row_number", + exprs: [], + over: { + partitionBy: [lhsCompiled], + orderBy + } + }, + alias: "rn" + } + ], + from: { type: "table", table: expr.table!, alias: "innerrn" } + }; + if (filterCompiled) { + innerrnQuery.where = filterCompiled; + } + + // Wrap in outer query + var outerrnQuery: JsonQLScalar = { + type: "scalar", + expr: { type: "field", tableAlias: "outerrn", column: "id" }, + from: { + type: "subquery", + query: innerrnQuery, + alias: "outerrn" + }, + where: { type: "op", op: "=", exprs: [{ type: "field", tableAlias: "outerrn", column: "rn" }, 1]} + }; + + return { + type: "op", + op: "in", + exprs: [ + this.compileExpr({expr: { type: "id", table: expr.table! }, tableAlias: options.tableAlias}), + outerrnQuery + ] + }; + + default: + throw new Error(`Unknown op ${expr.op}`); + } + } + + compileCaseExpr(options: { expr: CaseExpr, tableAlias: string }): JsonQLExpr { + const { + expr + } = options; + + const compiled: JsonQLCase = { + type: "case", + cases: _.map(expr.cases, c => { + return { + when: this.compileExpr({expr: c.when, tableAlias: options.tableAlias}), + then: this.compileExpr({expr: c.then, tableAlias: options.tableAlias}) + }; + }), + else: this.compileExpr({expr: expr.else, tableAlias: options.tableAlias}) + }; + + // Remove null cases + compiled.cases = _.filter(compiled.cases, c => c.when != null); + + // Return null if no cases + if (compiled.cases.length === 0) { + return null; + } + + return compiled; + } + + compileScoreExpr(options: { expr: ScoreExpr, tableAlias: string }): JsonQLExpr { + const { + expr + } = options; + const exprUtils = new ExprUtils(this.schema); + + // If empty, literal 0 + if (_.isEmpty(expr.scores)) { + return { type: "literal", value: 0 }; + } + + // Get type of input + const inputType = exprUtils.getExprType(expr.input); + + switch (inputType) { + case "enum": + return { + type: "case", + input: this.compileExpr({expr: expr.input, tableAlias: options.tableAlias}), + cases: _.map(_.pairs(expr.scores), pair => { + return { + when: { type: "literal", value: pair[0] }, + then: this.compileExpr({expr: pair[1], tableAlias: options.tableAlias}) + }; + }), + else: { type: "literal", value: 0 } + }; + case "enumset": + return { + type: "op", + op: "+", + exprs: _.map(_.pairs(expr.scores), pair => { + return { + type: "case", + cases: [ + { + when: { + type: "op", + op: "@>", + exprs: [ + convertToJsonB(this.compileExpr({expr: expr.input, tableAlias: options.tableAlias})), + convertToJsonB({ type: "literal", value: [pair[0]] }) + ] + }, + then: this.compileExpr({expr: pair[1], tableAlias: options.tableAlias}) + } + ], + else: { type: "literal", value: 0 } + }; + }) + }; + + // Null if no expression + default: + return null; + } + } + + compileBuildEnumsetExpr(options: { expr: BuildEnumsetExpr, tableAlias: string }): JsonQLExpr { + // Create enumset + // select to_jsonb(array_agg(bes.v)) from (select (case when true then 'x' end) as v union all select (case when true then 'y' end) as v ...) as bes where v is not null + + const { + expr + } = options; + + // Handle empty case + if (_.keys(expr.values).length === 0) { + return null; + } + + return { + type: "scalar", + expr: { + type: "op", + op: "to_jsonb", + exprs: [ + { + type: "op", + op: "array_agg", + exprs: [{ type: "field", tableAlias: "bes", column: "v" }] + } + ] + }, + from: { + type: "subquery", + alias: "bes", + query: { + type: "union all", + queries: _.map(_.pairs(expr.values), pair => { + return { + type: "query", + selects: [ + { + type: "select", + expr: { + type: "case", + cases: [{ when: this.compileExpr({expr: pair[1], tableAlias: options.tableAlias}), then: pair[0] }] + }, + alias: "v" + } + ] + } as JsonQLSelectQuery + }) + } + }, + + where: { + type: "op", + op: "is not null", + exprs: [{ type: "field", tableAlias: "bes", column: "v" }] + } + }; + } + + compileComparisonExpr(options: { expr: LegacyComparisonExpr, tableAlias: string }): JsonQLExpr { + let exprs; + const { + expr + } = options; + const exprUtils = new ExprUtils(this.schema); + + // Missing left-hand side type means null condition + const exprLhsType = exprUtils.getExprType(expr.lhs); + if (!exprLhsType) { + return null; + } + + // Missing right-hand side means null condition + if (exprUtils.getComparisonRhsType(exprLhsType, expr.op) && (expr.rhs == null)) { + return null; + } + + const lhsExpr = this.compileExpr({expr: expr.lhs, tableAlias: options.tableAlias}); + if (expr.rhs) { + const rhsExpr = this.compileExpr({expr: expr.rhs, tableAlias: options.tableAlias}); + exprs = [lhsExpr, rhsExpr]; + } else { + exprs = [lhsExpr]; + } + + // Handle special cases + switch (expr.op) { + case '= true': + return { type: "op", op: "=", exprs: [lhsExpr, { type: "literal", value: true }]}; + case '= false': + return { type: "op", op: "=", exprs: [lhsExpr, { type: "literal", value: false }]}; + case '= any': + return { type: "op", op: "=", modifier: "any", exprs }; + case 'between': + return { type: "op", op: "between", exprs: [lhsExpr, { type: "literal", value: (expr.rhs as any).value[0] }, { type: "literal", value: (expr.rhs as any).value[1] }] }; + default: + return { + type: "op", + op: expr.op, + exprs + }; + } + } + + compileLogicalExpr(options: { expr: LegacyLogicalExpr, tableAlias: string }): JsonQLExpr { + const { + expr + } = options; + + let compiledExprs = _.map(expr.exprs, e => this.compileExpr({expr: e, tableAlias: options.tableAlias})); + + // Remove nulls + compiledExprs = _.compact(compiledExprs); + + // Simplify + if (compiledExprs.length === 1) { + return compiledExprs[0]; + } + + if (compiledExprs.length === 0) { + return null; + } + + return { + type: "op", + op: expr.op, + exprs: compiledExprs + }; + } + + // Compiles a reference to a column or a JsonQL expression + // If parameter is a string, create a simple field expression + // If parameter is an object, inject tableAlias for `{alias}` + compileColumnRef(column: any, tableAlias: string): JsonQLExpr { + if (_.isString(column)) { + return { type: "field", tableAlias, column }; + } + + return injectTableAlias(column, tableAlias) as JsonQLExpr + } + + // Compiles a table, substituting with custom jsonql if required + compileTable(tableId: string, alias: string): JsonQLFrom { + const table = this.schema.getTable(tableId); + if (!table) { + throw new Error(`Table ${tableId} not found`); + } + + if (!table.jsonql) { + return { type: "table", table: tableId, alias }; + } else { + return { type: "subquery", query: table.jsonql, alias }; + } + } + + compileVariableExpr(options: { expr: VariableExpr, tableAlias: string }): JsonQLExpr { + // Get variable + const variable = _.findWhere(this.variables, {id: options.expr.variableId}); + if (!variable) { + throw new Error(`Variable ${options.expr.variableId} not found`); + } + + // Get value (which is always an expression) + const value = this.variableValues[variable.id]; + + // If expression, compile + if (value != null) { + return this.compileExpr({ expr: value, tableAlias: options.tableAlias }); + } else { + return null; + } + } +} + +// Converts a compiled expression to jsonb. Literals cannot use to_jsonb as they will +// trigger "could not determine polymorphic type because input has type unknown" unless the +// SQL is inlined +function convertToJsonB(compiledExpr: JsonQLExpr): JsonQLExpr { + if (compiledExpr == null) { + return compiledExpr; + } + + if (typeof compiledExpr == "number" || typeof compiledExpr == "boolean" || typeof compiledExpr == "string") { + return { type: "op", op: "::jsonb", exprs: [{ type: "literal", value: JSON.stringify(compiledExpr) }] }; + } + + // Literals are special and are cast to jsonb from a JSON string + if ((compiledExpr as any).type === "literal") { + return { type: "op", op: "::jsonb", exprs: [{ type: "literal", value: JSON.stringify((compiledExpr as JsonQLLiteral).value) }] }; + } + + // First convert using to_jsonb in case is array + return { type: "op", op: "to_jsonb", exprs: [compiledExpr] } +} diff --git a/src/ExprUtils.ts b/src/ExprUtils.ts index 58c08be..cb0fdcf 100644 --- a/src/ExprUtils.ts +++ b/src/ExprUtils.ts @@ -1271,6 +1271,7 @@ addOpItem({op: "array_agg", name: "Make list of", desc: "Aggregates results into addOpItem({op: "contains", name: "includes all of", resultType: "boolean", exprTypes: ["id[]", "id[]"]}); addOpItem({op: "intersects", name: "includes any of", resultType: "boolean", exprTypes: ["id[]", "id[]"]}); +addOpItem({op: "includes", name: "includes", resultType: "boolean", exprTypes: ["id[]", "id"]}); addOpItem({op: "count", name: "Total Number", desc: "Get total number of items", resultType: "number", exprTypes: [], prefix: true, aggr: true}); addOpItem({op: "percent", name: "Percent of Total", desc: "Percent of all items", resultType: "number", exprTypes: [], prefix: true, aggr: true}); diff --git a/src/types.ts b/src/types.ts index e671843..708aa95 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { JsonQL } from "jsonql" +import { JsonQL, JsonQLQuery } from "jsonql" export interface LocalizedString { _base: string, @@ -125,6 +125,9 @@ export interface ScalarExpr { /** @deprecated */ aggr?: string + /** @deprecated */ + where?: Expr + /** Array of join columns to follow to get to table of expr. All must be `join` type */ joins: string[] @@ -214,7 +217,7 @@ export interface Table { /** Optional custom JsonQL expression. This allows a simple table to be translated to an arbitrarily complex JsonQL expression before being sent to the server. * @deprecated This is not enforced everywhere as some queries don't use compileTable */ - jsonql?: JsonQL + jsonql?: JsonQLQuery /** sql expression that gets the table. Usually just name of the table. *Note*: this is only for when using a schema file for Water.org's visualization server */ sql?: string diff --git a/test/ExprCompilerTests.coffee b/test/ExprCompilerTests.coffee index 65986f1..92aeaa7 100644 --- a/test/ExprCompilerTests.coffee +++ b/test/ExprCompilerTests.coffee @@ -5,7 +5,7 @@ canonical = require 'canonical-json' moment = require 'moment' sinon = require 'sinon' Schema = require('../src/Schema').default -ExprCompiler = require '../src/ExprCompiler' +ExprCompiler = require('../src/ExprCompiler').default ColumnNotFoundException = require '../src/ColumnNotFoundException' setupTestExtension = require('./extensionSetup').setupTestExtension