Skip to content

Commit 2124cd7

Browse files
authored
Merge pull request #2960 from morozov/issues/2850
Handle default values as values, not SQL expressions
2 parents 9b75cd5 + 64c18da commit 2124cd7

File tree

11 files changed

+235
-65
lines changed

11 files changed

+235
-65
lines changed

UPGRADE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Upgrade to 2.10
22

3+
## MINOR BC BREAK: Default values are no longer handled as SQL expressions
4+
5+
They are converted to SQL literals (e.g. escaped). Clients must now specify default values in their initial form, not in the form of an SQL literal (e.g. escaped).
6+
7+
Before:
8+
9+
$column->setDefault('Foo\\\\Bar\\\\Baz');
10+
11+
After:
12+
13+
$column->setDefault('Foo\\Bar\\Baz');
14+
315
## Deprecated `Type::*` constants
416

517
The constants for built-in types have been moved from `Doctrine\DBAL\Types\Type` to a separate class `Doctrine\DBAL\Types\Types`.

lib/Doctrine/DBAL/Platforms/AbstractPlatform.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2311,7 +2311,7 @@ public function getDefaultValueDeclarationSQL($field)
23112311
return " DEFAULT '" . $this->convertBooleans($default) . "'";
23122312
}
23132313

2314-
return " DEFAULT '" . $default . "'";
2314+
return ' DEFAULT ' . $this->quoteStringLiteral($default);
23152315
}
23162316

23172317
/**

lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1629,7 +1629,7 @@ public function getDefaultValueDeclarationSQL($field)
16291629
return " DEFAULT '" . $this->convertBooleans($field['default']) . "'";
16301630
}
16311631

1632-
return " DEFAULT '" . $field['default'] . "'";
1632+
return ' DEFAULT ' . $this->quoteStringLiteral($field['default']);
16331633
}
16341634

16351635
/**

lib/Doctrine/DBAL/Schema/DB2SchemaManager.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
use const CASE_LOWER;
77
use function array_change_key_case;
88
use function is_resource;
9+
use function preg_match;
10+
use function str_replace;
911
use function strpos;
1012
use function strtolower;
1113
use function substr;
12-
use function trim;
1314

1415
/**
1516
* IBM Db2 Schema Manager.
@@ -47,7 +48,11 @@ protected function _getPortableTableColumnDefinition($tableColumn)
4748
$default = null;
4849

4950
if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') {
50-
$default = trim($tableColumn['default'], "'");
51+
$default = $tableColumn['default'];
52+
53+
if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
54+
$default = str_replace("''", "'", $matches[1]);
55+
}
5156
}
5257

5358
$type = $this->_platform->getDoctrineTypeMapping($tableColumn['typename']);

lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,36 @@
1313
use function explode;
1414
use function is_string;
1515
use function preg_match;
16-
use function str_replace;
17-
use function stripslashes;
1816
use function strpos;
1917
use function strtok;
2018
use function strtolower;
19+
use function strtr;
2120

2221
/**
2322
* Schema manager for the MySql RDBMS.
2423
*/
2524
class MySqlSchemaManager extends AbstractSchemaManager
2625
{
26+
/**
27+
* @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences
28+
*/
29+
private const MARIADB_ESCAPE_SEQUENCES = [
30+
'\\0' => "\0",
31+
"\\'" => "'",
32+
'\\"' => '"',
33+
'\\b' => "\b",
34+
'\\n' => "\n",
35+
'\\r' => "\r",
36+
'\\t' => "\t",
37+
'\\Z' => "\x1a",
38+
'\\\\' => '\\',
39+
'\\%' => '%',
40+
'\\_' => '_',
41+
42+
// Internally, MariaDB escapes single quotes using the standard syntax
43+
"''" => "'",
44+
];
45+
2746
/**
2847
* {@inheritdoc}
2948
*/
@@ -219,7 +238,7 @@ private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?str
219238
}
220239

221240
if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches)) {
222-
return stripslashes(str_replace("''", "'", $matches[1]));
241+
return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
223242
}
224243

