Commit 4c7b4763 authored by catch's avatar catch

Issue #1031122 by alexpott, greg.1.anderson, catch, sun, stefan.r, pwolanin:...

Issue #1031122 by alexpott, greg.1.anderson, catch, sun, stefan.r, pwolanin: postgres changeField() is unable to convert to bytea column type correctly
parent d551f8c9
......@@ -34,6 +34,10 @@ public function __construct() {
'function' => 'checkBinaryOutput',
'arguments' => array(),
);
$this->tasks[] = array(
'function' => 'checkStandardConformingStrings',
'arguments' => array(),
);
$this->tasks[] = array(
'function' => 'initializeDatabase',
'arguments' => array(),
......@@ -184,6 +188,58 @@ protected function checkBinaryOutputSuccess() {
return ($bytea_output == 'escape');
}
/**
* Ensures standard_conforming_strings setting is 'on'.
*
* When standard_conforming_strings setting is 'on' string literals ('...')
* treat backslashes literally, as specified in the SQL standard. This allows
* Drupal to convert between bytea, text and varchar columns.
*/
public function checkStandardConformingStrings() {
$database_connection = Database::getConnection();
if (!$this->checkStandardConformingStringsSuccess()) {
// First try to alter the database. If it fails, raise an error telling
// the user to do it themselves.
$connection_options = $database_connection->getConnectionOptions();
// It is safe to include the database name directly here, because this
// code is only called when a connection to the database is already
// established, thus the database name is guaranteed to be a correct
// value.
$query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';";
try {
$database_connection->query($query);
}
catch (\Exception $e) {
// Ignore possible errors when the user doesn't have the necessary
// privileges to ALTER the database.
}
// Close the database connection so that the configuration parameter
// is applied to the current connection.
Database::closeConnection();
// Recheck, if it fails, finally just rely on the end user to do the
// right thing.
if (!$this->checkStandardConformingStringsSuccess()) {
$replacements = array(
'%setting' => 'standard_conforming_strings',
'%current_value' => 'off',
'%needed_value' => 'on',
'!query' => "<code>" . $query . "</code>",
);
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
}
}
}
/**
* Verifies the standard_conforming_strings setting.
*/
protected function checkStandardConformingStringsSuccess() {
$standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField();
return ($standard_conforming_strings == 'on');
}
/**
* Make PostgreSQL Drupal friendly.
*/
......
......@@ -726,17 +726,23 @@ public function changeField($table, $field, $field_new, $spec, $new_keys = array
// Usually, we do this via a simple typecast 'USING fieldname::type'. But
// the typecast does not work for conversions to bytea.
// @see http://www.postgresql.org/docs/current/static/datatype-binary.html
$table_information = $this->queryTableInformation($table);
$is_bytea = !empty($table_information->blob_fields[$field]);
if ($spec['pgsql_type'] != 'bytea') {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
if ($is_bytea) {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING convert_from("' . $field . '"' . ", 'UTF8')");
}
else {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
}
}
else {
// Do not attempt to convert a field that is bytea already.
$table_information = $this->queryTableInformation($table);
if (!in_array($field, $table_information->blob_fields)) {
if (!$is_bytea) {
// Convert to a bytea type by using the SQL replace() function to
// convert any single backslashes in the field content to double
// backslashes ('\' to '\\').
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", '\\', '\\\\'), 'escape');");
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", E'\\\\', E'\\\\\\\\'), 'escape');");
}
}
......
......@@ -225,6 +225,14 @@ protected function setUp() {
// the event dispatcher which can prevent modules from registering events.
\Drupal::service('config.storage')->write('core.extension', array('module' => array(), 'theme' => array()));
// Ensure database tasks have been run.
require_once __DIR__ . '/../../../includes/install.inc';
$connection = Database::getConnection();
$errors = db_installer_object($connection->driver())->runTasks();
if (!empty($errors)) {
$this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
}
// Collect and set a fixed module list.
$class = get_class($this);
$modules = array();
......
......@@ -68,6 +68,14 @@ function testSetUp() {
// Verify that the settings.testing.php got taken into account.
$this->assertTrue(function_exists('simpletest_test_stub_settings_function'));
// Ensure that the database tasks have been run during set up. Neither MySQL
// nor SQLite make changes that are testable.
$database = $this->container->get('database');
if ($database->driver() == 'pgsql') {
$this->assertEqual('on', $database->query("SHOW standard_conforming_strings")->fetchField());
$this->assertEqual('escape', $database->query("SHOW bytea_output")->fetchField());
}
}
/**
......
......@@ -640,7 +640,7 @@ protected function assertFieldCharacteristics($table_name, $field_name, $field_s
}
/**
* Tests changing columns between numeric types.
* Tests changing columns between types.
*/
function testSchemaChangeField() {
$field_specs = array(
......@@ -661,6 +661,27 @@ function testSchemaChangeField() {
$this->assertFieldChange($old_spec, $new_spec);
}
}
$field_specs = array(
array('type' => 'varchar_ascii', 'length' => '255'),
array('type' => 'varchar', 'length' => '255'),
array('type' => 'text'),
array('type' => 'blob', 'size' => 'big'),
);
foreach ($field_specs as $i => $old_spec) {
foreach ($field_specs as $j => $new_spec) {
if ($i === $j) {
// Do not change a field into itself.
continue;
}
// Note if the serialized data contained an object this would fail on
// Postgres.
// @see https://www.drupal.org/node/1031122
$this->assertFieldChange($old_spec, $new_spec, serialize(['string' => "This \n has \\\\ some backslash \"*string action.\\n"]));
}
}
}
/**
......@@ -671,7 +692,7 @@ function testSchemaChangeField() {
* @param $new_spec
* The ending field specification.
*/
protected function assertFieldChange($old_spec, $new_spec) {
protected function assertFieldChange($old_spec, $new_spec, $test_data = NULL) {
$table_name = 'test_table_' . ($this->counter++);
$table_spec = array(
'fields' => array(
......@@ -689,9 +710,24 @@ protected function assertFieldChange($old_spec, $new_spec) {
// Remove inserted rows.
db_truncate($table_name)->execute();
if ($test_data) {
$id = db_insert($table_name)
->fields(['test_field'], [$test_data])
->execute();
}
// Change the field.
db_change_field($table_name, 'test_field', 'test_field', $new_spec);
if ($test_data) {
$field_value = db_select($table_name)
->fields($table_name, ['test_field'])
->condition('serial_column', $id)
->execute()
->fetchField();
$this->assertIdentical($field_value, $test_data);
}
// Check the field was changed.
$this->assertFieldCharacteristics($table_name, 'test_field', $new_spec);
......
......@@ -44,6 +44,14 @@ public function testDatabaseLoaded() {
$this->assertEqual(\Drupal::config('system.site')->get('name'), 'Site-Install');
$this->drupalGet('<front>');
$this->assertText('Site-Install');
// Ensure that the database tasks have been run during set up. Neither MySQL
// nor SQLite make changes that are testable.
$database = $this->container->get('database');
if ($database->driver() == 'pgsql') {
$this->assertEqual('on', $database->query("SHOW standard_conforming_strings")->fetchField());
$this->assertEqual('escape', $database->query("SHOW bytea_output")->fetchField());
}
}
/**
......
......@@ -343,6 +343,14 @@ private function bootKernel() {
// register() is only called if a new container was built/compiled.
$this->container = $kernel->getContainer();
// Ensure database tasks have been run.
require_once __DIR__ . '/../../../includes/install.inc';
$connection = Database::getConnection();
$errors = db_installer_object($connection->driver())->runTasks();
if (!empty($errors)) {
$this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
}
if ($modules) {
$this->container->get('module_handler')->loadAll();
}
......
......@@ -74,8 +74,8 @@ public function testSetUp() {
$GLOBALS['destroy-me'] = TRUE;
$this->assertArrayHasKey('destroy-me', $GLOBALS);
$schema = $this->container->get('database')->schema();
$schema->createTable('foo', array(
$database = $this->container->get('database');
$database->schema()->createTable('foo', array(
'fields' => array(
'number' => array(
'type' => 'int',
......@@ -84,7 +84,14 @@ public function testSetUp() {
),
),
));
$this->assertTrue($schema->tableExists('foo'));
$this->assertTrue($database->schema()->tableExists('foo'));
// Ensure that the database tasks have been run during set up. Neither MySQL
// nor SQLite make changes that are testable.
if ($database->driver() == 'pgsql') {
$this->assertEquals('on', $database->query("SHOW standard_conforming_strings")->fetchField());
$this->assertEquals('escape', $database->query("SHOW bytea_output")->fetchField());
}
}
/**
......
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