Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
152 / 152
100.00% covered (success)
100.00%
2 / 2
CRAP
n/a
0 / 0
createSqlSpecStub
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
registerBaseSqlDriverTests
100.00% covered (success)
100.00%
146 / 146
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10use Phexium\Domain\Specification\SpecificationInterface;
11use Tests\Phexium\Fake\Domain\Id;
12
13function createSqlSpecStub(string $sql, array $params): SpecificationInterface
14{
15    return new readonly class($sql, $params) implements SpecificationInterface {
16        public function __construct(
17            private string $sql,
18            private array $params
19        ) {}
20
21        public function toSql(): array
22        {
23            return ['sql' => $this->sql, 'params' => $this->params];
24        }
25
26        public function toInMemoryFilter(): callable
27        {
28            $params = $this->params;
29
30            return static fn (array $row): bool => array_all($params, fn ($value, $key): bool => isset($row[$key]) && $row[$key] === $value);
31        }
32    };
33}
34
35function registerBaseSqlDriverTests(): void
36{
37    describe('CRUD operations', function (): void {
38        it('inserts a new row via save', function (): void {
39            $this->driver->save('sample', ['id' => 1, 'name' => 'Test']);
40
41            $result = $this->driver->findAll('sample');
42
43            expect($result)->toHaveCount(1)
44                ->and($result[0]['name'])->toBe('Test')
45            ;
46        });
47
48        it('updates an existing row via save', function (): void {
49            $this->driver->save('sample', ['id' => 1, 'name' => 'Original']);
50            $this->driver->save('sample', ['id' => 1, 'name' => 'Updated']);
51
52            $result = $this->driver->findAll('sample');
53
54            expect($result)->toHaveCount(1)
55                ->and($result[0]['name'])->toBe('Updated')
56            ;
57        });
58
59        it('returns a row by id when it exists', function (): void {
60            $this->driver->save('sample', ['id' => 42, 'name' => 'Found']);
61
62            $id = Id::from(42);
63
64            $result = $this->driver->findById('sample', $id);
65
66            expect($result)->not->toBeNull()
67                ->and($result['name'])->toBe('Found')
68            ;
69        });
70
71        it('returns null when finding by a nonexistent id', function (): void {
72            $id = Id::from(999);
73
74            $result = $this->driver->findById('sample', $id);
75
76            expect($result)->toBeNull();
77        });
78
79        it('removes a row by id and returns the deleted count', function (): void {
80            $this->driver->save('sample', ['id' => 1, 'name' => 'ToDelete']);
81
82            $id = Id::from(1);
83
84            $count = $this->driver->deleteById('sample', $id);
85
86            expect($count)->toBe(1)
87                ->and($this->driver->findAll('sample'))->toBe([])
88            ;
89        });
90
91        it('returns zero when deleting a nonexistent row', function (): void {
92            $id = Id::from(999);
93
94            $count = $this->driver->deleteById('sample', $id);
95
96            expect($count)->toBe(0);
97        });
98
99        it('confirms existence when a row exists', function (): void {
100            $this->driver->save('sample', ['id' => 1, 'name' => 'Exists']);
101
102            $id = Id::from(1);
103
104            expect($this->driver->exists('sample', $id))->toBeTrue();
105        });
106
107        it('denies existence when a row does not exist', function (): void {
108            $id = Id::from(999);
109
110            expect($this->driver->exists('sample', $id))->toBeFalse();
111        });
112    });
113
114    describe('Querying', function (): void {
115        it('returns an empty array when the table is empty', function (): void {
116            $result = $this->driver->findAll('sample');
117
118            expect($result)->toBe([]);
119        });
120
121        it('returns the first matching row via findOneBy', function (): void {
122            $this->driver->save('sample', ['id' => 1, 'name' => 'First']);
123            $this->driver->save('sample', ['id' => 2, 'name' => 'First']);
124
125            $spec = createSqlSpecStub('name = :name', ['name' => 'First']);
126
127            $result = $this->driver->findOneBy('sample', $spec);
128
129            expect($result)->not->toBeNull()
130                ->and($result['name'])->toBe('First')
131            ;
132        });
133
134        it('returns null via findOneBy when no row matches', function (): void {
135            $this->driver->save('sample', ['id' => 1, 'name' => 'NotMatching']);
136
137            $spec = createSqlSpecStub('name = :name', ['name' => 'NoMatch']);
138
139            $result = $this->driver->findOneBy('sample', $spec);
140
141            expect($result)->toBeNull();
142        });
143
144        it('returns all matching rows via findBy', function (): void {
145            $this->driver->save('sample', ['id' => 1, 'name' => 'Match']);
146            $this->driver->save('sample', ['id' => 2, 'name' => 'NoMatch']);
147            $this->driver->save('sample', ['id' => 3, 'name' => 'Match']);
148
149            $spec = createSqlSpecStub('name = :name', ['name' => 'Match']);
150
151            $result = $this->driver->findBy('sample', $spec);
152
153            expect($result)->toHaveCount(2);
154        });
155
156        it('applies a limit via findBy', function (): void {
157            $this->driver->save('sample', ['id' => 1, 'name' => 'A']);
158            $this->driver->save('sample', ['id' => 2, 'name' => 'A']);
159            $this->driver->save('sample', ['id' => 3, 'name' => 'A']);
160
161            $spec = createSqlSpecStub('name = :name', ['name' => 'A']);
162
163            $result = $this->driver->findBy('sample', $spec, null, null, 2);
164
165            expect($result)->toHaveCount(2);
166        });
167
168        it('applies offset and limit via findBy', function (): void {
169            $this->driver->save('sample', ['id' => 1, 'name' => 'A']);
170            $this->driver->save('sample', ['id' => 2, 'name' => 'A']);
171            $this->driver->save('sample', ['id' => 3, 'name' => 'A']);
172
173            $spec = createSqlSpecStub('name = :name', ['name' => 'A']);
174
175            $result = $this->driver->findBy('sample', $spec, null, 1, 2);
176
177            expect($result)->toHaveCount(2);
178        });
179
180        it('returns a zero-indexed array after filtering via findBy', function (): void {
181            $this->driver->save('sample', ['id' => 100, 'name' => 'Keep']);
182            $this->driver->save('sample', ['id' => 200, 'name' => 'Skip']);
183            $this->driver->save('sample', ['id' => 300, 'name' => 'Keep']);
184
185            $spec = createSqlSpecStub('name = :name', ['name' => 'Keep']);
186
187            $result = $this->driver->findBy('sample', $spec);
188
189            expect(array_keys($result))->toBe([0, 1]);
190        });
191    });
192
193    describe('Counting', function (): void {
194        it('returns zero when the table is empty', function (): void {
195            $spec = createSqlSpecStub('1=1', []);
196
197            $result = $this->driver->countBy('sample', $spec);
198
199            expect($result)->toBe(0);
200        });
201
202        it('counts all rows with an always-true specification', function (): void {
203            $this->driver->save('sample', ['id' => 1, 'name' => 'A']);
204            $this->driver->save('sample', ['id' => 2, 'name' => 'B']);
205            $this->driver->save('sample', ['id' => 3, 'name' => 'C']);
206
207            $spec = createSqlSpecStub('1=1', []);
208
209            $result = $this->driver->countBy('sample', $spec);
210
211            expect($result)->toBe(3);
212        });
213
214        it('counts only matching rows with a filtering specification', function (): void {
215            $this->driver->save('sample', ['id' => 1, 'name' => 'Match']);
216            $this->driver->save('sample', ['id' => 2, 'name' => 'NoMatch']);
217            $this->driver->save('sample', ['id' => 3, 'name' => 'Match']);
218
219            $spec = createSqlSpecStub('name = :name', ['name' => 'Match']);
220
221            $result = $this->driver->countBy('sample', $spec);
222
223            expect($result)->toBe(2);
224        });
225    });
226
227    describe('Sorting', function (): void {
228        it('applies sorting via findBy', function (): void {
229            $this->driver->save('sample', ['id' => 1, 'name' => 'Zoo']);
230            $this->driver->save('sample', ['id' => 2, 'name' => 'Alpha']);
231
232            $spec = createSqlSpecStub('1=1', []);
233
234            $result = $this->driver->findBy('sample', $spec, ['name' => 'ASC']);
235
236            expect($result[0]['name'])->toBe('Alpha')
237                ->and($result[1]['name'])->toBe('Zoo')
238            ;
239        });
240
241        it('handles case-insensitive sort direction', function (): void {
242            $this->driver->save('sample', ['id' => 1, 'name' => 'Zoo']);
243            $this->driver->save('sample', ['id' => 2, 'name' => 'Alpha']);
244
245            $spec = createSqlSpecStub('1=1', []);
246
247            $result = $this->driver->findBy('sample', $spec, ['name' => 'asc']);
248
249            expect($result[0]['name'])->toBe('Alpha');
250        });
251    });
252}