diff --git a/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php b/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php index c46b391a..6d36af0b 100644 --- a/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php +++ b/modules/EcommerceFinance/Services/BusinessMirrorSynchronizer.php @@ -106,7 +106,6 @@ class BusinessMirrorSynchronizer ?Carbon $windowEnd, int $limit, ): array { - $names = array_map(fn (array $column): string => (string) $column['column_name'], $columns); $order = $this->orderColumns($dataset, $mode); $where = []; $bindings = []; @@ -129,7 +128,7 @@ class BusinessMirrorSynchronizer } $sql = 'SELECT TOP (' . $limit . ') ' - . implode(', ', array_map(fn (string $name): string => $this->quote($name), $names)) + . implode(', ', array_map(fn (array $column): string => $this->selectExpression($column), $columns)) . ' FROM ' . $this->quote($dataset->sourceSchema()) . '.' . $this->quote($dataset->sourceObject()) . ($where === [] ? '' : ' WHERE (' . implode(') AND (', $where) . ')') . ' ORDER BY ' . implode(', ', array_map(fn (string $name): string => $this->quote($name), $order)); @@ -137,6 +136,22 @@ class BusinessMirrorSynchronizer return $this->queries->forSource($dataset->sourceKey())->select($sql, $bindings); } + /** @param array $column */ + private function selectExpression(array $column): string + { + $name = (string) $column['column_name']; + $quoted = $this->quote($name); + + // FreeTDS may return SQL Server date values using the connection locale + // (for example "Jan 1 2026"). Convert temporal columns at the source so + // every mirror receives deterministic ISO values that MariaDB accepts. + return match (strtolower((string) ($column['data_type'] ?? ''))) { + 'date' => 'CONVERT(varchar(10), ' . $quoted . ', 23) AS ' . $quoted, + 'datetime', 'datetime2', 'smalldatetime' => 'CONVERT(varchar(27), ' . $quoted . ', 126) AS ' . $quoted, + default => $quoted, + }; + } + private function sqlServerDateTime(?Carbon $value): ?string { // ISO 8601 with millisecond precision is accepted consistently by SQL Server diff --git a/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php b/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php index 4a206e8c..93c65ee1 100644 --- a/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php @@ -69,6 +69,7 @@ class BusinessMirrorSynchronizerTest extends TestCase $this->column('Id', 1, 'int', null), $this->column('updateTime', 2, 'datetime2', null), $this->column('displayName', 3, 'nvarchar', 120), + $this->column('businessDate', 4, 'date', null), ]); $this->query = new FakeBusinessMirrorQuery(); @@ -88,8 +89,8 @@ class BusinessMirrorSynchronizerTest extends TestCase public function test_full_sync_materializes_complete_table_and_marks_state_ready(): void { $this->query->batches = [[ - ['Id' => 1, 'updateTime' => '2026-08-14 10:00:00.1234567', 'displayName' => '一号'], - ['Id' => 2, 'updateTime' => '2026-08-14 11:00:00.0000000', 'displayName' => '二号'], + ['Id' => 1, 'updateTime' => '2026-08-14 10:00:00.1234567', 'displayName' => '一号', 'businessDate' => '2026-08-14'], + ['Id' => 2, 'updateTime' => '2026-08-14 11:00:00.0000000', 'displayName' => '二号', 'businessDate' => '2026-08-15'], ]]; $result = $this->synchronizer->sync('test.dbo.records', true, 10, 1); @@ -105,6 +106,8 @@ class BusinessMirrorSynchronizerTest extends TestCase $this->assertNotNull($state->initial_completed_at); $this->assertSame(2, (int) $state->mirror_row_count); $this->assertStringContainsString('SELECT TOP (10)', $this->query->calls[0]['sql']); + $this->assertStringContainsString('CONVERT(varchar(27), [updateTime], 126) AS [updateTime]', $this->query->calls[0]['sql']); + $this->assertStringContainsString('CONVERT(varchar(10), [businessDate], 23) AS [businessDate]', $this->query->calls[0]['sql']); $this->assertStringContainsString('ORDER BY [Id]', $this->query->calls[0]['sql']); }