Compare commits
18 Commits
server-623
...
compound-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f5f78e39 | ||
|
|
00c53e2a6f | ||
|
|
5125c02156 | ||
|
|
f071c009b5 | ||
|
|
a3d9c52375 | ||
|
|
9193a7d1fb | ||
|
|
f37dc54f9f | ||
|
|
2f99cbab75 | ||
|
|
7d9c507576 | ||
|
|
82f3289d6b | ||
|
|
42066838c1 | ||
|
|
72d91de84c | ||
|
|
ebecb2102d | ||
|
|
fb8ebd45d8 | ||
|
|
a29dc11773 | ||
|
|
427fcea1ba | ||
|
|
0d6b561250 | ||
|
|
cfced033de |
142
data_loader.js
Normal file
142
data_loader.js
Normal file
@@ -0,0 +1,142 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
load("skunk_shared.js"); // For 'SharedSkunkState.'
|
||||
|
||||
const collNaive = db.naive;
|
||||
const collAttributePattern = db.attribute_pattern;
|
||||
const collEnhancedAttributePattern = db.enhanced_attribute_pattern;
|
||||
const collSingleWildCard = db.wildcard;
|
||||
const collCompoundWildCard = db.compound_wildcard;
|
||||
|
||||
const kIndexesForNaive = [
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr1": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr2": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr3": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr4": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr5": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr6": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr7": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr8": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr9": 1},
|
||||
{"field1": 1, "field2": 1, "field3": 1, "attributes.attr10": 1}
|
||||
];
|
||||
|
||||
const kIndexesForAttributes =
|
||||
[{"field1": 1, "field2": 1, "field3": 1, "attributes.k": 1, "attributes.v": 1}];
|
||||
const kIndexesForEnhancedAttributes = [{"field1": 1, "field2": 1, "field3": 1, "attributes": 1}];
|
||||
|
||||
const kIndexesForWildcard = [{"attributes.$**": 1}];
|
||||
const kIndexesForCompoundWildcard = [{field1: 1, field2: 1, field3: 1, "attributes.$**": 1}];
|
||||
|
||||
/**
|
||||
* Generates a document based on a template. It will take a template document, it will take
|
||||
* field names and data types.
|
||||
*
|
||||
* 'template':
|
||||
* The document must have a list of scalar fields, the name and data types of these fields will
|
||||
* be the same in the output document with random values. The document must have a field called
|
||||
* attribute that should be a subdocument and all its fields should be scalar (no arrays, no
|
||||
* subdocuments).
|
||||
*
|
||||
* If 'add_id' is true, generates a new ObjectId for the _id.
|
||||
*
|
||||
* Returns an object with random values based on the template
|
||||
*/
|
||||
function getBaseDocument(template, add_id) {
|
||||
let output = {}
|
||||
// iterates thru each field
|
||||
for (let [key, value] of Object.entries(template)) {
|
||||
if (key != SharedSkunkState.kAttributesField) {
|
||||
// If the field is not attribute get a random value based on the current type
|
||||
output[key] = SharedSkunkState.getRandomValue(value)
|
||||
} else {
|
||||
// If this is the attribute field it needs to be a subdocument or sub-array
|
||||
if (value instanceof Array) {
|
||||
let attributes = [];
|
||||
for (let entry of value) {
|
||||
attributes.push({k: entry.k, v: SharedSkunkState.getRandomValue(entry.v)});
|
||||
}
|
||||
output[key] = attributes;
|
||||
} else {
|
||||
let attributes = {};
|
||||
for (let [attrKey, attrValue] of Object.entries(value)) {
|
||||
attributes[attrKey] = SharedSkunkState.getRandomValue(attrValue);
|
||||
}
|
||||
output[key] = attributes;
|
||||
}
|
||||
}
|
||||
// overrides _id
|
||||
if (add_id) {
|
||||
output["_id"] = new ObjectId();
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function loadData(collection, indexes, template) {
|
||||
const kNumDocs = 100 * 1000;
|
||||
jsTestLog("Building indexes: " + tojson(indexes));
|
||||
for (let index of indexes) {
|
||||
collection.createIndex(index);
|
||||
}
|
||||
jsTestLog("Building bulk op for insert... Example doc: " + tojson(getBaseDocument(template)));
|
||||
const bulkOp = collection.initializeUnorderedBulkOp();
|
||||
for (let docId = 0; docId < kNumDocs; ++docId) {
|
||||
bulkOp.insert(getBaseDocument(template));
|
||||
}
|
||||
jsTestLog("Starting clock for insert...");
|
||||
let elapsed = Date.timeFunc(() => bulkOp.execute());
|
||||
jsTestLog(`Loading data done: ${elapsed}ms`);
|
||||
const indexStats = collection.aggregate([{$collStats: {storageStats: {scale: 1024 * 1024}}}])
|
||||
.toArray()[0]
|
||||
.storageStats.indexSizes;
|
||||
jsTestLog(`Index stats: ${tojson(indexStats)}`);
|
||||
return [elapsed, indexStats];
|
||||
}
|
||||
|
||||
let allStats = {};
|
||||
jsTestLog("Loading data for naive configuration...");
|
||||
let [elapsed, indexStats] = loadData(collNaive, kIndexesForNaive, SharedSkunkState.kTemplateDoc);
|
||||
allStats.naive = {
|
||||
loadingTime: elapsed,
|
||||
indexStats: indexStats,
|
||||
};
|
||||
|
||||
jsTestLog("Loading data for attribute configuration...");
|
||||
[elapsed, indexStats] =
|
||||
loadData(collAttributePattern, kIndexesForAttributes, SharedSkunkState.kAttributeTemplateDoc);
|
||||
allStats.attributePattern = {
|
||||
loadingTime: elapsed,
|
||||
indexStats: indexStats,
|
||||
};
|
||||
|
||||
jsTestLog("Loading data for enhanced attribute configuration...");
|
||||
[elapsed, indexStats] = loadData(collEnhancedAttributePattern,
|
||||
kIndexesForEnhancedAttributes,
|
||||
SharedSkunkState.kAttributeTemplateDoc);
|
||||
allStats.enhancedAttributePattern = {
|
||||
loadingTime: elapsed,
|
||||
indexStats: indexStats
|
||||
};
|
||||
|
||||
/*
|
||||
jsTestLog("Loading data for wildcard configuration...");
|
||||
[elapsed, indexStats] =
|
||||
loadData(collSingleWildCard, kIndexesForWildcard, SharedSkunkState.kTemplateDoc);
|
||||
allStats.singleWildcard = {
|
||||
loadingTime: elapsed,
|
||||
indexStats: indexStats,
|
||||
};
|
||||
*/
|
||||
|
||||
jsTestLog("Loading data for compound wildcard configuration...");
|
||||
[elapsed, indexStats] =
|
||||
loadData(collCompoundWildCard, kIndexesForCompoundWildcard, SharedSkunkState.kTemplateDoc);
|
||||
allStats.compoundWildcard = {
|
||||
loadingTime: elapsed,
|
||||
indexStats: indexStats,
|
||||
};
|
||||
|
||||
jsTestLog("Finished! " + tojson(allStats));
|
||||
}());
|
||||
@@ -59,6 +59,10 @@ createIndexAndVerifyWithDrop({"$**": 1},
|
||||
createIndexAndVerifyWithDrop({"$**": 1},
|
||||
{wildcardProjection: {_id: 0, a: 1, b: 1, c: 1}, name: kIndexName});
|
||||
|
||||
// Can create compound wildcard indexes.
|
||||
createIndexAndVerifyWithDrop({"$**": 1, "a": 1}, {wildcardProjection: {a: 0}, name: kIndexName});
|
||||
createIndexAndVerifyWithDrop({"a": 1, "$**": 1, }, {wildcardProjection: {a: 0}, name: kIndexName});
|
||||
|
||||
// Cannot create a wildcard index with a non-positive numeric key value.
|
||||
coll.dropIndexes();
|
||||
assert.commandFailedWithCode(coll.createIndex({"$**": 0}), ErrorCodes.CannotCreateIndex);
|
||||
@@ -95,10 +99,6 @@ assert.commandFailedWithCode(coll.createIndex({"a.$**": "text"}), ErrorCodes.Can
|
||||
assert.commandFailedWithCode(coll.createIndex({"a": "wildcard"}), ErrorCodes.CannotCreateIndex);
|
||||
assert.commandFailedWithCode(coll.createIndex({"$**": "wildcard"}), ErrorCodes.CannotCreateIndex);
|
||||
|
||||
// Cannot create a compound wildcard index.
|
||||
assert.commandFailedWithCode(coll.createIndex({"$**": 1, "a": 1}), ErrorCodes.CannotCreateIndex);
|
||||
assert.commandFailedWithCode(coll.createIndex({"a": 1, "$**": 1}), ErrorCodes.CannotCreateIndex);
|
||||
|
||||
// Cannot create an wildcard index with an invalid spec.
|
||||
assert.commandFailedWithCode(coll.createIndex({"a.$**.$**": 1}), ErrorCodes.CannotCreateIndex);
|
||||
assert.commandFailedWithCode(coll.createIndex({"$**.$**": 1}), ErrorCodes.CannotCreateIndex);
|
||||
|
||||
169
skunk_2021_perf.js
Normal file
169
skunk_2021_perf.js
Normal file
@@ -0,0 +1,169 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
load("skunk_shared.js"); // For 'SharedSkunkState.'
|
||||
|
||||
const collNaive = db.naive;
|
||||
const collAttributePattern = db.attribute_pattern;
|
||||
const collEnhancedAttributePattern = db.enhanced_attribute_pattern;
|
||||
const collSingleWildCard = db.wildcard;
|
||||
const collCompoundWildCard = db.compound_wildcard;
|
||||
|
||||
// Maximum value for the integer fields, minimum is 0
|
||||
const kMaxInt = 20;
|
||||
|
||||
// Minimum and maximum values for the DateTime fields
|
||||
const kBaseDate = new ISODate("1970-01-01T00:00:00Z");
|
||||
const kMaxDate = new ISODate("2070-01-01T00:00:00Z");
|
||||
const kMaximumSeconds = (kMaxDate.getTime() - kBaseDate.getTime()) / 1000;
|
||||
|
||||
function addAttributesToQuery(andClauses, attributes, numAttrs, useEnhancedAttributePattern) {
|
||||
// If this is the attribute field it needs to be a subdocument or sub-array
|
||||
if (attributes instanceof Array) {
|
||||
assert.lte(numAttrs, attributes.length);
|
||||
// Be sure to avoid selecting 'numAttrs' with replacement. If we allow specifying the same
|
||||
// attribute twice, we are likely to specify a query which is tautalogically false such as
|
||||
// attr1 = 4 and attr1 = 7.
|
||||
attributes = attributes.slice(); // Make a copy to avoid modifying the template.
|
||||
for (let i = 0; i < numAttrs; i++) {
|
||||
let selectedEntryIndex = Random.randInt(attributes.length);
|
||||
let attrEntry = attributes[selectedEntryIndex];
|
||||
attributes.splice(selectedEntryIndex, 1); // Take it out to avoid selecting it again.
|
||||
if (useEnhancedAttributePattern) {
|
||||
andClauses.push({
|
||||
[SharedSkunkState.kAttributesField]:
|
||||
{k: attrEntry.k, v: SharedSkunkState.getRandomValue(attrEntry.v)}
|
||||
});
|
||||
} else {
|
||||
andClauses.push({
|
||||
[SharedSkunkState.kAttributesField]: {
|
||||
$elemMatch:
|
||||
{k: attrEntry.k, v: SharedSkunkState.getRandomValue(attrEntry.v)}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let attrEntries = Object.entries(attributes);
|
||||
assert.lte(numAttrs, attrEntries.length);
|
||||
// Be sure to avoid selecting 'numAttrs' with replacement. If we allow specifying the same
|
||||
// attribute twice, we are likely to specify a query which is tautalogically false such as
|
||||
// attr1 = 4 and attr1 = 7.
|
||||
for (let i = 0; i < numAttrs; i++) {
|
||||
let selectedEntryIndex = Random.randInt(attrEntries.length);
|
||||
let [attrKey, attrVal] = attrEntries[selectedEntryIndex];
|
||||
attrEntries.splice(selectedEntryIndex, 1); // Take it out to avoid selecting it again.
|
||||
andClauses.push({
|
||||
[`${SharedSkunkState.kAttributesField}.${attrKey}`]:
|
||||
SharedSkunkState.getRandomValue(attrVal)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a compound equality query based off the template doc querying 'numField' top level fields
|
||||
// and 'numAttr' attributes stored within 'SharedSkunkState.kAttributesField'. The fields at the top
|
||||
// level matter for which index may be applicable, so they will be applied in order given in the
|
||||
// template document. The attributes will be added randomly - so a query on two attributes may be on
|
||||
// the 4th and 10th attribute for example.
|
||||
function buildQuery(template, numFields, numAttrs, useEnhancedAttributePattern) {
|
||||
let andClauses = [];
|
||||
let fieldsAdded = 0;
|
||||
for (let [topKey, topValue] of Object.entries(template)) {
|
||||
if (fieldsAdded == numFields) {
|
||||
break;
|
||||
}
|
||||
assert(
|
||||
topKey != SharedSkunkState.kAttributesField,
|
||||
"Added too many top-level fields. Ran out of non-attribute fields in the template (expected non-attribute fields to come first).");
|
||||
andClauses.push({[topKey]: SharedSkunkState.getRandomValue(topValue)});
|
||||
++fieldsAdded;
|
||||
}
|
||||
const attributes = template[SharedSkunkState.kAttributesField];
|
||||
addAttributesToQuery(andClauses, attributes, numAttrs, useEnhancedAttributePattern);
|
||||
return {$and: andClauses};
|
||||
}
|
||||
|
||||
function avgTime(func, runs) {
|
||||
let a = [];
|
||||
runs = runs || 10;
|
||||
|
||||
for (var i = 0; i < runs; i++) {
|
||||
a.push(Date.timeFunc(func))
|
||||
}
|
||||
|
||||
let out = {avg: Array.avg(a), stdDev: Array.stdDev(a)};
|
||||
out.sampStdDev = Math.sqrt((1 / (runs - 1)) * (out.stdDev * out.stdDev));
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildBatchOfQueries(template, numFields, numAttrs, useEnhancedAttributePattern) {
|
||||
const kNumUniqueQueries = 1000;
|
||||
|
||||
let allQueries = [];
|
||||
for (let i = 0; i < kNumUniqueQueries; i++) {
|
||||
allQueries.push(buildQuery(template, numFields, numAttrs, useEnhancedAttributePattern));
|
||||
}
|
||||
return allQueries;
|
||||
}
|
||||
|
||||
function runQueries(collection, queryBatch) {
|
||||
let numResults = [];
|
||||
for (let query of queryBatch) {
|
||||
numResults.push(collection.find(query).itcount());
|
||||
}
|
||||
print("Average number of results: " + Array.avg(numResults));
|
||||
}
|
||||
|
||||
function testAllNumAttrs(collection, templateDoc, maxNumAttrs, useEnhancedAttributePattern) {
|
||||
let allTimingInfo = [];
|
||||
for (let numAttrs = 1; numAttrs <= maxNumAttrs; ++numAttrs) {
|
||||
let queries = buildBatchOfQueries(templateDoc, 3, numAttrs, useEnhancedAttributePattern);
|
||||
jsTestLog("Example query: " + tojson(queries[0]));
|
||||
jsTestLog(`About to benchmark with ${numAttrs} attributes...`);
|
||||
let timingInfo = avgTime(() => runQueries(collection, queries), 5);
|
||||
jsTestLog("Avg time: " + tojson(timingInfo));
|
||||
allTimingInfo.push(timingInfo);
|
||||
}
|
||||
return allTimingInfo;
|
||||
}
|
||||
|
||||
const kMaxNumAttrs = 5;
|
||||
let allStats = {};
|
||||
jsTestLog("Testing compound wildcard configuration...");
|
||||
let allTimingInfo =
|
||||
testAllNumAttrs(collCompoundWildCard, SharedSkunkState.kTemplateDoc, kMaxNumAttrs);
|
||||
allStats.compoundWildcard = {
|
||||
timingInfo: allTimingInfo
|
||||
};
|
||||
|
||||
jsTestLog("Testing enhanced attribute configuration...");
|
||||
allTimingInfo = testAllNumAttrs(
|
||||
collEnhancedAttributePattern, SharedSkunkState.kAttributeTemplateDoc, kMaxNumAttrs, true);
|
||||
allStats.enhancedAttributePattern = {
|
||||
timingInfo: allTimingInfo
|
||||
};
|
||||
|
||||
jsTestLog("Testing attribute configuration...");
|
||||
allTimingInfo =
|
||||
testAllNumAttrs(collAttributePattern, SharedSkunkState.kAttributeTemplateDoc, kMaxNumAttrs);
|
||||
allStats.attributePattern = {
|
||||
timingInfo: allTimingInfo
|
||||
};
|
||||
|
||||
jsTestLog("Testing naive configuration...");
|
||||
allTimingInfo = testAllNumAttrs(collNaive, SharedSkunkState.kTemplateDoc, kMaxNumAttrs);
|
||||
allStats.naive = {
|
||||
timingInfo: allTimingInfo
|
||||
};
|
||||
|
||||
/*
|
||||
jsTestLog("Testing wildcard configuration...");
|
||||
allTimingInfo = testAllNumAttrs(collSingleWildCard, SharedSkunkState.kTemplateDoc, kMaxNumAttrs);
|
||||
allStats.singleWildcard = {
|
||||
timingInfo: allTimingInfo
|
||||
};
|
||||
*/
|
||||
|
||||
jsTestLog("Finished! " + tojson(allStats));
|
||||
}());
|
||||
131
skunk_shared.js
Normal file
131
skunk_shared.js
Normal file
@@ -0,0 +1,131 @@
|
||||
"use strict";
|
||||
|
||||
const SharedSkunkState = (function() {
|
||||
// String Catalog
|
||||
const kStringCatalog = [
|
||||
"foo",
|
||||
"bar",
|
||||
"baz",
|
||||
"qux",
|
||||
"quux",
|
||||
"corge",
|
||||
"grault",
|
||||
"garply",
|
||||
"waldo",
|
||||
"fred",
|
||||
"plugh",
|
||||
"xyzzy",
|
||||
"thud"
|
||||
];
|
||||
|
||||
// Maximum value for the integer fields, minimum is 0
|
||||
const kMaxInt = 20;
|
||||
|
||||
const kTemplateDoc = {
|
||||
field1: "",
|
||||
field2: "",
|
||||
field3: "",
|
||||
field4: 1,
|
||||
attributes: {
|
||||
attr1: "",
|
||||
attr2: "",
|
||||
attr3: 1,
|
||||
attr4: 1,
|
||||
attr5: 1,
|
||||
attr6: 1,
|
||||
attr7: 1,
|
||||
attr8: "",
|
||||
attr9: "",
|
||||
attr10: ""
|
||||
}
|
||||
};
|
||||
|
||||
const kAttributeTemplateDoc = {
|
||||
field1: "",
|
||||
field2: "",
|
||||
field3: "",
|
||||
field4: 1,
|
||||
attributes: [
|
||||
{k: "attr1", v: ""},
|
||||
{k: "attr2", v: ""},
|
||||
{k: "attr3", v: 1},
|
||||
{k: "attr4", v: 1},
|
||||
{k: "attr5", v: 1},
|
||||
{k: "attr6", v: 1},
|
||||
{k: "attr7", v: 1},
|
||||
{k: "attr8", v: ""},
|
||||
{k: "attr9", v: ""},
|
||||
{k: "attr10", v: ""},
|
||||
]
|
||||
};
|
||||
|
||||
const kEqualityQueryTemplate = {
|
||||
"field1": "",
|
||||
"field2": "",
|
||||
"field3": "",
|
||||
"attributes": {
|
||||
"attr1": "",
|
||||
"attr2": "",
|
||||
"attr3": 1,
|
||||
"attr4": 1,
|
||||
"attr5": 1,
|
||||
"attr6": 1,
|
||||
"attr7": 1,
|
||||
"attr8": "",
|
||||
"attr9": "",
|
||||
"attr10": ""
|
||||
}
|
||||
};
|
||||
|
||||
// name of the attrbiute field
|
||||
const kAttributesField = "attributes";
|
||||
|
||||
const kAttributesToQuery = 10;
|
||||
|
||||
/**
|
||||
* Generates a random dictated by the type of 'exampleValue'.
|
||||
*/
|
||||
function getRandomValue(exampleValue) {
|
||||
switch (typeof exampleValue) {
|
||||
case "number":
|
||||
return Random.randInt(kMaxInt);
|
||||
|
||||
case "boolean":
|
||||
return Random.randInt() % 2 == 0;
|
||||
|
||||
case "object":
|
||||
if (exampleValue == null) {
|
||||
return null
|
||||
}
|
||||
if (exampleValue instanceof Date) {
|
||||
return new Date(kBaseDate.getTime() + (Random.rand() * kMaximumSeconds));
|
||||
}
|
||||
throw Error("Unknown type");
|
||||
|
||||
case "string":
|
||||
return kStringCatalog[Random.randInt(kStringCatalog.length)];
|
||||
}
|
||||
throw Error("Unknown type");
|
||||
}
|
||||
|
||||
// Randomness generator
|
||||
Random.setRandomSeed();
|
||||
return {
|
||||
getRandomValue: getRandomValue,
|
||||
kTemplateDoc: kTemplateDoc,
|
||||
kAttributeTemplateDoc: kAttributeTemplateDoc,
|
||||
kAttributesField: kAttributesField,
|
||||
};
|
||||
})();
|
||||
|
||||
/*
|
||||
let query = {
|
||||
"$and": [
|
||||
{"field1": "qux"},
|
||||
{"field2": "waldo"},
|
||||
{"field3": "baz"},
|
||||
{"attributes": {"$elemMatch": {"k": "attr4", "v": 5}}},
|
||||
{"attributes": {"$elemMatch": {"k": "attr7", "v": 13}}}
|
||||
]
|
||||
};
|
||||
*/
|
||||
@@ -191,12 +191,6 @@ Status validateKeyPattern(const BSONObj& key, IndexDescriptor::IndexVersion inde
|
||||
<< "' index must be a non-zero number, not a string.");
|
||||
}
|
||||
|
||||
// Check if the wildcard index is compounded. If it is the key is invalid because
|
||||
// compounded wildcard indexes are disallowed.
|
||||
if (pluginName == IndexNames::WILDCARD && key.nFields() != 1) {
|
||||
return Status(code, "wildcard indexes do not allow compounding");
|
||||
}
|
||||
|
||||
// Ensure that the fields on which we are building the index are valid: a field must not
|
||||
// begin with a '$' unless it is part of a wildcard, DBRef or text index, and a field path
|
||||
// cannot contain an empty field. If a field cannot be created or updated, it should not be
|
||||
|
||||
@@ -66,6 +66,10 @@ public:
|
||||
return _keyGen.getWildcardProjection();
|
||||
}
|
||||
|
||||
BSONObj getKeyPattern() const {
|
||||
return _keyGen.getKeyPattern();
|
||||
}
|
||||
|
||||
private:
|
||||
void doGetKeys(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
const BSONObj& obj,
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "mongo/db/exec/projection_executor.h"
|
||||
#include "mongo/db/exec/projection_executor_builder.h"
|
||||
#include "mongo/db/jsobj.h"
|
||||
#include "mongo/db/matcher/expression_algo.h"
|
||||
#include "mongo/db/query/collation/collation_index_key.h"
|
||||
#include "mongo/db/query/projection_parser.h"
|
||||
|
||||
@@ -61,39 +62,99 @@ void popPathComponent(BSONElement elem, bool enclosingObjIsArray, FieldRef* path
|
||||
pathToElem->removeLastPart();
|
||||
}
|
||||
}
|
||||
|
||||
void validateWildcardIndexKeys(const std::vector<StringData>& nonWildcardFields,
|
||||
const StringData& wildcardField,
|
||||
const std::vector<StringData>& projectionInclusionSet,
|
||||
const std::set<StringData>& projectionExclusionSet) {
|
||||
for (const auto& nonWildcardField : nonWildcardFields) {
|
||||
if (wildcardField == "$**") {
|
||||
if (projectionInclusionSet.size()) {
|
||||
for (const auto& projInc : projectionInclusionSet) {
|
||||
uassert(
|
||||
9999901,
|
||||
"Compound wildcard index fields overlaps with the projected wildcard field",
|
||||
!(projInc == nonWildcardField ||
|
||||
expression::isPathPrefixOf(projInc, nonWildcardField) ||
|
||||
expression::isPathPrefixOf(nonWildcardField, projInc)));
|
||||
}
|
||||
} else {
|
||||
uassert(9999902,
|
||||
"Wildcard index does not allow compound key fields with the toplevel "
|
||||
"wildcard field without exclusion projection on them",
|
||||
std::find_if(projectionExclusionSet.begin(),
|
||||
projectionExclusionSet.end(),
|
||||
[&](const auto& exclude) {
|
||||
return exclude == nonWildcardField ||
|
||||
expression::isPathPrefixOf(exclude, nonWildcardField);
|
||||
}) != projectionExclusionSet.end());
|
||||
}
|
||||
}
|
||||
uassert(9999903,
|
||||
"Compound wildcard index does not allow fields overlapping with the wildcard field",
|
||||
!(wildcardField == nonWildcardField ||
|
||||
expression::isPathPrefixOf(wildcardField, nonWildcardField) ||
|
||||
expression::isPathPrefixOf(nonWildcardField, wildcardField)));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
constexpr StringData WildcardKeyGenerator::kSubtreeSuffix;
|
||||
|
||||
WildcardProjection WildcardKeyGenerator::createProjectionExecutor(BSONObj keyPattern,
|
||||
BSONObj pathProjection) {
|
||||
// We should never have a key pattern that contains more than a single element.
|
||||
invariant(keyPattern.nFields() == 1);
|
||||
WildcardProjection* proj = nullptr;
|
||||
std::vector<StringData> nonWildcardFields;
|
||||
StringData wildcardField;
|
||||
for (const auto& elem : keyPattern) {
|
||||
if (auto keyStr = elem.fieldNameStringData();
|
||||
(keyStr == "$**") || keyStr.endsWith(".$**")) {
|
||||
uassert(
|
||||
9999900, "Wildcard index does not allow multiple wildcard compound keys", !proj);
|
||||
// The _keyPattern is either { "$**": 1 } for all paths or { "path.$**": 1 } for a
|
||||
// single subtree. If we are indexing a single subtree, then we will project just that
|
||||
// path.
|
||||
auto suffixPos = keyStr.find(kSubtreeSuffix);
|
||||
wildcardField = keyStr.substr(0, suffixPos);
|
||||
|
||||
// The _keyPattern is either { "$**": ±1 } for all paths or { "path.$**": ±1 } for a single
|
||||
// subtree. If we are indexing a single subtree, then we will project just that path.
|
||||
auto indexRoot = keyPattern.firstElement().fieldNameStringData();
|
||||
auto suffixPos = indexRoot.find(kSubtreeSuffix);
|
||||
// If we're indexing a single subtree, we can't also specify a path projection.
|
||||
invariant(suffixPos == std::string::npos || pathProjection.isEmpty());
|
||||
|
||||
// If we're indexing a single subtree, we can't also specify a path projection.
|
||||
invariant(suffixPos == std::string::npos || pathProjection.isEmpty());
|
||||
// If this is a subtree projection, the projection spec is { "path.to.subtree": 1 }.
|
||||
// Otherwise, we use the path projection from the original command object. If the path
|
||||
// projection is empty we default to {_id: 0}, since empty projections are illegal and
|
||||
// will be rejected when parsed.
|
||||
auto projSpec = (suffixPos != std::string::npos
|
||||
? BSON(wildcardField << 1)
|
||||
: pathProjection.isEmpty() ? kDefaultProjection : pathProjection);
|
||||
|
||||
// If this is a subtree projection, the projection spec is { "path.to.subtree": 1 }. Otherwise,
|
||||
// we use the path projection from the original command object. If the path projection is empty
|
||||
// we default to {_id: 0}, since empty projections are illegal and will be rejected when parsed.
|
||||
auto projSpec = (suffixPos != std::string::npos
|
||||
? BSON(indexRoot.substr(0, suffixPos) << 1)
|
||||
: pathProjection.isEmpty() ? kDefaultProjection : pathProjection);
|
||||
|
||||
// Construct a dummy ExpressionContext for ProjectionExecutor. It's OK to set the
|
||||
// ExpressionContext's OperationContext and CollatorInterface to 'nullptr' and the namespace
|
||||
// string to '' here; since we ban computed fields from the projection, the ExpressionContext
|
||||
// will never be used.
|
||||
auto expCtx = make_intrusive<ExpressionContext>(nullptr, nullptr, NamespaceString());
|
||||
auto policies = ProjectionPolicies::wildcardIndexSpecProjectionPolicies();
|
||||
auto projection = projection_ast::parse(expCtx, projSpec, policies);
|
||||
return WildcardProjection{projection_executor::buildProjectionExecutor(
|
||||
expCtx, &projection, policies, projection_executor::kDefaultBuilderParams)};
|
||||
// Construct a dummy ExpressionContext for ProjectionExecutor. It's OK to set the
|
||||
// ExpressionContext's OperationContext and CollatorInterface to 'nullptr' and the
|
||||
// namespace string to '' here; since we ban computed fields from the projection, the
|
||||
// ExpressionContext will never be used.
|
||||
auto expCtx = make_intrusive<ExpressionContext>(nullptr, nullptr, NamespaceString());
|
||||
auto policies = ProjectionPolicies::wildcardIndexSpecProjectionPolicies();
|
||||
auto projection = projection_ast::parse(expCtx, projSpec, policies);
|
||||
proj = new WildcardProjection{projection_executor::buildProjectionExecutor(
|
||||
expCtx, &projection, policies, projection_executor::kDefaultBuilderParams)};
|
||||
} else {
|
||||
nonWildcardFields.push_back(elem.fieldNameStringData());
|
||||
}
|
||||
}
|
||||
// The projections on wildcard are only used when wildcardField is "$**" and are mutually
|
||||
// exclusive.
|
||||
std::vector<StringData> projectionInclusionSet;
|
||||
std::set<StringData> projectionExclusionSet;
|
||||
for (const auto& proj : pathProjection) {
|
||||
if (proj.numberInt() == 1) {
|
||||
projectionInclusionSet.push_back(proj.fieldNameStringData());
|
||||
} else {
|
||||
projectionExclusionSet.insert(proj.fieldNameStringData());
|
||||
}
|
||||
}
|
||||
validateWildcardIndexKeys(
|
||||
nonWildcardFields, wildcardField, projectionInclusionSet, projectionExclusionSet);
|
||||
return std::move(*proj);
|
||||
}
|
||||
|
||||
WildcardKeyGenerator::WildcardKeyGenerator(BSONObj keyPattern,
|
||||
@@ -105,23 +166,60 @@ WildcardKeyGenerator::WildcardKeyGenerator(BSONObj keyPattern,
|
||||
_collator(collator),
|
||||
_keyPattern(keyPattern),
|
||||
_keyStringVersion(keyStringVersion),
|
||||
_ordering(ordering) {}
|
||||
_ordering(ordering) {
|
||||
std::vector<const char*> fieldNames;
|
||||
for (const auto& elem : _keyPattern) {
|
||||
if (auto keyStr = elem.fieldNameStringData();
|
||||
(keyStr != "$**") && !keyStr.endsWith(".$**")) {
|
||||
fieldNames.push_back(elem.fieldName());
|
||||
}
|
||||
}
|
||||
std::vector<BSONElement> fixed(fieldNames.size());
|
||||
_indexKeyGen = std::make_unique<BtreeKeyGenerator>(
|
||||
fieldNames, fixed, true /* isSparse */, _collator, _keyStringVersion, _ordering);
|
||||
}
|
||||
|
||||
void WildcardKeyGenerator::generateKeys(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
BSONObj inputDoc,
|
||||
KeyStringSet* keys,
|
||||
KeyStringSet* multikeyPaths,
|
||||
boost::optional<RecordId> id) const {
|
||||
FieldRef rootPath;
|
||||
auto keysSequence = keys->extract_sequence();
|
||||
KeyStringSet nonWildcardKeys;
|
||||
SharedBufferFragmentBuilder allocator(KeyString::HeapBuilder::kHeapAllocatorDefaultBytes);
|
||||
MultikeyPaths nonWildcardMultikeyPaths;
|
||||
const auto skipMultikey = false;
|
||||
_indexKeyGen->getKeys(
|
||||
allocator, inputDoc, skipMultikey, &nonWildcardKeys, &nonWildcardMultikeyPaths);
|
||||
|
||||
// multikeyPaths is allowed to be nullptr
|
||||
KeyStringSet::sequence_type multikeyPathsSequence;
|
||||
if (multikeyPaths)
|
||||
multikeyPathsSequence = multikeyPaths->extract_sequence();
|
||||
BSONObjIterator keyPatternItr(_keyPattern);
|
||||
for (const auto& component : nonWildcardMultikeyPaths) {
|
||||
auto keyStr = (*keyPatternItr).fieldNameStringData();
|
||||
if ((keyStr != "$**") && !keyStr.endsWith(".$**")) {
|
||||
for (const auto& depth : component) {
|
||||
_addMultiKey(pooledBufferBuilder,
|
||||
FieldRef(FieldRef(keyStr).dottedSubstring(0, depth + 1)),
|
||||
&multikeyPathsSequence);
|
||||
}
|
||||
}
|
||||
++keyPatternItr;
|
||||
}
|
||||
|
||||
auto projected = _proj.exec()->applyTransformation(Document{inputDoc}).toBson();
|
||||
if (projected.isEmpty()) {
|
||||
*keys = std::move(nonWildcardKeys);
|
||||
return;
|
||||
}
|
||||
FieldRef rootPath;
|
||||
auto keysSequence = keys->extract_sequence();
|
||||
_traverseWildcard(pooledBufferBuilder,
|
||||
_proj.exec()->applyTransformation(Document{inputDoc}).toBson(),
|
||||
projected,
|
||||
false,
|
||||
&rootPath,
|
||||
&nonWildcardKeys,
|
||||
&keysSequence,
|
||||
multikeyPaths ? &multikeyPathsSequence : nullptr,
|
||||
id);
|
||||
@@ -134,6 +232,7 @@ void WildcardKeyGenerator::_traverseWildcard(SharedBufferFragmentBuilder& pooled
|
||||
BSONObj obj,
|
||||
bool objIsArray,
|
||||
FieldRef* path,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
KeyStringSet::sequence_type* multikeyPaths,
|
||||
boost::optional<RecordId> id) const {
|
||||
@@ -148,27 +247,30 @@ void WildcardKeyGenerator::_traverseWildcard(SharedBufferFragmentBuilder& pooled
|
||||
switch (elem.type()) {
|
||||
case BSONType::Array:
|
||||
// If this is a nested array, we don't descend it but instead index it as a value.
|
||||
if (_addKeyForNestedArray(pooledBufferBuilder, elem, *path, objIsArray, keys, id))
|
||||
if (_addKeyForNestedArray(
|
||||
pooledBufferBuilder, elem, *path, objIsArray, nonWildcardKeys, keys, id))
|
||||
break;
|
||||
|
||||
// Add an entry for the multi-key path, and then fall through to BSONType::Object.
|
||||
_addMultiKey(pooledBufferBuilder, *path, multikeyPaths);
|
||||
|
||||
case BSONType::Object:
|
||||
if (_addKeyForEmptyLeaf(pooledBufferBuilder, elem, *path, keys, id))
|
||||
if (_addKeyForEmptyLeaf(
|
||||
pooledBufferBuilder, elem, *path, nonWildcardKeys, keys, id))
|
||||
break;
|
||||
|
||||
_traverseWildcard(pooledBufferBuilder,
|
||||
elem.Obj(),
|
||||
elem.type() == BSONType::Array,
|
||||
path,
|
||||
nonWildcardKeys,
|
||||
keys,
|
||||
multikeyPaths,
|
||||
id);
|
||||
break;
|
||||
|
||||
default:
|
||||
_addKey(pooledBufferBuilder, elem, *path, keys, id);
|
||||
_addKey(pooledBufferBuilder, elem, *path, nonWildcardKeys, keys, id);
|
||||
}
|
||||
|
||||
// Remove the element's fieldname from the path, if it was pushed onto it earlier.
|
||||
@@ -180,11 +282,12 @@ bool WildcardKeyGenerator::_addKeyForNestedArray(SharedBufferFragmentBuilder& po
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
bool enclosingObjIsArray,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const {
|
||||
// If this element is an array whose parent is also an array, index it as a value.
|
||||
if (enclosingObjIsArray && elem.type() == BSONType::Array) {
|
||||
_addKey(pooledBufferBuilder, elem, fullPath, keys, id);
|
||||
_addKey(pooledBufferBuilder, elem, fullPath, nonWildcardKeys, keys, id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -193,6 +296,7 @@ bool WildcardKeyGenerator::_addKeyForNestedArray(SharedBufferFragmentBuilder& po
|
||||
bool WildcardKeyGenerator::_addKeyForEmptyLeaf(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const {
|
||||
invariant(elem.isABSONObj());
|
||||
@@ -202,6 +306,7 @@ bool WildcardKeyGenerator::_addKeyForEmptyLeaf(SharedBufferFragmentBuilder& pool
|
||||
_addKey(pooledBufferBuilder,
|
||||
elem.type() == BSONType::Array ? BSONElement{} : elem,
|
||||
fullPath,
|
||||
nonWildcardKeys,
|
||||
keys,
|
||||
id);
|
||||
return true;
|
||||
@@ -209,13 +314,11 @@ bool WildcardKeyGenerator::_addKeyForEmptyLeaf(SharedBufferFragmentBuilder& pool
|
||||
return false;
|
||||
}
|
||||
|
||||
void WildcardKeyGenerator::_addKey(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const {
|
||||
// Wildcard keys are of the form { "": "path.to.field", "": <collation-aware value> }.
|
||||
KeyString::PooledBuilder keyString(pooledBufferBuilder, _keyStringVersion, _ordering);
|
||||
void WildcardKeyGenerator::_addWildcard(KeyString::PooledBuilder& keyString,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const {
|
||||
keyString.appendString(fullPath.dottedField());
|
||||
if (_collator && elem) {
|
||||
keyString.appendBSONElement(elem, [&](StringData stringData) {
|
||||
@@ -226,11 +329,43 @@ void WildcardKeyGenerator::_addKey(SharedBufferFragmentBuilder& pooledBufferBuil
|
||||
} else {
|
||||
keyString.appendUndefined();
|
||||
}
|
||||
}
|
||||
|
||||
if (id) {
|
||||
keyString.appendRecordId(*id);
|
||||
void WildcardKeyGenerator::_addKey(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const {
|
||||
if (nonWildcardKeys->size()) {
|
||||
for (auto nonWildcardKeysIter = nonWildcardKeys->begin();
|
||||
nonWildcardKeysIter != nonWildcardKeys->end();
|
||||
++nonWildcardKeysIter) {
|
||||
// Wildcard keys are of the form { "": "path.to.field", "": <collation-aware value> }.
|
||||
KeyString::PooledBuilder keyString(pooledBufferBuilder, _keyStringVersion, _ordering);
|
||||
auto decodedNonWildcardKey = KeyString::toBson(*nonWildcardKeysIter, _ordering);
|
||||
BSONObjIterator decodeKeysIter(decodedNonWildcardKey);
|
||||
for (auto&& keyPatternElem : _keyPattern) {
|
||||
if (auto keyStr = keyPatternElem.fieldNameStringData();
|
||||
(keyStr == "$**") || keyStr.endsWith(".$**")) {
|
||||
_addWildcard(keyString, elem, fullPath, keys, id);
|
||||
} else {
|
||||
keyString.appendBSONElement(decodeKeysIter.next());
|
||||
}
|
||||
}
|
||||
if (id) {
|
||||
keyString.appendRecordId(*id);
|
||||
}
|
||||
keys->push_back(keyString.release());
|
||||
}
|
||||
} else {
|
||||
KeyString::PooledBuilder keyString(pooledBufferBuilder, _keyStringVersion, _ordering);
|
||||
_addWildcard(keyString, elem, fullPath, keys, id);
|
||||
if (id) {
|
||||
keyString.appendRecordId(*id);
|
||||
}
|
||||
keys->push_back(keyString.release());
|
||||
}
|
||||
keys->push_back(keyString.release());
|
||||
}
|
||||
|
||||
void WildcardKeyGenerator::_addMultiKey(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
@@ -240,11 +375,20 @@ void WildcardKeyGenerator::_addMultiKey(SharedBufferFragmentBuilder& pooledBuffe
|
||||
// 'multikeyPaths' may be nullptr if the access method is being used in an operation which does
|
||||
// not require multikey path generation.
|
||||
if (multikeyPaths) {
|
||||
auto key = BSON("" << 1 << "" << fullPath.dottedField());
|
||||
BSONObjBuilder keyBuilder;
|
||||
for (auto&& elem : _keyPattern) {
|
||||
if (elem.fieldNameStringData() == "$**" ||
|
||||
elem.fieldNameStringData().endsWith(".$**")) {
|
||||
keyBuilder.append("", 1);
|
||||
keyBuilder.append("", fullPath.dottedField());
|
||||
} else {
|
||||
keyBuilder.appendMinKey("");
|
||||
}
|
||||
}
|
||||
KeyString::PooledBuilder keyString(
|
||||
pooledBufferBuilder,
|
||||
_keyStringVersion,
|
||||
key,
|
||||
keyBuilder.obj(),
|
||||
_ordering,
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
multikeyPaths->push_back(keyString.release());
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#include "mongo/db/exec/wildcard_projection.h"
|
||||
#include "mongo/db/field_ref.h"
|
||||
#include "mongo/db/index/btree_key_generator.h"
|
||||
#include "mongo/db/query/collation/collator_interface.h"
|
||||
#include "mongo/db/storage/key_string.h"
|
||||
#include "mongo/db/storage/sorted_data_interface.h"
|
||||
@@ -66,6 +67,10 @@ public:
|
||||
return &_proj;
|
||||
}
|
||||
|
||||
BSONObj getKeyPattern() const {
|
||||
return _keyPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the appropriate Wildcard projection to the input doc, and then adds one key-value
|
||||
* pair to the set 'keys' for each leaf node in the post-projection document:
|
||||
@@ -86,6 +91,7 @@ private:
|
||||
BSONObj obj,
|
||||
bool objIsArray,
|
||||
FieldRef* path,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
KeyStringSet::sequence_type* multikeyPaths,
|
||||
boost::optional<RecordId> id) const;
|
||||
@@ -94,9 +100,15 @@ private:
|
||||
void _addMultiKey(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet::sequence_type* multikeyPaths) const;
|
||||
void _addWildcard(KeyString::PooledBuilder& keyString,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const;
|
||||
void _addKey(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const;
|
||||
|
||||
@@ -105,11 +117,13 @@ private:
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
bool enclosingObjIsArray,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const;
|
||||
bool _addKeyForEmptyLeaf(SharedBufferFragmentBuilder& pooledBufferBuilder,
|
||||
BSONElement elem,
|
||||
const FieldRef& fullPath,
|
||||
KeyStringSet* nonWildcardKeys,
|
||||
KeyStringSet::sequence_type* keys,
|
||||
boost::optional<RecordId> id) const;
|
||||
|
||||
@@ -118,5 +132,7 @@ private:
|
||||
const BSONObj _keyPattern;
|
||||
const KeyString::Version _keyStringVersion;
|
||||
const Ordering _ordering;
|
||||
|
||||
std::unique_ptr<BtreeKeyGenerator> _indexKeyGen;
|
||||
};
|
||||
} // namespace mongo
|
||||
|
||||
@@ -1173,5 +1173,275 @@ TEST_F(WildcardKeyGeneratorDottedFieldsTest, DoNotIndexDottedFieldsWithSimilarSu
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
struct WildcardKeyGeneratorCompoundTest : public WildcardKeyGeneratorTest {};
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, TopLevelKeyInclusionCompound) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{a: 1, '$**': 1}"),
|
||||
fromjson("{b: 1}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: 1, b: 1}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 1, '': 'b', '': 1}")});
|
||||
auto expectedMultikeyPaths = makeKeySet();
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, TopLevelKeyExclusionCompound) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{a: 1, '$**': 1}"),
|
||||
fromjson("{a: 0}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: {b: 'one', c: 2}}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': {b: 'one', c: 2}}")});
|
||||
|
||||
auto expectedMultikeyPaths = makeKeySet();
|
||||
|
||||
KeyStringSet outputKeys;
|
||||
KeyStringSet multikeyMetadataKeys;
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, TopLevelKeyMultiInclusionCompound) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{a: 1, '$**': 1}"),
|
||||
fromjson("{b: 1, d: 1}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{ a: [], b: {c: []}, d: [[], {e: []}]}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': undefined, '': 'b.c', '': undefined}"),
|
||||
fromjson("{'': undefined, '': 'd', '': []}"),
|
||||
fromjson("{'': undefined, '': 'd.e', '': undefined}")});
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'b.c'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'd'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'd.e'}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, MultikeyCompoundField) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{'d.e': 1, '$**': 1}"),
|
||||
fromjson("{d: 0}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{ a: [], b: {c: []}, d: [[], {e: []}]}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': undefined, '': 'a', '': undefined}"),
|
||||
fromjson("{'': undefined, '': 'b.c', '': undefined}")});
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'b.c'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'd'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'd.e'}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, ExtractMultikeyPathWithSingleField) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{'$**': 1, f: 1}"),
|
||||
fromjson("{f: 0}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: [{b: 1, c: 1}, {b: 2, c: {d: [1, 2]}}], e: [3, 5], f: 1}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 'a.b', '': 1, '': 1}"),
|
||||
fromjson("{'': 'a.b', '': 2, '': 1}"),
|
||||
fromjson("{'': 'a.c', '': 1, '': 1}"),
|
||||
fromjson("{'': 'a.c.d', '': 1, '': 1}"),
|
||||
fromjson("{'': 'a.c.d', '': 2, '': 1}"),
|
||||
fromjson("{'': 'e', '': 3, '': 1}"),
|
||||
fromjson("{'': 'e', '': 5, '': 1}")});
|
||||
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': 1, '': 'a', '': {$minKey: 1}}"),
|
||||
fromjson("{'': 1, '': 'a.c.d', '': {$minKey: 1}}"),
|
||||
fromjson("{'': 1, '': 'e', '': {$minKey: 1}}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, ExtractMultikeyPathWithArrayField) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{e: 1, '$**': 1}"),
|
||||
fromjson("{e: 0}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: [{b: 1, c: 1}, {b: 2, c: {d: [1, 2]}}], e: [3, 5], f: 1}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 3, '': 'a.b', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a.b', '': 2}"),
|
||||
fromjson("{'': 3, '': 'a.c', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a.c.d', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a.c.d', '': 2}"),
|
||||
fromjson("{'': 3, '': 'f', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.b', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.b', '': 2}"),
|
||||
fromjson("{'': 5, '': 'a.c', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.c.d', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.c.d', '': 2}"),
|
||||
fromjson("{'': 5, '': 'f', '': 1}")});
|
||||
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'a.c.d'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'e'}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, ExtractMultikeyPathWithArrayElemField) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{'a.b' : 1, '$**': 1}"),
|
||||
fromjson("{a: 0}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: [{b: 1, c: 1}, {b: 2, c: {d: [1, 2]}}], e: [3, 5], f: 1}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 1, '': 'e', '': 3}"),
|
||||
fromjson("{'': 1, '': 'e', '': 5}"),
|
||||
fromjson("{'': 1, '': 'f', '': 1}"),
|
||||
fromjson("{'': 2, '': 'e', '': 3}"),
|
||||
fromjson("{'': 2, '': 'e', '': 5}"),
|
||||
fromjson("{'': 2, '': 'f', '': 1}")});
|
||||
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'e'}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, MultipleCompoundField) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{'a.b': 1, '$**': 1, f: 1}"),
|
||||
fromjson("{a: 0, f: 0}"),
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: [{b: 1, c: 1}, {b: 2, c: {d: [1, 2]}}], e: [3, 5], f: 1}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 1, '': 'e', '': 3, '': 1}"),
|
||||
fromjson("{'': 1, '': 'e', '': 5, '': 1}"),
|
||||
fromjson("{'': 2, '': 'e', '': 3, '': 1}"),
|
||||
fromjson("{'': 2, '': 'e', '': 5, '': 1}")});
|
||||
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a', '': {$minKey: 1}}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'e', '': {$minKey: 1}}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, ExtractMultikeyPathAndDedupKeysCompound) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{'f.d': 1, 'a.$**': 1}"),
|
||||
{},
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: [1, 2, {b: 'one', c: 2}, {c: 2}], f: [{d: 3, e: 4}, {d: 3}]}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 3, '': 'a', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a', '': 2}"),
|
||||
fromjson("{'': 3, '': 'a.b', '': 'one'}"),
|
||||
fromjson("{'': 3, '': 'a.c', '': 2}")});
|
||||
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'f'}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
TEST_F(WildcardKeyGeneratorCompoundTest, ExtractSubtreeWithArrayElemField) {
|
||||
WildcardKeyGenerator keyGen{fromjson("{e: 1, 'a.$**': 1}"),
|
||||
{},
|
||||
nullptr,
|
||||
KeyString::Version::kLatestVersion,
|
||||
Ordering::make(BSONObj())};
|
||||
auto inputDoc = fromjson("{a: [{b: 1, c: 1}, {b: 2, c: {d: [1, 2]}}], e: [3, 5], f: 1}");
|
||||
|
||||
auto expectedKeys = makeKeySet({fromjson("{'': 3, '': 'a.b', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a.b', '': 2}"),
|
||||
fromjson("{'': 3, '': 'a.c', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a.c.d', '': 1}"),
|
||||
fromjson("{'': 3, '': 'a.c.d', '': 2}"),
|
||||
fromjson("{'': 5, '': 'a.b', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.b', '': 2}"),
|
||||
fromjson("{'': 5, '': 'a.c', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.c.d', '': 1}"),
|
||||
fromjson("{'': 5, '': 'a.c.d', '': 2}")});
|
||||
|
||||
auto expectedMultikeyPaths =
|
||||
makeKeySet({fromjson("{'': {$minKey: 1}, '': 1, '': 'a'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'a.c.d'}"),
|
||||
fromjson("{'': {$minKey: 1}, '': 1, '': 'e'}")},
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId));
|
||||
|
||||
auto outputKeys = makeKeySet();
|
||||
auto multikeyMetadataKeys = makeKeySet();
|
||||
keyGen.generateKeys(allocator, inputDoc, &outputKeys, &multikeyMetadataKeys);
|
||||
|
||||
ASSERT(assertKeysetsEqual(expectedKeys, outputKeys));
|
||||
ASSERT(assertKeysetsEqual(expectedMultikeyPaths, multikeyMetadataKeys));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mongo
|
||||
|
||||
@@ -335,6 +335,7 @@ env.CppUnitTest(
|
||||
"planner_access_test.cpp",
|
||||
"planner_analysis_test.cpp",
|
||||
"planner_ixselect_test.cpp",
|
||||
"planner_wildcard_helpers_test.cpp",
|
||||
"projection_ast_test.cpp",
|
||||
"projection_test.cpp",
|
||||
"query_planner_array_test.cpp",
|
||||
|
||||
@@ -209,9 +209,19 @@ IndexEntry indexEntryFromIndexCatalogEntry(OperationContext* opCtx,
|
||||
if (canonicalQuery) {
|
||||
stdx::unordered_set<std::string> fields;
|
||||
QueryPlannerIXSelect::getFields(canonicalQuery->root(), &fields);
|
||||
const auto projectedFields = projection_executor_utils::applyProjectionToFields(
|
||||
auto projectedFields = projection_executor_utils::applyProjectionToFields(
|
||||
wildcardProjection->exec(), fields);
|
||||
|
||||
// For a compound wildcard index, we also want to get the multikey information
|
||||
// for (non-wildcard) fields in the index which are being queried.
|
||||
for (auto& indexElem : wam->getKeyPattern()) {
|
||||
auto indexKey = indexElem.fieldNameStringData();
|
||||
if (fields.contains(indexKey.toString()) && indexKey != "$**" &&
|
||||
!indexKey.endsWith(".$**")) {
|
||||
projectedFields.insert(projectedFields.begin(), indexKey.toString());
|
||||
}
|
||||
}
|
||||
|
||||
multikeyPathSet =
|
||||
getWildcardMultikeyPathSet(wam, opCtx, projectedFields, &mkAccessStats);
|
||||
} else {
|
||||
|
||||
@@ -50,6 +50,7 @@ enum class BoundInclusion {
|
||||
*/
|
||||
struct OrderedIntervalList {
|
||||
OrderedIntervalList() {}
|
||||
OrderedIntervalList(std::vector<Interval> intervals) : intervals(std::move(intervals)) {}
|
||||
OrderedIntervalList(const std::string& n) : name(n) {}
|
||||
|
||||
// Must be ordered according to the index order.
|
||||
|
||||
@@ -250,6 +250,11 @@ struct IndexEntry : CoreIndexInfo {
|
||||
|
||||
bool unique;
|
||||
|
||||
// After index expansion, we can't tell which field is the wildcard field in a compound index.
|
||||
// We use this field to track the index into the keyPattern, multikeyPaths, and bounds of the
|
||||
// wildcard field, if one exists.
|
||||
size_t wildcardFieldIndex;
|
||||
|
||||
// Geo indices have extra parameters. We need those available to plan correctly.
|
||||
BSONObj infoObj;
|
||||
};
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
* This file contains tests for mongo/db/query/plan_cache.h
|
||||
*/
|
||||
|
||||
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
|
||||
|
||||
#include "mongo/db/query/plan_cache.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -50,6 +52,7 @@
|
||||
#include "mongo/db/query/query_planner_test_lib.h"
|
||||
#include "mongo/db/query/query_solution.h"
|
||||
#include "mongo/db/query/query_test_service_context.h"
|
||||
#include "mongo/logv2/log.h"
|
||||
#include "mongo/unittest/unittest.h"
|
||||
#include "mongo/util/assert_util.h"
|
||||
#include "mongo/util/scopeguard.h"
|
||||
@@ -1086,8 +1089,14 @@ protected:
|
||||
BSONObj testSoln = fromjson(solnJson);
|
||||
size_t matches = 0;
|
||||
for (auto&& soln : solns) {
|
||||
if (QueryPlannerTestLib::solutionMatches(testSoln, soln->root())) {
|
||||
auto matchStatus = QueryPlannerTestLib::solutionMatches(testSoln, soln->root());
|
||||
if (matchStatus.isOK()) {
|
||||
++matches;
|
||||
} else {
|
||||
LOGV2_DEBUG(51551100,
|
||||
2,
|
||||
"Mismatching solution: {reason}",
|
||||
"reason"_attr = matchStatus.reason());
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
@@ -1168,8 +1177,14 @@ protected:
|
||||
QuerySolution* firstMatchingSolution(const string& solnJson) const {
|
||||
BSONObj testSoln = fromjson(solnJson);
|
||||
for (auto&& soln : solns) {
|
||||
if (QueryPlannerTestLib::solutionMatches(testSoln, soln->root())) {
|
||||
auto matchStatus = QueryPlannerTestLib::solutionMatches(testSoln, soln->root());
|
||||
if (matchStatus.isOK()) {
|
||||
return soln.get();
|
||||
} else {
|
||||
LOGV2_DEBUG(51551101,
|
||||
2,
|
||||
"Mismatching solution: {reason}",
|
||||
"reason"_attr = matchStatus.reason());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1190,10 +1205,12 @@ protected:
|
||||
*/
|
||||
void assertSolutionMatches(QuerySolution* trueSoln, const string& solnJson) const {
|
||||
BSONObj testSoln = fromjson(solnJson);
|
||||
if (!QueryPlannerTestLib::solutionMatches(testSoln, trueSoln->root())) {
|
||||
auto matchStatus = QueryPlannerTestLib::solutionMatches(testSoln, trueSoln->root());
|
||||
if (!matchStatus.isOK()) {
|
||||
str::stream ss;
|
||||
ss << "Expected solution " << solnJson
|
||||
<< " did not match true solution: " << trueSoln->toString() << '\n';
|
||||
<< " did not match true solution: " << trueSoln->toString()
|
||||
<< ". Reason: " << matchStatus.reason() << '\n';
|
||||
FAIL(ss);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +661,9 @@ bool QueryPlannerIXSelect::nodeIsSupportedByWildcardIndex(const MatchExpression*
|
||||
// store keys for nested objects, meaning that any kind of comparison to an object or array
|
||||
// cannot be answered by the index (including with a $in).
|
||||
|
||||
// TODO: we should confirm whether this is correct after compound wildcard indexes are
|
||||
// supported: wildcard indexes can support object queries on the non-WC fields in the index.
|
||||
|
||||
if (ComparisonMatchExpression::isComparisonMatchExpression(queryExpr)) {
|
||||
const ComparisonMatchExpression* cmpExpr =
|
||||
static_cast<const ComparisonMatchExpression*>(queryExpr);
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "mongo/bson/bsonobjbuilder.h"
|
||||
#include "mongo/bson/util/builder.h"
|
||||
#include "mongo/db/exec/projection_executor_utils.h"
|
||||
#include "mongo/db/index/wildcard_key_generator.h"
|
||||
@@ -44,6 +45,67 @@
|
||||
namespace mongo {
|
||||
namespace wildcard_planning {
|
||||
namespace {
|
||||
auto getElement(const BSONObj& keyPattern, int i) {
|
||||
auto it = keyPattern.begin();
|
||||
for (int j = 0; j < i; j++) {
|
||||
it++;
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
auto getWildcardIndex(const BSONObj& keyPattern) {
|
||||
int i = 0;
|
||||
for (auto& elem : keyPattern) {
|
||||
if (elem.fieldNameStringData().endsWith("$**"_sd)) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Creates a BSONObj representing 'keyPattern' with the wildcard field name replaced by 'fieldName'.
|
||||
auto expandIndexKey(const BSONObj& keyPattern, const StringData& fieldName) {
|
||||
BSONObjBuilder bb;
|
||||
for (auto& elem : keyPattern) {
|
||||
if (elem.fieldNameStringData().endsWith("$**"_sd)) {
|
||||
bb.appendAs(elem, fieldName);
|
||||
} else {
|
||||
bb.append(elem);
|
||||
}
|
||||
}
|
||||
return bb.obj();
|
||||
}
|
||||
|
||||
/*
|
||||
* Push a new entry into the bounds vector for the leading '$_path' bound, and push corresponding
|
||||
* fields into the IndexScanNode's keyPattern and its multikeyPaths vector. Then, update the
|
||||
* wildcardFieldIndex for 'index'.
|
||||
*/
|
||||
auto insertPathPlaceHolder(IndexEntry* index, IndexBounds* bounds) {
|
||||
auto multikeyItr = index->multikeyPaths.begin();
|
||||
auto fieldsItr = bounds->fields.begin();
|
||||
for (int i = 0; i < (int)index->wildcardFieldIndex; i++) {
|
||||
multikeyItr = std::next(multikeyItr);
|
||||
fieldsItr = std::next(fieldsItr);
|
||||
}
|
||||
index->multikeyPaths.insert(multikeyItr, MultikeyComponents{});
|
||||
bounds->fields.insert(fieldsItr, {"$_path"});
|
||||
|
||||
BSONObjBuilder bb;
|
||||
int j = 0;
|
||||
for (auto& elem : index->keyPattern) {
|
||||
if (j == (int)index->wildcardFieldIndex) {
|
||||
bb.appendAs(elem, "$_path");
|
||||
}
|
||||
bb.append(elem);
|
||||
j++;
|
||||
}
|
||||
index->keyPattern = bb.obj();
|
||||
|
||||
index->wildcardFieldIndex += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the path 'fieldNameOrArrayIndexPath' to 'staticComparisonPath', ignoring any array
|
||||
* indices present in the former if they are not present in the latter. The 'multikeyPathComponents'
|
||||
@@ -138,20 +200,32 @@ FieldRef pathWithoutSpecifiedComponents(const FieldRef& path,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MultikeyPaths which indicates which components of 'indexedPath' are multikey, by
|
||||
* looking up multikeyness in 'multikeyPathSet'.
|
||||
* Returns a MultikeyPaths which indicates which components of the index key pattern are multikey
|
||||
* by looking up multikeyness in 'multikeyPathSet'. 'indexedPath' is used as the new key for the
|
||||
* wildcard element.
|
||||
*/
|
||||
MultikeyPaths buildMultiKeyPathsForExpandedWildcardIndexEntry(
|
||||
const FieldRef& indexedPath, const std::set<FieldRef>& multikeyPathSet) {
|
||||
FieldRef pathToLookup;
|
||||
MultikeyComponents multikeyPaths;
|
||||
for (size_t i = 0; i < indexedPath.numParts(); ++i) {
|
||||
pathToLookup.appendPart(indexedPath.getPart(i));
|
||||
if (fieldNameOrArrayIndexPathSetContains(multikeyPathSet, multikeyPaths, pathToLookup)) {
|
||||
multikeyPaths.insert(i);
|
||||
const FieldRef& indexedPath,
|
||||
const std::set<FieldRef>& multikeyPathSet,
|
||||
const BSONObj& keyPattern) {
|
||||
MultikeyPaths multikeyPaths = {};
|
||||
|
||||
// For each key in the index key pattern, determine which components are multikey.
|
||||
for (auto& elem : keyPattern) {
|
||||
const FieldRef& currField =
|
||||
elem.fieldNameStringData().endsWith("$**") ? indexedPath : FieldRef(elem.fieldName());
|
||||
FieldRef pathToLookup;
|
||||
MultikeyComponents comps;
|
||||
for (size_t i = 0; i < currField.numParts(); ++i) {
|
||||
pathToLookup.appendPart(currField.getPart(i));
|
||||
if (fieldNameOrArrayIndexPathSetContains(multikeyPathSet, comps, pathToLookup)) {
|
||||
comps.insert(i);
|
||||
}
|
||||
}
|
||||
multikeyPaths.insert(multikeyPaths.end(), comps);
|
||||
}
|
||||
return {multikeyPaths};
|
||||
|
||||
return multikeyPaths;
|
||||
}
|
||||
|
||||
std::set<FieldRef> generateFieldNameOrArrayIndexPathSet(const MultikeyComponents& multikeyPaths,
|
||||
@@ -230,14 +304,13 @@ std::set<FieldRef> generateFieldNameOrArrayIndexPathSet(const MultikeyComponents
|
||||
*/
|
||||
bool validateNumericPathComponents(const MultikeyPaths& multikeyPaths,
|
||||
const std::set<FieldRef>& includedPaths,
|
||||
const FieldRef& queryPath) {
|
||||
// $** multikeyPaths always have a singleton set, since they are single-element indexes.
|
||||
invariant(multikeyPaths.size() == 1);
|
||||
|
||||
const FieldRef& queryPath,
|
||||
const int indexOfWildcardField) {
|
||||
// Find the positions of all multikey path components in 'queryPath' that have a numerical path
|
||||
// component immediately after. For a queryPath of 'a.2.b' this will return position 0; that is,
|
||||
// 'a'. If no such multikey path was found, we are clear to proceed with planning.
|
||||
const auto arrayIndices = findArrayIndexPathComponents(multikeyPaths.front(), queryPath);
|
||||
const auto arrayIndices =
|
||||
findArrayIndexPathComponents(multikeyPaths[indexOfWildcardField], queryPath);
|
||||
if (arrayIndices.empty()) {
|
||||
return true;
|
||||
}
|
||||
@@ -300,6 +373,63 @@ bool validateNumericPathComponents(const MultikeyPaths& multikeyPaths,
|
||||
return arrayIndices[0] >= includePath->numParts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new IndexEntry representing 'wildcardIndex' after it has been expanded with 'fieldName'.
|
||||
* Appropriately sets the multikeyPaths of the new IndexEntry and validates that any numeric path
|
||||
* components in the query can be handled by this index.
|
||||
*/
|
||||
boost::optional<IndexEntry> expandAndValidateIndexEntry(const IndexEntry& wildcardIndex,
|
||||
const StringData& fieldName,
|
||||
const std::set<FieldRef>& includedPaths) {
|
||||
auto queryPath = FieldRef{fieldName};
|
||||
|
||||
// $** indices hold multikey metadata directly in the index keys, rather than in the index
|
||||
// catalog. In turn, the index key data is used to produce a set of multikey paths
|
||||
// in-memory. Here we convert this set of all multikey paths into a MultikeyPaths vector
|
||||
// which will indicate to the downstream planning code which components of 'fieldName' are
|
||||
// multikey.
|
||||
auto multikeyPaths = buildMultiKeyPathsForExpandedWildcardIndexEntry(
|
||||
queryPath, wildcardIndex.multikeyPathSet, wildcardIndex.keyPattern);
|
||||
|
||||
int wildcardPos = getWildcardIndex(wildcardIndex.keyPattern);
|
||||
invariant(wildcardPos >= 0);
|
||||
|
||||
// Check whether a query on the current fieldpath is answerable by the $** index, given any
|
||||
// numerical path components that may be present in the path string.
|
||||
if (!validateNumericPathComponents(multikeyPaths, includedPaths, queryPath, wildcardPos)) {
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
// The expanded IndexEntry is only considered multikey if the particular path represented by
|
||||
// this IndexEntry has a multikey path component. For instance, suppose we have index {$**:
|
||||
// 1} with "a" as the only multikey path. If we have a query on paths "a.b" and "c.d", then
|
||||
// we will generate two expanded index entries: one for "a.b" and "c.d". The "a.b" entry
|
||||
// will be marked as multikey because "a" is multikey, whereas the "c.d" entry will not be
|
||||
// marked as multikey.
|
||||
invariant((int)multikeyPaths.size() == wildcardIndex.keyPattern.nFields());
|
||||
const bool isMultikey = !multikeyPaths[wildcardPos].empty();
|
||||
|
||||
IndexEntry entry(expandIndexKey(wildcardIndex.keyPattern, fieldName),
|
||||
IndexType::INDEX_WILDCARD,
|
||||
IndexDescriptor::kLatestIndexVersion,
|
||||
isMultikey,
|
||||
std::move(multikeyPaths),
|
||||
// Expanded index entries always use the fixed-size multikey paths
|
||||
// representation, so we purposefully discard 'multikeyPathSet'.
|
||||
{},
|
||||
true, // sparse
|
||||
false, // unique
|
||||
{wildcardIndex.identifier.catalogName, fieldName.toString()},
|
||||
wildcardIndex.filterExpr,
|
||||
wildcardIndex.infoObj,
|
||||
wildcardIndex.collator,
|
||||
wildcardIndex.wildcardProjection);
|
||||
entry.wildcardFieldIndex = wildcardPos;
|
||||
|
||||
invariant("$_path"_sd != fieldName);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries whose bounds overlap the Object type bracket may require special handling, since the $**
|
||||
* index does not index complete objects but instead only contains the leaves along each of its
|
||||
@@ -349,9 +479,10 @@ void expandWildcardIndexEntry(const IndexEntry& wildcardIndex,
|
||||
std::vector<IndexEntry>* out) {
|
||||
invariant(out);
|
||||
invariant(wildcardIndex.type == INDEX_WILDCARD);
|
||||
// Should only have one field of the form {"path.$**" : 1}.
|
||||
invariant(wildcardIndex.keyPattern.nFields() == 1);
|
||||
invariant(wildcardIndex.keyPattern.firstElement().fieldNameStringData().endsWith("$**"));
|
||||
|
||||
// Should have a wilcard key in the index.
|
||||
int wildcardPos = getWildcardIndex(wildcardIndex.keyPattern);
|
||||
invariant(wildcardPos >= 0);
|
||||
|
||||
// $** indexes do not keep the multikey metadata inside the index catalog entry, as the amount
|
||||
// of metadata is not bounded. We do not expect IndexEntry objects for $** indexes to have a
|
||||
@@ -370,49 +501,21 @@ void expandWildcardIndexEntry(const IndexEntry& wildcardIndex,
|
||||
wildcardProjection->exhaustivePaths() ? *wildcardProjection->exhaustivePaths() : kEmptySet;
|
||||
out->reserve(out->size() + projectedFields.size());
|
||||
for (auto&& fieldName : projectedFields) {
|
||||
// Convert string 'fieldName' into a FieldRef, to better facilitate the subsequent checks.
|
||||
auto queryPath = FieldRef{fieldName};
|
||||
// $** indices hold multikey metadata directly in the index keys, rather than in the index
|
||||
// catalog. In turn, the index key data is used to produce a set of multikey paths
|
||||
// in-memory. Here we convert this set of all multikey paths into a MultikeyPaths vector
|
||||
// which will indicate to the downstream planning code which components of 'fieldName' are
|
||||
// multikey.
|
||||
auto multikeyPaths = buildMultiKeyPathsForExpandedWildcardIndexEntry(
|
||||
queryPath, wildcardIndex.multikeyPathSet);
|
||||
|
||||
// Check whether a query on the current fieldpath is answerable by the $** index, given any
|
||||
// numerical path components that may be present in the path string.
|
||||
if (!validateNumericPathComponents(multikeyPaths, includedPaths, queryPath)) {
|
||||
continue;
|
||||
if (auto entry = expandAndValidateIndexEntry(wildcardIndex, fieldName, includedPaths)) {
|
||||
out->push_back(std::move(entry.get()));
|
||||
}
|
||||
}
|
||||
|
||||
// The expanded IndexEntry is only considered multikey if the particular path represented by
|
||||
// this IndexEntry has a multikey path component. For instance, suppose we have index {$**:
|
||||
// 1} with "a" as the only multikey path. If we have a query on paths "a.b" and "c.d", then
|
||||
// we will generate two expanded index entries: one for "a.b" and "c.d". The "a.b" entry
|
||||
// will be marked as multikey because "a" is multikey, whereas the "c.d" entry will not be
|
||||
// marked as multikey.
|
||||
invariant(multikeyPaths.size() == 1u);
|
||||
const bool isMultikey = !multikeyPaths[0].empty();
|
||||
|
||||
IndexEntry entry(BSON(fieldName << wildcardIndex.keyPattern.firstElement()),
|
||||
IndexType::INDEX_WILDCARD,
|
||||
IndexDescriptor::kLatestIndexVersion,
|
||||
isMultikey,
|
||||
std::move(multikeyPaths),
|
||||
// Expanded index entries always use the fixed-size multikey paths
|
||||
// representation, so we purposefully discard 'multikeyPathSet'.
|
||||
{},
|
||||
true, // sparse
|
||||
false, // unique
|
||||
{wildcardIndex.identifier.catalogName, fieldName},
|
||||
wildcardIndex.filterExpr,
|
||||
wildcardIndex.infoObj,
|
||||
wildcardIndex.collator,
|
||||
wildcardIndex.wildcardProjection);
|
||||
|
||||
invariant("$_path"_sd != fieldName);
|
||||
out->push_back(std::move(entry));
|
||||
if (projectedFields.empty() && wildcardIndex.keyPattern.nFields() > 1) {
|
||||
// We are querying on fields not covered by the wildcard part of the index, but we have a
|
||||
// compound index, so we may still be able to support the query. For example, with index
|
||||
// {a: 1, 'b.$**': 1} and query {a: {$eq: 5}}, the index does support the query. Replace the
|
||||
// '$**' part of the wildcard element with a placeholder name, then output the index entry.
|
||||
auto wcElemName = getElement(wildcardIndex.keyPattern, wildcardPos)->fieldNameStringData();
|
||||
auto placeholder = wcElemName.substr(0, wcElemName.size() - 3) + "$_value";
|
||||
if (auto entry = expandAndValidateIndexEntry(wildcardIndex, placeholder, includedPaths)) {
|
||||
out->push_back(std::move(entry.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,8 +527,7 @@ BoundsTightness translateWildcardIndexBoundsAndTightness(const IndexEntry& index
|
||||
// only have a single keyPattern field and multikeyPath entry, but this is sufficient to
|
||||
// determine whether it will be necessary to adjust the tightness.
|
||||
invariant(index.type == IndexType::INDEX_WILDCARD);
|
||||
invariant(index.keyPattern.nFields() == 1);
|
||||
invariant(index.multikeyPaths.size() == 1);
|
||||
invariant(index.keyPattern.nFields() == (int)index.multikeyPaths.size());
|
||||
invariant(oil);
|
||||
|
||||
// If our bounds include any objects -- anything in the range ({}, []) -- then we will need to
|
||||
@@ -436,14 +538,19 @@ BoundsTightness translateWildcardIndexBoundsAndTightness(const IndexEntry& index
|
||||
// result set should include documents such as {a: {b: null}}; however, the wildcard index key
|
||||
// for this object will be {"": "a.b", "": null}, which means that the original bounds would
|
||||
// skip this document. We must also set the tightness to INEXACT_FETCH to avoid false positives.
|
||||
if (boundsOverlapObjectTypeBracket(*oil) && !oil->intervals.front().isMinToMax()) {
|
||||
|
||||
// TODO: Not convinced that we are only checking the bounds corresponding to the $** component.
|
||||
if (boundsOverlapObjectTypeBracket(*oil) && !oil->intervals.back().isMinToMax()) {
|
||||
oil->intervals = {IndexBoundsBuilder::allValues()};
|
||||
return BoundsTightness::INEXACT_FETCH;
|
||||
}
|
||||
|
||||
// If the query passes through any array indices, we must always fetch and filter the documents.
|
||||
// Here we know that the last field in key pattern is the wildcard field-- after expansion, we
|
||||
// can no longer tell just from looking at the keys.
|
||||
auto wcElem = getElement(index.keyPattern, index.wildcardFieldIndex);
|
||||
const auto arrayIndicesTraversedByQuery = findArrayIndexPathComponents(
|
||||
index.multikeyPaths.front(), FieldRef{index.keyPattern.firstElementFieldName()});
|
||||
index.multikeyPaths[index.wildcardFieldIndex], FieldRef{wcElem->fieldName()});
|
||||
|
||||
// If the list of array indices we traversed is non-empty, set the tightness to INEXACT_FETCH.
|
||||
return (arrayIndicesTraversedByQuery.empty() ? tightnessIn : BoundsTightness::INEXACT_FETCH);
|
||||
@@ -455,29 +562,25 @@ void finalizeWildcardIndexScanConfiguration(IndexScanNode* scan) {
|
||||
|
||||
// We should only ever reach this point when processing a $** index. Sanity check the arguments.
|
||||
invariant(index && index->type == IndexType::INDEX_WILDCARD);
|
||||
invariant(index->keyPattern.nFields() == 1);
|
||||
invariant(index->multikeyPaths.size() == 1);
|
||||
invariant(bounds && bounds->fields.size() == 1);
|
||||
invariant(bounds->fields.front().name == index->keyPattern.firstElementFieldName());
|
||||
invariant(index->keyPattern.nFields() == (int)index->multikeyPaths.size());
|
||||
invariant(bounds && bounds->fields.size() == index->multikeyPaths.size());
|
||||
invariant(index->wildcardFieldIndex >= 0);
|
||||
|
||||
// For $** indexes, the IndexEntry key pattern is {'path.to.field': ±1} but the actual keys in
|
||||
// the index are of the form {'$_path': ±1, 'path.to.field': ±1}, where the value of the first
|
||||
// field in each key is 'path.to.field'. We push a new entry into the bounds vector for the
|
||||
// leading '$_path' bound here. We also push corresponding fields into the IndexScanNode's
|
||||
// keyPattern and its multikeyPaths vector.
|
||||
index->multikeyPaths.insert(index->multikeyPaths.begin(), MultikeyComponents{});
|
||||
bounds->fields.insert(bounds->fields.begin(), {"$_path"});
|
||||
index->keyPattern =
|
||||
BSON("$_path" << index->keyPattern.firstElement() << index->keyPattern.firstElement());
|
||||
// field in each key is 'path.to.field'. We add the $_path field to the necessary objects here.
|
||||
insertPathPlaceHolder(index, bounds);
|
||||
|
||||
// Create a FieldRef to perform any necessary manipulations on the query path string.
|
||||
FieldRef queryPath{std::next(index->keyPattern.begin())->fieldNameStringData()};
|
||||
auto& multikeyPaths = index->multikeyPaths.back();
|
||||
FieldRef queryPath{
|
||||
getElement(index->keyPattern, index->wildcardFieldIndex)->fieldNameStringData()};
|
||||
auto& multikeyPaths = index->multikeyPaths[index->wildcardFieldIndex];
|
||||
|
||||
// If the bounds overlap the object type bracket, then we must retrieve all documents which
|
||||
// include the given path. We must therefore add bounds that encompass all its subpaths,
|
||||
// specifically the interval ["path.","path/") on "$_path".
|
||||
const bool requiresSubpathBounds = boundsOverlapObjectTypeBracket(bounds->fields.back());
|
||||
const bool requiresSubpathBounds =
|
||||
boundsOverlapObjectTypeBracket(bounds->fields[index->wildcardFieldIndex]);
|
||||
|
||||
// Account for fieldname-or-array-index semantics. $** indexes do not explicitly encode array
|
||||
// indices in their keys, so if this query traverses one or more multikey fields via an array
|
||||
@@ -486,10 +589,16 @@ void finalizeWildcardIndexScanConfiguration(IndexScanNode* scan) {
|
||||
auto paths =
|
||||
generateFieldNameOrArrayIndexPathSet(multikeyPaths, queryPath, requiresSubpathBounds);
|
||||
|
||||
// Get a pointer to the newly inserted $_path placeholder.
|
||||
auto fieldsItr = bounds->fields.begin();
|
||||
for (int i = 0; i < (int)index->wildcardFieldIndex - 1; i++) {
|
||||
fieldsItr = std::next(fieldsItr);
|
||||
}
|
||||
|
||||
// Add a $_path point-interval for each path that needs to be traversed in the index. If subpath
|
||||
// bounds are required, then we must add a further range interval on ["path.","path/").
|
||||
static const char subPathStart = '.', subPathEnd = static_cast<char>('.' + 1);
|
||||
auto& pathIntervals = bounds->fields.front().intervals;
|
||||
auto& pathIntervals = fieldsItr->intervals;
|
||||
for (const auto& fieldPath : paths) {
|
||||
auto path = fieldPath.dottedField().toString();
|
||||
pathIntervals.push_back(IndexBoundsBuilder::makePointInterval(path));
|
||||
@@ -503,8 +612,6 @@ void finalizeWildcardIndexScanConfiguration(IndexScanNode* scan) {
|
||||
scan->shouldDedup = true;
|
||||
}
|
||||
}
|
||||
// Ensure that the bounds' intervals are correctly aligned.
|
||||
IndexBoundsBuilder::alignBounds(bounds, index->keyPattern);
|
||||
}
|
||||
|
||||
bool isWildcardObjectSubpathScan(const IndexScanNode* node) {
|
||||
@@ -514,15 +621,19 @@ bool isWildcardObjectSubpathScan(const IndexScanNode* node) {
|
||||
}
|
||||
|
||||
// We expect consistent arguments, representing a $** index which has already been finalized.
|
||||
invariant(node->index.keyPattern.nFields() == 2);
|
||||
invariant(node->index.multikeyPaths.size() == 2);
|
||||
invariant(node->bounds.fields.size() == 2);
|
||||
invariant(node->bounds.fields.front().name == node->index.keyPattern.firstElementFieldName());
|
||||
invariant(node->bounds.fields.back().name ==
|
||||
std::next(node->index.keyPattern.begin())->fieldName());
|
||||
auto numFields = node->index.keyPattern.nFields();
|
||||
invariant((int)node->index.multikeyPaths.size() == numFields);
|
||||
invariant((int)node->bounds.fields.size() == numFields);
|
||||
|
||||
// The last two elements of the bounds and keyPattern should the $_path placeholder followed by
|
||||
// the wildcard element. Verify that the field names at these indexes match.
|
||||
invariant(node->bounds.fields[node->index.wildcardFieldIndex - 1].name ==
|
||||
getElement(node->index.keyPattern, node->index.wildcardFieldIndex - 1)->fieldName());
|
||||
invariant(node->bounds.fields[node->index.wildcardFieldIndex].name ==
|
||||
getElement(node->index.keyPattern, node->index.wildcardFieldIndex)->fieldName());
|
||||
|
||||
// Check the bounds on the query field for any intersections with the object type bracket.
|
||||
return boundsOverlapObjectTypeBracket(node->bounds.fields.back());
|
||||
return boundsOverlapObjectTypeBracket(node->bounds.fields[node->index.wildcardFieldIndex]);
|
||||
}
|
||||
|
||||
} // namespace wildcard_planning
|
||||
|
||||
@@ -87,7 +87,7 @@ bool isWildcardObjectSubpathScan(const IndexScanNode* node);
|
||||
* Return true if the intervals on the 'value' field will include subobjects, and
|
||||
* thus require the bounds on $_path to include ["path.", "path/").
|
||||
*/
|
||||
bool requiresSubpathBounds(const OrderedIntervalList& intervals);
|
||||
// bool requiresSubpathBounds(const OrderedIntervalList& intervals);
|
||||
|
||||
} // namespace wildcard_planning
|
||||
} // namespace mongo
|
||||
|
||||
538
src/mongo/db/query/planner_wildcard_helpers_test.cpp
Normal file
538
src/mongo/db/query/planner_wildcard_helpers_test.cpp
Normal file
@@ -0,0 +1,538 @@
|
||||
/**
|
||||
* Copyright (C) 2018-present MongoDB, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* As a special exception, the copyright holders give permission to link the
|
||||
* code of portions of this program with the OpenSSL library under certain
|
||||
* conditions as described in each individual source file and distribute
|
||||
* linked combinations including the program with the OpenSSL library. You
|
||||
* must comply with the Server Side Public License in all respects for
|
||||
* all of the code used other than as permitted herein. If you modify file(s)
|
||||
* with this exception, you may extend this exception to your version of the
|
||||
* file(s), but you are not obligated to do so. If you do not wish to do so,
|
||||
* delete this exception statement from your version. If you delete this
|
||||
* exception statement from all source files in the program, then also delete
|
||||
* it in the license file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file contains tests for mongo/db/query/planner_ixselect.cpp
|
||||
*/
|
||||
|
||||
#include "mongo/db/query/index_entry.h"
|
||||
|
||||
#include "mongo/db/index/wildcard_key_generator.h"
|
||||
#include "mongo/db/pipeline/aggregation_context_fixture.h"
|
||||
#include "mongo/db/query/planner_wildcard_helpers.h"
|
||||
#include "mongo/db/query/query_planner_test_fixture.h"
|
||||
#include "mongo/unittest/bson_test_util.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace mongo {
|
||||
|
||||
using PlannerWildcardHelpersTest = AggregationContextFixture;
|
||||
|
||||
/**************** The following section can be moved to planner_ixselect_test.cpp ****************/
|
||||
/*
|
||||
* Will compare 'keyPatterns' with 'entries'. As part of comparing, it will sort both of them.
|
||||
*/
|
||||
bool indexEntryKeyPatternsMatch(std::vector<BSONObj>* keyPatterns,
|
||||
std::vector<IndexEntry>* entries) {
|
||||
ASSERT_EQ(entries->size(), keyPatterns->size());
|
||||
|
||||
const auto cmpFn = [](const IndexEntry& a, const IndexEntry& b) {
|
||||
return SimpleBSONObjComparator::kInstance.evaluate(a.keyPattern < b.keyPattern);
|
||||
};
|
||||
|
||||
std::sort(entries->begin(), entries->end(), cmpFn);
|
||||
std::sort(keyPatterns->begin(), keyPatterns->end(), [](const BSONObj& a, const BSONObj& b) {
|
||||
return SimpleBSONObjComparator::kInstance.evaluate(a < b);
|
||||
});
|
||||
|
||||
return std::equal(keyPatterns->begin(),
|
||||
keyPatterns->end(),
|
||||
entries->begin(),
|
||||
[](const BSONObj& keyPattern, const IndexEntry& ie) -> bool {
|
||||
return SimpleBSONObjComparator::kInstance.evaluate(keyPattern ==
|
||||
ie.keyPattern);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper which constructs an IndexEntry and returns it along with an owned ProjectionExecutor,
|
||||
// which is non-null if the requested entry represents a wildcard index and null otherwise. When
|
||||
// non-null, it simulates the ProjectionExecutor that is owned by the $** IndexAccessMethod.
|
||||
auto makeIndexEntry(BSONObj keyPattern,
|
||||
MultikeyPaths multiKeyPaths,
|
||||
std::set<FieldRef> multiKeyPathSet = {},
|
||||
BSONObj infoObj = BSONObj()) {
|
||||
auto wcElem = std::find_if(keyPattern.begin(), keyPattern.end(), [](auto&& elem) {
|
||||
return elem.fieldNameStringData().endsWith("$**"_sd);
|
||||
});
|
||||
auto wcProj = wcElem != keyPattern.end() && wcElem->fieldNameStringData().endsWith("$**"_sd)
|
||||
? std::make_unique<WildcardProjection>(WildcardKeyGenerator::createProjectionExecutor(
|
||||
keyPattern, infoObj.getObjectField("wildcardProjection")))
|
||||
: std::unique_ptr<WildcardProjection>(nullptr);
|
||||
|
||||
auto multiKey = !multiKeyPathSet.empty() ||
|
||||
std::any_of(multiKeyPaths.cbegin(), multiKeyPaths.cend(), [](const auto& entry) {
|
||||
return !entry.empty();
|
||||
});
|
||||
return std::make_pair(IndexEntry(keyPattern,
|
||||
IndexNames::nameToType(IndexNames::findPluginName(keyPattern)),
|
||||
IndexDescriptor::kLatestIndexVersion,
|
||||
multiKey,
|
||||
multiKeyPaths,
|
||||
multiKeyPathSet,
|
||||
false,
|
||||
false,
|
||||
CoreIndexInfo::Identifier("test_foo"),
|
||||
nullptr,
|
||||
{},
|
||||
nullptr,
|
||||
wcProj.get()),
|
||||
std::move(wcProj));
|
||||
}
|
||||
|
||||
std::unique_ptr<MatchExpression> parseMatchExpression(const BSONObj& obj) {
|
||||
boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
|
||||
StatusWithMatchExpression status = MatchExpressionParser::parse(obj, std::move(expCtx));
|
||||
ASSERT_TRUE(status.isOK());
|
||||
return std::unique_ptr<MatchExpression>(status.getValue().release());
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, ExpandSimpleWildcardIndexEntry) {
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a"};
|
||||
const auto indexEntry = makeIndexEntry(BSON("$**" << 1), {});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
ASSERT_BSONOBJ_EQ(out[0].keyPattern, fromjson("{a: 1}"));
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, ExpandCompoundWildcardIndexEntry) {
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a", "b", "c"};
|
||||
const auto indexEntry = makeIndexEntry(
|
||||
BSON("a" << 1 << "$**" << 1), {}, {}, {fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
ASSERT_EQ(out.size(), 2u);
|
||||
std::vector<BSONObj> expectedKeyPatterns = {fromjson("{a: 1, b: 1}"), fromjson("{a: 1, c: 1}")};
|
||||
indexEntryKeyPatternsMatch(&expectedKeyPatterns, &out);
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, ExpandCompoundWildcardIndexEntryNoMatch) {
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"c", "b"};
|
||||
const auto indexEntry = makeIndexEntry(
|
||||
BSON("a" << 1 << "$**" << 1), {}, {}, {fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
ASSERT_EQ(out.size(), 2u);
|
||||
std::vector<BSONObj> expectedKeyPatterns = {fromjson("{a: 1, b: 1}"), fromjson("{a: 1, c: 1}")};
|
||||
indexEntryKeyPatternsMatch(&expectedKeyPatterns, &out);
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, ExpandEnsureMultikeySetForAllCompoundFields) {
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a", "b"};
|
||||
const auto indexEntry = makeIndexEntry(BSON("a" << 1 << "$**" << 1),
|
||||
{},
|
||||
{FieldRef("a"), FieldRef("b"), FieldRef("c")},
|
||||
{fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
ASSERT_TRUE(out[0].multikey);
|
||||
ASSERT_EQ(out[0].multikeyPaths.size(), 2);
|
||||
ASSERT(out[0].multikeyPaths[0] == MultikeyComponents{0u}); // a is a multikey path
|
||||
ASSERT(out[0].multikeyPaths[1] == MultikeyComponents{0u}); // and so is b
|
||||
ASSERT_BSONOBJ_EQ(out[0].keyPattern, {fromjson("{a: 1, b: 1}")});
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, ExpandEnsureMultikeySetForAllCompoundFieldsDotted) {
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a.b", "c.d.e"};
|
||||
const auto indexEntry = makeIndexEntry(
|
||||
BSON("a.b" << 1 << "$**" << 1),
|
||||
{},
|
||||
{FieldRef("a"), FieldRef("a.b"), FieldRef("b"), FieldRef("c"), FieldRef("c.d.e")},
|
||||
{fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
ASSERT_TRUE(out[0].multikey);
|
||||
ASSERT_EQ(out[0].multikeyPaths.size(), 2);
|
||||
ASSERT((out[0].multikeyPaths[0] == MultikeyComponents{0u, 1u})); // a and a.b are multikey
|
||||
ASSERT((out[0].multikeyPaths[1] == MultikeyComponents{0u, 2u})); // c and c.d.e are multikey
|
||||
ASSERT_BSONOBJ_EQ(out[0].keyPattern, {fromjson("{'a.b': 1, 'c.d.e': 1}")});
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, ExpandEnsureMultikeySetForAllCompoundFieldsWithFlippedKeyOrder) {
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a.b", "c.d.e"};
|
||||
const auto indexEntry = makeIndexEntry(
|
||||
BSON("$**" << 1 << "a.b" << 1),
|
||||
{},
|
||||
{FieldRef("a"), FieldRef("a.b"), FieldRef("b"), FieldRef("c"), FieldRef("c.d.e")},
|
||||
{fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
ASSERT_EQ(out.size(), 1u);
|
||||
ASSERT_TRUE(out[0].multikey);
|
||||
ASSERT_EQ(out[0].multikeyPaths.size(), 2);
|
||||
ASSERT((out[0].multikeyPaths[0] == MultikeyComponents{0u, 2u})); // c and c.d.e are multikey
|
||||
ASSERT((out[0].multikeyPaths[1] == MultikeyComponents{0u, 1u})); // a and a.b are multikey
|
||||
ASSERT_BSONOBJ_EQ(out[0].keyPattern, {fromjson("{'c.d.e': 1, 'a.b': 1}")});
|
||||
}
|
||||
|
||||
/*************************************** end section ***************************************/
|
||||
|
||||
// translateWildcardIndexBoundsAndTightness
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, TranslateBoundsWithWildcard) {
|
||||
// expand first
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a", "b"};
|
||||
const auto indexEntry = makeIndexEntry(
|
||||
BSON("a" << 1 << "$**" << 1), {}, {}, {fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
// This expression can only be over one field. WTS that given a query on a field and a compound
|
||||
// index on that field (followed by wildcard) that we translate properly.
|
||||
BSONObj obj = fromjson("{a: {$lte: 1}}");
|
||||
auto expr = parseMatchExpression(obj);
|
||||
BSONElement elt = obj.firstElement();
|
||||
OrderedIntervalList oil;
|
||||
IndexBoundsBuilder::BoundsTightness tightness;
|
||||
IndexBoundsBuilder::translate(expr.get(), elt, out[0], &oil, &tightness);
|
||||
ASSERT_EQUALS(oil.name, "a");
|
||||
ASSERT_EQUALS(oil.intervals.size(), 1U);
|
||||
ASSERT_EQUALS(
|
||||
Interval::INTERVAL_EQUALS,
|
||||
oil.intervals[0].compare(Interval(fromjson("{'': -Infinity, '': 1}"), true, true)));
|
||||
ASSERT(tightness == IndexBoundsBuilder::EXACT);
|
||||
}
|
||||
|
||||
TEST_F(PlannerWildcardHelpersTest, TranslateBoundsWithWildcardFlippedIndexKeys) {
|
||||
// expand first
|
||||
std::vector<IndexEntry> out;
|
||||
stdx::unordered_set<std::string> fields{"a", "b"};
|
||||
const auto indexEntry = makeIndexEntry(
|
||||
BSON("$**" << 1 << "a" << 1), {}, {}, {fromjson("{wildcardProjection: {a: 0}}")});
|
||||
wildcard_planning::expandWildcardIndexEntry(indexEntry.first, fields, &out);
|
||||
|
||||
// This expression can only be over one field. WTS that given a query on field b and a compound
|
||||
// index on a wildcard including b (followed by another field) that we translate properly.
|
||||
BSONObj obj = fromjson("{b: {$lte: 1}}");
|
||||
auto expr = parseMatchExpression(obj);
|
||||
BSONElement elt = obj.firstElement();
|
||||
OrderedIntervalList oil;
|
||||
IndexBoundsBuilder::BoundsTightness tightness;
|
||||
IndexBoundsBuilder::translate(expr.get(), elt, out[0], &oil, &tightness);
|
||||
ASSERT_EQUALS(oil.name, "b");
|
||||
ASSERT_EQUALS(oil.intervals.size(), 1U);
|
||||
ASSERT_EQUALS(
|
||||
Interval::INTERVAL_EQUALS,
|
||||
oil.intervals[0].compare(Interval(fromjson("{'': -Infinity, '': 1}"), true, true)));
|
||||
ASSERT(tightness == IndexBoundsBuilder::EXACT);
|
||||
}
|
||||
|
||||
|
||||
// How to test?
|
||||
// finalizeWildcardIndexScanConfiguration(IndexScanNode* scan);
|
||||
// isWildcardObjectSubpathScan(const IndexScanNode* node);
|
||||
|
||||
|
||||
/********** The following section can be moved to query_planner_wildcard_index_test.cpp **********/
|
||||
class QueryPlannerWildcardTest : public QueryPlannerTest {
|
||||
protected:
|
||||
void setUp() final {
|
||||
QueryPlannerTest::setUp();
|
||||
|
||||
// We're interested in testing plans that use a $** index, so don't generate collection
|
||||
// scans.
|
||||
params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
|
||||
}
|
||||
|
||||
void addWildcardIndex(BSONObj keyPattern,
|
||||
const std::set<std::string>& multikeyPathSet = {},
|
||||
BSONObj wildcardProjection = BSONObj{},
|
||||
MatchExpression* partialFilterExpr = nullptr,
|
||||
CollatorInterface* collator = nullptr,
|
||||
const std::string& indexName = "indexName") {
|
||||
// Convert the set of std::string to a set of FieldRef.
|
||||
std::set<FieldRef> multikeyFieldRefs;
|
||||
for (auto&& path : multikeyPathSet) {
|
||||
ASSERT_TRUE(multikeyFieldRefs.emplace(path).second);
|
||||
}
|
||||
ASSERT_EQ(multikeyPathSet.size(), multikeyFieldRefs.size());
|
||||
|
||||
const bool isMultikey = !multikeyPathSet.empty();
|
||||
BSONObj infoObj = BSON("wildcardProjection" << wildcardProjection);
|
||||
|
||||
_proj = WildcardKeyGenerator::createProjectionExecutor(keyPattern, wildcardProjection);
|
||||
|
||||
params.indices.push_back({std::move(keyPattern),
|
||||
IndexType::INDEX_WILDCARD,
|
||||
IndexDescriptor::kLatestIndexVersion,
|
||||
isMultikey,
|
||||
{}, // multikeyPaths
|
||||
std::move(multikeyFieldRefs),
|
||||
false, // sparse
|
||||
false, // unique
|
||||
IndexEntry::Identifier{indexName},
|
||||
partialFilterExpr,
|
||||
std::move(infoObj),
|
||||
collator,
|
||||
_proj.get_ptr()});
|
||||
}
|
||||
|
||||
boost::optional<WildcardProjection> _proj;
|
||||
};
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexQueryOnlyOnNonWCFieldWithProjection) {
|
||||
addWildcardIndex(fromjson("{a: 1, '$**': 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, '$_path': 1, '$_value': 1}, bounds: {'a': "
|
||||
"[[5, 5, true, true]], '$_path': [['$_value', '$_value', true, true]], '$_value': "
|
||||
"[['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexQueryOnlyOnNonWCField) {
|
||||
addWildcardIndex(fromjson("{a: 1, 'b.$**': 1}"), {});
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, '$_path': 1, 'b.$_value': 1}, bounds: {'a': "
|
||||
"[[5, 5, true, true]], '$_path': [['b.$_value', 'b.$_value', true, true]], 'b.$_value': "
|
||||
"[['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexQueryOnMultipleNonWCField) {
|
||||
addWildcardIndex(fromjson("{a: 1, x: 1, 'b.$**': 1}"), {});
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}}"));
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, x: 1, '$_path': 1, 'b.$_value': 1}, bounds: {'a':"
|
||||
"[[5, 5, true, true]], 'x': [['MinKey', 'MaxKey', true, true]], '$_path': [['b.$_value', "
|
||||
"'b.$_value', true, true]], 'b.$_value': [['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}, x: {$lt: 2}}"));
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, x: 1, '$_path': 1, 'b.$_value': 1}, bounds: {'a':"
|
||||
"[[5, 5, true, true]], 'x': [[-Infinity, 2, true, false]], '$_path': [['b.$_value', "
|
||||
"'b.$_value', true, true]], 'b.$_value': [['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexBasic) {
|
||||
addWildcardIndex(fromjson("{a: 1, '$**': 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}, x: {$lt: 3}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, $_path: 1, x: 1}, bounds: {'a': "
|
||||
"[[5, 5, true, true]], '$_path': [['x', 'x', true, true]], 'x': [[-Infinity, 3, true, "
|
||||
"false]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexIsNotUsedWhenQueryNotOnIndexPrefix) {
|
||||
addWildcardIndex(fromjson("{a: 1, '$**': 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{x: {$lt: 3}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists("{cscan: {dir: 1}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest,
|
||||
CompoundWildcardIndexIsNotUsedWhenQueryNotOnIndexPrefixAndNotIncludedInWildcard) {
|
||||
addWildcardIndex(fromjson("{a: 1, 'b.$**': 1}"), {});
|
||||
|
||||
runQuery(fromjson("{c: {$eq: 5}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists("{cscan: {dir: 1}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundEqualsNullQueryDoesUseWildcardIndexes) {
|
||||
addWildcardIndex(fromjson("{a: 1, '$**': 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{a: {$lt: 2}, x: {$eq: null}}"));
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {filter: {'x': {$eq: null}}, node: "
|
||||
"{ixscan: {filter: null, pattern: {'a': 1, '$_path': 1, 'x': 1},"
|
||||
"bounds: {'a': [[-Infinity, 2, true, false]], '$_path': [['x','x',true,true]], 'x': "
|
||||
"[['MinKey','MaxKey',true,true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardWithMultikeyField) {
|
||||
addWildcardIndex(
|
||||
fromjson("{a: 1, '$**': 1}"), {"b"} /* 'b' marked as multikey field */, fromjson("{a: 0}"));
|
||||
runQuery(fromjson("{a: {$eq: 5}, b: {$gt: 0}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, $_path: 1, b: 1}, bounds: {'a': "
|
||||
"[[5, 5, true, true]], '$_path': [['b','b',true,true]], b: "
|
||||
"[[0,Infinity,false,true]]}}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest,
|
||||
CompoundWildcardMultiplePredicatesOverNestedFieldWithFirstComponentMultikey) {
|
||||
addWildcardIndex(fromjson("{x: 1, '$**': 1}"), {"a"}, fromjson("{x: 0}"));
|
||||
runQuery(fromjson("{x: {$lt: 2}, 'a.b': {$gt: 0, $lt: 9}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {filter: {'a.b': {$gt: 0}}, node: "
|
||||
"{ixscan: {filter: null, pattern: {'x': 1, '$_path': 1, 'a.b': 1},"
|
||||
"bounds: {'x': [[-Infinity, 2, true, false]], '$_path': [['a.b','a.b',true,true]], 'a.b': "
|
||||
"[[-Infinity,9,true,false]]}}}}}");
|
||||
// TODO SERVER-56118 This solution should be generated.
|
||||
// assertSolutionExists(
|
||||
// "{fetch: {filter: {'a.b': {$gt: 0}}, node: "
|
||||
// "{ixscan: {filter: null, pattern: {'x': 1, '$_path': 1, 'a.b': 1},"
|
||||
// "bounds: {'x': [[-Infinity, 2, true, false]], '$_path': [['a.b','a.b',true,true]], 'a.b':
|
||||
// "
|
||||
// "[[0,Infinity,false,true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest,
|
||||
CompoundWildcardAllPredsEligibleForIndexUseGenerateCandidatePlans) {
|
||||
addWildcardIndex(fromjson("{x: 1, 'a.$**': 1}"), {"a.b", "a.c"});
|
||||
runQuery(
|
||||
fromjson("{x: {$eq: 2}, 'a.b': {$gt: 0, $lt: 9}, 'a.c': {$gt: 11, $lt: 20}, d: {$gt: 31, "
|
||||
"$lt: 40}}"));
|
||||
|
||||
// TODO SERVER-56118: Should generate 4 plans here. Missing the plans where $gts are bounded
|
||||
// instead of $lts.
|
||||
assertNumSolutions(2U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {filter: {'a.b':{$gt:0,$lt: 9},'a.c':{$gt:11},d:{$gt:31,$lt:40}}, node: "
|
||||
"{ixscan: {filter: null, pattern: {'x': 1, '$_path': 1, 'a.c': 1},"
|
||||
"bounds: {'x': [[2, 2, true, true]], '$_path': [['a.c','a.c',true,true]], 'a.c': "
|
||||
"[[-Infinity,20,true,false]]}}}}}");
|
||||
assertSolutionExists(
|
||||
"{fetch: {filter: {'a.b':{$gt:0},'a.c':{$gt:11,$lt:20},d:{$gt:31,$lt:40}}, node: "
|
||||
"{ixscan: {filter: null, pattern: {'x': 1, '$_path': 1, 'a.b': 1},"
|
||||
"bounds: {'x': [[2, 2, true, true]], '$_path': [['a.b','a.b',true,true]], 'a.b': "
|
||||
"[[-Infinity,9,true,false]]}}}}}");
|
||||
}
|
||||
|
||||
|
||||
// The following tests create compound wildcard indexes where the wildcard component is not last.
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest,
|
||||
CompoundWildcardIndexQueryOnlyOnNonWCFieldWithProjectionFlippedOrder) {
|
||||
addWildcardIndex(fromjson("{'$**': 1, a: 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{c: {$eq: 5}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {'$_path': 1, 'c': 1, a: 1}, bounds: {'$_path': "
|
||||
"[['c', 'c', true, true]], 'c': [[5, 5, true, true]],"
|
||||
"'a': [['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexQueryWCFieldInMiddleOfKey) {
|
||||
addWildcardIndex(fromjson("{a: 1, 'b.$**': 1, x: 1}"), {});
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}}"));
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, '$_path': 1, 'b.$_value': 1, x: 1}, bounds: {'a':"
|
||||
"[[5, 5, true, true]], '$_path': [['b.$_value', 'b.$_value', true, true]], 'b.$_value': "
|
||||
"[['MinKey', 'MaxKey', true, true]], 'x': [['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}, 'b.c': {$lt: 2}}"));
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, '$_path': 1, 'b.c': 1, x: 1}, bounds: {'a':"
|
||||
"[[5, 5, true, true]], '$_path': [['b.c', 'b.c', true, true]], 'b.c': "
|
||||
"[[-Infinity, 2, true, false]], 'x': [['MinKey', 'MaxKey', true, true]]}}}}}");
|
||||
|
||||
runQuery(fromjson("{a: {$eq: 5}, 'b.c': {$lt: 2}, 'x': {$eq: 4}}"));
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, '$_path': 1, 'b.c': 1, x: 1}, bounds: {'a':"
|
||||
"[[5, 5, true, true]], '$_path': [['b.c', 'b.c', true, true]], 'b.c': "
|
||||
"[[-Infinity, 2, true, false]], 'x': [[4, 4, true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexBasicWithFlippedIndexKeys) {
|
||||
addWildcardIndex(fromjson("{'$**': 1, a: 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{x: {$lt: 3}, a: {$eq: 5}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {$_path: 1, x: 1, a: 1}, bounds: {'$_path': "
|
||||
"[['x', 'x', true, true]], 'x': [[-Infinity, 3, true, false]],"
|
||||
"'a': [[5, 5, true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardIndexIsNotUsedWhenQueryNotOnPrefixFlippedKeys) {
|
||||
addWildcardIndex(fromjson("{'$**': 1, a: 1}"), {}, fromjson("{a: 0}"));
|
||||
|
||||
runQuery(fromjson("{a: {$lt: 3}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists("{cscan: {dir: 1}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest, CompoundWildcardWithMultikeyFieldFlippedKeys) {
|
||||
addWildcardIndex(
|
||||
fromjson("{'$**': 1, a: 1}"), {"b"} /* 'b' marked as multikey field */, fromjson("{a: 0}"));
|
||||
runQuery(fromjson("{b: {$gt: 0}, a: {$eq: 5}}"));
|
||||
|
||||
assertNumSolutions(1U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {$_path: 1, b: 1, a: 1}, bounds: {'$_path': "
|
||||
"[['b','b',true,true]], b: [[0,Infinity,false,true]], "
|
||||
"'a': [[5, 5, true, true]]}}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerWildcardTest,
|
||||
CompoundWildcardMultiplePredicatesOverNestedFieldWithFirstComponentMultikeyFlippedKeys) {
|
||||
addWildcardIndex(fromjson("{'$**': 1, x: 1}"), {"a"}, fromjson("{x: 0}"));
|
||||
runQuery(fromjson("{'a.b': {$gt: 0, $lt: 9}, x: {$lt: 2}}"));
|
||||
|
||||
assertNumSolutions(2U);
|
||||
assertSolutionExists(
|
||||
"{fetch: {filter: {'a.b': {$gt: 0}}, node: "
|
||||
"{ixscan: {filter: null, pattern: {'$_path': 1, 'a.b': 1, 'x': 1},"
|
||||
"bounds: {'$_path': [['a.b','a.b',true,true]], 'a.b': [[-Infinity,9,true,false]],"
|
||||
"'x': [[-Infinity, 2, true, false]]}}}}}");
|
||||
assertSolutionExists(
|
||||
"{fetch: {filter: {'a.b': {$lt: 9}}, node: "
|
||||
"{ixscan: {filter: null, pattern: {'$_path': 1, 'a.b': 1, 'x': 1},"
|
||||
"bounds: {'$_path': [['a.b','a.b',true,true]], 'a.b': [[0,Infinity,false,true]],"
|
||||
"'x': [[-Infinity, 2, true, false]]}}}}}");
|
||||
}
|
||||
} // namespace mongo
|
||||
@@ -1276,6 +1276,19 @@ TEST_F(QueryPlannerTest, CannotIntersectBoundsWhenSecondFieldIsMultikey) {
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, "
|
||||
"bounds: {a: [[2, 2, true, true]], b: [[-Infinity, 10, true, false]]}}}}}");
|
||||
// TODO SERVER-56118 expect to see another plan with (0, Infinity] also.
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerTest, CanBuildGtBoundsOnSecondFieldIfFirstIsMultikey) {
|
||||
MultikeyPaths multikeyPaths{MultikeyComponents{}, {0U}};
|
||||
addIndex(BSON("a" << 1 << "b" << 1), multikeyPaths);
|
||||
runQuery(fromjson("{a: 2, b: {$gte: 0}}"));
|
||||
|
||||
assertNumSolutions(2U);
|
||||
assertSolutionExists("{cscan: {dir: 1, filter: {a: 2, b: {$gte: 0}}}}");
|
||||
assertSolutionExists(
|
||||
"{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, "
|
||||
"bounds: {a: [[2, 2, true, true]], b: [[0, Infinity, true, true]]}}}}}");
|
||||
}
|
||||
|
||||
TEST_F(QueryPlannerTest, CanIntersectBoundsWhenSecondFieldIsMultikeyButHasElemMatch) {
|
||||
|
||||
@@ -517,7 +517,8 @@ void QueryPlannerTest::assertNumSolutions(size_t expectSolutions) const {
|
||||
}
|
||||
str::stream ss;
|
||||
ss << "expected " << expectSolutions << " solutions but got " << getNumSolutions()
|
||||
<< " instead. solutions generated: " << '\n';
|
||||
<< " instead. Run with --verbose=vv to see reasons for mismatch. Solutions generated: "
|
||||
<< '\n';
|
||||
dumpSolutions(ss);
|
||||
FAIL(ss);
|
||||
}
|
||||
@@ -526,8 +527,15 @@ size_t QueryPlannerTest::numSolutionMatches(const std::string& solnJson) const {
|
||||
BSONObj testSoln = fromjson(solnJson);
|
||||
size_t matches = 0;
|
||||
for (auto&& soln : solns) {
|
||||
if (QueryPlannerTestLib::solutionMatches(testSoln, soln->root(), relaxBoundsCheck)) {
|
||||
auto matchStatus =
|
||||
QueryPlannerTestLib::solutionMatches(testSoln, soln->root(), relaxBoundsCheck);
|
||||
if (matchStatus.isOK()) {
|
||||
++matches;
|
||||
} else {
|
||||
LOGV2_DEBUG(51551101,
|
||||
2,
|
||||
"Mismatching solution: {reason}",
|
||||
"reason"_attr = matchStatus.reason());
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
@@ -540,7 +548,9 @@ void QueryPlannerTest::assertSolutionExists(const std::string& solnJson, size_t
|
||||
}
|
||||
str::stream ss;
|
||||
ss << "expected " << numMatches << " matches for solution " << solnJson << " but got "
|
||||
<< matches << " instead. all solutions generated: " << '\n';
|
||||
<< matches
|
||||
<< " instead. Run with --verbose=vv to see reasons for mismatch. All solutions generated: "
|
||||
<< '\n';
|
||||
dumpSolutions(ss);
|
||||
FAIL(ss);
|
||||
}
|
||||
@@ -558,7 +568,9 @@ void QueryPlannerTest::assertHasOneSolutionOf(const std::vector<std::string>& so
|
||||
}
|
||||
str::stream ss;
|
||||
ss << "assertHasOneSolutionOf expected one matching solution"
|
||||
<< " but got " << matches << " instead. all solutions generated: " << '\n';
|
||||
<< " but got " << matches
|
||||
<< " instead. Run with --verbose=vv to see reasons for mismatch. All solutions generated: "
|
||||
<< '\n';
|
||||
dumpSolutions(ss);
|
||||
FAIL(ss);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,9 +59,9 @@ public:
|
||||
* Means that the index bounds on field 'a' consist of the two intervals
|
||||
* [1, 2) and (3, 4] and the index bounds on field 'b' are [-Infinity, Infinity].
|
||||
*/
|
||||
static bool boundsMatch(const BSONObj& testBounds,
|
||||
const IndexBounds trueBounds,
|
||||
bool relaxBoundsCheck);
|
||||
static Status boundsMatch(const BSONObj& testBounds,
|
||||
const IndexBounds trueBounds,
|
||||
bool relaxBoundsCheck);
|
||||
|
||||
/**
|
||||
* @param testSoln -- a BSON representation of a query solution
|
||||
@@ -69,16 +69,16 @@ public:
|
||||
* @param: relaxBoundsCheck -- If 'true', will perform a relaxed "subset" check on index bounds.
|
||||
* Will perform a full check otherwise.
|
||||
*
|
||||
* Returns true if the BSON representation matches the actual
|
||||
* tree, otherwise returns false.
|
||||
* Returns Status::OK() if the BSON representation matches the actual tree, otherwise returns
|
||||
* a non-OK status indicating what did not match.
|
||||
*/
|
||||
static bool solutionMatches(const BSONObj& testSoln,
|
||||
const QuerySolutionNode* trueSoln,
|
||||
bool relaxBoundsCheck = false);
|
||||
static Status solutionMatches(const BSONObj& testSoln,
|
||||
const QuerySolutionNode* trueSoln,
|
||||
bool relaxBoundsCheck = false);
|
||||
|
||||
static bool solutionMatches(const std::string& testSoln,
|
||||
const QuerySolutionNode* trueSoln,
|
||||
bool relaxBoundsCheck = false) {
|
||||
static Status solutionMatches(const std::string& testSoln,
|
||||
const QuerySolutionNode* trueSoln,
|
||||
bool relaxBoundsCheck = false) {
|
||||
return solutionMatches(fromjson(testSoln), trueSoln, relaxBoundsCheck);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -536,9 +536,9 @@ FieldAvailability IndexScanNode::getFieldAvailability(const string& field) const
|
||||
size_t keyPatternFieldIndex = 0;
|
||||
for (auto&& elt : index.keyPattern) {
|
||||
// For $** indexes, the keyPattern is prefixed by a virtual field, '$_path'. We therefore
|
||||
// skip the first keyPattern field when deciding whether we can provide the requested field.
|
||||
if (index.type == IndexType::INDEX_WILDCARD && !keyPatternFieldIndex) {
|
||||
invariant(elt.fieldNameStringData() == "$_path"_sd);
|
||||
// skip this keyPattern field when deciding whether we can provide the requested field.
|
||||
if (index.type == IndexType::INDEX_WILDCARD && !keyPatternFieldIndex &&
|
||||
elt.fieldNameStringData() == "$_path"_sd) {
|
||||
++keyPatternFieldIndex;
|
||||
continue;
|
||||
}
|
||||
@@ -766,25 +766,17 @@ ProvidedSortSet computeSortsForScan(const IndexEntry& index,
|
||||
// fact, $-prefixed path components are illegal in queries in most contexts, so misinterpreting
|
||||
// this as a path in user-data could trigger subsequent assertions.
|
||||
if (index.type == IndexType::INDEX_WILDCARD) {
|
||||
invariant(bounds.fields.size() == 2u);
|
||||
|
||||
// No sorts are provided if the bounds for '$_path' consist of multiple intervals. This can
|
||||
// happen for existence queries. For example, {a: {$exists: true}} results in bounds
|
||||
// [["a","a"], ["a.", "a/")] for '$_path' so that keys from documents where "a" is a nested
|
||||
// object are in bounds.
|
||||
if (bounds.fields[0].intervals.size() != 1u) {
|
||||
// object are in bounds. The '$_path' field must be immediately before the wildcard field.
|
||||
if (bounds.fields[index.wildcardFieldIndex - 1].intervals.size() != 1u) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Strip '$_path' out of 'sortPattern' and then proceed with regular sort analysis.
|
||||
BSONObjIterator it{sortPatternProvidedByIndex};
|
||||
invariant(it.more());
|
||||
auto pathElement = it.next();
|
||||
invariant(pathElement.fieldNameStringData() == "$_path"_sd);
|
||||
invariant(it.more());
|
||||
auto secondElement = it.next();
|
||||
invariant(!it.more());
|
||||
sortPatternProvidedByIndex = BSONObjBuilder{}.append(secondElement).obj();
|
||||
invariant(sortPatternProvidedByIndex.hasField("$_path"));
|
||||
sortPatternProvidedByIndex = sortPatternProvidedByIndex.removeField("$_path"_sd);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -32,36 +32,72 @@
|
||||
#include "mongo/db/query/wildcard_multikey_paths.h"
|
||||
|
||||
#include "mongo/db/concurrency/write_conflict_exception.h"
|
||||
#include "mongo/db/exec/document_value/value.h"
|
||||
#include "mongo/db/index/wildcard_access_method.h"
|
||||
#include "mongo/db/query/index_bounds_builder.h"
|
||||
|
||||
namespace mongo {
|
||||
|
||||
namespace {
|
||||
|
||||
bool isWildcardPart(BSONElement keyPatternElem) {
|
||||
return keyPatternElem.fieldNameStringData() == "$**" ||
|
||||
keyPatternElem.fieldNameStringData().endsWith(".$**");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the multikey path from a metadata key stored within a wildcard index.
|
||||
*/
|
||||
static FieldRef extractMultikeyPathFromIndexKey(const IndexKeyEntry& entry) {
|
||||
static FieldRef extractMultikeyPathFromIndexKey(BSONObj keyPattern, const IndexKeyEntry& entry) {
|
||||
invariant(RecordIdReservations::isReserved(entry.loc));
|
||||
invariant(
|
||||
entry.loc.getLong() ==
|
||||
RecordIdReservations::reservedIdFor(ReservationId::kWildcardMultikeyMetadataId).getLong());
|
||||
|
||||
// Validate that the first piece of the key is the integer 1.
|
||||
BSONObjIterator iter(entry.key);
|
||||
invariant(iter.more());
|
||||
const auto firstElem = iter.next();
|
||||
invariant(firstElem.isNumber());
|
||||
invariant(firstElem.numberInt() == 1);
|
||||
invariant(iter.more());
|
||||
BSONObjIterator indexIter(entry.key);
|
||||
BSONObjIterator keyPatternIter(keyPattern);
|
||||
while (keyPatternIter.more()) {
|
||||
invariant(indexIter.more());
|
||||
const auto indexKeyElem = indexIter.next();
|
||||
const auto keyPatternElem = keyPatternIter.next();
|
||||
if (isWildcardPart(keyPatternElem)) {
|
||||
invariant(indexKeyElem.isNumber());
|
||||
invariant(indexKeyElem.numberInt() == 1);
|
||||
invariant(indexIter.more());
|
||||
|
||||
// Extract the path from the second piece of the key.
|
||||
const auto secondElem = iter.next();
|
||||
invariant(!iter.more());
|
||||
invariant(secondElem.type() == BSONType::String);
|
||||
|
||||
return FieldRef(secondElem.valueStringData());
|
||||
// Extract the path from the second piece of the key.
|
||||
const auto secondIndexElem = indexIter.next();
|
||||
invariant(secondIndexElem.type() == BSONType::String);
|
||||
return FieldRef(secondIndexElem.valueStringData());
|
||||
} else {
|
||||
// This is a non-wildcard part of the index key which should be ignored and always
|
||||
// encoded as MinKey for multikey paths.
|
||||
invariant(indexKeyElem.type() == BSONType::MinKey);
|
||||
}
|
||||
}
|
||||
MONGO_UNREACHABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the normal keyPattern which is expected to include at least one wildcard part, inflates
|
||||
* each wildcard part into two: one for the path, one for the value.
|
||||
* For example: {a: 1, "$**": -1} is inflated to {a: 1, $path: 1, $value: -1}
|
||||
*/
|
||||
BSONObj inflateKeyPatternForBounds(BSONObj keyPattern) {
|
||||
BSONObjBuilder inflated;
|
||||
for (auto&& elem : keyPattern) {
|
||||
if (isWildcardPart(elem)) {
|
||||
inflated.append("$path", 1);
|
||||
inflated.appendAs(elem, "$value");
|
||||
} else {
|
||||
inflated.append(elem);
|
||||
}
|
||||
}
|
||||
return inflated.obj();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* Retrieves from the wildcard index the set of multikey path metadata keys bounded by
|
||||
* 'indexBounds'. Returns the set of multikey paths represented by the keys.
|
||||
@@ -70,6 +106,7 @@ static std::set<FieldRef> getWildcardMultikeyPathSetHelper(const WildcardAccessM
|
||||
OperationContext* opCtx,
|
||||
const IndexBounds& indexBounds,
|
||||
MultikeyMetadataAccessStats* stats) {
|
||||
const auto keyPattern = wam->getKeyPattern();
|
||||
return writeConflictRetry(
|
||||
opCtx, "wildcard multikey path retrieval", "", [&]() -> std::set<FieldRef> {
|
||||
stats->numSeeks = 0;
|
||||
@@ -77,8 +114,8 @@ static std::set<FieldRef> getWildcardMultikeyPathSetHelper(const WildcardAccessM
|
||||
auto cursor = wam->newCursor(opCtx);
|
||||
|
||||
constexpr int kForward = 1;
|
||||
const auto keyPattern = BSON("" << 1 << "" << 1);
|
||||
IndexBoundsChecker checker(&indexBounds, keyPattern, kForward);
|
||||
auto newPattern = inflateKeyPatternForBounds(keyPattern);
|
||||
IndexBoundsChecker checker(&indexBounds, newPattern, kForward);
|
||||
IndexSeekPoint seekPoint;
|
||||
if (!checker.getStartSeekPoint(&seekPoint)) {
|
||||
return {};
|
||||
@@ -98,7 +135,7 @@ static std::set<FieldRef> getWildcardMultikeyPathSetHelper(const WildcardAccessM
|
||||
|
||||
switch (checker.checkKey(entry->key, &seekPoint)) {
|
||||
case IndexBoundsChecker::VALID:
|
||||
multikeyPaths.emplace(extractMultikeyPathFromIndexKey(*entry));
|
||||
multikeyPaths.emplace(extractMultikeyPathFromIndexKey(keyPattern, *entry));
|
||||
entry = cursor->next();
|
||||
break;
|
||||
|
||||
@@ -174,32 +211,64 @@ std::set<FieldRef> getWildcardMultikeyPathSet(const WildcardAccessMethod* wam,
|
||||
invariant(stats);
|
||||
IndexBounds indexBounds;
|
||||
|
||||
// Multikey metadata keys are stored with the number "1" in the first position of the index to
|
||||
// differentiate them from user-data keys, which contain a string representing the path.
|
||||
OrderedIntervalList multikeyPathFlagOil;
|
||||
multikeyPathFlagOil.intervals.push_back(IndexBoundsBuilder::makePointInterval(BSON("" << 1)));
|
||||
indexBounds.fields.push_back(std::move(multikeyPathFlagOil));
|
||||
for (auto&& elem : wam->getKeyPattern()) {
|
||||
if (isWildcardPart(elem)) {
|
||||
// This is a wildcard component, so we need two bounds:
|
||||
// Multikey metadata keys are stored with the number "1" in the first position of the
|
||||
// index to differentiate them from user-data keys, which contain a string representing
|
||||
// the path. Anything with a number "1" in the first position represents a path which is
|
||||
// multikey, and the second position will be that path.
|
||||
|
||||
OrderedIntervalList fieldNameOil;
|
||||
// Make the point interval for the number "1".
|
||||
indexBounds.fields.push_back(
|
||||
OrderedIntervalList{{IndexBoundsBuilder::makePointInterval(BSON("" << 1))}});
|
||||
|
||||
for (const auto& field : fieldSet) {
|
||||
auto intervals = getMultikeyPathIndexIntervalsForField(FieldRef(field));
|
||||
fieldNameOil.intervals.insert(fieldNameOil.intervals.end(),
|
||||
std::make_move_iterator(intervals.begin()),
|
||||
std::make_move_iterator(intervals.end()));
|
||||
// Now make the range interval for any paths. Here we make a series of point intervals
|
||||
// for each path of interest given in 'fieldSet'.
|
||||
OrderedIntervalList fieldNameOil;
|
||||
for (const auto& field : fieldSet) {
|
||||
auto intervals = getMultikeyPathIndexIntervalsForField(FieldRef(field));
|
||||
fieldNameOil.intervals.insert(fieldNameOil.intervals.end(),
|
||||
std::make_move_iterator(intervals.begin()),
|
||||
std::make_move_iterator(intervals.end()));
|
||||
}
|
||||
|
||||
// IndexBoundsBuilder::unionize() sorts the OrderedIntervalList allowing for in order
|
||||
// index traversal.
|
||||
IndexBoundsBuilder::unionize(&fieldNameOil);
|
||||
indexBounds.fields.push_back(std::move(fieldNameOil));
|
||||
} else {
|
||||
// This is not a wildcard path component of the index. To find the multikey metadata, we
|
||||
// need to look in the point range for MinKey.
|
||||
indexBounds.fields.push_back(
|
||||
OrderedIntervalList{{IndexBoundsBuilder::makePointInterval(BSON("" << MINKEY))}});
|
||||
}
|
||||
}
|
||||
|
||||
// IndexBoundsBuilder::unionize() sorts the OrderedIntervalList allowing for in order index
|
||||
// traversal.
|
||||
IndexBoundsBuilder::unionize(&fieldNameOil);
|
||||
indexBounds.fields.push_back(std::move(fieldNameOil));
|
||||
|
||||
return getWildcardMultikeyPathSetHelper(wam, opCtx, indexBounds, stats);
|
||||
}
|
||||
|
||||
namespace {
|
||||
BSONObj makeMultiKeyIndexBound(BSONObj keyPattern, Value bound) {
|
||||
BSONObjBuilder boundBuilder;
|
||||
for (auto&& elem : keyPattern) {
|
||||
if (elem.fieldNameStringData() == "$**" || elem.fieldNameStringData().endsWith(".$**")) {
|
||||
boundBuilder.append("", 1);
|
||||
boundBuilder << "" << bound;
|
||||
} else {
|
||||
boundBuilder.appendMinKey("");
|
||||
}
|
||||
}
|
||||
return boundBuilder.obj();
|
||||
}
|
||||
} // namespace
|
||||
std::set<FieldRef> getWildcardMultikeyPathSet(const WildcardAccessMethod* wam,
|
||||
OperationContext* opCtx,
|
||||
MultikeyMetadataAccessStats* stats) {
|
||||
const auto keyPattern = wam->getKeyPattern();
|
||||
// All of the keys storing multikeyness metadata are prefixed by a value of 1. Establish
|
||||
// an index cursor which will scan this range.
|
||||
const BSONObj metadataKeyRangeBegin = makeMultiKeyIndexBound(keyPattern, Value(MINKEY));
|
||||
const BSONObj metadataKeyRangeEnd = makeMultiKeyIndexBound(keyPattern, Value(MAXKEY));
|
||||
return writeConflictRetry(opCtx, "wildcard multikey path retrieval", "", [&]() {
|
||||
invariant(stats);
|
||||
stats->numSeeks = 0;
|
||||
@@ -207,11 +276,6 @@ std::set<FieldRef> getWildcardMultikeyPathSet(const WildcardAccessMethod* wam,
|
||||
|
||||
auto cursor = wam->newCursor(opCtx);
|
||||
|
||||
// All of the keys storing multikeyness metadata are prefixed by a value of 1. Establish
|
||||
// an index cursor which will scan this range.
|
||||
const BSONObj metadataKeyRangeBegin = BSON("" << 1 << "" << MINKEY);
|
||||
const BSONObj metadataKeyRangeEnd = BSON("" << 1 << "" << MAXKEY);
|
||||
|
||||
constexpr bool inclusive = true;
|
||||
cursor->setEndPosition(metadataKeyRangeEnd, inclusive);
|
||||
|
||||
@@ -228,7 +292,7 @@ std::set<FieldRef> getWildcardMultikeyPathSet(const WildcardAccessMethod* wam,
|
||||
std::set<FieldRef> multikeyPaths{};
|
||||
while (entry) {
|
||||
++stats->keysExamined;
|
||||
multikeyPaths.emplace(extractMultikeyPathFromIndexKey(*entry));
|
||||
multikeyPaths.emplace(extractMultikeyPathFromIndexKey(keyPattern, *entry));
|
||||
|
||||
entry = cursor->next();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user