Skip to content

Commit

Permalink
Add INSERT INTO ... SELECT support.
Browse files Browse the repository at this point in the history
  • Loading branch information
markstory committed Mar 5, 2013
1 parent f5cbbc3 commit 9b15e82
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 58 deletions.
66 changes: 52 additions & 14 deletions lib/Cake/Model/Datasource/Database/Expression/ValuesExpression.php
Expand Up @@ -17,6 +17,7 @@
*/
namespace Cake\Model\Datasource\Database\Expression;

use Cake\Error;
use Cake\Model\Datasource\Database\Expression;
use Cake\Model\Datasource\Database\Query;
use \Countable;
Expand All @@ -31,6 +32,7 @@ class ValuesExpression implements Expression {

protected $_values = [];
protected $_columns = [];
protected $_hasQuery = false;

public function __construct($columns) {
$this->_columns = $columns;
Expand All @@ -39,10 +41,23 @@ public function __construct($columns) {
/**
* Add a row of data to be inserted.
*
* @param array $data Array of data to append into the insert.
* @param array|Query $data Array of data to append into the insert, or
* a query for doing INSERT INTO .. SELECT style commands
* @return void
* @throws Cake\Error\Exception When mixing array + Query data types.
*/
public function add($data) {
if (
count($this->_values) &&
($data instanceof Query || ($this->_hasQuery && is_array($data)))
) {
throw new Error\Exception(
__d('cake_dev', 'You cannot mix subqueries and array data in inserts.')
);
}
if ($data instanceof Query) {
$this->_hasQuery = true;
}
$this->_values[] = $data;
}

Expand All @@ -56,15 +71,17 @@ public function 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.
'type' => null,
'placeholder' => $i,
'value' => $value
];
$i++;
if (is_array($row)) {
$row = array_merge($defaults, $row);
foreach ($row as $column => $value) {
$bindings[] = [
// TODO add types.
'type' => null,
'placeholder' => $i,
'value' => $value
];
$i++;
}
}
}
return $bindings;
Expand All @@ -76,14 +93,35 @@ public function bindings() {
* @return string
*/
public function sql() {
if (empty($this->_values)) {
return '';
}
if ($this->_hasQuery) {
return ' ' . $this->_values[0]->sql();
}
$placeholders = [];
$numColumns = count($this->_columns);

foreach ($this->_values as $row) {
if (is_array($row)) {
$placeholders[] = implode(', ', array_fill(0, $numColumns, '?'));
}
$placeholders[] = implode(', ', array_fill(0, $numColumns, '?'));
}
return sprintf(' VALUES (%s)', implode('), (', $placeholders));
}

/**
* Traverse the values expression.
*
* This method will also traverse any queries that are to be used in the INSERT
* values.
*
* @param callable $visitor The visitor to traverse the expression with.
* @return void
*/
public function traverse(callable $visitor) {
if (!$this->_hasQuery) {
return;
}
return sprintf('(%s)', implode('), (', $placeholders));
$this->_values[0]->traverse($visitor);
}

}
17 changes: 2 additions & 15 deletions lib/Cake/Model/Datasource/Database/Query.php
Expand Up @@ -81,7 +81,6 @@ class Query implements Expression, IteratorAggregate {
protected $_templates = [
'delete' => 'DELETE',
'update' => 'UPDATE %s',
'values' => ' VALUES %s',
'where' => ' WHERE %s',
'group' => ' GROUP BY %s ',
'having' => ' HAVING %s ',
Expand Down Expand Up @@ -1147,20 +1146,7 @@ protected function _buildInsertPart($parts) {
* @return string SQL fragment.
*/
protected function _buildValuesPart($parts) {
$columns = $this->_parts['insert'][1];
$defaults = array_fill_keys($columns, null);
$placeholders = [];
$values = [];
foreach ($parts as $part) {
if (is_array($part)) {
$values[] = array_values($part + $defaults);
$placeholders[] = implode(', ', array_fill(0, count($part), '?'));
}
}
return sprintf(
' VALUES (%s)',
implode('), (', $placeholders)
);
return implode('', $parts);
}

/**
Expand Down Expand Up @@ -1459,6 +1445,7 @@ protected function _bindParams($statement) {
$expression->traverse($visitor);
}
if ($expression instanceof ValuesExpression) {
$expression->traverse($binder);
$visitor($expression);
}
};
Expand Down
110 changes: 81 additions & 29 deletions lib/Cake/Test/TestCase/Model/Datasource/Database/QueryTest.php
Expand Up @@ -1741,6 +1741,7 @@ public function testUpdateWithExpression() {
* You cannot call values() before insert() it causes all sorts of pain.
*
* @expectedException Cake\Error\Exception
* @return void
*/
public function testInsertValuesBeforeInsertFailure() {
$query = new Query($this->connection);
Expand Down Expand Up @@ -1775,18 +1776,15 @@ public function testInsertSimple() {
$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' => 1,
'author_id' => null,
'title' => 'mark',
'body' => 'test insert'
[
'id' => 1,
'author_id' => null,
'title' => 'mark',
'body' => 'test insert'
]
];
$this->assertEquals($expected, $result->fetchAll('assoc')[0]);
$this->assertTable('articles', 1, $expected);
}

/**
Expand All @@ -1813,39 +1811,34 @@ public function testInsertSparseRow() {
$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'
[
'id' => null,
'author_id' => null,
'title' => 'mark',
'body' => 'test insert'
]
];
$this->assertEquals($expected, $result->fetchAll('assoc')[0]);
$this->assertTable('articles', 1, $expected);
}

/**
* Test inserting multiple rows.
* Test inserting multiple rows with sparse data.
*
* @return void
*/
public function testInsertMultipleRows() {
public function testInsertMultipleRowsSparse() {
$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(
Expand All @@ -1854,15 +1847,74 @@ public function testInsertMultipleRows() {
);

$result = $query->execute();
$this->assertCount(2, $result, '2 row should be inserted');
}
$this->assertCount(2, $result, '2 rows should be inserted');

public function testInsertMultipleRowsSparse() {
$this->markTestIncomplete();
$expected = [
[
'id' => 1,
'author_id' => null,
'title' => null,
'body' => 'test insert'
],
[
'id' => 2,
'author_id' => null,
'title' => 'jose',
'body' => null,
],
];
$this->assertTable('articles', 2, $expected);
}

/**
* Test that INSERT INTO ... SELECT works.
*
* @return void
*/
public function testInsertFromSelect() {
$this->markTestIncomplete();
$this->_insertTwoRecords();
$select = (new Query($this->connection))->select('name, "some text", 99')
->from('authors')
->where(['id' => 1]);

$query = new Query($this->connection);
$query->insert('articles', ['title', 'body', 'author_id'])
->values($select);

$result = $query->sql(false);
$this->assertContains('INSERT INTO articles (title, body, author_id) SELECT', $result);
$this->assertContains('SELECT name, "some text", 99 FROM authors', $result);
$result = $query->execute();

$this->assertCount(1, $result);
$result = (new Query($this->connection))->select('*')
->from('articles')
->where(['author_id' => 99])
->execute();
$this->assertCount(1, $result);
$expected = [
'id' => null,
'title' => 'Chuck Norris',
'body' => 'some text',
'author_id' => 99,
];
$this->assertEquals($expected, $result->fetch('assoc'));
}

/**
* Assertion for comparing a table's contents with what is in it.
*
* @param string $table
* @param int $count
* @param array $rows
* @return void
*/
protected function assertTable($table, $count, $rows) {
$result = (new Query($this->connection))->select('*')
->from($table)
->execute();
$this->assertCount($count, $result, 'Row count is incorrect');
$this->assertEquals($rows, $result->fetchAll('assoc'));
}

}

0 comments on commit 9b15e82

Please sign in to comment.