From 67b3fd474d43d3212334e56b3fa0373aabbc36dd Mon Sep 17 00:00:00 2001 From: HugoFara Date: Tue, 30 Jun 2026 22:17:02 +0200 Subject: [PATCH] fix(db): parse SQL schema files regardless of line endings (#241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SqlFileParser::parseFile split statements on `';' . PHP_EOL`, which is `";\n"` on a Linux host. A schema file with CRLF (or CR) line endings — e.g. committed from Windows or baked into an image — never matches that boundary, so the entire baseline.sql collapses into one multi-statement string. mysqli_query() runs only the first statement and rejects the rest with error 1064, surfacing as "Internal Server Error - A database error occurred." on first boot (issue #241). Read the file whole, normalize CRLF/CR to LF up front, then split. This makes parsing independent of the file's origin OS and also fixes CR-only files, which fgets() could not line-split at all. Add a data-provider regression test covering LF, CRLF, and CR. --- .../Infrastructure/Database/SqlFileParser.php | 29 +++++++------- .../backend/Core/Utils/SqlFileParserTest.php | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/Shared/Infrastructure/Database/SqlFileParser.php b/src/Shared/Infrastructure/Database/SqlFileParser.php index 34f44cae9..cc70997c3 100644 --- a/src/Shared/Infrastructure/Database/SqlFileParser.php +++ b/src/Shared/Infrastructure/Database/SqlFileParser.php @@ -36,35 +36,36 @@ class SqlFileParser */ public static function parseFile(string $filename): array { - $handle = @fopen($filename, 'r'); - if ($handle === false) { + $content = @file_get_contents($filename); + if ($content === false) { return array(); } + // Normalize line endings up front so statement splitting does not depend + // on the file's origin OS. A CRLF/CR file (e.g. committed from Windows or + // baked into an image) must parse the same as an LF file on a Linux host, + // where PHP_EOL is "\n" and would never match a ";\r\n" boundary. + $content = str_replace(["\r\n", "\r"], "\n", $content); + $queries_list = array(); $curr_content = ''; - while ($stream = fgets($handle)) { + foreach (explode("\n", $content) as $line) { // Skip comments - if (str_starts_with($stream, '-- ')) { + if (str_starts_with($line, '-- ')) { continue; } - // Add stream to accumulator - $curr_content .= $stream; - // Get queries - $queries = explode(';' . PHP_EOL, $curr_content); - // Replace line by remainders of the last element (incomplete line) + // Add line (with its newline restored) to the accumulator + $curr_content .= $line . "\n"; + // Pull out every complete statement, keep the trailing remainder + $queries = explode(";\n", $curr_content); $curr_content = array_pop($queries); foreach ($queries as $query) { $queries_list[] = trim($query); } } // Add final query if there's any remaining content - if (!empty(trim($curr_content))) { + if (trim($curr_content) !== '') { $queries_list[] = trim($curr_content); } - if (!feof($handle)) { - // Throw error - } - fclose($handle); return $queries_list; } } diff --git a/tests/backend/Core/Utils/SqlFileParserTest.php b/tests/backend/Core/Utils/SqlFileParserTest.php index 3d985902a..58a31cb77 100644 --- a/tests/backend/Core/Utils/SqlFileParserTest.php +++ b/tests/backend/Core/Utils/SqlFileParserTest.php @@ -5,6 +5,7 @@ namespace Lwt\Tests\Core\Utils; use Lwt\Shared\Infrastructure\Database\SqlFileParser; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; /** @@ -52,4 +53,43 @@ public function testParseFileNonexistent(): void $this->assertIsArray($result); $this->assertEmpty($result); } + + /** + * Statements must be split regardless of the file's line endings. + * + * Regression test for a CRLF schema file producing a single un-split blob + * that mysqli rejects with error 1064 on a Linux host (issue #241). + */ + #[DataProvider('lineEndingProvider')] + public function testParseFileSplitsStatementsForAnyLineEnding(string $eol): void + { + $sqlContent = "-- Test SQL file" . $eol . + "CREATE TABLE a (id INT);" . $eol . + "CREATE TABLE b (id INT);" . $eol . + "INSERT INTO a VALUES (1);"; + + $tempFile = sys_get_temp_dir() . '/test_sql_' . uniqid() . '.sql'; + file_put_contents($tempFile, $sqlContent); + + $queries = SqlFileParser::parseFile($tempFile); + unlink($tempFile); + + // Each statement must come back individually, not concatenated. + $this->assertSame( + ['CREATE TABLE a (id INT)', 'CREATE TABLE b (id INT)', 'INSERT INTO a VALUES (1)'], + $queries + ); + } + + /** + * @return array + */ + public static function lineEndingProvider(): array + { + return [ + 'LF (Unix)' => ["\n"], + 'CRLF (Windows)' => ["\r\n"], + 'CR (classic Mac)' => ["\r"], + ]; + } }