225244
switch ($columnDefault) {

lib/Doctrine/DBAL/Schema/OracleSchemaManager.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use function assert;
1414
use function preg_match;
1515
use function sprintf;
16+
use function str_replace;
1617
use function strpos;
1718
use function strtolower;
1819
use function strtoupper;
@@ -144,8 +145,10 @@ protected function _getPortableTableColumnDefinition($tableColumn)
144145
}
145146

146147
if ($tableColumn['data_default'] !== null) {
147-
// Default values returned from database are enclosed in single quotes.
148-
$tableColumn['data_default'] = trim($tableColumn['data_default'], "'");
148+
// Default values returned from database are represented as literal expressions
149+
if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches)) {
150+
$tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
151+
}
149152
}
150153

151154
if ($tableColumn['data_precision'] !== null) {

lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use function preg_replace;
2222
use function sprintf;
2323
use function str_replace;
24-
use function stripos;
2524
use function strlen;
2625
use function strpos;
2726
use function strtolower;
@@ -330,11 +329,9 @@ protected function _getPortableTableColumnDefinition($tableColumn)
330329
$autoincrement = true;
331330
}
332331

333-
if (preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
332+
if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches)) {
334333
$tableColumn['default'] = $matches[1];
335-
}
336-
337-
if (stripos($tableColumn['default'], 'NULL') === 0) {
334+
} elseif (preg_match('/^NULL::/', $tableColumn['default'])) {
338335
$tableColumn['default'] = null;
339336
}
340337

@@ -395,11 +392,12 @@ protected function _getPortableTableColumnDefinition($tableColumn)
395392
$length = null;
396393
break;
397394
case 'text':
398-
$fixed = false;
399-
break;
395+
case '_varchar':
400396
case 'varchar':
397+
$tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
398+
$fixed = false;
399+
break;
401400
case 'interval':
402-
case '_varchar':
403401
$fixed = false;
404402
break;
405403
case 'char':
@@ -479,4 +477,16 @@ private function fixVersion94NegativeNumericDefaultValue($defaultValue)
479477

480478
return $defaultValue;
481479
}
480+
481+
/**
482+
* Parses a default value expression as given by PostgreSQL
483+
*/
484+
private function parseDefaultExpression(?string $default) : ?string
485+
{
486+
if ($default === null) {
487+
return $default;
488+
}
489+
490+
return str_replace("''", "'", $default);
491+
}
482492
}

lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use function str_replace;
1717
use function strpos;
1818
use function strtok;
19-
use function trim;
2019

