From f15e66ad8fbd4e3119fe1838db78e28bf7345226 Mon Sep 17 00:00:00 2001 From: blaipr Date: Thu, 24 Sep 2026 19:22:46 +0200 Subject: [PATCH] fix: a backup restores text that looks like a number as text --- .../Export/Services/BackupFile.php | 13 ++++- .../Export/Services/FileBackupServiceTest.php | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/Application/Export/Services/BackupFile.php b/src/Application/Export/Services/BackupFile.php index df726be8a..c138a35a3 100644 --- a/src/Application/Export/Services/BackupFile.php +++ b/src/Application/Export/Services/BackupFile.php @@ -210,8 +210,17 @@ private function backupTables(string $dbName, BackupHandlers $handlers): void function (mixed $value) { if ($value === null) { return 'NULL'; - } elseif (is_numeric($value)) { - return $value; + } + + // Decided by what the column is, not by what the value looks like. PDO + // hands integer columns back as ints and every text column as a string, so + // only a real number is written bare. This used to ask is_numeric(), which + // is true of text that merely looks numeric — a login of `0123`, an account + // named `0800`, a note reading `1e5` — and a bare literal is a number to + // MySQL, so restoring the backup stored `123`, `800` and `100000` in their + // place. A quoted number is still read correctly into a numeric column. + if (is_int($value) || is_float($value)) { + return (string)$value; } return $this->databaseUtil->escape((string)$value); diff --git a/tests/Unit/Application/Export/Services/FileBackupServiceTest.php b/tests/Unit/Application/Export/Services/FileBackupServiceTest.php index e81d275a9..a796b4460 100644 --- a/tests/Unit/Application/Export/Services/FileBackupServiceTest.php +++ b/tests/Unit/Application/Export/Services/FileBackupServiceTest.php @@ -144,6 +144,59 @@ public function testDoBackup(): void $this->assertSame(TMP_PATH, $this->builtPath); } + /** + * Text that looks like a number is dumped as text. + * + * The dump used to write any value is_numeric() accepted as a bare literal, and a bare literal + * is a number to MySQL — so restoring a login of `0123`, an account named `0800` or a note of + * `1e5` stored `123`, `800` and `100000` in their place, and ` 7` lost its space. Only what PDO + * hands back as a number — an integer or float column — is written bare now. + * + * @throws ServiceException + * @throws Exception + */ + public function testTextThatLooksLikeANumberIsDumpedAsText(): void + { + $this->config->getConfigData()->setDbName('a_db'); + + $this->database->method('runQuery')->willReturnCallback( + fn() => $this->buildCreateResult('table') + ); + $this->database->method('doFetchWithOptions')->willReturnCallback(static function () { + yield ['0123', 5, '1e5', ' 7', 2.5, null]; + }); + + $databaseUtil = $this->createStub(DatabaseUtilService::class); + $databaseUtil->method('escape')->willReturnCallback(static fn(string $value) => "'" . $value . "'"); + + $written = []; + + $this->dbFileHandler->method('write')->willReturnCallback( + function (string $data) use (&$written) { + $written[] = $data; + + return $this->dbFileHandler; + } + ); + + $backup = new BackupFile( + $this->application, + $this->database, + $databaseUtil, + new \SP\Infrastructure\Database\QueryDataFactory(), + $this->backupHandlersFactory + ); + + $backup->doBackup(TMP_PATH, APP_PATH); + + $table = DatabaseUtilService::TABLES[0]; + + self::assertContains( + sprintf("INSERT INTO `%s` VALUES('0123',5,'1e5',' 7',2.5,NULL);", $table) . PHP_EOL, + $written + ); + } + private function buildCreateResult(string $type): QueryResult { $data = new stdClass();