summaryrefslogtreecommitdiff
path: root/jstests/aggregation/sources/geonear/distancefield_and_includelocs.js
blob: c2406299a4b57c71c1a833b7437ea4bf410495c8 (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
/**
 * Tests the behavior of the $geoNear stage by varying 'distanceField' and 'includeLocs'
 * (specifically, by specifying nested fields, overriding existing fields, and so on).
 */
(function() {
"use strict";

load("jstests/aggregation/extras/utils.js");  // For 'customDocumentEq'.

const coll = db.getCollection("geonear_distancefield_and_includelocs");
coll.drop();

/**
 * Runs an aggregation with a $geoNear stage using 'geoSpec' and an optional $project stage
 * using 'projSpec'. Returns the first result; that is, the result closest to the "near" point.
 */
function firstGeoNearResult(geoSpec, projSpec) {
    geoSpec.spherical = true;
    const pipeline = [{$geoNear: geoSpec}, {$limit: 1}];
    if (projSpec) {
        pipeline.push({$project: projSpec});
    }

    const res = coll.aggregate(pipeline).toArray();
    assert.eq(1, res.length, tojson(res));
    return res[0];
}

// Use documents with a variety of different fields: scalars, arrays, legacy points and GeoJSON
// objects.
const docWithLegacyPoint = {
    _id: "legacy",
    geo: [1, 1],
    ptForNearQuery: [1, 1],
    scalar: "foo",
    arr: [{a: 1, b: 1}, {a: 2, b: 2}],
};
const docWithGeoPoint = {
    _id: "point",
    geo: {type: "Point", coordinates: [1, 0]},
    ptForNearQuery: [1, 0],
    scalar: "bar",
    arr: [{a: 3, b: 3}, {a: 4, b: 4}],
};
const docWithGeoLine = {
    _id: "linestring",
    geo: {type: "LineString", coordinates: [[0, 0], [-1, -1]]},
    ptForNearQuery: [-1, -1],
    scalar: "baz",
    arr: [{a: 5, b: 5}, {a: 6, b: 6}],
};

// We test with a 2dsphere index, since 2d indexes can't support GeoJSON objects.
assert.commandWorked(coll.createIndex({geo: "2dsphere"}));

// Populate the collection.
assert.commandWorked(coll.insert(docWithLegacyPoint));
assert.commandWorked(coll.insert(docWithGeoPoint));
assert.commandWorked(coll.insert(docWithGeoLine));

// Define a custom way to compare documents since the results here might differ by insignificant
// amounts.
const assertCloseEnough = (left, right) => assert(customDocumentEq({
                                                      left: left,
                                                      right: right,
                                                      valueComparator: (a, b) => {
                                                          if (typeof a !== "number") {
                                                              return a === b;
                                                          }
                                                          // Allow some minor differences in the
                                                          // numbers.
                                                          return Math.abs(a - b) < 1e-10;
                                                      }
                                                  }),
                                                  () => `[${tojson(left)}] != [${tojson(right)}]`);

[docWithLegacyPoint, docWithGeoPoint, docWithGeoLine].forEach(doc => {
    const docPlusNewFields = (newDoc) => Object.extend(Object.extend({}, doc), newDoc);

    //
    // Tests for "distanceField".
    //
    const expectedDistance = 0.0000000000000001;

    // Test that "distanceField" can be computed in a new field.
    assertCloseEnough(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "newField"}),
                      docPlusNewFields({newField: expectedDistance}));

    // Test that "distanceField" can be computed in a new nested field.
    assertCloseEnough(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "nested.field"}),
                      docPlusNewFields({nested: {field: expectedDistance}}));

    // Test that "distanceField" can overwrite an existing scalar field.
    assertCloseEnough(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "scalar"}),
                      docPlusNewFields({scalar: expectedDistance}));

    // Test that "distanceField" can completely overwrite an existing array field.
    assertCloseEnough(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "arr"}),
                      docPlusNewFields({arr: expectedDistance}));

    // TODO (SERVER-35561): When "includeLocs" shares a path prefix with an existing field, the
    // fields are overwritten, even if they could be preserved.
    assertCloseEnough(firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "arr.b"}),
                      docPlusNewFields({arr: {b: expectedDistance}}));

    //
    // Tests for both "includeLocs" and "distanceField".
    //

    // Test that "distanceField" and "includeLocs" can both be specified.
    assertCloseEnough(
        firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "dist", includeLocs: "loc"}),
        docPlusNewFields({dist: expectedDistance, loc: doc.geo}));

    // Test that "distanceField" and "includeLocs" can be the same path. The result is arbitrary
    // ("includeLocs" wins).
    assertCloseEnough(
        firstGeoNearResult(
            {near: doc.ptForNearQuery, distanceField: "newField", includeLocs: "newField"}),
        docPlusNewFields({newField: doc.geo}));

    // Test that "distanceField" and "includeLocs" are both preserved when their paths share a
    // prefix but do not conflict.
    assertCloseEnough(
        firstGeoNearResult(
            {near: doc.ptForNearQuery, distanceField: "comp.dist", includeLocs: "comp.loc"}),
        docPlusNewFields({comp: {dist: expectedDistance, loc: doc.geo}}));

    //
    // Tests for "includeLocs" only. Project out the distance field.
    //
    const removeDistFieldProj = {d: 0};

    // Test that "includeLocs" can be computed in a new field.
    assertCloseEnough(
        firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "d", includeLocs: "newField"},
                           removeDistFieldProj),
        docPlusNewFields({newField: doc.geo}));

    // Test that "includeLocs" can be computed in a new nested field.
    assertCloseEnough(
        firstGeoNearResult(
            {near: doc.ptForNearQuery, distanceField: "d", includeLocs: "nested.field"},
            removeDistFieldProj),
        docPlusNewFields({nested: {field: doc.geo}}));

    // Test that "includeLocs" can overwrite an existing scalar field.
    assertCloseEnough(
        firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "d", includeLocs: "scalar"},
                           removeDistFieldProj),
        docPlusNewFields({scalar: doc.geo}));

    // Test that "includeLocs" can completely overwrite an existing array field.
    assertCloseEnough(
        firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "d", includeLocs: "arr"},
                           removeDistFieldProj),
        docPlusNewFields({arr: doc.geo}));

    // TODO (SERVER-35561): When "includeLocs" shares a path prefix with an existing field, the
    // fields are overwritten, even if they could be preserved.
    assertCloseEnough(
        firstGeoNearResult({near: doc.ptForNearQuery, distanceField: "d", includeLocs: "arr.a"},
                           removeDistFieldProj),
        docPlusNewFields({arr: {a: doc.geo}}));
});
}());