From 20990ed2046a8fd48f3fa7c5e4841bae118260de Mon Sep 17 00:00:00 2001
From: doctrine <doctrine@625475ce-881a-0410-a577-b389adb331d8>
Date: Mon, 29 May 2006 08:43:21 +0000
Subject: [PATCH] DQL: direct one-to-one relation fetching bug fixed

---
 classes/Collection/Batch.class.php  | 11 +++--
 classes/IndexGenerator.php          |  2 +-
 classes/Query.class.php             | 33 +++++++++----
 classes/Record.class.php            | 45 ++++++++++++++---
 classes/Sensei/Sensei.class.php     |  9 +---
 classes/Session.class.php           | 40 ---------------
 classes/Table.class.php             |  8 ++-
 classes/Validator/Nospace.class.php |  2 +-
 tests/QueryTestCase.class.php       | 75 +++++++++++++++++++++++------
 tests/SenseiTestCase.class.php      |  7 ++-
 tests/UnitTestCase.class.php        |  1 +
 tests/classes.php                   | 49 +++++++++++++++++++
 tests/run.php                       |  2 -
 13 files changed, 197 insertions(+), 87 deletions(-)

diff --git a/classes/Collection/Batch.class.php b/classes/Collection/Batch.class.php
index ed5d0ce64..bae8bcaae 100644
--- a/classes/Collection/Batch.class.php
+++ b/classes/Collection/Batch.class.php
@@ -28,6 +28,7 @@ class Doctrine_Collection_Batch extends Doctrine_Collection {
 
     /**
      * @param integer $batchSize    batch size
+     * @return boolean
      */
     public function setBatchSize($batchSize) {
         $batchSize = (int) $batchSize;
@@ -38,15 +39,19 @@ class Doctrine_Collection_Batch extends Doctrine_Collection {
         return true;
     }
     /**
+     * returns the batch size of this collection
+     *
      * @return integer
      */
     public function getBatchSize() {
         return $this->batchSize;
     }
     /**
-     * load                         load a specified element, by loading the batch the element is part of
-     * @param Doctrine_Record $record              data access object
-     * @return boolean              whether or not the load operation was successful
+     * load                                        
+     * loads a specified element, by loading the batch the element is part of
+     *
+     * @param Doctrine_Record $record              record to be loaded
+     * @return boolean                             whether or not the load operation was successful
      */
     public function load(Doctrine_Record $record) {
         if(empty($this->data))
diff --git a/classes/IndexGenerator.php b/classes/IndexGenerator.php
index 00600031d..01b1bcd9d 100644
--- a/classes/IndexGenerator.php
+++ b/classes/IndexGenerator.php
@@ -22,4 +22,4 @@ class Doctrine_IndexGenerator {
         return $value;
     }
 }
-?> 
+?>
diff --git a/classes/Query.class.php b/classes/Query.class.php
index e3e283b25..4da8612ce 100644
--- a/classes/Query.class.php
+++ b/classes/Query.class.php
@@ -419,8 +419,7 @@ class Doctrine_Query extends Doctrine_Access {
 
                 $array = $this->parseData($stmt);
 
-                $colls = array();
-
+                $colls = array();    
 
                 foreach($array as $data) {
                     /**
@@ -465,14 +464,30 @@ class Doctrine_Query extends Doctrine_Access {
                                 $coll->add($record);
                             } else {
                                 $pointer = $this->joins[$name];
-                                
-                                $last = $prev[$pointer]->getLast();
 
-                                if( ! $last->hasReference($name)) {
-                                    $prev[$name] = $this->getCollection($name);
-                                    $last->initReference($prev[$name],$this->connectors[$name]);
-                                }
-                                $last->addReference($record);
+                                $fk = $this->tables[$pointer]->getForeignKey($this->tables[$pointer]->getAlias($name));
+
+                                switch($fk->getType()):
+                                    case Doctrine_Relation::ONE_COMPOSITE:
+                                    case Doctrine_Relation::ONE_AGGREGATE:
+                                        $last = $prev[$pointer]->getLast();
+
+                                        $last->rawSet($this->connectors[$name]->getLocal(), $record->getID());
+
+                                        $last->initSingleReference($record);
+                                        
+                                        $prev[$name] = $record;
+                                    break;
+                                    default:
+                                        // one-to-many relation or many-to-many relation
+                                        $last = $prev[$pointer]->getLast();
+    
+                                        if( ! $last->hasReference($name)) {
+                                            $prev[$name] = $this->getCollection($name);
+                                            $last->initReference($prev[$name],$this->connectors[$name]);
+                                        }
+                                        $last->addReference($record);
+                                endswitch;
                             }
                         }
 
diff --git a/classes/Record.class.php b/classes/Record.class.php
index aab162589..7200846a4 100644
--- a/classes/Record.class.php
+++ b/classes/Record.class.php
@@ -83,6 +83,10 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
      * @var integer $index                  this index is used for creating object identifiers
      */
     private static $index = 1;
+    /**
+     * @var Doctrine_Null $nullObject       a Doctrine_Null object used for SQL null value testing
+     */
+    private static $nullObject;
     /**
      * @var integer $oid                    object identifier
      */
@@ -157,6 +161,12 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
             $this->table->getRepository()->add($this);
         }
     }
+    /**
+     * initNullObject
+     */
+    public static function initNullObject() {
+        self::$nullObject = new Doctrine_Null;
+    }
     /** 
      * setUp
      * implemented by child classes
@@ -402,7 +412,7 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
             if(is_array($this->data[$name])) {
 
                 // no use trying to load the data from database if the Doctrine_Record is not a proxy
-                if($this->state == Doctrine_Record::STATE_PROXY) {   
+                if($this->state == Doctrine_Record::STATE_PROXY) {
                     if( ! empty($this->collections)) {
                         foreach($this->collections as $collection) {
                             $collection->load($this);
@@ -582,11 +592,7 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
         $this->table->getSession()->save($this);
 
         foreach($saveLater as $fk) {
-
-            $table = $fk->getTable();
-            $foreign = $fk->getForeign();
-            $local   = $fk->getLocal();
-
+            $table   = $fk->getTable();
             $alias   = $this->table->getAlias($table->getComponentName());
 
             if(isset($this->references[$alias])) {
@@ -836,6 +842,17 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
     final public function getID() {
         return $this->id;
     }
+    /**
+     * getLast
+     * this method is used internally be Doctrine_Query
+     * it is needed to provide compatibility between
+     * records and collections
+     *
+     * @return Doctrine_Record
+     */
+    public function getLast() {
+        return $this;
+    }
     /**
      * hasRefence
      * @param string $name
@@ -845,8 +862,22 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
         return isset($this->references[$name]);
     }
     /**
+     * initalizes a one-to-one relation
+     *
+     * @param Doctrine_Record $record
+     * @param Doctrine_Relation $connector
+     * @return void
+     */
+    public function initSingleReference(Doctrine_Record $record) {
+        $name = $this->table->getAlias($record->getTable()->getComponentName());
+        $this->references[$name] = $record;
+    }
+    /**
+     * initalizes a one-to-many / many-to-many relation
+     *
      * @param Doctrine_Collection $coll
-     * @param string $connectorField
+     * @param Doctrine_Relation $connector
+     * @return void
      */
     public function initReference(Doctrine_Collection $coll, Doctrine_Relation $connector) {
         $name = $this->table->getAlias($coll->getTable()->getComponentName());
diff --git a/classes/Sensei/Sensei.class.php b/classes/Sensei/Sensei.class.php
index e84b70766..2f4b6c9ab 100644
--- a/classes/Sensei/Sensei.class.php
+++ b/classes/Sensei/Sensei.class.php
@@ -1,10 +1,8 @@
 <?php
 class Sensei_Group extends Doctrine_Record { }
-//class Sensei_Company extends Sensei_Group { }
-
+class Sensei_Company extends Sensei_Group { }
 class Sensei_User extends Doctrine_Record { }
-//class Sensei_Customer extends Sensei_User { }
-
+class Sensei_Customer extends Sensei_User { }
 class Sensei_Entity extends Doctrine_Record {
     /**
      * setTableDefinition
@@ -65,10 +63,7 @@ class Sensei_Session extends Doctrine_Record {
         $this->hasColumn("created","integer");
     }
 }
-
 class Sensei_Exception extends Exception { }
-
-
 class Sensei extends Doctrine_Access {
     const ATTR_LIFESPAN = 0;
     /**
diff --git a/classes/Session.class.php b/classes/Session.class.php
index c1ac6b780..814290b37 100644
--- a/classes/Session.class.php
+++ b/classes/Session.class.php
@@ -246,46 +246,7 @@ abstract class Doctrine_Session extends Doctrine_Configurable implements Countab
     public function create($name) {
         return $this->getTable($name)->create();
     }
-    public function buildFlushTree2(array $tables) {
-        $tree = array();
-        foreach($tables as $table) {
-            if( ! ($table instanceof Doctrine_Table))
-                $table = $this->getTable($table);
 
-            $name  = $table->getComponentName();
-            $index = array_search($name,$tree);
-            if($index === false)
-                $tree[] = $name;
-
-
-            foreach($table->getForeignKeys() as $rel) {
-                $name  = $rel->getTable()->getComponentName();
-                $index = array_search($name,$tree);
-
-                if($rel instanceof Doctrine_ForeignKey) {
-                    if($index !== false)
-                        unset($tree[$index]);
-
-                    $tree[] = $name;
-                } elseif($rel instanceof Doctrine_LocalKey) {
-                    if($index !== false)
-                        unset($tree[$index]);
-
-                    array_unshift($tree, $name);
-                } elseif($rel instanceof Doctrine_Association) {
-                    $t = $rel->getAssociationFactory();
-                    $n = $t->getComponentName();
-                    $index = array_search($n,$tree);
-
-                    if($index !== false)
-                        unset($tree[$index]);
-
-                    $tree[] = $n;
-                }
-            }
-        }
-        return array_values($tree);
-    }
 
     /**
      * buildFlushTree
@@ -573,7 +534,6 @@ abstract class Doctrine_Session extends Doctrine_Configurable implements Countab
                 $increment = true;
             }
 
-
             foreach($inserts as $k => $record) {
                 $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreSave($record);
                 // listen the onPreInsert event
diff --git a/classes/Table.class.php b/classes/Table.class.php
index 7ba886b1c..3fe6601e9 100644
--- a/classes/Table.class.php
+++ b/classes/Table.class.php
@@ -81,6 +81,10 @@ class Doctrine_Table extends Doctrine_Configurable {
      * @var array $boundAliases                         bound relation aliases
      */
     private $boundAliases       = array();
+    /**
+     * @var integer $columnCount                        cached column count
+     */
+    private $columnCount;
 
 
     /**
@@ -135,6 +139,8 @@ class Doctrine_Table extends Doctrine_Configurable {
         if(method_exists($record,"setTableDefinition")) {
             $record->setTableDefinition();
 
+            $this->columnCount = count($this->columns);
+
             if(isset($this->columns)) {
                 $method    = new ReflectionMethod($this->name,"setTableDefinition");
                 $class     = $method->getDeclaringClass();
@@ -784,7 +790,7 @@ class Doctrine_Table extends Doctrine_Configurable {
      * @return integer
      */
     final public function getColumnCount() {
-        return count($this->columns);                                       	
+        return $this->columnCount;                                       	
     }
     /**
      * returns all columns and their definitions
diff --git a/classes/Validator/Nospace.class.php b/classes/Validator/Nospace.class.php
index f7eda274d..532cccf48 100644
--- a/classes/Validator/Nospace.class.php
+++ b/classes/Validator/Nospace.class.php
@@ -8,7 +8,7 @@ class Doctrine_Validator_NoSpace {
      * @return boolean
      */
     public function validate(Doctrine_Record $record, $key, $value, $args) {
-        if(preg_match("/[\s\r\t\n]/", $value))
+        if(trim($value) === '')
             return false;
 
         return true;
diff --git a/tests/QueryTestCase.class.php b/tests/QueryTestCase.class.php
index 77982091a..615a8abfe 100644
--- a/tests/QueryTestCase.class.php
+++ b/tests/QueryTestCase.class.php
@@ -7,6 +7,46 @@ class Doctrine_QueryTestCase extends Doctrine_UnitTestCase {
         $this->tables[] = "Forum_Thread";
         parent::prepareTables();
     }
+
+    public function testOneToOneRelationFetching() {
+
+        $count = $this->dbh->count();
+        $users = $this->query->from("User-l:Email-i")->execute();
+        $this->assertEqual(($count + 1), $this->dbh->count());
+        $this->assertTrue($users instanceof Doctrine_Collection_Lazy);
+        $count = $this->dbh->count();
+
+        foreach($users as $user) {
+            // iterate through users and test that no additional queries are needed
+            $user->Email->address;
+            $this->assertEqual($count, $this->dbh->count());
+        }
+        $users[0]->Account->amount = 3000;
+        $this->assertEqual($users[0]->Account->amount, 3000);
+        $this->assertTrue($users[0]->Account instanceof Account);
+        $this->assertEqual($users[0]->id, $users[0]->Account->entity_id);
+
+        $users[0]->Account->save();
+
+        $users[0]->refresh();
+        $this->assertEqual($users[0]->Account->amount, 3000);
+        $this->assertTrue($users[0]->Account instanceof Account);
+
+        $this->assertEqual($users[0]->id, $users[0]->Account->entity_id);
+
+        $this->assertEqual($users[0]->Account->getState(), Doctrine_Record::STATE_CLEAN);
+        $users[0]->getTable()->clear();
+        $users[0]->Account->getTable()->clear();
+
+        $count = $this->dbh->count();
+        $users = $this->query->from("User-l:Account-i")->execute();
+        $this->assertEqual(($count + 1), $this->dbh->count());
+        $this->assertTrue($users instanceof Doctrine_Collection_Lazy);
+        $this->assertEqual($users->count(), 1);
+
+        $this->assertEqual($users[0]->Account->amount,3000);
+    }
+
     public function testNotValidLazyPropertyFetching() {
         $q = new Doctrine_Query($this->session);
         
@@ -49,7 +89,6 @@ class Doctrine_QueryTestCase extends Doctrine_UnitTestCase {
         $this->assertEqual($count + 1, count($this->dbh));
     }
 
-
     public function testQueryWithComplexAliases() {
         $q = new Doctrine_Query($this->session);
 
@@ -209,7 +248,7 @@ class Doctrine_QueryTestCase extends Doctrine_UnitTestCase {
         $this->assertEqual($query->offset, 3);
         $this->assertEqual($coll->count(), 3);
     }
-    public function testPrepared() {
+    public function testPreparedQuery() {
         $coll = $this->session->query("FROM User WHERE User.name = :name", array(":name" => "zYne"));
         $this->assertEqual($coll->count(), 1);
     }
@@ -223,6 +262,25 @@ class Doctrine_QueryTestCase extends Doctrine_UnitTestCase {
         $this->assertEqual($users->count(),8);
         $this->assertTrue($users[0]->name == "Arnold Schwarzenegger");
     }
+    public function testBatchFetching() {
+        $query = new Doctrine_Query($this->session);
+        $users = $query->query("FROM User-b");
+        $this->assertEqual(trim($query->getQuery()),
+        "SELECT entity.id AS User__id FROM entity WHERE (entity.type = 0)");
+
+        $this->assertEqual($users[0]->name, "zYne");
+        $this->assertTrue($users instanceof Doctrine_Collection_Batch);
+    }
+    public function testLazyFetching() {
+        $query = new Doctrine_Query($this->session);
+        $users = $query->query("FROM User-l");
+        $this->assertEqual(trim($query->getQuery()),
+        "SELECT entity.id AS User__id FROM entity WHERE (entity.type = 0)");
+
+        $this->assertEqual($users[0]->name, "zYne");
+        $this->assertTrue($users instanceof Doctrine_Collection_Lazy);
+
+    }
 
     public function testQuery() {
 
@@ -326,20 +384,8 @@ class Doctrine_QueryTestCase extends Doctrine_UnitTestCase {
         $count2 = $this->session->getDBH()->count();
 
 
-        $users = $query->query("FROM User-b");
-        $this->assertEqual(trim($query->getQuery()),
-        "SELECT entity.id AS User__id FROM entity WHERE (entity.type = 0)");
-
-        $this->assertEqual($users[0]->name, "zYne");
-        $this->assertTrue($users instanceof Doctrine_Collection_Batch);
 
 
-        $users = $query->query("FROM User-l");
-        $this->assertEqual(trim($query->getQuery()),
-        "SELECT entity.id AS User__id FROM entity WHERE (entity.type = 0)");
-
-        $this->assertEqual($users[0]->name, "zYne");
-        $this->assertTrue($users instanceof Doctrine_Collection_Lazy);
 
 
         //$this->clearCache();
@@ -403,5 +449,6 @@ class Doctrine_QueryTestCase extends Doctrine_UnitTestCase {
         //$this->assertTrue(isset($values['max']));
 
     }
+
 }
 ?>
diff --git a/tests/SenseiTestCase.class.php b/tests/SenseiTestCase.class.php
index 38ca46053..b86ee90a3 100644
--- a/tests/SenseiTestCase.class.php
+++ b/tests/SenseiTestCase.class.php
@@ -1,5 +1,6 @@
 <?php
 require_once("../classes/Doctrine.class.php");
+
 Doctrine::loadAll();
 
 class Sensei_UnitTestCase extends UnitTestCase { 
@@ -13,6 +14,9 @@ class Sensei_UnitTestCase extends UnitTestCase {
     private $init = false;
 
     public function init() {
+        if(headers_sent())
+            throw new Exception("'".ob_end_flush()."'");
+
         $this->manager   = Doctrine_Manager::getInstance();
         $this->manager->setAttribute(Doctrine::ATTR_CACHE, Doctrine::CACHE_NONE);
 
@@ -40,7 +44,7 @@ class Sensei_UnitTestCase extends UnitTestCase {
         
         $entity->loginname = "Chuck Norris";
         $entity->password = "toughguy";
-        
+
         $entity->save();
 
         $this->init   = true;
@@ -109,6 +113,5 @@ class Sensei_UnitTestCase extends UnitTestCase {
 
         $this->assertEqual($this->record->entity_id, 0);
     }
-
 }
 ?>
diff --git a/tests/UnitTestCase.class.php b/tests/UnitTestCase.class.php
index b2325acda..313b43df9 100644
--- a/tests/UnitTestCase.class.php
+++ b/tests/UnitTestCase.class.php
@@ -49,6 +49,7 @@ class Doctrine_UnitTestCase extends UnitTestCase {
             $this->manager->setAttribute(Doctrine::ATTR_LISTENER, $this->listener);
         }
 
+        $this->query = new Doctrine_Query($this->session);
         $this->prepareTables();
         $this->prepareData();
     }
diff --git a/tests/classes.php b/tests/classes.php
index 50cf51a76..64efcb0f6 100644
--- a/tests/classes.php
+++ b/tests/classes.php
@@ -207,4 +207,53 @@ class Forum_Thread extends Doctrine_Record {
         $this->ownsMany("Forum_Entry as Entries", "Forum_Entry.thread_id");
     }
 }
+class App extends Doctrine_Record {
+    public function setTableDefinition() {
+        $this->hasColumn("name", "string", 32);
+        $this->hasColumn("user_id", "integer", 11);
+        $this->hasColumn("app_category_id", "integer", 11);
+    }
+    public function setUp() {
+        $this->hasOne("User","User.id");
+        $this->hasMany("App_Category as Category","App_Category.id");
+    }        
+}
+
+class App_User extends Doctrine_Record {
+    public function setTableDefinition() {
+        $this->hasColumn("first_name", "string", 32);
+        $this->hasColumn("last_name", "string", 32);
+        $this->hasColumn("email", "string", 128, "email");
+        $this->hasColumn("username", "string", 16, "unique, nospace");
+        $this->hasColumn("password", "string", 128, "notblank");
+        $this->hasColumn("country", "string", 2, "country");
+        $this->hasColumn("zipcode", "string", 9, "nospace");
+    }
+    public function setUp() {
+        $this->hasMany("App","App.user_id");
+    }    
+}
+ 
+class App_Category extends Doctrine_Record {
+    public function setTableDefinition() {
+        $this->hasColumn("name", "string", 32);
+        $this->hasColumn("parent_id","integer");
+    }
+    public function setUp() {
+        $this->hasMany("App","App.app_category_id");
+        $this->hasMany("App_Category as Parent","App_Category.parent_id");
+    }
+}
+ 
+/**
+$apps = $con->query("FROM App.Category");
+
+if (!empty($apps))
+{
+        foreach ($apps as $app)
+        {
+                print '<p>' . $app->Category[0]->name . ' => ' . $app->name . '</p>';
+        }
+}
+*/
 ?>
diff --git a/tests/run.php b/tests/run.php
index d7b5ba035..278bbe54c 100644
--- a/tests/run.php
+++ b/tests/run.php
@@ -52,8 +52,6 @@ $test->addTestCase(new Doctrine_QueryTestCase());
 //$test->addTestCase(new Doctrine_Cache_SqliteTestCase());
 
 
-
-
 print "<pre>";
 $test->run(new HtmlReporter());
 $cache = Doctrine_Manager::getInstance()->getCurrentSession()->getCacheHandler();