1# Licensed under the Apache License, Version 2.0 (the "License"); you may not
2# use this file except in compliance with the License. You may obtain a copy of
3# the License at
4#
5#   http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations under
11# the License.
12
13import json
14import mango
15import unittest
16import user_docs
17import math
18from hypothesis import given, assume, example
19import hypothesis.strategies as st
20
21
22@unittest.skipIf(mango.has_text_service(), "text service exists")
23class TextIndexCheckTests(mango.DbPerClass):
24    def test_create_text_index(self):
25        body = json.dumps({"index": {}, "type": "text"})
26        resp = self.db.sess.post(self.db.path("_index"), data=body)
27        assert resp.status_code == 503, resp
28
29
30@unittest.skipUnless(mango.has_text_service(), "requires text service")
31class BasicTextTests(mango.UserDocsTextTests):
32    def test_simple(self):
33        docs = self.db.find({"$text": "Stephanie"})
34        assert len(docs) == 1
35        assert docs[0]["name"]["first"] == "Stephanie"
36
37    def test_with_integer(self):
38        docs = self.db.find({"name.first": "Stephanie", "age": 48})
39        assert len(docs) == 1
40        assert docs[0]["name"]["first"] == "Stephanie"
41        assert docs[0]["age"] == 48
42
43    def test_with_boolean(self):
44        docs = self.db.find({"name.first": "Stephanie", "manager": False})
45        assert len(docs) == 1
46        assert docs[0]["name"]["first"] == "Stephanie"
47        assert docs[0]["manager"] == False
48
49    def test_with_array(self):
50        faves = ["Ruby", "C", "Python"]
51        docs = self.db.find({"name.first": "Stephanie", "favorites": faves})
52        assert docs[0]["name"]["first"] == "Stephanie"
53        assert docs[0]["favorites"] == faves
54
55    def test_array_ref(self):
56        docs = self.db.find({"favorites.1": "Python"})
57        assert len(docs) == 4
58        for d in docs:
59            assert "Python" in d["favorites"]
60
61        # Nested Level
62        docs = self.db.find({"favorites.0.2": "Python"})
63        assert len(docs) == 1
64        for d in docs:
65            assert "Python" in d["favorites"][0][2]
66
67    def test_number_ref(self):
68        docs = self.db.find({"11111": "number_field"})
69        assert len(docs) == 1
70        assert docs[0]["11111"] == "number_field"
71
72        docs = self.db.find({"22222.33333": "nested_number_field"})
73        assert len(docs) == 1
74        assert docs[0]["22222"]["33333"] == "nested_number_field"
75
76    def test_lt(self):
77        docs = self.db.find({"age": {"$lt": 22}})
78        assert len(docs) == 0
79
80        docs = self.db.find({"age": {"$lt": 23}})
81        assert len(docs) == 1
82        assert docs[0]["user_id"] == 9
83
84        docs = self.db.find({"age": {"$lt": 33}})
85        assert len(docs) == 2
86        for d in docs:
87            assert d["user_id"] in (1, 9)
88
89        docs = self.db.find({"age": {"$lt": 34}})
90        assert len(docs) == 3
91        for d in docs:
92            assert d["user_id"] in (1, 7, 9)
93
94        docs = self.db.find({"company": {"$lt": "Dreamia"}})
95        assert len(docs) == 1
96        assert docs[0]["company"] == "Affluex"
97
98        docs = self.db.find({"foo": {"$lt": "bar car apple"}})
99        assert len(docs) == 0
100
101    def test_lte(self):
102        docs = self.db.find({"age": {"$lte": 21}})
103        assert len(docs) == 0
104
105        docs = self.db.find({"age": {"$lte": 22}})
106        assert len(docs) == 1
107        assert docs[0]["user_id"] == 9
108
109        docs = self.db.find({"age": {"$lte": 33}})
110        assert len(docs) == 3
111        for d in docs:
112            assert d["user_id"] in (1, 7, 9)
113
114        docs = self.db.find({"company": {"$lte": "Dreamia"}})
115        assert len(docs) == 2
116        for d in docs:
117            assert d["user_id"] in (0, 11)
118
119        docs = self.db.find({"foo": {"$lte": "bar car apple"}})
120        assert len(docs) == 1
121        assert docs[0]["user_id"] == 14
122
123    def test_eq(self):
124        docs = self.db.find({"age": 21})
125        assert len(docs) == 0
126
127        docs = self.db.find({"age": 22})
128        assert len(docs) == 1
129        assert docs[0]["user_id"] == 9
130
131        docs = self.db.find({"age": {"$eq": 22}})
132        assert len(docs) == 1
133        assert docs[0]["user_id"] == 9
134
135        docs = self.db.find({"age": 33})
136        assert len(docs) == 1
137        assert docs[0]["user_id"] == 7
138
139    def test_ne(self):
140        docs = self.db.find({"age": {"$ne": 22}})
141        assert len(docs) == len(user_docs.DOCS) - 1
142        for d in docs:
143            assert d["age"] != 22
144
145        docs = self.db.find({"$not": {"age": 22}})
146        assert len(docs) == len(user_docs.DOCS) - 1
147        for d in docs:
148            assert d["age"] != 22
149
150    def test_gt(self):
151        docs = self.db.find({"age": {"$gt": 77}})
152        assert len(docs) == 2
153        for d in docs:
154            assert d["user_id"] in (3, 13)
155
156        docs = self.db.find({"age": {"$gt": 78}})
157        assert len(docs) == 1
158        assert docs[0]["user_id"] == 3
159
160        docs = self.db.find({"age": {"$gt": 79}})
161        assert len(docs) == 0
162
163        docs = self.db.find({"company": {"$gt": "Zialactic"}})
164        assert len(docs) == 0
165
166        docs = self.db.find({"foo": {"$gt": "bar car apple"}})
167        assert len(docs) == 0
168
169        docs = self.db.find({"foo": {"$gt": "bar car"}})
170        assert len(docs) == 1
171        assert docs[0]["user_id"] == 14
172
173    def test_gte(self):
174        docs = self.db.find({"age": {"$gte": 77}})
175        assert len(docs) == 2
176        for d in docs:
177            assert d["user_id"] in (3, 13)
178
179        docs = self.db.find({"age": {"$gte": 78}})
180        assert len(docs) == 2
181        for d in docs:
182            assert d["user_id"] in (3, 13)
183
184        docs = self.db.find({"age": {"$gte": 79}})
185        assert len(docs) == 1
186        assert docs[0]["user_id"] == 3
187
188        docs = self.db.find({"age": {"$gte": 80}})
189        assert len(docs) == 0
190
191        docs = self.db.find({"company": {"$gte": "Zialactic"}})
192        assert len(docs) == 1
193        assert docs[0]["company"] == "Zialactic"
194
195        docs = self.db.find({"foo": {"$gte": "bar car apple"}})
196        assert len(docs) == 1
197        assert docs[0]["user_id"] == 14
198
199    def test_and(self):
200        docs = self.db.find({"age": 22, "manager": True})
201        assert len(docs) == 1
202        assert docs[0]["user_id"] == 9
203
204        docs = self.db.find({"age": 22, "manager": False})
205        assert len(docs) == 0
206
207        docs = self.db.find({"$and": [{"age": 22}, {"manager": True}]})
208        assert len(docs) == 1
209        assert docs[0]["user_id"] == 9
210
211        docs = self.db.find({"$and": [{"age": 22}, {"manager": False}]})
212        assert len(docs) == 0
213
214        docs = self.db.find({"$text": "Ramona", "age": 22})
215        assert len(docs) == 1
216        assert docs[0]["user_id"] == 9
217
218        docs = self.db.find({"$and": [{"$text": "Ramona"}, {"age": 22}]})
219        assert len(docs) == 1
220        assert docs[0]["user_id"] == 9
221
222        docs = self.db.find({"$and": [{"$text": "Ramona"}, {"$text": "Floyd"}]})
223        assert len(docs) == 1
224        assert docs[0]["user_id"] == 9
225
226    def test_or(self):
227        docs = self.db.find({"$or": [{"age": 22}, {"age": 33}]})
228        assert len(docs) == 2
229        for d in docs:
230            assert d["user_id"] in (7, 9)
231
232        q = {"$or": [{"$text": "Ramona"}, {"$text": "Stephanie"}]}
233        docs = self.db.find(q)
234        assert len(docs) == 2
235        for d in docs:
236            assert d["user_id"] in (0, 9)
237
238        q = {"$or": [{"$text": "Ramona"}, {"age": 22}]}
239        docs = self.db.find(q)
240        assert len(docs) == 1
241        assert docs[0]["user_id"] == 9
242
243    def test_and_or(self):
244        q = {"age": 22, "$or": [{"manager": False}, {"location.state": "Missouri"}]}
245        docs = self.db.find(q)
246        assert len(docs) == 1
247        assert docs[0]["user_id"] == 9
248
249        q = {"$or": [{"age": 22}, {"age": 43, "manager": True}]}
250        docs = self.db.find(q)
251        assert len(docs) == 2
252        for d in docs:
253            assert d["user_id"] in (9, 10)
254
255        q = {"$or": [{"$text": "Ramona"}, {"age": 43, "manager": True}]}
256        docs = self.db.find(q)
257        assert len(docs) == 2
258        for d in docs:
259            assert d["user_id"] in (9, 10)
260
261    def test_nor(self):
262        docs = self.db.find({"$nor": [{"age": 22}, {"age": 33}]})
263        assert len(docs) == 13
264        for d in docs:
265            assert d["user_id"] not in (7, 9)
266
267    def test_in_with_value(self):
268        docs = self.db.find({"age": {"$in": [1, 5]}})
269        assert len(docs) == 0
270
271        docs = self.db.find({"age": {"$in": [1, 5, 22]}})
272        assert len(docs) == 1
273        assert docs[0]["user_id"] == 9
274
275        docs = self.db.find({"age": {"$in": [1, 5, 22, 31]}})
276        assert len(docs) == 2
277        for d in docs:
278            assert d["user_id"] in (1, 9)
279
280        docs = self.db.find({"age": {"$in": [22, 31]}})
281        assert len(docs) == 2
282        for d in docs:
283            assert d["user_id"] in (1, 9)
284
285        # Limits on boolean clauses?
286        docs = self.db.find({"age": {"$in": list(range(1000))}})
287        assert len(docs) == 15
288
289    def test_in_with_array(self):
290        vals = ["Random Garbage", 52, {"Versions": {"Alpha": "Beta"}}]
291        docs = self.db.find({"favorites": {"$in": vals}})
292        assert len(docs) == 1
293        assert docs[0]["user_id"] == 1
294
295        vals = ["Lisp", "Python"]
296        docs = self.db.find({"favorites": {"$in": vals}})
297        assert len(docs) == 10
298
299        vals = [{"val1": 1, "val2": "val2"}]
300        docs = self.db.find({"test_in": {"$in": vals}})
301        assert len(docs) == 1
302        assert docs[0]["user_id"] == 2
303
304    def test_nin_with_value(self):
305        docs = self.db.find({"age": {"$nin": [1, 5]}})
306        assert len(docs) == len(user_docs.DOCS)
307
308        docs = self.db.find({"age": {"$nin": [1, 5, 22]}})
309        assert len(docs) == len(user_docs.DOCS) - 1
310        for d in docs:
311            assert d["user_id"] != 9
312
313        docs = self.db.find({"age": {"$nin": [1, 5, 22, 31]}})
314        assert len(docs) == len(user_docs.DOCS) - 2
315        for d in docs:
316            assert d["user_id"] not in (1, 9)
317
318        docs = self.db.find({"age": {"$nin": [22, 31]}})
319        assert len(docs) == len(user_docs.DOCS) - 2
320        for d in docs:
321            assert d["user_id"] not in (1, 9)
322
323        # Limits on boolean clauses?
324        docs = self.db.find({"age": {"$nin": list(range(1000))}})
325        assert len(docs) == 0
326
327    def test_nin_with_array(self):
328        vals = ["Random Garbage", 52, {"Versions": {"Alpha": "Beta"}}]
329        docs = self.db.find({"favorites": {"$nin": vals}})
330        assert len(docs) == len(user_docs.DOCS) - 1
331        for d in docs:
332            assert d["user_id"] != 1
333
334        vals = ["Lisp", "Python"]
335        docs = self.db.find({"favorites": {"$nin": vals}})
336        assert len(docs) == 5
337
338        vals = [{"val1": 1, "val2": "val2"}]
339        docs = self.db.find({"test_in": {"$nin": vals}})
340        assert len(docs) == 0
341
342    def test_all(self):
343        vals = ["Ruby", "C", "Python", {"Versions": {"Alpha": "Beta"}}]
344        docs = self.db.find({"favorites": {"$all": vals}})
345        assert len(docs) == 1
346        assert docs[0]["user_id"] == 1
347
348        # This matches where favorites either contains
349        # the nested array, or is the nested array. This is
350        # notably different than the non-nested array in that
351        # it does not match a re-ordered version of the array.
352        # The fact that user_id 14 isn't included demonstrates
353        # this behavior.
354        vals = [["Lisp", "Erlang", "Python"]]
355        docs = self.db.find({"favorites": {"$all": vals}})
356        assert len(docs) == 2
357        for d in docs:
358            assert d["user_id"] in (3, 9)
359
360    def test_exists_field(self):
361        docs = self.db.find({"exists_field": {"$exists": True}})
362        assert len(docs) == 2
363        for d in docs:
364            assert d["user_id"] in (7, 8)
365
366        docs = self.db.find({"exists_field": {"$exists": False}})
367        assert len(docs) == len(user_docs.DOCS) - 2
368        for d in docs:
369            assert d["user_id"] not in (7, 8)
370
371    def test_exists_array(self):
372        docs = self.db.find({"exists_array": {"$exists": True}})
373        assert len(docs) == 2
374        for d in docs:
375            assert d["user_id"] in (9, 10)
376
377        docs = self.db.find({"exists_array": {"$exists": False}})
378        assert len(docs) == len(user_docs.DOCS) - 2
379        for d in docs:
380            assert d["user_id"] not in (9, 10)
381
382    def test_exists_object(self):
383        docs = self.db.find({"exists_object": {"$exists": True}})
384        assert len(docs) == 2
385        for d in docs:
386            assert d["user_id"] in (11, 12)
387
388        docs = self.db.find({"exists_object": {"$exists": False}})
389        assert len(docs) == len(user_docs.DOCS) - 2
390        for d in docs:
391            assert d["user_id"] not in (11, 12)
392
393    def test_exists_object_member(self):
394        docs = self.db.find({"exists_object.should": {"$exists": True}})
395        assert len(docs) == 1
396        assert docs[0]["user_id"] == 11
397
398        docs = self.db.find({"exists_object.should": {"$exists": False}})
399        assert len(docs) == len(user_docs.DOCS) - 1
400        for d in docs:
401            assert d["user_id"] != 11
402
403    def test_exists_and(self):
404        q = {
405            "$and": [
406                {"manager": {"$exists": True}},
407                {"exists_object.should": {"$exists": True}},
408            ]
409        }
410        docs = self.db.find(q)
411        assert len(docs) == 1
412        assert docs[0]["user_id"] == 11
413
414        q = {
415            "$and": [
416                {"manager": {"$exists": False}},
417                {"exists_object.should": {"$exists": True}},
418            ]
419        }
420        docs = self.db.find(q)
421        assert len(docs) == 0
422
423        # Translates to manager exists or exists_object.should doesn't
424        # exist, which will match all docs
425        q = {"$not": q}
426        docs = self.db.find(q)
427        assert len(docs) == len(user_docs.DOCS)
428
429    def test_value_chars(self):
430        q = {"complex_field_value": '+-(){}[]^~&&*||"\\/?:!'}
431        docs = self.db.find(q)
432        assert len(docs) == 1
433
434    def test_regex(self):
435        docs = self.db.find(
436            {"age": {"$gt": 40}, "location.state": {"$regex": "(?i)new.*"}}
437        )
438        assert len(docs) == 2
439        assert docs[0]["user_id"] == 2
440        assert docs[1]["user_id"] == 10
441
442    # test lucene syntax in $text
443
444
445@unittest.skipUnless(mango.has_text_service(), "requires text service")
446class ElemMatchTests(mango.FriendDocsTextTests):
447    def test_elem_match_non_object(self):
448        q = {"bestfriends": {"$elemMatch": {"$eq": "Wolverine", "$eq": "Cyclops"}}}
449        docs = self.db.find(q)
450        self.assertEqual(len(docs), 1)
451        self.assertEqual(docs[0]["bestfriends"], ["Wolverine", "Cyclops"])
452
453        q = {"results": {"$elemMatch": {"$gte": 80, "$lt": 85}}}
454
455        docs = self.db.find(q)
456        self.assertEqual(len(docs), 1)
457        self.assertEqual(docs[0]["results"], [82, 85, 88])
458
459    def test_elem_match(self):
460        q = {"friends": {"$elemMatch": {"name.first": "Vargas"}}}
461        docs = self.db.find(q)
462        self.assertEqual(len(docs), 2)
463        for d in docs:
464            self.assertIn(d["user_id"], (0, 1))
465
466        q = {"friends": {"$elemMatch": {"name.first": "Ochoa", "name.last": "Burch"}}}
467        docs = self.db.find(q)
468        self.assertEqual(len(docs), 1)
469        self.assertEqual(docs[0]["user_id"], 4)
470
471        # Check that we can do logic in elemMatch
472        q = {"friends": {"$elemMatch": {"name.first": "Ochoa", "type": "work"}}}
473        docs = self.db.find(q)
474        self.assertEqual(len(docs), 2)
475        for d in docs:
476            self.assertIn(d["user_id"], (1, 15))
477
478        q = {
479            "friends": {
480                "$elemMatch": {
481                    "name.first": "Ochoa",
482                    "$or": [{"type": "work"}, {"type": "personal"}],
483                }
484            }
485        }
486        docs = self.db.find(q)
487        self.assertEqual(len(docs), 3)
488        for d in docs:
489            self.assertIn(d["user_id"], (1, 4, 15))
490
491        # Same as last, but using $in
492        q = {
493            "friends": {
494                "$elemMatch": {
495                    "name.first": "Ochoa",
496                    "type": {"$in": ["work", "personal"]},
497                }
498            }
499        }
500        docs = self.db.find(q)
501        self.assertEqual(len(docs), 3)
502        for d in docs:
503            self.assertIn(d["user_id"], (1, 4, 15))
504
505        q = {
506            "$and": [
507                {"friends": {"$elemMatch": {"id": 0, "name": {"$exists": True}}}},
508                {
509                    "friends": {
510                        "$elemMatch": {
511                            "$or": [
512                                {"name": {"first": "Campos", "last": "Freeman"}},
513                                {
514                                    "name": {
515                                        "$in": [
516                                            {"first": "Gibbs", "last": "Mccarty"},
517                                            {"first": "Wilkins", "last": "Chang"},
518                                        ]
519                                    }
520                                },
521                            ]
522                        }
523                    }
524                },
525            ]
526        }
527        docs = self.db.find(q)
528        self.assertEqual(len(docs), 3)
529        for d in docs:
530            self.assertIn(d["user_id"], (10, 11, 12))
531
532
533@unittest.skipUnless(mango.has_text_service(), "requires text service")
534class AllMatchTests(mango.FriendDocsTextTests):
535    def test_all_match(self):
536        q = {"friends": {"$allMatch": {"type": "personal"}}}
537        docs = self.db.find(q)
538        assert len(docs) == 2
539        for d in docs:
540            assert d["user_id"] in (8, 5)
541
542        # Check that we can do logic in allMatch
543        q = {
544            "friends": {
545                "$allMatch": {
546                    "name.first": "Ochoa",
547                    "$or": [{"type": "work"}, {"type": "personal"}],
548                }
549            }
550        }
551        docs = self.db.find(q)
552        assert len(docs) == 1
553        assert docs[0]["user_id"] == 15
554
555        # Same as last, but using $in
556        q = {
557            "friends": {
558                "$allMatch": {
559                    "name.first": "Ochoa",
560                    "type": {"$in": ["work", "personal"]},
561                }
562            }
563        }
564        docs = self.db.find(q)
565        assert len(docs) == 1
566        assert docs[0]["user_id"] == 15
567
568
569# Test numeric strings for $text
570@unittest.skipUnless(mango.has_text_service(), "requires text service")
571class NumStringTests(mango.DbPerClass):
572    @classmethod
573    def setUpClass(klass):
574        super(NumStringTests, klass).setUpClass()
575        klass.db.recreate()
576        if mango.has_text_service():
577            klass.db.create_text_index()
578
579    # not available for python 2.7.x
580    def isFinite(num):
581        not (math.isinf(num) or math.isnan(num))
582
583    @given(f=st.floats().filter(isFinite).map(str) | st.floats().map(lambda f: f.hex()))
584    @example("NaN")
585    @example("Infinity")
586    def test_floating_point_val(self, f):
587        doc = {"number_string": f}
588        self.db.save_doc(doc)
589        q = {"$text": f}
590        docs = self.db.find(q)
591        if len(docs) == 1:
592            assert docs[0]["number_string"] == f
593        if len(docs) == 2:
594            if docs[0]["number_string"] != f:
595                assert docs[1]["number_string"] == f
596        q = {"number_string": f}
597        docs = self.db.find(q)
598        if len(docs) == 1:
599            assert docs[0]["number_string"] == f
600        if len(docs) == 2:
601            if docs[0]["number_string"] != f:
602                assert docs[1]["number_string"] == f
603