feat(test): add multi-engine integration matrix
This commit is contained in:
130
tests/Integration/ModelSearchTest.php
Normal file
130
tests/Integration/ModelSearchTest.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
use DateTime;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* ModelSearchTest - DuckBrain integration matrix
|
||||
*
|
||||
* Family: search (mysql CONCAT branch vs sqlite/pgsql CAST||LIKE branch),
|
||||
* plus filters that bind every supported column type and must agree on
|
||||
* matches across engines. Search terms are chosen case-insensitively stable
|
||||
* (MySQL LIKE is case-insensitive by collation, PostgreSQL's is not).
|
||||
*/
|
||||
#[Group('integration')]
|
||||
final class ModelSearchTest extends TestCase
|
||||
{
|
||||
/** @var array<string,int> */
|
||||
private static array $ids = [];
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
self::createTable('users', [
|
||||
'id' => 'pk',
|
||||
'username' => 'string',
|
||||
'email' => 'string',
|
||||
'bio' => 'text',
|
||||
'age' => 'number',
|
||||
'is_active' => 'bool',
|
||||
'last_login' => 'timestamp',
|
||||
]);
|
||||
|
||||
$seed = [
|
||||
['alice', 'alice@duckbrain.dev', 'alpha keeper', 20, true],
|
||||
['bob', 'bob@duckbrain.dev', 'beta reader', 30, true],
|
||||
['carol', 'carol@duckbrain.dev', 'gamma dreamer', 40, false],
|
||||
['dave', 'dave@duckbrain.dev', 'delta builder', 50, true],
|
||||
];
|
||||
|
||||
foreach ($seed as [$username, $email, $bio, $age, $active]) {
|
||||
$user = new User();
|
||||
$user->username = $username;
|
||||
$user->email = $email;
|
||||
$user->bio = $bio;
|
||||
$user->age = $age;
|
||||
$user->isActive = $active;
|
||||
$user->lastLogin = new DateTime('2026-09-05 10:00:00');
|
||||
$user->save();
|
||||
self::$ids[$username] = (int) $user->id;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private static function idsOf(array $rows): array
|
||||
{
|
||||
$ids = array_map(static fn (User $u): int => (int) $u->id, $rows);
|
||||
sort($ids);
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function searchMatchesAcrossAllDefaultColumns(): void
|
||||
{
|
||||
// 'ali' occurs only in alice's username/bio space.
|
||||
$this->assertSame(
|
||||
[self::$ids['alice']],
|
||||
self::idsOf(User::search('ali')->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function searchFindsNumericValueThroughTextCasting(): void
|
||||
{
|
||||
// The number column must be matched by its textual form: sqlite
|
||||
// casts REAL to '30.0' while mysql/pgsql yield '30'; both contain 30.
|
||||
$this->assertSame(
|
||||
[self::$ids['bob']],
|
||||
self::idsOf(User::search('30')->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function searchHonorsExplicitColumnList(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[self::$ids['alice']],
|
||||
self::idsOf(User::search('lpha', ['bio'])->get())
|
||||
);
|
||||
$this->assertSame(
|
||||
[self::$ids['carol']],
|
||||
self::idsOf(User::search('arol', ['username', 'email'])->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function searchCombinesWithWhere(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[self::$ids['alice']],
|
||||
self::idsOf(User::where('is_active', '1')->search('li')->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function everyColumnTypeFiltersConsistently(): void
|
||||
{
|
||||
$this->assertSame([self::$ids['alice']], self::idsOf(User::where('id', self::$ids['alice'])->get()));
|
||||
$this->assertSame([self::$ids['bob']], self::idsOf(User::where('email', 'bob@duckbrain.dev')->get()));
|
||||
$this->assertSame([self::$ids['carol']], self::idsOf(User::where('is_active', '0')->get()));
|
||||
$this->assertCount(4, User::where('last_login', '>', '2026-09-05 09:00:00')->get());
|
||||
$this->assertSame([self::$ids['dave']], self::idsOf(User::where('age', '>=', '50')->get()));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function booleanFalseSurvivesTheRoundTrip(): void
|
||||
{
|
||||
// Promise of the matrix: typed properties hydrate faithfully on the
|
||||
// three engines. PostgreSQL reports boolean false as 'f' and the
|
||||
// core casts nothing -> this is the R2 probe.
|
||||
$carol = User::getById(self::$ids['carol']);
|
||||
|
||||
$this->assertNotNull($carol);
|
||||
$this->assertFalse($carol->isActive, 'inactive user must hydrate as bool false');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user