diff --git a/includes/database/database.inc b/includes/database/database.inc index fcbd925a2beace95508920cbc50c612e94d86254..a5da925de980dc0a1e0a1c24f193fe792b60c744 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -1412,6 +1412,19 @@ class ExplicitTransactionsNotSupportedException extends Exception { } */ class InvalidMergeQueryException extends Exception {} +/** + * Exception thrown if an insert query specifies a field twice. + * + * It is not allowed to specify a field as default and insert field, this + * exception is thrown if that is the case. + */ +class FieldsOverlapException extends Exception {} + +/** + * Exception thrown if an insert query doesn't specify insert or default fields. + */ +class NoFieldsException extends Exception {} + /** * A wrapper class for creating and managing database transactions. * diff --git a/includes/database/mysql/query.inc b/includes/database/mysql/query.inc index 84caf03868d584d2a16893c4478f9a64b25bd32a..09878f96dc8ebc6a157c652041f9a4fd53ce4169 100644 --- a/includes/database/mysql/query.inc +++ b/includes/database/mysql/query.inc @@ -15,31 +15,24 @@ class InsertQuery_mysql extends InsertQuery { public function execute() { - - // Confirm that the user did not try to specify an identical - // field and default field. - if (array_intersect($this->insertFields, $this->defaultFields)) { - throw new PDOException('You may not specify the same field to have a value and a schema-default value.'); - } - - if (count($this->insertFields) + count($this->defaultFields) == 0 && empty($this->fromQuery)) { + if (!$this->preExecute()) { return NULL; } - // Don't execute query without values. - if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) { - return NULL; - } - - $last_insert_id = 0; - - $max_placeholder = 0; - $values = array(); - foreach ($this->insertValues as $insert_values) { - foreach ($insert_values as $value) { - $values[':db_insert_placeholder_' . $max_placeholder++] = $value; + // If we're selecting from a SelectQuery, finish building the query and + // pass it back, as any remaining options are irrelevant. + if (empty($this->fromQuery)) { + $max_placeholder = 0; + $values = array(); + foreach ($this->insertValues as $insert_values) { + foreach ($insert_values as $value) { + $values[':db_insert_placeholder_' . $max_placeholder++] = $value; + } } } + else { + $values = $this->fromQuery->getArguments(); + } $last_insert_id = $this->connection->query((string)$this, $values, $this->queryOptions); @@ -56,6 +49,8 @@ public function __toString() { // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); + // If we're selecting from a SelectQuery, finish building the query and + // pass it back, as any remaining options are irrelevant. if (!empty($this->fromQuery)) { return "INSERT $delay INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery; } diff --git a/includes/database/pgsql/query.inc b/includes/database/pgsql/query.inc index 489d57e657f460a5cb30755d57459a59215cf26c..ab16a70ec0e7dbd8015a633cd4077c81ab77b090 100644 --- a/includes/database/pgsql/query.inc +++ b/includes/database/pgsql/query.inc @@ -15,19 +15,7 @@ class InsertQuery_pgsql extends InsertQuery { public function execute() { - - // Confirm that the user did not try to specify an identical - // field and default field. - if (array_intersect($this->insertFields, $this->defaultFields)) { - throw new PDOException('You may not specify the same field to have a value and a schema-default value.'); - } - - if (count($this->insertFields) + count($this->defaultFields) == 0 && empty($this->fromQuery)) { - return NULL; - } - - // Don't execute query without values. - if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) { + if (!$this->preExecute()) { return NULL; } @@ -56,6 +44,11 @@ public function execute() { } } } + if (!empty($this->fromQuery)) { + foreach ($this->fromQuery->getArguments() as $key => $value) { + $stmt->bindParam($key, $value); + } + } // PostgreSQL requires the table name to be specified explicitly // when requesting the last insert ID, so we pass that in via @@ -82,6 +75,8 @@ public function __toString() { // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); + // If we're selecting from a SelectQuery, finish building the query and + // pass it back, as any remaining options are irrelevant. if (!empty($this->fromQuery)) { return "INSERT INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery; } diff --git a/includes/database/query.inc b/includes/database/query.inc index b27eb6a0c003aa55482447774238f23675a9cbe3..b6b47f48d90175632b0239df3c36e83f14eb183e 100644 --- a/includes/database/query.inc +++ b/includes/database/query.inc @@ -287,7 +287,8 @@ class InsertQuery extends Query { /** * A SelectQuery object to fetch the rows that should be inserted. - * + * + * @var SelectQueryInterface */ protected $fromQuery; @@ -432,29 +433,20 @@ public function from(SelectQueryInterface $query) { * in multi-insert loops. */ public function execute() { - - $last_insert_id = 0; - - // Check if a SelectQuery is passed in and use that. - if (!empty($this->fromQuery)) { - return $this->connection->query((string) $this, array(), $this->queryOptions); - } - - // Confirm that the user did not try to specify an identical - // field and default field. - if (array_intersect($this->insertFields, $this->defaultFields)) { - throw new PDOException('You may not specify the same field to have a value and a schema-default value.'); - } - - if (count($this->insertFields) + count($this->defaultFields) == 0) { + if (!$this->preExecute()) { return NULL; } - // Don't execute query without values. - if (!isset($this->insertValues[0]) && count($this->insertFields) > 0) { - return NULL; + // If we're selecting from a SelectQuery, finish building the query and + // pass it back, as any remaining options are irrelevant. + if (!empty($this->fromQuery)) { + $sql = (string)$this; + // The SelectQuery may contain arguments, load and pass them through. + return $this->connection->query($sql, $this->fromQuery->getArguments(), $this->queryOptions); } + $last_insert_id = 0; + // Each insert happens in its own query in the degenerate case. However, // we wrap it in a transaction so that it is atomic where possible. On many // databases, such as SQLite, this is also a notable performance boost. @@ -490,6 +482,42 @@ public function __toString() { return 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; } + + /** + * Generic preparation and validation for an INSERT query. + * + * @return + * TRUE if the validation was successful, FALSE if not. + */ + protected function preExecute() { + // Confirm that the user did not try to specify an identical + // field and default field. + if (array_intersect($this->insertFields, $this->defaultFields)) { + throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.'); + } + + if (!empty($this->fromQuery)) { + // We have to assume that the used aliases match the insert fields. + // Regular fields are added to the query before expressions, maintain the + // same order for the insert fields. + // This behavior can be overriden by calling fields() manually as only the + // first call to fields() does have an effect. + $this->fields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions()))); + } + + // Don't execute query without fields. + if (count($this->insertFields) + count($this->defaultFields) == 0) { + throw new NoFieldsException('There are no fields available to insert with.'); + } + + // If no values have been added, silently ignore this query. This can happen + // if values are added conditionally, so we don't want to throw an + // exception. + if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) { + return FALSE; + } + return TRUE; + } } /** diff --git a/includes/database/sqlite/query.inc b/includes/database/sqlite/query.inc index 287fc9008d6f300359ffee26caf1ff1601ccc117..35314e8a81e5d3c398c4cdfd8b5a2152de7c28f9 100644 --- a/includes/database/sqlite/query.inc +++ b/includes/database/sqlite/query.inc @@ -21,11 +21,7 @@ class InsertQuery_sqlite extends InsertQuery { public function execute() { - if (count($this->insertFields) + count($this->defaultFields) == 0 && empty($this->fromQuery)) { - return NULL; - } - // Don't execute query without values. - if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) { + if (!$this->preExecute()) { return NULL; } if (count($this->insertFields)) { @@ -40,6 +36,8 @@ public function __toString() { // Produce as many generic placeholders as necessary. $placeholders = array_fill(0, count($this->insertFields), '?'); + // If we're selecting from a SelectQuery, finish building the query and + // pass it back, as any remaining options are irrelevant. if (!empty($this->fromQuery)) { return "INSERT INTO {" . $this->table . '} (' . implode(', ', $this->insertFields) . ') ' . $this->fromQuery; } diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index bae378c9c2b8a69c0f1cc5117689ec85aaae2c9a..c354e9a8ed9b52e65f8ebe10eea6c6c88353a0de 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -518,10 +518,21 @@ class DatabaseInsertTestCase extends DatabaseTestCase { * Test that the INSERT INTO ... SELECT ... syntax works. */ function testInsertSelect() { - $query = db_select('test_people', 'tp')->fields('tp', array('name', 'age', 'job')); + $query = db_select('test_people', 'tp'); + // The query builder will always append expressions after fields. + // Add the expression first to test that the insert fields are correctly + // re-ordered. + $query->addExpression('tp.age', 'age'); + $query + ->fields('tp', array('name','job')) + ->condition('tp.name', 'Meredith'); + // The resulting query should be equivalent to: + // INSERT INTO test (age, name, job) + // SELECT tp.age AS age, tp.name AS name, tp.job AS job + // FROM test_people tp + // WHERE tp.name = 'Meredith' db_insert('test') - ->fields(array('name', 'age', 'job')) ->from($query) ->execute(); @@ -603,8 +614,13 @@ class DatabaseInsertDefaultsTestCase extends DatabaseTestCase { function testDefaultEmptyInsert() { $num_records_before = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField(); - $result = db_insert('test')->execute(); - $this->assertNull($result, t('Return NULL as no fields are specified.')); + try { + $result = db_insert('test')->execute(); + // This is only executed if no exception has been thrown. + $this->fail(t('Expected exception NoFieldsException has not been thrown.')); + } catch (NoFieldsException $e) { + $this->pass(t('Expected exception NoFieldsException has been thrown.')); + } $num_records_after = (int) db_query('SELECT COUNT(*) FROM {test}')->fetchField(); $this->assertIdentical($num_records_before, $num_records_after, t('Do nothing as no fields are specified.'));