Compare commits

...

18 Commits

Author SHA1 Message Date
Charlie Swanson
c1f5f78e39 Add performance test scripts 2021-04-19 09:44:29 -04:00
Hana Pearlman
00c53e2a6f Permit compound wildcard index creation in js test 2021-04-16 16:30:08 +00:00
Hana Pearlman
5125c02156 Don't use set intersection 2021-04-16 14:45:39 +00:00
Hana Pearlman
f071c009b5 Merge 2021-04-16 14:17:34 +00:00
Hana Pearlman
a3d9c52375 Small typo fix 2021-04-16 14:13:48 +00:00
Hana Pearlman
9193a7d1fb Get multikey info for all wildcard index fields being queried 2021-04-16 14:12:15 +00:00
Hana Pearlman
f37dc54f9f Get multikey info for all wildcard index fields being queried 2021-04-16 13:43:11 +00:00
Hana Pearlman
2f99cbab75 Support wc fields in arbitrary index positions 2021-04-16 13:11:27 +00:00
Yuhong Zhang
7d9c507576 Add multikeyPath info of compound fields to multikeySet in key generation 2021-04-15 22:23:36 +00:00
Hana Pearlman
82f3289d6b Add multikey info for non-WC fields in planner 2021-04-15 19:56:56 +00:00
Hana Pearlman
42066838c1 Support queries on prefix of compound WC index 2021-04-15 15:36:37 +00:00
Yuhong Zhang
72d91de84c Adds restrictions on overlapping compound fields and removes the unnecessary invariant 2021-04-15 15:17:21 +00:00
Charlie Swanson
ebecb2102d File ticket and update TODO comments for known planner limitation 2021-04-15 10:08:30 -04:00
Hana Pearlman
fb8ebd45d8 Remove bounds invariant check in finalizeWildcardIndexScanConfiguration, add more tests 2021-04-14 21:34:39 +00:00
Yuhong Zhang
a29dc11773 Compound index key generation using BtreeKeyGenerator 2021-04-14 17:39:13 +00:00
Charlie Swanson
427fcea1ba Improvements to test harness 2021-04-14 13:03:16 -04:00
Hana Pearlman
0d6b561250 Make some changes to planner_wildcard_helpers to allow for compound wc indexes 2021-04-14 13:56:25 +00:00
Charlie Swanson
cfced033de Add initial implementation of compound wilcard index bounds 2021-04-13 15:39:49 -04:00
24 changed files with 2347 additions and 367 deletions

142
data_loader.js Normal file
View 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));
}());

View File

@@ -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
View 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
View 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}}}
]
};
*/

View File

@@ -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

View File

@@ -66,6 +66,10 @@ public:
return _keyGen.getWildcardProjection();
}
BSONObj getKeyPattern() const {
return _keyGen.getKeyPattern();
}
private:
void doGetKeys(SharedBufferFragmentBuilder& pooledBufferBuilder,
const BSONObj& obj,

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);
}
};

View File

@@ -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);
}
//

View File

@@ -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();
}