diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index b0f6fc1c..c1339bdb 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -1040,10 +1040,22 @@ public function castToBool($value): mixed */ protected function getDefaultValueDefinition(mixed $default, ?string $columnType = null): string { + $datetimeTypes = [ + static::PHINX_TYPE_DATETIME, + static::PHINX_TYPE_TIMESTAMP, + static::PHINX_TYPE_TIME, + static::PHINX_TYPE_DATE, + ]; + if ($default instanceof Literal) { $default = (string)$default; - } elseif (is_string($default) && stripos($default, 'CURRENT_TIMESTAMP') !== 0) { - // Ensure a defaults of CURRENT_TIMESTAMP(3) is not quoted. + } elseif (is_string($default) && stripos($default, 'CURRENT_TIMESTAMP') === 0) { + // Only skip quoting CURRENT_TIMESTAMP for datetime-related column types. + // For other types (like string), it should be quoted as a literal string value. + if (!in_array($columnType, $datetimeTypes, true)) { + $default = $this->quoteString($default); + } + } elseif (is_string($default)) { $default = $this->quoteString($default); } elseif (is_bool($default)) { $default = $this->castToBool($default); diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index f42812b5..76b71f4d 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -624,7 +624,8 @@ static function ($value) { $extra = ' ' . implode(' ', $extras); if (($row['Default'] !== null)) { - $extra .= $this->getDefaultValueDefinition($row['Default']); + $phinxTypeInfo = $this->getPhinxType($row['Type']); + $extra .= $this->getDefaultValueDefinition($row['Default'], $phinxTypeInfo['name']); } $definition = $row['Type'] . ' ' . $null . $extra . $comment; diff --git a/tests/TestCase/Db/Adapter/AbstractAdapterTest.php b/tests/TestCase/Db/Adapter/AbstractAdapterTest.php index abb83da1..cfe6b300 100644 --- a/tests/TestCase/Db/Adapter/AbstractAdapterTest.php +++ b/tests/TestCase/Db/Adapter/AbstractAdapterTest.php @@ -4,6 +4,7 @@ namespace Migrations\Test\Db\Adapter; use Migrations\Db\Adapter\AbstractAdapter; +use Migrations\Db\Literal; use Migrations\Test\TestCase\Db\Adapter\DefaultAdapterTrait; use PDOException; use Phinx\Config\Config; @@ -144,4 +145,132 @@ public function fetchAll(string $sql): array $this->assertEquals([], $adapter->getVersionLog()); } + + /** + * Test CURRENT_TIMESTAMP default value quoting behavior (bug #1891). + * + * This test verifies that CURRENT_TIMESTAMP is only treated as a SQL function + * for datetime-related column types. For other column types like string, + * 'CURRENT_TIMESTAMP' should be quoted as a literal string value. + * + * @see https://github.com/cakephp/phinx/issues/1891 + */ + #[DataProvider('currentTimestampDefaultValueProvider')] + public function testCurrentTimestampDefaultValueQuoting($default, $columnType, $expected) + { + $adapter = new class (['version_order' => Config::VERSION_ORDER_CREATION_TIME]) extends AbstractAdapter { + use DefaultAdapterTrait; + + public function quoteString(string $value): string + { + return "'" . $value . "'"; + } + + // Expose the protected method for testing + public function testGetDefaultValueDefinition(mixed $default, ?string $columnType = null): string + { + return $this->getDefaultValueDefinition($default, $columnType); + } + }; + + $actual = $adapter->testGetDefaultValueDefinition($default, $columnType); + $this->assertEquals($expected, $actual); + } + + /** + * Data provider for CURRENT_TIMESTAMP quoting test. + * + * @return array + */ + public static function currentTimestampDefaultValueProvider(): array + { + return [ + // CURRENT_TIMESTAMP on datetime types should NOT be quoted + 'CURRENT_TIMESTAMP on datetime' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_DATETIME, + ' DEFAULT CURRENT_TIMESTAMP', + ], + 'CURRENT_TIMESTAMP on timestamp' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_TIMESTAMP, + ' DEFAULT CURRENT_TIMESTAMP', + ], + 'CURRENT_TIMESTAMP on time' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_TIME, + ' DEFAULT CURRENT_TIMESTAMP', + ], + 'CURRENT_TIMESTAMP on date' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_DATE, + ' DEFAULT CURRENT_TIMESTAMP', + ], + 'CURRENT_TIMESTAMP(3) on datetime' => [ + 'CURRENT_TIMESTAMP(3)', + AbstractAdapter::PHINX_TYPE_DATETIME, + ' DEFAULT CURRENT_TIMESTAMP(3)', + ], + + // CURRENT_TIMESTAMP on non-datetime types SHOULD be quoted (bug #1891) + 'CURRENT_TIMESTAMP on string should be quoted' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_STRING, + " DEFAULT 'CURRENT_TIMESTAMP'", + ], + 'CURRENT_TIMESTAMP on text should be quoted' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_TEXT, + " DEFAULT 'CURRENT_TIMESTAMP'", + ], + 'CURRENT_TIMESTAMP on char should be quoted' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_CHAR, + " DEFAULT 'CURRENT_TIMESTAMP'", + ], + 'CURRENT_TIMESTAMP on integer should be quoted' => [ + 'CURRENT_TIMESTAMP', + AbstractAdapter::PHINX_TYPE_INTEGER, + " DEFAULT 'CURRENT_TIMESTAMP'", + ], + + // Regular string defaults should always be quoted + 'Regular string default' => [ + 'default_value', + AbstractAdapter::PHINX_TYPE_STRING, + " DEFAULT 'default_value'", + ], + 'Regular string on datetime' => [ + 'some_string', + AbstractAdapter::PHINX_TYPE_DATETIME, + " DEFAULT 'some_string'", + ], + + // Literal values should not be quoted + 'Literal value' => [ + Literal::from('NOW()'), + AbstractAdapter::PHINX_TYPE_DATETIME, + ' DEFAULT NOW()', + ], + + // Boolean defaults + 'Boolean true' => [ + true, + AbstractAdapter::PHINX_TYPE_BOOLEAN, + ' DEFAULT 1', + ], + 'Boolean false' => [ + false, + AbstractAdapter::PHINX_TYPE_BOOLEAN, + ' DEFAULT 0', + ], + + // Null columnType (should quote CURRENT_TIMESTAMP when type is unknown) + 'CURRENT_TIMESTAMP with null column type' => [ + 'CURRENT_TIMESTAMP', + null, + " DEFAULT 'CURRENT_TIMESTAMP'", + ], + ]; + } }