diff --git a/Cake/Database/Expression/Comparison.php b/Cake/Database/Expression/Comparison.php index 2cccd522b6a..861832ecefa 100644 --- a/Cake/Database/Expression/Comparison.php +++ b/Cake/Database/Expression/Comparison.php @@ -162,12 +162,12 @@ protected function _bindValue($generator, $value, $type) { * * @param array|\Traversable $value the value to flatten * @param ValueBinder $generator - * @param string $type the type to cast values to + * @param string|array $type the type to cast values to * @return string */ protected function _flattenValue($value, $generator, $type = null) { $parts = []; - foreach ($value as $v) { + foreach ($value as $k => $v) { $parts[] = $this->_bindValue($generator, $v, $type); } return implode(',', $parts); diff --git a/Cake/Database/Expression/TupleComparison.php b/Cake/Database/Expression/TupleComparison.php index b14f15901fb..29c8a98e956 100644 --- a/Cake/Database/Expression/TupleComparison.php +++ b/Cake/Database/Expression/TupleComparison.php @@ -80,15 +80,24 @@ protected function _stringifyValues($generator) { continue; } - $type = isset($this->_type[$i]) ? $this->_type[$i] : null; - if ($this->_isMulti($i, $type)) { - $type = str_replace('[]', '', $type); - $value = $this->_flattenValue($value, $generator, $type); - $values[] = "($value)"; + $type = $this->_type; + $multiType = is_array($type); + $isMulti = $this->_isMulti($i, $type); + $type = $isMulti ? str_replace('[]', '', $type) : $type; + + if ($isMulti) { + $bound = []; + foreach ($value as $k => $val) { + $valType = $multiType ? $type[$k] : $type; + $bound[] = $this->_bindValue($generator, $val, $valType); + } + + $values[] = sprintf('(%s)', implode(',', $bound)); continue; } - $values[] = $this->_bindValue($generator, $value, $type); + $type = $valType = $multiType ? $type[$i] : $type; + $values[] = $this->_bindValue($generator, $value, $valType); } return implode(', ', $values); diff --git a/Cake/ORM/Association/ExternalAssociationTrait.php b/Cake/ORM/Association/ExternalAssociationTrait.php index 82f41845e5f..73abba338b3 100644 --- a/Cake/ORM/Association/ExternalAssociationTrait.php +++ b/Cake/ORM/Association/ExternalAssociationTrait.php @@ -266,8 +266,14 @@ protected function _buildQuery($options) { */ protected function _addFilteringCondition($query, $key, $filter) { if (is_array($key)) { - $tuple = new TupleComparison($key, $filter, [], 'IN'); - return $query->andWhere($tuple); + $types = []; + $defaults = $query->defaultTypes(); + foreach ($key as $k) { + if (isset($defaults[$k])) { + $types[] = $defaults[$k]; + } + } + return $query->andWhere(new TupleComparison($key, $filter, $types, 'IN')); } return $query->andWhere([$key . ' IN' => $filter]); } diff --git a/Cake/ORM/Association/HasMany.php b/Cake/ORM/Association/HasMany.php index 336d1b506a7..742a71b3d5b 100644 --- a/Cake/ORM/Association/HasMany.php +++ b/Cake/ORM/Association/HasMany.php @@ -1,7 +1,5 @@ ['type' => 'integer'], + 'author_id' => ['type' => 'integer', 'null' => true], + 'site_id' => ['type' => 'integer', 'null' => true], + 'title' => ['type' => 'string', 'null' => true], + 'body' => 'text', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id', 'site_id']]] + ]; + +/** + * records property + * + * @var array + */ + public $records = [ + [ + 'id' => 1, + 'author_id' => 1, + 'site_id' => 1, + 'title' => 'First Article', + 'body' => 'First Article Body', + ], + [ + 'id' => 2, + 'author_id' => 3, + 'site_id' => 2, + 'title' => 'Second Article', + 'body' => 'Second Article Body', + ], + [ + 'id' => 3, + 'author_id' => 1, + 'site_id' => 2, + 'title' => 'Third Article', + 'body' => 'Third Article Body', + ], + [ + 'id' => 4, + 'author_id' => 3, + 'site_id' => 1, + 'title' => 'Fourth Article', + 'body' => 'Fourth Article Body', + ] + ]; + +} diff --git a/Test/Fixture/SiteAuthorFixture.php b/Test/Fixture/SiteAuthorFixture.php new file mode 100644 index 00000000000..1fc1f2485a6 --- /dev/null +++ b/Test/Fixture/SiteAuthorFixture.php @@ -0,0 +1,46 @@ + ['type' => 'integer'], + 'name' => ['type' => 'string', 'default' => null], + 'site_id' => ['type' => 'integer', 'null' => true], + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id', 'site_id']]] + ]; + +/** + * records property + * + * @var array + */ + public $records = [ + ['id' => 1, 'name' => 'mark', 'site_id' => 1], + ['id' => 2, 'name' => 'juan', 'site_id' => 2], + ['id' => 3, 'name' => 'jose', 'site_id' => 2], + ['id' => 4, 'name' => 'andy', 'site_id' => 1] + ]; + +} + diff --git a/Test/TestCase/ORM/CompositeKeysTest.php b/Test/TestCase/ORM/CompositeKeysTest.php new file mode 100644 index 00000000000..090db2a66e8 --- /dev/null +++ b/Test/TestCase/ORM/CompositeKeysTest.php @@ -0,0 +1,132 @@ +connection = ConnectionManager::get('test'); + } + +/** + * Data provider for the two types of strategies HasMany implements + * + * @return void + */ + public function strategiesProvider() { + return [['subquery'], ['select']]; + } + +/** + * Tests that HasMany associations are correctly eager loaded and results + * correctly nested when multiple foreignKeys are used + * + * @dataProvider strategiesProvider + * @return void + */ + public function testHasManyEagerCompositeKeys($strategy) { + $table = TableRegistry::get('SiteAuthors'); + TableRegistry::get('SiteArticles'); + $table->hasMany('SiteArticles', [ + 'propertyName' => 'articles', + 'strategy' => $strategy, + 'sort' => ['SiteArticles.id' => 'asc'], + 'foreignKey' => ['author_id', 'site_id'] + ]); + $query = new Query($this->connection, $table); + + $results = $query->select() + ->contain('SiteArticles') + ->hydrate(false) + ->toArray(); + $expected = [ + [ + 'id' => 1, + 'name' => 'mark', + 'site_id' => 1, + 'articles' => [ + [ + 'id' => 1, + 'title' => 'First Article', + 'body' => 'First Article Body', + 'author_id' => 1, + 'site_id' => 1 + ] + ] + ], + [ + 'id' => 2, + 'name' => 'juan', + 'site_id' => 2 + ], + [ + 'id' => 3, + 'name' => 'jose', + 'site_id' => 2, + 'articles' => [ + [ + 'id' => 2, + 'title' => 'Second Article', + 'body' => 'Second Article Body', + 'author_id' => 3, + 'site_id' => 2 + ] + ] + ], + [ + 'id' => 4, + 'name' => 'andy', + 'site_id' => 1 + ] + ]; + $this->assertEquals($expected, $results); + + $results = $query->repository($table) + ->select() + ->contain(['SiteArticles' => ['conditions' => ['id' => 2]]]) + ->hydrate(false) + ->toArray(); + unset($expected[0]['articles']); + $this->assertEquals($expected, $results); + $this->assertEquals($table->association('SiteArticles')->strategy(), $strategy); + } + +} diff --git a/Test/TestCase/ORM/QueryTest.php b/Test/TestCase/ORM/QueryTest.php index c313d9cf045..2940085d684 100644 --- a/Test/TestCase/ORM/QueryTest.php +++ b/Test/TestCase/ORM/QueryTest.php @@ -16,7 +16,6 @@ */ namespace Cake\Test\TestCase\ORM; -use Cake\Core\Configure; use Cake\Database\ConnectionManager; use Cake\Database\Expression\IdentifierExpression; use Cake\Database\Expression\OrderByExpression;