mirror of
https://github.com/phpbb/phpbb.git
synced 2025-06-08 04:18:52 +00:00
If a value is provide for an auto_increment type of column, certain DBMSes do not update their internal sequencers. If a row is inserted later, it can be given an ID that is already in use, resulting in an error. The database test cases now resynchronise the sequencers before the tests are run. PHPBB3-11219
558 lines
14 KiB
PHP
558 lines
14 KiB
PHP
<?php
|
|
/**
|
|
*
|
|
* @package testing
|
|
* @copyright (c) 2011 phpBB Group
|
|
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
|
|
*
|
|
*/
|
|
|
|
require_once dirname(__FILE__) . '/../../phpBB/includes/functions_install.php';
|
|
require_once dirname(__FILE__) . '/phpbb_database_connection_odbc_pdo_wrapper.php';
|
|
|
|
class phpbb_database_test_connection_manager
|
|
{
|
|
private $config;
|
|
private $dbms;
|
|
private $pdo;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param array $config Tests database configuration as returned by
|
|
* phpbb_database_test_case::get_database_config()
|
|
*/
|
|
public function __construct($config)
|
|
{
|
|
$this->config = $config;
|
|
$this->dbms = $this->get_dbms_data($this->config['dbms']);
|
|
}
|
|
|
|
/**
|
|
* Return the current PDO instance
|
|
*/
|
|
public function get_pdo()
|
|
{
|
|
return $this->pdo;
|
|
}
|
|
|
|
/**
|
|
* Creates a PDO connection for the configured database.
|
|
*
|
|
* @param bool $use_db Whether the DSN should be tied to a
|
|
* particular database making it impossible
|
|
* to delete that database.
|
|
*/
|
|
public function connect($use_db = true)
|
|
{
|
|
$dsn = $this->dbms['PDO'] . ':';
|
|
|
|
switch ($this->dbms['PDO'])
|
|
{
|
|
case 'sqlite2':
|
|
$dsn .= $this->config['dbhost'];
|
|
break;
|
|
|
|
case 'sqlsrv':
|
|
// prefix the hostname (or DSN) with Server= so using just (local)\SQLExpress
|
|
// works for example, further parameters can still be appended using ;x=y
|
|
$dsn .= 'Server=';
|
|
// no break -> rest like ODBC
|
|
case 'odbc':
|
|
// for ODBC assume dbhost is a suitable DSN
|
|
// e.g. Driver={SQL Server Native Client 10.0};Server=(local)\SQLExpress;
|
|
$dsn .= $this->config['dbhost'];
|
|
|
|
// Primarily for MSSQL Native/Azure as ODBC needs it in $dbhost, attached to the Server param
|
|
if ($this->config['dbport'])
|
|
{
|
|
$port_delimiter = (defined('PHP_OS') && substr(PHP_OS, 0, 3) === 'WIN') ? ',' : ':';
|
|
$dsn .= $port_delimiter . $this->config['dbport'];
|
|
}
|
|
|
|
if ($use_db)
|
|
{
|
|
$dsn .= ';Database=' . $this->config['dbname'];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
$dsn .= 'host=' . $this->config['dbhost'];
|
|
|
|
if ($this->config['dbport'])
|
|
{
|
|
$dsn .= ';port=' . $this->config['dbport'];
|
|
}
|
|
|
|
if ($use_db)
|
|
{
|
|
$dsn .= ';dbname=' . $this->config['dbname'];
|
|
}
|
|
else if ($this->dbms['PDO'] == 'pgsql')
|
|
{
|
|
// Postgres always connects to a
|
|
// database. If the database is not
|
|
// specified here, but the username
|
|
// is specified, then connection
|
|
// will be to the database named
|
|
// as the username.
|
|
//
|
|
// For greater compatibility, connect
|
|
// instead to postgres database which
|
|
// should always exist:
|
|
// http://www.postgresql.org/docs/9.0/static/manage-ag-templatedbs.html
|
|
$dsn .= ';dbname=postgres';
|
|
}
|
|
break;
|
|
}
|
|
|
|
// These require different connection strings on the phpBB side than they do in PDO
|
|
// so you must provide a DSN string for ODBC separately
|
|
if (!empty($this->config['custom_dsn']) && ($this->config['dbms'] == 'mssql' || $this->config['dbms'] == 'firebird'))
|
|
{
|
|
$dsn = 'odbc:' . $this->config['custom_dsn'];
|
|
}
|
|
|
|
try
|
|
{
|
|
switch ($this->config['dbms'])
|
|
{
|
|
case 'mssql':
|
|
case 'mssql_odbc':
|
|
$this->pdo = new phpbb_database_connection_odbc_pdo_wrapper('mssql', 0, $dsn, $this->config['dbuser'], $this->config['dbpasswd']);
|
|
break;
|
|
|
|
case 'firebird':
|
|
if (!empty($this->config['custom_dsn']))
|
|
{
|
|
$this->pdo = new phpbb_database_connection_odbc_pdo_wrapper('firebird', 0, $dsn, $this->config['dbuser'], $this->config['dbpasswd']);
|
|
break;
|
|
}
|
|
// Fall through if they're using the firebird PDO driver and not the generic ODBC driver
|
|
|
|
default:
|
|
$this->pdo = new PDO($dsn, $this->config['dbuser'], $this->config['dbpasswd']);
|
|
break;
|
|
}
|
|
}
|
|
catch (PDOException $e)
|
|
{
|
|
$cleaned_dsn = str_replace($this->config['dbpasswd'], '*password*', $dsn);
|
|
throw new Exception("Unable do connect to $cleaned_dsn using PDO with error: {$e->getMessage()}");
|
|
}
|
|
|
|
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
}
|
|
|
|
/**
|
|
* Load the phpBB database schema into the database
|
|
*/
|
|
public function load_schema()
|
|
{
|
|
$this->ensure_connected(__METHOD__);
|
|
|
|
$directory = dirname(__FILE__) . '/../../phpBB/install/schemas/';
|
|
$this->load_schema_from_file($directory);
|
|
}
|
|
|
|
/**
|
|
* Drop the database if it exists and re-create it
|
|
*
|
|
* Note: This does not load the schema, and it is suggested
|
|
* to re-connect after calling to get use_db isolation.
|
|
*/
|
|
public function recreate_db()
|
|
{
|
|
switch ($this->config['dbms'])
|
|
{
|
|
case 'sqlite':
|
|
case 'firebird':
|
|
$this->connect();
|
|
// Drop all of the tables
|
|
foreach ($this->get_tables() as $table)
|
|
{
|
|
$this->pdo->exec('DROP TABLE ' . $table);
|
|
}
|
|
$this->purge_extras();
|
|
break;
|
|
|
|
case 'oracle':
|
|
$this->connect();
|
|
// Drop all of the tables
|
|
foreach ($this->get_tables() as $table)
|
|
{
|
|
$this->pdo->exec('DROP TABLE ' . $table . ' CASCADE CONSTRAINTS');
|
|
}
|
|
$this->purge_extras();
|
|
break;
|
|
|
|
default:
|
|
$this->connect(false);
|
|
|
|
try
|
|
{
|
|
$this->pdo->exec('DROP DATABASE ' . $this->config['dbname']);
|
|
|
|
try
|
|
{
|
|
$this->pdo->exec('CREATE DATABASE ' . $this->config['dbname']);
|
|
}
|
|
catch (PDOException $e)
|
|
{
|
|
throw new Exception("Unable to re-create database: {$e->getMessage()}");
|
|
}
|
|
}
|
|
catch (PDOException $e)
|
|
{
|
|
// try to delete all tables if dropping the database was not possible.
|
|
foreach ($this->get_tables() as $table)
|
|
{
|
|
$this->pdo->exec('DROP TABLE ' . $table);
|
|
}
|
|
$this->purge_extras();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves a list of all tables from the database.
|
|
*
|
|
* @return array(string)
|
|
*/
|
|
public function get_tables()
|
|
{
|
|
$this->ensure_connected(__METHOD__);
|
|
|
|
switch ($this->config['dbms'])
|
|
{
|
|
case 'mysql':
|
|
case 'mysql4':
|
|
case 'mysqli':
|
|
$sql = 'SHOW TABLES';
|
|
break;
|
|
|
|
case 'sqlite':
|
|
$sql = 'SELECT name
|
|
FROM sqlite_master
|
|
WHERE type = "table"';
|
|
break;
|
|
|
|
case 'mssql':
|
|
case 'mssql_odbc':
|
|
case 'mssqlnative':
|
|
$sql = "SELECT name
|
|
FROM sysobjects
|
|
WHERE type='U'";
|
|
break;
|
|
|
|
case 'postgres':
|
|
$sql = 'SELECT relname
|
|
FROM pg_stat_user_tables';
|
|
break;
|
|
|
|
case 'firebird':
|
|
$sql = 'SELECT rdb$relation_name
|
|
FROM rdb$relations
|
|
WHERE rdb$view_source is null
|
|
AND rdb$system_flag = 0';
|
|
break;
|
|
|
|
case 'oracle':
|
|
$sql = 'SELECT table_name
|
|
FROM USER_TABLES';
|
|
break;
|
|
}
|
|
|
|
$result = $this->pdo->query($sql);
|
|
|
|
$tables = array();
|
|
while ($row = $result->fetch(PDO::FETCH_NUM))
|
|
{
|
|
$tables[] = current($row);
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Throw an exception if not connected
|
|
*/
|
|
protected function ensure_connected($method_name)
|
|
{
|
|
if (null === $this->pdo)
|
|
{
|
|
throw new Exception(sprintf('You must connect before calling %s', $method_name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compile the correct schema filename (as per create_schema_files) and
|
|
* load it into the database.
|
|
*/
|
|
protected function load_schema_from_file($directory)
|
|
{
|
|
$schema = $this->dbms['SCHEMA'];
|
|
|
|
if ($this->config['dbms'] == 'mysql')
|
|
{
|
|
$sth = $this->pdo->query('SELECT VERSION() AS version');
|
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (version_compare($row['version'], '4.1.3', '>='))
|
|
{
|
|
$schema .= '_41';
|
|
}
|
|
else
|
|
{
|
|
$schema .= '_40';
|
|
}
|
|
}
|
|
|
|
$filename = $directory . $schema . '_schema.sql';
|
|
|
|
$queries = file_get_contents($filename);
|
|
$sql = phpbb_remove_comments($queries);
|
|
|
|
$sql = split_sql_file($sql, $this->dbms['DELIM']);
|
|
|
|
foreach ($sql as $query)
|
|
{
|
|
$this->pdo->exec($query);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map a phpBB dbms driver name to dbms data array
|
|
*/
|
|
protected function get_dbms_data($dbms)
|
|
{
|
|
$available_dbms = array(
|
|
'firebird' => array(
|
|
'SCHEMA' => 'firebird',
|
|
'DELIM' => ';;',
|
|
'PDO' => 'firebird',
|
|
),
|
|
'mysqli' => array(
|
|
'SCHEMA' => 'mysql_41',
|
|
'DELIM' => ';',
|
|
'PDO' => 'mysql',
|
|
),
|
|
'mysql' => array(
|
|
'SCHEMA' => 'mysql',
|
|
'DELIM' => ';',
|
|
'PDO' => 'mysql',
|
|
),
|
|
'mssql' => array(
|
|
'SCHEMA' => 'mssql',
|
|
'DELIM' => 'GO',
|
|
'PDO' => 'odbc',
|
|
),
|
|
'mssql_odbc'=> array(
|
|
'SCHEMA' => 'mssql',
|
|
'DELIM' => 'GO',
|
|
'PDO' => 'odbc',
|
|
),
|
|
'mssqlnative' => array(
|
|
'SCHEMA' => 'mssql',
|
|
'DELIM' => 'GO',
|
|
'PDO' => 'sqlsrv',
|
|
),
|
|
'oracle' => array(
|
|
'SCHEMA' => 'oracle',
|
|
'DELIM' => '/',
|
|
'PDO' => 'oci',
|
|
),
|
|
'postgres' => array(
|
|
'SCHEMA' => 'postgres',
|
|
'DELIM' => ';',
|
|
'PDO' => 'pgsql',
|
|
),
|
|
'sqlite' => array(
|
|
'SCHEMA' => 'sqlite',
|
|
'DELIM' => ';',
|
|
'PDO' => 'sqlite2',
|
|
),
|
|
);
|
|
|
|
if (isset($available_dbms[$dbms]))
|
|
{
|
|
return $available_dbms[$dbms];
|
|
}
|
|
else
|
|
{
|
|
$message = "Supplied dbms \"$dbms\" is not a valid phpBB dbms, must be one of: ";
|
|
$message .= implode(', ', array_keys($available_dbms));
|
|
throw new Exception($message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes extra objects from a database. This is for cases where dropping the database fails.
|
|
*/
|
|
public function purge_extras()
|
|
{
|
|
$this->ensure_connected(__METHOD__);
|
|
$queries = array();
|
|
|
|
switch ($this->config['dbms'])
|
|
{
|
|
case 'firebird':
|
|
$sql = 'SELECT RDB$GENERATOR_NAME
|
|
FROM RDB$GENERATORS
|
|
WHERE RDB$SYSTEM_FLAG = 0';
|
|
$result = $this->pdo->query($sql);
|
|
|
|
while ($row = $result->fetch(PDO::FETCH_NUM))
|
|
{
|
|
$queries[] = 'DROP GENERATOR ' . current($row);
|
|
}
|
|
break;
|
|
|
|
case 'oracle':
|
|
$sql = 'SELECT sequence_name
|
|
FROM USER_SEQUENCES';
|
|
$result = $this->pdo->query($sql);
|
|
|
|
while ($row = $result->fetch(PDO::FETCH_NUM))
|
|
{
|
|
$queries[] = 'DROP SEQUENCE ' . current($row);
|
|
}
|
|
break;
|
|
}
|
|
|
|
foreach ($queries as $query)
|
|
{
|
|
$this->pdo->exec($query);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs synchronisations on the database after a fixture has been loaded
|
|
*/
|
|
public function post_setup_synchronisation()
|
|
{
|
|
$this->ensure_connected(__METHOD__);
|
|
$queries = array();
|
|
|
|
switch ($this->config['dbms'])
|
|
{
|
|
case 'oracle':
|
|
// Get all of the information about the sequences
|
|
$sql = "SELECT t.table_name, tc.column_name, d.referenced_name as sequence_name
|
|
FROM USER_TRIGGERS t
|
|
JOIN USER_DEPENDENCIES d on d.name = t.trigger_name
|
|
JOIN USER_TRIGGER_COLS tc on (tc.trigger_name = t.trigger_name)
|
|
WHERE d.referenced_type = 'SEQUENCE'
|
|
AND d.type = 'TRIGGER'";
|
|
$result = $this->pdo->query($sql);
|
|
|
|
while ($row = $result->fetch(PDO::FETCH_ASSOC))
|
|
{
|
|
// Get the current max value of the table
|
|
$sql = "SELECT MAX({$row['COLUMN_NAME']}) + 1 FROM {$row['TABLE_NAME']}";
|
|
|
|
$max_result = $this->pdo->query($sql);
|
|
$max_row = $max_result->fetch(PDO::FETCH_NUM);
|
|
|
|
if (!$max_row)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$maxval = current($max_row);
|
|
if ($maxval == null)
|
|
{
|
|
$maxval = 1;
|
|
}
|
|
|
|
// Get the sequence's next value
|
|
$sql = "SELECT {$row['SEQUENCE_NAME']}.nextval FROM dual";
|
|
try
|
|
{
|
|
$nextval_result = $this->pdo->query($sql);
|
|
}
|
|
catch (PDOException $e)
|
|
{
|
|
// If we catch an exception here it's because the sequencer hasn't been initialized yet.
|
|
// If the table hasn't been used, then there's nothing to do.
|
|
continue;
|
|
}
|
|
$nextval_row = $nextval_result->fetch(PDO::FETCH_NUM);
|
|
|
|
if ($nextval_row)
|
|
{
|
|
$nextval = current($nextval_row);
|
|
|
|
// Make sure we aren't setting the new increment to zero.
|
|
if ($maxval != $nextval)
|
|
{
|
|
// This is a multi-step process. First we need to get the sequence back into position.
|
|
// That means either advancing it or moving it backwards. Sequences have a minimum value
|
|
// of 1, so you cannot go past that. Once the offset it determined, you have to request
|
|
// the next sequence value to actually move the pointer into position. So if you're at 21
|
|
// and need to be back at 1, set the incrementer to -20. When it's requested, it'll give
|
|
// you 1. Then we have to set the increment amount back to 1 to resume normal behavior.
|
|
|
|
// Move the sequence to the correct position.
|
|
$sql = "ALTER SEQUENCE {$row['SEQUENCE_NAME']} INCREMENT BY " . ($maxval - $nextval);
|
|
$this->pdo->exec($sql);
|
|
|
|
// Select the next value to actually update the sequence values
|
|
$sql = "SELECT {$row['SEQUENCE_NAME']}.nextval FROM dual";
|
|
$this->pdo->query($sql);
|
|
|
|
// Reset the sequence's increment amount
|
|
$sql = "ALTER SEQUENCE {$row['SEQUENCE_NAME']} INCREMENT BY 1";
|
|
$this->pdo->exec($sql);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'postgres':
|
|
// First get the sequences
|
|
$sequences = array();
|
|
$sql = "SELECT relname FROM pg_class WHERE relkind = 'S'";
|
|
$result = $this->pdo->query($sql);
|
|
while ($row = $result->fetch(PDO::FETCH_ASSOC))
|
|
{
|
|
$sequences[] = $row['relname'];
|
|
}
|
|
|
|
// Now get the name of the column using it
|
|
foreach ($sequences as $sequence)
|
|
{
|
|
$table = str_replace('_seq', '', $sequence);
|
|
$sql = "SELECT column_name FROM information_schema.columns
|
|
WHERE table_name = '$table'
|
|
AND column_default = 'nextval(''$sequence''::regclass)'";
|
|
$result = $this->pdo->query($sql);
|
|
$row = $result->fetch(PDO::FETCH_ASSOC);
|
|
|
|
// Finally, set the new sequence value
|
|
if ($row)
|
|
{
|
|
$column = $row['column_name'];
|
|
|
|
// Get the old value if it exists, or use 1 if it doesn't
|
|
$sql = "SELECT COALESCE((SELECT MAX({$column}) + 1 FROM {$table}), 1) AS val";
|
|
$result = $this->pdo->query($sql);
|
|
$row = $result->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($row)
|
|
{
|
|
// The last parameter is false so that the system doesn't increment it again
|
|
$queries[] = "SELECT SETVAL('{$sequence}', {$row['val']}, false)";
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
foreach ($queries as $query)
|
|
{
|
|
$this->pdo->exec($query);
|
|
}
|
|
}
|
|
}
|