246 lines
7.1 KiB
PHP
246 lines
7.1 KiB
PHP
<?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());
|
|
}
|
|
}
|