From 781df1968736568243e4b1c10f3fd9bdb0030256 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 11 Mar 2026 20:37:49 +0600 Subject: [PATCH 1/7] =?UTF-8?q?orWhere()=20null=20=E2=86=92=20IS=20NULL=20?= =?UTF-8?q?(not=20col=20=3D=20=3F=20with=20null=20binding)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Model/Query/EntityModelQueryTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index dd8bbbb..1d0b200 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -2770,4 +2770,24 @@ public function testWithAge(): void $this->assertFalse($user->age > 10000000000); } } + + + public function testOrWhereNullGeneratesIsNull(): void + { + $users = MockUser::where('status', 'active') + ->orWhere('email', null) + ->get(); + + $this->assertCount(2, $users); + } + + public function testOrWhereNullSql(): void + { + $sql = MockUser::where('status', 'active') + ->orWhere('email', null) + ->toSql(); + + $this->assertStringContainsString('IS NULL', $sql); + $this->assertStringNotContainsString('email = ?', $sql); + } } From a809d13c7a94e5737fcc3472799bfcb9bb61ec75 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 11 Mar 2026 20:39:21 +0600 Subject: [PATCH 2/7] distinct() / increment() / decrement() with complex conditions --- tests/Model/Query/EntityModelQueryTest.php | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index 1d0b200..f5f4805 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -2790,4 +2790,62 @@ public function testOrWhereNullSql(): void $this->assertStringContainsString('IS NULL', $sql); $this->assertStringNotContainsString('email = ?', $sql); } + + public function testDistinctWithWhereIn(): void + { + // distinct() must work with whereIn, not just simple = conditions + $userIds = MockPost::query() + ->whereIn('user_id', [1, 2]) + ->distinct('user_id'); + + $this->assertEquals([1, 2], $userIds->toArray()); + } + + public function testIncrementWithWhereIn(): void + { + // increment() with whereIn must update all matched rows correctly + $affected = MockPost::query() + ->whereIn('id', [1, 2]) + ->increment('views', 10); + + $this->assertEquals(2, $affected); + + $post1 = MockPost::find(1); + $post2 = MockPost::find(2); + + $this->assertEquals(110, $post1->views); + $this->assertEquals(60, $post2->views); + } + + public function testDecrementWithWhereIn(): void + { + $affected = MockPost::query() + ->whereIn('id', [1, 2]) + ->decrement('views', 5); + + $this->assertEquals(2, $affected); + + $post1 = MockPost::find(1); + $post2 = MockPost::find(2); + + $this->assertEquals(95, $post1->views); + $this->assertEquals(45, $post2->views); + } + + public function testIncrementWithNestedWhere(): void + { + $affected = MockPost::query() + ->where(function ($q) { + $q->where('status', 1) + ->whereIn('user_id', [1, 2]); + }) + ->increment('views', 100); + + // post 1 (views=100), post 3 (views=200), post 4 (views=150) match + $this->assertEquals(3, $affected); + + $this->assertEquals(200, MockPost::find(1)->views); + $this->assertEquals(300, MockPost::find(3)->views); + $this->assertEquals(250, MockPost::find(4)->views); + } } From 131a561ace9fbe563ce53e813697e9d8cd9be4b7 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 11 Mar 2026 20:47:52 +0600 Subject: [PATCH 3/7] saveMany() adds timestamps --- tests/Model/Query/EntityModelQueryTest.php | 61 ++++++++++++++++++---- tests/Support/Model/MockAnotherUser.php | 13 +++++ 2 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 tests/Support/Model/MockAnotherUser.php diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index f5f4805..6dcbae7 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -3,20 +3,21 @@ namespace Tests\Unit\Model\Query; use App\Models\Product; -use Tests\Support\Model\MockUser; -use Tests\Support\Model\MockTag; -use Tests\Support\Model\MockPost; -use Tests\Support\Model\MockComment; -use Tests\Support\MockContainer; -use Phaseolies\Support\UrlGenerator; -use Phaseolies\Support\Facades\DB; -use Phaseolies\Support\Collection; -use Phaseolies\Http\Request; +use PDO; use Phaseolies\Database\Database; use Phaseolies\DI\Container; +use Phaseolies\Http\Request; +use Phaseolies\Support\Collection; +use Phaseolies\Support\Facades\DB; +use Phaseolies\Support\UrlGenerator; use PHPUnit\Framework\TestCase; -use PDO; +use Tests\Support\MockContainer; +use Tests\Support\Model\MockAnotherUser; +use Tests\Support\Model\MockComment; +use Tests\Support\Model\MockPost; use Tests\Support\Model\MockProduct; +use Tests\Support\Model\MockTag; +use Tests\Support\Model\MockUser; class EntityModelQueryTest extends TestCase { @@ -57,6 +58,18 @@ private function createTestTables(): void ) "); + $this->pdo->exec(" + CREATE TABLE userss ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE, + age INTEGER, + status TEXT DEFAULT 'active', + created_at TEXT, + updated_at TEXT + ) + "); + // Create posts table $this->pdo->exec(" CREATE TABLE posts ( @@ -2848,4 +2861,32 @@ public function testIncrementWithNestedWhere(): void $this->assertEquals(300, MockPost::find(3)->views); $this->assertEquals(250, MockPost::find(4)->views); } + + public function testSaveManyAddsTimestamps(): void + { + // MockAnotherUser has timestamps true + MockAnotherUser::saveMany([ + ['name' => 'Timestamp Test', 'email' => 'ts@example.com', 'age' => 20, 'status' => 'active'], + ]); + + $user = MockAnotherUser::where('email', 'ts@example.com')->first(); + + $this->assertNotNull($user->created_at, 'created_at must be set by saveMany()'); + $this->assertNotNull($user->updated_at, 'updated_at must be set by saveMany()'); + } + + public function testSaveManyWithExistingCreatedAtIsNotOverwritten(): void + { + $fixed = '2020-01-01 00:00:00'; + + MockAnotherUser::saveMany([ + ['name' => 'Fixed Date', 'email' => 'fixed@example.com', 'age' => 20, 'status' => 'active', 'created_at' => $fixed], + ]); + + $user = MockAnotherUser::where('email', 'fixed@example.com')->first(); + + $this->assertEquals($fixed, $user->created_at, 'Existing created_at must not be overwritten'); + } + + } diff --git a/tests/Support/Model/MockAnotherUser.php b/tests/Support/Model/MockAnotherUser.php new file mode 100644 index 0000000..b04f58d --- /dev/null +++ b/tests/Support/Model/MockAnotherUser.php @@ -0,0 +1,13 @@ + Date: Wed, 11 Mar 2026 20:49:03 +0600 Subject: [PATCH 4/7] getDirtyAttributes() strict comparison --- tests/Model/Query/EntityModelQueryTest.php | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index 6dcbae7..201be24 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -2888,5 +2888,39 @@ public function testSaveManyWithExistingCreatedAtIsNotOverwritten(): void $this->assertEquals($fixed, $user->created_at, 'Existing created_at must not be overwritten'); } - + public function testDirtyAttributesDetectsZeroToEmptyString(): void + { + $post = MockPost::find(1); // views = 100 (string from DB) + + // Simulate setting views to '' (empty string) + $post->setAttribute('views', ''); + + $dirty = $post->getDirtyAttributes(); + + $this->assertArrayHasKey('views', $dirty, "'' vs '100' must be detected as dirty"); + } + + public function testDirtyAttributesDoesNotFalselyMarkStringIntAsDirty(): void + { + $post = MockPost::find(1); // views comes back as string "100" from SQLite + + // Setting same value as integer — should NOT be dirty due to string cast + $post->setAttribute('views', 100); + + $dirty = $post->getDirtyAttributes(); + + $this->assertArrayNotHasKey('views', $dirty, "'100' vs 100 must not be dirty"); + } + + public function testDirtyAttributesBothNullNotDirty(): void + { + $user = MockUser::find(1); + // email exists, force-set original to null and current to null + $user->setOriginalAttributes(array_merge($user->getOriginalAttributes(), ['email' => null])); + $user->setAttribute('email', null); + + $dirty = $user->getDirtyAttributes(); + + $this->assertArrayNotHasKey('email', $dirty, 'null === null must not be dirty'); + } } From a2303461a59ffe9846182bd32d597884ade70ca9 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 11 Mar 2026 20:50:08 +0600 Subject: [PATCH 5/7] Model::__get() re-throws exceptions --- tests/Model/Query/EntityModelQueryTest.php | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index 201be24..c4cf7b0 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -2923,4 +2923,31 @@ public function testDirtyAttributesBothNullNotDirty(): void $this->assertArrayNotHasKey('email', $dirty, 'null === null must not be dirty'); } + + public function testMagicGetReturnsAttributeCorrectly(): void + { + $user = MockUser::find(1); + + $this->assertEquals('John Doe', $user->name); + $this->assertEquals('john@example.com', $user->email); + } + + public function testMagicGetReturnsNullForNonExistentProperty(): void + { + $user = MockUser::find(1); + + // Non-existent property must return null, not throw + $this->assertNull($user->nonExistentProperty); + } + + public function testMagicGetReturnsRelationCollection(): void + { + $user = MockUser::find(1); + + // Lazy load posts via __get + $posts = $user->posts; + + $this->assertInstanceOf(Collection::class, $posts); + $this->assertCount(3, $posts); + } } From c45c6448a3314f94b6b40293698d2cff8f413def Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 11 Mar 2026 20:53:05 +0600 Subject: [PATCH 6/7] toArray() pivot data and null relations --- tests/Model/Query/EntityModelQueryTest.php | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index c4cf7b0..18ece5b 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -2950,4 +2950,44 @@ public function testMagicGetReturnsRelationCollection(): void $this->assertInstanceOf(Collection::class, $posts); $this->assertCount(3, $posts); } + + + public function testToArrayIncludesPivotData(): void + { + $post = MockPost::query() + ->select('id', 'title', 'user_id') + ->embed('tags') + ->find(1); + + $array = $post->toArray(); + + $this->assertArrayHasKey('tags', $array); + $this->assertArrayHasKey('pivot', $array['tags'][0]); + $this->assertIsObject($array['tags'][0]['pivot']); + $this->assertEquals(1, $array['tags'][0]['pivot']->post_id); + } + + public function testToArrayNullRelationIncludedAsNull(): void + { + $user = MockUser::find(1); + + // Manually set a null relation + $user->setRelation('profile', null); + + $array = $user->toArray(); + + $this->assertArrayHasKey('profile', $array); + $this->assertNull($array['profile']); + } + + public function testToArrayRecursesIntoNestedRelations(): void + { + $post = MockPost::embed('user')->find(1); + + $array = $post->toArray(); + + $this->assertArrayHasKey('user', $array); + $this->assertIsArray($array['user']); + $this->assertEquals('John Doe', $array['user']['name']); + } } From 8310de4058859ab6a98213c9d000bc8e7efadb08 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 11 Mar 2026 20:54:24 +0600 Subject: [PATCH 7/7] sanitize() preserves empty string --- tests/Model/Query/EntityModelQueryTest.php | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Model/Query/EntityModelQueryTest.php b/tests/Model/Query/EntityModelQueryTest.php index 18ece5b..4726f41 100644 --- a/tests/Model/Query/EntityModelQueryTest.php +++ b/tests/Model/Query/EntityModelQueryTest.php @@ -2990,4 +2990,38 @@ public function testToArrayRecursesIntoNestedRelations(): void $this->assertIsArray($array['user']); $this->assertEquals('John Doe', $array['user']['name']); } + + public function testSanitizePreservesEmptyString(): void + { + $tag = new MockTag(); + $tag->name = ''; + + // Must remain '' not become null + $this->assertSame('', $tag->name); + } + + public function testSanitizeTrimsWhitespace(): void + { + $tag = new MockTag(); + $tag->name = ' PHP '; + + $this->assertSame('PHP', $tag->name); + } + + public function testSanitizePreservesNull(): void + { + $user = MockUser::find(1); + $user->setAttribute('status', null); + + $this->assertNull($user->status); + } + + public function testSanitizeWhitespaceOnlyBecomesEmptyStringNotNull(): void + { + $tag = new MockTag(); + $tag->name = ' '; + + // Must trim to '' not null + $this->assertSame('', $tag->name); + } }