Skip to content

Commit 149a2e7

Browse files
authored
Merge pull request #118 from perplorm/bugfix-text-defaults-handling
Allow default values in MySQL/MariaDB text columns
2 parents 6047085 + a29919c commit 149a2e7

File tree

4 files changed

+178
-3
lines changed

4 files changed

+178
-3
lines changed

src/Propel/Generator/Platform/MysqlPlatform.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,9 @@ public function getColumnDDL(Column $col): string
503503
if ($def && $def->isExpression()) {
504504
throw new EngineException('DATE columns cannot have default *expressions* in MySQL.');
505505
}
506-
} elseif ($sqlType === 'TEXT' || $sqlType === 'BLOB') {
506+
} elseif ($sqlType === 'BLOB') {
507507
if ($domain->getDefaultValue()) {
508-
throw new EngineException('BLOB and TEXT columns cannot have DEFAULT values. in MySQL.');
508+
throw new EngineException('BLOB columns cannot have DEFAULT values in MySQL.');
509509
}
510510
}
511511

src/Propel/Generator/Reverse/MysqlSchemaParser.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Propel\Generator\Reverse;
66

77
use PDO;
8+
use Propel\Generator\Exception\EngineException;
89
use Propel\Generator\Model\Column;
910
use Propel\Generator\Model\ColumnDefaultValue;
1011
use Propel\Generator\Model\Database;
@@ -18,6 +19,7 @@
1819
use Propel\Runtime\Connection\ConnectionInterface;
1920
use RuntimeException;
2021
use function array_keys;
22+
use function assert;
2123
use function count;
2224
use function explode;
2325
use function implode;
@@ -26,7 +28,10 @@
2628
use function preg_match;
2729
use function preg_match_all;
2830
use function sprintf;
31+
use function str_ends_with;
2932
use function str_replace;
33+
use function str_starts_with;
34+
use function stripslashes;
3035
use function strpos;
3136
use function strtolower;
3237
use function strtoupper;
@@ -362,13 +367,17 @@ protected function parseType(string $typeDeclaration): array
362367
protected function extractDefaultValue(?string $parsedValue, string $propelType, string $nativeType, string $extra): ?ColumnDefaultValue
363368
{
364369
// BLOBs can't have any default values in MySQL
365-
$isBlob = preg_match('~blob|text~', $nativeType);
370+
$isBlob = preg_match('/blob/', $nativeType);
366371

367372
if ($parsedValue === null || $isBlob) {
368373
return null;
369374
}
370375
$default = $parsedValue;
371376

377+
if ($parsedValue !== '' && preg_match('/text/', $nativeType)) {
378+
$default = $this->unwrapDefaultValueString($parsedValue);
379+
}
380+
372381
if ($propelType == PropelTypes::BOOLEAN) {
373382
if ($parsedValue == '1') {
374383
$default = 'true';
@@ -391,6 +400,33 @@ protected function extractDefaultValue(?string $parsedValue, string $propelType,
391400
return new ColumnDefaultValue($default, $type);
392401
}
393402

403+
/**
404+
* Cleanup of default value string expressions as returned from DBMS.
405+
*
406+
* MariaDB returns a quoted string: "'Saying \'Foo\' means nothing'"
407+
* MySQL 8.0+ escapes again and adds a charset prefix: "_utf8mb3\'Saying \\\'Foo\\\' means nothing\'"
408+
*
409+
* @param string $value
410+
*
411+
* @throws \Propel\Generator\Exception\EngineException
412+
*
413+
* @return string
414+
*/
415+
protected function unwrapDefaultValueString(string $value): string
416+
{
417+
if (str_starts_with($value, '_')) {
418+
$value = stripslashes($value); // unescape outer quotes
419+
$firstQuotePos = strpos($value, "'");
420+
assert($firstQuotePos !== false);
421+
$value = substr($value, $firstQuotePos); // remove charset prefix
422+
}
423+
if ($value !== '' && !(str_starts_with($value, "'") && str_ends_with($value, "'"))) {
424+
throw new EngineException("Expected default value to be quoted, but got: `$value`");
425+
}
426+
427+
return stripslashes(substr($value, 1, -1)); // unquote string
428+
}
429+
394430
/**
395431
* Load and set table description.
396432
*

tests/Propel/Tests/Generator/Platform/MysqlPlatformTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
use Propel\Generator\Builder\Util\SchemaReader;
1212
use Propel\Generator\Config\GeneratorConfig;
13+
use Propel\Generator\Exception\EngineException;
1314
use Propel\Generator\Model\Column;
15+
use Propel\Generator\Model\ColumnDefaultValue;
1416
use Propel\Generator\Model\IdMethod;
1517
use Propel\Generator\Model\IdMethodParameter;
1618
use Propel\Generator\Model\Index;
@@ -522,6 +524,32 @@ public function testGetColumnDDLIgnoresSizeOnInteger(string $integerType, bool $
522524
}
523525

524526

527+
/**
528+
* @return void
529+
*/
530+
public function testGetColumnDDLTextDefaultValue(): void
531+
{
532+
$column = new Column('foo');
533+
$column->getDomain()->copy($this->getPlatform()->getDomainForType(PropelTypes::LONGVARCHAR));
534+
$column->getDomain()->setDefaultValue(new ColumnDefaultValue('hello', ColumnDefaultValue::TYPE_VALUE));
535+
$expected = '`foo` TEXT DEFAULT \'hello\'';
536+
$this->assertEquals($expected, $this->getPlatform()->getColumnDDL($column));
537+
}
538+
539+
/**
540+
* @return void
541+
*/
542+
public function testGetColumnDDLBlobDefaultValueThrowsException(): void
543+
{
544+
$this->expectException(EngineException::class);
545+
$this->expectExceptionMessage('BLOB columns cannot have DEFAULT values in MySQL.');
546+
547+
$column = new Column('bar');
548+
$column->getDomain()->copy($this->getPlatform()->getDomainForType(PropelTypes::BLOB));
549+
$column->getDomain()->setDefaultValue(new ColumnDefaultValue('data', ColumnDefaultValue::TYPE_VALUE));
550+
$this->getPlatform()->getColumnDDL($column);
551+
}
552+
525553
/**
526554
* @return void
527555
*/

tests/Propel/Tests/Generator/Reverse/MysqlSchemaParserTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
namespace Propel\Tests\Generator\Reverse;
1010

1111
use PDO;
12+
use Propel\Generator\Config\QuickGeneratorConfig;
1213
use Propel\Generator\Model\Column;
1314
use Propel\Generator\Model\Table;
1415
use Propel\Generator\Model\ColumnDefaultValue;
16+
use Propel\Generator\Model\Database;
17+
use Propel\Generator\Platform\DefaultPlatform;
1518
use Propel\Generator\Reverse\MysqlSchemaParser;
1619
use Propel\Tests\Bookstore\Map\BookTableMap;
1720

@@ -150,4 +153,112 @@ public function testOnUpdateIsImported(): void
150153
$this->assertEquals(ColumnDefaultValue::TYPE_EXPR, $updatedAtColumn->getDefaultValue()->getType());
151154
$this->assertEquals('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', $updatedAtColumn->getDefaultValue()->getValue());
152155
}
156+
157+
/**
158+
* @return string[][]
159+
*/
160+
public function TextColumnDefaultValueDataProvider(): array
161+
{
162+
return [
163+
["_latin1\'Foo\'", 'Foo'],
164+
["_utf8mb3\'Foo\'", 'Foo'],
165+
["_utf8mb3\'Saying \\\'Foo\\\' means nothing\'", "Saying 'Foo' means nothing"],
166+
["_utf8mb3\'Saying \"Foo\" means nothing\'", 'Saying "Foo" means nothing'],
167+
["_utf8mb4\'\'", ''],
168+
["'Saying \'Foo\' means nothing'", "Saying 'Foo' means nothing"],
169+
["'Foo'", 'Foo'],
170+
["''", ''],
171+
['', ''],
172+
];
173+
}
174+
175+
/**
176+
* @dataProvider TextColumnDefaultValueDataProvider
177+
*
178+
* @param string $defaultValue
179+
* @param string $expected
180+
*
181+
* @return void
182+
*/
183+
public function testTextColumnDefaultValue(string $defaultValue, string $expected): void
184+
{
185+
$parser = new MysqlSchemaParser();
186+
$actual = $this->callMethod($parser, 'unwrapDefaultValueString', [$defaultValue]);
187+
188+
$this->assertSame($expected, $actual);
189+
}
190+
191+
/**
192+
* @return void
193+
*/
194+
public function testTextDefaultValues(): void
195+
{
196+
$serverVersion = $this->con->getAttribute(PDO::ATTR_SERVER_VERSION);
197+
$isMariaDb = stripos($serverVersion, 'mariadb') !== false;
198+
199+
if ($isMariaDb) {
200+
if (version_compare(preg_replace('/^.*?(\d+\.\d+\.\d+).*$/', '$1', $serverVersion), '10.2.1', '<')) {
201+
$this->markTestSkipped('TEXT columns with DEFAULT values require MariaDB 10.2.1+');
202+
}
203+
} else {
204+
if (version_compare($serverVersion, '8.0.13', '<')) {
205+
$this->markTestSkipped('TEXT columns with DEFAULT values require MySQL 8.0.13+');
206+
}
207+
}
208+
209+
$this->con->exec('DROP TABLE IF EXISTS test_text_defaults');
210+
$this->con->exec("CREATE TABLE test_text_defaults (
211+
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
212+
content_text TEXT DEFAULT ('hello text'),
213+
content_text_escaped TEXT DEFAULT ('foo says \'bar\''),
214+
content_text_empty TEXT DEFAULT (''),
215+
content_text_not_null TEXT NOT NULL,
216+
content_text_none TEXT
217+
)");
218+
219+
try {
220+
$parser = new MysqlSchemaParser($this->con);
221+
$parser->setGeneratorConfig(new QuickGeneratorConfig());
222+
223+
$database = new Database();
224+
$database->setPlatform(new DefaultPlatform());
225+
$parser->parse($database);
226+
227+
$table = $database->getTable('test_text_defaults');
228+
$this->assertNotNull($table, 'Table test_text_defaults should be parsed');
229+
230+
// TEXT with default value
231+
$contentText = $table->getColumn('content_text');
232+
$this->assertNotNull($contentText, 'Column content_text should exist');
233+
$this->assertNotNull($contentText->getDefaultValue(), 'TEXT column default value should be preserved');
234+
$this->assertEquals(ColumnDefaultValue::TYPE_VALUE, $contentText->getDefaultValue()->getType());
235+
$this->assertEquals('hello text', $contentText->getDefaultValue()->getValue());
236+
237+
// TEXT with default value
238+
$contentText = $table->getColumn('content_text_escaped');
239+
$this->assertNotNull($contentText, 'Column content_text should exist');
240+
$this->assertNotNull($contentText->getDefaultValue(), 'TEXT column default value should be preserved');
241+
$this->assertEquals(ColumnDefaultValue::TYPE_VALUE, $contentText->getDefaultValue()->getType());
242+
$this->assertEquals("foo says 'bar'", $contentText->getDefaultValue()->getValue());
243+
244+
// TEXT with empty string default
245+
$contentTextEmpty = $table->getColumn('content_text_empty');
246+
$this->assertNotNull($contentTextEmpty, 'Column content_text_empty should exist');
247+
$this->assertNotNull($contentTextEmpty->getDefaultValue(), 'TEXT column with empty default should be preserved');
248+
$this->assertEquals(ColumnDefaultValue::TYPE_VALUE, $contentTextEmpty->getDefaultValue()->getType());
249+
$this->assertEquals('', $contentTextEmpty->getDefaultValue()->getValue());
250+
251+
// TEXT NOT NULL without default
252+
$contentTextNotNull = $table->getColumn('content_text_not_null');
253+
$this->assertNotNull($contentTextNotNull, 'Column content_text_not_null should exist');
254+
$this->assertNull($contentTextNotNull->getDefaultValue(), 'TEXT NOT NULL column without default should have no default');
255+
256+
// TEXT without default (nullable, implicit DEFAULT NULL)
257+
$contentTextNone = $table->getColumn('content_text_none');
258+
$this->assertNotNull($contentTextNone, 'Column content_text_none should exist');
259+
$this->assertNull($contentTextNone->getDefaultValue(), 'TEXT column without explicit default should have no default');
260+
} finally {
261+
$this->con->exec('DROP TABLE IF EXISTS test_text_defaults');
262+
}
263+
}
153264
}

0 commit comments

Comments
 (0)