From de9cd100e1febdbc35596e5df38b2dfbdf094353 Mon Sep 17 00:00:00 2001 From: wakqasahmed Date: Mon, 14 Sep 2026 21:09:24 +0200 Subject: [PATCH] fix: preserve sub-second precision in BaseModel datetime timestamps When a model's $dateFormat is 'datetime' and the connection's dateFormat['datetime'] mask includes .v/.u (millisecond/microsecond precision), auto-generated created_at/updated_at timestamps always rendered .000000 instead of the real sub-second value. setDate() converted the current Time instance to a Unix timestamp integer before formatting it, which discards anything below whole-second precision. timeToDate() already formats a Time object directly and retains sub-second precision, but nothing routed the no-explicit-date case through it. Fixes #10540 --- system/BaseModel.php | 6 +++-- tests/system/Models/GeneralModelTest.php | 26 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 64d083fe4d2b..ad79f6f42ec6 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1425,9 +1425,11 @@ protected function doProtectFieldsForInsert(array $row): array */ protected function setDate(?int $userDate = null) { - $currentDate = $userDate ?? Time::now()->getTimestamp(); + if ($userDate === null && $this->dateFormat === 'datetime') { + return $this->timeToDate(Time::now()); + } - return $this->intToDate($currentDate); + return $this->intToDate($userDate ?? Time::now()->getTimestamp()); } /** diff --git a/tests/system/Models/GeneralModelTest.php b/tests/system/Models/GeneralModelTest.php index bd9f2d14109e..eb72f6aec5eb 100644 --- a/tests/system/Models/GeneralModelTest.php +++ b/tests/system/Models/GeneralModelTest.php @@ -15,8 +15,11 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Exceptions\BadMethodCallException; +use CodeIgniter\I18n\Time; use CodeIgniter\Model; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Models\JobModel; use Tests\Support\Models\UserModel; @@ -39,6 +42,7 @@ protected function tearDown(): void { parent::tearDown(); $this->resetServices(); + Time::setTestNow(); } /** @@ -111,6 +115,28 @@ public function testSetAllowedFields(): void $this->assertSame($allowed2, $this->getPrivateProperty($model, 'allowedFields')); } + #[DataProvider('provideCurrentTimestampPreservesSubseconds')] + public function testCurrentTimestampPreservesSubseconds(string $format, string $expected): void + { + Time::setTestNow('2024-07-09 09:13:34.654321'); + + $db = new MockConnection(['dateFormat' => ['datetime' => $format]]); + $model = $this->createModel(UserModel::class, $db); + $date = self::getPrivateMethodInvoker($model, 'setDate'); + + $this->assertSame($expected, $date()); + } + + /** + * @return iterable + */ + public static function provideCurrentTimestampPreservesSubseconds(): iterable + { + yield 'milliseconds' => ['Y-m-d H:i:s.v', '2024-07-09 09:13:34.654']; + + yield 'microseconds' => ['Y-m-d H:i:s.u', '2024-07-09 09:13:34.654321']; + } + public function testBuilderUsesModelTable(): void { $builder = $this->createModel(UserModel::class)->builder(); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 6dee8ee9aa6d..6efbe6951cc0 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -36,6 +36,7 @@ Deprecations Bugs Fixed ********** +- **BaseModel:** Fixed a bug where auto-generated ``created_at``/``updated_at`` timestamps always rendered ``.000000`` for a ``'datetime'`` ``$dateFormat`` whose connection ``dateFormat`` mask includes ``.v``/``.u``, instead of the real sub-second value. - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally.