summaryrefslogtreecommitdiff
path: root/jstests/aggregation/sources/setWindowFields/desugar.js
blob: a758bc55133e9ce140213690fe2dcbd280ead932 (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
/**
 * Test how $setWindowFields desugars.
 *
 * We handle partitionBy and sortBy by generating a separate $sort stage.
 *
 * @tags: [
 *   # We assume the pipeline is not split into a shardsPart and mergerPart.
 *   assumes_unsharded_collection,
 *   # We're testing the explain plan, not the query results, so the facet passthrough would fail.
 *   do_not_wrap_aggregations_in_facets,
 * ]
 */
(function() {
"use strict";

const coll = db[jsTestName()];
coll.insert({});

// Use .explain() to see what the stage desugars to.
// The result is formatted as explain-output, which differs from MQL syntax in some cases:
// for example {$sort: {a: 1}} explains as {$sort: {sortKey: {a: 1}}}.
function desugar(stage) {
    const result = coll.explain().aggregate([
        // prevent stages from being absorbed into the .find() layer
        {$_internalInhibitOptimization: {}},
        stage,
    ]);
    assert.commandWorked(result);
    assert(Array.isArray(result.stages), result);
    // The first two stages should be the .find() cursor and the inhibit-optimization stage;
    // the rest of the stages are what the user's 'stage' expanded to.
    assert(result.stages[0].$cursor, result);
    assert(result.stages[1].$_internalInhibitOptimization, result);
    return result.stages.slice(2);
}

// Often, the desugared stages include a generated temporary name.
// When this happens, it's always in the first stage, an $addFields.
function extractTmp(stages) {
    assert(stages[0].$addFields, stages);
    const tmp = Object.keys(stages[0].$addFields)[0];
    assert(tmp, stages);
    return tmp;
}

// No partitionBy and no sortBy means we don't need to sort the input.
assert.eq(desugar({$setWindowFields: {output: {}}}), [
    {$_internalSetWindowFields: {output: {}}},
]);

// 'sortBy' becomes an explicit $sort stage.
assert.eq(desugar({$setWindowFields: {sortBy: {ts: 1}, output: {}}}), [
    {$sort: {sortKey: {ts: 1}}},
    {$_internalSetWindowFields: {sortBy: {ts: 1}, output: {}}},
]);

// 'partitionBy' a field becomes an explicit $sort stage.
assert.eq(desugar({$setWindowFields: {partitionBy: "$zip", output: {}}}), [
    {$sort: {sortKey: {zip: 1}}},
    {$_internalSetWindowFields: {partitionBy: "$zip", output: {}}},
]);

// 'partitionBy' an expression becomes $set + $sort + $unset.
// Also, the _internal stage reads from the already-computed field.
let stages = desugar({$setWindowFields: {partitionBy: {$toLower: "$country"}, output: {}}});
let tmp = extractTmp(stages);
assert.eq(stages, [
    {$addFields: {[tmp]: {$toLower: ["$country"]}}},
    {$sort: {sortKey: {[tmp]: 1}}},
    {$_internalSetWindowFields: {partitionBy: '$' + tmp, output: {}}},
    {$project: {[tmp]: false, _id: true}},
]);

// $sort first by partitionBy, then sortBy, because we sort within each partition.
assert.eq(
    desugar({$setWindowFields: {partitionBy: "$zip", sortBy: {ts: -1, _id: 1}, output: {}}}), [
        {$sort: {sortKey: {zip: 1, ts: -1, _id: 1}}},
        {$_internalSetWindowFields: {partitionBy: "$zip", sortBy: {ts: -1, _id: 1}, output: {}}},
    ]);

stages = desugar({
    $setWindowFields: {partitionBy: {$toLower: "$country"}, sortBy: {ts: -1, _id: 1}, output: {}}
});
tmp = extractTmp(stages);
assert.eq(stages, [
    {$addFields: {[tmp]: {$toLower: ["$country"]}}},
    {$sort: {sortKey: {[tmp]: 1, ts: -1, _id: 1}}},
    {$_internalSetWindowFields: {partitionBy: '$' + tmp, sortBy: {ts: -1, _id: 1}, output: {}}},
    {$project: {[tmp]: false, _id: true}},
]);
})();