UNPKG

@nozbe/watermelondb

Version:

Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast

234 lines (210 loc) 6.85 kB
// @flow /* eslint-disable no-use-before-define */ // don't import whole `utils` to keep worker size small import invariant from '../../../../utils/common/invariant' import likeToRegexp from '../../../../utils/fp/likeToRegexp' import type { QueryAssociation, SerializedQuery } from '../../../../Query' import type { Operator, WhereDescription, On, And, Or, Where, Clause, Comparison, } from '../../../../QueryDescription' import { type TableName, type ColumnName } from '../../../../Schema' export type LokiRawQuery = Object | typeof undefined type LokiOperator = | '$aeq' | '$eq' | '$gt' | '$gte' | '$lt' | '$lte' | '$ne' | '$in' | '$nin' | '$between' | '$regex' | '$containsString' type LokiKeyword = LokiOperator | '$and' | '$or' export type LokiJoin = $Exact<{ table: TableName<any>, query: LokiRawQuery, mapKey: ColumnName, joinKey: ColumnName, }> export type LokiQuery = $Exact<{ table: TableName<any>, query: LokiRawQuery, hasJoins: boolean, }> const weakNotNull = { $not: { $aeq: null } } const encodeComparison = (comparison: Comparison, value: any): LokiRawQuery => { // TODO: It's probably possible to improve performance of those operators by making them // binary-search compatible (i.e. don't use $and, $not) // TODO: We might be able to use $jgt, $jbetween, etc. — but ensure the semantics are right // and it won't break indexing const { operator } = comparison if (comparison.right.column) { // Encode for column comparisons switch (operator) { case 'eq': return { $$aeq: value } case 'notEq': return { $not: { $$aeq: value } } case 'gt': return { $$gt: value } case 'gte': return { $$gte: value } case 'weakGt': return { $$gt: value } case 'lt': return { $and: [{ $$lt: value }, weakNotNull] } case 'lte': return { $and: [{ $$lte: value }, weakNotNull] } default: throw new Error(`Illegal operator ${operator} for column comparisons`) } } else { switch (operator) { case 'eq': return { $aeq: value } case 'notEq': return { $not: { $aeq: value } } case 'gt': return { $gt: value } case 'gte': return { $gte: value } case 'weakGt': return { $gt: value } // Note: yup, this is correct (for non-column comparisons) case 'lt': return { $and: [{ $lt: value }, weakNotNull] } case 'lte': return { $and: [{ $lte: value }, weakNotNull] } case 'oneOf': return { $in: value } case 'notIn': return { $and: [{ $nin: value }, weakNotNull] } case 'between': return { $between: value } case 'like': return { $regex: likeToRegexp(value) } case 'notLike': return { $and: [{ $not: { $eq: null } }, { $not: { $regex: likeToRegexp(value) } }], } case 'includes': return { $containsString: value } default: throw new Error(`Unknown operator ${operator}`) } } } const columnCompRequiresColumnNotNull: { [$FlowFixMe<Operator>]: boolean } = { gt: true, gte: true, lt: true, lte: true, } const encodeWhereDescription: (WhereDescription) => LokiRawQuery = ({ left, comparison }) => { const { operator, right } = comparison const col: string = left // $FlowFixMe - NOTE: order of ||s is important here, since .value can be falsy, but .column and .values are either truthy or are undefined const comparisonRight: any = right.column || right.values || right.value if (typeof right.value === 'string') { // we can do fast path as we know that eq and aeq do the same thing for strings if (operator === 'eq') { return { [col]: { $eq: comparisonRight } } } else if (operator === 'notEq') { return { [col]: { $ne: comparisonRight } } } } const colName: ?string = (right: any).column const encodedComparison = encodeComparison(comparison, comparisonRight) if (colName && columnCompRequiresColumnNotNull[operator]) { return { $and: [{ [col]: encodedComparison }, { [colName]: weakNotNull }] } } return { [col]: encodedComparison } } const encodeCondition: (QueryAssociation[]) => (Clause) => LokiRawQuery = (associations) => (clause) => { switch (clause.type) { case 'and': return encodeAnd(associations, clause) case 'or': return encodeOr(associations, clause) case 'where': return encodeWhereDescription(clause) case 'on': return encodeJoin(associations, clause) case 'loki': return clause.expr default: throw new Error(`Unknown clause ${clause.type}`) } } const encodeConditions: (QueryAssociation[], Where[]) => LokiRawQuery[] = ( associations, conditions, ) => conditions.map(encodeCondition(associations)) const encodeAndOr = (op: LokiKeyword) => (associations: QueryAssociation[], clause: And | Or): LokiRawQuery => { const conditions = encodeConditions(associations, clause.conditions) // flatten return conditions.length === 1 ? conditions[0] : // $FlowFixMe { [op]: conditions } } const encodeAnd: (QueryAssociation[], And) => LokiRawQuery = encodeAndOr('$and') const encodeOr: (QueryAssociation[], Or) => LokiRawQuery = encodeAndOr('$or') // Note: empty query returns `undefined` because // Loki's Collection.count() works but count({}) doesn't const concatRawQueries = (queries: LokiRawQuery[]): LokiRawQuery => { switch (queries.length) { case 0: return undefined case 1: return queries[0] default: return { $and: queries } } } const encodeRootConditions: (QueryAssociation[], Where[]) => LokiRawQuery = ( associations, conditions, ) => concatRawQueries(encodeConditions(associations, conditions)) const encodeJoin = (associations: QueryAssociation[], on: On): LokiRawQuery => { const { table, conditions } = on const association = associations.find(({ to }) => table === to) invariant( association, 'To nest Q.on inside Q.and/Q.or you must explicitly declare Q.experimentalJoinTables at the beginning of the query', ) const { info } = association return { $join: { table, query: encodeRootConditions(associations, (conditions: any)), mapKey: info.type === 'belongs_to' ? 'id' : info.foreignKey, joinKey: info.type === 'belongs_to' ? info.key : 'id', }, } } export default function encodeQuery(query: SerializedQuery): LokiQuery { const { table, description: { where, joinTables, sql }, associations, } = query invariant(!sql, '[Loki] Q.unsafeSqlQuery are not supported with LokiJSAdapter') return { table, query: encodeRootConditions(associations, where), hasJoins: !!joinTables.length, } }