diff --git a/lib/Cake/Model/Datasource/Database/Expression/ValuesExpression.php b/lib/Cake/Model/Datasource/Database/Expression/ValuesExpression.php index 85ce1794bf1..eadda7ab567 100644 --- a/lib/Cake/Model/Datasource/Database/Expression/ValuesExpression.php +++ b/lib/Cake/Model/Datasource/Database/Expression/ValuesExpression.php @@ -30,6 +30,11 @@ class ValuesExpression implements Expression { protected $_values = []; + protected $_columns = []; + + public function __construct($columns) { + $this->_columns = $columns; + } /** * Add a row of data to be inserted. @@ -49,7 +54,9 @@ public function add($data) { public function bindings() { $bindings = []; $i = 0; + $defaults = array_fill_keys($this->_columns, null); foreach ($this->_values as $row) { + $row = array_merge($defaults, $row); foreach ($row as $column => $value) { $bindings[] = [ // TODO add types. @@ -70,9 +77,10 @@ public function bindings() { */ public function sql() { $placeholders = []; + $numColumns = count($this->_columns); foreach ($this->_values as $row) { if (is_array($row)) { - $placeholders[] = implode(', ', array_fill(0, count($row), '?')); + $placeholders[] = implode(', ', array_fill(0, $numColumns, '?')); } } return sprintf('(%s)', implode('), (', $placeholders)); diff --git a/lib/Cake/Model/Datasource/Database/Query.php b/lib/Cake/Model/Datasource/Database/Query.php index 75f5601fe42..6220da81f22 100644 --- a/lib/Cake/Model/Datasource/Database/Query.php +++ b/lib/Cake/Model/Datasource/Database/Query.php @@ -18,6 +18,7 @@ namespace Cake\Model\Datasource\Database; use IteratorAggregate; +use Cake\Error; use Cake\Model\Datasource\Database\Expression\Comparison; use Cake\Model\Datasource\Database\Expression\OrderByExpression; use Cake\Model\Datasource\Database\Expression\QueryExpression; @@ -1165,6 +1166,9 @@ protected function _buildValuesPart($parts) { /** * Create an insert query. * + * Note calling this method will reset any data previously set + * with Query::values() + * * @param string $table The table name to insert into. * @param array $columns The columns to insert into. * @return Query @@ -1173,6 +1177,7 @@ public function insert($table, $columns) { $this->_dirty = true; $this->_type = 'insert'; $this->_parts['insert'] = [$table, $columns]; + $this->_parts['values'] = new ValuesExpression($columns); return $this; } @@ -1185,10 +1190,19 @@ public function insert($table, $columns) { * * @param array|Query $data The data to insert. * @return Query + * @throws Cake\Error\Exception if you try to set values before declaring columns. + * Or if you try to set values on non-insert queries. */ public function values($data) { - if (empty($this->_parts['values'])) { - $this->_parts['values'] = new ValuesExpression(); + if ($this->_type !== 'insert') { + throw new Error\Exception( + __d('cake_dev', 'You cannot add values before defining columns to use.') + ); + } + if (empty($this->_parts['insert'])) { + throw new Error\Exception( + __d('cake_dev', 'You cannot add values before defining columns to use.') + ); } $this->_dirty = true; $this->_parts['values']->add($data); diff --git a/lib/Cake/Test/TestCase/Model/Datasource/Database/QueryTest.php b/lib/Cake/Test/TestCase/Model/Datasource/Database/QueryTest.php index f206287df6b..ac8ebc6a0af 100644 --- a/lib/Cake/Test/TestCase/Model/Datasource/Database/QueryTest.php +++ b/lib/Cake/Test/TestCase/Model/Datasource/Database/QueryTest.php @@ -1737,6 +1737,20 @@ public function testUpdateWithExpression() { $this->assertCount(1, $result); } +/** + * You cannot call values() before insert() it causes all sorts of pain. + * + * @expectedException Cake\Error\Exception + */ + public function testInsertValuesBeforeInsertFailure() { + $query = new Query($this->connection); + $query->select('*')->values([ + 'id' => 1, + 'title' => 'mark', + 'body' => 'test insert' + ]); + } + /** * Test inserting a single row. * @@ -1775,12 +1789,72 @@ public function testInsertSimple() { $this->assertEquals($expected, $result->fetchAll('assoc')[0]); } +/** + * Test an insert when not all the listed fields are provided. + * Columns should be matched up where possible. + * + * @return void + */ public function testInsertSparseRow() { - $this->markTestIncomplete(); + $this->_createAuthorsAndArticles(); + + $query = new Query($this->connection); + $query->insert('articles', ['id', 'title', 'body']) + ->values([ + 'body' => 'test insert', + 'title' => 'mark', + ]); + $result = $query->sql(false); + $this->assertEquals( + 'INSERT INTO articles (id, title, body) VALUES (?, ?, ?)', + $result + ); + + $result = $query->execute(); + $this->assertCount(1, $result, '1 row should be inserted'); + + $result = (new Query($this->connection))->select('*') + ->from('articles') + ->execute(); + $this->assertCount(1, $result); + + $expected = [ + 'id' => null, + 'author_id' => null, + 'title' => 'mark', + 'body' => 'test insert' + ]; + $this->assertEquals($expected, $result->fetchAll('assoc')[0]); } +/** + * Test inserting multiple rows. + * + * @return void + */ public function testInsertMultipleRows() { - $this->markTestIncomplete(); + $this->_createAuthorsAndArticles(); + + $query = new Query($this->connection); + $query->insert('articles', ['id', 'title', 'body']) + ->values([ + 'id' => 1, + 'title' => 'mark', + 'body' => 'test insert' + ]) + ->values([ + 'id' => 2, + 'title' => 'jose', + 'body' => 'test insert' + ]); + $result = $query->sql(false); + $this->assertEquals( + 'INSERT INTO articles (id, title, body) VALUES (?, ?, ?), (?, ?, ?)', + $result + ); + + $result = $query->execute(); + $this->assertCount(2, $result, '2 row should be inserted'); } public function testInsertMultipleRowsSparse() {