Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DiscriminatorMap field values injected too often #250

Closed
AD7six opened this issue Feb 8, 2012 · 20 comments
Closed

DiscriminatorMap field values injected too often #250

AD7six opened this issue Feb 8, 2012 · 20 comments

Comments

@AD7six
Copy link
Contributor

AD7six commented Feb 8, 2012

Using a document with the following annotations:

/**
* @MongoDB\Document(collection="users")
* @MongoDB\InheritanceType("SINGLE_COLLECTION")
* @MongoDB\DiscriminatorField(fieldName="type")
* @MongoDB\DiscriminatorMap({"user"="User", "brand"="Brand", "merchant"="Merchant"})
*/
class User {

And code like so:

    $return = $repository->createQueryBuider() 
        ->update()
        ->field('_id')->equals(new \MongoId($userId))
        ->field('type')->set($type)
        ->getQuery()
        ->execute();

Executes a query like so (profiler output):

       {
         "ts" : ISODate("2012-02-08T10:18:29.076Z"), 
         "op" : "update", "ns" : "mydb.users", 
         "query" : {
             "_id" : ObjectId("4efa6a7a411e208429000007"), 
             "type" : { "$in" : [ "user", "brand", "merchant" ] }
         }, 
         "updateobj" : {
             "$set" : { "type" : "brand" }, 
             **"type" : { "$in" : [ "user", "brand", "merchant" ] }**
         }, 
         "exception" : "assertion db/ops/update.h:315", 
         "millis" : 1, 
         "client" : "127.0.0.1", 
         "user" : ""
       }

expected results would be a query ala:

db.users.update(
    {_id: ObjectId("4efa6a7a411e208429000007"), <optional autoinjected type condition>},
    {$set: {type: "brand"}}
) 

The problem isn't restricted to updates, any query which does /not/ have the discriminatory field as a top-level array key will have it auto-injected generating invalid syntax, for example:

    $qb = $repository->createQueryBuider()
    $return = $qb
            ->addOr($qb->expr()->field('email')->equals($input))   
            ->addOr($qb->expr()->field('username')->equals($input))   
            ->addOr($qb->expr()->field('name')->equals($input))   
            ->getQuery()
            ->getSingleResult();

Will execute a query like so (profiler output):

{   
    "ts" : ISODate("2012-02-08T15:09:31.452Z"),
    "op" : "query",
    "ns" : "mydb.users",
    "query" : {
        "$query" : {
            "$or" : {
                "0" : {
                    "email" : "foo@bar.com",
                    "type" : { "$in" : [ "user", "brand", "merchant" ] }
                },
                "1" : {
                    "username" : "foo@bar.com",
                     "type" : { "$in" : [ "user", "brand", "merchant" ] }
                },
                "2" : {
                    "name" : "foo@bar.com",
                     "type" : { "$in" : [ "user", "brand", "merchant" ] }
                },
                **"type" : { "$in" : [ "user", "brand", "merchant" ] }**
            },
             "type" : { "$in" : [ "user", "brand", "merchant" ] }
        },
        "$orderby" : [ ]
    },
    "ntoreturn" : 1,
    "exception" : "$or requires nonempty array",
    "exceptionCode" : 13262,
    "responseLength" : 73,
    "millis" : 0,
    "client" : "127.0.0.1",
    "user" : ""
}

expected query conditions would be:

{
    $or: [{email: "foo@bar.com"}, {username: "foo@bar.com"}, {name: "foo@bar.com"}],
    <autoinjected type condition>
}
@jmikola
Copy link
Member

jmikola commented Feb 8, 2012

Is the type field above both a mapped field and a discriminator? IIRC, an exception is thrown if you attempt to do so, so I assume not.

When working through a recent issue with discriminator fields, I spoke to @jwage about mapping a discriminator field as a PHP property in my document model (in order to have a getter for it). I believe he said it's purposely not hydrated and cannot be used as a property-mapped field.

I'm not sure you can refer to the discriminator field in a query, though. From your example:

   $return = $repository->createQueryBuider() 
        ->update()
        ->field('_id')->equals(new \MongoId($userId))
        ->field('type')->set($type)
        ->getQuery()
        ->execute();

I believe it's only to be set after inferring the class name against the discriminator map. That could explain the odd behavior here. If it's not to be supported, perhaps ODM needs to throw an exception to be explicit.

@AD7six
Copy link
Contributor Author

AD7six commented Feb 8, 2012

I'm not clear what your question regarding mapped fields means - there is no "map" except DiscriminatorMap in any of the classes related to the User document being used (User, Brand, Merchant).

Granted, updating a discriminatory field is probably an edge case. The first query in the ticket is purely because the User object deliberately doesn't have a setter for this property (but I need to update it).

However, it doesn't matter what field is in the update query, e.g. attempting to update name with the same syntax as above yields:

