Skip to content

Commit

Permalink
Add sparse insert support.
Browse files Browse the repository at this point in the history
You can now omit some or all columns from any row of data.
  • Loading branch information
markstory committed Mar 4, 2013
1 parent cc37aa6 commit f5cbbc3
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 5 deletions.
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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));
Expand Down
18 changes: 16 additions & 2 deletions lib/Cake/Model/Datasource/Database/Query.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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);
Expand Down
78 changes: 76 additions & 2 deletions lib/Cake/Test/TestCase/Model/Datasource/Database/QueryTest.php
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit f5cbbc3

Please sign in to comment.