summaryrefslogtreecommitdiff
path: root/src/mongo/dbtests/queryoptimizercursortests.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/dbtests/queryoptimizercursortests.cpp')
-rw-r--r--src/mongo/dbtests/queryoptimizercursortests.cpp4889
1 files changed, 4889 insertions, 0 deletions
diff --git a/src/mongo/dbtests/queryoptimizercursortests.cpp b/src/mongo/dbtests/queryoptimizercursortests.cpp
new file mode 100644
index 00000000000..d8094e04a9f
--- /dev/null
+++ b/src/mongo/dbtests/queryoptimizercursortests.cpp
@@ -0,0 +1,4889 @@
+// queryoptimizertests.cpp : query optimizer unit tests
+//
+
+/**
+ * Copyright (C) 2009 10gen Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "pch.h"
+
+#include "../db/queryoptimizercursorimpl.h"
+#include "../db/queryoptimizer.h"
+#include "../db/instance.h"
+#include "mongo/db/pdfile.h"
+#include "mongo/db/clientcursor.h"
+#include "mongo/db/json.h"
+#include "mongo/client/dbclientcursor.h"
+#include "dbtests.h"
+
+namespace mongo {
+ void __forceLinkGeoPlugin();
+ shared_ptr<Cursor> newQueryOptimizerCursor( const char *ns, const BSONObj &query,
+ const BSONObj &order = BSONObj(),
+ const QueryPlanSelectionPolicy &planPolicy =
+ QueryPlanSelectionPolicy::any(),
+ bool requireOrder = true,
+ const shared_ptr<const ParsedQuery> &parsedQuery =
+ shared_ptr<const ParsedQuery>() );
+} // namespace mongo
+
+namespace QueryOptimizerCursorTests {
+
+ void dropCollection( const char *ns ) {
+ string errmsg;
+ BSONObjBuilder result;
+ dropCollection( ns, errmsg, result );
+ }
+
+ using boost::shared_ptr;
+
+ namespace CachedMatchCounter {
+
+ using mongo::CachedMatchCounter;
+
+ class Count {
+ public:
+ void run() {
+ long long aggregateNscanned;
+ CachedMatchCounter c( aggregateNscanned, 0 );
+ ASSERT_EQUALS( 0, c.count() );
+ ASSERT_EQUALS( 0, c.cumulativeCount() );
+
+ c.resetMatch();
+ ASSERT( !c.knowMatch() );
+
+ c.setMatch( false );
+ ASSERT( c.knowMatch() );
+
+ c.countMatch( DiskLoc() );
+ ASSERT_EQUALS( 0, c.count() );
+ ASSERT_EQUALS( 0, c.cumulativeCount() );
+
+ c.resetMatch();
+ ASSERT( !c.knowMatch() );
+
+ c.setMatch( true );
+ ASSERT( c.knowMatch() );
+
+ c.countMatch( DiskLoc() );
+ ASSERT_EQUALS( 1, c.count() );
+ ASSERT_EQUALS( 1, c.cumulativeCount() );
+
+ // Don't count the same match twice, without checking the document location.
+ c.countMatch( DiskLoc( 1, 1 ) );
+ ASSERT_EQUALS( 1, c.count() );
+ ASSERT_EQUALS( 1, c.cumulativeCount() );
+
+ // Reset and count another match.
+ c.resetMatch();
+ c.setMatch( true );
+ c.countMatch( DiskLoc( 1, 1 ) );
+ ASSERT_EQUALS( 2, c.count() );
+ ASSERT_EQUALS( 2, c.cumulativeCount() );
+ }
+ };
+
+ class Accumulate {
+ public:
+ void run() {
+ long long aggregateNscanned;
+ CachedMatchCounter c( aggregateNscanned, 10 );
+ ASSERT_EQUALS( 0, c.count() );
+ ASSERT_EQUALS( 10, c.cumulativeCount() );
+
+ c.setMatch( true );
+ c.countMatch( DiskLoc() );
+ ASSERT_EQUALS( 1, c.count() );
+ ASSERT_EQUALS( 11, c.cumulativeCount() );
+ }
+ };
+
+ class Dedup {
+ public:
+ void run() {
+ long long aggregateNscanned;
+ CachedMatchCounter c( aggregateNscanned, 0 );
+
+ c.setCheckDups( true );
+ c.setMatch( true );
+ c.countMatch( DiskLoc() );
+ ASSERT_EQUALS( 1, c.count() );
+
+ c.resetMatch();
+ c.setMatch( true );
+ c.countMatch( DiskLoc() );
+ ASSERT_EQUALS( 1, c.count() );
+ }
+ };
+
+ class Nscanned {
+ public:
+ void run() {
+ long long aggregateNscanned = 5;
+ CachedMatchCounter c( aggregateNscanned, 0 );
+ ASSERT_EQUALS( 0, c.nscanned() );
+ ASSERT_EQUALS( 5, c.aggregateNscanned() );
+
+ c.updateNscanned( 4 );
+ ASSERT_EQUALS( 4, c.nscanned() );
+ ASSERT_EQUALS( 9, c.aggregateNscanned() );
+ }
+ };
+
+ } // namespace CachedMatchCounter
+
+ namespace SmallDupSet {
+
+ using mongo::SmallDupSet;
+
+ class Upgrade {
+ public:
+ void run() {
+ SmallDupSet d;
+ for( int i = 0; i < 100; ++i ) {
+ ASSERT( !d.getsetdup( DiskLoc( 0, i ) ) );
+ for( int j = 0; j <= i; ++j ) {
+ ASSERT( d.getdup( DiskLoc( 0, j ) ) );
+ }
+ }
+ }
+ };
+
+ class UpgradeRead {
+ public:
+ void run() {
+ SmallDupSet d;
+ d.getsetdup( DiskLoc( 0, 0 ) );
+ for( int i = 0; i < 550; ++i ) {
+ ASSERT( d.getdup( DiskLoc( 0, 0 ) ) );
+ }
+ ASSERT( d.getsetdup( DiskLoc( 0, 0 ) ) );
+ }
+ };
+
+ class UpgradeWrite {
+ public:
+ void run() {
+ SmallDupSet d;
+ for( int i = 0; i < 550; ++i ) {
+ ASSERT( !d.getsetdup( DiskLoc( 0, i ) ) );
+ }
+ for( int i = 0; i < 550; ++i ) {
+ ASSERT( d.getsetdup( DiskLoc( 0, i ) ) );
+ }
+ }
+ };
+
+ } // namespace SmallDupSet
+
+ class DurationTimerStop {
+ public:
+ void run() {
+ DurationTimer t;
+ while( t.duration() == 0 );
+ ASSERT( t.duration() > 0 );
+ t.stop();
+ ASSERT( t.duration() > 0 );
+ ASSERT( t.duration() > 0 );
+ }
+ };
+
+ class Base {
+ public:
+ Base() {
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ string err;
+ userCreateNS( ns(), BSONObj(), err, false );
+ dropCollection( ns() );
+ }
+ ~Base() {
+ cc().curop()->reset();
+ }
+ protected:
+ DBDirectClient _cli;
+ static const char *ns() { return "unittests.QueryOptimizerTests"; }
+ void setQueryOptimizerCursor( const BSONObj &query, const BSONObj &order = BSONObj() ) {
+ setQueryOptimizerCursorWithoutAdvancing( query, order );
+ if ( ok() && !mayReturnCurrent() ) {
+ advance();
+ }
+ }
+ void setQueryOptimizerCursorWithoutAdvancing( const BSONObj &query, const BSONObj &order = BSONObj() ) {
+ _c = newQueryOptimizerCursor( ns(), query, order );
+ }
+ bool ok() const { return _c->ok(); }
+ /** Handles matching and deduping. */
+ bool advance() {
+ while( _c->advance() && !mayReturnCurrent() );
+ return ok();
+ }
+ int itcount() {
+ int ret = 0;
+ while( ok() ) {
+ ++ret;
+ advance();
+ }
+ return ret;
+ }
+ BSONObj current() const { return _c->current(); }
+ DiskLoc currLoc() const { return _c->currLoc(); }
+ void prepareToTouchEarlierIterate() { _c->prepareToTouchEarlierIterate(); }
+ void recoverFromTouchingEarlierIterate() { _c->recoverFromTouchingEarlierIterate(); }
+ bool mayReturnCurrent() {
+// return _c->currentMatches() && !_c->getsetdup( _c->currLoc() );
+ return ( !_c->matcher() || _c->matcher()->matchesCurrent( _c.get() ) ) && !_c->getsetdup( _c->currLoc() );
+ }
+ void prepareToYield() const { _c->prepareToYield(); }
+ void recoverFromYield() {
+ _c->recoverFromYield();
+ if ( ok() && !mayReturnCurrent() ) {
+ advance();
+ }
+ }
+ shared_ptr<Cursor> c() { return _c; }
+ long long nscanned() const { return _c->nscanned(); }
+ unsigned nNsCursors() const {
+ set<CursorId> nsCursors;
+ ClientCursor::find( ns(), nsCursors );
+ return nsCursors.size();
+ }
+ BSONObj cachedIndexForQuery( const BSONObj &query, const BSONObj &order = BSONObj() ) {
+ QueryPattern queryPattern = FieldRangeSet( ns(), query, true, true ).pattern( order );
+ NamespaceDetailsTransient &nsdt = NamespaceDetailsTransient::get( ns() );
+ return nsdt.cachedQueryPlanForPattern( queryPattern ).indexKey();
+ }
+ private:
+ shared_ptr<Cursor> _c;
+ };
+
+ /** No results for empty collection. */
+ class Empty : public Base {
+ public:
+ void run() {
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<QueryOptimizerCursor> c =
+ dynamic_pointer_cast<QueryOptimizerCursor>
+ ( newQueryOptimizerCursor( ns(), BSONObj() ) );
+ ASSERT( !c->ok() );
+ ASSERT_THROWS( c->_current(), AssertionException );
+ ASSERT_THROWS( c->current(), AssertionException );
+ ASSERT( c->currLoc().isNull() );
+ ASSERT( !c->advance() );
+ ASSERT_THROWS( c->currKey(), AssertionException );
+ ASSERT_THROWS( c->getsetdup( DiskLoc() ), AssertionException );
+ ASSERT_THROWS( c->isMultiKey(), AssertionException );
+ ASSERT_THROWS( c->matcher(), AssertionException );
+
+ ASSERT_THROWS( c->initialFieldRangeSet(), AssertionException );
+ ASSERT_THROWS( c->currentPlanScanAndOrderRequired(), AssertionException );
+ ASSERT_THROWS( c->keyFieldsOnly(), AssertionException );
+ ASSERT_THROWS( c->runningInitialInOrderPlan(), AssertionException );
+ ASSERT_THROWS( c->hasPossiblyExcludedPlans(), AssertionException );
+
+ // ok
+ c->initialCandidatePlans();
+ c->completePlanOfHybridSetScanAndOrderRequired();
+ c->clearIndexesForPatterns();
+ c->abortOutOfOrderPlans();
+ c->noteIterate( false, false, false );
+ c->explainQueryInfo();
+ }
+ };
+
+ /** Simple table scan. */
+ class Unindexed : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSONObj() );
+ ASSERT_EQUALS( 2, itcount() );
+ }
+ };
+
+ /** Basic test with two indexes and deduping requirement. */
+ class Basic : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT( ok() );
+ ASSERT_EQUALS( BSON( "_id" << 1 << "a" << 2 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 2 << "a" << 1 ), current() );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ };
+
+ class NoMatch : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 5 << LT << 4 << "a" << GT << 0 ) );
+ ASSERT( !ok() );
+ }
+ };
+
+ /** Order of results indicates that interleaving is occurring. */
+ class Interleaved : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 3 << "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 2 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT( ok() );
+ ASSERT_EQUALS( BSON( "_id" << 1 << "a" << 2 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 3 << "a" << 1 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 2 << "a" << 2 ), current() );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ };
+
+ /** Some values on each index do not match. */
+ class NotMatch : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 10 ) );
+ _cli.insert( ns(), BSON( "_id" << 10 << "a" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 11 << "a" << 12 ) );
+ _cli.insert( ns(), BSON( "_id" << 12 << "a" << 11 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 5 << "a" << GT << 5 ) );
+ ASSERT( ok() );
+ ASSERT_EQUALS( BSON( "_id" << 11 << "a" << 12 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 12 << "a" << 11 ), current() );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ };
+
+ /** After the first 101 matches for a plan, we stop interleaving the plans. */
+ class StopInterleaving : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 101; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+ for( int i = 101; i < 200; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << (301-i) ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << -1 << "a" << GT << -1 ) );
+ for( int i = 0; i < 200; ++i ) {
+ ASSERT( ok() );
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ advance();
+ }
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ };
+
+ /** Test correct deduping with the takeover cursor. */
+ class TakeoverWithDup : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 101; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+ _cli.insert( ns(), BSON( "_id" << 500 << "a" << BSON_ARRAY( 0 << 300 ) ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << -1 << "a" << GT << -1 ) );
+ ASSERT_EQUALS( 102, itcount() );
+ }
+ };
+
+ /** Test usage of matcher with takeover cursor. */
+ class TakeoverWithNonMatches : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 101; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+ _cli.insert( ns(), BSON( "_id" << 101 << "a" << 600 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << -1 << "a" << LT << 500 ) );
+ ASSERT_EQUALS( 101, itcount() );
+ }
+ };
+
+ /** Check deduping of dups within just the takeover cursor. */
+ class TakeoverWithTakeoverDup : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 101; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i*2 << "a" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << i*2+1 << "a" << 1 ) );
+ }
+ _cli.insert( ns(), BSON( "_id" << 202 << "a" << BSON_ARRAY( 2 << 3 ) ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << -1 << "a" << GT << 0) );
+ ASSERT_EQUALS( 102, itcount() );
+ }
+ };
+
+ /** Basic test with $or query. */
+ class BasicOr : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 0 ) << BSON( "a" << 1 ) ) ) );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 1 << "a" << 1 ), current() );
+ ASSERT( !advance() );
+ }
+ };
+
+ /** $or first clause empty. */
+ class OrFirstClauseEmpty : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << -1 ) << BSON( "a" << 1 ) ) ) );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 1 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 1 << "a" << 1 ), current() );
+ ASSERT( !advance() );
+ }
+ };
+
+ /** $or second clause empty. */
+ class OrSecondClauseEmpty : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 0 ) << BSON( "_id" << -1 ) << BSON( "a" << 1 ) ) ) );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 1 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 1 << "a" << 1 ), current() );
+ ASSERT( !advance() );
+ }
+ };
+
+ /** $or multiple clauses empty empty. */
+ class OrMultipleClausesEmpty : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 2 ) << BSON( "_id" << 4 ) << BSON( "_id" << 0 ) << BSON( "_id" << -1 ) << BSON( "_id" << 6 ) << BSON( "a" << 1 ) << BSON( "_id" << 9 ) ) ) );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 1 ), current() );
+ ASSERT( advance() );
+ ASSERT_EQUALS( BSON( "_id" << 1 << "a" << 1 ), current() );
+ ASSERT( !advance() );
+ }
+ };
+
+ /** Check that takeover occurs at proper match count with $or clauses */
+ class TakeoverCountOr : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 60; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 0 ) );
+ }
+ for( int i = 60; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 1 ) );
+ }
+ for( int i = 120; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << (200-i) ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "a" << 0 ) << BSON( "a" << 1 ) << BSON( "_id" << GTE << 120 << "a" << GT << 1 ) ) ) );
+ for( int i = 0; i < 120; ++i ) {
+ ASSERT( ok() );
+ advance();
+ }
+ // Expect to be scanning on _id index only.
+ for( int i = 120; i < 150; ++i ) {
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ advance();
+ }
+ ASSERT( !ok() );
+ }
+ };
+
+ /** Takeover just at end of clause. */
+ class TakeoverEndOfOrClause : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 102; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i ) );
+ }
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << LT << 101 ) << BSON( "_id" << 101 ) ) ) );
+ for( int i = 0; i < 102; ++i ) {
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ advance();
+ }
+ ASSERT( !ok() );
+ }
+ };
+
+ class TakeoverBeforeEndOfOrClause : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 101; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i ) );
+ }
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << LT << 100 ) << BSON( "_id" << 100 ) ) ) );
+ for( int i = 0; i < 101; ++i ) {
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ advance();
+ }
+ ASSERT( !ok() );
+ }
+ };
+
+ class TakeoverAfterEndOfOrClause : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 103; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i ) );
+ }
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << LT << 102 ) << BSON( "_id" << 102 ) ) ) );
+ for( int i = 0; i < 103; ++i ) {
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ advance();
+ }
+ ASSERT( !ok() );
+ }
+ };
+
+ /** Test matching and deduping done manually by cursor client. */
+ class ManualMatchingDeduping : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 10 ) );
+ _cli.insert( ns(), BSON( "_id" << 10 << "a" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 11 << "a" << 12 ) );
+ _cli.insert( ns(), BSON( "_id" << 12 << "a" << 11 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr< Cursor > c = newQueryOptimizerCursor( ns(), BSON( "_id" << GT << 5 << "a" << GT << 5 ) );
+ ASSERT( c->ok() );
+
+ // _id 10 {_id:1}
+ ASSERT_EQUALS( 10, c->current().getIntField( "_id" ) );
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->matcherPtr()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+
+ // _id 0 {a:1}
+ ASSERT_EQUALS( 0, c->current().getIntField( "_id" ) );
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+
+ // _id 0 {$natural:1}
+ ASSERT_EQUALS( 0, c->current().getIntField( "_id" ) );
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+
+ // _id 11 {_id:1}
+ ASSERT_EQUALS( BSON( "_id" << 11 << "a" << 12 ), c->current() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->matcherPtr()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ ASSERT( c->advance() );
+
+ // _id 12 {a:1}
+ ASSERT_EQUALS( BSON( "_id" << 12 << "a" << 11 ), c->current() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ ASSERT( c->advance() );
+
+ // _id 10 {$natural:1}
+ ASSERT_EQUALS( 10, c->current().getIntField( "_id" ) );
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+
+ // _id 12 {_id:1}
+ ASSERT_EQUALS( BSON( "_id" << 12 << "a" << 11 ), c->current() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ ASSERT( c->advance() );
+
+ // _id 11 {a:1}
+ ASSERT_EQUALS( BSON( "_id" << 11 << "a" << 12 ), c->current() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ ASSERT( c->advance() );
+
+ // _id 11 {$natural:1}
+ ASSERT_EQUALS( 11, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+
+ // {_id:1} scan is complete.
+ ASSERT( !c->advance() );
+ ASSERT( !c->ok() );
+
+ // Scan the results again - this time the winning plan has been
+ // recorded.
+ c = newQueryOptimizerCursor( ns(), BSON( "_id" << GT << 5 << "a" << GT << 5 ) );
+ ASSERT( c->ok() );
+
+ // _id 10 {_id:1}
+ ASSERT_EQUALS( 10, c->current().getIntField( "_id" ) );
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+
+ // _id 11 {_id:1}
+ ASSERT_EQUALS( BSON( "_id" << 11 << "a" << 12 ), c->current() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ ASSERT( c->advance() );
+
+ // _id 12 {_id:1}
+ ASSERT_EQUALS( BSON( "_id" << 12 << "a" << 11 ), c->current() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+
+ // {_id:1} scan complete
+ ASSERT( !c->advance() );
+ ASSERT( !c->ok() );
+ }
+ };
+
+ /** Curr key must be correct for currLoc for correct matching. */
+ class ManualMatchingUsingCurrKey : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << "a" ) );
+ _cli.insert( ns(), BSON( "_id" << "b" ) );
+ _cli.insert( ns(), BSON( "_id" << "ba" ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr< Cursor > c = newQueryOptimizerCursor( ns(), fromjson( "{_id:/a/}" ) );
+ ASSERT( c->ok() );
+ // "a"
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ ASSERT( c->advance() );
+ ASSERT( c->ok() );
+
+ // "b"
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+ ASSERT( c->ok() );
+
+ // "ba"
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ /** Test matching and deduping done manually by cursor client. */
+ class ManualMatchingDedupingTakeover : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 0 ) );
+ }
+ _cli.insert( ns(), BSON( "_id" << 300 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr< Cursor > c = newQueryOptimizerCursor( ns(), BSON( "$or" << BSON_ARRAY( BSON( "_id" << LT << 300 ) << BSON( "a" << 1 ) ) ) );
+ for( int i = 0; i < 151; ++i ) {
+ ASSERT( c->ok() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ c->advance();
+ }
+ ASSERT( !c->ok() );
+ }
+ };
+
+ /** Test single key matching bounds. */
+ class Singlekey : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "a" << "10" ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr< Cursor > c = newQueryOptimizerCursor( ns(), BSON( "a" << GT << 1 << LT << 5 ) );
+ // Two sided bounds work.
+ ASSERT( !c->ok() );
+ }
+ };
+
+ /** Test multi key matching bounds. */
+ class Multikey : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 1 << 10 ) ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "a" << GT << 5 << LT << 3 ) );
+ // Multi key bounds work.
+ ASSERT( ok() );
+ }
+ };
+
+ /** Add other plans when the recorded one is doing more poorly than expected. */
+ class AddOtherPlans : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 0 << "b" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 << "b" << 0 ) );
+ for( int i = 100; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 100 << "b" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "a" << 0 << "b" << 0 ) );
+
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 << "b" << 0 ), c->current() );
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+
+ ASSERT( c->advance() );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 << "b" << 0 ), c->current() );
+ ASSERT_EQUALS( BSON( "b" << 1 ), c->indexKeyPattern() );
+
+ ASSERT( c->advance() );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 << "b" << 0 ), c->current() );
+ // Unindexed plan
+ ASSERT_EQUALS( BSONObj(), c->indexKeyPattern() );
+ ASSERT( !c->advance() );
+
+ c = newQueryOptimizerCursor( ns(), BSON( "a" << 100 << "b" << 149 ) );
+ // Try {a:1}, which was successful previously.
+ for( int i = 0; i < 12; ++i ) {
+ ASSERT( 149 != c->current().getIntField( "b" ) );
+ ASSERT( c->advance() );
+ }
+ bool sawB1Index = false;
+ do {
+ if ( c->indexKeyPattern() == BSON( "b" << 1 ) ) {
+ ASSERT_EQUALS( 149, c->current().getIntField( "b" ) );
+ // We should try the {b:1} index and only see one result from it.
+ ASSERT( !sawB1Index );
+ sawB1Index = true;
+ }
+ } while ( c->advance() );
+ ASSERT( sawB1Index );
+ }
+ };
+
+ /** Add other plans when the recorded one is doing more poorly than expected, with deletion. */
+ class AddOtherPlansDelete : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 0 << "b" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 << "b" << 0 ) );
+ for( int i = 100; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 100 << "b" << i ) );
+ }
+ for( int i = 199; i >= 150; --i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 100 << "b" << 150 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "a" << 0 << "b" << 0 ) );
+
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 << "b" << 0 ), c->current() );
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+
+ ASSERT( c->advance() );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 << "b" << 0 ), c->current() );
+ ASSERT_EQUALS( BSON( "b" << 1 ), c->indexKeyPattern() );
+
+ ASSERT( c->advance() );
+ ASSERT_EQUALS( BSON( "_id" << 0 << "a" << 0 << "b" << 0 ), c->current() );
+ // Unindexed plan
+ ASSERT_EQUALS( BSONObj(), c->indexKeyPattern() );
+ ASSERT( !c->advance() );
+
+ c = newQueryOptimizerCursor( ns(), BSON( "a" << 100 << "b" << 150 ) );
+ // Try {a:1}, which was successful previously.
+ for( int i = 0; i < 12; ++i ) {
+ ASSERT( 150 != c->current().getIntField( "b" ) );
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+ ASSERT( c->advance() );
+ }
+ // Now try {b:1} plan.
+ ASSERT_EQUALS( BSON( "b" << 1 ), c->indexKeyPattern() );
+ ASSERT_EQUALS( 150, c->current().getIntField( "b" ) );
+ ASSERT( c->currentMatches() );
+ int id = c->current().getIntField( "_id" );
+ c->advance();
+ c->prepareToTouchEarlierIterate();
+ _cli.remove( ns(), BSON( "_id" << id ) );
+ c->recoverFromTouchingEarlierIterate();
+ int count = 1;
+ while( c->ok() ) {
+ if ( c->currentMatches() ) {
+ ++count;
+ int id = c->current().getIntField( "_id" );
+ c->advance();
+ c->prepareToTouchEarlierIterate();
+ _cli.remove( ns(), BSON( "_id" << id ) );
+ c->recoverFromTouchingEarlierIterate();
+ }
+ else {
+ c->advance();
+ }
+ }
+ ASSERT_EQUALS( 50, count );
+ }
+ };
+
+ /**
+ * Add other plans when the recorded one is doing more poorly than expected, with deletion before
+ * and after adding the additional plans.
+ */
+ class AddOtherPlansContinuousDelete : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << 0 << "b" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 << "b" << 0 ) );
+ for( int i = 100; i < 400; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i << "b" << ( 499 - i ) ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "a" << GTE << -1 << LTE << 0 << "b" << GTE << -1 << LTE << 0 ) );
+ while( c->advance() );
+ // {a:1} plan should be recorded now.
+
+ c = newQueryOptimizerCursor( ns(), BSON( "a" << GTE << 100 << LTE << 400 << "b" << GTE << 100 << LTE << 400 ) );
+ int count = 0;
+ while( c->ok() ) {
+ if ( c->currentMatches() ) {
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ ++count;
+ int id = c->current().getIntField( "_id" );
+ c->advance();
+ c->prepareToTouchEarlierIterate();
+ _cli.remove( ns(), BSON( "_id" << id ) );
+ c->recoverFromTouchingEarlierIterate();
+ } else {
+ c->advance();
+ }
+ }
+ ASSERT_EQUALS( 300, count );
+ ASSERT_EQUALS( 2U, _cli.count( ns(), BSONObj() ) );
+ }
+ };
+
+ /**
+ * When an index becomes multikey and ceases to be optimal for a query, attempt other plans
+ * quickly.
+ */
+ class AddOtherPlansWhenOptimalBecomesNonOptimal : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 << "b" << 1 ) );
+
+ {
+ // Create a btree cursor on an optimal a:1,b:1 plan.
+ Client::ReadContext ctx( ns() );
+ shared_ptr<Cursor> cursor = getCursor();
+ ASSERT_EQUALS( "BtreeCursor a_1_b_1", cursor->toString() );
+
+ // The optimal a:1,b:1 plan is recorded.
+ ASSERT_EQUALS( BSON( "a" << 1 << "b" << 1 ),
+ cachedIndexForQuery( BSON( "a" << 1 ), BSON( "b" << 1 ) ) );
+ }
+
+ // Make the a:1,b:1 index multikey.
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << BSON_ARRAY( 1 << 2 ) ) );
+
+ // Create a QueryOptimizerCursor, without an optimal plan.
+ Client::ReadContext ctx( ns() );
+ shared_ptr<Cursor> cursor = getCursor();
+ ASSERT_EQUALS( "QueryOptimizerCursor", cursor->toString() );
+ ASSERT_EQUALS( BSON( "a" << 1 << "b" << 1 ), cursor->indexKeyPattern() );
+ ASSERT( cursor->advance() );
+ // An alternative plan is quickly attempted.
+ ASSERT_EQUALS( BSONObj(), cursor->indexKeyPattern() );
+ }
+ private:
+ static shared_ptr<Cursor> getCursor() {
+ // The a:1,b:1 index will be optimal for this query and sort if single key, but if
+ // the index is multi key only one of the upper or lower constraints will be applied and
+ // the index will not be optimal.
+ BSONObj query = BSON( "a" << GTE << 1 << LTE << 1 );
+ BSONObj order = BSON( "b" << 1 );
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << query << "$orderby" << order ),
+ BSONObj() ) );
+ return NamespaceDetailsTransient::getCursor( ns(), query, order,
+ QueryPlanSelectionPolicy::any(), 0,
+ parsedQuery, false );
+ }
+ };
+
+ /** Check $or clause range elimination. */
+ class OrRangeElimination : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "$or" << BSON_ARRAY( BSON( "_id" << GT << 0 ) << BSON( "_id" << 1 ) ) ) );
+ ASSERT( c->ok() );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ /** Check $or match deduping - in takeover cursor. */
+ class OrDedup : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "$or" << BSON_ARRAY( BSON( "_id" << LT << 140 ) << BSON( "_id" << 145 ) << BSON( "a" << 145 ) ) ) );
+
+ while( c->current().getIntField( "_id" ) < 140 ) {
+ ASSERT( c->advance() );
+ }
+ // Match from second $or clause.
+ ASSERT_EQUALS( 145, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+ // Match from third $or clause.
+ ASSERT_EQUALS( 145, c->current().getIntField( "_id" ) );
+ // $or deduping is handled by the matcher.
+ ASSERT( !c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ /** Standard dups with a multikey cursor. */
+ class EarlyDups : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 0 << 1 << 200 ) ) );
+ for( int i = 2; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "a" << GT << -1 ) );
+ ASSERT_EQUALS( 149, itcount() );
+ }
+ };
+
+ /** Pop or clause in takeover cursor. */
+ class OrPopInTakeover : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i ) );
+ }
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "$or" << BSON_ARRAY( BSON( "_id" << LTE << 147 ) << BSON( "_id" << 148 ) << BSON( "_id" << 149 ) ) ) );
+ for( int i = 0; i < 150; ++i ) {
+ ASSERT( c->ok() );
+ ASSERT_EQUALS( i, c->current().getIntField( "_id" ) );
+ c->advance();
+ }
+ ASSERT( !c->ok() );
+ }
+ };
+
+ /** Or clause iteration abandoned once full collection scan is performed. */
+ class OrCollectionScanAbort : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 0 << "a" << BSON_ARRAY( 1 << 2 << 3 << 4 << 5 ) << "b" << 4 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << BSON_ARRAY( 6 << 7 << 8 << 9 << 10 ) << "b" << 4 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "$or" << BSON_ARRAY( BSON( "a" << LT << 6 << "b" << 4 ) << BSON( "a" << GTE << 6 << "b" << 4 ) ) ) );
+
+ ASSERT( c->ok() );
+
+ // _id 0 on {a:1}
+ ASSERT_EQUALS( 0, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ c->advance();
+
+ // _id 0 on {$natural:1}
+ ASSERT_EQUALS( 0, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ c->advance();
+
+ // _id 0 on {a:1}
+ ASSERT_EQUALS( 0, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ c->advance();
+
+ // _id 1 on {$natural:1}
+ ASSERT_EQUALS( 1, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ c->advance();
+
+ // _id 0 on {a:1}
+ ASSERT_EQUALS( 0, c->current().getIntField( "_id" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ c->advance();
+
+ // {$natural:1} finished
+ ASSERT( !c->ok() );
+ }
+ };
+
+ namespace Yield {
+
+ /** Yield cursor and delete current entry, then continue iteration. */
+ class NoOp : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ prepareToYield();
+ recoverFromYield();
+ }
+ }
+ };
+
+ /** Yield cursor and delete current entry. */
+ class Delete : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << 1 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( !ok() );
+ ASSERT( !advance() );
+ }
+ }
+ };
+
+ /** Yield cursor and delete current entry, then continue iteration. */
+ class DeleteContinue : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yield cursor and delete current entry, then continue iteration. */
+ class DeleteContinueFurther : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 3 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 3, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yield and update current. */
+ class Update : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "a" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "a" ) );
+ prepareToYield();
+ }
+
+ _cli.update( ns(), BSON( "a" << 1 ), BSON( "$set" << BSON( "a" << 3 ) ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "a" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yield and drop collection. */
+ class Drop : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.dropCollection( ns() );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ASSERT_THROWS( recoverFromYield(), MsgAssertionException );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yield and drop collection with $or query. */
+ class DropOr : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 1 ) << BSON( "_id" << 2 ) ) ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.dropCollection( ns() );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ASSERT_THROWS( recoverFromYield(), MsgAssertionException );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yield and remove document with $or query. */
+ class RemoveOr : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 1 ) << BSON( "_id" << 2 ) ) ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ /** Yield and overwrite current in capped collection. */
+ class CappedOverwrite : public Base {
+ public:
+ void run() {
+ _cli.createCollection( ns(), 1000, true );
+ _cli.insert( ns(), BSON( "x" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "x" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "x" ) );
+ prepareToYield();
+ }
+
+ int x = 2;
+ while( _cli.count( ns(), BSON( "x" << 1 ) ) > 0 ) {
+ _cli.insert( ns(), BSON( "x" << x++ ) );
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ASSERT_THROWS( recoverFromYield(), MsgAssertionException );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yield and drop unrelated index - see SERVER-2454. */
+ class DropIndex : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << 1 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.dropIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ASSERT_THROWS( recoverFromYield(), MsgAssertionException );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yielding with multiple plans active. */
+ class MultiplePlansNoOp : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yielding with advance and multiple plans active. */
+ class MultiplePlansAdvanceNoOp : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 3 << "a" << 3 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ advance();
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 3, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yielding with delete and multiple plans active. */
+ class MultiplePlansDelete : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 3 << "a" << 4 ) );
+ _cli.insert( ns(), BSON( "_id" << 4 << "a" << 3 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ advance();
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 2 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c()->recoverFromYield();
+ ASSERT( ok() );
+ // index {a:1} active during yield
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 3, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 4, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yielding with delete, multiple plans active, and $or clause. */
+ class MultiplePlansDeleteOr : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 1 << "a" << 2 ) << BSON( "_id" << 2 << "a" << 1 ) ) ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c()->recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yielding with delete, multiple plans active with advancement to the second, and $or clause. */
+ class MultiplePlansDeleteOrAdvance : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 2 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "$or" << BSON_ARRAY( BSON( "_id" << 1 << "a" << 2 ) << BSON( "_id" << 2 << "a" << 1 ) ) ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ c()->advance();
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c()->recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /** Yielding with multiple plans and capped overwrite. */
+ class MultiplePlansCappedOverwrite : public Base {
+ public:
+ void run() {
+ _cli.createCollection( ns(), 1000, true );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "_id" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ int i = 1;
+ while( _cli.count( ns(), BSON( "_id" << 1 ) ) > 0 ) {
+ ++i;
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ // {$natural:1} plan does not recover, {_id:1} plan does.
+ ASSERT( 1 < current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ /**
+ * Yielding with multiple plans and capped overwrite with unrecoverable cursor
+ * active at time of yield.
+ */
+ class MultiplePlansCappedOverwriteManual : public Base {
+ public:
+ void run() {
+ _cli.createCollection( ns(), 1000, true );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ shared_ptr<Cursor> c;
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c = newQueryOptimizerCursor( ns(), BSON( "a" << GT << 0 << "b" << GT << 0 ) );
+ ASSERT_EQUALS( 1, c->current().getIntField( "a" ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ c->advance();
+ ASSERT_EQUALS( 1, c->current().getIntField( "a" ) );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ c->prepareToYield();
+ }
+
+ int i = 1;
+ while( _cli.count( ns(), BSON( "a" << 1 ) ) > 0 ) {
+ ++i;
+ _cli.insert( ns(), BSON( "a" << i << "b" << i ) );
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c->recoverFromYield();
+ ASSERT( c->ok() );
+ // {$natural:1} plan does not recover, {_id:1} plan does.
+ ASSERT( 1 < c->current().getIntField( "a" ) );
+ }
+ }
+ };
+
+ /**
+ * Yielding with multiple plans and capped overwrite with unrecoverable cursor
+ * inctive at time of yield.
+ */
+ class MultiplePlansCappedOverwriteManual2 : public Base {
+ public:
+ void run() {
+ _cli.createCollection( ns(), 1000, true );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "_id" << 1 ) );
+
+ shared_ptr<Cursor> c;
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c = newQueryOptimizerCursor( ns(), BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT_EQUALS( 1, c->current().getIntField( "_id" ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ c->prepareToYield();
+ }
+
+ int n = 1;
+ while( _cli.count( ns(), BSON( "_id" << 1 ) ) > 0 ) {
+ ++n;
+ _cli.insert( ns(), BSON( "_id" << n << "a" << n ) );
+ }
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ c->recoverFromYield();
+ ASSERT( c->ok() );
+ // {$natural:1} plan does not recover, {_id:1} plan does.
+ ASSERT( 1 < c->current().getIntField( "_id" ) );
+ ASSERT( !c->getsetdup( c->currLoc() ) );
+ int i = c->current().getIntField( "_id" );
+ ASSERT( c->advance() );
+ ASSERT( c->getsetdup( c->currLoc() ) );
+ while( i < n ) {
+ ASSERT( c->advance() );
+ ++i;
+ ASSERT_EQUALS( i, c->current().getIntField( "_id" ) );
+ }
+ }
+ }
+ };
+
+ /** Yield with takeover cursor. */
+ class Takeover : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GTE << 0 << "a" << GTE << 0 ) );
+ for( int i = 0; i < 120; ++i ) {
+ ASSERT( advance() );
+ }
+ ASSERT( ok() );
+ ASSERT_EQUALS( 120, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 120 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 121, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 122, current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ /** Yield with BacicCursor takeover cursor. */
+ class TakeoverBasic : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << BSON_ARRAY( i << i+1 ) ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ auto_ptr<ClientCursor> cc;
+ auto_ptr<ClientCursor::YieldData> data( new ClientCursor::YieldData() );
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "b" << NE << 0 << "a" << GTE << 0 ) );
+ cc.reset( new ClientCursor( QueryOption_NoCursorTimeout, c(), ns() ) );
+ for( int i = 0; i < 120; ++i ) {
+ ASSERT( advance() );
+ }
+ ASSERT( ok() );
+ ASSERT_EQUALS( 120, current().getIntField( "_id" ) );
+ cc->prepareToYield( *data );
+ }
+ _cli.remove( ns(), BSON( "_id" << 120 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ASSERT( ClientCursor::recoverFromYield( *data ) );
+ ASSERT( ok() );
+ ASSERT_EQUALS( 121, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 122, current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ /** Yield with advance of inactive cursor. */
+ class InactiveCursorAdvance : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 10 - i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "a" << GT << 0 ) );
+ ASSERT( ok() );
+ ASSERT_EQUALS( 1, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 9, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 2, current().getIntField( "_id" ) );
+ prepareToYield();
+ }
+
+ _cli.remove( ns(), BSON( "_id" << 9 ) );
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ ASSERT_EQUALS( 8, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 3, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 7, current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ class TakeoverUpdateBase : public Base {
+ public:
+ TakeoverUpdateBase() :
+ _lastId( -1 ) {
+ populateWithKey( "a" );
+ populateWithKey( "b" );
+ _cli.ensureIndex( ns(), BSON( "c" << 1 ) );
+ }
+ virtual ~TakeoverUpdateBase() {}
+ void run() {
+ advanceToEndOfARangeAndUpdate();
+ advanceThroughBRange();
+ }
+ protected:
+ virtual BSONObj query() const = 0;
+ void advanceToEndOfARangeAndUpdate() {
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( query() );
+ for( int i = 0; i < 149; ++i ) {
+ advance();
+ }
+ ASSERT( ok() );
+ // The current iterate corresponds to the last 'a' document.
+ ASSERT_EQUALS( BSON( "_id" << 149 << "a" << 1 ), current() );
+ ASSERT_EQUALS( BSON( "a" << 1 ), c()->indexKeyPattern() );
+ prepareToYield();
+ }
+
+ _cli.update( ns(), BSON( "_id" << 149 ), BSON( "$set" << BSON( "a" << -1 ) ) );
+ }
+ void advanceThroughBRange() {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( ok() );
+ // The current iterate corresponds to the first 'b' document.
+ ASSERT_EQUALS( BSON( "_id" << 150 << "b" << 1 ), current() );
+ ASSERT_EQUALS( BSON( "b" << 1 ), c()->indexKeyPattern() );
+ // Eventually the last 'b' document is reached.
+ while( current() != BSON( "_id" << 299 << "b" << 1 ) ) {
+ ASSERT( advance() );
+ }
+ ASSERT( !advance() );
+ }
+ private:
+ void populateWithKey( const string &key ) {
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << nextId() << key << 1 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( key << 1 ) );
+ }
+ int nextId() {
+ return ++_lastId;
+ }
+ int _lastId;
+ };
+
+ /** An update causing an index key change advances the cursor past the last iterate. */
+ class TakeoverUpdateKeyAtEndOfIteration : public TakeoverUpdateBase {
+ public:
+ virtual BSONObj query() const { return BSON( "a" << 1 ); }
+ void run() {
+ advanceToEndOfARangeAndUpdate();
+
+ {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ recoverFromYield();
+ ASSERT( !ok() );
+ }
+ }
+ };
+
+ /**
+ * An update causing an index key change advances the cursor past the last iterate of a $or
+ * clause.
+ */
+ class TakeoverUpdateKeyAtEndOfClause : public TakeoverUpdateBase {
+ virtual BSONObj query() const { return fromjson( "{$or:[{a:1},{b:1}]}" ); }
+ };
+
+ /**
+ * An update causing an index key change advances the cursor past the last iterate of a $or
+ * clause, and also past an empty clause.
+ */
+ class TakeoverUpdateKeyPrecedingEmptyClause : public TakeoverUpdateBase {
+ virtual BSONObj query() const { return fromjson( "{$or:[{a:1},{c:1},{b:1}]}" ); }
+ };
+
+ } // namespace Yield
+
+ class OrderId : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i ) );
+ }
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSONObj(), BSON( "_id" << 1 ) );
+
+ for( int i = 0; i < 10; ++i, advance() ) {
+ ASSERT( ok() );
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ class OrderMultiIndex : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << 1 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GTE << 0 << "a" << GTE << 0 ), BSON( "_id" << 1 ) );
+
+ for( int i = 0; i < 10; ++i, advance() ) {
+ ASSERT( ok() );
+ ASSERT_EQUALS( i, current().getIntField( "_id" ) );
+ }
+ }
+ };
+
+ class OrderReject : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i % 5 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "a" << GTE << 3 ), BSON( "_id" << 1 ) );
+
+ ASSERT( ok() );
+ ASSERT_EQUALS( 3, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 4, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 8, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 9, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ }
+ };
+
+ class OrderNatural : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 5 ) );
+ _cli.insert( ns(), BSON( "_id" << 4 ) );
+ _cli.insert( ns(), BSON( "_id" << 6 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 ), BSON( "$natural" << 1 ) );
+
+ ASSERT( ok() );
+ ASSERT_EQUALS( 5, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 4, current().getIntField( "_id" ) );
+ ASSERT( advance() );
+ ASSERT_EQUALS( 6, current().getIntField( "_id" ) );
+ ASSERT( !advance() );
+ }
+ };
+
+ class OrderUnindexed : public Base {
+ public:
+ void run() {
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ ASSERT( !newQueryOptimizerCursor( ns(), BSONObj(), BSON( "a" << 1 ) ).get() );
+ }
+ };
+
+ class RecordedOrderInvalid : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 2 << "b" << 2 ) );
+ _cli.insert( ns(), BSON( "a" << 3 << "b" << 3 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ // Plan {a:1} will be chosen and recorded.
+ ASSERT( _cli.query( ns(), QUERY( "a" << 2 ).sort( "b" ) )->more() );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "a" << 2 ),
+ BSON( "b" << 1 ) );
+ // Check that we are scanning {b:1} not {a:1}, since {a:1} is not properly ordered.
+ for( int i = 0; i < 3; ++i ) {
+ ASSERT( c->ok() );
+ c->advance();
+ }
+ ASSERT( !c->ok() );
+ }
+ };
+
+ class KillOp : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "b" << 2 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Client::ReadContext ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+ ASSERT( ok() );
+ cc().curop()->kill();
+ // First advance() call throws, subsequent calls just fail.
+ ASSERT_THROWS( advance(), MsgAssertionException );
+ ASSERT( !advance() );
+ }
+ };
+
+ class KillOpFirstClause : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "b" << 2 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Client::ReadContext ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "$or" << BSON_ARRAY( BSON( "_id" << GT << 0 ) << BSON( "b" << GT << 0 ) ) ) );
+ ASSERT( c->ok() );
+ cc().curop()->kill();
+ // First advance() call throws, subsequent calls just fail.
+ ASSERT_THROWS( c->advance(), MsgAssertionException );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ class Nscanned : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "a" << i ) );
+ }
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "_id" << GTE << 0 << "a" << GTE << 0 ) );
+ ASSERT( c->ok() );
+ ASSERT_EQUALS( 2, c->nscanned() );
+ c->advance();
+ ASSERT( c->ok() );
+ ASSERT_EQUALS( 2, c->nscanned() );
+ c->advance();
+ for( int i = 3; i < 222; ++i ) {
+ ASSERT( c->ok() );
+ c->advance();
+ }
+ ASSERT( !c->ok() );
+ }
+ };
+
+ namespace TouchEarlierIterate {
+
+ /* Test 'touching earlier iterate' without doc modifications. */
+ class Basic : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "b" << 2 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Client::ReadContext ctx( ns() );
+ shared_ptr<Cursor> c = newQueryOptimizerCursor( ns(), BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+
+ ASSERT( c->ok() );
+ while( c->ok() ) {
+ DiskLoc loc = c->currLoc();
+ BSONObj obj = c->current();
+ c->prepareToTouchEarlierIterate();
+ c->recoverFromTouchingEarlierIterate();
+ ASSERT( loc == c->currLoc() );
+ ASSERT_EQUALS( obj, c->current() );
+ c->advance();
+ }
+ }
+ };
+
+ /* Test 'touching earlier iterate' with doc modifications. */
+ class Delete : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 << "b" << 2 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ DiskLoc firstLoc;
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+ ASSERT( ok() );
+ firstLoc = currLoc();
+ ASSERT( c()->advance() );
+ prepareToTouchEarlierIterate();
+
+ _cli.remove( ns(), BSON( "_id" << 1 ), true );
+
+ recoverFromTouchingEarlierIterate();
+ ASSERT( ok() );
+ while( ok() ) {
+ ASSERT( firstLoc != currLoc() );
+ c()->advance();
+ }
+ }
+ };
+
+ /* Test 'touch earlier iterate' with several doc modifications. */
+ class DeleteMultiple : public Base {
+ public:
+ void run() {
+ for( int i = 1; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "b" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ set<DiskLoc> deleted;
+ int id = 0;
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+ while( 1 ) {
+ if ( !ok() ) {
+ break;
+ }
+ ASSERT( deleted.count( currLoc() ) == 0 );
+ id = current()["_id"].Int();
+ deleted.insert( currLoc() );
+ c()->advance();
+ prepareToTouchEarlierIterate();
+
+ _cli.remove( ns(), BSON( "_id" << id ), true );
+
+ recoverFromTouchingEarlierIterate();
+ }
+ ASSERT_EQUALS( 9U, deleted.size() );
+ }
+ };
+
+ /* Test 'touch earlier iterate' after an earlier yield. */
+ class DeleteAfterYield : public Base {
+ public:
+ void run() {
+ for( int i = 0; i < 3; ++i ) {
+ _cli.insert( ns(), BSON( "b" << i ) );
+ }
+
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSONObj() );
+ ASSERT( ok() );
+ ASSERT_EQUALS( 0, c()->current()[ "b" ].Int() );
+
+ // Record the position of document b:0 in the cursor's component ClientCursor.
+ c()->prepareToYield();
+ c()->recoverFromYield();
+ ASSERT( ok() );
+
+ // Advance the cursor past document b:0.
+ ASSERT( c()->advance() );
+ ASSERT_EQUALS( 1, current()[ "b" ].Int() );
+
+ // Remove document b:0.
+ c()->prepareToTouchEarlierIterate();
+ // A warning message will be logged for the component ClientCursor if it is not
+ // configured with 'doing deletes'.
+ _cli.remove( ns(), BSON( "b" << 0 ), true );
+ c()->recoverFromTouchingEarlierIterate();
+
+ // Check that the cursor recovers properly after b:0 is deleted.
+ ASSERT( ok() );
+ ASSERT_EQUALS( 1, current()[ "b" ].Int() );
+ ASSERT( c()->advance() );
+ ASSERT_EQUALS( 2, current()[ "b" ].Int() );
+ ASSERT( !c()->advance() );
+ }
+ };
+
+ /* Test 'touch earlier iterate' with takeover. */
+ class Takeover : public Base {
+ public:
+ void run() {
+ for( int i = 1; i < 600; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "b" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Client::ReadContext ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+
+ ASSERT( ok() );
+ int count = 1;
+ while( ok() ) {
+ DiskLoc loc = currLoc();
+ BSONObj obj = current();
+ prepareToTouchEarlierIterate();
+ recoverFromTouchingEarlierIterate();
+ ASSERT( loc == currLoc() );
+ ASSERT_EQUALS( obj, current() );
+ count += mayReturnCurrent();
+ c()->advance();
+ }
+ ASSERT_EQUALS( 599, count );
+ }
+ };
+
+ /* Test 'touch earlier iterate' with takeover and deletes. */
+ class TakeoverDeleteMultiple : public Base {
+ public:
+ void run() {
+ for( int i = 1; i < 600; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "b" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ set<DiskLoc> deleted;
+ int id = 0;
+
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursorWithoutAdvancing( BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+ while( 1 ) {
+ if ( !ok() ) {
+ break;
+ }
+ ASSERT( deleted.count( currLoc() ) == 0 );
+ id = current()["_id"].Int();
+ ASSERT( c()->currentMatches() );
+ ASSERT( !c()->getsetdup( currLoc() ) );
+ deleted.insert( currLoc() );
+ c()->advance();
+ prepareToTouchEarlierIterate();
+
+ _cli.remove( ns(), BSON( "_id" << id ), true );
+
+ recoverFromTouchingEarlierIterate();
+ }
+ ASSERT_EQUALS( 599U, deleted.size() );
+ }
+ };
+
+ /* Test 'touch earlier iterate' with unindexed cursor takeover and deletes. */
+ class UnindexedTakeoverDeleteMultiple : public Base {
+ public:
+ void run() {
+ for( int i = 1; i < 600; ++i ) {
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( i << i+1 ) << "b" << BSON_ARRAY( i << i+1 ) << "_id" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ set<DiskLoc> deleted;
+ int id = 0;
+
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursorWithoutAdvancing( BSON( "a" << GT << 0 << "b" << GT << 0 ) );
+ while( 1 ) {
+ if ( !ok() ) {
+ break;
+ }
+ ASSERT( deleted.count( currLoc() ) == 0 );
+ id = current()["_id"].Int();
+ ASSERT( c()->currentMatches() );
+ ASSERT( !c()->getsetdup( currLoc() ) );
+ deleted.insert( currLoc() );
+ // Advance past the document before deleting it.
+ DiskLoc loc = currLoc();
+ while( ok() && loc == currLoc() ) {
+ c()->advance();
+ }
+ prepareToTouchEarlierIterate();
+
+ _cli.remove( ns(), BSON( "_id" << id ), true );
+
+ recoverFromTouchingEarlierIterate();
+ }
+ ASSERT_EQUALS( 599U, deleted.size() );
+ }
+ };
+
+ /* Test 'touch earlier iterate' with takeover and deletes, with multiple advances in a row. */
+ class TakeoverDeleteMultipleMultiAdvance : public Base {
+ public:
+ void run() {
+ for( int i = 1; i < 600; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "b" << i ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ set<DiskLoc> deleted;
+ int id = 0;
+
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "_id" << GT << 0 << "b" << GT << 0 ) );
+ while( 1 ) {
+ if ( !ok() ) {
+ break;
+ }
+ ASSERT( deleted.count( currLoc() ) == 0 );
+ id = current()["_id"].Int();
+ ASSERT( c()->currentMatches() );
+ deleted.insert( currLoc() );
+ advance();
+ prepareToTouchEarlierIterate();
+
+ _cli.remove( ns(), BSON( "_id" << id ), true );
+
+ recoverFromTouchingEarlierIterate();
+ }
+ ASSERT_EQUALS( 599U, deleted.size() );
+ }
+ };
+
+ } // namespace TouchEarlierIterate
+
+ /* Test yield recovery failure of component capped cursor. */
+ class InitialCappedWrapYieldRecoveryFailure : public Base {
+ public:
+ void run() {
+ _cli.createCollection( ns(), 1000, true );
+ _cli.insert( ns(), BSON( "_id" << 1 << "x" << 1 ) );
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "x" << GT << 0 ) );
+ ASSERT_EQUALS( 1, current().getIntField( "x" ) );
+
+ ClientCursor::Holder p( new ClientCursor( QueryOption_NoCursorTimeout, c(), ns() ) );
+ ClientCursor::YieldData yieldData;
+ p->prepareToYield( yieldData );
+
+ int x = 2;
+ while( _cli.count( ns(), BSON( "x" << 1 ) ) > 0 ) {
+ _cli.insert( ns(), BSON( "_id" << x << "x" << x ) );
+ ++x;
+ }
+
+ // TODO - Might be preferable to return false rather than assert here.
+ ASSERT_THROWS( ClientCursor::recoverFromYield( yieldData ), AssertionException );
+ }
+ };
+
+ /* Test yield recovery failure of takeover capped cursor. */
+ class TakeoverCappedWrapYieldRecoveryFailure : public Base {
+ public:
+ void run() {
+ _cli.createCollection( ns(), 10000, true );
+ for( int i = 0; i < 300; ++i ) {
+ _cli.insert( ns(), BSON( "_id" << i << "x" << i ) );
+ }
+
+ ClientCursor::Holder p;
+ ClientCursor::YieldData yieldData;
+ {
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ setQueryOptimizerCursor( BSON( "x" << GTE << 0 ) );
+ for( int i = 0; i < 299; ++i ) {
+ advance();
+ }
+ ASSERT_EQUALS( 299, current().getIntField( "x" ) );
+
+ p.reset( new ClientCursor( QueryOption_NoCursorTimeout, c(), ns() ) );
+ p->prepareToYield( yieldData );
+ }
+
+ int i = 300;
+ while( _cli.count( ns(), BSON( "x" << 299 ) ) > 0 ) {
+ _cli.insert( ns(), BSON( "_id" << i << "x" << i ) );
+ ++i;
+ }
+
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ ASSERT( !ClientCursor::recoverFromYield( yieldData ) );
+ }
+ };
+
+ namespace ClientCursor {
+
+ using mongo::ClientCursor;
+
+ /** Test that a ClientCursor holding a QueryOptimizerCursor may be safely invalidated. */
+ class Invalidate : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ClientCursor::Holder p
+ ( new ClientCursor
+ ( QueryOption_NoCursorTimeout,
+ NamespaceDetailsTransient::getCursor
+ ( ns(), BSON( "a" << GTE << 0 << "b" << GTE << 0 ) ),
+ ns() ) );
+ ClientCursor::invalidate( ns() );
+ ASSERT_EQUALS( 0U, nNsCursors() );
+ }
+ };
+
+ /** Test that a ClientCursor holding a QueryOptimizerCursor may be safely timed out. */
+ class TimeoutClientCursorHolder : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ ClientCursor::Holder p
+ ( new ClientCursor
+ ( 0,
+ NamespaceDetailsTransient::getCursor
+ ( ns(), BSON( "a" << GTE << 0 << "b" << GTE << 0 ) ),
+ ns() ) );
+
+ // Construct component client cursors.
+ ClientCursor::YieldData yieldData;
+ p->prepareToYield( yieldData );
+ ASSERT( nNsCursors() > 1 );
+
+ ClientCursor::invalidate( ns() );
+ ASSERT_EQUALS( 0U, nNsCursors() );
+ }
+ };
+
+ /** Test that a ClientCursor holding a QueryOptimizerCursor may be safely timed out. */
+ class Timeout : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ClientCursor::Holder p
+ ( new ClientCursor
+ ( 0,
+ NamespaceDetailsTransient::getCursor
+ ( ns(), BSON( "a" << GTE << 0 << "b" << GTE << 0 ) ),
+ ns() ) );
+
+ // Construct component client cursors.
+ ClientCursor::YieldData yieldData;
+ p->prepareToYield( yieldData );
+ ASSERT( nNsCursors() > 1 );
+
+ ClientCursor::idleTimeReport( 600001 );
+ ASSERT_EQUALS( 0U, nNsCursors() );
+ }
+ };
+
+ /**
+ * Test that a ClientCursor properly recovers a QueryOptimizerCursor after a btree
+ * modification in preparation for a pre delete advance.
+ */
+ class AboutToDeleteRecoverFromYield : public Base {
+ public:
+ void run() {
+ // Create a sparse index, so we can easily remove entries from it with an update.
+ _cli.insert( Namespace( ns() ).getSisterNS( "system.indexes" ).c_str(),
+ BSON( "ns" << ns() << "key" << BSON( "a" << 1 ) << "name" << "idx" <<
+ "sparse" << true ) );
+
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << i << "b" << 0 ) );
+ }
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ClientCursor::Holder p
+ ( new ClientCursor
+ ( QueryOption_NoCursorTimeout,
+ NamespaceDetailsTransient::getCursor
+ ( ns(), BSON( "a" << GTE << 0 << "b" << 0 ) ),
+ ns() ) );
+
+ // Iterate until after MultiCursor takes over.
+ int readTo = 110;
+ while( p->current()[ "a" ].number() < readTo ) {
+ p->advance();
+ }
+
+ // Check that the btree plan was picked.
+ ASSERT_EQUALS( BSON( "a" << 1 ), p->indexKeyPattern() );
+
+ // Yield the cursor.
+ ClientCursor::YieldData yieldData;
+ ASSERT( p->prepareToYield( yieldData ) );
+
+ // Remove keys from the a:1 index, invalidating the cursor's position.
+ _cli.update( ns(), BSON( "a" << LT << 100 ), BSON( "$unset" << BSON( "a" << 1 ) ),
+ false, true );
+
+ // Delete the cursor's current document. If the cursor's position is recovered
+ // improperly in preparation for deleting the document, this will cause an
+ // assertion.
+ _cli.remove( ns(), BSON( "a" << readTo ) );
+
+ // Check that the document was deleted.
+ ASSERT_EQUALS( BSONObj(), _cli.findOne( ns(), BSON( "a" << readTo ) ) );
+
+ // Recover the cursor.
+ ASSERT( p->recoverFromYield( yieldData ) );
+
+ // Check that the remaining documents are iterated as expected.
+ for( int i = 111; i < 150; ++i ) {
+ ASSERT_EQUALS( i, p->current()[ "a" ].number() );
+ p->advance();
+ }
+ ASSERT( !p->ok() );
+ }
+ };
+
+ /**
+ * Test that a ClientCursor properly prepares a QueryOptimizerCursor to yield after a pre
+ * delete advance.
+ */
+ class AboutToDeletePrepareToYield : public Base {
+ public:
+ void run() {
+ // Create two indexes for serial $or clause traversal.
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ for( int i = 0; i < 110; ++i ) {
+ _cli.insert( ns(), BSON( "a" << i ) );
+ }
+ _cli.insert( ns(), BSON( "b" << 1 ) );
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+ ClientCursor::Holder p
+ ( new ClientCursor
+ ( QueryOption_NoCursorTimeout,
+ NamespaceDetailsTransient::getCursor
+ ( ns(), OR( BSON( "a" << GTE << 0 ), BSON( "b" << 1 ) ) ),
+ ns() ) );
+
+ // Iterate until after MultiCursor takes over.
+ int readTo = 109;
+ while( p->current()[ "a" ].number() < readTo ) {
+ p->advance();
+ }
+
+ // Check the key pattern.
+ ASSERT_EQUALS( BSON( "a" << 1 ), p->indexKeyPattern() );
+
+ // Yield the cursor.
+ ClientCursor::YieldData yieldData;
+ ASSERT( p->prepareToYield( yieldData ) );
+
+ // Delete the cursor's current document. The cursor should advance to the b:1
+ // index.
+ _cli.remove( ns(), BSON( "a" << readTo ) );
+
+ // Check that the document was deleted.
+ ASSERT_EQUALS( BSONObj(), _cli.findOne( ns(), BSON( "a" << readTo ) ) );
+
+ // Recover the cursor. If the cursor was not properly prepared for yielding
+ // after the pre deletion advance, this will assert.
+ ASSERT( p->recoverFromYield( yieldData ) );
+
+ // Check that the remaining documents are as expected.
+ ASSERT_EQUALS( 1, p->current()[ "b" ].number() );
+ ASSERT( !p->advance() );
+ }
+ };
+
+ /** The collection of a QueryOptimizerCursor stored in a ClientCursor is dropped. */
+ class Drop : public Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 2 ) );
+
+ ClientCursor::YieldData yieldData;
+ {
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ ClientCursor::Holder p
+ ( new ClientCursor
+ ( QueryOption_NoCursorTimeout,
+ NamespaceDetailsTransient::getCursor
+ ( ns(), BSON( "_id" << GT << 0 << "z" << 0 ) ),
+ ns() ) );
+
+ ASSERT_EQUALS( "QueryOptimizerCursor", p->c()->toString() );
+ ASSERT_EQUALS( 1, p->c()->current().getIntField( "_id" ) );
+ ASSERT( p->prepareToYield( yieldData ) );
+ }
+
+ // No assertion is expected when the collection is dropped and the cursor cannot be
+ // recovered.
+
+ _cli.dropCollection( ns() );
+
+ {
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ ASSERT( !ClientCursor::recoverFromYield( yieldData ) );
+ }
+ }
+ };
+
+ } // namespace ClientCursor
+
+ class AllowOutOfOrderPlan : public Base {
+ public:
+ void run() {
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c =
+ newQueryOptimizerCursor( ns(), BSONObj(), BSON( "a" << 1 ),
+ QueryPlanSelectionPolicy::any(), false );
+ ASSERT( c );
+ }
+ };
+
+ class NoTakeoverByOutOfOrderPlan : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ // Add enough early matches that the {$natural:1} plan would be chosen if it did not
+ // require scan and order.
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 << "b" << 1 ) );
+ }
+ // Add non matches early on the {a:1} plan.
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 5 ) );
+ }
+ // Add enough matches outside the {a:1} index range that the {$natural:1} scan will not
+ // complete before the {a:1} plan records 101 matches and is selected for takeover.
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 3 << "b" << 10 ) );
+ }
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c =
+ newQueryOptimizerCursor( ns(), BSON( "a" << LT << 3 << "b" << 1 ), BSON( "a" << 1 ),
+ QueryPlanSelectionPolicy::any(), false );
+ ASSERT( c );
+ BSONObj idxKey;
+ while( c->ok() ) {
+ idxKey = c->indexKeyPattern();
+ c->advance();
+ }
+ // Check that the ordered plan {a:1} took over, despite the unordered plan {$natural:1}
+ // seeing > 101 matches.
+ ASSERT_EQUALS( BSON( "a" << 1 ), idxKey );
+ }
+ };
+
+ /** If no in order plans are possible, an out of order plan may take over. */
+ class OutOfOrderOnlyTakeover : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 300; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c =
+ newQueryOptimizerCursor( ns(), BSON( "a" << 1 ), BSON( "b" << 1 ),
+ QueryPlanSelectionPolicy::any(), false );
+ ASSERT( c );
+ while( c->advance() );
+ // Check that one of the plans took over, and we didn't scan both plans until the a:1
+ // index completed (which would yield an nscanned near 600).
+ ASSERT( c->nscanned() < 500 );
+ }
+ };
+
+ class CoveredIndex : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 10 ) );
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0, BSONObj(), BSON( "_id" << 0 << "a" << 1 ) ) );
+ shared_ptr<QueryOptimizerCursor> c =
+ dynamic_pointer_cast<QueryOptimizerCursor>
+ ( newQueryOptimizerCursor( ns(), BSON( "a" << GTE << 0 << "b" << GTE << 0 ),
+ BSON( "a" << 1 ), QueryPlanSelectionPolicy::any(), false,
+ parsedQuery ) );
+ bool foundA = false;
+ bool foundB = false;
+ while( c->ok() ) {
+ if ( c->indexKeyPattern() == BSON( "a" << 1 ) ) {
+ foundA = true;
+ ASSERT( c->keyFieldsOnly() );
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->keyFieldsOnly()->hydrate( c->currKey() ) );
+ }
+ if ( c->indexKeyPattern() == BSON( "b" << 1 ) ) {
+ foundB = true;
+ ASSERT( !c->keyFieldsOnly() );
+ }
+ c->advance();
+ }
+ ASSERT( foundA );
+ ASSERT( foundB );
+ }
+ };
+
+ class CoveredIndexTakeover : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ }
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0, BSONObj(), BSON( "_id" << 0 << "a" << 1 ) ) );
+ shared_ptr<QueryOptimizerCursor> c =
+ dynamic_pointer_cast<QueryOptimizerCursor>
+ ( newQueryOptimizerCursor( ns(), fromjson( "{$or:[{a:1},{b:1},{a:2}]}" ), BSONObj(),
+ QueryPlanSelectionPolicy::any(), false, parsedQuery ) );
+ bool foundA = false;
+ bool foundB = false;
+ while( c->ok() ) {
+ if ( c->indexKeyPattern() == BSON( "a" << 1 ) ) {
+ foundA = true;
+ ASSERT( c->keyFieldsOnly() );
+ ASSERT( BSON( "a" << 1 ) == c->keyFieldsOnly()->hydrate( c->currKey() ) ||
+ BSON( "a" << 2 ) == c->keyFieldsOnly()->hydrate( c->currKey() ) );
+ }
+ if ( c->indexKeyPattern() == BSON( "b" << 1 ) ) {
+ foundB = true;
+ ASSERT( !c->keyFieldsOnly() );
+ }
+ c->advance();
+ }
+ ASSERT( foundA );
+ ASSERT( foundB );
+ }
+ };
+
+ class PlanChecking : public Base {
+ public:
+ virtual ~PlanChecking() {}
+ protected:
+ void nPlans( int n, const BSONObj &query, const BSONObj &order ) {
+ auto_ptr< FieldRangeSetPair > frsp( new FieldRangeSetPair( ns(), query ) );
+ auto_ptr< FieldRangeSetPair > frspOrig( new FieldRangeSetPair( *frsp ) );
+ scoped_ptr<QueryPlanSet> s( QueryPlanSet::make( ns(), frsp, frspOrig, query, order,
+ shared_ptr<const ParsedQuery>(),
+ BSONObj(), QueryPlanGenerator::Use,
+ BSONObj(), BSONObj(), true ) );
+ ASSERT_EQUALS( n, s->nPlans() );
+ }
+ static shared_ptr<QueryOptimizerCursor> getCursor( const BSONObj &query,
+ const BSONObj &order ) {
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << query << "$orderby" << order ),
+ BSONObj() ) );
+ shared_ptr<Cursor> cursor =
+ NamespaceDetailsTransient::getCursor( ns(), query, order,
+ QueryPlanSelectionPolicy::any(), 0, parsedQuery,
+ false );
+ shared_ptr<QueryOptimizerCursor> ret =
+ dynamic_pointer_cast<QueryOptimizerCursor>( cursor );
+ ASSERT( ret );
+ return ret;
+ }
+ void runQuery( const BSONObj &query, const BSONObj &order ) {
+ shared_ptr<QueryOptimizerCursor> cursor = getCursor( query, order );
+ while( cursor->advance() );
+ }
+ };
+
+ class SaveGoodIndex : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+
+ // No best plan - all must be tried.
+ nPlans( 3 );
+ runQuery();
+ // Best plan selected by query.
+ nPlans( 1 );
+ nPlans( 1 );
+ Helpers::ensureIndex( ns(), BSON( "c" << 1 ), false, "c_1" );
+ // Best plan cleared when new index added.
+ nPlans( 3 );
+ runQuery();
+ // Best plan selected by query.
+ nPlans( 1 );
+
+ {
+ DBDirectClient client;
+ for( int i = 0; i < 334; ++i ) {
+ client.insert( ns(), BSON( "i" << i ) );
+ client.update( ns(), QUERY( "i" << i ), BSON( "i" << i + 1 ) );
+ client.remove( ns(), BSON( "i" << i + 1 ) );
+ }
+ }
+ // Best plan cleared by ~1000 writes.
+ nPlans( 3 );
+
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << BSON( "a" << 4 ) <<
+ "$hint" << BSON( "$natural" << 1 ) ),
+ BSON( "b" << 1 ) ) );
+ shared_ptr<Cursor> cursor =
+ NamespaceDetailsTransient::getCursor( ns(), BSON( "a" << 4 ), BSONObj(),
+ QueryPlanSelectionPolicy::any(), 0, parsedQuery,
+ false );
+ while( cursor->advance() );
+ // No plan recorded when a hint is used.
+ nPlans( 3 );
+
+ shared_ptr<ParsedQuery> parsedQuery2
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << BSON( "a" << 4 ) <<
+ "$orderby" << BSON( "b" << 1 << "c" << 1 ) ),
+ BSONObj() ) );
+ shared_ptr<Cursor> cursor2 =
+ NamespaceDetailsTransient::getCursor( ns(), BSON( "a" << 4 ),
+ BSON( "b" << 1 << "c" << 1 ),
+ QueryPlanSelectionPolicy::any(), 0,
+ parsedQuery2, false );
+ while( cursor2->advance() );
+ // Plan recorded was for a different query pattern (different sort spec).
+ nPlans( 3 );
+
+ // Best plan still selected by query after all these other tests.
+ runQuery();
+ nPlans( 1 );
+ }
+ private:
+ void nPlans( int n ) {
+ return PlanChecking::nPlans( n, BSON( "a" << 4 ), BSON( "b" << 1 ) );
+ }
+ void runQuery() {
+ return PlanChecking::runQuery( BSON( "a" << 4 ), BSON( "b" << 1 ) );
+ }
+ };
+
+ class PossiblePlans : public PlanChecking {
+ protected:
+ void checkCursor( bool mayRunInOrderPlan, bool mayRunOutOfOrderPlan,
+ bool runningInitialInOrderPlan, bool possiblyExcludedPlans ) {
+ CandidatePlanCharacter plans = _cursor->initialCandidatePlans();
+ ASSERT_EQUALS( mayRunInOrderPlan, plans.mayRunInOrderPlan() );
+ ASSERT_EQUALS( mayRunOutOfOrderPlan, plans.mayRunOutOfOrderPlan() );
+ ASSERT_EQUALS( runningInitialInOrderPlan, _cursor->runningInitialInOrderPlan() );
+ ASSERT_EQUALS( possiblyExcludedPlans, _cursor->hasPossiblyExcludedPlans() );
+ }
+ void setCursor( const BSONObj &query, const BSONObj &order ) {
+ _cursor = PlanChecking::getCursor( query, order );
+ }
+ void runCursor( bool completePlanOfHybridSetScanAndOrderRequired = false ) {
+ while( _cursor->ok() ) {
+ checkIterate( _cursor );
+ _cursor->advance();
+ }
+ ASSERT_EQUALS( completePlanOfHybridSetScanAndOrderRequired,
+ _cursor->completePlanOfHybridSetScanAndOrderRequired() );
+ }
+ void runCursorUntilTakeover() {
+ // This is a bit of a hack, relying on initialFieldRangeSet() being nonzero before
+ // takeover and zero after takeover.
+ while( _cursor->ok() && _cursor->initialFieldRangeSet() ) {
+ checkIterate( _cursor );
+ _cursor->advance();
+ }
+ }
+ void checkTakeoverCursor( bool currentPlanScanAndOrderRequired ) {
+ ASSERT( !_cursor->initialFieldRangeSet() );
+ ASSERT_EQUALS( currentPlanScanAndOrderRequired,
+ _cursor->currentPlanScanAndOrderRequired() );
+ ASSERT( !_cursor->completePlanOfHybridSetScanAndOrderRequired() );
+ ASSERT( !_cursor->runningInitialInOrderPlan() );
+ ASSERT( !_cursor->hasPossiblyExcludedPlans() );
+ }
+ virtual void checkIterate( const shared_ptr<QueryOptimizerCursor> &cursor ) const = 0;
+ shared_ptr<QueryOptimizerCursor> _cursor;
+ };
+
+ class PossibleInOrderPlans : public PossiblePlans {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ nPlans( 2, BSON( "a" << 1 << "x" << 1 ), BSONObj() );
+ setCursor( BSON( "a" << 1 << "x" << 1 ), BSONObj() );
+ checkCursor( false );
+ ASSERT( _cursor->initialFieldRangeSet()->range( "a" ).equality() );
+ ASSERT( !_cursor->initialFieldRangeSet()->range( "b" ).equality() );
+ ASSERT( _cursor->initialFieldRangeSet()->range( "x" ).equality() );
+
+ // Without running the (nonempty) cursor, no cached plan is recorded.
+ setCursor( BSON( "a" << 1 << "x" << 1 ), BSONObj() );
+ checkCursor( false );
+
+ // Running the cursor records the plan.
+ runCursor();
+ nPlans( 1, BSON( "a" << 1 << "x" << 1 ), BSONObj() );
+ setCursor( BSON( "a" << 1 << "x" << 1 ), BSONObj() );
+ checkCursor( true );
+
+ // Other plans may be added.
+ setCursor( BSON( "a" << 2 << "x" << 1 ), BSONObj() );
+ checkCursor( true );
+ for( int i = 0; i < 10; ++i, _cursor->advance() );
+ // The natural plan has been added in.
+ checkCursor( false );
+ nPlans( 1, BSON( "a" << 2 << "x" << 1 ), BSONObj() );
+ runCursor();
+
+ // The a:1 plan was recorded again.
+ nPlans( 1, BSON( "a" << 2 << "x" << 1 ), BSONObj() );
+ setCursor( BSON( "a" << 2 << "x" << 1 ), BSONObj() );
+ checkCursor( true );
+
+ // Clear the recorded plan manually.
+ _cursor->clearIndexesForPatterns();
+ nPlans( 2, BSON( "a" << 2 << "x" << 1 ), BSONObj() );
+ setCursor( BSON( "a" << 2 << "x" << 1 ), BSONObj() );
+ checkCursor( false );
+
+ // Add more data, and run until takeover occurs.
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 3 << "x" << 1 ) );
+ }
+
+ setCursor( BSON( "a" << 3 << "x" << 1 ), BSONObj() );
+ checkCursor( false );
+ runCursorUntilTakeover();
+ ASSERT( _cursor->ok() );
+ checkTakeoverCursor( false );
+
+ // Try again, with a cached plan this time.
+ setCursor( BSON( "a" << 3 << "x" << 1 ), BSONObj() );
+ checkCursor( true );
+ runCursorUntilTakeover();
+ checkTakeoverCursor( false );
+ }
+ private:
+ void checkCursor( bool runningInitialCachedPlan ) {
+ return PossiblePlans::checkCursor( true, false, true, runningInitialCachedPlan );
+ }
+ virtual void checkIterate( const shared_ptr<QueryOptimizerCursor> &cursor ) const {
+ ASSERT( !cursor->currentPlanScanAndOrderRequired() );
+ ASSERT( !cursor->completePlanOfHybridSetScanAndOrderRequired() );
+ }
+ };
+
+ class PossibleOutOfOrderPlans : public PossiblePlans {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ }
+ _cli.insert( ns(), BSON( "b" << 2 ) );
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ nPlans( 3, BSON( "a" << 1 << "b" << 1 ), BSON( "x" << 1 ) );
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "x" << 1 ) );
+ checkCursor( false );
+ ASSERT( _cursor->initialFieldRangeSet()->range( "a" ).equality() );
+ ASSERT( _cursor->initialFieldRangeSet()->range( "b" ).equality() );
+ ASSERT( !_cursor->initialFieldRangeSet()->range( "x" ).equality() );
+
+ // Without running the (nonempty) cursor, no cached plan is recorded.
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "x" << 1 ) );
+ checkCursor( false );
+
+ // Running the cursor records the plan.
+ runCursor();
+ nPlans( 1, BSON( "a" << 1 << "b" << 1 ), BSON( "x" << 1 ) );
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "x" << 1 ) );
+ checkCursor( true );
+
+ // Other plans may be added.
+ setCursor( BSON( "a" << 2 << "b" << 2 ), BSON( "x" << 1 ) );
+ checkCursor( true );
+ for( int i = 0; i < 10; ++i, _cursor->advance() );
+ // The other plans have been added in.
+ checkCursor( false );
+ runCursor();
+
+ // The b:1 plan was recorded.
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "x" << 1 ) );
+ checkCursor( true );
+
+ // Clear the recorded plan manually.
+ _cursor->clearIndexesForPatterns();
+ setCursor( BSON( "a" << 2 << "x" << 1 ), BSON( "x" << 1 ) );
+ checkCursor( false );
+
+ // Add more data, and run until takeover occurs.
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 3 << "b" << 3 ) );
+ }
+
+ setCursor( BSON( "a" << 3 << "b" << 3 ), BSON( "x" << 1 ) );
+ checkCursor( false );
+ runCursorUntilTakeover();
+ ASSERT( _cursor->ok() );
+ checkTakeoverCursor( true );
+
+ // Try again, with a cached plan this time.
+ setCursor( BSON( "a" << 3 << "b" << 3 ), BSON( "x" << 1 ) );
+ checkCursor( true );
+ runCursorUntilTakeover();
+ checkTakeoverCursor( true );
+ }
+ private:
+ void checkCursor( bool runningInitialCachedPlan ) {
+ return PossiblePlans::checkCursor( false, true, false, runningInitialCachedPlan );
+ }
+ virtual void checkIterate( const shared_ptr<QueryOptimizerCursor> &cursor ) const {
+ ASSERT( cursor->currentPlanScanAndOrderRequired() );
+ ASSERT( !cursor->completePlanOfHybridSetScanAndOrderRequired() );
+ }
+ };
+
+ class PossibleBothPlans : public PossiblePlans {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 2 << "b" << 1 ) );
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ if ( i % 10 == 0 ) {
+ _cli.insert( ns(), BSON( "b" << 2 ) );
+ }
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ nPlans( 3, BSON( "a" << 1 << "b" << 1 ), BSON( "b" << 1 ) );
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "b" << 1 ) );
+ checkCursor( true, false );
+ ASSERT( _cursor->initialFieldRangeSet()->range( "a" ).equality() );
+ ASSERT( _cursor->initialFieldRangeSet()->range( "b" ).equality() );
+ ASSERT( !_cursor->initialFieldRangeSet()->range( "x" ).equality() );
+
+ // Without running the (nonempty) cursor, no cached plan is recorded.
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "b" << 1 ) );
+ checkCursor( true, false );
+
+ // Running the cursor records the a:1 plan.
+ runCursor( true );
+ nPlans( 1, BSON( "a" << 1 << "b" << 1 ), BSON( "b" << 1 ) );
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "b" << 1 ) );
+ checkCursor( false, true );
+
+ // Other plans may be added.
+ setCursor( BSON( "a" << 2 << "b" << 2 ), BSON( "b" << 1 ) );
+ checkCursor( false, true );
+ for( int i = 0; i < 10; ++i, _cursor->advance() );
+ // The other plans have been added in (including ordered b:1).
+ checkCursor( true, false );
+ runCursor( false );
+
+ // The b:1 plan was recorded.
+ setCursor( BSON( "a" << 1 << "b" << 1 ), BSON( "b" << 1 ) );
+ checkCursor( true, true );
+
+ // Clear the recorded plan manually.
+ _cursor->clearIndexesForPatterns();
+ setCursor( BSON( "a" << 2 << "b" << 1 ), BSON( "b" << 1 ) );
+ checkCursor( true, false );
+
+ // Add more data, and run until takeover occurs.
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 3 << "b" << 3 ) );
+ }
+
+ setCursor( BSON( "a" << 3 << "b" << 3 ), BSON( "b" << 1 ) );
+ checkCursor( true, false );
+ runCursorUntilTakeover();
+ ASSERT( _cursor->ok() );
+ checkTakeoverCursor( false );
+ ASSERT_EQUALS( BSON( "b" << 1 ), _cursor->indexKeyPattern() );
+
+ // Try again, with a cached plan this time.
+ setCursor( BSON( "a" << 3 << "b" << 3 ), BSON( "b" << 1 ) );
+ checkCursor( true, true );
+ runCursorUntilTakeover();
+ checkTakeoverCursor( false );
+ ASSERT_EQUALS( BSON( "b" << 1 ), _cursor->indexKeyPattern() );
+ }
+ private:
+ void checkCursor( bool runningInitialInOrderPlan, bool runningInitialCachedPlan ) {
+ return PossiblePlans::checkCursor( true, true, runningInitialInOrderPlan,
+ runningInitialCachedPlan );
+ }
+ virtual void checkIterate( const shared_ptr<QueryOptimizerCursor> &cursor ) const {
+ if ( cursor->indexKeyPattern() == BSON( "b" << 1 ) ) {
+ ASSERT( !cursor->currentPlanScanAndOrderRequired() );
+ }
+ else {
+ ASSERT( cursor->currentPlanScanAndOrderRequired() );
+ }
+ ASSERT( !cursor->completePlanOfHybridSetScanAndOrderRequired() );
+ }
+ };
+
+ class AbortOutOfOrderPlans : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+
+ shared_ptr<QueryOptimizerCursor> c = getCursor( BSON( "a" << 1 << "b" << BSONNULL ),
+ BSON( "a" << 1 ) );
+ // Wait until a $natural plan result is returned.
+ while( c->indexKeyPattern() != BSONObj() ) {
+ c->advance();
+ }
+ // Abort the natural plan.
+ c->abortOutOfOrderPlans();
+ c->advance();
+ // Check that no more results from the natural plan are returned.
+ ASSERT( c->ok() );
+ while( c->ok() ) {
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+ c->advance();
+ }
+ ASSERT( !c->completePlanOfHybridSetScanAndOrderRequired() );
+ }
+ };
+
+ class AbortOutOfOrderPlanOnLastMatch : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 1 << 2 ) ) );
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+
+ shared_ptr<QueryOptimizerCursor> c =
+ getCursor( BSON( "a" << GTE << 1 << "b" << BSONNULL ), BSON( "a" << 1 ) );
+ // Wait until 10 (all) $natural plan results are returned.
+ for( int i = 0; i < 10; ++i ) {
+ while( c->indexKeyPattern() != BSONObj() ) {
+ c->advance();
+ }
+ c->advance();
+ }
+ // Abort the natural plan.
+ c->abortOutOfOrderPlans();
+ c->advance();
+ // Check that no more results from the natural plan are returned, and the cursor is not
+ // done iterating.
+ ASSERT( c->ok() );
+ while( c->ok() ) {
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+ c->advance();
+ }
+ ASSERT( !c->completePlanOfHybridSetScanAndOrderRequired() );
+ }
+ };
+
+ /** Out of order plans are not added after abortOutOfOrderPlans() is called. */
+ class AbortOutOfOrderPlansBeforeAddOtherPlans : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << -1 << "b" << 0 ) );
+ for( int b = 0; b < 2; ++b ) {
+ for( int a = 0; a < 10; ++a ) {
+ _cli.insert( ns(), BSON( "a" << a << "b" << b ) );
+ }
+ }
+
+ // Selectivity is better for the a:1 index.
+ _aPreferableQuery = BSON( "a" << GTE << -100 << LTE << -1 << "b" << 0 );
+ // Selectivity is better for the b:1 index.
+ _bPreferableQuery = BSON( "a" << GTE << 0 << LTE << 100 << "b" << 0 );
+
+ Client::ReadContext ctx( ns() );
+
+ // If abortOutOfOrderPlans() is not set, other plans will be attempted.
+ recordAIndex();
+ _cursor = getCursor( _bPreferableQuery, BSON( "a" << 1 ) );
+ checkInitialIteratePlans();
+ // The b:1 index is attempted later.
+ checkBIndexUsed( true );
+
+ // If abortOutOfOrderPlans() is set, other plans will not be attempted.
+ recordAIndex();
+ _cursor = getCursor( _bPreferableQuery, BSON( "a" << 1 ) );
+ checkInitialIteratePlans();
+ _cursor->abortOutOfOrderPlans();
+ // The b:1 index is not attempted.
+ checkBIndexUsed( false );
+ }
+ private:
+ /** Record the a:1 index for the query pattern of interest. */
+ void recordAIndex() const {
+ NamespaceDetailsTransient &nsdt = NamespaceDetailsTransient::get( ns() );
+ nsdt.clearQueryCache();
+ shared_ptr<QueryOptimizerCursor> c = getCursor( _aPreferableQuery, BSON( "a" << 1 ) );
+ while( c->advance() );
+ FieldRangeSet aPreferableFields( ns(), _aPreferableQuery, true, true );
+ ASSERT_EQUALS( BSON( "a" << 1 ),
+ nsdt.cachedQueryPlanForPattern
+ ( aPreferableFields.pattern( BSON( "a" << 1 ) ) ).indexKey() );
+ }
+ /** The first results come from the recorded index. */
+ void checkInitialIteratePlans() const {
+ for( int i = 0; i < 5; ++i ) {
+ ASSERT_EQUALS( BSON( "a" << 1 ), _cursor->indexKeyPattern() );
+ }
+ }
+ /** Check if the b:1 index is used during iteration. */
+ void checkBIndexUsed( bool expected ) const {
+ bool bIndexUsed = false;
+ while( _cursor->advance() ) {
+ if ( BSON( "b" << 1 ) == _cursor->indexKeyPattern() ) {
+ bIndexUsed = true;
+ }
+ }
+ ASSERT_EQUALS( expected, bIndexUsed );
+ }
+ BSONObj _aPreferableQuery;
+ BSONObj _bPreferableQuery;
+ shared_ptr<QueryOptimizerCursor> _cursor;
+ };
+
+ class TakeoverOrRangeElimination : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ }
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ }
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 3 ) );
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+
+ shared_ptr<QueryOptimizerCursor> c =
+ getCursor( fromjson( "{$or:[{a:{$lte:2}},{a:{$gte:2}},{a:9}]}" ), BSONObj() );
+
+ int count = 0;
+ while( c->ok() ) {
+ c->advance();
+ ++count;
+ }
+ ASSERT_EQUALS( 160, count );
+ }
+ };
+
+ class TakeoverOrDedups : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ }
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 << "b" << 2 ) );
+ }
+ for( int i = 0; i < 20; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 3 << "b" << 3 ) );
+ }
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+
+ BSONObj query =
+ BSON(
+ "$or" << BSON_ARRAY(
+ BSON(
+ "a" << GTE << 0 << LTE << 2 <<
+ "b" << GTE << 0 << LTE << 2
+ ) <<
+ BSON(
+ "a" << GTE << 1 << LTE << 3 <<
+ "b" << GTE << 1 << LTE << 3
+ ) <<
+ BSON(
+ "a" << GTE << 1 << LTE << 4 <<
+ "b" << GTE << 1 << LTE << 4
+ )
+ )
+ );
+
+ shared_ptr<QueryOptimizerCursor> c = getCursor( query, BSONObj() );
+
+ int count = 0;
+ while( c->ok() ) {
+ if ( ( c->indexKeyPattern() == BSON( "a" << 1 << "b" << 1 ) ) &&
+ c->currentMatches() ) {
+ ++count;
+ }
+ c->advance();
+ }
+ ASSERT_EQUALS( 160, count );
+ }
+ };
+
+ /** Proper index matching when transitioning between $or clauses after a takeover. */
+ class TakeoverOrDifferentIndex : public PlanChecking {
+ public:
+ void run() {
+ for( int i = 0; i < 120; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 2 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ for( int i = 0; i < 130; ++i ) {
+ _cli.insert( ns(), BSON( "b" << 3 << "a" << 4 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "b" << 1 << "a" << 1 ) );
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+
+ // This $or query will scan index a:1,b:1 then b:1,a:1. If the key pattern is specified
+ // incorrectly for the second clause, matching will fail.
+ setQueryOptimizerCursor( fromjson( "{$or:[{a:1,b:{$gte:0}},{b:3,a:{$gte:0}}]}" ) );
+ // All documents match, and there are no dups.
+ ASSERT_EQUALS( 250, itcount() );
+ }
+ };
+
+ /**
+ * An ordered plan returns all results, including when it takes over, even when it duplicates an
+ * entry of an out of order plan.
+ */
+ class TakeoverOrderedPlanDupsOutOfOrderPlan : public PlanChecking {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 1; i < 200; ++i ) {
+ _cli.insert( ns(), BSON( "a" << i << "b" << 0 ) );
+ }
+ // Insert this document last, so that most documents are read from the $natural cursor
+ // before the a:1 cursor.
+ _cli.insert( ns(), BSON( "a" << 0 << "b" << 0 ) );
+
+ Client::ReadContext ctx( ns() );
+ shared_ptr<QueryOptimizerCursor> cursor =
+ getCursor( BSON( "a" << GTE << 0 << "b" << 0 ), BSON( "a" << 1 ) );
+ int nextA = 0;
+ for( ; cursor->ok(); cursor->advance() ) {
+ if ( cursor->indexKeyPattern() == BSON( "a" << 1 ) ) {
+ // Check that the expected 'a' value is present and in order.
+ ASSERT_EQUALS( nextA++, cursor->current()[ "a" ].number() );
+ }
+ }
+ ASSERT_EQUALS( 200, nextA );
+ }
+ };
+
+ /** Check that an elemMatchKey can be retrieved from MatchDetails using a qo cursor. */
+ class ElemMatchKey : public Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a.b" << 1 ) );
+ _cli.insert( ns(), fromjson( "{ a:[ { b:1 } ] }" ) );
+
+ Client::ReadContext ctx( ns() );
+ setQueryOptimizerCursor( BSON( "a.b" << 1 ) );
+ MatchDetails details;
+ details.requestElemMatchKey();
+ ASSERT( c()->currentMatches( &details ) );
+ // The '0' entry of the 'a' array is matched.
+ ASSERT_EQUALS( string( "0" ), details.elemMatchKey() );
+ }
+ };
+
+ namespace GetCursor {
+
+ class Base : public QueryOptimizerCursorTests::Base {
+ public:
+ Base() {
+ // create collection
+ _cli.insert( ns(), BSON( "_id" << 5 ) );
+ }
+ virtual ~Base() {}
+ void run() {
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ bool simpleEqualityMatch;
+ if ( expectException() ) {
+ ASSERT_THROWS
+ ( NamespaceDetailsTransient::getCursor
+ ( ns(), query(), order(), planPolicy(), &simpleEqualityMatch ),
+ MsgAssertionException );
+ return;
+ }
+ _query = query();
+ _parsedQuery.reset( new ParsedQuery( ns(), skip(), limit(), 0, _query,
+ BSONObj() ) );
+ BSONObj extractedQuery = _query;
+ if ( !_query["$query"].eoo() ) {
+ extractedQuery = _query["$query"].Obj();
+ }
+ shared_ptr<Cursor> c =
+ NamespaceDetailsTransient::getCursor( ns(), extractedQuery, order(), planPolicy(),
+ &simpleEqualityMatch, _parsedQuery, false );
+ ASSERT_EQUALS( expectSimpleEquality(), simpleEqualityMatch );
+ string type = c->toString().substr( 0, expectedType().length() );
+ ASSERT_EQUALS( expectedType(), type );
+ check( c );
+ }
+ protected:
+ virtual string expectedType() const { return "TESTDUMMY"; }
+ virtual bool expectException() const { return false; }
+ virtual bool expectSimpleEquality() const { return false; }
+ virtual BSONObj query() const { return BSONObj(); }
+ virtual BSONObj order() const { return BSONObj(); }
+ virtual int skip() const { return 0; }
+ virtual int limit() const { return 0; }
+ virtual const QueryPlanSelectionPolicy &planPolicy() const {
+ return QueryPlanSelectionPolicy::any();
+ }
+ virtual void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( !c->matcher() );
+ ASSERT_EQUALS( 5, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ private:
+ BSONObj _query;
+ shared_ptr<ParsedQuery> _parsedQuery;
+ };
+
+ class NoConstraints : public Base {
+ string expectedType() const { return "BasicCursor"; }
+ };
+
+ class SimpleId : public Base {
+ public:
+ SimpleId() {
+ _cli.insert( ns(), BSON( "_id" << 0 ) );
+ _cli.insert( ns(), BSON( "_id" << 10 ) );
+ }
+ string expectedType() const { return "BtreeCursor _id_"; }
+ BSONObj query() const { return BSON( "_id" << 5 ); }
+ };
+
+ class OptimalIndex : public Base {
+ public:
+ OptimalIndex() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 5 ) );
+ _cli.insert( ns(), BSON( "a" << 6 ) );
+ }
+ string expectedType() const { return "BtreeCursor a_1"; }
+ BSONObj query() const { return BSON( "a" << GTE << 5 ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->matcher() );
+ ASSERT_EQUALS( 5, c->current().getIntField( "a" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( c->advance() );
+ ASSERT_EQUALS( 6, c->current().getIntField( "a" ) );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ class SimpleKeyMatch : public Base {
+ public:
+ SimpleKeyMatch() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.update( ns(), BSONObj(), BSON( "$set" << BSON( "a" << true ) ) );
+ }
+ string expectedType() const { return "BtreeCursor a_1"; }
+ bool expectSimpleEquality() const { return true; }
+ BSONObj query() const { return BSON( "a" << true ); }
+ virtual void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT_EQUALS( 5, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ class Geo : public Base {
+ public:
+ Geo() {
+ _cli.insert( ns(), BSON( "_id" << 44 << "loc" << BSON_ARRAY( 44 << 45 ) ) );
+ _cli.ensureIndex( ns(), BSON( "loc" << "2d" ) );
+ }
+ string expectedType() const { return "GeoSearchCursor"; }
+ BSONObj query() const { return fromjson( "{ loc : { $near : [50,50] } }" ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->matcher() );
+ ASSERT( c->matcher()->matchesCurrent( c.get() ) );
+ ASSERT_EQUALS( 44, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ class GeoNumWanted : public Base {
+ public:
+ GeoNumWanted() {
+ _cli.ensureIndex( ns(), BSON( "loc" << "2d" ) );
+ for( int i = 0; i < 140; ++i ) {
+ _cli.insert( ns(), BSON( "loc" << BSON_ARRAY( 44 << 45 ) ) );
+ }
+ }
+ string expectedType() const { return "GeoSearchCursor"; }
+ BSONObj query() const { return fromjson( "{ loc : { $near : [50,50] } }" ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ int count = 0;
+ while( c->ok() ) {
+ ++count;
+ c->advance();
+ }
+ ASSERT_EQUALS( 130, count );
+ }
+ int skip() const { return 27; }
+ int limit() const { return 103; }
+ };
+
+ class PreventOutOfOrderPlan : public QueryOptimizerCursorTests::Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 5 ) );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = NamespaceDetailsTransient::getCursor( ns(), BSONObj(), BSON( "b" << 1 ) );
+ ASSERT( !c );
+ }
+ };
+
+ class AllowOutOfOrderPlan : public Base {
+ public:
+ void run() {
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << BSONObj() <<
+ "$orderby" << BSON( "a" << 1 ) ),
+ BSONObj() ) );
+ shared_ptr<Cursor> c =
+ NamespaceDetailsTransient::getCursor( ns(), BSONObj(), BSON( "a" << 1 ),
+ QueryPlanSelectionPolicy::any(), 0,
+ parsedQuery, false );
+ ASSERT( c );
+ }
+ };
+
+ class BestSavedOutOfOrder : public QueryOptimizerCursorTests::Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 5 << "b" << BSON_ARRAY( 1 << 2 << 3 << 4 << 5 ) ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "b" << 6 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ // record {_id:1} index for this query
+ ASSERT( _cli.query( ns(), QUERY( "_id" << GT << 0 << "b" << GT << 0 ).sort( "b" ) )->more() );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ shared_ptr<Cursor> c = NamespaceDetailsTransient::getCursor( ns(), BSON( "_id" << GT << 0 << "b" << GT << 0 ), BSON( "b" << 1 ) );
+ // {_id:1} requires scan and order, so {b:1} must be chosen.
+ ASSERT( c );
+ ASSERT_EQUALS( 5, c->current().getIntField( "_id" ) );
+ }
+ };
+
+ /**
+ * If an optimal plan is cached, return a Cursor for it rather than a QueryOptimizerCursor.
+ */
+ class BestSavedOptimal : public QueryOptimizerCursorTests::Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "_id" << 1 << "q" << 1 ) );
+ ASSERT( _cli.query( ns(), QUERY( "_id" << GT << 0 ) )->more() );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ // Check the plan that was recorded for this query.
+ ASSERT_EQUALS( BSON( "_id" << 1 ), cachedIndexForQuery( BSON( "_id" << GT << 0 ) ) );
+ shared_ptr<Cursor> c = NamespaceDetailsTransient::getCursor( ns(), BSON( "_id" << GT << 0 ) );
+ // No need for query optimizer cursor since the plan is optimal.
+ ASSERT_EQUALS( "BtreeCursor _id_", c->toString() );
+ }
+ };
+
+ /** If a non optimal plan is a candidate a QueryOptimizerCursor should be returned, even if plan has been recorded. */
+ class BestSavedNotOptimal : public QueryOptimizerCursorTests::Base {
+ public:
+ void run() {
+ _cli.insert( ns(), BSON( "_id" << 1 << "q" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "q" << 1 ) );
+ // Record {_id:1} index for this query
+ ASSERT( _cli.query( ns(), QUERY( "q" << 1 << "_id" << 1 ) )->more() );
+ Lock::GlobalWrite lk;
+ Client::Context ctx( ns() );
+ ASSERT_EQUALS( BSON( "_id" << 1 ),
+ cachedIndexForQuery( BSON( "q" << 1 << "_id" << 1 ) ) );
+ shared_ptr<Cursor> c = NamespaceDetailsTransient::getCursor( ns(), BSON( "q" << 1 << "_id" << 1 ) );
+ // Need query optimizer cursor since the cached plan is not optimal.
+ ASSERT_EQUALS( "QueryOptimizerCursor", c->toString() );
+ }
+ };
+
+ class MultiIndex : public Base {
+ public:
+ MultiIndex() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ string expectedType() const { return "QueryOptimizerCursor"; }
+ BSONObj query() const { return BSON( "_id" << GT << 0 << "a" << GT << 0 ); }
+ void check( const shared_ptr<Cursor> &c ) {}
+ };
+
+ class Hint : public Base {
+ public:
+ Hint() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ string expectedType() const { return "BtreeCursor a_1"; }
+ BSONObj query() const {
+ return BSON( "$query" << BSON( "_id" << 1 ) << "$hint" << BSON( "a" << 1 ) );
+ }
+ void check( const shared_ptr<Cursor> &c ) {}
+ };
+
+ class Snapshot : public Base {
+ public:
+ Snapshot() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ string expectedType() const { return "BtreeCursor _id_"; }
+ BSONObj query() const {
+ return BSON( "$query" << BSON( "a" << 1 ) << "$snapshot" << true );
+ }
+ void check( const shared_ptr<Cursor> &c ) {}
+ };
+
+ class SnapshotCappedColl : public Base {
+ public:
+ SnapshotCappedColl() {
+ _cli.dropCollection( ns() );
+ _cli.createCollection( ns(), 1000, true );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ string expectedType() const { return "BtreeCursor _id_"; }
+ BSONObj query() const {
+ return BSON( "$query" << BSON( "a" << 1 ) << "$snapshot" << true );
+ }
+ void check( const shared_ptr<Cursor> &c ) {}
+ };
+
+ class Min : public Base {
+ public:
+ Min() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ string expectedType() const { return "BtreeCursor a_1"; }
+ BSONObj query() const {
+ return BSON( "$query" << BSONObj() << "$min" << BSON( "a" << 1 ) );
+ }
+ void check( const shared_ptr<Cursor> &c ) {}
+ };
+
+ class Max : public Base {
+ public:
+ Max() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ string expectedType() const { return "BtreeCursor a_1"; }
+ BSONObj query() const {
+ return BSON( "$query" << BSONObj() << "$max" << BSON( "a" << 1 ) );
+ }
+ void check( const shared_ptr<Cursor> &c ) {}
+ };
+
+ namespace RequireIndex {
+
+ class Base : public GetCursor::Base {
+ const QueryPlanSelectionPolicy &planPolicy() const {
+ return QueryPlanSelectionPolicy::indexOnly();
+ }
+ };
+
+ class NoConstraints : public Base {
+ bool expectException() const { return true; }
+ };
+
+ class SimpleId : public Base {
+ string expectedType() const { return "BtreeCursor _id_"; }
+ BSONObj query() const { return BSON( "_id" << 5 ); }
+ };
+
+ class UnindexedQuery : public Base {
+ bool expectException() const { return true; }
+ BSONObj query() const { return BSON( "a" << GTE << 5 ); }
+ };
+
+ class IndexedQuery : public Base {
+ public:
+ IndexedQuery() {
+ _cli.insert( ns(), BSON( "_id" << 6 << "a" << 6 << "c" << 4 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 << "b" << 1 << "c" << 1 ) );
+ }
+ string expectedType() const { return "QueryOptimizerCursor"; }
+ BSONObj query() const { return BSON( "a" << GTE << 5 << "c" << 4 ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->matcher() );
+ ASSERT_EQUALS( 6, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ class SecondOrClauseIndexed : public Base {
+ public:
+ SecondOrClauseIndexed() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "b" << 1 ) );
+ }
+ string expectedType() const { return "QueryOptimizerCursor"; }
+ BSONObj query() const { return fromjson( "{$or:[{a:1},{b:1}]}" ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->matcher() );
+ ASSERT( c->advance() );
+ ASSERT( !c->advance() ); // 2 matches exactly
+ }
+ };
+
+ class SecondOrClauseUnindexed : public Base {
+ public:
+ SecondOrClauseUnindexed() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ }
+ bool expectException() const { return true; }
+ BSONObj query() const { return fromjson( "{$or:[{a:1},{b:1}]}" ); }
+ };
+
+ class SecondOrClauseUnindexedUndetected : public Base {
+ public:
+ SecondOrClauseUnindexedUndetected() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "b" << 1 ) );
+ }
+ string expectedType() const { return "QueryOptimizerCursor"; }
+ BSONObj query() const { return fromjson( "{$or:[{a:1},{b:1}]}" ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->matcher() );
+ // An unindexed cursor is required for the second clause, but is not allowed.
+ ASSERT_THROWS( c->advance(), MsgAssertionException );
+ }
+ };
+
+ class RecordedUnindexedPlan : public Base {
+ public:
+ RecordedUnindexedPlan() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 1 << 2 << 3 ) << "b" << 1 ) );
+ auto_ptr<DBClientCursor> cursor =
+ _cli.query( ns(), QUERY( "a" << GT << 0 << "b" << 1 ).explain() );
+ BSONObj explain = cursor->next();
+ ASSERT_EQUALS( "BasicCursor", explain[ "cursor" ].String() );
+ }
+ string expectedType() const { return "QueryOptimizerCursor"; }
+ BSONObj query() const { return BSON( "a" << GT << 0 << "b" << 1 ); }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+ while( c->advance() ) {
+ ASSERT_EQUALS( BSON( "a" << 1 ), c->indexKeyPattern() );
+ }
+ }
+ };
+
+ } // namespace RequireIndex
+
+ namespace IdElseNatural {
+
+ class Base : public GetCursor::Base {
+ const QueryPlanSelectionPolicy &planPolicy() const {
+ return QueryPlanSelectionPolicy::idElseNatural();
+ }
+ };
+
+ class AllowOptimalNaturalPlan : public Base {
+ string expectedType() const { return "BasicCursor"; }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( !c->matcher() );
+ ASSERT_EQUALS( 5, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ };
+
+ class AllowOptimalIdPlan : public Base {
+ string expectedType() const { return "BtreeCursor _id_"; }
+ BSONObj query() const { return BSON( "_id" << 5 ); }
+ };
+
+ class HintedIdForQuery : public Base {
+ public:
+ HintedIdForQuery( const BSONObj &query ) : _query( query ) {
+ _cli.remove( ns(), BSONObj() );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ }
+ string expectedType() const { return "BtreeCursor _id_"; }
+ BSONObj query() const { return _query; }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->currentMatches() );
+ ASSERT_EQUALS( 1, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ private:
+ BSONObj _query;
+ };
+
+ class HintedNaturalForQuery : public Base {
+ public:
+ HintedNaturalForQuery( const BSONObj &query ) : _query( query ) {
+ _cli.dropCollection( ns() );
+ _cli.createCollection( ns(), 1024, true );
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "_id" << 1 << "a" << 1 ) );
+ }
+ ~HintedNaturalForQuery() {
+ _cli.dropCollection( ns() );
+ }
+ string expectedType() const { return "ForwardCappedCursor"; }
+ BSONObj query() const { return _query; }
+ void check( const shared_ptr<Cursor> &c ) {
+ ASSERT( c->ok() );
+ ASSERT( c->currentMatches() );
+ ASSERT_EQUALS( 1, c->current().getIntField( "_id" ) );
+ ASSERT( !c->advance() );
+ }
+ private:
+ BSONObj _query;
+ };
+
+ } // namespace IdElseNatural
+
+ /**
+ * Generating a cursor for an invalid query asserts, even if the collection is empty or
+ * missing.
+ */
+ class MatcherValidation : public Base {
+ public:
+ void run() {
+ // Matcher validation with an empty collection.
+ _cli.remove( ns(), BSONObj() );
+ // The historical behavior has been to generate a MsgAssertionException for the
+ // multiple cursors case, but it should be a UserException.
+ checkInvalidQueryAssertions<MsgAssertionException>();
+
+ // Matcher validation with a missing collection.
+ _cli.dropCollection( ns() );
+ checkInvalidQueryAssertions<UserException>();
+ }
+ private:
+ template<class MultipleCursorException>
+ static void checkInvalidQueryAssertions() {
+ Client::ReadContext ctx( ns() );
+
+ // An invalid query generaing a single query plan asserts.
+ BSONObj invalidQuery = fromjson( "{$and:[{$atomic:true}]}" );
+ assertInvalidQueryAssertion<UserException>( invalidQuery );
+
+ // An invalid query generating multiple query plans asserts.
+ BSONObj invalidIdQuery = fromjson( "{_id:0,$and:[{$atomic:true}]}" );
+ assertInvalidQueryAssertion<MultipleCursorException>( invalidIdQuery );
+ }
+ template<class Exception>
+ static void assertInvalidQueryAssertion( const BSONObj &query ) {
+ ASSERT_THROWS( NamespaceDetailsTransient::getCursor( ns(), query, BSONObj() ),
+ Exception );
+ }
+ };
+
+ } // namespace GetCursor
+
+ namespace Explain {
+
+ class ClearRecordedIndex : public QueryOptimizerCursorTests::Base {
+ public:
+ void run() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ BSONObj query = BSON( "a" << 1 << "b" << 1 );
+ shared_ptr<Cursor> c =
+ NamespaceDetailsTransient::getCursor( ns(), query );
+ while( c->advance() );
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << query << "$explain" << true ),
+ BSONObj() ) );
+ c = NamespaceDetailsTransient::getCursor( ns(), query, BSONObj(), QueryPlanSelectionPolicy::any(), 0,
+ parsedQuery, false );
+ set<BSONObj> indexKeys;
+ while( c->ok() ) {
+ indexKeys.insert( c->indexKeyPattern() );
+ c->advance();
+ }
+ ASSERT( indexKeys.size() > 1 );
+ }
+ };
+
+ class Base : public QueryOptimizerCursorTests::Base {
+ public:
+ virtual ~Base() {}
+ void run() {
+ setupCollection();
+
+ Lock::DBWrite lk(ns());
+ Client::Context ctx( ns() );
+ shared_ptr<ParsedQuery> parsedQuery
+ ( new ParsedQuery( ns(), 0, 0, 0,
+ BSON( "$query" << query() << "$explain" << true ),
+ fields() ) );
+ _cursor =
+ dynamic_pointer_cast<QueryOptimizerCursor>
+ ( NamespaceDetailsTransient::getCursor( ns(), query(), BSONObj(), QueryPlanSelectionPolicy::any(), 0,
+ parsedQuery, false ) );
+ ASSERT( _cursor );
+
+ handleCursor();
+
+ _explainInfo = _cursor->explainQueryInfo();
+ _explain = _explainInfo->bson();
+
+ checkExplain();
+ }
+ protected:
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 2 << "b" << 1 ) );
+ }
+ virtual BSONObj query() const { return BSON( "a" << 1 << "b" << 1 ); }
+ virtual BSONObj fields() const { return BSONObj(); }
+ virtual void handleCursor() {
+ }
+ virtual void checkExplain() {
+ }
+ shared_ptr<QueryOptimizerCursor> _cursor;
+ shared_ptr<ExplainQueryInfo> _explainInfo;
+ BSONObj _explain;
+ };
+
+ class Initial : public Base {
+ virtual void checkExplain() {
+ ASSERT( !_explain[ "cursor" ].eoo() );
+ ASSERT( !_explain[ "isMultiKey" ].Bool() );
+ ASSERT_EQUALS( 0, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 0, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 2, _explain[ "nscannedAllPlans" ].number() );
+ ASSERT( !_explain[ "scanAndOrder" ].Bool() );
+ ASSERT( !_explain[ "indexOnly" ].Bool() );
+ ASSERT_EQUALS( 0, _explain[ "nYields" ].Int() );
+ ASSERT_EQUALS( 0, _explain[ "nChunkSkips" ].number() );
+ ASSERT( !_explain[ "millis" ].eoo() );
+ ASSERT( !_explain[ "indexBounds" ].eoo() );
+ ASSERT_EQUALS( 2U, _explain[ "allPlans" ].Array().size() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( "BtreeCursor a_1", plan1[ "cursor" ].String() );
+ ASSERT_EQUALS( 0, plan1[ "n" ].number() );
+ ASSERT_EQUALS( 0, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan1[ "nscanned" ].number() );
+ ASSERT_EQUALS( fromjson( "{a:[[1,1]]}" ), plan1[ "indexBounds" ].Obj() );
+
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( "BasicCursor", plan2[ "cursor" ].String() );
+ ASSERT_EQUALS( 0, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 0, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan2[ "nscanned" ].number() );
+ ASSERT_EQUALS( BSONObj(), plan2[ "indexBounds" ].Obj() );
+ }
+ };
+
+ class Empty : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ virtual void handleCursor() {
+ ASSERT( !_cursor->ok() );
+ }
+ virtual void checkExplain() {
+ ASSERT( !_explain[ "cursor" ].eoo() );
+ ASSERT( !_explain[ "isMultiKey" ].Bool() );
+ ASSERT_EQUALS( 0, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 0, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 0, _explain[ "nscannedAllPlans" ].number() );
+ ASSERT( !_explain[ "scanAndOrder" ].Bool() );
+ ASSERT( !_explain[ "indexOnly" ].Bool() );
+ ASSERT_EQUALS( 0, _explain[ "nYields" ].Int() );
+ ASSERT_EQUALS( 0, _explain[ "nChunkSkips" ].number() );
+ ASSERT( !_explain[ "millis" ].eoo() );
+ ASSERT( !_explain[ "indexBounds" ].eoo() );
+ ASSERT_EQUALS( 2U, _explain[ "allPlans" ].Array().size() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( "BtreeCursor a_1", plan1[ "cursor" ].String() );
+ ASSERT_EQUALS( 0, plan1[ "n" ].number() );
+ ASSERT_EQUALS( 0, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 0, plan1[ "nscanned" ].number() );
+ ASSERT_EQUALS( fromjson( "{a:[[1,1]]}" ), plan1[ "indexBounds" ].Obj() );
+
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( "BasicCursor", plan2[ "cursor" ].String() );
+ ASSERT_EQUALS( 0, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 0, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 0, plan2[ "nscanned" ].number() );
+ ASSERT_EQUALS( BSONObj(), plan2[ "indexBounds" ].Obj() );
+ }
+ };
+
+ class SimpleCount : public Base {
+ virtual void handleCursor() {
+ while( _cursor->ok() ) {
+ MatchDetails matchDetails;
+ if ( _cursor->currentMatches( &matchDetails ) &&
+ !_cursor->getsetdup( _cursor->currLoc() ) ) {
+ _cursor->noteIterate( true, true, false );
+ }
+ else {
+ _cursor->noteIterate( false, matchDetails.hasLoadedRecord(), false );
+ }
+ _cursor->advance();
+ }
+ }
+ virtual void checkExplain() {
+ ASSERT_EQUALS( "BtreeCursor a_1", _explain[ "cursor" ].String() );
+ ASSERT_EQUALS( 1, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 2, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 2, _explain[ "nscannedAllPlans" ].number() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( 1, plan1[ "n" ].number() );
+ ASSERT_EQUALS( 1, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan1[ "nscanned" ].number() );
+
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( 1, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 1, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan2[ "nscanned" ].number() );
+ }
+ };
+
+ class IterateOnly : public Base {
+ virtual BSONObj query() const { return BSON( "a" << GT << 0 << "b" << 1 ); }
+ virtual void handleCursor() {
+ while( _cursor->ok() ) {
+ _cursor->advance();
+ }
+ }
+ virtual void checkExplain() {
+ ASSERT_EQUALS( "BtreeCursor a_1", _explain[ "cursor" ].String() );
+ ASSERT_EQUALS( 0, _explain[ "n" ].number() ); // needs to be set with noteIterate()
+ ASSERT_EQUALS( 0, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 3, _explain[ "nscannedAllPlans" ].number() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( 2, plan1[ "n" ].number() );
+ ASSERT_EQUALS( 2, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 2, plan1[ "nscanned" ].number() );
+
+ // Not fully incremented without checking for matches.
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( 1, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 1, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan2[ "nscanned" ].number() );
+ }
+ };
+
+ class ExtraMatchChecks : public Base {
+ virtual BSONObj query() const { return BSON( "a" << GT << 0 << "b" << 1 ); }
+ virtual void handleCursor() {
+ while( _cursor->ok() ) {
+ _cursor->currentMatches();
+ _cursor->currentMatches();
+ _cursor->currentMatches();
+ _cursor->advance();
+ }
+ }
+ virtual void checkExplain() {
+ ASSERT_EQUALS( "BtreeCursor a_1", _explain[ "cursor" ].String() );
+ ASSERT_EQUALS( 0, _explain[ "n" ].number() ); // needs to be set with noteIterate()
+ ASSERT_EQUALS( 0, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 4, _explain[ "nscannedAllPlans" ].number() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( 2, plan1[ "n" ].number() );
+ // nscannedObjects are not deduped.
+ ASSERT_EQUALS( 6, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 2, plan1[ "nscanned" ].number() );
+
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( 2, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 6, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 2, plan2[ "nscanned" ].number() );
+ }
+ };
+
+ class PartialIteration : public Base {
+ virtual void handleCursor() {
+ _cursor->currentMatches();
+ _cursor->advance();
+ _cursor->noteIterate( true, true, false );
+ }
+ virtual void checkExplain() {
+ ASSERT_EQUALS( "BtreeCursor a_1", _explain[ "cursor" ].String() );
+ ASSERT_EQUALS( 1, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 1, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 2, _explain[ "nscannedAllPlans" ].number() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( 1, plan1[ "n" ].number() );
+ ASSERT_EQUALS( 1, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan1[ "nscanned" ].number() );
+
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( 0, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 0, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 1, plan2[ "nscanned" ].number() );
+ }
+ };
+
+ class Multikey : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 1 << 2 ) ) );
+ }
+ virtual void handleCursor() {
+ while( _cursor->advance() );
+ }
+ virtual void checkExplain() {
+ ASSERT( _explain[ "isMultiKey" ].Bool() );
+ }
+ };
+
+ class MultikeyInitial : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 1 << 2 ) ) );
+ }
+ virtual void checkExplain() {
+ ASSERT( _explain[ "isMultiKey" ].Bool() );
+ }
+ };
+
+ class BecomesMultikey : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ }
+ virtual void checkExplain() {
+ ASSERT( !_explain[ "isMultiKey" ].Bool() );
+
+ _cursor->prepareToYield();
+ {
+ dbtemprelease t;
+ _cli.insert( ns(), BSON( "a" << BSON_ARRAY( 1 << 2 ) ) );
+ }
+ _cursor->recoverFromYield();
+ _cursor->currentMatches();
+ ASSERT( _explainInfo->bson()[ "isMultiKey" ].Bool() );
+ }
+ };
+
+ class CountAndYield : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 5; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ }
+ }
+ virtual void handleCursor() {
+ _nYields = 0;
+ while( _cursor->ok() ) {
+ _cursor->prepareToYield();
+ ++_nYields;
+ _cursor->recoverFromYield();
+ _cursor->noteYield();
+ MatchDetails matchDetails;
+ if ( _cursor->currentMatches( &matchDetails ) &&
+ !_cursor->getsetdup( _cursor->currLoc() ) ) {
+ _cursor->noteIterate( true, true, false );
+ }
+ else {
+ _cursor->noteIterate( false, matchDetails.hasLoadedRecord(), false );
+ }
+ _cursor->advance();
+ }
+ }
+ virtual void checkExplain() {
+ ASSERT( _nYields > 0 );
+ ASSERT_EQUALS( _nYields, _explain[ "nYields" ].Int() );
+
+ ASSERT_EQUALS( "BtreeCursor a_1", _explain[ "cursor" ].String() );
+ ASSERT_EQUALS( 5, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 10, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 10, _explain[ "nscannedAllPlans" ].number() );
+
+ BSONObj plan1 = _explain[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( 5, plan1[ "n" ].number() );
+ ASSERT_EQUALS( 5, plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 5, plan1[ "nscanned" ].number() );
+
+ BSONObj plan2 = _explain[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( 5, plan2[ "n" ].number() );
+ ASSERT_EQUALS( 5, plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 5, plan2[ "nscanned" ].number() );
+ }
+ protected:
+ int _nYields;
+ };
+
+ class MultipleClauses : public CountAndYield {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ for( int i = 0; i < 4; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ }
+ _cli.insert( ns(), BSON( "a" << 0 << "b" << 1 ) );
+ }
+ virtual BSONObj query() const { return fromjson( "{$or:[{a:1,c:null},{b:1,c:null}]}"); }
+ virtual void checkExplain() {
+ ASSERT_EQUALS( 18, _nYields );
+
+ ASSERT_EQUALS( 5, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 18, _explain[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 18, _explain[ "nscannedAllPlans" ].number() );
+
+ BSONObj clause1 = _explain[ "clauses" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( "BtreeCursor a_1", clause1[ "cursor" ].String() );
+ ASSERT_EQUALS( 4, clause1[ "n" ].number() );
+ ASSERT_EQUALS( 8, clause1[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 8, clause1[ "nscannedAllPlans" ].number() );
+ ASSERT_EQUALS( 8, clause1[ "nYields" ].Int() );
+
+ BSONObj c1plan1 = clause1[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( "BtreeCursor a_1", c1plan1[ "cursor" ].String() );
+ ASSERT_EQUALS( 4, c1plan1[ "n" ].number() );
+ ASSERT_EQUALS( 4, c1plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 4, c1plan1[ "nscanned" ].number() );
+
+ BSONObj c1plan2 = clause1[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( "BasicCursor", c1plan2[ "cursor" ].String() );
+ ASSERT_EQUALS( 4, c1plan2[ "n" ].number() );
+ ASSERT_EQUALS( 4, c1plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 4, c1plan2[ "nscanned" ].number() );
+
+ BSONObj clause2 = _explain[ "clauses" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( "BtreeCursor b_1", clause2[ "cursor" ].String() );
+ ASSERT_EQUALS( 1, clause2[ "n" ].number() );
+ ASSERT_EQUALS( 10, clause2[ "nscannedObjectsAllPlans" ].number() );
+ ASSERT_EQUALS( 10, clause2[ "nscannedAllPlans" ].number() );
+ ASSERT_EQUALS( 10, clause2[ "nYields" ].Int() );
+
+ BSONObj c2plan1 = clause2[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( "BtreeCursor b_1", c2plan1[ "cursor" ].String() );
+ ASSERT_EQUALS( 5, c2plan1[ "n" ].number() );
+ ASSERT_EQUALS( 5, c2plan1[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 5, c2plan1[ "nscanned" ].number() );
+
+ BSONObj c2plan2 = clause2[ "allPlans" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( "BasicCursor", c2plan2[ "cursor" ].String() );
+ ASSERT_EQUALS( 5, c2plan2[ "n" ].number() );
+ ASSERT_EQUALS( 5, c2plan2[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 5, c2plan2[ "nscanned" ].number() );
+ }
+ };
+
+ class MultiCursorTakeover : public CountAndYield {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 20; i >= 1; --i ) {
+ for( int j = 0; j < i; ++j ) {
+ _cli.insert( ns(), BSON( "a" << i ) );
+ }
+ }
+ }
+ virtual BSONObj query() const {
+ BSONArrayBuilder bab;
+ for( int i = 20; i >= 1; --i ) {
+ bab << BSON( "a" << i );
+ }
+ return BSON( "$or" << bab.arr() );
+ }
+ virtual void checkExplain() {
+ ASSERT_EQUALS( 20U, _explain[ "clauses" ].Array().size() );
+ for( int i = 20; i >= 1; --i ) {
+ BSONObj clause = _explain[ "clauses" ].Array()[ 20-i ].Obj();
+ ASSERT_EQUALS( "BtreeCursor a_1", clause[ "cursor" ].String() );
+ ASSERT_EQUALS( BSON( "a" << BSON_ARRAY( BSON_ARRAY( i << i ) ) ),
+ clause[ "indexBounds" ].Obj() );
+ ASSERT_EQUALS( i, clause[ "n" ].number() );
+ ASSERT_EQUALS( i, clause[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( i, clause[ "nscanned" ].number() );
+ ASSERT_EQUALS( i, clause[ "nYields" ].Int() );
+
+ ASSERT_EQUALS( 1U, clause[ "allPlans" ].Array().size() );
+ BSONObj plan = clause[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( i, plan[ "n" ].number() );
+ ASSERT_EQUALS( i, plan[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( i, plan[ "nscanned" ].number() );
+ }
+
+ ASSERT_EQUALS( 210, _explain[ "n" ].number() );
+ ASSERT_EQUALS( 210, _explain[ "nscannedObjects" ].number() );
+ ASSERT_EQUALS( 210, _explain[ "nscanned" ].number() );
+ }
+ };
+
+ class NChunkSkipsTakeover : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 200; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ }
+ for( int i = 0; i < 200; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 2 << "b" << 2 ) );
+ }
+ }
+ virtual BSONObj query() const { return fromjson( "{$or:[{a:1,b:1},{a:2,b:2}]}" ); }
+ virtual void handleCursor() {
+ ASSERT_EQUALS( "QueryOptimizerCursor", _cursor->toString() );
+ int i = 0;
+ while( _cursor->ok() ) {
+ if ( _cursor->currentMatches() && !_cursor->getsetdup( _cursor->currLoc() ) ) {
+ _cursor->noteIterate( true, true, i++ %2 == 0 );
+ }
+ _cursor->advance();
+ }
+ }
+ virtual void checkExplain() {
+ // Historically, nChunkSkips has been excluded from the query summary.
+ ASSERT( _explain[ "nChunkSkips" ].eoo() );
+
+ BSONObj clause0 = _explain[ "clauses" ].Array()[ 0 ].Obj();
+ ASSERT_EQUALS( 100, clause0[ "nChunkSkips" ].number() );
+ BSONObj plan0 = clause0[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT( plan0[ "nChunkSkips" ].eoo() );
+
+ BSONObj clause1 = _explain[ "clauses" ].Array()[ 1 ].Obj();
+ ASSERT_EQUALS( 100, clause1[ "nChunkSkips" ].number() );
+ BSONObj plan1 = clause1[ "allPlans" ].Array()[ 0 ].Obj();
+ ASSERT( plan1[ "nChunkSkips" ].eoo() );
+ }
+ };
+
+ class CoveredIndex : public Base {
+ virtual BSONObj query() const { return fromjson( "{$or:[{a:1},{a:2}]}" ); }
+ virtual BSONObj fields() const { return BSON( "_id" << 0 << "a" << 1 ); }
+ virtual void handleCursor() {
+ ASSERT_EQUALS( "QueryOptimizerCursor", _cursor->toString() );
+ while( _cursor->advance() );
+ }
+ virtual void checkExplain() {
+ BSONObj clause0 = _explain[ "clauses" ].Array()[ 0 ].Obj();
+ ASSERT( clause0[ "indexOnly" ].Bool() );
+
+ BSONObj clause1 = _explain[ "clauses" ].Array()[ 1 ].Obj();
+ ASSERT( clause1[ "indexOnly" ].Bool() );
+ }
+ };
+
+ class CoveredIndexTakeover : public Base {
+ virtual void setupCollection() {
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ for( int i = 0; i < 150; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 ) );
+ }
+ _cli.insert( ns(), BSON( "a" << 2 ) );
+ }
+ virtual BSONObj query() const { return fromjson( "{$or:[{a:1},{a:2}]}" ); }
+ virtual BSONObj fields() const { return BSON( "_id" << 0 << "a" << 1 ); }
+ virtual void handleCursor() {
+ ASSERT_EQUALS( "QueryOptimizerCursor", _cursor->toString() );
+ while( _cursor->advance() );
+ }
+ virtual void checkExplain() {
+ BSONObj clause0 = _explain[ "clauses" ].Array()[ 0 ].Obj();
+ ASSERT( clause0[ "indexOnly" ].Bool() );
+
+ BSONObj clause1 = _explain[ "clauses" ].Array()[ 1 ].Obj();
+ ASSERT( clause1[ "indexOnly" ].Bool() );
+ }
+ };
+
+ /**
+ * Check that the plan with the most matches is reported at the top of the explain output
+ * in the absence of a done or picked plan.
+ */
+ class VirtualPickedPlan : public Base {
+ public:
+ void run() {
+ Client::WriteContext ctx( ns() );
+
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "b" << 1 ) );
+ _cli.ensureIndex( ns(), BSON( "c" << 1 ) );
+
+ shared_ptr<Cursor> aCursor
+ ( NamespaceDetailsTransient::getCursor( ns(), BSON( "a" << 1 ) ) );
+ shared_ptr<Cursor> bCursor
+ ( NamespaceDetailsTransient::getCursor( ns(), BSON( "b" << 1 ) ) );
+ shared_ptr<Cursor> cCursor
+ ( NamespaceDetailsTransient::getCursor( ns(), BSON( "c" << 1 ) ) );
+
+ shared_ptr<ExplainPlanInfo> aPlan( new ExplainPlanInfo() );
+ aPlan->notePlan( *aCursor, false, false );
+ shared_ptr<ExplainPlanInfo> bPlan( new ExplainPlanInfo() );
+ bPlan->notePlan( *bCursor, false, false );
+ shared_ptr<ExplainPlanInfo> cPlan( new ExplainPlanInfo() );
+ cPlan->notePlan( *cCursor, false, false );
+
+ aPlan->noteIterate( true, false, *aCursor ); // one match a
+ bPlan->noteIterate( true, false, *bCursor ); // two matches b
+ bPlan->noteIterate( true, false, *bCursor );
+ cPlan->noteIterate( true, false, *cCursor ); // one match c
+
+ shared_ptr<ExplainClauseInfo> clause( new ExplainClauseInfo() );
+ clause->addPlanInfo( aPlan );
+ clause->addPlanInfo( bPlan );
+ clause->addPlanInfo( cPlan );
+
+ ASSERT_EQUALS( "BtreeCursor b_1", clause->bson()[ "cursor" ].String() );
+ }
+ };
+
+ /** Simple check that an explain result can contain a large value of n. */
+ class LargeN : public Base {
+ public:
+ void run() {
+ Lock::GlobalWrite lk;
+
+ Client::Context ctx( ns() );
+
+ shared_ptr<Cursor> cursor
+ ( NamespaceDetailsTransient::getCursor( ns(), BSONObj() ) );
+ ExplainSinglePlanQueryInfo explainHelper;
+ explainHelper.notePlan( *cursor, false, false );
+ explainHelper.noteIterate( false, false, false, *cursor );
+
+ shared_ptr<ExplainQueryInfo> explain = explainHelper.queryInfo();
+ explain->reviseN( 3000000000000LL );
+
+ ASSERT_EQUALS( 3000000000000LL, explain->bson()[ "n" ].Long() );
+ }
+ };
+
+ /**
+ * Base class to test yield count when a cursor yields and its current iterate is deleted.
+ */
+ class NYieldsAdvanceBase : public Base {
+ protected:
+ virtual int aValueToDelete() const = 0;
+ virtual int numDocuments() const {
+ // Above the takeover threshold at 101 matches.
+ return 200;
+ }
+ private:
+ virtual void setupCollection() {
+ // Insert some matching documents.
+ for( int i = 0; i < numDocuments(); ++i ) {
+ _cli.insert( ns(), BSON( "a" << i << "b" << 0 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ virtual BSONObj query() const {
+ // A query that will match all inserted documents and generate a
+ // QueryOptimizerCursor with a single candidate plan, on the a:1 index.
+ return BSON( "a" << GTE << 0 << "b" << 0 );
+ }
+ virtual void handleCursor() {
+ _clientCursor.reset
+ ( new mongo::ClientCursor( QueryOption_NoCursorTimeout, _cursor,
+ ns() ) );
+ // Advance to the document with the specified 'a' value.
+ while( _cursor->current()[ "a" ].number() != aValueToDelete() ) {
+ _cursor->advance();
+ }
+
+ // Yield the cursor.
+ mongo::ClientCursor::YieldData yieldData;
+ _clientCursor->prepareToYield( yieldData );
+
+ // Remove the document with the specified 'a' value.
+ _cli.remove( ns(), BSON( "a" << aValueToDelete() ) );
+
+ // Recover the cursor and note that a yield occurred.
+ _clientCursor->recoverFromYield( yieldData );
+ _cursor->noteYield();
+
+ // Exhaust the cursor (not strictly necessary).
+ while( _cursor->advance() );
+ }
+ virtual void checkExplain() {
+ // The cursor was yielded once.
+ ASSERT_EQUALS( 1, _explain[ "nYields" ].number() );
+ }
+ mongo::ClientCursor::Holder _clientCursor;
+ };
+
+ /** nYields reporting of a QueryOptimizerCursor before it enters takeover mode. */
+ class NYieldsAdvanceBasic : public NYieldsAdvanceBase {
+ virtual int aValueToDelete() const {
+ // Before the MutiCursor takes over at 101 matches.
+ return 50;
+ }
+ };
+
+ /** nYields reporting of a QueryOptimizerCursor after it enters takeover mode. */
+ class NYieldsAdvanceTakeover : public NYieldsAdvanceBase {
+ virtual int aValueToDelete() const { return 150; }
+ };
+
+ /** nYields reporting when a cursor is exhausted during a yield. */
+ class NYieldsAdvanceDeleteLast : public NYieldsAdvanceBase {
+ virtual int aValueToDelete() const { return 0; }
+ virtual int numDocuments() const { return 1; }
+ };
+
+ /** nYields reporting when a takeover cursor is exhausted during a yield. */
+ class NYieldsAdvanceDeleteLastTakeover : public NYieldsAdvanceBase {
+ virtual int aValueToDelete() const { return 199; }
+ };
+
+ /** Explain reporting on yield recovery failure. */
+ class YieldRecoveryFailure : public Base {
+ virtual void setupCollection() {
+ // Insert some matching documents.
+ for( int i = 0; i < 10; ++i ) {
+ _cli.insert( ns(), BSON( "a" << 1 << "b" << 1 ) );
+ }
+ _cli.ensureIndex( ns(), BSON( "a" << 1 ) );
+ }
+ virtual void handleCursor() {
+ _clientCursor.reset
+ ( new mongo::ClientCursor( QueryOption_NoCursorTimeout, _cursor,
+ ns() ) );
+
+ // Scan 5 matches.
+ int numMatches = 0;
+ while( numMatches < 5 ) {
+ if ( _cursor->currentMatches() && !_cursor->getsetdup( _cursor->currLoc() ) ) {
+ _cursor->noteIterate( true, true, false );
+ ++numMatches;
+ }
+ _cursor->noteIterate( false, true, false );
+ _cursor->advance();
+ }
+
+ // Yield the cursor.
+ mongo::ClientCursor::YieldData yieldData;
+ _clientCursor->prepareToYield( yieldData );
+
+ // Drop the collection and invalidate the ClientCursor.
+ {
+ dbtemprelease release;
+ _cli.dropCollection( ns() );
+ }
+ ASSERT( !_clientCursor->recoverFromYield( yieldData ) );
+
+ // Note a yield after the ClientCursor was invalidated.
+ _cursor->noteYield();
+ }
+ virtual void checkExplain() {
+ // The cursor was yielded once.
+ ASSERT_EQUALS( 1, _explain[ "nYields" ].number() );
+ // Five matches were scanned.
+ ASSERT_EQUALS( 5, _explain[ "n" ].number() );
+ // Matches were identified for each plan.
+ BSONObjIterator plans( _explain[ "allPlans" ].embeddedObject() );
+ while( plans.more() ) {
+ ASSERT( 0 < plans.next().Obj()[ "n" ].number() );
+ }
+ }
+ mongo::ClientCursor::Holder _clientCursor;
+ };
+
+ } // namespace Explain
+
+ class All : public Suite {
+ public:
+ All() : Suite( "queryoptimizercursor" ) {}
+
+ void setupTests() {
+ __forceLinkGeoPlugin();
+ add<CachedMatchCounter::Count>();
+ add<CachedMatchCounter::Accumulate>();
+ add<CachedMatchCounter::Dedup>();
+ add<CachedMatchCounter::Nscanned>();
+ add<SmallDupSet::Upgrade>();
+ add<SmallDupSet::UpgradeRead>();
+ add<SmallDupSet::UpgradeWrite>();
+ add<DurationTimerStop>();
+ add<Empty>();
+ add<Unindexed>();
+ add<Basic>();
+ add<NoMatch>();
+ add<Interleaved>();
+ add<NotMatch>();
+ add<StopInterleaving>();
+ add<TakeoverWithDup>();
+ add<TakeoverWithNonMatches>();
+ add<TakeoverWithTakeoverDup>();
+ add<BasicOr>();
+ add<OrFirstClauseEmpty>();
+ add<OrSecondClauseEmpty>();
+ add<OrMultipleClausesEmpty>();
+ add<TakeoverCountOr>();
+ add<TakeoverEndOfOrClause>();
+ add<TakeoverBeforeEndOfOrClause>();
+ add<TakeoverAfterEndOfOrClause>();
+ add<ManualMatchingDeduping>();
+ add<ManualMatchingUsingCurrKey>();
+ add<ManualMatchingDedupingTakeover>();
+ add<Singlekey>();
+ add<Multikey>();
+ add<AddOtherPlans>();
+ add<AddOtherPlansDelete>();
+ add<AddOtherPlansContinuousDelete>();
+ add<AddOtherPlansWhenOptimalBecomesNonOptimal>();
+ add<OrRangeElimination>();
+ add<OrDedup>();
+ add<EarlyDups>();
+ add<OrPopInTakeover>();
+ add<OrCollectionScanAbort>();
+ add<Yield::NoOp>();
+ add<Yield::Delete>();
+ add<Yield::DeleteContinue>();
+ add<Yield::DeleteContinueFurther>();
+ add<Yield::Update>();
+ add<Yield::Drop>();
+ add<Yield::DropOr>();
+ add<Yield::RemoveOr>();
+ add<Yield::CappedOverwrite>();
+ add<Yield::DropIndex>();
+ add<Yield::MultiplePlansNoOp>();
+ add<Yield::MultiplePlansAdvanceNoOp>();
+ add<Yield::MultiplePlansDelete>();
+ add<Yield::MultiplePlansDeleteOr>();
+ add<Yield::MultiplePlansDeleteOrAdvance>();
+ add<Yield::MultiplePlansCappedOverwrite>();
+ add<Yield::MultiplePlansCappedOverwriteManual>();
+ add<Yield::MultiplePlansCappedOverwriteManual2>();
+ add<Yield::Takeover>();
+ add<Yield::TakeoverBasic>();
+ add<Yield::InactiveCursorAdvance>();
+ add<Yield::TakeoverUpdateKeyAtEndOfIteration>();
+ add<Yield::TakeoverUpdateKeyAtEndOfClause>();
+ add<Yield::TakeoverUpdateKeyPrecedingEmptyClause>();
+ add<OrderId>();
+ add<OrderMultiIndex>();
+ add<OrderReject>();
+ add<OrderNatural>();
+ add<OrderUnindexed>();
+ add<RecordedOrderInvalid>();
+ add<KillOp>();
+ add<KillOpFirstClause>();
+ add<Nscanned>();
+ add<TouchEarlierIterate::Basic>();
+ add<TouchEarlierIterate::Delete>();
+ add<TouchEarlierIterate::DeleteMultiple>();
+ add<TouchEarlierIterate::DeleteAfterYield>();
+ add<TouchEarlierIterate::Takeover>();
+ add<TouchEarlierIterate::TakeoverDeleteMultiple>();
+ add<TouchEarlierIterate::UnindexedTakeoverDeleteMultiple>();
+ add<TouchEarlierIterate::TakeoverDeleteMultipleMultiAdvance>();
+ add<InitialCappedWrapYieldRecoveryFailure>();
+ add<TakeoverCappedWrapYieldRecoveryFailure>();
+ add<ClientCursor::Invalidate>();
+ add<ClientCursor::Timeout>();
+ add<ClientCursor::AboutToDeleteRecoverFromYield>();
+ add<ClientCursor::AboutToDeletePrepareToYield>();
+ add<ClientCursor::Drop>();
+ add<AllowOutOfOrderPlan>();
+ add<NoTakeoverByOutOfOrderPlan>();
+ add<OutOfOrderOnlyTakeover>();
+ add<CoveredIndex>();
+ add<CoveredIndexTakeover>();
+ add<SaveGoodIndex>();
+ add<PossibleInOrderPlans>();
+ add<PossibleOutOfOrderPlans>();
+ add<PossibleBothPlans>();
+ add<AbortOutOfOrderPlans>();
+ add<AbortOutOfOrderPlanOnLastMatch>();
+ add<AbortOutOfOrderPlansBeforeAddOtherPlans>();
+ add<TakeoverOrRangeElimination>();
+ add<TakeoverOrDedups>();
+ add<TakeoverOrDifferentIndex>();
+ add<TakeoverOrderedPlanDupsOutOfOrderPlan>();
+ add<ElemMatchKey>();
+ add<GetCursor::NoConstraints>();
+ add<GetCursor::SimpleId>();
+ add<GetCursor::OptimalIndex>();
+ add<GetCursor::SimpleKeyMatch>();
+ add<GetCursor::Geo>();
+ add<GetCursor::GeoNumWanted>();
+ add<GetCursor::PreventOutOfOrderPlan>();
+ add<GetCursor::AllowOutOfOrderPlan>();
+ add<GetCursor::BestSavedOutOfOrder>();
+ add<GetCursor::BestSavedOptimal>();
+ add<GetCursor::BestSavedNotOptimal>();
+ add<GetCursor::MultiIndex>();
+ add<GetCursor::Hint>();
+ add<GetCursor::Snapshot>();
+ add<GetCursor::SnapshotCappedColl>();
+ add<GetCursor::Min>();
+ add<GetCursor::Max>();
+ add<GetCursor::RequireIndex::NoConstraints>();
+ add<GetCursor::RequireIndex::SimpleId>();
+ add<GetCursor::RequireIndex::UnindexedQuery>();
+ add<GetCursor::RequireIndex::IndexedQuery>();
+ add<GetCursor::RequireIndex::SecondOrClauseIndexed>();
+ add<GetCursor::RequireIndex::SecondOrClauseUnindexed>();
+ add<GetCursor::RequireIndex::SecondOrClauseUnindexedUndetected>();
+ add<GetCursor::RequireIndex::RecordedUnindexedPlan>();
+ add<GetCursor::IdElseNatural::AllowOptimalNaturalPlan>();
+ add<GetCursor::IdElseNatural::AllowOptimalIdPlan>();
+ add<GetCursor::IdElseNatural::HintedIdForQuery>( BSON( "_id" << 1 ) );
+ add<GetCursor::IdElseNatural::HintedIdForQuery>( BSON( "a" << 1 ) );
+ add<GetCursor::IdElseNatural::HintedIdForQuery>( BSON( "_id" << 1 << "a" << 1 ) );
+ add<GetCursor::IdElseNatural::HintedNaturalForQuery>( BSONObj() );
+ // now capped collections have _id index by default, so skip these
+ //add<GetCursor::IdElseNatural::HintedNaturalForQuery>( BSON( "_id" << 1 ) );
+ //add<GetCursor::IdElseNatural::HintedNaturalForQuery>( BSON( "a" << 1 ) );
+ //add<GetCursor::IdElseNatural::HintedNaturalForQuery>( BSON( "_id" << 1 << "a" << 1 ) );
+ add<GetCursor::MatcherValidation>();
+ add<Explain::ClearRecordedIndex>();
+ add<Explain::Initial>();
+ add<Explain::Empty>();
+ add<Explain::SimpleCount>();
+ add<Explain::IterateOnly>();
+ add<Explain::ExtraMatchChecks>();
+ add<Explain::PartialIteration>();
+ add<Explain::Multikey>();
+ add<Explain::MultikeyInitial>();
+ add<Explain::BecomesMultikey>();
+ add<Explain::CountAndYield>();
+ add<Explain::MultipleClauses>();
+ add<Explain::MultiCursorTakeover>();
+ add<Explain::NChunkSkipsTakeover>();
+ add<Explain::CoveredIndex>();
+ add<Explain::CoveredIndexTakeover>();
+ add<Explain::VirtualPickedPlan>();
+ add<Explain::LargeN>();
+ add<Explain::NYieldsAdvanceBasic>();
+ add<Explain::NYieldsAdvanceTakeover>();
+ add<Explain::NYieldsAdvanceDeleteLast>();
+ add<Explain::NYieldsAdvanceDeleteLastTakeover>();
+ add<Explain::YieldRecoveryFailure>();
+ }
+ } myall;
+
+} // namespace QueryOptimizerTests
+