PageRenderTime 52ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/packages/minimongo/sort.js

https://gitlab.com/0072016/SDK-JavaScript-
JavaScript | 417 lines | 246 code | 54 blank | 117 comment | 57 complexity | 3aca85eaeb5912273ea4feecb45831f6 MD5 | raw file
  1. // Give a sort spec, which can be in any of these forms:
  2. // {"key1": 1, "key2": -1}
  3. // [["key1", "asc"], ["key2", "desc"]]
  4. // ["key1", ["key2", "desc"]]
  5. //
  6. // (.. with the first form being dependent on the key enumeration
  7. // behavior of your javascript VM, which usually does what you mean in
  8. // this case if the key names don't look like integers ..)
  9. //
  10. // return a function that takes two objects, and returns -1 if the
  11. // first object comes first in order, 1 if the second object comes
  12. // first, or 0 if neither object comes before the other.
  13. Minimongo.Sorter = function (spec, options) {
  14. var self = this;
  15. options = options || {};
  16. self._sortSpecParts = [];
  17. self._sortFunction = null;
  18. var addSpecPart = function (path, ascending) {
  19. if (!path)
  20. throw Error("sort keys must be non-empty");
  21. if (path.charAt(0) === '$')
  22. throw Error("unsupported sort key: " + path);
  23. self._sortSpecParts.push({
  24. path: path,
  25. lookup: makeLookupFunction(path, {forSort: true}),
  26. ascending: ascending
  27. });
  28. };
  29. if (spec instanceof Array) {
  30. for (var i = 0; i < spec.length; i++) {
  31. if (typeof spec[i] === "string") {
  32. addSpecPart(spec[i], true);
  33. } else {
  34. addSpecPart(spec[i][0], spec[i][1] !== "desc");
  35. }
  36. }
  37. } else if (typeof spec === "object") {
  38. _.each(spec, function (value, key) {
  39. addSpecPart(key, value >= 0);
  40. });
  41. } else if (typeof spec === "function") {
  42. self._sortFunction = spec;
  43. } else {
  44. throw Error("Bad sort specification: " + JSON.stringify(spec));
  45. }
  46. // If a function is specified for sorting, we skip the rest.
  47. if (self._sortFunction)
  48. return;
  49. // To implement affectedByModifier, we piggy-back on top of Matcher's
  50. // affectedByModifier code; we create a selector that is affected by the same
  51. // modifiers as this sort order. This is only implemented on the server.
  52. if (self.affectedByModifier) {
  53. var selector = {};
  54. _.each(self._sortSpecParts, function (spec) {
  55. selector[spec.path] = 1;
  56. });
  57. self._selectorForAffectedByModifier = new Minimongo.Matcher(selector);
  58. }
  59. self._keyComparator = composeComparators(
  60. _.map(self._sortSpecParts, function (spec, i) {
  61. return self._keyFieldComparator(i);
  62. }));
  63. // If you specify a matcher for this Sorter, _keyFilter may be set to a
  64. // function which selects whether or not a given "sort key" (tuple of values
  65. // for the different sort spec fields) is compatible with the selector.
  66. self._keyFilter = null;
  67. options.matcher && self._useWithMatcher(options.matcher);
  68. };
  69. // In addition to these methods, sorter_project.js defines combineIntoProjection
  70. // on the server only.
  71. _.extend(Minimongo.Sorter.prototype, {
  72. getComparator: function (options) {
  73. var self = this;
  74. // If we have no distances, just use the comparator from the source
  75. // specification (which defaults to "everything is equal".
  76. if (!options || !options.distances) {
  77. return self._getBaseComparator();
  78. }
  79. var distances = options.distances;
  80. // Return a comparator which first tries the sort specification, and if that
  81. // says "it's equal", breaks ties using $near distances.
  82. return composeComparators([self._getBaseComparator(), function (a, b) {
  83. if (!distances.has(a._id))
  84. throw Error("Missing distance for " + a._id);
  85. if (!distances.has(b._id))
  86. throw Error("Missing distance for " + b._id);
  87. return distances.get(a._id) - distances.get(b._id);
  88. }]);
  89. },
  90. _getPaths: function () {
  91. var self = this;
  92. return _.pluck(self._sortSpecParts, 'path');
  93. },
  94. // Finds the minimum key from the doc, according to the sort specs. (We say
  95. // "minimum" here but this is with respect to the sort spec, so "descending"
  96. // sort fields mean we're finding the max for that field.)
  97. //
  98. // Note that this is NOT "find the minimum value of the first field, the
  99. // minimum value of the second field, etc"... it's "choose the
  100. // lexicographically minimum value of the key vector, allowing only keys which
  101. // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x:
  102. // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and
  103. // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key.
  104. _getMinKeyFromDoc: function (doc) {
  105. var self = this;
  106. var minKey = null;
  107. self._generateKeysFromDoc(doc, function (key) {
  108. if (!self._keyCompatibleWithSelector(key))
  109. return;
  110. if (minKey === null) {
  111. minKey = key;
  112. return;
  113. }
  114. if (self._compareKeys(key, minKey) < 0) {
  115. minKey = key;
  116. }
  117. });
  118. // This could happen if our key filter somehow filters out all the keys even
  119. // though somehow the selector matches.
  120. if (minKey === null)
  121. throw Error("sort selector found no keys in doc?");
  122. return minKey;
  123. },
  124. _keyCompatibleWithSelector: function (key) {
  125. var self = this;
  126. return !self._keyFilter || self._keyFilter(key);
  127. },
  128. // Iterates over each possible "key" from doc (ie, over each branch), calling
  129. // 'cb' with the key.
  130. _generateKeysFromDoc: function (doc, cb) {
  131. var self = this;
  132. if (self._sortSpecParts.length === 0)
  133. throw new Error("can't generate keys without a spec");
  134. // maps index -> ({'' -> value} or {path -> value})
  135. var valuesByIndexAndPath = [];
  136. var pathFromIndices = function (indices) {
  137. return indices.join(',') + ',';
  138. };
  139. var knownPaths = null;
  140. _.each(self._sortSpecParts, function (spec, whichField) {
  141. // Expand any leaf arrays that we find, and ignore those arrays
  142. // themselves. (We never sort based on an array itself.)
  143. var branches = expandArraysInBranches(spec.lookup(doc), true);
  144. // If there are no values for a key (eg, key goes to an empty array),
  145. // pretend we found one null value.
  146. if (!branches.length)
  147. branches = [{value: null}];
  148. var usedPaths = false;
  149. valuesByIndexAndPath[whichField] = {};
  150. _.each(branches, function (branch) {
  151. if (!branch.arrayIndices) {
  152. // If there are no array indices for a branch, then it must be the
  153. // only branch, because the only thing that produces multiple branches
  154. // is the use of arrays.
  155. if (branches.length > 1)
  156. throw Error("multiple branches but no array used?");
  157. valuesByIndexAndPath[whichField][''] = branch.value;
  158. return;
  159. }
  160. usedPaths = true;
  161. var path = pathFromIndices(branch.arrayIndices);
  162. if (_.has(valuesByIndexAndPath[whichField], path))
  163. throw Error("duplicate path: " + path);
  164. valuesByIndexAndPath[whichField][path] = branch.value;
  165. // If two sort fields both go into arrays, they have to go into the
  166. // exact same arrays and we have to find the same paths. This is
  167. // roughly the same condition that makes MongoDB throw this strange
  168. // error message. eg, the main thing is that if sort spec is {a: 1,
  169. // b:1} then a and b cannot both be arrays.
  170. //
  171. // (In MongoDB it seems to be OK to have {a: 1, 'a.x.y': 1} where 'a'
  172. // and 'a.x.y' are both arrays, but we don't allow this for now.
  173. // #NestedArraySort
  174. // XXX achieve full compatibility here
  175. if (knownPaths && !_.has(knownPaths, path)) {
  176. throw Error("cannot index parallel arrays");
  177. }
  178. });
  179. if (knownPaths) {
  180. // Similarly to above, paths must match everywhere, unless this is a
  181. // non-array field.
  182. if (!_.has(valuesByIndexAndPath[whichField], '') &&
  183. _.size(knownPaths) !== _.size(valuesByIndexAndPath[whichField])) {
  184. throw Error("cannot index parallel arrays!");
  185. }
  186. } else if (usedPaths) {
  187. knownPaths = {};
  188. _.each(valuesByIndexAndPath[whichField], function (x, path) {
  189. knownPaths[path] = true;
  190. });
  191. }
  192. });
  193. if (!knownPaths) {
  194. // Easy case: no use of arrays.
  195. var soleKey = _.map(valuesByIndexAndPath, function (values) {
  196. if (!_.has(values, ''))
  197. throw Error("no value in sole key case?");
  198. return values[''];
  199. });
  200. cb(soleKey);
  201. return;
  202. }
  203. _.each(knownPaths, function (x, path) {
  204. var key = _.map(valuesByIndexAndPath, function (values) {
  205. if (_.has(values, ''))
  206. return values[''];
  207. if (!_.has(values, path))
  208. throw Error("missing path?");
  209. return values[path];
  210. });
  211. cb(key);
  212. });
  213. },
  214. // Takes in two keys: arrays whose lengths match the number of spec
  215. // parts. Returns negative, 0, or positive based on using the sort spec to
  216. // compare fields.
  217. _compareKeys: function (key1, key2) {
  218. var self = this;
  219. if (key1.length !== self._sortSpecParts.length ||
  220. key2.length !== self._sortSpecParts.length) {
  221. throw Error("Key has wrong length");
  222. }
  223. return self._keyComparator(key1, key2);
  224. },
  225. // Given an index 'i', returns a comparator that compares two key arrays based
  226. // on field 'i'.
  227. _keyFieldComparator: function (i) {
  228. var self = this;
  229. var invert = !self._sortSpecParts[i].ascending;
  230. return function (key1, key2) {
  231. var compare = LocalCollection._f._cmp(key1[i], key2[i]);
  232. if (invert)
  233. compare = -compare;
  234. return compare;
  235. };
  236. },
  237. // Returns a comparator that represents the sort specification (but not
  238. // including a possible geoquery distance tie-breaker).
  239. _getBaseComparator: function () {
  240. var self = this;
  241. if (self._sortFunction)
  242. return self._sortFunction;
  243. // If we're only sorting on geoquery distance and no specs, just say
  244. // everything is equal.
  245. if (!self._sortSpecParts.length) {
  246. return function (doc1, doc2) {
  247. return 0;
  248. };
  249. }
  250. return function (doc1, doc2) {
  251. var key1 = self._getMinKeyFromDoc(doc1);
  252. var key2 = self._getMinKeyFromDoc(doc2);
  253. return self._compareKeys(key1, key2);
  254. };
  255. },
  256. // In MongoDB, if you have documents
  257. // {_id: 'x', a: [1, 10]} and
  258. // {_id: 'y', a: [5, 15]},
  259. // then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5).
  260. // But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not
  261. // match the selector, and 5 comes before 10).
  262. //
  263. // The way this works is pretty subtle! For example, if the documents
  264. // are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and
  265. // {_id: 'y', a: [{x: 5}, {x: 15}]}),
  266. // then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and
  267. // C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}})
  268. // both follow this rule (y before x). (ie, you do have to apply this
  269. // through $elemMatch.)
  270. //
  271. // So if you pass a matcher to this sorter's constructor, we will attempt to
  272. // skip sort keys that don't match the selector. The logic here is pretty
  273. // subtle and undocumented; we've gotten as close as we can figure out based
  274. // on our understanding of Mongo's behavior.
  275. _useWithMatcher: function (matcher) {
  276. var self = this;
  277. if (self._keyFilter)
  278. throw Error("called _useWithMatcher twice?");
  279. // If we are only sorting by distance, then we're not going to bother to
  280. // build a key filter.
  281. // XXX figure out how geoqueries interact with this stuff
  282. if (_.isEmpty(self._sortSpecParts))
  283. return;
  284. var selector = matcher._selector;
  285. // If the user just passed a literal function to find(), then we can't get a
  286. // key filter from it.
  287. if (selector instanceof Function)
  288. return;
  289. var constraintsByPath = {};
  290. _.each(self._sortSpecParts, function (spec, i) {
  291. constraintsByPath[spec.path] = [];
  292. });
  293. _.each(selector, function (subSelector, key) {
  294. // XXX support $and and $or
  295. var constraints = constraintsByPath[key];
  296. if (!constraints)
  297. return;
  298. // XXX it looks like the real MongoDB implementation isn't "does the
  299. // regexp match" but "does the value fall into a range named by the
  300. // literal prefix of the regexp", ie "foo" in /^foo(bar|baz)+/ But
  301. // "does the regexp match" is a good approximation.
  302. if (subSelector instanceof RegExp) {
  303. // As far as we can tell, using either of the options that both we and
  304. // MongoDB support ('i' and 'm') disables use of the key filter. This
  305. // makes sense: MongoDB mostly appears to be calculating ranges of an
  306. // index to use, which means it only cares about regexps that match
  307. // one range (with a literal prefix), and both 'i' and 'm' prevent the
  308. // literal prefix of the regexp from actually meaning one range.
  309. if (subSelector.ignoreCase || subSelector.multiline)
  310. return;
  311. constraints.push(regexpElementMatcher(subSelector));
  312. return;
  313. }
  314. if (isOperatorObject(subSelector)) {
  315. _.each(subSelector, function (operand, operator) {
  316. if (_.contains(['$lt', '$lte', '$gt', '$gte'], operator)) {
  317. // XXX this depends on us knowing that these operators don't use any
  318. // of the arguments to compileElementSelector other than operand.
  319. constraints.push(
  320. ELEMENT_OPERATORS[operator].compileElementSelector(operand));
  321. }
  322. // See comments in the RegExp block above.
  323. if (operator === '$regex' && !subSelector.$options) {
  324. constraints.push(
  325. ELEMENT_OPERATORS.$regex.compileElementSelector(
  326. operand, subSelector));
  327. }
  328. // XXX support {$exists: true}, $mod, $type, $in, $elemMatch
  329. });
  330. return;
  331. }
  332. // OK, it's an equality thing.
  333. constraints.push(equalityElementMatcher(subSelector));
  334. });
  335. // It appears that the first sort field is treated differently from the
  336. // others; we shouldn't create a key filter unless the first sort field is
  337. // restricted, though after that point we can restrict the other sort fields
  338. // or not as we wish.
  339. if (_.isEmpty(constraintsByPath[self._sortSpecParts[0].path]))
  340. return;
  341. self._keyFilter = function (key) {
  342. return _.all(self._sortSpecParts, function (specPart, index) {
  343. return _.all(constraintsByPath[specPart.path], function (f) {
  344. return f(key[index]);
  345. });
  346. });
  347. };
  348. }
  349. });
  350. // Given an array of comparators
  351. // (functions (a,b)->(negative or positive or zero)), returns a single
  352. // comparator which uses each comparator in order and returns the first
  353. // non-zero value.
  354. var composeComparators = function (comparatorArray) {
  355. return function (a, b) {
  356. for (var i = 0; i < comparatorArray.length; ++i) {
  357. var compare = comparatorArray[i](a, b);
  358. if (compare !== 0)
  359. return compare;
  360. }
  361. return 0;
  362. };
  363. };