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