From 5f9271e67e20bcb22bf25b20f415f19d35ec487d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 5 Oct 2026 16:27:43 +0400 Subject: [PATCH 1/2] test(MySQL): cover CURRENT_TIMESTAMP defaults on columns with fractional seconds Assisted-By: Claude Opus 5.5 --- .../MySQL/Schema/DatetimeColumnTest.php | 54 +++++++++++++++++++ .../Driver/MySQL/Schema/MySQLColumnTest.php | 38 +++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/Database/Unit/Driver/MySQL/Schema/MySQLColumnTest.php diff --git a/tests/Database/Functional/Driver/MySQL/Schema/DatetimeColumnTest.php b/tests/Database/Functional/Driver/MySQL/Schema/DatetimeColumnTest.php index 4958f2e6..7fdd44f4 100644 --- a/tests/Database/Functional/Driver/MySQL/Schema/DatetimeColumnTest.php +++ b/tests/Database/Functional/Driver/MySQL/Schema/DatetimeColumnTest.php @@ -7,6 +7,8 @@ // phpcs:ignore use Cycle\Database\Driver\Handler; use Cycle\Database\Exception\HandlerException; +use Cycle\Database\Injection\Fragment; +use Cycle\Database\Schema\AbstractColumn; use Cycle\Database\Tests\Functional\Driver\Common\Schema\DatetimeColumnTest as CommonClass; /** @@ -17,6 +19,13 @@ class DatetimeColumnTest extends CommonClass { public const DRIVER = 'mysql'; + public static function fractionalCurrentTimestampProvider(): iterable + { + yield 'datetime(6)' => ['datetime', 6]; + yield 'datetime(3)' => ['datetime', 3]; + yield 'timestamp(6)' => ['timestamp', 6]; + } + public function testTimestampDatetimeZero(): void { $this->expectExceptionMessage( @@ -68,4 +77,49 @@ public function testDatetimeWithSize(): void $this->assertSame('datetime', $schema->column('datetime_data')->getInternalType()); $this->assertSame(3, $schema->column('datetime_data')->getSize()); } + + /** + * @dataProvider fractionalCurrentTimestampProvider + */ + public function testCurrentTimestampWithSize(string $type, int $size): void + { + $schema = $this->schema('table'); + $schema->primary('id'); + $schema->$type('target', size: $size)->nullable(false)->defaultValue(AbstractColumn::DATETIME_NOW); + $schema->save(Handler::DO_ALL); + + $this->assertSameAsInDB($schema); + + $saved = $this->schema('table'); + $this->assertSame($size, $saved->column('target')->getSize()); + $this->assertEquals( + new Fragment(AbstractColumn::DATETIME_NOW), + $saved->column('target')->getDefaultValue(), + ); + + $saved->$type('target', size: $size)->nullable(false)->defaultValue(AbstractColumn::DATETIME_NOW); + $this->assertFalse($saved->getComparator()->hasChanges()); + + $this->database->table('table')->insertOne(['id' => 1]); + $this->assertNotNull($this->database->table('table')->select('target')->fetchAll()[0]['target']); + } + + public function testExistingCurrentTimestampWithSizeIsReflected(): void + { + $this->database->execute( + 'CREATE TABLE `table` ( + `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY, + `target` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) + )', + ); + + $schema = $this->schema('table'); + $this->assertEquals( + new Fragment(AbstractColumn::DATETIME_NOW), + $schema->column('target')->getDefaultValue(), + ); + + $schema->datetime('target', size: 6)->nullable(false)->defaultValue(AbstractColumn::DATETIME_NOW); + $this->assertFalse($schema->getComparator()->hasChanges()); + } } diff --git a/tests/Database/Unit/Driver/MySQL/Schema/MySQLColumnTest.php b/tests/Database/Unit/Driver/MySQL/Schema/MySQLColumnTest.php new file mode 100644 index 00000000..1ef0cbed --- /dev/null +++ b/tests/Database/Unit/Driver/MySQL/Schema/MySQLColumnTest.php @@ -0,0 +1,38 @@ + ['datetime', 'CURRENT_TIMESTAMP']; + yield 'MySQL with fsp' => ['datetime(6)', 'CURRENT_TIMESTAMP(6)']; + yield 'MySQL timestamp with fsp' => ['timestamp(3)', 'CURRENT_TIMESTAMP(3)']; + yield 'MariaDB' => ['datetime', 'current_timestamp()']; + yield 'MariaDB with fsp' => ['datetime(6)', 'current_timestamp(6)']; + } + + /** + * @dataProvider currentTimestampDefaultProvider + */ + public function testReflectedCurrentTimestampDefault(string $type, string $default): void + { + $column = MySQLColumn::createInstance('table', [ + 'Field' => 'target', + 'Type' => $type, + 'Comment' => '', + 'Null' => 'NO', + 'Default' => $default, + 'Extra' => 'DEFAULT_GENERATED', + ]); + + $this->assertEquals(new Fragment(MySQLColumn::DATETIME_NOW), $column->getDefaultValue()); + } +} From ceda1f08a1f942c93b628666a77da3cad916af02 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 5 Oct 2026 16:27:44 +0400 Subject: [PATCH 2/2] fix(MySQL): render CURRENT_TIMESTAMP with the column's fractional seconds precision MySQL rejects `datetime(6) DEFAULT CURRENT_TIMESTAMP` with error 1067: the default must carry the same precision as the column. The reflected `CURRENT_TIMESTAMP(6)` (MariaDB: `current_timestamp(6)`) is normalized back to `DATETIME_NOW`, so reading such a column no longer throws and saving the same declaration again gives no diff. Fixes #277 Assisted-By: Claude Opus 5.5 --- src/Driver/MySQL/Schema/MySQLColumn.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Driver/MySQL/Schema/MySQLColumn.php b/src/Driver/MySQL/Schema/MySQLColumn.php index 04ed1bbf..675a6d73 100644 --- a/src/Driver/MySQL/Schema/MySQLColumn.php +++ b/src/Driver/MySQL/Schema/MySQLColumn.php @@ -514,13 +514,27 @@ protected function formatDatetime( string $type, string|int|\DateTimeInterface $value, ): \DateTimeInterface|FragmentInterface|string { - if ($value === 'current_timestamp()') { + // MySQL reflects the default as CURRENT_TIMESTAMP(6) on a datetime(6), MariaDB as current_timestamp(6). + if (\is_string($value) && \preg_match('/^current_timestamp(?:\(\d*\))?$/i', $value) === 1) { $value = self::DATETIME_NOW; } return parent::formatDatetime($type, $value); } + #[\Override] + protected function quoteDefault(DriverInterface $driver): string + { + $defaultValue = parent::quoteDefault($driver); + + // MySQL requires CURRENT_TIMESTAMP to carry the column's fractional seconds precision. + if ($this->size > 0 && $defaultValue === self::DATETIME_NOW && \in_array($this->type, ['datetime', 'timestamp'], true)) { + return self::DATETIME_NOW . "({$this->size})"; + } + + return $defaultValue; + } + private function sqlStatementInteger(DriverInterface $driver): string { return \sprintf(