summaryrefslogtreecommitdiff
path: root/jstests/noPassthroughWithMongod/wildcard_index_collation.js
blob: 92addd75de6ee13884d2463e8fddb3095f8ec6e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
 * Test that $** indexes obey collation rules for document values, while the virtual $_path
 * components stored alongside these values in the index always use simple binary comparison.
 *
 * TODO: SERVER-36198: Move this test back into jstests/core/collation.js
 */
(function() {
    "user strict";

    load("jstests/aggregation/extras/utils.js");  // For arrayEq.
    load("jstests/libs/analyze_plan.js");         // For getPlanStages.
    load("jstests/libs/get_index_helpers.js");    // For GetIndexHelpers.

    const assertArrayEq = (l, r) => assert(arrayEq(l, r));

    const coll = db.wildcard_collation;
    coll.drop();

    // Extracts the winning plan for the given query and projection from the explain output.
    const winningPlan = (query, proj) => coll.find(query, proj).explain().queryPlanner.winningPlan;

    // Runs the given query and confirms that: (1) the $** was used to answer the query, (2) the
    // results produced by the $** index match the given 'expectedResults', and (3) the same output
    // is produced by a COLLSCAN with the same collation.
    function assertWildcardIndexAnswersQuery(query, expectedResults, projection) {
        // Verify that the $** index can answer this query.
        const ixScans = getPlanStages(winningPlan(query, (projection || {_id: 0})), "IXSCAN");
        assert.gt(ixScans.length, 0, tojson(coll.find(query).explain()));
        ixScans.forEach((ixScan) => assert(ixScan.keyPattern.$_path));

        // Assert that the $** index produces the expected results, and that these are the same
        // as those produced by a COLLSCAN with the same collation.
        const wildcardResults = coll.find(query, (projection || {_id: 0})).toArray();
        assertArrayEq(wildcardResults, expectedResults);
        assertArrayEq(wildcardResults,
                      coll.find(query, (projection || {_id: 0}))
                          .collation({locale: "en_US", strength: 1})
                          .hint({$natural: 1})
                          .toArray());
    }

    // Confirms that the index matching the given keyPattern has the specified collation.
    function assertIndexHasCollation(keyPattern, collation) {
        var indexSpecs = coll.getIndexes();
        var found = GetIndexHelpers.findByKeyPattern(indexSpecs, keyPattern, collation);
        assert.neq(null,
                   found,
                   "Index with key pattern " + tojson(keyPattern) + " and collation " +
                       tojson(collation) + " not found: " + tojson(indexSpecs));
    }

    try {
        // Required in order to build $** indexes.
        assert.commandWorked(
            db.adminCommand({setParameter: 1, internalQueryAllowAllPathsIndexes: true}));

        // Recreate the collection with a default case-insensitive collation.
        assert.commandWorked(
            db.createCollection(coll.getName(), {collation: {locale: "en_US", strength: 1}}));

        // Confirm that the $** index inherits the collection's default collation.
        assert.commandWorked(coll.createIndex({"$**": 1}));
        assertIndexHasCollation({"$**": 1}, {
            locale: "en_US",
            caseLevel: false,
            caseFirst: "off",
            strength: 1,
            numericOrdering: false,
            alternate: "non-ignorable",
            maxVariable: "punct",
            normalization: false,
            backwards: false,
            version: "57.1",
        });

        // Insert a series of documents whose fieldnames and values differ only by case.
        assert.commandWorked(coll.insert({a: {b: "string", c: "STRING"}, d: "sTrInG", e: 5}));
        assert.commandWorked(coll.insert({a: {b: "STRING", c: "string"}, d: "StRiNg", e: 5}));
        assert.commandWorked(coll.insert({A: {B: "string", C: "STRING"}, d: "sTrInG", E: 5}));
        assert.commandWorked(coll.insert({A: {B: "STRING", C: "string"}, d: "StRiNg", E: 5}));

        // Confirm that only the document's values adhere to the case-insensitive collation. The
        // field paths, which are also present in the $** index keys, are evaluated using simple
        // binary comparison; so for instance, path "a.b" does *not* match path "A.B".
        assertWildcardIndexAnswersQuery({"a.b": "string"}, [
            {a: {b: "string", c: "STRING"}, d: "sTrInG", e: 5},
            {a: {b: "STRING", c: "string"}, d: "StRiNg", e: 5}
        ]);
        assertWildcardIndexAnswersQuery({"A.B": "string"}, [
            {A: {B: "string", C: "STRING"}, d: "sTrInG", E: 5},
            {A: {B: "STRING", C: "string"}, d: "StRiNg", E: 5}
        ]);

        // All documents in the collection are returned if we query over both upper- and lower-case
        // fieldnames, or when the fieldname has a consistent case across all documents.
        const allDocs = coll.find({}, {_id: 0}).toArray();
        assertWildcardIndexAnswersQuery({$or: [{"a.c": "string"}, {"A.C": "string"}]}, allDocs);
        assertWildcardIndexAnswersQuery({d: "string"}, allDocs);

        // Confirm that the $** index also differentiates between upper and lower fieldname case
        // when querying fields which do not contain string values.
        assertWildcardIndexAnswersQuery({e: 5}, [
            {a: {b: "string", c: "STRING"}, d: "sTrInG", e: 5},
            {a: {b: "STRING", c: "string"}, d: "StRiNg", e: 5}
        ]);
        assertWildcardIndexAnswersQuery({E: 5}, [
            {A: {B: "string", C: "STRING"}, d: "sTrInG", E: 5},
            {A: {B: "STRING", C: "string"}, d: "StRiNg", E: 5}
        ]);

        // Confirm that the $** index produces a covered plan for a query on non-string, non-object,
        // non-array values.
        assert(isIndexOnly(coll.getDB(), winningPlan({e: 5}, {_id: 0, e: 1})));
        assert(isIndexOnly(coll.getDB(), winningPlan({E: 5}, {_id: 0, E: 1})));

        // Confirm that the $** index differentiates fieldname case when attempting to cover.
        assert(!isIndexOnly(coll.getDB(), winningPlan({e: 5}, {_id: 0, E: 1})));
        assert(!isIndexOnly(coll.getDB(), winningPlan({E: 5}, {_id: 0, e: 1})));

        // Confirm that attempting to project the virtual $_path field which is present in $** index
        // keys produces a non-covered solution, which nonetheless returns the correct results.
        assert(!isIndexOnly(coll.getDB(), winningPlan({e: 5}, {_id: 0, e: 1, $_path: 1})));
        assertWildcardIndexAnswersQuery({e: 5}, [{e: 5}, {e: 5}], {_id: 0, e: 1, $_path: 1});
    } finally {
        // Disable $** indexes once the tests have either completed or failed.
        db.adminCommand({setParameter: 1, internalQueryAllowAllPathsIndexes: false});
    }
})();