summaryrefslogtreecommitdiff
path: root/jstests/core/views/views_validation.js
blob: a0e02d0b2c71294dbe5cfcb69587ca65f06cedbb (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
129
130
131
132
133
// @tags: [requires_non_retryable_commands]

(function() {
    "use strict";
    let viewsDb = db.getSiblingDB("views_validation");
    const kMaxViewDepth = 20;

    function makeView(viewName, viewOn, pipeline, expectedErrorCode) {
        let options = {create: viewName, viewOn: viewOn};
        if (pipeline) {
            options["pipeline"] = pipeline;
        }
        let res = viewsDb.runCommand(options);
        if (expectedErrorCode !== undefined) {
            assert.commandFailedWithCode(
                res, expectedErrorCode, "Invalid view created " + tojson(options));
        } else {
            assert.commandWorked(res, "Could not create view " + tojson(options));
        }

        return viewsDb.getCollection(viewName);
    }

    function makeLookup(from) {
        return {
            $lookup:
                {from: from, as: "as", localField: "localField", foreignField: "foreignField"}
        };
    }

    function makeGraphLookup(from) {
        return {
            $graphLookup: {
                from: from,
                as: "as",
                startWith: "startWith",
                connectFromField: "connectFromField",
                connectToField: "connectToField"
            }
        };
    }

    function makeFacet(from) {
        return {$facet: {"Facet Key": [makeLookup(from)]}};
    }

    function clear() {
        assert.commandWorked(viewsDb.dropDatabase());
    }

    clear();

    // Check that simple cycles are disallowed.
    makeView("a", "a", [], ErrorCodes.GraphContainsCycle);
    makeView("a", "b", [makeLookup("a")], ErrorCodes.GraphContainsCycle);
    clear();

    makeView("a", "b", ErrorCodes.OK);
    makeView("b", "a", [], ErrorCodes.GraphContainsCycle);
    makeView("b", "c", [makeLookup("a")], ErrorCodes.GraphContainsCycle);
    clear();

    makeView("a", "b");
    makeView("b", "c");
    makeView("c", "a", [], ErrorCodes.GraphContainsCycle);
    clear();

    /*
     * Check that view validation does not naively recurse on already visited views.
     *
     * Make a tree of depth 20 as with one view per level follows:
     *                     1
     *       -----------------------------
     *      2         2         2         2
     *    -----     -----     -----     -----
     *   3 3 3 3   3 3 3 3   3 3 3 3   3 3 3 3
     *     ...       ...       ...       ...
     *
     * So view i depends on the view (i+1) four times. Since it should only need to recurse
     * down one branch completely for each creation, since this should only need to check a maximum
     * of 20 views instead of 4^20 views.
     */

    for (let i = 1; i <= kMaxViewDepth; i++) {
        let childView = "v" + (i + 1);
        makeView("v" + i,
                 childView,
                 [makeLookup(childView), makeGraphLookup(childView), makeFacet(childView)]);
    }

    // Check that any higher depth leads to failure
    makeView("v21", "v22", [], ErrorCodes.ViewDepthLimitExceeded);
    makeView("v0", "v1", [], ErrorCodes.ViewDepthLimitExceeded);
    makeView("v0", "ok", [makeLookup("v1")], ErrorCodes.ViewDepthLimitExceeded);

    // But adding to the middle should be ok.
    makeView("vMid", "v10");
    clear();

    // Check that $graphLookup and $facet also check for cycles.
    makeView("a", "b", [makeGraphLookup("a")], ErrorCodes.GraphContainsCycle);
    makeView("a", "b", [makeGraphLookup("b")]);
    makeView("b", "c", [makeGraphLookup("a")], ErrorCodes.GraphContainsCycle);
    clear();

    makeView("a", "b", [makeFacet("a")], ErrorCodes.GraphContainsCycle);
    makeView("a", "b", [makeFacet("b")]);
    makeView("b", "c", [makeFacet("a")], ErrorCodes.GraphContainsCycle);
    clear();

    // Check that collMod also checks for cycles.
    makeView("a", "b");
    makeView("b", "c");
    assert.commandFailedWithCode(viewsDb.runCommand({collMod: "b", viewOn: "a", pipeline: []}),
                                 ErrorCodes.GraphContainsCycle,
                                 "collmod changed view to create a cycle");

    // Check that collMod disallows the specification of invalid pipelines.
    assert.commandFailedWithCode(viewsDb.runCommand({collMod: "b", viewOn: "c", pipeline: {}}),
                                 ErrorCodes.InvalidOptions,
                                 "collMod modified view to have invalid pipeline");
    assert.commandFailedWithCode(
        viewsDb.runCommand({collMod: "b", viewOn: "c", pipeline: {0: {$limit: 7}}}),
        ErrorCodes.InvalidOptions,
        "collMod modified view to have invalid pipeline");
    clear();

    // Check that invalid pipelines are disallowed. The following $lookup is missing the 'as' field.
    makeView("a",
             "b",
             [{"$lookup": {from: "a", localField: "b", foreignField: "c"}}],
             ErrorCodes.FailedToParse);
}());