packages/minimongo/matcher.js
import LocalCollection from './local_collection.js';
import {
compileDocumentSelector,
hasOwn,
nothingMatcher,
} from './common.js';
const Decimal = Package['mongo-decimal']?.Decimal || class DecimalStub {}
// The minimongo selector compiler!
// Terminology:
// - a 'selector' is the EJSON object representing a selector
// - a 'matcher' is its compiled form (whether a full Minimongo.Matcher
// object or one of the component lambdas that matches parts of it)
// - a 'result object' is an object with a 'result' field and maybe
// distance and arrayIndices.
// - a 'branched value' is an object with a 'value' field and maybe
// 'dontIterate' and 'arrayIndices'.
// - a 'document' is a top-level object that can be stored in a collection.
// - a 'lookup function' is a function that takes in a document and returns
// an array of 'branched values'.
// - a 'branched matcher' maps from an array of branched values to a result
// object.
// - an 'element matcher' maps from a single value to a bool.
// Main entry point.
// var matcher = new Minimongo.Matcher({a: {$gt: 5}});
// if (matcher.documentMatches({a: 7})) ...
export default class Matcher {
constructor(selector, isUpdate) {
// A set (object mapping string -> *) of all of the document paths looked
// at by the selector. Also includes the empty string if it may look at any
// path (eg, $where).
this._paths = {};
// Set to true if compilation finds a $near.
this._hasGeoQuery = false;
// Set to true if compilation finds a $where.
this._hasWhere = false;
// Set to false if compilation finds anything other than a simple equality
// or one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used
// with scalars as operands.
this._isSimple = true;
// Set to a dummy document which always matches this Matcher. Or set to null
// if such document is too hard to find.
this._matchingDocument = undefined;
// A clone of the original selector. It may just be a function if the user
// passed in a function; otherwise is definitely an object (eg, IDs are
// translated into {_id: ID} first. Used by canBecomeTrueByModifier and
// Sorter._useWithMatcher.
this._selector = null;
this._docMatcher = this._compileSelector(selector);
// Set to true if selection is done for an update operation
// Default is false
// Used for $near array update (issue #3599)
this._isUpdate = isUpdate;
}
documentMatches(doc) {
if (doc !== Object(doc)) {
throw Error('documentMatches needs a document');
}
return this._docMatcher(doc);
}
hasGeoQuery() {
return this._hasGeoQuery;
}
hasWhere() {
return this._hasWhere;
}
isSimple() {
return this._isSimple;
}
// Given a selector, return a function that takes one argument, a
// document. It returns a result object.
_compileSelector(selector) {
// you can pass a literal function instead of a selector
if (selector instanceof Function) {
this._isSimple = false;
this._selector = selector;
this._recordPathUsed('');
return doc => ({result: !!selector.call(doc)});
}
// shorthand -- scalar _id
if (LocalCollection._selectorIsId(selector)) {
this._selector = {_id: selector};
this._recordPathUsed('_id');
return doc => ({result: EJSON.equals(doc._id, selector)});
}
// protect against dangerous selectors. falsey and {_id: falsey} are both
// likely programmer error, and not what you want, particularly for
// destructive operations.
if (!selector || hasOwn.call(selector, '_id') && !selector._id) {
this._isSimple = false;
return nothingMatcher;
}
// Top level can't be an array or true or binary.
if (Array.isArray(selector) ||
EJSON.isBinary(selector) ||
typeof selector === 'boolean') {
throw new Error(`Invalid selector: ${selector}`);
}
this._selector = EJSON.clone(selector);
return compileDocumentSelector(selector, this, {isRoot: true});
}
// Returns a list of key paths the given selector is looking for. It includes
// the empty string if there is a $where.
_getPaths() {
return Object.keys(this._paths);
}
_recordPathUsed(path) {
this._paths[path] = true;
}
}
// helpers used by compiled selector code
LocalCollection._f = {
// XXX for _all and _in, consider building 'inquery' at compile time..
_type(v) {
if (typeof v === 'number') {
return 1;
}
if (typeof v === 'string') {
return 2;
}
if (typeof v === 'boolean') {
return 8;
}
if (Array.isArray(v)) {
return 4;
}
if (v === null) {
return 10;
}
// note that typeof(/x/) === "object"
if (v instanceof RegExp) {
return 11;
}
if (typeof v === 'function') {
return 13;
}
if (v instanceof Date) {
return 9;
}
if (EJSON.isBinary(v)) {
return 5;
}
if (v instanceof MongoID.ObjectID) {
return 7;
}
if (v instanceof Decimal) {
return 1;
}
// object
return 3;
// XXX support some/all of these:
// 14, symbol
// 15, javascript code with scope
// 16, 18: 32-bit/64-bit integer
// 17, timestamp
// 255, minkey
// 127, maxkey
},
// deep equality test: use for literal document and array matches
_equal(a, b) {
return EJSON.equals(a, b, {keyOrderSensitive: true});
},
// maps a type code to a value that can be used to sort values of different
// types
_typeorder(t) {
// http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types
// XXX what is the correct sort position for Javascript code?
// ('100' in the matrix below)
// XXX minkey/maxkey
return [
-1, // (not a type)
1, // number
2, // string
3, // object
4, // array
5, // binary
-1, // deprecated
6, // ObjectID
7, // bool
8, // Date
0, // null
9, // RegExp
-1, // deprecated
100, // JS code
2, // deprecated (symbol)
100, // JS code
1, // 32-bit int
8, // Mongo timestamp
1 // 64-bit int
][t];
},
// compare two values of unknown type according to BSON ordering
// semantics. (as an extension, consider 'undefined' to be less than
// any other value.) return negative if a is less, positive if b is
// less, or 0 if equal
_cmp(a, b) {
if (a === undefined) {
return b === undefined ? 0 : -1;
}
if (b === undefined) {
return 1;
}
let ta = LocalCollection._f._type(a);
let tb = LocalCollection._f._type(b);
const oa = LocalCollection._f._typeorder(ta);
const ob = LocalCollection._f._typeorder(tb);
if (oa !== ob) {
return oa < ob ? -1 : 1;
}
// XXX need to implement this if we implement Symbol or integers, or
// Timestamp
if (ta !== tb) {
throw Error('Missing type coercion logic in _cmp');
}
if (ta === 7) { // ObjectID
// Convert to string.
ta = tb = 2;
a = a.toHexString();
b = b.toHexString();
}
if (ta === 9) { // Date
// Convert to millis.
ta = tb = 1;
a = isNaN(a) ? 0 : a.getTime();
b = isNaN(b) ? 0 : b.getTime();
}
if (ta === 1) { // double
if (a instanceof Decimal) {
return a.minus(b).toNumber();
} else {
return a - b;
}
}
if (tb === 2) // string
return a < b ? -1 : a === b ? 0 : 1;
if (ta === 3) { // Object
// this could be much more efficient in the expected case ...
const toArray = object => {
const result = [];
Object.keys(object).forEach(key => {
result.push(key, object[key]);
});
return result;
};
return LocalCollection._f._cmp(toArray(a), toArray(b));
}
if (ta === 4) { // Array
for (let i = 0; ; i++) {
if (i === a.length) {
return i === b.length ? 0 : -1;
}
if (i === b.length) {
return 1;
}
const s = LocalCollection._f._cmp(a[i], b[i]);
if (s !== 0) {
return s;
}
}
}
if (ta === 5) { // binary
// Surprisingly, a small binary blob is always less than a large one in
// Mongo.
if (a.length !== b.length) {
return a.length - b.length;
}
for (let i = 0; i < a.length; i++) {
if (a[i] < b[i]) {
return -1;
}
if (a[i] > b[i]) {
return 1;
}
}
return 0;
}
if (ta === 8) { // boolean
if (a) {
return b ? 0 : 1;
}
return b ? -1 : 0;
}
if (ta === 10) // null
return 0;
if (ta === 11) // regexp
throw Error('Sorting not supported on regular expression'); // XXX
// 13: javascript code
// 14: symbol
// 15: javascript code with scope
// 16: 32-bit integer
// 17: timestamp
// 18: 64-bit integer
// 255: minkey
// 127: maxkey
if (ta === 13) // javascript code
throw Error('Sorting not supported on Javascript code'); // XXX
throw Error('Unknown type to sort');
},
};