Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-8172

a query field with a $not operator should not be used to populate an upsert document

    • Fully Compatible
    • ALL
    • Hide

      db.test.update({_id: 1, r: {$not: {$elemMatch: {"a": 1}}}}, {$push: {r:

      {"a": 1, "e": 0}

      }},

      {upsert: true}

      );

      Show
      db.test.update({_id: 1, r: {$not: {$elemMatch: {"a": 1}}}}, {$push: {r: {"a": 1, "e": 0} }}, {upsert: true} );

      In 2.0.8, createNewFromQuery performs the following check to see if a
      query field represents a value that should be incorporated in the
      upsert document, or an operator:

      if ( e.type() == Object && e.embeddedObject().firstElementFieldName()[0] == '$' ) {
          // this means this is a $gt type filter, so don't make part of the new object continue;
      }
      

      In 2.2.2, that check became the following:

      if ( e.type() == Object && e.embeddedObject().firstElementFieldName()[0] == '$' ) {
          // we have something like { x : { $gt : 5 } }
          // this can be a query piece
          // or can be a dbref or something
       
          int op = e.embeddedObject().firstElement().getGtLtOp( -1 );
          if ( op >= 0 ) {
              // this means this is a $gt type filter, so don't make part of the new object continue;
          }
      }
      

      Now we only consider the object field to represent an operator if
      getGtLtOp() can interpret the field name as an operator. However,
      this is not a complete check because getGtLtOp() only understands
      simple field-value operators. It does not parse $not (or $and, etc.).
      This means that the {$not: {$elemMatch:

      {"a": 1}

      }} portion of the
      query is interpreted by the upsert implementation a value, not an
      operator. The upserted document starts with a value of the 'r' field
      of {$not: {$elemMatch:

      {"a": 1}

      }}. $push cannot operate on this value
      because it is not an array, which causes the error the user reported.

      Aaron

      ------------------------------------

      In 2.2.x $elemMatch breaks $push when used inside of $not and with an upsert. This is useful is you want to push only when an existing property doesn't exist inside of an array.

      An update in 2.0 such as:
      db.test.update({_id: 1, r: {$not: {$elemMatch: {"a": 1}}}}, {$push: {r:

      {"a": 1, "e": 0}

      }},

      {upsert: true}

      );
      or even:
      db.test.update({_id: 1, r: {$not: {$elemMatch: {$or: [

      {"a": 1}

      ,

      {"e": 1}

      ]}}}}, {$push: {r:

      {"a": 1, "e": 0}

      }},

      {upsert: true}

      );
      Would successfully create a new document:
      { "_id" : 1, "r" : [

      { "a" : 1, "e" : 0 }

      ] }

      If you called either of those updates again, it would expectantly throw a 11000 error:
      E11000 duplicate key error index: test.test.$id dup key: { : "hello" }
      Which meant that there already was a value inside "r" with either

      {"a": 1}

      or

      {"e": 1}

      .

      Now in 2.2 either of those updates fails with error:
      Cannot apply $push/$pushAll modifier to non-array

      There was a proposed workaround (using $nin): http://stackoverflow.com/questions/12391334/mongodb-upsert-with-push-and-not
      But that doesn't actually work. The $nin fails to actually look inside the array, which is expected as it wouldn't seem that it would anyways.

            Assignee:
            aaron Aaron Staple
            Reporter:
            fastest963 James Hartig
            Votes:
            1 Vote for this issue
            Watchers:
            7 Start watching this issue

              Created:
              Updated:
              Resolved: