actual/packages/loot-core/src/server/aql/compiler.js

1134 lines
29 KiB
JavaScript

let _uid = 0;
function resetUid() {
_uid = 0;
}
function uid(tableName) {
_uid++;
return tableName + _uid;
}
class CompileError extends Error {}
function nativeDateToInt(date) {
let pad = x => (x < 10 ? '0' : '') + x;
return date.getFullYear() + pad(date.getMonth() + 1) + pad(date.getDate());
}
function dateToInt(date) {
return parseInt(date.replace(/-/g, ''));
}
export function addTombstone(schema, tableName, tableId, whereStr) {
let hasTombstone = schema[tableName].tombstone != null;
return hasTombstone ? `${whereStr} AND ${tableId}.tombstone = 0` : whereStr;
}
function popPath(path) {
let parts = path.split('.');
return { path: parts.slice(0, -1).join('.'), field: parts[parts.length - 1] };
}
function isKeyword(str) {
return str === 'group';
}
export function quoteAlias(alias) {
return alias.indexOf('.') === -1 && !isKeyword(alias) ? alias : `"${alias}"`;
}
function typed(value, type, { literal = false } = {}) {
return { value, type, literal };
}
function getFieldDescription(schema, tableName, field) {
if (schema[tableName] == null) {
throw new CompileError(`Table "${tableName}" does not exist in the schema`);
}
let fieldDesc = schema[tableName][field];
if (fieldDesc == null) {
throw new CompileError(
`Field "${field}" does not exist in table "${tableName}"`
);
}
return fieldDesc;
}
function makePath(state, path) {
let { schema, paths } = state;
let parts = path.split('.');
if (parts.length < 2) {
throw new CompileError('Invalid path: ' + path);
}
let initialTable = parts[0];
let tableName = parts.slice(1).reduce((tableName, field) => {
let table = schema[tableName];
if (table == null) {
throw new CompileError(`Path error: ${tableName} table does not exist`);
}
if (!table[field] || table[field].ref == null) {
throw new CompileError(
`Field not joinable on table ${tableName}: "${field}"`
);
}
return table[field].ref;
}, initialTable);
let joinTable;
let parentParts = parts.slice(0, -1);
if (parentParts.length === 1) {
joinTable = parentParts[0];
} else {
let parentPath = parentParts.join('.');
let parentDesc = paths.get(parentPath);
if (!parentDesc) {
throw new CompileError('Path does not exist: ' + parentPath);
}
joinTable = parentDesc.tableId;
}
return {
tableName: tableName,
tableId: uid(tableName),
joinField: parts[parts.length - 1],
joinTable
};
}
function resolvePath(state, path) {
let paths = path.split('.');
let tableId;
paths = paths.reduce(
(acc, name) => {
let fullName = acc.context + '.' + name;
return {
context: fullName,
path: [...acc.path, fullName]
};
},
{ context: state.implicitTableName, path: [] }
).path;
paths.forEach(path => {
if (!state.paths.get(path)) {
state.paths.set(path, makePath(state, path));
}
});
let pathInfo = state.paths.get(paths[paths.length - 1]);
return pathInfo;
}
function transformField(state, name) {
if (typeof name !== 'string') {
throw new CompileError('Invalid field name, must be a string');
}
let { path, field } = popPath(name);
let pathInfo;
if (path === '') {
pathInfo = {
tableName: state.implicitTableName,
tableId: state.implicitTableId
};
} else {
pathInfo = resolvePath(state, path);
}
let fieldDesc = getFieldDescription(state.schema, pathInfo.tableName, field);
// If this is a field that references an item in another table, that
// item could have been deleted. If that's the case, we want to
// return `null` instead of an id pointing to a deleted item. This
// converts an id reference into a path that pulls the id through a
// table join which will filter out dead items, resulting in a
// `null` id if the item is deleted
if (
state.validateRefs &&
fieldDesc.ref &&
fieldDesc.type === 'id' &&
field !== 'id'
) {
let refPath = state.implicitTableName + '.' + name;
let refPathInfo = state.paths.get(refPath);
if (!refPathInfo) {
refPathInfo = makePath(state, refPath);
refPathInfo.noMapping = true;
state.paths.set(refPath, refPathInfo);
}
field = 'id';
pathInfo = refPathInfo;
}
let fieldStr = pathInfo.tableId + '.' + field;
return typed(fieldStr, fieldDesc.type);
}
function parseDate(str) {
let m = str.match(/^(\d{4}-\d{2}-\d{2})$/);
if (m) {
return typed(dateToInt(m[1]), 'date', { literal: true });
}
return null;
}
function parseMonth(str) {
let m = str.match(/^(\d{4}-\d{2})$/);
if (m) {
return typed(dateToInt(m[1]), 'date', { literal: true });
}
return null;
}
function parseYear(str) {
let m = str.match(/^(\d{4})$/);
if (m) {
return typed(dateToInt(m[1]), 'date', { literal: true });
}
return null;
}
function badDateFormat(str, type) {
throw new CompileError(`Bad ${type} format: ${str}`);
}
function inferParam(param, type) {
let existingType = param.paramType;
if (existingType) {
let casts = {
date: ['string'],
'date-month': ['date'],
'date-year': ['date', 'date-month'],
id: ['string'],
float: ['integer']
};
if (
existingType !== type &&
(!casts[type] || !casts[type].includes(existingType))
) {
throw new Error(
`Parameter "${name}" can't convert to ${type} (already inferred as ${existingType})`
);
}
} else {
param.paramType = type;
}
}
function castInput(state, expr, type) {
if (expr.type === type) {
return expr;
} else if (expr.type === 'param') {
inferParam(expr, type);
return typed(expr.value, type);
} else if (expr.type === 'null') {
if (!expr.literal) {
throw new CompileError("A non-literal null doesn't make sense");
}
if (type === 'boolean') {
return typed(0, 'boolean', { literal: true });
}
return expr;
}
// These are all things that can be safely casted automatically
if (type === 'date') {
if (expr.type === 'string') {
if (expr.literal) {
return parseDate(expr.value) || badDateFormat(expr.value, 'date');
} else {
throw new CompileError(
'Casting string fields to dates is not supported'
);
}
}
throw new CompileError(`Can't cast ${expr.type} to date`);
} else if (type === 'date-month') {
let expr2;
if (expr.type === 'date') {
expr2 = expr;
} else if (expr.type === 'string' || expr.type === 'any') {
expr2 =
parseMonth(expr.value) ||
parseDate(expr.value) ||
badDateFormat(expr.value, 'date-month');
} else {
throw new CompileError(`Can't cast ${expr.type} to date-month`);
}
if (expr2.literal) {
return typed(
dateToInt(expr2.value.toString().slice(0, 6)),
'date-month',
{ literal: true }
);
} else {
return typed(
`CAST(SUBSTR(${expr2.value}, 1, 6) AS integer)`,
'date-month'
);
}
} else if (type === 'date-year') {
let expr2;
if (expr.type === 'date' || expr.type === 'date-month') {
expr2 = expr;
} else if (expr.type === 'string') {
expr2 =
parseYear(expr.value) ||
parseMonth(expr.value) ||
parseDate(expr.value) ||
badDateFormat(expr.value, 'date-year');
} else {
throw new CompileError(`Can't cast ${expr.type} to date-year`);
}
if (expr2.literal) {
return typed(dateToInt(expr2.value.toString().slice(0, 4)), 'date-year', {
literal: true
});
} else {
return typed(
`CAST(SUBSTR(${expr2.value}, 1, 4) AS integer)`,
'date-year'
);
}
} else if (type === 'id') {
if (expr.type === 'string') {
return typed(expr.value, 'id', { literal: expr.literal });
}
} else if (type === 'float') {
if (expr.type === 'integer') {
return typed(expr.value, 'float', { literal: expr.literal });
}
}
if (expr.type === 'any') {
return typed(expr.value, type, { literal: expr.literal });
}
throw new CompileError(`Can't convert ${expr.type} to ${type}`);
}
// TODO: remove state from these functions
function val(state, expr, type) {
let castedExpr = expr;
// Cast the type if necessary
if (type) {
castedExpr = castInput(state, expr, type);
}
if (castedExpr.literal) {
if (castedExpr.type === 'id') {
return `'${castedExpr.value}'`;
} else if (castedExpr.type === 'string') {
// Escape quotes
let value = castedExpr.value.replace(/'/g, "''");
return `'${value}'`;
}
}
return castedExpr.value;
}
function valArray(state, arr, types) {
return arr.map((value, idx) => val(state, value, types ? types[idx] : null));
}
function validateArgLength(arr, min, max) {
if (max == null) {
max = min;
}
if (min != null && arr.length < min) {
throw new CompileError('Too few arguments');
}
if (max != null && arr.length > max) {
throw new CompileError('Too many arguments');
}
}
//// Nice errors
function saveStack(type, func) {
return (state, ...args) => {
if (state == null || state.compileStack == null) {
throw new CompileError(
'This function cannot track error data. ' +
'It needs to accept the compiler state as the first argument.'
);
}
state.compileStack.push({ type, args });
let ret = func(state, ...args);
state.compileStack.pop();
return ret;
};
}
function prettyValue(value) {
if (typeof value === 'string') {
return value;
} else if (value === undefined) {
return 'undefined';
}
let str = JSON.stringify(value);
if (str.length > 70) {
let expanded = JSON.stringify(value, null, 2);
return expanded.split('\n').join('\n ');
}
return str;
}
function getCompileError(error, stack) {
if (stack.length === 0) {
return error;
}
let stackStr = stack
.slice(1)
.reverse()
.map(entry => {
switch (entry.type) {
case 'expr':
case 'function':
return prettyValue(entry.args[0]);
case 'op': {
let [fieldRef, opData] = entry.args;
return prettyValue({ [fieldRef]: opData });
}
case 'value':
return prettyValue(entry.value);
default:
return '';
}
})
.map(str => '\n ' + str)
.join('');
const rootMethod = stack[0].type;
const methodArgs = stack[0].args[0];
stackStr += `\n ${rootMethod}(${prettyValue(
methodArgs.length === 1 ? methodArgs[0] : methodArgs
)})`;
// In production, hide internal stack traces
if (process.env.NODE_ENV === 'production') {
const err = new CompileError();
err.message = `${error.message}\n\nExpression stack:` + stackStr;
err.stack = null;
return err;
}
error.message = `${error.message}\n\nExpression stack:` + stackStr;
return error;
}
//// Compiler
function compileLiteral(value) {
if (value === undefined) {
throw new CompileError('`undefined` is not a valid query value');
} else if (value === null) {
return typed('NULL', 'null', { literal: true });
} else if (value instanceof Date) {
return typed(nativeDateToInt(value), 'date', { literal: true });
} else if (typeof value === 'string') {
// Allow user to escape $, and quote the string to make it a
// string literal in the output
value = value.replace(/\\\$/g, '$');
return typed(value, 'string', { literal: true });
} else if (typeof value === 'boolean') {
return typed(value ? 1 : 0, 'boolean', { literal: true });
} else if (typeof value === 'number') {
return typed(value, (value | 0) === value ? 'integer' : 'float', {
literal: true
});
} else if (Array.isArray(value)) {
return typed(value, 'array', { literal: true });
} else {
throw new CompileError(
'Unsupported type of expression: ' + JSON.stringify(value)
);
}
}
const compileExpr = saveStack('expr', (state, expr) => {
if (typeof expr === 'string') {
// Field reference
if (expr[0] === '$') {
let fieldRef = expr === '$' ? state.implicitField : expr.slice(1);
if (fieldRef == null || fieldRef === '') {
throw new CompileError('Invalid field reference: ' + expr);
}
return transformField(state, fieldRef);
}
// Named parameter
if (expr[0] === ':') {
let param = { value: '?', type: 'param', paramName: expr.slice(1) };
state.namedParameters.push(param);
return param;
}
}
if (expr !== null) {
if (Array.isArray(expr)) {
return compileLiteral(expr);
} else if (
typeof expr === 'object' &&
Object.keys(expr).find(k => k[0] === '$')
) {
// It's a function call
return compileFunction(state, expr);
}
}
return compileLiteral(expr);
});
function assertType(name, data, acceptedTypes) {
if (acceptedTypes.indexOf(data.type) === -1) {
throw new CompileError(
`Invalid type of expression to ${name}, must be one of ${JSON.stringify(
acceptedTypes
)}: ${JSON.stringify(data.value)}`
);
}
}
function assertArgLength(name, args, len) {
if (args.length !== len) {
throw new CompileError(
`Invalid number of args to ${name}: expected ${len} but received ${args.length}`
)();
}
}
const compileFunction = saveStack('function', (state, func) => {
let [name] = Object.keys(func);
let argExprs = func[name];
if (!Array.isArray(argExprs)) {
argExprs = [argExprs];
}
if (name[0] !== '$') {
throw new CompileError(
`Unknown property "${name}". Did you mean to call a function? Try prefixing it with $`
);
}
let args = argExprs;
// `$condition` is a special-case where it will be evaluated later
if (name !== '$condition') {
args = argExprs.map(arg => compileExpr(state, arg));
}
switch (name) {
// aggregate functions
case '$sum': {
validateArgLength(args, 1);
let [arg1] = valArray(state, args, ['float']);
return typed(`SUM(${arg1})`, args[0].type);
}
case '$sumOver': {
let [arg1] = valArray(state, args, ['float']);
let order = state.orders
? 'ORDER BY ' + compileOrderBy(state, state.orders)
: '';
return typed(
`(SUM(${arg1}) OVER (${order} ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING))`,
args[0].type
);
}
case '$count': {
validateArgLength(args, 1);
let [arg1] = valArray(state, args);
return typed(`COUNT(${arg1})`, 'integer');
}
// string functions
case '$substr': {
validateArgLength(args, 2, 3);
let [arg1, arg2, arg3] = valArray(state, args, [
'string',
'integer',
'integer'
]);
return typed(`SUBSTR(${arg1}, ${arg2}, ${arg3})`, 'string');
}
case '$lower': {
validateArgLength(args, 1);
let [arg1] = valArray(state, args, ['string']);
return typed(`LOWER(${arg1})`, 'string');
}
// integer/float functions
case '$neg': {
validateArgLength(args, 1);
let [arg1] = valArray(state, args, ['float']);
return typed(`(-${val(state, args[0])})`, args[0].type);
}
case '$abs': {
validateArgLength(args, 1);
let [arg1] = valArray(state, args, ['float']);
return typed(`ABS(${val(state, args[0])})`, args[0].type);
}
case '$idiv': {
validateArgLength(args, 2);
let [arg1, arg2] = valArray(state, args, ['integer', 'integer']);
return typed(
`(${val(state, args[0])} / ${val(state, args[1])})`,
args[0].type
);
}
// date functions
case '$month': {
validateArgLength(args, 1);
return castInput(state, args[0], 'date-month');
}
case '$year': {
validateArgLength(args, 1);
return castInput(state, args[0], 'date-year');
}
// various functions
case '$condition':
validateArgLength(args, 1);
let conds = compileConditions(state, args[0]);
return typed(conds.join(' AND '), 'boolean');
case '$nocase':
validateArgLength(args, 1);
let [arg1] = valArray(state, args, ['string']);
return typed(`${arg1} COLLATE NOCASE`, args[0].type);
case '$literal': {
validateArgLength(args, 1);
if (!args[0].literal) {
throw new CompileError('Literal not passed to $literal');
}
return args[0];
}
default:
throw new CompileError(`Unknown function: ${name}`);
}
});
const compileOp = saveStack('op', (state, fieldRef, opData) => {
let { $transform, ...opExpr } = opData;
let [op] = Object.keys(opExpr);
let rhs = compileExpr(state, opData[op]);
let lhs;
if ($transform) {
lhs = compileFunction(
{ ...state, implicitField: fieldRef },
typeof $transform === 'string' ? { [$transform]: '$' } : $transform
);
} else {
lhs = compileExpr(state, '$' + fieldRef);
}
switch (op) {
case '$gte': {
let [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
return `${left} >= ${right}`;
}
case '$lte': {
let [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
return `${left} <= ${right}`;
}
case '$gt': {
let [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
return `${left} > ${right}`;
}
case '$lt': {
let [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
return `${left} < ${right}`;
}
case '$eq': {
if (castInput(state, rhs, lhs.type).type === 'null') {
return `${val(state, lhs)} IS NULL`;
}
let [left, right] = valArray(state, [lhs, rhs], [null, lhs.type]);
if (rhs.type === 'param') {
let orders = state.namedParameters.map(param => {
return param === rhs || param === lhs ? [param, { ...param }] : param;
});
state.namedParameters = [].concat.apply([], orders);
return `CASE
WHEN ${left} IS NULL THEN ${right} IS NULL
ELSE ${left} = ${right}
END`;
}
return `${left} = ${right}`;
}
case '$oneof': {
let [left, right] = valArray(state, [lhs, rhs], [null, 'array']);
// Dedupe the ids
let ids = [...new Set(right)];
return `${left} IN (` + ids.map(id => `'${id}'`).join(',') + ')';
}
case '$like': {
let [left, right] = valArray(state, [lhs, rhs], ['string', 'string']);
return `${left} LIKE ${right}`;
}
default:
throw new CompileError(`Unknown operator: ${op}`);
}
});
function compileConditions(state, conds) {
if (!Array.isArray(conds)) {
// Convert the object form `{foo: 1, bar:2}` into the array form
// `[{foo: 1}, {bar:2}]`
conds = Object.entries(conds).map(cond => {
return { [cond[0]]: cond[1] };
});
}
return conds.filter(Boolean).reduce((res, condsObj) => {
let compiled = Object.entries(condsObj)
.map(([field, cond]) => {
// Allow a falsy value in the lhs of $and and $or to allow for
// quick forms like `$or: amount != 0 && ...`
if (field === '$and') {
if (!cond) {
return null;
}
return compileAnd(state, cond);
} else if (field === '$or') {
if (!cond) {
return null;
}
return compileOr(state, cond);
}
if (
typeof cond === 'string' ||
typeof cond === 'number' ||
typeof cond === 'boolean' ||
cond instanceof Date ||
cond == null
) {
return compileOp(state, field, { $eq: cond });
}
if (Array.isArray(cond)) {
// An array of conditions for a field is implicitly an `and`
return cond.map(c => compileOp(state, field, c)).join(' AND ');
}
return compileOp(state, field, cond);
})
.filter(Boolean);
return [...res, ...compiled];
}, []);
}
function compileOr(state, conds) {
// Same as above
if (!conds) {
return '0';
}
let res = compileConditions(state, conds);
if (res.length === 0) {
return '0';
}
return '(' + res.join('\n OR ') + ')';
}
function compileAnd(state, conds) {
// Same as above
if (!conds) {
return '1';
}
let res = compileConditions(state, conds);
if (res.length === 0) {
return '1';
}
return '(' + res.join('\n AND ') + ')';
}
const compileWhere = saveStack('filter', (state, conds) => {
return compileAnd(state, conds);
});
function compileJoins(state, tableRef, internalTableFilters) {
let joins = [];
state.paths.forEach((desc, path) => {
let {
tableName,
tableId,
joinField,
joinTable,
noMapping
} = state.paths.get(path);
let on = `${tableId}.id = ${tableRef(joinTable)}.${quoteAlias(joinField)}`;
let filters = internalTableFilters(tableName);
if (filters.length > 0) {
on +=
' AND ' +
compileAnd(
{ ...state, implicitTableName: tableName, implicitTableId: tableId },
filters
);
}
joins.push(
`LEFT JOIN ${
noMapping ? tableName : tableRef(tableName, true)
} ${tableId} ON ${addTombstone(state.schema, tableName, tableId, on)}`
);
if (state.dependencies.indexOf(tableName) === -1) {
state.dependencies.push(tableName);
}
});
return joins.join('\n');
}
function expandStar(state, expr) {
let path;
let pathInfo;
if (expr === '*') {
pathInfo = {
tableName: state.implicitTableName,
tableId: state.implicitTableId
};
} else if (expr.match(/\.\*$/)) {
let result = popPath(expr);
path = result.path;
pathInfo = resolvePath(state, result.path);
}
let table = state.schema[pathInfo.tableName];
if (table == null) {
throw new Error(`Table "${pathInfo.tableName}" does not exist`);
}
return Object.keys(table).map(field => (path ? `${path}.${field}` : field));
}
const compileSelect = saveStack(
'select',
(state, exprs, isAggregate, orders) => {
// Always include the id if it's not an aggregate
if (!isAggregate && !exprs.includes('id') && !exprs.includes('*')) {
exprs = exprs.concat(['id']);
}
let select = exprs.map(expr => {
if (typeof expr === 'string') {
if (expr.indexOf('*') !== -1) {
let fields = expandStar(state, expr);
return fields
.map(field => {
let compiled = compileExpr(state, '$' + field);
state.outputTypes.set(field, compiled.type);
return compiled.value + ' AS ' + quoteAlias(field);
})
.join(', ');
}
let compiled = compileExpr(state, '$' + expr);
state.outputTypes.set(expr, compiled.type);
return compiled.value + ' AS ' + quoteAlias(expr);
}
let [name, value] = Object.entries(expr)[0];
if (name[0] === '$') {
state.compileStack.push({ type: 'value', value: expr });
throw new CompileError(
`Invalid field "${name}", are you trying to select a function? You need to name the expression`
);
}
if (typeof value === 'string') {
let compiled = compileExpr(state, '$' + value);
state.outputTypes.set(name, compiled.type);
return `${compiled.value} AS ${quoteAlias(name)}`;
}
let compiled = compileFunction({ ...state, orders }, value);
state.outputTypes.set(name, compiled.type);
return compiled.value + ` AS ${quoteAlias(name)}`;
});
return select.join(', ');
}
);
const compileGroupBy = saveStack('groupBy', (state, exprs) => {
let groupBy = exprs.map(expr => {
if (typeof expr === 'string') {
return compileExpr(state, '$' + expr).value;
}
return compileFunction(state, expr).value;
});
return groupBy.join(', ');
});
const compileOrderBy = saveStack('orderBy', (state, exprs) => {
let orderBy = exprs.map(expr => {
let compiled;
let dir = null;
if (typeof expr === 'string') {
compiled = compileExpr(state, '$' + expr).value;
} else {
let entries = Object.entries(expr);
let entry = entries[0];
// Check if this is a field reference
if (entries.length === 1 && entry[0][0] !== '$') {
dir = entry[1];
compiled = compileExpr(state, '$' + entry[0]).value;
} else {
// Otherwise it's a function
let { $dir, ...func } = expr;
dir = $dir;
compiled = compileFunction(state, func).value;
}
}
if (dir != null) {
if (dir !== 'desc' && dir !== 'asc') {
throw new CompileError('Invalid order direction: ' + dir);
}
return `${compiled} ${dir}`;
}
return compiled;
});
return orderBy.join(', ');
});
let AGGREGATE_FUNCTIONS = ['$sum', '$count'];
function isAggregateFunction(expr) {
if (typeof expr !== 'object' || Array.isArray(expr)) {
return false;
}
let [name, argExprs] = Object.entries(expr)[0];
if (!Array.isArray(argExprs)) {
argExprs = [argExprs];
}
if (AGGREGATE_FUNCTIONS.indexOf(name) !== -1) {
return true;
}
return !!argExprs.find(ex => isAggregateFunction(ex));
}
export function isAggregateQuery(queryState) {
// it's aggregate if:
// either an aggregate function is used in `select`
// or a `groupBy` exists
if (queryState.groupExpressions.length > 0) {
return true;
}
return !!queryState.selectExpressions.find(expr => {
if (typeof expr !== 'string') {
let [name, value] = Object.entries(expr)[0];
return isAggregateFunction(value);
}
return false;
});
}
export function compileQuery(queryState, schema, schemaConfig = {}) {
let { withDead, validateRefs = true, tableOptions, rawMode } = queryState;
let {
tableViews = {},
tableFilters = name => [],
customizeQuery = queryState => queryState
} = schemaConfig;
let internalTableFilters = name => {
let filters = tableFilters(name);
// These filters cannot join tables and must be simple strings
for (let filter of filters) {
if (Array.isArray(filter)) {
throw new CompileError(
'Invalid internal table filter: only object filters are supported'
);
}
if (Object.keys(filter)[0].indexOf('.') !== -1) {
throw new CompileError(
'Invalid internal table filter: field names cannot contain paths'
);
}
}
return filters;
};
let tableRef = (name, isJoin) => {
let view =
typeof tableViews === 'function'
? tableViews(name, { withDead, isJoin, tableOptions })
: tableViews[name];
return view || name;
};
let tableName = queryState.table;
let {
filterExpressions,
selectExpressions,
groupExpressions,
orderExpressions,
limit,
offset,
calculation
} = customizeQuery(queryState);
let select = '';
let where = '';
let joins = '';
let groupBy = '';
let orderBy = '';
let dependences = [];
let state = {
schema,
implicitTableName: tableName,
implicitTableId: tableRef(tableName),
paths: new Map(),
dependencies: [tableName],
compileStack: [],
outputTypes: new Map(),
validateRefs,
namedParameters: []
};
resetUid();
try {
select = compileSelect(
state,
selectExpressions,
isAggregateQuery(queryState),
orderExpressions
);
if (filterExpressions.length > 0) {
let result = compileWhere(state, filterExpressions);
where = 'WHERE ' + result;
} else {
where = 'WHERE 1';
}
if (!rawMode) {
let filters = internalTableFilters(tableName);
if (filters.length > 0) {
where += ' AND ' + compileAnd(state, filters);
}
}
if (groupExpressions.length > 0) {
let result = compileGroupBy(state, groupExpressions);
groupBy = 'GROUP BY ' + result;
}
// Orders don't matter if doing a single calculation
if (orderExpressions.length > 0) {
let result = compileOrderBy(state, orderExpressions);
orderBy = 'ORDER BY ' + result;
}
if (state.paths.size > 0) {
joins = compileJoins(state, tableRef, internalTableFilters);
}
} catch (e) {
if (e instanceof CompileError) {
throw getCompileError(e, state.compileStack);
}
throw e;
}
let sqlPieces = {
select,
from: tableRef(tableName),
joins,
where,
groupBy,
orderBy,
limit,
offset
};
return {
sqlPieces,
state
};
}
export function defaultConstructQuery(queryState, state, sqlPieces) {
let s = sqlPieces;
let where = queryState.withDead
? s.where
: addTombstone(
state.schema,
state.implicitTableName,
state.implicitTableId,
s.where
);
return `
SELECT ${s.select} FROM ${s.from}
${s.joins}
${where}
${s.groupBy}
${s.orderBy}
${s.limit != null ? `LIMIT ${s.limit}` : ''}
${s.offset != null ? `OFFSET ${s.offset}` : ''}
`;
}
export function generateSQLWithState(queryState, schema, schemaConfig) {
let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
}
export function generateSQL(queryState) {
return generateSQLWithState(queryState).sql;
}