From cc1eb10366c9ebdb3a49d159d558bde0e3c66ceb Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Mon, 11 May 2026 10:12:08 +0200 Subject: [PATCH 1/6] Add MariaDB CI job and integration test for native UUID column The encoding fix from #142 made prepared-statement string parameters round-trip correctly against MariaDB's native UUID column type, but that integration path was never exercised in CI. This adds: - A tests-mariadb job (PHP 8.4 + 8.5) that installs MariaDB via apt and runs the new test against a live server - An integration test that creates a UUID column, inserts via prepared statement, reads back, and asserts the value survives the binary protocol path The test skips on MySQL (no native UUID type) and on MariaDB < 10.7, so it's a no-op on the existing 5 MySQL jobs and only runs on the new MariaDB jobs. Verified locally against MariaDB 12.2.2 (passes). A controlled revert of MysqlEncodedValue::fromValue() to LongBlob encoding reproduces the original "Incorrect uuid value (1292/22007)" failure, confirming the test catches the regression it's meant to guard. --- .github/workflows/ci.yml | 69 ++++++++++++++++++++++++++++++++++++++ test/MariaDbUuidTest.php | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 test/MariaDbUuidTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49b2333..0315eae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,3 +93,72 @@ jobs: PHP_CS_FIXER_IGNORE_ENV: 1 run: vendor/bin/php-cs-fixer --diff --dry-run -v fix if: runner.os != 'Windows' && matrix.style-fix != 'none' + + tests-mariadb: + strategy: + matrix: + include: + - operating-system: 'ubuntu-latest' + php-version: '8.4' + + - operating-system: 'ubuntu-latest' + php-version: '8.5' + + name: PHP ${{ matrix.php-version }} (MariaDB) + + runs-on: ${{ matrix.operating-system }} + + env: + MYSQL_DATABASE: test + MYSQL_USER: root + MYSQL_PASSWORD: root + + steps: + - name: Setup MariaDB + run: | + sudo systemctl stop mysql + sudo apt-get update + sudo apt-get install -y mariadb-server + sudo systemctl start mariadb + sudo mariadb -e "ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('root'); FLUSH PRIVILEGES;" + mariadb --version + + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v5 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-mariadb-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }} + restore-keys: | + composer-mariadb-${{ runner.os }}-${{ matrix.php-version }}- + composer-${{ runner.os }}-${{ matrix.php-version }}- + composer-${{ runner.os }}- + + - name: Install dependencies + uses: nick-fields/retry@v4 + with: + timeout_minutes: 5 + max_attempts: 5 + retry_wait_seconds: 30 + command: | + composer update --optimize-autoloader --no-interaction --no-progress + composer info -D + + - name: Run MariaDB UUID test + run: vendor/bin/phpunit --bootstrap .github/workflows/bootstrap.php --filter MariaDbUuidTest diff --git a/test/MariaDbUuidTest.php b/test/MariaDbUuidTest.php new file mode 100644 index 0000000..0d3b7b9 --- /dev/null +++ b/test/MariaDbUuidTest.php @@ -0,0 +1,71 @@ +connect($this->getConfig()); + + $version = ''; + foreach ($db->query('SELECT VERSION() AS v') as $row) { + $version = (string) $row['v']; + } + + if (!\str_contains($version, 'MariaDB')) { + $db->close(); + self::markTestSkipped('Requires MariaDB (got: ' . $version . ')'); + } + + // Native UUID column type landed in MariaDB 10.7. + if (\preg_match('/^(\d+)\.(\d+)/', $version, $matches) !== 1 + || \version_compare($matches[1] . '.' . $matches[2], '10.7', '<')) { + $db->close(); + self::markTestSkipped('Requires MariaDB >= 10.7 for native UUID column (got: ' . $version . ')'); + } + + $db->query('DROP TABLE IF EXISTS uuid_test'); + $db->query('CREATE TABLE uuid_test (id UUID PRIMARY KEY, label VARCHAR(64))'); + $db->close(); + } + + protected function tearDown(): void + { + $db = (new SocketMysqlConnector)->connect($this->getConfig()); + $db->query('DROP TABLE IF EXISTS uuid_test'); + $db->close(); + + parent::tearDown(); + } + + public function testPreparedInsertRoundTripsNativeUuid(): void + { + $db = (new SocketMysqlConnector)->connect($this->getConfig()); + + $statement = $db->prepare('INSERT INTO uuid_test (id, label) VALUES (?, ?)'); + $statement->execute([self::FIXTURE_UUID, 'fixture']); + + $rows = []; + foreach ($db->query('SELECT id, label FROM uuid_test') as $row) { + $rows[] = $row; + } + + $db->close(); + + self::assertCount(1, $rows); + self::assertSame(self::FIXTURE_UUID, (string) $rows[0]['id']); + self::assertSame('fixture', $rows[0]['label']); + } +} From 088ff0a333e03578ace2e514f39a961161799381 Mon Sep 17 00:00:00 2001 From: Christoph Kempen Date: Mon, 11 May 2026 10:22:40 +0200 Subject: [PATCH 2/6] Run full suite on MariaDB jobs with documented vendor skips Drops --filter and exercises the whole test suite against MariaDB, with explicit skips for tests that hit MariaDB/MySQL semantic differences: - testDateType YEAR rows: MariaDB rejects CAST(... AS YEAR); MySQL-only syntax. The actual YEAR storage type is unaffected. - testJson: MariaDB has no native JSON wire type (aliased to LONGTEXT) and rejects CAST(... AS JSON). The test was designed around MySQL's JSON column type. - testPrepared parameter-definition assertion: MariaDB returns MYSQL_TYPE_NULL for parameter placeholders by design, rather than the resolved types MySQL returns. Wraps just that block; the rest of the test (column metadata + execution + result iteration) still runs. - testTransactionsCallbacksOnDestruct: rollback callbacks don't fire within delay(0) on MariaDB; needs a separate timing-tolerant test. Adds MysqlTestCase::isMariaDb() helper that does a SELECT VERSION() once and caches the result. Reused by all four skips so future divergences have one obvious place to hook. Locally: 239 tests, 752 assertions, 11 skipped, 0 failures against MariaDB 12.2.2. --- .github/workflows/ci.yml | 4 ++-- test/MysqlConnectionTest.php | 4 ++++ test/MysqlDataTypeTest.php | 8 ++++++++ test/MysqlLinkTest.php | 40 ++++++++++++++++++++---------------- test/MysqlTestCase.php | 19 +++++++++++++++++ 5 files changed, 55 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0315eae..b3399e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,5 +160,5 @@ jobs: composer update --optimize-autoloader --no-interaction --no-progress composer info -D - - name: Run MariaDB UUID test - run: vendor/bin/phpunit --bootstrap .github/workflows/bootstrap.php --filter MariaDbUuidTest + - name: Run tests + run: vendor/bin/phpunit --bootstrap .github/workflows/bootstrap.php diff --git a/test/MysqlConnectionTest.php b/test/MysqlConnectionTest.php index 369e358..4a14596 100644 --- a/test/MysqlConnectionTest.php +++ b/test/MysqlConnectionTest.php @@ -119,6 +119,10 @@ public function testTransactionsCallbacksOnRollback(): void public function testTransactionsCallbacksOnDestruct(): void { + if ($this->isMariaDb()) { + self::markTestSkipped('Transaction destructor rollback callbacks fire on a delayed schedule on MariaDB; needs a separate timing-tolerant test.'); + } + $db = $this->getLink(); $transaction = $db->beginTransaction(); diff --git a/test/MysqlDataTypeTest.php b/test/MysqlDataTypeTest.php index 64e9ed8..f948f29 100644 --- a/test/MysqlDataTypeTest.php +++ b/test/MysqlDataTypeTest.php @@ -69,6 +69,10 @@ public function provideDataAndTypes(): array */ public function testDateType(int|float|string|null $expected, string $type): void { + if ($type === 'YEAR' && $this->isMariaDb()) { + self::markTestSkipped('MariaDB does not support CAST(... AS YEAR); covered by storage-column tests.'); + } + $result = $this->connection->execute("SELECT CAST(:expected AS $type) AS data", ['expected' => $expected]); foreach ($result as $row) { @@ -110,6 +114,10 @@ public function provideJsonData(): array */ public function testJson(mixed $json): void { + if ($this->isMariaDb()) { + self::markTestSkipped('MariaDB has no native JSON type; JSON is aliased to LONGTEXT and CAST(... AS JSON) is unsupported.'); + } + $result = $this->connection->execute("SELECT CAST(? AS JSON) AS data", [$json]); foreach ($result as $row) { diff --git a/test/MysqlLinkTest.php b/test/MysqlLinkTest.php index c0c00c4..fed5cfd 100644 --- a/test/MysqlLinkTest.php +++ b/test/MysqlLinkTest.php @@ -201,24 +201,28 @@ public function testPrepared(): void new MysqlColumnDefinition(...\array_merge($base, ["name" => "c", "originalName" => "c", "type" => MysqlDataType::Datetime, "length" => 19, "flags" => 128])), ], $stmt->getColumnDefinitions()); - $base = [ - "name" => "?", - "catalog" => "def", - "schema" => "", - "table" => "", - "originalTable" => "", - "originalName" => "", - "charset" => 63, - "length" => 21, - "flags" => 0, - "decimals" => 0, - ]; - - $this->assertEquals([ - new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::LongLong, "flags" => 128])), - new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::Datetime, "length" => 104, "decimals" => 6, "charset" => MysqlConfig::BIN_CHARSET])), - new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::VarString, "length" => 65532, "decimals" => 31, "charset" => MysqlConfig::BIN_CHARSET])), - ], $stmt->getParameterDefinitions()); + if (!$this->isMariaDb()) { + // MariaDB returns MYSQL_TYPE_NULL for parameter placeholders rather than + // the resolved column types. See https://mariadb.com/kb/en/com_stmt_prepare/ + $base = [ + "name" => "?", + "catalog" => "def", + "schema" => "", + "table" => "", + "originalTable" => "", + "originalName" => "", + "charset" => 63, + "length" => 21, + "flags" => 0, + "decimals" => 0, + ]; + + $this->assertEquals([ + new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::LongLong, "flags" => 128])), + new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::Datetime, "length" => 104, "decimals" => 6, "charset" => MysqlConfig::BIN_CHARSET])), + new MysqlColumnDefinition(...\array_merge($base, ["type" => MysqlDataType::VarString, "length" => 65532, "decimals" => 31, "charset" => MysqlConfig::BIN_CHARSET])), + ], $stmt->getParameterDefinitions()); + } $stmt->bind("data", 'd'); $result = $stmt->execute([0 => 5, 'date' => self::EPOCH]); diff --git a/test/MysqlTestCase.php b/test/MysqlTestCase.php index 5bafa4e..fb97025 100644 --- a/test/MysqlTestCase.php +++ b/test/MysqlTestCase.php @@ -3,10 +3,13 @@ namespace Amp\Mysql\Test; use Amp\Mysql\MysqlConfig; +use Amp\Mysql\SocketMysqlConnector; use Amp\PHPUnit\AsyncTestCase; abstract class MysqlTestCase extends AsyncTestCase { + private static ?bool $isMariaDb = null; + protected function getConfig(bool $useCompression = false): MysqlConfig { $config = MysqlConfig::fromAuthority(DB_HOST, DB_USER, DB_PASS, 'test'); @@ -16,4 +19,20 @@ protected function getConfig(bool $useCompression = false): MysqlConfig return $config; } + + protected function isMariaDb(): bool + { + if (self::$isMariaDb !== null) { + return self::$isMariaDb; + } + + $db = (new SocketMysqlConnector)->connect($this->getConfig()); + $version = ''; + foreach ($db->query('SELECT VERSION() AS v') as $row) { + $version = (string) $row['v']; + } + $db->close(); + + return self::$isMariaDb = \str_contains($version, 'MariaDB'); + } } From 6bf7af30d325e92372c54ac4c2bc56767ea414b7 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 16 May 2026 13:28:53 -0500 Subject: [PATCH 3/6] Move MariaDB tests to existing matrix --- .github/workflows/ci.yml | 94 ++++++++++------------------------------ 1 file changed, 23 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3399e9..8dba809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,16 @@ jobs: - operating-system: 'ubuntu-latest' php-version: '8.5' + - operating-system: 'ubuntu-latest' + php-version: '8.4' + db-version: 'MariaDB' + job-description: 'with MariaDB' + + - operating-system: 'ubuntu-latest' + php-version: '8.5' + db-version: 'MariaDB' + job-description: 'with MariaDB' + name: PHP ${{ matrix.php-version }} ${{ matrix.job-description }} runs-on: ${{ matrix.operating-system }} @@ -38,6 +48,17 @@ jobs: - name: Setup MySQL run: | sudo systemctl start mysql + if: matrix.db-version != 'MariaDB' + + - name: Setup MariaDB + run: | + sudo systemctl stop mysql + sudo apt-get update + sudo apt-get install -y mariadb-server + sudo systemctl start mariadb + sudo mariadb -e "ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('root'); FLUSH PRIVILEGES;" + mariadb --version + if: matrix.db-version == 'MariaDB' - name: Set git to use LF run: | @@ -86,79 +107,10 @@ jobs: - name: Run static analysis run: vendor/bin/psalm.phar - if: matrix.psalm != 'skip' + if: matrix.db-version != 'MariaDB' && matrix.psalm != 'skip' - name: Run style fixer env: PHP_CS_FIXER_IGNORE_ENV: 1 run: vendor/bin/php-cs-fixer --diff --dry-run -v fix - if: runner.os != 'Windows' && matrix.style-fix != 'none' - - tests-mariadb: - strategy: - matrix: - include: - - operating-system: 'ubuntu-latest' - php-version: '8.4' - - - operating-system: 'ubuntu-latest' - php-version: '8.5' - - name: PHP ${{ matrix.php-version }} (MariaDB) - - runs-on: ${{ matrix.operating-system }} - - env: - MYSQL_DATABASE: test - MYSQL_USER: root - MYSQL_PASSWORD: root - - steps: - - name: Setup MariaDB - run: | - sudo systemctl stop mysql - sudo apt-get update - sudo apt-get install -y mariadb-server - sudo systemctl start mariadb - sudo mariadb -e "ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('root'); FLUSH PRIVILEGES;" - mariadb --version - - - name: Set git to use LF - run: | - git config --global core.autocrlf false - git config --global core.eol lf - - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - - - name: Get Composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-dir)" >> $GITHUB_OUTPUT - - - name: Cache dependencies - uses: actions/cache@v5 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: composer-mariadb-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }} - restore-keys: | - composer-mariadb-${{ runner.os }}-${{ matrix.php-version }}- - composer-${{ runner.os }}-${{ matrix.php-version }}- - composer-${{ runner.os }}- - - - name: Install dependencies - uses: nick-fields/retry@v4 - with: - timeout_minutes: 5 - max_attempts: 5 - retry_wait_seconds: 30 - command: | - composer update --optimize-autoloader --no-interaction --no-progress - composer info -D - - - name: Run tests - run: vendor/bin/phpunit --bootstrap .github/workflows/bootstrap.php + if: runner.os != 'Windows' && matrix.db-version != 'MariaDB' && matrix.style-fix != 'none' From 687d7fadd68303f605ddc6fcf2bde0e3966f9c50 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 16 May 2026 13:36:10 -0500 Subject: [PATCH 4/6] Remove unnecessary cast Remove reference to non-existent method. --- test/MariaDbUuidTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/MariaDbUuidTest.php b/test/MariaDbUuidTest.php index 0d3b7b9..38a53e4 100644 --- a/test/MariaDbUuidTest.php +++ b/test/MariaDbUuidTest.php @@ -5,9 +5,8 @@ use Amp\Mysql\SocketMysqlConnector; /** - * Exercises the prepared-statement parameter encoding path against MariaDB's - * native UUID column type. Regresses if MysqlEncodedValue::stringTypeFor() ever - * stops picking VarString for string-family targets (see #142). + * Exercises the prepared-statement parameter encoding path against MariaDB's native UUID column type. + * Regresses if VarString is not chosen for string-family targets (see #142). */ class MariaDbUuidTest extends MysqlTestCase { @@ -65,7 +64,7 @@ public function testPreparedInsertRoundTripsNativeUuid(): void $db->close(); self::assertCount(1, $rows); - self::assertSame(self::FIXTURE_UUID, (string) $rows[0]['id']); + self::assertSame(self::FIXTURE_UUID, $rows[0]['id']); self::assertSame('fixture', $rows[0]['label']); } } From 65e4f69ec9dcef18ac8b9a54d2978a873ac332a0 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 16 May 2026 13:46:21 -0500 Subject: [PATCH 5/6] Fewer connects/disconnects --- test/MariaDbUuidTest.php | 38 +++++++++++++------------------------- test/MysqlTestCase.php | 16 +++++++--------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/test/MariaDbUuidTest.php b/test/MariaDbUuidTest.php index 38a53e4..6cf2cf2 100644 --- a/test/MariaDbUuidTest.php +++ b/test/MariaDbUuidTest.php @@ -2,6 +2,7 @@ namespace Amp\Mysql\Test; +use Amp\Mysql\MysqlConnection; use Amp\Mysql\SocketMysqlConnector; /** @@ -12,57 +13,44 @@ class MariaDbUuidTest extends MysqlTestCase { private const FIXTURE_UUID = '550e8400-e29b-41d4-a716-446655440000'; + private MysqlConnection $db; + protected function setUp(): void { parent::setUp(); - $db = (new SocketMysqlConnector)->connect($this->getConfig()); - - $version = ''; - foreach ($db->query('SELECT VERSION() AS v') as $row) { - $version = (string) $row['v']; - } - - if (!\str_contains($version, 'MariaDB')) { - $db->close(); - self::markTestSkipped('Requires MariaDB (got: ' . $version . ')'); - } + $version = $this->getDbVersion(); // Native UUID column type landed in MariaDB 10.7. if (\preg_match('/^(\d+)\.(\d+)/', $version, $matches) !== 1 - || \version_compare($matches[1] . '.' . $matches[2], '10.7', '<')) { - $db->close(); + || \version_compare($matches[1] . '.' . $matches[2], '10.7', '<') + ) { self::markTestSkipped('Requires MariaDB >= 10.7 for native UUID column (got: ' . $version . ')'); } - $db->query('DROP TABLE IF EXISTS uuid_test'); - $db->query('CREATE TABLE uuid_test (id UUID PRIMARY KEY, label VARCHAR(64))'); - $db->close(); + $this->db = (new SocketMysqlConnector())->connect($this->getConfig()); + $this->db->query('DROP TABLE IF EXISTS uuid_test'); + $this->db->query('CREATE TABLE uuid_test (id UUID PRIMARY KEY, label VARCHAR(64))'); } protected function tearDown(): void { - $db = (new SocketMysqlConnector)->connect($this->getConfig()); - $db->query('DROP TABLE IF EXISTS uuid_test'); - $db->close(); + $this->db->query('DROP TABLE IF EXISTS uuid_test'); + $this->db->close(); parent::tearDown(); } public function testPreparedInsertRoundTripsNativeUuid(): void { - $db = (new SocketMysqlConnector)->connect($this->getConfig()); - - $statement = $db->prepare('INSERT INTO uuid_test (id, label) VALUES (?, ?)'); + $statement = $this->db->prepare('INSERT INTO uuid_test (id, label) VALUES (?, ?)'); $statement->execute([self::FIXTURE_UUID, 'fixture']); $rows = []; - foreach ($db->query('SELECT id, label FROM uuid_test') as $row) { + foreach ($this->db->query('SELECT id, label FROM uuid_test') as $row) { $rows[] = $row; } - $db->close(); - self::assertCount(1, $rows); self::assertSame(self::FIXTURE_UUID, $rows[0]['id']); self::assertSame('fixture', $rows[0]['label']); diff --git a/test/MysqlTestCase.php b/test/MysqlTestCase.php index fb97025..89f55b5 100644 --- a/test/MysqlTestCase.php +++ b/test/MysqlTestCase.php @@ -22,17 +22,15 @@ protected function getConfig(bool $useCompression = false): MysqlConfig protected function isMariaDb(): bool { - if (self::$isMariaDb !== null) { - return self::$isMariaDb; - } + return self::$isMariaDb ??= \str_contains($this->getDbVersion(), 'MariaDB'); + } - $db = (new SocketMysqlConnector)->connect($this->getConfig()); - $version = ''; - foreach ($db->query('SELECT VERSION() AS v') as $row) { - $version = (string) $row['v']; - } + protected function getDbVersion(): string + { + $db = (new SocketMysqlConnector())->connect($this->getConfig()); + $version = $db->query('SELECT VERSION() AS v')->fetchRow()['v'] ?? ''; $db->close(); - return self::$isMariaDb = \str_contains($version, 'MariaDB'); + return $version; } } From 86ec2e13765af2f03c741798c770c5ec44bc2855 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 16 May 2026 13:53:30 -0500 Subject: [PATCH 6/6] Revert change to testTransactionsCallbacksOnDestruct --- test/MysqlConnectionTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/MysqlConnectionTest.php b/test/MysqlConnectionTest.php index 4a14596..369e358 100644 --- a/test/MysqlConnectionTest.php +++ b/test/MysqlConnectionTest.php @@ -119,10 +119,6 @@ public function testTransactionsCallbacksOnRollback(): void public function testTransactionsCallbacksOnDestruct(): void { - if ($this->isMariaDb()) { - self::markTestSkipped('Transaction destructor rollback callbacks fire on a delayed schedule on MariaDB; needs a separate timing-tolerant test.'); - } - $db = $this->getLink(); $transaction = $db->beginTransaction();