131 lines
4.3 KiB
PHP
131 lines
4.3 KiB
PHP
<?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');
|
|
}
|
|
}
|