 {   
     "ts" : ISODate("2012-02-08T21:22:28.415Z"), 
     "op" : "update", 
     "ns" : "mydb.users", 
     "query" : { 
         "_id" : ObjectId("4efacf06411e206706000004"), 
         "type" : { "$in" : [ "user", "brand", "merchant" ] } 
     },  
     "updateobj" : { 
         "$set" : { 
             "name" : "foo", 
             "type" : { "$in" : [ "user", "brand", "merchant" ] } 
         },  
         "type" : { "$in" : [ "user", "brand", "merchant" ] } 
     },  
     "exception" : "assertion db/ops/update.h:315",
     "millis" : 1,
     "client" : "127.0.0.1",
     "user" : ""
 }

And in any event, criteria which contain an $operator should not IMO be modified at all (e.g. $not: { ... <autoinjected type condition>} - totally undesirable), and obviously not update objects either.

I did take a look at what changes would be required but it looks like a none trivial change to prevent this as and I don't want to waste your time providing a patch that is useless (since I'm not familiar with the doctrine code).

@jmikola
Copy link
Member

jmikola commented Feb 8, 2012

By mapped field I meant something like this:

/** @MongoDB\String */
protected $type;

That was something I was playing around with two weeks ago, and it resulted in an exception because type was already registered (i.e. mapped) in the class metadata as a discriminator field. And even without the annotation, Doctrine will not hydrate a property corresponding to the discriminator field. Sorry for straying off-topic with that.

In your last example, it's clear that "type" : { "$in" : [ "user", "brand", "merchant" ] } does not belong in a $set command. The behavior you're seeing could certainly be the result of some recent changes that fixed discriminator bugs. I believe one bug @jwage fixed was discriminators not being set at all on some update/upsert queries. I'll wait for him to chime in on this, unless you'd like to back-pedal through his recent commits over the last two weeks and see if anything stands out.

@jwage
Copy link
Member

jwage commented Feb 9, 2012

Hi, can you provide a test case for the expected behavior? and I will work on a fix.

@AD7six
Copy link
Contributor Author

AD7six commented Feb 9, 2012

Possibly, though this is a good reason not to:

[10:18][andy@work:~/www/apps/myapp/vendor/doctrine-mongodb-odm(master)]$ phpunit 
PHPUnit 3.6.7 by Sebastian Bergmann.

Configuration read from /home/andy/www/apps/myapp/vendor/doctrine-mongodb-odm/phpunit.xml.dist

.......................................................Segmentation fault
[10:18][andy@work:~/www/apps/myapp/vendor/doctrine-mongodb-odm(master)]$ 

@kusmierz
Copy link

kusmierz commented Mar 2, 2012

Any news/progress?

@jwage
Copy link
Member

jwage commented Mar 2, 2012

Hi, I don't have any test case or anything to work from. Can you help provide something for us to work against?

@kusmierz
Copy link

kusmierz commented Mar 2, 2012

<?php
/**
 * @MongoDB\Document(collection="articles", repositoryClass="Acme\AcmeBundle\Document\ArticleRepository")
 * @MongoDB\DiscriminatorField(fieldName="type")
 * @MongoDB\DiscriminatorMap({"article"="Article", "gallery"="\Acme\AcmeBundle\Document\Gallery"})
 */
class Article

now I have in <Acme\AcmeBundle\Document\ArticleRepository>:

<?php

namespace Acme\AcmeBundle\Document;

use Doctrine\ODM\MongoDB\DocumentRepository;

/**
 * ArticleRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class ArticleRepository extends DocumentRepository
{
    /**
     * Find tag for ajax action
     *
     * @param string $value
     * @return array
     */
    public function findAjaxValue($value)
    {
        // simple validation
        if (empty($value) || !is_string($value)) {
            return array();
        }

        $value = (string) $value;
        return $this->findBy(array(
            'public' => true,
            '$or' => array(
                array('_id' => $value), // można wkleić ID
                array('title' => array('$regex' => '^' . preg_quote($value) . '.*')),
            )), array('guid' => true), 20);
    }
}

And as result I've got exception "$and/$or/$nor must be a nonempty array/500 Internal Server Error - MongoCursorException".

The query:

db.articles.find({
    "public": true,
    "$or": {
        "0": { "_id": "p" },
        "1": { "title": { "$regex": "^p.*" } },
        "type": { "$in": [ "article" ] } // !!!!!????
    },
    "type": { "$in": [ "article" ] }
}).sort({ "guid": true });

I think that lines are ambiguous. You can't inject "type": { "$in": [ "article" ] } in $or statement!

@jwage
Copy link
Member

