diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49b2333..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,10 +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' + if: runner.os != 'Windows' && matrix.db-version != 'MariaDB' && matrix.style-fix != 'none' diff --git a/test/MariaDbUuidTest.php b/test/MariaDbUuidTest.php new file mode 100644 index 0000000..6cf2cf2 --- /dev/null +++ b/test/MariaDbUuidTest.php @@ -0,0 +1,58 @@ +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', '<') + ) { + self::markTestSkipped('Requires MariaDB >= 10.7 for native UUID column (got: ' . $version . ')'); + } + + $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 + { + $this->db->query('DROP TABLE IF EXISTS uuid_test'); + $this->db->close(); + + parent::tearDown(); + } + + public function testPreparedInsertRoundTripsNativeUuid(): void + { + $statement = $this->db->prepare('INSERT INTO uuid_test (id, label) VALUES (?, ?)'); + $statement->execute([self::FIXTURE_UUID, 'fixture']); + + $rows = []; + foreach ($this->db->query('SELECT id, label FROM uuid_test') as $row) { + $rows[] = $row; + } + + self::assertCount(1, $rows); + self::assertSame(self::FIXTURE_UUID, $rows[0]['id']); + self::assertSame('fixture', $rows[0]['label']); + } +} 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..89f55b5 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,18 @@ protected function getConfig(bool $useCompression = false): MysqlConfig return $config; } + + protected function isMariaDb(): bool + { + return self::$isMariaDb ??= \str_contains($this->getDbVersion(), 'MariaDB'); + } + + protected function getDbVersion(): string + { + $db = (new SocketMysqlConnector())->connect($this->getConfig()); + $version = $db->query('SELECT VERSION() AS v')->fetchRow()['v'] ?? ''; + $db->close(); + + return $version; + } }