Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@
- `mariadb` test suite, run with `vendor/bin/talon run mariadb`. Backed by `resources/phpunit.mariadb.xml` and `resources/schema/mariadb.sql`. [#26](https://github.com/phalcon/talon/issues/26)
- `mariadb:11.4` service in `docker-compose.yml`, and the matching service in both CI jobs. [#26](https://github.com/phalcon/talon/issues/26)
- `phpunit/phpcov` (^9) as a dev dependency, used to merge the per-suite coverage into `tests/_output/coverage.xml`. [#26](https://github.com/phalcon/talon/issues/26)
- `Phalcon\Talon\Database\Dialect`, an enum resolving a PDO connection to `Mysql`, `Pgsql`, or `Sqlite`, with per-dialect identifier quoting. [#27](https://github.com/phalcon/talon/issues/27)
- `DatabaseTrait::getDialect()`, returning the connection's `Dialect`. Use it to choose SQL syntax; use `getDriver()` to tell MariaDB from MySQL. [#27](https://github.com/phalcon/talon/issues/27)

### Fixed

- `Connection::select()` now matches a NULL criterion with `IS NULL` instead of `col = :col`, which matched nothing. `assertNotInDatabase('t', ['x' => null])` previously passed regardless of the data. [#27](https://github.com/phalcon/talon/issues/27)
- `Connection::select()` now quotes table and column identifiers per dialect, so reserved words such as `order` and `key` work. [#27](https://github.com/phalcon/talon/issues/27)
- `DATA_POSTGRES_SCHEMA` is now applied to the connection as `SET search_path`. It was previously read into the options and ignored. [#27](https://github.com/phalcon/talon/issues/27)

### Removed

- `resources/schema/mariadb.sql`, a byte-identical copy of `mysql.sql`. `DatabaseIntegrationTest` now loads the schema named by each suite's `dump_file` rather than deriving the path from the driver name. [#27](https://github.com/phalcon/talon/issues/27)

## [0.8.0](https://github.com/phalcon/talon/releases/tag/v0.8.0) (2026-07-15)

### Changed
Expand Down
2 changes: 1 addition & 1 deletion resources/phpunit.mariadb.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</source>
<php>
<env name="driver" value="mariadb"/>
<env name="dump_file" value="resources/schema/mariadb.sql"/>
<env name="dump_file" value="resources/schema/mysql.sql"/>
<env name="initial_queries" value="SET NAMES utf8mb4;"/>
</php>
</phpunit>
2 changes: 0 additions & 2 deletions resources/schema/mariadb.sql

This file was deleted.

28 changes: 25 additions & 3 deletions src/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public function getPdo(): PDO
$this->pdo->exec('PRAGMA journal_mode = WAL');
}

// Before initial_queries, so those can rely on the search path.
if ($this->driver === 'pgsql') {
$schema = $options['schema'] ?? '';
if (is_string($schema) && $schema !== '') {
$this->pdo->exec(
'SET search_path TO ' . Dialect::Pgsql->quoteIdentifier($schema)
);
}
}

$initialQueries = $this->settings->get('initial_queries', '');
if (is_string($initialQueries) && $initialQueries !== '') {
$this->pdo->exec($initialQueries);
Expand Down Expand Up @@ -86,15 +96,27 @@ public function loadSchema(string $dumpFile): void
*/
public function select(string $table, array $criteria = []): array
{
$dialect = Dialect::fromPdo($this->getPdo());

$where = [];
$params = [];
$index = 0;

foreach ($criteria as $key => $value) {
$where[] = $key . ' = :' . $key;
$params[$key] = $value;
$column = $dialect->quoteIdentifier((string) $key);

// `col = :p` never matches NULL in any dialect; bind no parameter.
if ($value === null) {
$where[] = $column . ' IS NULL';
continue;
}

$placeholder = 'p' . $index++;
$where[] = $column . ' = :' . $placeholder;
$params[$placeholder] = $value;
}

$sql = 'SELECT * FROM ' . $table;
$sql = 'SELECT * FROM ' . $dialect->quoteIdentifier($table);
if ($where !== []) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
Expand Down
59 changes: 59 additions & 0 deletions src/Database/Dialect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* This file is part of the Phalcon Talon.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Talon\Database;

use PDO;
use Phalcon\Talon\Exceptions\UnknownDriver;

use function explode;
use function implode;
use function is_string;
use function str_replace;

enum Dialect: string
{
case Mysql = 'mysql';
case Pgsql = 'pgsql';
case Sqlite = 'sqlite';

/**
* MariaDB connects through pdo_mysql, so PDO reports 'mysql' for it. That
* is correct here - the SQL dialect is the same. Use the configured driver
* name when a test needs to tell the two servers apart.
*/
public static function fromPdo(PDO $pdo): self
{
$name = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$name = is_string($name) ? $name : '';

return self::tryFrom($name) ?? throw new UnknownDriver($name);
}

public function quoteIdentifier(string $name): string
{
$delimiter = match ($this) {
self::Mysql => '`',
self::Pgsql, self::Sqlite => '"',
};

$quoted = [];
foreach (explode('.', $name) as $segment) {
$quoted[] = $delimiter
. str_replace($delimiter, $delimiter . $delimiter, $segment)
. $delimiter;
}

return implode('.', $quoted);
}
}
6 changes: 6 additions & 0 deletions src/Traits/DatabaseTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Phalcon\Talon\Contracts\Connection as ConnectionContract;
use Phalcon\Talon\Contracts\Settings;
use Phalcon\Talon\Database\Connection;
use Phalcon\Talon\Database\Dialect;
use Phalcon\Talon\Talon;

use function getenv;
Expand Down Expand Up @@ -74,6 +75,11 @@ public function getConnection(): ConnectionContract
return self::$connections[$driver];
}

public function getDialect(): Dialect
{
return Dialect::fromPdo($this->getConnection()->getPdo());
}

public function getDriver(): string
{
return $this->databaseDriver();
Expand Down
70 changes: 70 additions & 0 deletions tests/Database/ConnectionDriverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/**
* This file is part of the Phalcon Talon.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Talon\Tests\Database;

use PDO;
use Phalcon\Talon\Database\Dialect;
use Phalcon\Talon\PHPUnit\AbstractDatabaseTestCase;
use Phalcon\Talon\Settings;
use Phalcon\Talon\Talon;

final class ConnectionDriverTest extends AbstractDatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

Talon::useSettings(Settings::fromEnv());
self::resetConnections();
}

protected function tearDown(): void
{
Talon::reset();

parent::tearDown();
}

public function testDialectMatchesTheConfiguredDriver(): void
{
$expected = match ($this->getDriver()) {
'mariadb', 'mysql' => Dialect::Mysql,
'pgsql' => Dialect::Pgsql,
default => Dialect::Sqlite,
};

$this->assertSame($expected, $this->getDialect());
}

public function testMariadbReportsTheMysqlDialect(): void
{
if ($this->getDriver() !== 'mariadb') {
$this->markTestSkipped('MariaDB only');
}

$version = $this->getConnection()->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);

$this->assertIsString($version);
$this->assertStringContainsString('MariaDB', $version);
$this->assertSame(Dialect::Mysql, $this->getDialect());
}

public function testTheDialectValueIsThePdoDriverName(): void
{
$this->assertSame(
$this->getDialect()->value,
$this->getConnection()->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME)
);
}
}
15 changes: 10 additions & 5 deletions tests/Database/DatabaseIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
use Phalcon\Talon\Settings;
use Phalcon\Talon\Talon;

use function getenv;
use function is_string;

/**
* Driver-agnostic: runs against sqlite, mysql, or pgsql depending on the `driver`
* env set by the chosen phpunit config.
* Driver-agnostic: runs against sqlite, mysql, mariadb, or pgsql depending on the
* `driver` env set by the chosen phpunit config. The schema comes from that
* config's `dump_file`, so a suite can point at any schema it likes.
*/
final class DatabaseIntegrationTest extends AbstractDatabaseTestCase
{
Expand All @@ -32,8 +33,12 @@ protected function setUp(): void
Talon::useSettings(Settings::fromEnv());
self::resetConnections();

$driver = getenv('driver') ?: 'sqlite';
$this->getConnection()->loadSchema($this->getSettings()->rootPath('resources/schema/' . $driver . '.sql'));
$dumpFile = $this->getSettings()->get('dump_file');
$this->getConnection()->loadSchema(
$this->getSettings()->rootPath(
is_string($dumpFile) && $dumpFile !== '' ? $dumpFile : 'resources/schema/sqlite.sql'
)
);
$this->getConnection()->execute("INSERT INTO users (id, email) VALUES (1, 'john.connor@skynet.dev')");
}

Expand Down
51 changes: 51 additions & 0 deletions tests/Database/Fixtures/AbstractDriverSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* This file is part of the Phalcon Talon.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Talon\Tests\Database\Fixtures;

use Phalcon\Talon\Database\Dialect;

/**
* Per-dialect schema statements. The methods are abstract on purpose: a new
* dialect cannot be silently forgotten by a fixture that only implements some
* of them.
*/
abstract class AbstractDriverSchema
{
/**
* @return list<string>
*/
public function sqlFor(Dialect $dialect): array
{
return match ($dialect) {
Dialect::Mysql => $this->sqlMysql(),
Dialect::Pgsql => $this->sqlPgsql(),
Dialect::Sqlite => $this->sqlSqlite(),
};
}

/**
* @return list<string>
*/
abstract protected function sqlMysql(): array;

/**
* @return list<string>
*/
abstract protected function sqlPgsql(): array;

/**
* @return list<string>
*/
abstract protected function sqlSqlite(): array;
}
61 changes: 61 additions & 0 deletions tests/Database/PostgresSchemaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/**
* This file is part of the Phalcon Talon.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Talon\Tests\Database;

use Phalcon\Talon\Database\Connection;
use Phalcon\Talon\PHPUnit\AbstractDatabaseTestCase;
use Phalcon\Talon\Settings;
use Phalcon\Talon\Talon;

final class PostgresSchemaTest extends AbstractDatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

if ($this->getDriver() !== 'pgsql') {
$this->markTestSkipped('search_path applies to PostgreSQL only');
}

Talon::useSettings(Settings::fromEnv());
self::resetConnections();

$this->getConnection()->execute('CREATE SCHEMA IF NOT EXISTS talon_alt');
$this->getConnection()->execute('DROP TABLE IF EXISTS talon_alt.widgets');
$this->getConnection()->execute('CREATE TABLE talon_alt.widgets (id INTEGER)');
$this->getConnection()->execute('INSERT INTO talon_alt.widgets VALUES (7)');
}

protected function tearDown(): void
{
if ($this->getDriver() === 'pgsql') {
$this->getConnection()->execute('DROP SCHEMA IF EXISTS talon_alt CASCADE');
}

Talon::reset();

parent::tearDown();
}

public function testSearchPathResolvesUnqualifiedNames(): void
{
$settings = Settings::fromEnv(['DATA_POSTGRES_SCHEMA' => 'talon_alt']);
$scoped = new Connection($settings, 'pgsql');

$rows = $scoped->select('widgets');

$this->assertCount(1, $rows);
$this->assertEquals(7, $rows[0]['id']);
}
}
Loading