Commit 6d332767 authored by mcdruid's avatar mcdruid

Issue #1713332 by amateescu, mcdruid, Xen, droplet, pietmarcus, bzrudi71,...

Issue #1713332 by amateescu, mcdruid, Xen, droplet, pietmarcus, bzrudi71, znerol, czigor, marcelovani, TR, wulff, pp, Damien Tournoud, David_Rothstein, webchick, apassi, dcrocks, opdavies, Mixologic, xjm, zserno, andypost, Pancho, catch, gaas, Fabianx, Crell, alexpott: The SQLite database driver fails to drop simpletest tables
parent 997f000a
......@@ -310,6 +310,13 @@ abstract class DatabaseConnection extends PDO {
*/
protected $escapedAliases = array();
/**
* List of un-prefixed table names, keyed by prefixed table names.
*
* @var array
*/
protected $unprefixedTablesMap = array();
function __construct($dsn, $username, $password, $driver_options = array()) {
// Initialize and prepare the connection prefix.
$this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
......@@ -338,7 +345,9 @@ public function destroy() {
// Destroy all references to this connection by setting them to NULL.
// The Statement class attribute only accepts a new value that presents a
// proper callable, so we reset it to PDOStatement.
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
if (!empty($this->statementClass)) {
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
}
$this->schema = NULL;
}
......@@ -442,6 +451,13 @@ protected function setPrefix($prefix) {
$this->prefixReplace[] = $this->prefixes['default'];
$this->prefixSearch[] = '}';
$this->prefixReplace[] = '';
// Set up a map of prefixed => un-prefixed tables.
foreach ($this->prefixes as $table_name => $prefix) {
if ($table_name !== 'default') {
$this->unprefixedTablesMap[$prefix . $table_name] = $table_name;
}
}
}
/**
......@@ -477,6 +493,17 @@ public function tablePrefix($table = 'default') {
}
}
/**
* Gets a list of individually prefixed table names.
*
* @return array
* An array of un-prefixed table names, keyed by their fully qualified table
* names (i.e. prefix + table_name).
*/
public function getUnprefixedTablesMap() {
return $this->unprefixedTablesMap;
}
/**
* Prepares a query string and returns the prepared statement.
*
......@@ -2840,7 +2867,6 @@ function db_field_exists($table, $field) {
*
* @param $table_expression
* An SQL expression, for example "simpletest%" (without the quotes).
* BEWARE: this is not prefixed, the caller should take care of that.
*
* @return
* Array, both the keys and the values are the matching tables.
......@@ -2849,6 +2875,23 @@ function db_find_tables($table_expression) {
return Database::getConnection()->schema()->findTables($table_expression);
}
/**
* Finds all tables that are like the specified base table name. This is a
* backport of the change made to db_find_tables in Drupal 8 to work with
* virtual, un-prefixed table names. The original function is retained for
* Backwards Compatibility.
* @see https://www.drupal.org/node/2552435
*
* @param $table_expression
* An SQL expression, for example "simpletest%" (without the quotes).
*
* @return
* Array, both the keys and the values are the matching tables.
*/
function db_find_tables_d8($table_expression) {
return Database::getConnection()->schema()->findTablesD8($table_expression);
}
function _db_create_keys_sql($spec) {
return Database::getConnection()->schema()->createKeysSql($spec);
}
......
......@@ -169,6 +169,11 @@
*/
abstract class DatabaseSchema implements QueryPlaceholderInterface {
/**
* The database connection.
*
* @var DatabaseConnection
*/
protected $connection;
/**
......@@ -346,6 +351,69 @@ public function findTables($table_expression) {
return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
}
/**
* Finds all tables that are like the specified base table name. This is a
* backport of the change made to findTables in Drupal 8 to work with virtual,
* un-prefixed table names. The original function is retained for Backwards
* Compatibility.
* @see https://www.drupal.org/node/2552435
*
* @param string $table_expression
* An SQL expression, for example "cache_%" (without the quotes).
*
* @return array
* Both the keys and the values are the matching tables.
*/
public function findTablesD8($table_expression) {
// Load all the tables up front in order to take into account per-table
// prefixes. The actual matching is done at the bottom of the method.
$condition = $this->buildTableNameCondition('%', 'LIKE');
$condition->compile($this->connection, $this);
$individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
$default_prefix = $this->connection->tablePrefix();
$default_prefix_length = strlen($default_prefix);
$tables = array();
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
$results = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
foreach ($results as $table) {
// Take into account tables that have an individual prefix.
if (isset($individually_prefixed_tables[$table->table_name])) {
$prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name]));
}
elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) {
// This table name does not start the default prefix, which means that
// it is not managed by Drupal so it should be excluded from the result.
continue;
}
else {
$prefix_length = $default_prefix_length;
}
// Remove the prefix from the returned tables.
$unprefixed_table_name = substr($table->table_name, $prefix_length);
// The pattern can match a table which is the same as the prefix. That
// will become an empty string when we remove the prefix, which will
// probably surprise the caller, besides not being a prefixed table. So
// remove it.
if (!empty($unprefixed_table_name)) {
$tables[$unprefixed_table_name] = $unprefixed_table_name;
}
}
// Convert the table expression from its SQL LIKE syntax to a regular
// expression and escape the delimiter that will be used for matching.
$table_expression = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($table_expression, '/'));
$tables = preg_grep('/^' . $table_expression . '$/i', $tables);
return $tables;
}
/**
* Check if a column exists in the given table.
*
......
......@@ -140,10 +140,10 @@ public function __destruct() {
$count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField();
// We can prune the database file if it doesn't have any tables.
if ($count == 0) {
// Detach the database.
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
// Destroy the database file.
if ($count == 0 && $this->connectionOptions['database'] != ':memory:') {
// Detaching the database fails at this point, but no other queries
// are executed after the connection is destructed so we can simply
// remove the database file.
unlink($this->connectionOptions['database'] . '-' . $prefix);
}
}
......@@ -155,6 +155,18 @@ public function __destruct() {
}
}
/**
* Gets all the attached databases.
*
* @return array
* An array of attached database names.
*
* @see DatabaseConnection_sqlite::__construct().
*/
public function getAttachedDatabases() {
return $this->attachedDatabases;
}
/**
* SQLite compatibility implementation for the IF() SQL function.
*/
......
......@@ -668,6 +668,9 @@ public function fieldSetNoDefault($table, $field) {
$this->alterTable($table, $old_schema, $new_schema);
}
/**
* {@inheritdoc}
*/
public function findTables($table_expression) {
// Don't add the prefix, $table_expression already includes the prefix.
$info = $this->getPrefixInfo($table_expression, FALSE);
......@@ -680,4 +683,32 @@ public function findTables($table_expression) {
));
return $result->fetchAllKeyed(0, 0);
}
/**
* {@inheritdoc}
*/
public function findTablesD8($table_expression) {
$tables = array();
// The SQLite implementation doesn't need to use the same filtering strategy
// as the parent one because individually prefixed tables live in their own
// schema (database), which means that neither the main database nor any
// attached one will contain a prefixed table name, so we just need to loop
// over all known schemas and filter by the user-supplied table expression.
$attached_dbs = $this->connection->getAttachedDatabases();
foreach ($attached_dbs as $schema) {
// Can't use query placeholders for the schema because the query would
// have to be :prefixsqlite_master, which does not work. We also need to
// ignore the internal SQLite tables.
$result = db_query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array(
':type' => 'table',
':table_name' => $table_expression,
':pattern' => 'sqlite_%',
));
$tables += $result->fetchAllKeyed(0, 0);
}
return $tables;
}
}
......@@ -1677,15 +1677,12 @@ protected function tearDown() {
file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10));
// Remove all prefixed tables.
$tables = db_find_tables($this->databasePrefix . '%');
$connection_info = Database::getConnectionInfo('default');
$tables = db_find_tables($connection_info['default']['prefix']['default'] . '%');
$tables = db_find_tables_d8('%');
if (empty($tables)) {
$this->fail('Failed to find test tables to drop.');
}
$prefix_length = strlen($connection_info['default']['prefix']['default']);
foreach ($tables as $table) {
if (db_drop_table(substr($table, $prefix_length))) {
if (db_drop_table($table)) {
unset($tables[$table]);
}
}
......
......@@ -563,7 +563,7 @@ function simpletest_clean_environment() {
* Removed prefixed tables from the database that are left over from crashed tests.
*/
function simpletest_clean_database() {
$tables = db_find_tables(Database::getConnection()->prefixTables('{simpletest}') . '%');
$tables = db_find_tables_d8(Database::getConnection()->prefixTables('{simpletest}') . '%');
$schema = drupal_get_schema_unprocessed('simpletest');
$count = 0;
foreach (array_diff_key($tables, $schema) as $table) {
......
......@@ -381,4 +381,60 @@ class SchemaTestCase extends DrupalWebTestCase {
db_drop_field($table_name, $field_name);
}
/**
* Tests the findTables() method.
*/
public function testFindTables() {
// We will be testing with three tables, two of them using the default
// prefix and the third one with an individually specified prefix.
// Set up a new connection with different connection info.
$connection_info = Database::getConnectionInfo();
// Add per-table prefix to the second table.
$new_connection_info = $connection_info['default'];
$new_connection_info['prefix']['test_2_table'] = $new_connection_info['prefix']['default'] . '_shared_';
Database::addConnectionInfo('test', 'default', $new_connection_info);
Database::setActiveConnection('test');
// Create the tables.
$table_specification = array(
'description' => 'Test table.',
'fields' => array(
'id' => array(
'type' => 'int',
'default' => NULL,
),
),
);
Database::getConnection()->schema()->createTable('test_1_table', $table_specification);
Database::getConnection()->schema()->createTable('test_2_table', $table_specification);
Database::getConnection()->schema()->createTable('the_third_table', $table_specification);
// Check the "all tables" syntax.
$tables = Database::getConnection()->schema()->findTablesD8('%');
sort($tables);
$expected = array(
'test_1_table',
// This table uses a per-table prefix, yet it is returned as un-prefixed.
'test_2_table',
'the_third_table',
);
$this->assertTrue(!array_diff($expected, $tables), 'All tables were found.');
// Check the restrictive syntax.
$tables = Database::getConnection()->schema()->findTablesD8('test_%');
sort($tables);
$expected = array(
'test_1_table',
'test_2_table',
);
$this->assertEqual($tables, $expected, 'Two tables were found.');
// Go back to the initial connection.
Database::setActiveConnection('default');
}
}
......@@ -28,7 +28,7 @@ class ModuleTestCase extends DrupalWebTestCase {
* specified base table. Defaults to TRUE.
*/
function assertTableCount($base_table, $count = TRUE) {
$tables = db_find_tables(Database::getConnection()->prefixTables('{' . $base_table . '}') . '%');
$tables = db_find_tables_d8($base_table . '%');
if ($count) {
return $this->assertTrue($tables, format_string('Tables matching "@base_table" found.', array('@base_table' => $base_table)));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment