diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php index e2b9821..d195a94 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php @@ -24,64 +24,75 @@ class Statement extends StatementPrefetch implements StatementInterface { /** * {@inheritdoc} * - * The PDO SQLite layer doesn't replace numeric placeholders in queries - * correctly, and this makes numeric expressions (such as COUNT(*) >= :count) - * fail. We replace numeric placeholders in the query ourselves to work - * around this bug. - * - * See http://bugs.php.net/bug.php?id=45259 for more details. + * SQLite only supports 999 placeholders which is not sufficient in many cases. + * Using PDO::ATTR_EMULATE_PREPARES attribute would be the simplest solution for this but + * the SQLite PDO driver does not support it. So we do it here instead. + * Drupal does not utilise prepared statements much anyway so this is no performance burden. */ protected function getStatement($query, &$args = array()) { + $dbh = $this->dbh; + $quote_value = function($value) use($dbh) { + if (is_null($value)) { + return 'NULL'; + } + elseif (is_float($value)) { + // Force the conversion to float so as not to lose precision in the automatic cast. + $value = sprintf('%F', $value); + return $value; + } + elseif (is_int($value)) { + return $value; + } + else { + // In this place we can't tell if it's a string or a BLOB. Fortunately + // SQLite eats LOB-encoded strings and connection encoding is UTF8. + $value = $dbh->quote($value, \PDO::PARAM_LOB); + return $value; + } + }; + if (count($args)) { // Check if $args is a simple numeric array. if (range(0, count($args) - 1) === array_keys($args)) { // In that case, we have unnamed placeholders. - $count = 0; - $new_args = array(); - foreach ($args as $value) { - if (is_float($value) || is_int($value)) { - if (is_float($value)) { - // Force the conversion to float so as not to loose precision - // in the automatic cast. - $value = sprintf('%F', $value); - } - $query = substr_replace($query, $value, strpos($query, '?'), 1); - } - else { - $placeholder = ':db_statement_placeholder_' . $count++; - $query = substr_replace($query, $placeholder, strpos($query, '?'), 1); - $new_args[$placeholder] = $value; - } - } - $args = $new_args; + // Now let's replace every '?' placeholder with one item from $replacements. + $get_replacement = function() use($args, $quote_value) { + static $i = 0; + $value = $args[$i]; + $i += 1; + return $quote_value($value); + }; + $query = preg_replace_callback('/\?/u', $get_replacement, $query); } else { // Else, this is using named placeholders. - foreach ($args as $placeholder => $value) { - if (is_float($value) || is_int($value)) { - if (is_float($value)) { - // Force the conversion to float so as not to loose precision - // in the automatic cast. - $value = sprintf('%F', $value); + $get_replacement = function($match) use($args, $quote_value) { + $placeholder = $match[0]; + static $replacements = array(); + if (!isset($replacements[$placeholder])) { + if (isset($args[$placeholder])) { + $replacements[$placeholder] = $quote_value($args[$placeholder]); } - - // We will remove this placeholder from the query as PDO throws an - // exception if the number of placeholders in the query and the - // arguments does not match. - unset($args[$placeholder]); - // PDO allows placeholders to not be prefixed by a colon. See - // http://marc.info/?l=php-internals&m=111234321827149&w=2 for - // more. - if ($placeholder[0] != ':') { - $placeholder = ":$placeholder"; + else { + // PDO allows placeholders to not be prefixed by a colon. See + // http://marc.info/?l=php-internals&m=111234321827149&w=2 for more. + $placeholder_without_colon = substr($placeholder, 1); + if (isset($args[$placeholder_without_colon])) { + $replacements[$placeholder] = $quote_value($args[$placeholder]); + } + else { + $replacements[$placeholder] = $placeholder; + } } - // When replacing the placeholders, make sure we search for the - // exact placeholder. For example, if searching for - // ':db_placeholder_1', do not replace ':db_placeholder_11'. - $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query); + return $replacements[$placeholder]; } - } + }; + // We match any valid php identifier after a colon. + $query = preg_replace_callback('/:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\b/u', $get_replacement, $query); } + + // We replaced all placeholders so there must not be any arguments any more. + $args = array(); } return $this->pdoConnection->prepare($query); diff --git a/core/tests/Drupal/KernelTests/Core/Database/Driver/sqlite/SqliteStatementTest.php b/core/tests/Drupal/KernelTests/Core/Database/Driver/sqlite/SqliteStatementTest.php new file mode 100644 index 0000000..3835408 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/Driver/sqlite/SqliteStatementTest.php @@ -0,0 +1,106 @@ +fields('t', array('age')) + ->range(0, 1); + + $range = array_map('intval', range(1, $this->number_of_variables)); + $query->condition('t.age', $range, 'not in'); + + $has_exception = FALSE; + try { + $query->execute(); + } + catch (DatabaseExceptionWrapper $e) { + $has_exception = TRUE; + } + + $this->assertFalse($has_exception, 'DB driver can handle >999 SQL integer variables.'); + } + + /** + * @covers ::getStatement + */ + public function testManyFloatVariables() { + $query = db_select('test', 't') + ->fields('t', array('age')) + ->range(0, 1); + + $range = array_map('floatval', range(1, $this->number_of_variables)); + $query->condition('t.age', $range, 'not in'); + + $has_exception = FALSE; + try { + $query->execute(); + } + catch (DatabaseExceptionWrapper $e) { + $has_exception = TRUE; + } + + $this->assertFalse($has_exception, 'DB driver can handle >999 SQL float variables.'); + } + + /** + * @covers ::getStatement + */ + public function testManyStringVariables() { + $query = db_select('test', 't') + ->fields('t', array('name')) + ->range(0, 1); + + $range = array_map('strval', range(1, $this->number_of_variables)); + $query->condition('t.name', $range, 'not in'); + + $has_exception = FALSE; + try { + $query->execute(); + } + catch (DatabaseExceptionWrapper $e) { + $has_exception = TRUE; + } + + $this->assertFalse($has_exception, 'DB driver can handle >999 SQL string variables.'); + } + + /** + * @covers ::getStatement + */ + public function testManyBlobVariables() { + $query = db_select('test_one_blob', 't') + ->fields('t', array('blob1')) + ->range(0, 1); + + $range = array_map(function($number) { return "This is\000a test: $number."; }, range(1, $this->number_of_variables)); + $query->condition('t.blob1', $range, 'not in'); + + $has_exception = FALSE; + try { + $query->execute(); + } + catch (DatabaseExceptionWrapper $e) { + $has_exception = TRUE; + } + + $this->assertFalse($has_exception, 'DB driver can handle >999 SQL blob variables.'); + } + +}