@nozbe/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
190 lines (186 loc) • 9.21 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.default = void 0;
var _common = require("../../../utils/common");
var Q = _interopRequireWildcard(require("../../../QueryDescription"));
var _encodeValue = _interopRequireDefault(require("../encodeValue"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/* eslint-disable no-use-before-define */
function mapJoin(array, mapper, joiner) {
// NOTE: DO NOT try to optimize this by concatenating strings together. In non-JIT JSC,
// concatenating strings is extremely slow (5000ms vs 120ms on 65K sample)
return array.map(mapper).join(joiner);
}
var encodeValues = function (values) {
return "(".concat(mapJoin(values, _encodeValue.default, ', '), ")");
};
var getComparisonRight = function (table, comparisonRight) {
if (comparisonRight.values) {
return encodeValues(comparisonRight.values);
} else if (comparisonRight.column) {
return "\"".concat(table, "\".\"").concat(comparisonRight.column, "\"");
}
return 'undefined' !== typeof comparisonRight.value ? (0, _encodeValue.default)(comparisonRight.value) : 'null';
};
// Note: it's necessary to use `is` / `is not` for NULL comparisons to work correctly
// See: https://sqlite.org/lang_expr.html
var operators = {
eq: 'is',
notEq: 'is not',
gt: '>',
gte: '>=',
weakGt: '>',
// For non-column comparison case
lt: '<',
lte: '<=',
oneOf: 'in',
notIn: 'not in',
between: 'between',
like: 'like',
notLike: 'not like'
};
var encodeComparison = function (table, comparison) {
var {
operator: operator
} = comparison;
if ('between' === operator) {
var {
right: right
} = comparison;
return right.values ? "between ".concat((0, _encodeValue.default)(right.values[0]), " and ").concat((0, _encodeValue.default)(right.values[1])) : '';
}
return "".concat(operators[operator], " ").concat(getComparisonRight(table, comparison.right));
};
var encodeWhere = function (table, associations) {
return function (where) {
switch (where.type) {
case 'and':
return "(".concat(encodeAndOr(associations, 'and', table, where.conditions), ")");
case 'or':
return "(".concat(encodeAndOr(associations, 'or', table, where.conditions), ")");
case 'where':
return encodeWhereCondition(associations, table, where.left, where.comparison);
case 'on':
if ('production' !== process.env.NODE_ENV) {
(0, _common.invariant)(associations.some(function ({
to: to
}) {
return to === where.table;
}), 'To nest Q.on inside Q.and/Q.or you must explicitly declare Q.experimentalJoinTables at the beginning of the query');
}
return "(".concat(encodeAndOr(associations, 'and', where.table, where.conditions), ")");
case 'sql':
return where.expr;
default:
throw new Error("Unknown clause ".concat(where.type));
}
};
};
var encodeWhereCondition = function (associations, table, left, comparison) {
var {
operator: operator
} = comparison;
// if right operand is a value, we can use simple comparison
// if a column, we must check for `not null > null`
if ('weakGt' === operator && comparison.right.column) {
return encodeWhere(table, associations)(Q.or(
// $FlowFixMe
Q.where(left, Q.gt(Q.column(comparison.right.column))), Q.and(Q.where(left, Q.notEq(null)), Q.where(comparison.right.column, null))));
} else if ('includes' === operator) {
return "instr(\"".concat(table, "\".\"").concat(left, "\", ").concat(getComparisonRight(table, comparison.right), ")");
}
return "\"".concat(table, "\".\"").concat(left, "\" ").concat(encodeComparison(table, comparison));
};
var encodeAndOr = function (associations, op, table, conditions) {
if (conditions.length) {
return mapJoin(conditions, encodeWhere(table, associations), " ".concat(op, " "));
}
return '';
};
var andJoiner = ' and ';
var encodeConditions = function (table, description, associations) {
var clauses = mapJoin(description.where, encodeWhere(table, associations), andJoiner);
return clauses.length ? " where ".concat(clauses) : '';
};
// If query contains `on()` conditions on tables with which the primary table has a has-many
// relation, then we need to add `distinct` on the query to ensure there are no duplicates
var encodeMethod = function (table, countMode, needsDistinct) {
if (countMode) {
return needsDistinct ? "select count(distinct \"".concat(table, "\".\"id\") as \"count\" from \"").concat(table, "\"") : "select count(*) as \"count\" from \"".concat(table, "\"");
}
return needsDistinct ? "select distinct \"".concat(table, "\".* from \"").concat(table, "\"") : "select \"".concat(table, "\".* from \"").concat(table, "\"");
};
var encodeAssociation = function (description) {
return function ({
from: mainTable,
to: joinedTable,
info: association
}) {
// TODO: We have a problem here. For all of eternity, WatermelonDB Q.ons were encoded using JOIN
// However, this precludes many legitimate use cases for Q.ons once you start nesting them
// (e.g. get tasks where X or has a tag assignment that Y -- if there is no tag assignment, this will
// fail to join)
// LEFT JOIN needs to be used to address this… BUT technically that's a breaking change. I never
// considered a possiblity of making a query like `Q.on(relation_id, x != 'bla')`. Before this would
// only match if there IS a relation, but with LEFT JOIN it would also match if record does not have
// this relation. I don't know if there are legitimate use cases where this would change anything
// so I need more time to think about whether this breaking change is OK to make or if we need to
// do something more clever/add option/whatever.
// so for now, i'm making an extreeeeemelyyyy bad hack to make sure that there's no breaking change
// for existing code and code with nested Q.ons probably works (with caveats)
var usesOldJoinStyle = description.where.some(function (clause) {
return 'on' === clause.type && clause.table === joinedTable;
});
var joinKeyword = usesOldJoinStyle ? ' join ' : ' left join ';
var joinBeginning = "".concat(joinKeyword, "\"").concat(joinedTable, "\" on \"").concat(joinedTable, "\".");
return 'belongs_to' === association.type ? "".concat(joinBeginning, "\"id\" = \"").concat(mainTable, "\".\"").concat(association.key, "\"") : "".concat(joinBeginning, "\"").concat(association.foreignKey, "\" = \"").concat(mainTable, "\".\"id\"");
};
};
var encodeJoin = function (description, associations) {
return associations.length ? associations.map(encodeAssociation(description)).join('') : '';
};
var encodeOrderBy = function (table, sortBys) {
if (0 === sortBys.length) {
return '';
}
var orderBys = sortBys.map(function (sortBy) {
return "\"".concat(table, "\".\"").concat(sortBy.sortColumn, "\" ").concat(sortBy.sortOrder);
}).join(', ');
return " order by ".concat(orderBys);
};
var encodeLimitOffset = function (limit, offset) {
if (!limit) {
return '';
}
var optionalOffsetStmt = offset ? " offset ".concat(offset) : '';
return " limit ".concat(limit).concat(optionalOffsetStmt);
};
var encodeQuery = function (query, countMode = false) {
var {
table: table,
description: description,
associations: associations
} = query;
// TODO: Test if encoding a `select x.id from x` query speeds up queryIds() calls
if (description.sql) {
var {
sql: _sql,
values: values
} = description.sql;
return [_sql, values];
}
var hasToManyJoins = associations.some(function ({
info: info
}) {
return 'has_many' === info.type;
});
if ('production' !== process.env.NODE_ENV) {
description.take && (0, _common.invariant)(!countMode, 'take/skip is not currently supported with counting. Please contribute to fix this!');
(0, _common.invariant)(!description.lokiTransform, 'unsafeLokiTransform not supported with SQLite');
}
var sql = encodeMethod(table, countMode, hasToManyJoins) + encodeJoin(description, associations) + encodeConditions(table, description, associations) + encodeOrderBy(table, description.sortBy) + encodeLimitOffset(description.take, description.skip);
return [sql, []];
};
var _default = exports.default = encodeQuery;