diff --git a/UPGRADE.md b/UPGRADE.md index 53bf9420dde..8a98c6a0104 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,3 +1,17 @@ +# Upgrade to 2.10 + +## MINOR BC BREAK: escaped default values + +Default values will be automatically escaped. So default values must now be specified non-escaped. + +Before: + + $column->setDefault('Foo\\\\Bar\\\\Baz'); + +After: + + $column->setDefault('Foo\\Bar\\Baz'); + # Upgrade to 2.9 ## Deprecated `Statement::fetchColumn()` with an invalid index diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index abb7333ac1d..57664e6b8d6 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -2311,7 +2311,7 @@ public function getDefaultValueDeclarationSQL($field) return " DEFAULT '" . $this->convertBooleans($default) . "'"; } - return " DEFAULT '" . $default . "'"; + return ' DEFAULT ' . $this->quoteStringLiteral($default); } /** diff --git a/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php b/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php index d0f50562785..7734a5ce329 100644 --- a/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php +++ b/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php @@ -394,11 +394,12 @@ protected function _getPortableTableColumnDefinition($tableColumn) $length = null; break; case 'text': - $fixed = false; - break; + case '_varchar': case 'varchar': + $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']); + $fixed = false; + break; case 'interval': - case '_varchar': $fixed = false; break; case 'char': @@ -478,4 +479,16 @@ private function fixVersion94NegativeNumericDefaultValue($defaultValue) return $defaultValue; } + + /** + * Parses a default value expression as given by PostgreSQL + */ + private function parseDefaultExpression(?string $default) : ?string + { + if ($default === null) { + return $default; + } + + return str_replace("''", "'", $default); + } } diff --git a/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php b/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php index e819d963f03..94c3e936fbd 100644 --- a/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php +++ b/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php @@ -325,8 +325,9 @@ protected function _getPortableTableColumnDefinition($tableColumn) $default = null; } if ($default !== null) { - // SQLite returns strings wrapped in single quotes, so we need to strip them - $default = preg_replace("/^'(.*)'$/", '\1', $default); + // SQLite returns strings wrapped in single quotes and escaped, so we need to strip them + $default = preg_replace("/^'(.*)'$/s", '\1', $default); + $default = str_replace("''", "'", $default); } $notnull = (bool) $tableColumn['notnull']; diff --git a/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php b/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php index aeb9f4c90dd..15c99c0395a 100644 --- a/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php +++ b/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php @@ -1495,6 +1495,67 @@ public function testCreateAndListSequences() : void self::assertEquals($sequence2InitialValue, $actualSequence2->getInitialValue()); } + /** + * Returns potential escaped literals from all platforms combined. + * + * @see https://dev.mysql.com/doc/refman/5.7/en/string-literals.html + * @see http://www.sqlite.org/lang_expr.html + * @see https://www.postgresql.org/docs/9.6/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE + * + * @return mixed[][] + */ + private function getEscapedLiterals() : iterable + { + return [ + ['An ASCII NUL (X\'00\')', "foo\\0bar"], + ['Single quote, C-style', "foo\\'bar"], + ['Single quote, doubled-style', "foo''bar"], + ['Double quote, C-style', 'foo\\"bar'], + ['Double quote, double-style', 'foo""bar'], + ['Backspace', 'foo\\bbar'], + ['New-line', 'foo\\nbar'], + ['Carriage return', 'foo\\rbar'], + ['Tab', 'foo\\tbar'], + ['ASCII 26 (Control+Z)', 'foo\\Zbar'], + ['Backslash (\)', 'foo\\\\bar'], + ['Percent (%)', 'foo\\%bar'], + ['Underscore (_)', 'foo\\_bar'], + ]; + } + + private function createTableForDefaultValues() : void + { + $table = new Table('string_escaped_default_value'); + foreach ($this->getEscapedLiterals() as $i => $literal) { + $table->addColumn('field' . $i, 'string', ['default' => $literal[1]]); + } + + $table->addColumn('def_foo', 'string'); + $this->schemaManager->dropAndCreateTable($table); + } + + public function testEscapedDefaultValueCanBeIntrospected() : void + { + $this->createTableForDefaultValues(); + + $onlineTable = $this->schemaManager->listTableDetails('string_escaped_default_value'); + foreach ($this->getEscapedLiterals() as $i => $literal) { + self::assertSame($literal[1], $onlineTable->getColumn('field' . $i)->getDefault(), 'should be able introspect the value of default for: ' . $literal[0]); + } + } + + public function testEscapedDefaultValueCanBeInserted() : void + { + $this->createTableForDefaultValues(); + + $this->connection->insert('string_escaped_default_value', ['def_foo' => 'foo']); + + foreach ($this->getEscapedLiterals() as $i => $literal) { + $value = $this->connection->fetchColumn('SELECT field' . $i . ' FROM string_escaped_default_value'); + self::assertSame($literal[1], $value, 'inserted default value should be the configured default value for: ' . $literal[0]); + } + } + /** * @group #3086 */