summaryrefslogtreecommitdiff
path: root/jstests/change_streams/pipeline_cannot_modify_id_field.js
blob: 20909ab4f9ac3462a7f643776ef787d27bb7d6dd (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
134
135
136
137
138
139
140
141
142
143
144
/**
 * Tests that stages which modify or remove the _id field are not permitted to run in a
 * $changeStream pipeline.
 */
(function() {
"use strict";

load("jstests/libs/collection_drop_recreate.js");  // For assert[Drop|Create]Collection.

const coll = assertDropAndRecreateCollection(db, jsTestName());

// Bare-bones $changeStream pipeline which will be augmented during tests.
const changeStream = [{$changeStream: {}}];

// Test-cases of transformations that modify or remove _id, and are thus disallowed.
const idModifyingTransformations = [
    {$project: {_id: 0}},
    {$project: {_id: "newValue"}},
    {$project: {_id: "$otherField"}},
    {$project: {_id: 0, otherField: 0}},
    {$project: {_id: 0, otherField: 1}},
    {$project: {"_id._data": 0}},
    {$project: {"_id._data": 1}},
    {$project: {"_id._data": "newValue"}},
    {$project: {"_id._data": "$_id._data"}},  // Disallowed because it discards _typeBits.
    {$project: {"_id._data": "$otherField"}},
    {$project: {"_id.otherField": 1}},
    {$project: {"_id._typeBits": 0}},
    [
        {$project: {otherField: "$_id"}},
        {$project: {otherField: 0}},
        {$project: {_id: "$otherField"}}
    ],
    {$project: {_id: {data: "$_id._data", typeBits: "$_id._typeBits"}}},    // Fields renamed.
    {$project: {_id: {_typeBits: "$_id._typeBits", _data: "$_id._data"}}},  // Fields reordered.
    {$project: {_id: {_data: "$_id._typeBits", _typeBits: "$_id._data"}}},  // Fields swapped.
    {$set: {_id: "newValue"}},
    {$set: {_id: "$otherField"}},
    {$set: {"_id._data": "newValue"}},
    {$set: {"_id._data": "$otherField"}},
    {$set: {"_id.otherField": "newValue"}},  // New subfield added to _id.
    [
        {$addFields: {otherField: "$_id"}},
        {$set: {otherField: "newValue"}},
        {$set: {_id: "$otherField"}}
    ],
    [
        // Fields renamed.
        {$addFields: {newId: {data: "$_id._data", typeBits: "$_id._typeBits"}}},
        {$set: {_id: "$newId"}}
    ],
    [
        // Fields reordered.
        {$addFields: {newId: {_typeBits: "$_id._typeBits", _data: "$_id._data"}}},
        {$set: {_id: "$newId"}}
    ],
    [
        // Fields swapped.
        {$addFields: {newId: {_data: "$_id._typeBits", _typeBits: "$_id._data"}}},
        {$set: {_id: "$newId"}}
    ],
    {$replaceRoot: {newRoot: {otherField: "$_id"}}},
    {$replaceWith: {otherField: "$_id"}},
    {$redact: {$cond: {if: {$gt: ["$_id", {}]}, then: "$$DESCEND", else: "$$PRUNE"}}}  // _id:0
];

// Test-cases of projections which are allowed: explicit inclusion of _id, implicit inclusion of
// _id, renames which retain the full _id field, exclusion of unrelated fields, addition of and
// modifications to unrelated fields, sequential renames which ultimately preserve _id, etc.
const idPreservingTransformations = [
    {$project: {_id: 1}},
    {$project: {_id: 1, otherField: 0}},
    {$project: {_id: 1, otherField: 1}},
    {$project: {_id: "$_id", otherField: 1}},
    {$project: {"_id.otherField": 0}},
    {$project: {otherField: 1}},
    {$project: {otherField: 0}},
    {$project: {otherField: "$_id"}},
    [
        {$project: {otherField: "$_id"}},
        {$project: {otherField: 1}},
        {$project: {_id: "$otherField"}}
    ],
    {$project: {"_id._data": 1, "_id._typeBits": 1}},
    {$project: {_id: {_data: "$_id._data", _typeBits: "$_id._typeBits"}}},
    {$set: {_id: "$_id"}},
    {$addFields: {otherField: "newValue"}},
    {$set: {_id: {_data: "$_id._data", _typeBits: "$_id._typeBits"}}},
    [{$addFields: {otherField: "$_id"}}, {$set: {_id: "$otherField"}}],
    [
        {$addFields: {newId: {_data: "$_id._data", _typeBits: "$_id._typeBits"}}},
        {$set: {_id: "$newId"}}
    ],
    {$replaceRoot: {newRoot: {_id: "$_id"}}},
    {$replaceWith: {_id: "$_id"}},
    {
        $redact: {
            $cond: {
                if: {
                    $or: [
                        // Keeps _id, descends into fullDocument.
                        {$not: {$isArray: "$tags"}},
                        {$gt: [{$size: {$setIntersection: ["$tags", ["USA"]]}}, 0]}
                    ]
                },
                then: "$$DESCEND",
                else: "$$PRUNE"
            }
        }
    },
    {$redact: "$$DESCEND"},  // Descends through entire document, retaining all of it.
    {$redact: "$$KEEP"}      // Keeps entire document.
];

let docId = 0;

// Verify that each of the whitelisted transformations above succeeds.
for (let transform of idPreservingTransformations) {
    const cmdRes = assert.commandWorked(
        db.runCommand(
            {aggregate: coll.getName(), pipeline: changeStream.concat(transform), cursor: {}}),
        transform);
    assert.commandWorked(coll.insert({_id: docId++}));
    assert.soon(() => {
        const getMoreRes = assert.commandWorked(
            db.runCommand({getMore: cmdRes.cursor.id, collection: coll.getName()}), transform);
        return getMoreRes.cursor.nextBatch.length > 0;
    }, transform);
}

// Verify that each of the blacklisted transformations above are rejected.
for (let transform of idModifyingTransformations) {
    const cmdRes = assert.commandWorked(
        db.runCommand(
            {aggregate: coll.getName(), pipeline: changeStream.concat(transform), cursor: {}}),
        transform);
    assert.commandWorked(coll.insert({_id: docId++}));
    assert.soon(() => {
        const getMoreRes = db.runCommand({getMore: cmdRes.cursor.id, collection: coll.getName()});
        return !getMoreRes.ok &&
            assert.commandFailedWithCode(getMoreRes, ErrorCodes.ChangeStreamFatalError, transform);
    }, transform);
}
}());