2120
/**
2221
* SQL Server Schema Manager.
@@ -107,7 +106,7 @@ protected function _getPortableTableColumnDefinition($tableColumn)
107106
'length' => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
108107
'unsigned' => false,
109108
'fixed' => (bool) $fixed,
110-
'default' => $default !== 'NULL' ? $default : null,
109+
'default' => $default,
111110
'notnull' => (bool) $tableColumn['notnull'],
112111
'scale' => $tableColumn['scale'],
113112
'precision' => $tableColumn['precision'],
@@ -124,10 +123,18 @@ protected function _getPortableTableColumnDefinition($tableColumn)
124123
return $column;
125124
}
126125

127-
private function parseDefaultExpression(string $value) : string
126+
private function parseDefaultExpression(string $value) : ?string
128127
{
129-
while (preg_match('/^\((.*)\)$/', $value, $matches)) {
130-
$value = trim($matches[1], "'");
128+
while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
129+
$value = $matches[1];
130+
}
131+
132+
if ($value === 'NULL') {
133+
return null;
134+
}
135+
136+
if (preg_match('/^\'(.*)\'$/s', $value, $matches)) {
137+
$value = str_replace("''", "'", $matches[1]);
131138
}
132139

133140
if ($value === 'getdate()') {

lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,14 @@ protected function _getPortableTableColumnDefinition($tableColumn)
324324
if ($default === 'NULL') {
325325
$default = null;
326326
}
327+
327328
if ($default !== null) {
328-
// SQLite returns strings wrapped in single quotes, so we need to strip them
329-
$default = preg_replace("/^'(.*)'$/", '\1', $default);
329+
// SQLite returns the default value as a literal expression, so we need to parse it
330+
if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
331+
$default = str_replace("''", "'", $matches[1]);
332+
}
330333
}
334+
331335
$notnull = (bool) $tableColumn['notnull'];
332336

333337
if (! isset($tableColumn['name'])) {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\DBAL\Functional\Schema;
6+
7+
use Doctrine\DBAL\Schema\Table;
8+
use Doctrine\Tests\DbalFunctionalTestCase;
9+
use function sprintf;
10+
11+
class DefaultValueTest extends DbalFunctionalTestCase
12+
{
13+
/** @var bool */
14+
private static $initialized = false;
15+
16+
protected function setUp() : void
17+
{
18+
parent::setUp();
19+
20+
if (self::$initialized) {
21+
return;
22+
}
23+
24+
self::$initialized = true;
25+
26+
$table = new Table('default_value');
27+
$table->addColumn('id', 'integer');
28+
29+
foreach (self::columnProvider() as [$name, $default]) {
30+
$table->addColumn($name, 'string', [
31+
'default' => $default,
32+
'notnull' => false,
33+
]);
34+
}
35+
36+
$this->connection->getSchemaManager()
37+
->dropAndCreateTable($table);
38+
39+
$this->connection->insert('default_value', ['id' => 1]);
40+
}
41+
42+
/**
43+
* @dataProvider columnProvider
44+
*/
45+
public function testEscapedDefaultValueCanBeIntrospected(string $name, $expectedDefault) : void
46+
{
47+
self::assertSame(
48+
$expectedDefault,
49+
$this->connection
50+
->getSchemaManager()
51+
->listTableDetails('default_value')
52+
->getColumn($name)
53+
->getDefault()
54+
);
55+
}
56+
57+
/**
58+
* @dataProvider columnProvider
59+
*/
60+
public function testEscapedDefaultValueCanBeInserted(string $name, $expectedDefault) : void
61+
{
62+
$value = $this->connection->fetchColumn(
63+
sprintf('SELECT %s FROM default_value', $name)
64+
);
65+
66+
self::assertSame($expectedDefault, $value);
67+
}
68+
69+
/**
70+
* Returns potential escaped literals from all platforms combined.
71+
*
72+
* @see https://dev.mysql.com/doc/refman/5.7/en/string-literals.html
73+
* @see http://www.sqlite.org/lang_expr.html
74+
* @see https://www.postgresql.org/docs/9.6/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE
75+
*
76+
* @return mixed[][]
77+
*/
78+
public static function columnProvider() : iterable
79+
{
80+
return [
81+
'Single quote' => [
82+
'single_quote',
83+
"foo'bar",
84+
],
85+
'Single quote, doubled' => [
86+
'single_quote_doubled',
87+
"foo''bar",
88+
],
89+
'Double quote' => [
90+
'double_quote',
91+
'foo"bar',
92+
],
93+
'Double quote, doubled' => [
94+
'double_quote_doubled',
95+
'foo""bar',
96+
],
97+
'Backspace' => [
98+
'backspace',
99+
"foo\x08bar",
100+
],
101+
'New line' => [
102+
'new_line',
103+
"foo\nbar",
104+
],
105+
'Carriage return' => [
106+
'carriage_return',
107+
"foo\rbar",
108+
],
109+
'Tab' => [
110+
'tab',
111+
"foo\tbar",
112+
],
113+
'Substitute' => [
114+
'substitute',
115+
"foo\x1abar",
116+
],
117+
'Backslash' => [
118+
'backslash',
119+
'foo\\bar',
120+
],
121+
'Backslash, doubled' => [
122+
'backslash_doubled',
123+
'foo\\\\bar',
124+
],
125+
'Percent' => [
126+
'percent_sign',
127+
'foo%bar',
128+
],
129+
'Underscore' => [
130+
'underscore',
131+
'foo_bar',
132+
],
133+
'NULL string' => [
134+
'null_string',
135+
'NULL',
136+
],
137+
'NULL value' => [
138+
'null_value',
139+
null,
140+
],
141+
'SQL expression' => [
142+
'sql_expression',
143+
"'; DROP DATABASE doctrine --",
144+
],
145+
'No double conversion' => [
146+
'no_double_conversion',
147+
"\\'",
148+
],
149+
];
150+
}
151+
}

0 commit comments

Comments
 (0)