jwage commented Mar 2, 2012

Can you make a phpunit test case so I can run it?

@kusmierz
Copy link

kusmierz commented Mar 3, 2012

I don't know if it's correct test case, but it works (I mean it doesn't ;)

<?php

// (...)
use Documents\Functional\SameCollection1;

class PersistenceBuilderTest extends BaseTest
{
    // (...)
    public function testFindWithOrOnCollectionWithDiscriminatorMap()
    {
        $id = '4f28aa84acee41388900000a';
        $ids = array($id);

        /**
         * @var \Doctrine\ODM\MongoDB\Query\Builder $qb
         */
        $qb = $this->dm->createQueryBuilder('Documents\Functional\SameCollection1');
        $qb
            ->addOr($qb->expr()->field('id')->in($ids))
            ->select('id')->hydrate(false);
        /**
         * @var \Doctrine\ODM\MongoDB\Query\Query $query
         * @var \Doctrine\ODM\MongoDB\Cursor $results
         */
        $query = $qb->getQuery();
        $results = $query->execute();
        $debug = $query->debug();

        $this->assertEquals($id, (string) current($debug['$or'][0]['_id']['$in']));

        $this->assertInstanceOf('Doctrine\MongoDB\Cursor', $results);
        $this->assertEquals(0, $query->count());
    }
    // (...)
}

@kusmierz
Copy link

kusmierz commented Mar 5, 2012

Is it fixable? :)

@jwage
Copy link
Member

jwage commented Mar 5, 2012

Most likely, I just haven't had a chance to look at it yet. I will try and make time to work on the odm tonight after work.

@kusmierz
Copy link

kusmierz commented Mar 6, 2012

Thanks in advance :)

@wedgybo
Copy link
Contributor

wedgybo commented Mar 14, 2012

A quick update on what causes this. It looks like this part here gets run on every pass and adds in the discriminator values to all commands.

if ($this->class->hasDiscriminator() && ! isset($query[$this->class->discriminatorField['name']])) {
    $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
    $query[$this->class->discriminatorField['name']] = array('$in' => $discriminatorValues);
}

I've created a quick hack which involves only running that section of code when the query is being generated for the final time.

library/Doctrine/ODM/MongoDB/Query/Builder.php
233

  •    $query['query'] = $this->expr->getQuery();
    
  •   $query['query'] = $this->expr->getQuery($this->class->hasDiscriminator());
    

And then look for that being true when preparing the query. I'll paste the full patch if anyone is interested, but I think this is a pretty hacky way to do it. I'm sure there's a better way of detecting when that discriminator code should be added.

@kusmierz
Copy link

I think this would be better:

diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
index 907d8df..a8aa512 100644
--- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
+++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
@@ -780,11 +780,13 @@ class DocumentPersister
         if (is_scalar($query) || $query instanceof \MongoId) {
             $query = array('_id' => $query);
         }
-        if ($this->class->hasDiscriminator() && ! isset($query[$this->class->discriminatorField['name']])) {
+        static $queryNestLevel = 0;
+        if (!$queryNestLevel && $this->class->hasDiscriminator() && ! isset($query[$this->class->discriminatorField['name']])) {
             $discriminatorValues = $this->getClassDiscriminatorValues($this->class);
             $query[$this->class->discriminatorField['name']] = array('$in' => $discriminatorValues);
         }
         $newQuery = array();
+        ++ $queryNestLevel;
         if ($query) {
             foreach ($query as $key => $value) {
                 if (isset($key[0]) && $key[0] === $this->cmd && is_array($value)) {
@@ -795,6 +797,7 @@ class DocumentPersister
             }
             $newQuery = $this->convertTypes($newQuery);
         }
+        -- $queryNestLevel;
         return $newQuery;
     }

It will add discriminator thing only once to the most top of the query (and don't inject in values). What do you think? It's also connected with #283 issue.

@kusmierz
Copy link

@jwage we are waiting for your patch ;)

@jwage
Copy link
Member

jwage commented Mar 23, 2012

I was hoping someone would send a pull request with the failing test since we use this awesome system with that feature :) I guess I will resort to copying and pasting the test and committing it up myself :(

jwage added a commit that referenced this issue Mar 23, 2012
@kusmierz
Copy link

see the test commited in 03d3bd7, I have injected value even in $newObj object in update method, could you look?

@kusmierz
Copy link

kusmierz commented Apr 3, 2012

Any progress? :)

@jwage
Copy link
Member

jwage commented Apr 3, 2012

I haven't had time to look yet.

@jwage jwage closed this as completed in 32ab585 May 17, 2012
jmikola pushed a commit that referenced this issue May 17, 2012
This was originally included in PR #261. It tests the fixes for #250, #283 and #284.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants