Index: includes/database/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/database.inc,v retrieving revision 1.33 diff -u -p -r1.33 database.inc --- includes/database/database.inc 8 Dec 2008 21:41:53 -0000 1.33 +++ includes/database/database.inc 10 Dec 2008 05:08:00 -0000 @@ -349,10 +349,14 @@ abstract class DatabaseConnection extend * @param $query * The query string as SQL, with curly-braces surrounding the * table names. + * @param $query + * Whether or not to cache the prepared statement for later reuse in this + * same request. Usually we want to, but queries that require preprocessing + * cannot be safely cached. * @return * A PDO prepared statement ready for its execute() method. */ - protected function prepareQuery($query) { + protected function prepareQuery($query, $cache = TRUE) { $query = self::prefixTables($query); if (empty($this->preparedStatements[$query])) { // Call PDO::prepare. @@ -473,7 +477,8 @@ abstract class DatabaseConnection extend $stmt->execute(NULL, $options); } else { - $stmt = $this->prepareQuery($query); + $modified = $this->expandArguments($query, $args); + $stmt = $this->prepareQuery($query, !$modified); $stmt->execute($args, $options); } @@ -508,6 +513,50 @@ abstract class DatabaseConnection extend } /** + * Expand out shorthand placeholders. + * + * Drupal supports an alternate syntax for doing arrays of values. We therefore + * need to expand them out into a full, executable query string. + * + * @param $query + * The query string to modify. + * @param $args + * The arguments for the query. + * @return + * TRUE if the query was modified, FALSE otherwise. + */ + protected function expandArguments(&$query, &$args) { + $modified = FALSE; + + foreach ($args as $key => $data) { + // is_array() is slower than checking a string value, so do that first. + if ($key[0] == '@') { + $new_keys = array(); + $base = $key; + $base[0] = ':'; + foreach ($data as $i => $value) { + $candidate_placeholder = $base . '_' . $i; + while (isset($args[$candidate_placeholder])) { + $candidate_placeholder .= mt_rand(); + } + $new_keys[$candidate_placeholder] = $value; + } + + // Update the query with the new placeholders. + $query = str_replace($key, implode(', ', $new_keys), $query); + + // Update the args array with the new placeholders. + unset($args[$key]); + $args += $new_keys; + + $modified = TRUE; + } + } + + return $modified; + } + + /** * Prepare and return a SELECT query object with the specified ID. * * @see SelectQuery Index: modules/simpletest/tests/database_test.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/database_test.test,v retrieving revision 1.25 diff -u -p -r1.25 database_test.test --- modules/simpletest/tests/database_test.test 8 Dec 2008 22:02:45 -0000 1.25 +++ modules/simpletest/tests/database_test.test 10 Dec 2008 05:08:00 -0000 @@ -1856,3 +1856,29 @@ class DatabaseTemporaryQueryTestCase ext $this->assertFalse(db_table_exists('temporary'), t('The temporary table is, indeed, temporary.')); } } + +/** + * Drupal-specific SQL syntax tests. + */ +class DatabaseQueryTestCase extends DatabaseTestCase { + function getInfo() { + return array( + 'name' => t('Custom query syntax tests'), + 'description' => t('Test Drupal\'s extended prepared statement syntax..'), + 'group' => t('Database'), + ); + } + + function setUp() { + parent::setUp('database_test'); + } + + /** + * Confirm that temporary tables work and are limited to one request. + */ + function testArraySubstitution() { + $names = db_query("SELECT name FROM {test} WHERE age IN (@ages) ORDER BY age", array('@ages' => array(25, 26, 27)))->fetchAll(); + + $this->assertEqual(count($names), 3, t('Correct number of names returned')); + } +}