packages/sql-catalog/tests/Unit/Extension/Laravel/BuilderCallsTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Extension\Laravel;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlCatalog\Core\Analysis\FunctionScope;
11use SqlCatalog\Core\Analysis\Interpreter;
12use SqlCatalog\Core\Evaluation\ArrayEntry;
13use SqlCatalog\Core\Evaluation\ArrayTerm;
14use SqlCatalog\Core\Evaluation\Domain;
15use SqlCatalog\Core\Evaluation\Environment;
16use SqlCatalog\Core\Evaluation\LiteralTerm;
17use SqlCatalog\Core\Evaluation\ObjectMemory;
18use SqlCatalog\Core\Evaluation\ObjectTerm;
19use SqlCatalog\Core\Evaluation\OpaqueTerm;
20use SqlCatalog\Core\Evaluation\PatternTerm;
21use SqlCatalog\Core\Php\ProgramIndex;
22use SqlCatalog\Core\Text\LiteralText;
23use SqlCatalog\Core\Text\TextGeneralization;
24use SqlCatalog\Core\Text\TextHole;
25use SqlCatalog\Core\Text\TextPattern;
26use SqlCatalog\Core\Type\TypeShape;
27use SqlCatalog\Extension\Laravel\BuilderCalls;
28use SqlCatalog\Extension\Laravel\Clauses;
29use SqlCatalog\Extension\Laravel\Grammar;
30use SqlCatalog\Extension\Laravel\ModelMetadata;
31use SqlCatalog\Extension\Laravel\Predicates;
32use SqlCatalog\Extension\Laravel\QueryState;
33
34#[CoversClass(BuilderCalls::class)]
35#[UsesClass(Domain::class)]
36#[UsesClass(ArrayTerm::class)]
37#[UsesClass(ArrayEntry::class)]
38#[UsesClass(ObjectTerm::class)]
39#[UsesClass(LiteralTerm::class)]
40#[UsesClass(OpaqueTerm::class)]
41#[UsesClass(PatternTerm::class)]
42#[UsesClass(TextPattern::class)]
43#[UsesClass(TextHole::class)]
44#[UsesClass(LiteralText::class)]
45#[UsesClass(TextGeneralization::class)]
46#[UsesClass(TypeShape::class)]
47#[UsesClass(QueryState::class)]
48#[UsesClass(Grammar::class)]
49#[UsesClass(Clauses::class)]
50#[UsesClass(\SqlCatalog\Extension\Laravel\SelectCompiler::class)]
51#[UsesClass(Predicates::class)]
52#[UsesClass(ModelMetadata::class)]
53#[UsesClass(ProgramIndex::class)]
54#[UsesClass(Environment::class)]
55#[UsesClass(ObjectMemory::class)]
56#[UsesClass(Interpreter::class)]
57#[UsesClass(FunctionScope::class)]
58#[UsesClass(\SqlCatalog\Core\Analysis\CallEvaluator::class)]
59#[UsesClass(\SqlCatalog\Core\Analysis\ConstantReader::class)]
60#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\CalleeReturns::class)]
61#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\CallerIndex::class)]
62#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Callers::class)]
63#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Deriver::class)]
64#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\EntryBinder::class)]
65#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\FreeNames::class)]
66#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\ModifiedNames::class)]
67#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\PropertyWrites::class)]
68#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\SliceExecutor::class)]
69#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\AssignmentSteps::class)]
70#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\BackwardSlicer::class)]
71#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\Slice\LoopPasses::class)]
72#[UsesClass(\SqlCatalog\Core\Analysis\Derivation\SourceTree::class)]
73#[UsesClass(\SqlCatalog\Core\Analysis\EvaluationBudget::class)]
74#[UsesClass(\SqlCatalog\Core\Analysis\ExpressionEvaluator::class)]
75#[UsesClass(\SqlCatalog\Core\Analysis\ReferenceEvaluator::class)]
76#[UsesClass(\SqlCatalog\Core\Analysis\SinkFinder::class)]
77#[UsesClass(\SqlCatalog\Core\Analysis\SinkMatcher::class)]
78#[UsesClass(\SqlCatalog\Core\Php\DeclaredGlobals::class)]
79#[UsesClass(\SqlCatalog\Core\Php\NodeText::class)]
80#[UsesClass(\SqlCatalog\Core\Analysis\BuiltinCallModel::class)]
81#[UsesClass(\SqlCatalog\Core\Analysis\FunctionModel\Registry::class)]
82final class BuilderCallsTest extends TestCase
83{
84    public function testIsBuilderRecognizesSourceDeclaredModelsAndBothBuilderContracts(): void
85    {
86        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
87        self::assertTrue($calls->isBuilder(BuilderCalls::QUERY));
88        self::assertTrue($calls->isBuilder(ModelMetadata::BUILDER));
89        self::assertTrue($calls->isBuilder(ModelMetadata::MODEL));
90        self::assertFalse($calls->isBuilder('Collection'));
91    }
92
93    public function testStaticCallCreatesFacadeQueriesAndDoesNotModelUnrelatedStatics(): void
94    {
95        $calls = new BuilderCalls(new ProgramIndex(), 'sqlite', dialects: \SqlCatalog\Facade\Builtins::dialects());
96        $env = new Environment();
97        $value = $calls->staticCall(BuilderCalls::FACADE, 'table', [Domain::literal('users')], $env);
98        self::assertNotNull($value?->soleObject());
99        self::assertNull($calls->staticCall('Other', 'table', [], $env));
100        $query = $calls->staticCall(ModelMetadata::MODEL, 'query', [], $env)?->soleObject();
101        self::assertSame(ModelMetadata::BUILDER, $query?->className);
102    }
103
104    public function testMethodCallMutatesTheExistingIdentityAndLeavesOtherClassesAlone(): void
105    {
106        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
107        $env = new Environment();
108        $query = $calls->allocate(BuilderCalls::QUERY, new QueryState(['dialect' => Domain::literal('sqlite')]));
109        $result = $calls->methodCall(Domain::of($query), 'where', [Domain::literal('id'), Domain::literal(1)], $env)?->soleObject();
110        self::assertSame($query->identity, $result?->identity);
111        self::assertNotNull($result);
112        self::assertSame('"id" = ?', QueryState::from($result)->items('where')[0]->soleLiteral()?->value);
113        self::assertNull($calls->methodCall(Domain::of(new ObjectTerm('Collection')), 'where', [], $env));
114        $connection = Domain::of(new ObjectTerm('Illuminate\Database\SQLiteConnection'));
115        self::assertSame(BuilderCalls::QUERY, $calls->methodCall($connection, 'table', [Domain::literal('users')], $env)?->soleObject()?->className);
116    }
117
118    public function testCallbackReturnsNullWithoutARegisteredRunner(): void
119    {
120        $index = new ProgramIndex();
121        $calls = new BuilderCalls($index, dialects: \SqlCatalog\Facade\Builtins::dialects());
122        self::assertNull($calls->callback(new \PhpParser\Node\Expr\MethodCall(new \PhpParser\Node\Expr\Variable('q'), 'where'), new ObjectTerm(BuilderCalls::QUERY), 'where', [], new Environment(), new FunctionScope('test.php'), (new Interpreter($index, []))->evaluatorFor()));
123    }
124
125    public function testIsConnectionRecognizesConcreteGrammarContracts(): void
126    {
127        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
128        self::assertTrue($calls->isConnection('Illuminate\Database\MySqlConnection'));
129        self::assertTrue($calls->isConnection(BuilderCalls::CONNECTION));
130        self::assertFalse($calls->isConnection('Unknown'));
131    }
132
133    public function testConnectionDialectUsesConcreteConnectionsBeforeTheFallback(): void
134    {
135        $calls = new BuilderCalls(new ProgramIndex(), 'mysql', dialects: \SqlCatalog\Facade\Builtins::dialects());
136        self::assertSame('sqlite', $calls->connectionDialect('Illuminate\Database\SQLiteConnection'));
137        self::assertSame('pgsql', $calls->connectionDialect('Illuminate\Database\PostgresConnection'));
138        self::assertSame('mysql', $calls->connectionDialect(BuilderCalls::CONNECTION));
139        self::assertNull((new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects()))->connectionDialect(null));
140    }
141
142    public function testConnectionCallAllocatesIndependentQueriesAndExplicitRawExpressions(): void
143    {
144        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
145        $state = new QueryState(['dialect' => Domain::literal('pgsql')]);
146        $env = new Environment();
147        $a = $calls->connectionCall('table', [Domain::literal('users')], $state, $env)?->soleObject();
148        $b = $calls->connectionCall('table', [Domain::literal('users')], $state, $env)?->soleObject();
149        self::assertNotSame($a?->identity, $b?->identity);
150        self::assertSame('Illuminate\Database\Query\Expression', $calls->connectionCall('raw', [Domain::literal('now()')], $state, $env)?->soleObject()?->className);
151        self::assertSame(BuilderCalls::CONNECTION, $calls->connectionCall('connection', [], $state, $env)?->soleObject()?->className);
152        self::assertNull($calls->connectionCall('table', [], $state, $env));
153    }
154
155    public function testAllocateNeverReusesAnIdentityForTheSameState(): void
156    {
157        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
158        self::assertNotSame($calls->allocate(BuilderCalls::QUERY, new QueryState())->identity, $calls->allocate(BuilderCalls::QUERY, new QueryState())->identity);
159    }
160
161    public function testMutateRetainsEarlierUnknownEffects(): void
162    {
163        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
164        $object = $calls->allocate(BuilderCalls::QUERY, new QueryState(['dialect' => Domain::literal('sqlite')]));
165        $env = new Environment();
166        $unknown = $calls->mutate($object, 'macro', [], $env)->soleObject();
167        self::assertNotNull($unknown);
168        $updated = $calls->mutate($unknown, 'limit', [Domain::literal(3)], $env)->soleObject();
169        self::assertNotNull($updated);
170        self::assertFalse(QueryState::from($updated)->get('problem')->isExact());
171        self::assertSame(3, QueryState::from($updated)->get('limit')->soleLiteral()?->value);
172    }
173
174    public function testPositionalRejectsNamedAndUnpackedArguments(): void
175    {
176        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
177        $value = new \PhpParser\Node\Scalar\String_('id');
178        self::assertTrue($calls->positional([new \PhpParser\Node\Arg($value)]));
179        self::assertFalse($calls->positional([new \PhpParser\Node\Arg($value, name: new \PhpParser\Node\Identifier('column'))]));
180        self::assertFalse($calls->positional([new \PhpParser\Node\Arg($value, unpack: true)]));
181    }
182
183    public function testUnsupportedInvalidatesAliasesWithoutLosingTheirIdentity(): void
184    {
185        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
186        $object = $calls->allocate(BuilderCalls::QUERY, new QueryState());
187        $env = new Environment(['alias' => Domain::of($object)]);
188        $calls->unsupported(Domain::of($object), $env, 'unknown mutation');
189        $read = $env->read('alias')->soleObject();
190        self::assertNotNull($read);
191        self::assertSame($object->identity, $read->identity);
192        self::assertFalse(QueryState::from($read)->get('problem')->isExact());
193    }
194
195    public function testExecutionPreservesFirstLimitAndReturnsACollectionForGet(): void
196    {
197        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
198        $object = $calls->allocate(BuilderCalls::QUERY, new QueryState());
199        $env = new Environment(['q' => Domain::of($object)]);
200        $calls->execution(Domain::of($object), 'first', [], $env);
201        $after = $env->read('q')->soleObject();
202        self::assertNotNull($after);
203        self::assertSame(1, QueryState::from($after)->get('limit')->soleLiteral()?->value);
204        self::assertSame('Illuminate\Support\Collection', $calls->execution(Domain::of($after), 'get', [], $env)->type()->soleClassName());
205    }
206
207    #[\PHPUnit\Framework\Attributes\DataProvider('providerExecutionTypes')]
208    public function testExecutionReturnsTheFrameworkTypeWithoutInventingAnotherQuery(string $method, string $type): void
209    {
210        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
211        $object = $calls->allocate(BuilderCalls::QUERY, new QueryState(['key' => Domain::literal('id'), 'dialect' => Domain::literal('sqlite')]));
212        $result = $calls->execution(Domain::of($object), $method, [Domain::literal(7)], new Environment());
213        self::assertSame([$type], $result->type()->names);
214    }
215
216    /**
217     * @return iterable<array{string, string}>
218     */
219    public static function providerExecutionTypes(): iterable
220    {
221        foreach (['get', 'all', 'pluck'] as $method) {
222            yield [$method, 'Illuminate\Support\Collection'];
223        }
224        foreach (['first', 'firstorfail', 'find'] as $method) {
225            yield [$method, 'stdClass'];
226        }
227        foreach (['exists', 'doesntexist', 'insert', 'insertorignore'] as $method) {
228            yield [$method, 'bool'];
229        }
230        foreach (['count', 'update', 'delete', 'insertgetid'] as $method) {
231            yield [$method, 'int'];
232        }
233    }
234
235    public function testExecutionFindRetainsItsPredicateAndTheModelReturnType(): void
236    {
237        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
238        $object = $calls->allocate(ModelMetadata::BUILDER, new QueryState(['key' => Domain::literal('users.id'), 'model' => Domain::literal('App\User'), 'dialect' => Domain::literal('sqlite')]));
239        $env = new Environment(['q' => Domain::of($object)]);
240        $result = $calls->execution(Domain::of($object), 'find', [Domain::literal(7)], $env);
241        self::assertSame('App\User', $result->type()->soleClassName());
242        $after = $env->read('q')->soleObject();
243        self::assertNotNull($after);
244        self::assertSame('"users"."id" = ?', QueryState::from($after)->items('where')[0]->soleLiteral()?->value);
245        self::assertSame(7, QueryState::from($after)->items('whereBindings')[0]->soleLiteral()?->value);
246    }
247
248    public function testMutateOnlyChangesSoftDeleteModesForModelsUsingTheTrait(): void
249    {
250        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
251        $object = $calls->allocate(ModelMetadata::BUILDER, new QueryState(['softDeletes' => Domain::literal(true)]));
252        $env = new Environment();
253        $with = $calls->mutate($object, 'withtrashed', [], $env)->soleObject();
254        $only = $calls->mutate($object, 'onlytrashed', [], $env)->soleObject();
255        self::assertNotNull($with);
256        self::assertNotNull($only);
257        self::assertSame('withtrashed', QueryState::from($with)->string('trashed'));
258        self::assertSame([], QueryState::from($with)->items('where'));
259        self::assertSame('withtrashed', QueryState::from($only)->string('trashed'));
260        self::assertFalse(QueryState::from($only)->items('where')[0]->isExact());
261        $plain = $calls->allocate(ModelMetadata::BUILDER, new QueryState());
262        $unsupported = $calls->mutate($plain, 'withtrashed', [], $env)->soleObject();
263        self::assertNotNull($unsupported);
264        self::assertFalse(QueryState::from($unsupported)->get('problem')->isExact());
265    }
266
267    public function testConnectionCallPreservesRawSqlAndAnExplicitConnectionName(): void
268    {
269        $calls = new BuilderCalls(new ProgramIndex(), 'mysql', dialects: \SqlCatalog\Facade\Builtins::dialects());
270        $env = new Environment();
271        $raw = $calls->connectionCall('raw', [Domain::literal('count(*)')], new QueryState(), $env)?->soleObject();
272        self::assertNotNull($raw);
273        self::assertSame('count(*)', QueryState::from($raw)->string('sql'));
274        self::assertNotNull($calls->connectionCall('connection', [Domain::literal('reporting')], new QueryState(), $env)?->soleObject());
275        self::assertSame('mysql', $calls->connectionDialect('Illuminate\Database\MySqlConnection'));
276    }
277
278    public function testMutateNamesTheUnmodelledOperationInItsDiagnostic(): void
279    {
280        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
281        $object = $calls->allocate(BuilderCalls::QUERY, new QueryState());
282        $updated = $calls->mutate($object, 'customFilter', [], new Environment())->soleObject();
283        self::assertNotNull($updated);
284        $problem = QueryState::from($updated)->get('problem')->terms[0];
285        self::assertInstanceOf(OpaqueTerm::class, $problem);
286        self::assertSame('Unmodelled Laravel effect: customFilter', $problem->expression);
287        $result = $calls->execution(Domain::of($object), 'get', [], new Environment())->terms[0];
288        self::assertInstanceOf(OpaqueTerm::class, $result);
289        self::assertSame('Laravel get', $result->expression);
290    }
291
292
293    public function testBoundedMarksTooManyAlternativesAsOneOpenStateInsteadOfLosingEffects(): void
294    {
295        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
296        $terms = array_map(static fn (int $i): ObjectTerm => new ObjectTerm(BuilderCalls::QUERY, identity: 'a', state: (new QueryState(['limit' => Domain::literal($i)]))->array()), range(0, BuilderCalls::MAX_ALTERNATIVES));
297        $bounded = $calls->bounded($terms, Domain::unknown());
298        self::assertTrue($bounded->widened);
299        self::assertNotNull($bounded->soleObject());
300        self::assertFalse(QueryState::from($bounded->soleObject())->get('problem')->isExact());
301        $kept = $calls->bounded(array_slice($terms, 0, BuilderCalls::MAX_ALTERNATIVES), Domain::unknown());
302        self::assertCount(BuilderCalls::MAX_ALTERNATIVES, $kept->terms);
303        self::assertFalse($kept->widened);
304        self::assertCount(1, $calls->bounded(array_fill(0, BuilderCalls::MAX_ALTERNATIVES + 1, new LiteralTerm(1)), Domain::unknown())->terms);
305    }
306
307    public function testIsConnectionRecognizesInjectedConnectionContracts(): void
308    {
309        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
310        self::assertTrue($calls->isConnection('Illuminate\\Database\\ConnectionInterface'));
311        self::assertTrue($calls->isConnection('Illuminate\\Database\\DatabaseManager'));
312        self::assertTrue($calls->isConnection('Illuminate\\Database\\ConnectionResolverInterface'));
313        self::assertFalse($calls->isConnection('Illuminate\\Support\\Collection'));
314    }
315
316    public function testConnectionCallAliasesTheTableAndKeepsUnknownTablesOpen(): void
317    {
318        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
319        $state = new QueryState(['dialect' => Domain::literal('sqlite')]);
320        $aliased = $calls->connectionCall('table', [Domain::literal('users'), Domain::literal('u')], $state, new Environment())?->soleObject();
321        self::assertNotNull($aliased);
322        self::assertSame('users as u', QueryState::from($aliased)->get('table')->soleLiteral()?->value);
323        $sub = $calls->connectionCall('table', [Domain::of(new ObjectTerm(BuilderCalls::QUERY))], $state, new Environment())?->soleObject();
324        self::assertNotNull($sub);
325        self::assertFalse(QueryState::from($sub)->get('problem')->isExact());
326    }
327
328    public function testMutateTreatsIdentityCallsAndEagerLoadsWithoutOpeningTheQuery(): void
329    {
330        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
331        $env = new Environment();
332        $query = $calls->allocate(BuilderCalls::QUERY, new QueryState(['dialect' => Domain::literal('sqlite'), 'model' => Domain::literal('User'), 'softDeletes' => Domain::literal(true)]));
333        $identities = array_map(static fn (string $method): array => QueryState::from($calls->mutate($query, $method, [], $env)->soleObject() ?? $query)->fields, ['tobase', 'newquery', 'getquery']);
334        self::assertSame([$query->state?->named(), $query->state?->named(), $query->state?->named()], $identities);
335        $eager = $calls->mutate($query, 'with', [Domain::literal('posts')], $env)->soleObject();
336        self::assertNotNull($eager);
337        self::assertTrue(QueryState::from($eager)->get('eager')->soleLiteral()?->value);
338        $plain = $calls->mutate($calls->allocate(BuilderCalls::QUERY, new QueryState(['dialect' => Domain::literal('sqlite')])), 'with', [Domain::literal('posts')], $env)->soleObject();
339        self::assertNotNull($plain);
340        self::assertFalse(QueryState::from($plain)->get('problem')->isExact());
341        $posts = $calls->allocate(BuilderCalls::QUERY, new QueryState(['dialect' => Domain::literal('sqlite'), 'model' => Domain::literal('Post'), 'table' => Domain::literal('posts'), 'softDeletes' => Domain::literal(true), 'deletedColumn' => Domain::literal('deleted_at')]));
342        $restored = $calls->mutate($calls->mutate($posts, 'onlytrashed', [], $env)->soleObject() ?? $posts, 'withouttrashed', [], $env)->soleObject();
343        self::assertNotNull($restored);
344        self::assertSame('withtrashed', QueryState::from($restored)->string('trashed'));
345        self::assertSame(['"posts"."deleted_at" is not null', 'and "posts"."deleted_at" is null'], array_map(static fn (Domain $v): mixed => $v->soleLiteral()?->value, QueryState::from($restored)->items('where')));
346    }
347
348    public function testExecutionRetainsTheWindowOfSingleRowReadsAndTheirReturnTypes(): void
349    {
350        $calls = new BuilderCalls(new ProgramIndex(), dialects: \SqlCatalog\Facade\Builtins::dialects());
351        $env = new Environment();
352        $query = $calls->allocate(BuilderCalls::QUERY, new QueryState(['dialect' => Domain::literal('sqlite'), 'key' => Domain::literal('id')]));
353        $env->objects()->remember($query);
354        self::assertSame('mixed', $calls->execution(Domain::of($query), 'value', [Domain::literal('email')], $env)->type()->display());
355        self::assertSame(1, QueryState::from($env->objects()->read(Domain::of($query))->soleObject() ?? $query)->get('limit')->soleLiteral()?->value);
356        $calls->execution(Domain::of($query), 'sole', [], $env);
357        self::assertSame(2, QueryState::from($env->objects()->read(Domain::of($query))->soleObject() ?? $query)->get('limit')->soleLiteral()->value);
358        self::assertSame('stdClass', $calls->execution(Domain::of($query), 'findorfail', [Domain::literal(1)], $env)->type()->display());
359        self::assertSame('"id" = ?', QueryState::from($env->objects()->read(Domain::of($query))->soleObject() ?? $query)->items('where')[0]->soleLiteral()?->value);
360        self::assertSame('Illuminate\\Pagination\\LengthAwarePaginator', $calls->execution(Domain::of($query), 'paginate', [], $env)->type()->display());
361        self::assertArrayNotHasKey('problem', QueryState::from($env->objects()->read(Domain::of($query))->soleObject() ?? $query)->fields);
362    }
363}
364