feat(test): add multi-engine integration matrix
This commit is contained in:
245
tests/Integration/ModelFilterTest.php
Normal file
245
tests/Integration/ModelFilterTest.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Libs\Model;
|
||||
use Tests\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* ModelFilterTest - DuckBrain integration matrix
|
||||
*
|
||||
* Family: select, from, where, and, or, whereIn, whereNotIn, whereNull,
|
||||
* whereNotNull, whereExists, whereNotExists, limit, orderBy, count, plus
|
||||
* resetQuery/buildQuery observed through behavior (state must not leak
|
||||
* between executed queries).
|
||||
*/
|
||||
#[Group('integration')]
|
||||
final class ModelFilterTest extends TestCase
|
||||
{
|
||||
/** @var list<int> ids of alice, bob, carol, dave */
|
||||
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',
|
||||
]);
|
||||
self::createTable('posts', [
|
||||
'id' => 'pk',
|
||||
'user_id' => 'number',
|
||||
'title' => 'string',
|
||||
'content' => 'text',
|
||||
'rating' => 'number',
|
||||
]);
|
||||
|
||||
$rows = [
|
||||
['alice', 'alice@duckbrain.dev', 'first', 20, true],
|
||||
['bob', 'bob@duckbrain.dev', 'second', 30, true],
|
||||
['carol', null, 'third', 40, false],
|
||||
['dave', 'dave@duckbrain.dev', null, 50, true],
|
||||
];
|
||||
|
||||
foreach ($rows 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->save();
|
||||
self::$ids[] = (int) $user->id;
|
||||
}
|
||||
|
||||
// A post owned by alice only, for the EXISTS tests.
|
||||
$post = new \Tests\Models\Post();
|
||||
$post->userId = self::$ids[0];
|
||||
$post->title = 'hello world';
|
||||
$post->content = 'body';
|
||||
$post->rating = 4.5;
|
||||
$post->save();
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private static function sortedIds(array $instances): array
|
||||
{
|
||||
$ids = array_map(static fn (Model $m): int => (int) $m->id, $instances);
|
||||
sort($ids);
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function whereEqualityAndOperatorForms(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[self::$ids[1]],
|
||||
self::sortedIds(User::where('username', 'bob')->get())
|
||||
);
|
||||
$this->assertSame(
|
||||
[self::$ids[2], self::$ids[3]],
|
||||
self::sortedIds(User::where('age', '>', '35')->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function andNestsConditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[self::$ids[3]],
|
||||
self::sortedIds(User::where('age', '>', '25')->and('username', 'dave')->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function orCombinesAlternativeConditions(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[self::$ids[0], self::$ids[3]],
|
||||
self::sortedIds(User::where('username', 'alice')->or('username', 'dave')->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function whereInAndWhereNotIn(): void
|
||||
{
|
||||
$in = self::sortedIds(User::whereIn('username', ['alice', 'carol'])->get());
|
||||
$this->assertSame([self::$ids[0], self::$ids[2]], $in);
|
||||
|
||||
$notIn = self::sortedIds(User::whereNotIn('username', ['alice', 'carol'])->get());
|
||||
$this->assertSame([self::$ids[1], self::$ids[3]], $notIn);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function whereNullAndWhereNotNull(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[self::$ids[3]],
|
||||
self::sortedIds(User::whereNull('bio')->get())
|
||||
);
|
||||
$this->assertSame(
|
||||
[self::$ids[2]],
|
||||
self::sortedIds(User::whereNull('email')->get())
|
||||
);
|
||||
$this->assertCount(3, User::whereNotNull('email')->get());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function whereExistsAndWhereNotExists(): void
|
||||
{
|
||||
$correlated = 'SELECT 1 FROM posts WHERE posts.user_id = users.id';
|
||||
|
||||
$this->assertSame(
|
||||
[self::$ids[0]],
|
||||
self::sortedIds(User::whereExists($correlated)->get())
|
||||
);
|
||||
$this->assertSame(
|
||||
[self::$ids[1], self::$ids[2], self::$ids[3]],
|
||||
self::sortedIds(User::whereNotExists($correlated)->get())
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function selectAndFromRestrictTheProjection(): void
|
||||
{
|
||||
$rows = User::select('username')->from('users')->where('username', 'bob')->get();
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame('bob', $rows[0]->username);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function limitSingleAndOffsetForms(): void
|
||||
{
|
||||
$firstTwo = User::orderBy('id', 'ASC')->limit(2)->get();
|
||||
$this->assertSame(
|
||||
[self::$ids[0], self::$ids[1]],
|
||||
array_map(static fn (Model $m): int => (int) $m->id, $firstTwo)
|
||||
);
|
||||
|
||||
// limit(offset, quantity) -> skip the first two.
|
||||
$lastTwo = User::orderBy('id', 'ASC')->limit(2, 2)->get();
|
||||
$this->assertSame(
|
||||
[self::$ids[2], self::$ids[3]],
|
||||
array_map(static fn (Model $m): int => (int) $m->id, $lastTwo)
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function orderByColumnAscendingAndDescending(): void
|
||||
{
|
||||
$asc = array_map(static fn (Model $m): string => (string) $m->age, User::orderBy('age', 'ASC')->get());
|
||||
$this->assertSame(['20', '30', '40', '50'], array_map('strval', array_map('intval', $asc)));
|
||||
|
||||
$descFirst = User::orderBy('age', 'DESC')->getFirst();
|
||||
$this->assertSame('dave', $descFirst->username);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function countWithAndWithoutConditions(): void
|
||||
{
|
||||
$this->assertSame(4, User::count());
|
||||
$this->assertSame(3, User::where('age', '>', '25')->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function countWithUseLimit(): void
|
||||
{
|
||||
$this->assertSame(2, User::orderBy('id', 'ASC')->limit(2)->count(true, true));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function countWithoutResetKeepsTheOriginalQuery(): void
|
||||
{
|
||||
User::where('age', '>', '25');
|
||||
$count = User::count(false);
|
||||
|
||||
$this->assertSame(3, $count);
|
||||
$this->assertSame(
|
||||
[self::$ids[1], self::$ids[2], self::$ids[3]],
|
||||
self::sortedIds(User::get()),
|
||||
'count(false) must leave the where active; get() consumes it'
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function queryStateResetsAfterExecution(): void
|
||||
{
|
||||
$one = User::where('id', self::$ids[0])->getFirst();
|
||||
$this->assertSame('alice', $one->username);
|
||||
|
||||
$this->assertCount(4, User::all(), 'where must not leak into the next query');
|
||||
$this->assertCount(4, User::get());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
public function orderByRandReturnsEveryRow(): void
|
||||
{
|
||||
// The readme matrix promises orderBy ok on the three engines.
|
||||
// Model emits literal RAND(); sqlite and PostgreSQL know RANDOM().
|
||||
// This test documents the promise; failures are findings, not noise.
|
||||
$this->assertCount(4, User::orderBy('RAND')->get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user