summaryrefslogtreecommitdiff
path: root/jstests/sharding/extract_shard_key_values.js
blob: 15f9c936658b071e8b7cf26f8cdd02d82db18817 (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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//
// Tests that documents in a sharded collection with missing shard key fields are treated as if they
// contain an explicit null value for any missing fields.
//
// @tags: [
//   uses_multi_shard_transactions,
//   uses_transactions,
// ]

// Cannot run the filtering metadata check on tests that run refineCollectionShardKey.
TestData.skipCheckShardFilteringMetadata = true;

(function() {
'use strict';

load("jstests/sharding/updateOne_without_shard_key/libs/write_without_shard_key_test_util.js");

const st = new ShardingTest({shards: 2});
const mongos = st.s0;
const primaryShard = st.shard0.shardName;
const secondaryShard = st.shard1.shardName;
const kDbName = 'db';
const kCollName = 'foo';
const kNsName = kDbName + '.' + kCollName;
const kOldKeyDoc = {
    a: 1
};
const kNewKeyDoc = {
    a: 1,
    b: 1
};

function orphanDocCount() {
    // Since count() includes orphaned documents while find({}).itcount() excludes them, their
    // difference corresponds to the number of orphaned documents in 'db.foo'.
    return mongos.getCollection(kNsName).count() - mongos.getCollection(kNsName).find({}).itcount();
}

function dropAndReshardColl() {
    assert.commandWorked(mongos.getDB(kDbName).runCommand({drop: kCollName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: kNsName, key: kOldKeyDoc}));
    assert.commandWorked(mongos.getCollection(kNsName).createIndex(kNewKeyDoc));

    // Insert six documents such that three correspond to the old shard key and three correspond to
    // the new shard key. Verify that there are no orphaned documents in 'db.foo'.
    assert.commandWorked(mongos.getCollection(kNsName).insert({a: 1}));
    assert.commandWorked(mongos.getCollection(kNsName).insert({a: 10}));
    assert.commandWorked(mongos.getCollection(kNsName).insert({a: null}));
    assert.commandWorked(mongos.getCollection(kNsName).insert({a: 1, b: 1}));
    assert.commandWorked(mongos.getCollection(kNsName).insert({a: 10, b: 1}));
    assert.commandWorked(mongos.getCollection(kNsName).insert({a: null, b: 1}));
    assert.eq(0, orphanDocCount());
}

function isOwnedByShard(shardName, doc) {
    let isOwned = false;
    mongos.getCollection(kNsName)
        .find(doc)
        .explain('executionStats')
        .executionStats.executionStages.shards.forEach((shard) => {
            if (shard.shardName.localeCompare(shardName) === 0) {
                isOwned = (1 === shard.nReturned);
            }
        });
    return isOwned;
}

function isOwnedByPrimaryShard(doc) {
    return isOwnedByShard(primaryShard, doc);
}

function isOwnedBySecondaryShard(doc) {
    return isOwnedByShard(secondaryShard, doc);
}

assert.commandWorked(mongos.adminCommand({enableSharding: kDbName}));
st.ensurePrimaryShard(kDbName, primaryShard);

jsTestLog('********** ORPHAN FILTERING **********');

dropAndReshardColl();

// Verify that moving a chunk to the secondary shard produces two orphaned documents in 'db.foo' as
// a result of migrating documents {a: 10} and {a: 10, b: 1}.
assert.commandWorked(mongos.adminCommand({split: kNsName, middle: {a: 5}}));
assert.commandWorked(mongos.adminCommand({moveChunk: kNsName, find: {a: 5}, to: secondaryShard}));
assert.lte(orphanDocCount(), 2);

// Verify that refining the shard key produces no additional orphaned documents in 'db.foo'.
assert.commandWorked(mongos.adminCommand({refineCollectionShardKey: kNsName, key: kNewKeyDoc}));
assert.lte(orphanDocCount(), 2);

jsTestLog('********** REQUEST TARGETING **********');

dropAndReshardColl();

// Ensure that there exist two chunks belonging to 'db.foo' covering the entire key range.
//
// Chunk 1: {a: MinKey, b: MinKey} -->> {a: 5, b: MinKey} (belongs to the primary shard)
// Chunk 2: {a: 5, b: MinKey} -->> {a: MaxKey, b: MaxKey} (belongs to the secondary shard)
assert.commandWorked(mongos.adminCommand({split: kNsName, middle: {a: 5}}));
assert.commandWorked(mongos.adminCommand({moveChunk: kNsName, find: {a: 5}, to: secondaryShard}));
assert.commandWorked(mongos.adminCommand({refineCollectionShardKey: kNsName, key: kNewKeyDoc}));

// Verify that chunk 1 owns documents {a: 1}, {a: null}, {a: 1, b: 1}, {a: null, b: 1} while chunk 2
// owns documents {a: 10} and {a: 10, b: 1}.
assert(isOwnedByPrimaryShard({a: 1, b: {$exists: false}}));
assert(isOwnedBySecondaryShard({a: 10, b: {$exists: false}}));
assert(isOwnedByPrimaryShard({a: null, b: {$exists: false}}));
assert(isOwnedByPrimaryShard({a: 1, b: 1}));
assert(isOwnedBySecondaryShard({a: 10, b: 1}));
assert(isOwnedByPrimaryShard({a: null, b: 1}));

// Verify that find targets shards without treating missing shard key fields as null values.
let docsArr = mongos.getCollection(kNsName).find({b: 1}, {_id: 0}).sort({a: 1}).toArray();
assert.eq(3, docsArr.length);
assert.eq({a: null, b: 1}, docsArr[0]);
assert.eq({a: 1, b: 1}, docsArr[1]);
assert.eq({a: 10, b: 1}, docsArr[2]);

// Verify that count targets shards without treating missing shard key fields as null values.
assert.eq(3, mongos.getCollection(kNsName).count({b: 1}));

// Verify that distinct targets shards without treating missing shard key fields as null values.
const valuesArr = mongos.getCollection(kNsName).distinct('a').sort();
assert.eq(3, valuesArr.length);
assert.eq(1, valuesArr[0]);
assert.eq(10, valuesArr[1]);
assert.eq(null, valuesArr[2]);

// Verify that insert targets shards as if missing shard key fields were null values.
assert.commandWorked(mongos.getCollection(kNsName).insert({b: 10}));
docsArr = mongos.getCollection(kNsName).find({b: 10}).toArray();
assert(isOwnedByPrimaryShard({b: 10}));
assert(!isOwnedBySecondaryShard({b: 10}));

// Verify that delete targets shards without treating missing shard key fields as null values.
assert.commandWorked(mongos.getCollection(kNsName).insert({a: 10, b: 10}));
assert(!isOwnedByPrimaryShard({a: 10, b: 10}));
assert(isOwnedBySecondaryShard({a: 10, b: 10}));

assert.commandWorked(mongos.getCollection(kNsName).remove({b: 10}));
docsArr = mongos.getCollection(kNsName).find({b: 10}, {_id: 0}).toArray();
assert.eq(0, docsArr.length);

// Verify that a query-style update targets shards without treating missing shard key fields as null
// values.
assert.commandWorked(mongos.getCollection(kNsName).update({b: 1}, {$set: {c: 1}}, {multi: true}));
docsArr = mongos.getCollection(kNsName).find({c: 1}, {_id: 0}).sort({a: 1}).toArray();
assert.eq(3, docsArr.length);
assert.eq({a: null, b: 1, c: 1}, docsArr[0]);
assert.eq({a: 1, b: 1, c: 1}, docsArr[1]);
assert.eq({a: 10, b: 1, c: 1}, docsArr[2]);

// Verify that a replacement update targets shards while treating missing shard keys as null values.

// Insert documents all with {d: 1} so they're matched by the update query.
assert.commandWorked(mongos.getCollection(kNsName).insert({a: -100, b: 1, c: 1, d: 1}));
assert.commandWorked(mongos.getCollection(kNsName).insert({a: 0, b: 1, c: 2, d: 1}));
assert.commandWorked(mongos.getCollection(kNsName).insert({a: 100, b: 1, c: 3, d: 1}));

// Verify we cannot update a shard key value via a query that's missing the shard key, despite being
// able to target using the replacement document.

// Need to start a session to change the shard key.
const session = st.s.startSession({retryWrites: true});
const sessionDB = session.getDatabase(kDbName);
const sessionColl = sessionDB[kCollName];

// Sharded updateOnes that do not directly target a shard can now use the two phase write
// protocol to execute.
if (WriteWithoutShardKeyTestUtil.isWriteWithoutShardKeyFeatureEnabled(st.s)) {
    assert.commandWorked(sessionColl.update({d: 1}, {b: 1, c: 4, d: 1}));
} else {
    assert.commandFailedWithCode(sessionColl.update({d: 1}, {b: 1, c: 4, d: 1}), 31025);
}

// Verify that an upsert targets shards without treating missing shard key fields as null values.
// This implies that upsert still requires the entire shard key to be specified in the query.
assert.writeErrorWithCode(
    mongos.getCollection(kNsName).update({b: 1}, {$set: {c: 2}}, {upsert: true}),
    ErrorCodes.ShardKeyNotFound);

// Sharded findAndModify that do not directly target a shard can now use the two phase write
// protocol to execute.
if (WriteWithoutShardKeyTestUtil.isWriteWithoutShardKeyFeatureEnabled(st.s)) {
    assert.commandWorked(sessionColl.insert({_id: "findAndModify", a: 1}));
    let res = assert.commandWorked(sessionDB.runCommand(
        {findAndModify: kCollName, query: {a: 1}, update: {$set: {updated: true}}}));
    assert.eq(1, res.lastErrorObject.n);
} else {
    assert.commandWorked(sessionColl.insert({_id: "findAndModify", a: 1}));
    assert.commandFailedWithCode(
        sessionDB.runCommand(
            {findAndModify: kCollName, query: {a: 1}, update: {$set: {updated: true}}}),
        ErrorCodes.ShardKeyNotFound);
}

st.stop();
})();