packages/ztd-query-core/tests/Unit/QueryExecutorTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use Tests\Fake\FakeConnection;
11use Tests\Fake\FakeSqlRewriter;
12use Tests\Fake\FakeStatement;
13use ZtdQuery\Config\ZtdConfig;
14use ZtdQuery\Connection\Exception\DatabaseException;
15use ZtdQuery\Connection\ResultSet;
16use ZtdQuery\Exception\ForeignKeyViolationException;
17use ZtdQuery\Exception\MissingPrimaryKeyException;
18use ZtdQuery\Platform\CopySupport;
19use ZtdQuery\Platform\CopyTarget;
20use ZtdQuery\Platform\MissingResultColumnTypeResolver;
21use ZtdQuery\Platform\ParameterBindingCompiler;
22use ZtdQuery\Platform\ResultColumnTypeResolver;
23use ZtdQuery\QueryExecutor;
24use ZtdQuery\ResultSelectRunner;
25use ZtdQuery\Rewrite\QueryKind;
26use ZtdQuery\Rewrite\RewritePlan;
27use ZtdQuery\RewriteRefusal;
28use ZtdQuery\Schema\Key\CandidateKeySet;
29use ZtdQuery\Schema\Key\ForeignKeyDefinition;
30use ZtdQuery\Schema\TableDefinition;
31use ZtdQuery\Schema\TableDefinitionRegistry;
32use ZtdQuery\Shadow\Mutation\MutationRowIdentity;
33use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
34use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
35use ZtdQuery\Shadow\ReferentialIntegrityEnforcer;
36use ZtdQuery\Shadow\Row\RowPairing;
37use ZtdQuery\Shadow\ShadowStore;
38use ZtdQuery\Shadow\ShadowTransactions;
39use ZtdQuery\Sql\TransactionStatement;
40
41#[CoversClass(QueryExecutor::class)]
42#[UsesClass(\ZtdQuery\Schema\ColumnDeclaration::class)]
43#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeyMatch::class)]
44#[UsesClass(ZtdConfig::class)]
45#[UsesClass(\ZtdQuery\Session::class)]
46#[UsesClass(\ZtdQuery\Schema\ViewDefinitionSet::class)]
47#[UsesClass(ShadowStore::class)]
48#[UsesClass(ShadowTransactions::class)]
49#[UsesClass(TableDefinitionRegistry::class)]
50#[UsesClass(TableDefinition::class)]
51#[UsesClass(CandidateKeySet::class)]
52#[UsesClass(ResultSelectRunner::class)]
53#[UsesClass(ResultSet::class)]
54#[UsesClass(DatabaseException::class)]
55#[UsesClass(RewritePlan::class)]
56#[UsesClass(UpdateMutation::class)]
57#[UsesClass(InsertMutation::class)]
58#[UsesClass(MutationRowIdentity::class)]
59#[UsesClass(ForeignKeyDefinition::class)]
60#[UsesClass(ForeignKeyViolationException::class)]
61#[UsesClass(ReferentialIntegrityEnforcer::class)]
62#[UsesClass(MissingPrimaryKeyException::class)]
63#[UsesClass(CopyTarget::class)]
64#[UsesClass(MissingResultColumnTypeResolver::class)]
65#[UsesClass(TransactionStatement::class)]
66#[UsesClass(\ZtdQuery\Shadow\ShadowApplication::class)]
67#[UsesClass(\ZtdQuery\Shadow\Mutation\MutationImpact::class)]
68#[UsesClass(\ZtdQuery\GenericExecuteResult::class)]
69#[UsesClass(\ZtdQuery\Shadow\ShadowSavepoint::class)]
70#[UsesClass(\ZtdQuery\Shadow\Mutation\RowConstraints::class)]
71#[UsesClass(\ZtdQuery\Shadow\Mutation\ConflictSearch::class)]
72#[UsesClass(\ZtdQuery\Shadow\ForeignKeyCascade::class)]
73#[UsesClass(\ZtdQuery\Shadow\ForeignKeyEnds::class)]
74#[UsesClass(\ZtdQuery\Shadow\ForeignKeyIntegrity::class)]
75#[UsesClass(\ZtdQuery\Shadow\ParentKeyLookup::class)]
76#[UsesClass(\ZtdQuery\Shadow\Row\RowMatch::class)]
77#[UsesClass(\ZtdQuery\Shadow\Row\RowMultiset::class)]
78#[UsesClass(\ZtdQuery\Shadow\Row\TableTransition::class)]
79#[UsesClass(\ZtdQuery\Shadow\TableTransitions::class)]
80#[UsesClass(RewriteRefusal::class)]
81#[UsesClass(RowPairing::class)]
82#[UsesClass(\ZtdQuery\Schema\RowSet::class)]
83final class QueryExecutorTest extends TestCase
84{
85    public function testDisableEnableDisableEnableAndDisable(): void
86    {
87        $shadowStore = new ShadowStore();
88        $registry = new TableDefinitionRegistry();
89        $rewriter = new FakeSqlRewriter($shadowStore, $registry);
90        $connection = new FakeConnection();
91        $executor = \Tests\Fake\QueryExecutorBuilder::create(
92            $rewriter,
93            $shadowStore,
94            new ResultSelectRunner(),
95            ZtdConfig::default(),
96            $connection,
97        );
98
99        self::assertTrue($executor->session()->isEnabled());
100        self::assertNull($executor->session()->tableDefinition('users'));
101        self::assertNull($executor->platform()->copySupport());
102        self::assertNull($executor->copyTarget('users', null));
103        self::assertNull($executor->platform()->parameterBindingCompiler());
104        self::assertInstanceOf(MissingResultColumnTypeResolver::class, $executor->platform()->resultColumnTypeResolver());
105
106        $executor->session()->disable();
107        self::assertFalse($executor->session()->isEnabled());
108
109        $executor->session()->enable();
110        self::assertTrue($executor->session()->isEnabled());
111    }
112
113    public function testTableDefinitionReturnsRegisteredSchemaOrNull(): void
114    {
115        $shadowStore = new ShadowStore();
116        $registry = new TableDefinitionRegistry();
117        $definition = new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], ['id'], []);
118        $registry->register('users', $definition);
119        $executor = \Tests\Fake\QueryExecutorBuilder::create(
120            new FakeSqlRewriter($shadowStore, $registry),
121            $shadowStore,
122            new ResultSelectRunner(),
123            ZtdConfig::default(),
124            new FakeConnection(),
125            registry: $registry,
126        );
127
128        self::assertSame($definition, $executor->session()->tableDefinition('users'));
129        self::assertNull($executor->session()->tableDefinition('missing'));
130    }
131
132    public function testParameterBindingCompilerResultColumnTypeResolverParameterBindingCompilerDelegatesCopyTargetsToTheConfiguredPlatformSupport(): void
133    {
134        $shadowStore = new ShadowStore();
135        $registry = new TableDefinitionRegistry();
136        $definition = new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], ['id'], []);
137        $registry->register('users', $definition);
138        $target = new CopyTarget(['public', 'users'], ['id']);
139        $copy = self::createStub(CopySupport::class);
140        $copy->method('tableName')->willReturnMap([
141            ['public.users', 'users'],
142            ['missing', 'missing'],
143        ]);
144        $copy->method('target')->willReturn($target);
145        $compiler = self::createStub(ParameterBindingCompiler::class);
146        $typeResolver = self::createStub(ResultColumnTypeResolver::class);
147        $executor = \Tests\Fake\QueryExecutorBuilder::create(
148            new FakeSqlRewriter($shadowStore, $registry),
149            $shadowStore,
150            new ResultSelectRunner(),
151            ZtdConfig::default(),
152            new FakeConnection(),
153            registry: $registry,
154            copySupport: $copy,
155            parameterBindingCompiler: $compiler,
156            resultColumnTypeResolver: $typeResolver,
157        );
158
159        self::assertSame($copy, $executor->platform()->copySupport());
160        self::assertSame($target, $executor->copyTarget('public.users', 'id'));
161        self::assertNull($executor->copyTarget('missing', null));
162        self::assertSame($compiler, $executor->platform()->parameterBindingCompiler());
163        self::assertSame($typeResolver, $executor->platform()->resultColumnTypeResolver());
164    }
165
166    public function testSplitStatementsUsesPlatformRewriter(): void
167    {
168        $shadowStore = new ShadowStore();
169        $rewriter = new FakeSqlRewriter($shadowStore, new TableDefinitionRegistry());
170        $executor = \Tests\Fake\QueryExecutorBuilder::create(
171            $rewriter,
172            $shadowStore,
173            new ResultSelectRunner(),
174            ZtdConfig::default(),
175            new FakeConnection(),
176        );
177
178        self::assertSame(
179            ['SELECT 1', 'SELECT 2'],
180            $executor->splitStatements(' SELECT 1; SELECT 2 '),
181        );
182    }
183
184    public function testSessionTransactionsRestoreTheSameCatalogUsedByTheRewriter(): void
185    {
186        $shadowStore = new ShadowStore();
187        $registry = new TableDefinitionRegistry();
188        $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], [], []);
189        $registry->register('users', $definition);
190        $executor = \Tests\Fake\QueryExecutorBuilder::create(
191            new FakeSqlRewriter($shadowStore, $registry),
192            $shadowStore,
193            new ResultSelectRunner(),
194            ZtdConfig::default(),
195            new FakeConnection(),
196            registry: $registry,
197        );
198
199        $executor->session()->transactions()->begin();
200        $registry->unregister('users');
201        $executor->session()->transactions()->rollBack();
202
203        self::assertSame($definition, $registry->get('users'));
204    }
205
206    public function testMutationFailureIsConvertedToDatabaseException(): void
207    {
208        $shadowStore = new ShadowStore();
209        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
210        $registry = new TableDefinitionRegistry();
211        $executor = \Tests\Fake\QueryExecutorBuilder::create(
212            new FakeSqlRewriter($shadowStore, $registry),
213            $shadowStore,
214            new ResultSelectRunner(),
215            ZtdConfig::default(),
216            new FakeConnection(),
217        );
218        $plan = new RewritePlan(
219            "SELECT 1 AS id, 'Bob' AS name",
220            QueryKind::WRITE_SIMULATED,
221            new UpdateMutation('users', []),
222        );
223
224        try {
225            $executor->processExecutedStatement($plan, new FakeStatement([['id' => 1, 'name' => 'Bob']]));
226            self::fail('Expected a database exception.');
227        } catch (DatabaseException $exception) {
228            self::assertSame(0, $exception->getCode());
229            self::assertInstanceOf(MissingPrimaryKeyException::class, $exception->getPrevious());
230        }
231    }
232
233    public function testForeignKeyFailureRestoresShadowState(): void
234    {
235        $shadowStore = new ShadowStore();
236        $shadowStore->set('parents', []);
237        $shadowStore->set('children', []);
238        $registry = new TableDefinitionRegistry();
239        $registry->register('parents', new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []));
240        $registry->register('children', new TableDefinition(
241            ['id', 'parent_id'],
242            ['id' => 'INT', 'parent_id' => 'INT'],
243            ['id'],
244            ['id'],
245            [],
246            foreignKeys: ['fk_parent' => new ForeignKeyDefinition(['parent_id'], 'parents', ['id'])],
247        ));
248        $executor = \Tests\Fake\QueryExecutorBuilder::create(
249            new FakeSqlRewriter($shadowStore, $registry),
250            $shadowStore,
251            new ResultSelectRunner(),
252            ZtdConfig::default(),
253            new FakeConnection(),
254            registry: $registry,
255        );
256        $plan = new RewritePlan(
257            'SELECT 1 AS id, 999 AS parent_id',
258            QueryKind::WRITE_SIMULATED,
259            new InsertMutation('children'),
260        );
261
262        try {
263            $executor->processExecutedStatement($plan, new FakeStatement([['id' => 1, 'parent_id' => 999]]));
264            self::fail('Expected a database exception.');
265        } catch (DatabaseException $exception) {
266            self::assertInstanceOf(ForeignKeyViolationException::class, $exception->getPrevious());
267            self::assertSame([], $shadowStore->get('children'));
268        }
269    }
270
271    public function testIsEnabledFollowsWhatWasAskedFor(): void
272    {
273        $store = new ShadowStore();
274        $registry = new TableDefinitionRegistry();
275        $executor = \Tests\Fake\QueryExecutorBuilder::create(
276            new FakeSqlRewriter($store, $registry),
277            $store,
278            new ResultSelectRunner(),
279            ZtdConfig::default(),
280            new FakeConnection(),
281            registry: $registry,
282        );
283
284        self::assertTrue($executor->session()->isEnabled());
285
286        $executor->session()->disable();
287
288        self::assertFalse($executor->session()->isEnabled());
289    }
290
291    public function testDisableStopsZtdWithoutTouchingTheShadow(): void
292    {
293        $store = new ShadowStore();
294        $store->set('users', [['id' => 1]]);
295        $registry = new TableDefinitionRegistry();
296        $executor = \Tests\Fake\QueryExecutorBuilder::create(
297            new FakeSqlRewriter($store, $registry),
298            $store,
299            new ResultSelectRunner(),
300            ZtdConfig::default(),
301            new FakeConnection(),
302            registry: $registry,
303        );
304
305        $executor->session()->disable();
306
307        self::assertSame([['id' => 1]], $store->get('users'));
308    }
309
310    public function testShouldExecuteIsFalseOnlyForAPlanNothingIsToBeRunFor(): void
311    {
312        $store = new ShadowStore();
313        $registry = new TableDefinitionRegistry();
314        $executor = \Tests\Fake\QueryExecutorBuilder::create(
315            new FakeSqlRewriter($store, $registry),
316            $store,
317            new ResultSelectRunner(),
318            ZtdConfig::default(),
319            new FakeConnection(),
320            registry: $registry,
321        );
322
323        self::assertTrue($executor->shouldExecute(new RewritePlan('SELECT 1', QueryKind::READ)));
324        self::assertFalse($executor->shouldExecute(new RewritePlan('SELECT 1', QueryKind::SKIPPED)));
325    }
326
327    public function testNeedsPostProcessingIsTrueForTheKindsThatChangeTheShadow(): void
328    {
329        $store = new ShadowStore();
330        $registry = new TableDefinitionRegistry();
331        $executor = \Tests\Fake\QueryExecutorBuilder::create(
332            new FakeSqlRewriter($store, $registry),
333            $store,
334            new ResultSelectRunner(),
335            ZtdConfig::default(),
336            new FakeConnection(),
337            registry: $registry,
338        );
339
340        self::assertTrue($executor->needsPostProcessing(new RewritePlan('x', QueryKind::WRITE_SIMULATED)));
341        self::assertTrue($executor->needsPostProcessing(new RewritePlan('x', QueryKind::DDL_SIMULATED)));
342        self::assertFalse($executor->needsPostProcessing(new RewritePlan('x', QueryKind::READ)));
343    }
344
345    public function testCreateEmptyWriteResultAnswersASimulatedWriteWithNothingToFetch(): void
346    {
347        $store = new ShadowStore();
348        $registry = new TableDefinitionRegistry();
349        $executor = \Tests\Fake\QueryExecutorBuilder::create(
350            new FakeSqlRewriter($store, $registry),
351            $store,
352            new ResultSelectRunner(),
353            ZtdConfig::default(),
354            new FakeConnection(),
355            registry: $registry,
356        );
357
358        $result = $executor->createEmptyWriteResult();
359
360        self::assertSame(QueryKind::WRITE_SIMULATED, $result->kind());
361        self::assertSame([], $result->fetchAll());
362    }
363
364    public function testLastInsertIdIsFalseUntilSomethingHasBeenInserted(): void
365    {
366        $store = new ShadowStore();
367        $registry = new TableDefinitionRegistry();
368        $executor = \Tests\Fake\QueryExecutorBuilder::create(
369            new FakeSqlRewriter($store, $registry),
370            $store,
371            new ResultSelectRunner(),
372            ZtdConfig::default(),
373            new FakeConnection(),
374            registry: $registry,
375        );
376
377        self::assertFalse($executor->session()->lastInsertId());
378    }
379
380    public function testTransactionStatementIsNothingForAStatementThatIsNotOne(): void
381    {
382        $store = new ShadowStore();
383        $registry = new TableDefinitionRegistry();
384        $executor = \Tests\Fake\QueryExecutorBuilder::create(
385            new FakeSqlRewriter($store, $registry),
386            $store,
387            new ResultSelectRunner(),
388            ZtdConfig::default(),
389            new FakeConnection(),
390            registry: $registry,
391        );
392
393        self::assertNull($executor->transactionStatement('SELECT 1'));
394    }
395
396    public function testCommitTransactionKeepsWhatTheTransactionDid(): void
397    {
398        $store = new ShadowStore();
399        $store->set('users', [['id' => 1]]);
400        $registry = new TableDefinitionRegistry();
401        $executor = \Tests\Fake\QueryExecutorBuilder::create(
402            new FakeSqlRewriter($store, $registry),
403            $store,
404            new ResultSelectRunner(),
405            ZtdConfig::default(),
406            new FakeConnection(),
407            registry: $registry,
408        );
409
410        $executor->session()->transactions()->begin();
411        $store->set('users', []);
412        $executor->session()->transactions()->commit();
413        $executor->session()->transactions()->rollBack();
414
415        self::assertSame([], $store->get('users'));
416    }
417
418    public function testApplyTransactionStatementDoesWhatTheStatementSays(): void
419    {
420        $store = new ShadowStore();
421        $store->set('users', [['id' => 1]]);
422        $registry = new TableDefinitionRegistry();
423        $executor = \Tests\Fake\QueryExecutorBuilder::create(
424            new FakeSqlRewriter($store, $registry),
425            $store,
426            new ResultSelectRunner(),
427            ZtdConfig::default(),
428            new FakeConnection(),
429            registry: $registry,
430        );
431
432        TransactionStatement::begin()->apply($executor->session()->transactions());
433        $store->set('users', []);
434        TransactionStatement::rollback()->apply($executor->session()->transactions());
435
436        self::assertSame([['id' => 1]], $store->get('users'));
437    }
438
439    public function testCopySupportIsNothingWhereTheDialectHasNoCopy(): void
440    {
441        $store = new ShadowStore();
442        $registry = new TableDefinitionRegistry();
443        $executor = \Tests\Fake\QueryExecutorBuilder::create(
444            new FakeSqlRewriter($store, $registry),
445            $store,
446            new ResultSelectRunner(),
447            ZtdConfig::default(),
448            new FakeConnection(),
449            registry: $registry,
450        );
451
452        self::assertNull($executor->platform()->copySupport());
453    }
454
455    public function testCopyTargetIsNothingWhereTheDialectHasNoCopy(): void
456    {
457        $store = new ShadowStore();
458        $registry = new TableDefinitionRegistry();
459        $executor = \Tests\Fake\QueryExecutorBuilder::create(
460            new FakeSqlRewriter($store, $registry),
461            $store,
462            new ResultSelectRunner(),
463            ZtdConfig::default(),
464            new FakeConnection(),
465            registry: $registry,
466        );
467
468        self::assertNull($executor->copyTarget('users', null));
469    }
470
471    public function testParameterBindingCompilerIsNothingWhereTheDriverBindsThemItself(): void
472    {
473        $store = new ShadowStore();
474        $registry = new TableDefinitionRegistry();
475        $executor = \Tests\Fake\QueryExecutorBuilder::create(
476            new FakeSqlRewriter($store, $registry),
477            $store,
478            new ResultSelectRunner(),
479            ZtdConfig::default(),
480            new FakeConnection(),
481            registry: $registry,
482        );
483
484        self::assertNull($executor->platform()->parameterBindingCompiler());
485    }
486
487    public function testRewriteAnswersThePlanTheRewriterGives(): void
488    {
489        $store = new ShadowStore();
490        $registry = new TableDefinitionRegistry();
491        $executor = \Tests\Fake\QueryExecutorBuilder::create(
492            new FakeSqlRewriter($store, $registry),
493            $store,
494            new ResultSelectRunner(),
495            ZtdConfig::default(),
496            new FakeConnection(),
497            registry: $registry,
498        );
499
500        $plan = $executor->rewrite('SELECT 1');
501
502        self::assertSame(QueryKind::READ, $plan->kind());
503    }
504
505    public function testProcessExecutedStatementReadsAReadStatementStraightBack(): void
506    {
507        $store = new ShadowStore();
508        $registry = new TableDefinitionRegistry();
509        $executor = \Tests\Fake\QueryExecutorBuilder::create(
510            new FakeSqlRewriter($store, $registry),
511            $store,
512            new ResultSelectRunner(),
513            ZtdConfig::default(),
514            new FakeConnection(),
515            registry: $registry,
516        );
517        $plan = new RewritePlan('SELECT 1', QueryKind::READ);
518
519        $result = $executor->processExecutedStatement($plan, new FakeStatement([['id' => 1]]));
520
521        self::assertSame([['id' => 1]], $result->fetchAll());
522    }
523
524    public function testApplyShadowWritesTheMutationAndAnswersWhatItCameTo(): void
525    {
526        $store = new ShadowStore();
527        $store->set('users', []);
528        $registry = new TableDefinitionRegistry();
529        $executor = \Tests\Fake\QueryExecutorBuilder::create(
530            new FakeSqlRewriter($store, $registry),
531            $store,
532            new ResultSelectRunner(),
533            ZtdConfig::default(),
534            new FakeConnection(),
535            registry: $registry,
536        );
537
538        $impact = $executor->applyShadow(new InsertMutation('users'), new ResultSet([['id' => 1]], []), 'INSERT');
539
540        self::assertTrue($impact->isInsertLike());
541        self::assertSame([['id' => 1]], $store->get('users'));
542    }
543
544    public function testExecStatementAnswersHowManyRowsAReadStatementCameTo(): void
545    {
546        $store = new ShadowStore();
547        $registry = new TableDefinitionRegistry();
548        $executor = \Tests\Fake\QueryExecutorBuilder::create(
549            new FakeSqlRewriter($store, $registry),
550            $store,
551            new ResultSelectRunner(),
552            ZtdConfig::default(),
553            new FakeConnection(),
554            registry: $registry,
555        );
556
557        self::assertSame(0, $executor->execStatement('SELECT 1'));
558    }
559
560    public function testRunResultSelectAndApplyShadowReadsBackWhatTheStatementWouldHaveWritten(): void
561    {
562        $store = new ShadowStore();
563        $store->set('users', []);
564        $registry = new TableDefinitionRegistry();
565        $executor = \Tests\Fake\QueryExecutorBuilder::create(
566            new FakeSqlRewriter($store, $registry),
567            $store,
568            new ResultSelectRunner(),
569            ZtdConfig::default(),
570            new FakeConnection(),
571            registry: $registry,
572        );
573        $plan = new RewritePlan('SELECT 1', QueryKind::WRITE_SIMULATED, new InsertMutation('users'));
574
575        $rows = $executor->runResultSelectAndApplyShadow(
576            $plan,
577            static fn (string $sql): FakeStatement => new FakeStatement([['id' => 1]]),
578        );
579
580        self::assertSame([['id' => 1]], $rows);
581    }
582
583    public function testEnableTurnsZtdBackOn(): void
584    {
585        $store = new ShadowStore();
586        $registry = new TableDefinitionRegistry();
587        $executor = \Tests\Fake\QueryExecutorBuilder::create(
588            new FakeSqlRewriter($store, $registry),
589            $store,
590            new ResultSelectRunner(),
591            ZtdConfig::default(),
592            new FakeConnection(),
593            registry: $registry,
594        );
595        $executor->session()->disable();
596
597        $executor->session()->enable();
598
599        self::assertTrue($executor->session()->isEnabled());
600    }
601
602    public function testRollBackTransactionPutsTheShadowBackToWhereItBegan(): void
603    {
604        $store = new ShadowStore();
605        $store->set('users', [['id' => 1]]);
606        $registry = new TableDefinitionRegistry();
607        $executor = \Tests\Fake\QueryExecutorBuilder::create(
608            new FakeSqlRewriter($store, $registry),
609            $store,
610            new ResultSelectRunner(),
611            ZtdConfig::default(),
612            new FakeConnection(),
613            registry: $registry,
614        );
615
616        $executor->session()->transactions()->begin();
617        $store->set('users', []);
618        $executor->session()->transactions()->rollBack();
619
620        self::assertSame([['id' => 1]], $store->get('users'));
621    }
622
623    public function testResultColumnTypeResolverAnswersTheOneTheSessionWasBuiltWith(): void
624    {
625        $store = new ShadowStore();
626        $registry = new TableDefinitionRegistry();
627        $executor = \Tests\Fake\QueryExecutorBuilder::create(
628            new FakeSqlRewriter($store, $registry),
629            $store,
630            new ResultSelectRunner(),
631            ZtdConfig::default(),
632            new FakeConnection(),
633            registry: $registry,
634        );
635
636        self::assertInstanceOf(MissingResultColumnTypeResolver::class, $executor->platform()->resultColumnTypeResolver());
637    }
638
639    public function testTransactionsAnswersWhatATransactionStatementIsAppliedTo(): void
640    {
641        $store = new ShadowStore();
642        $store->set('users', [['id' => 1]]);
643        $registry = new TableDefinitionRegistry();
644        $executor = \Tests\Fake\QueryExecutorBuilder::create(
645            new FakeSqlRewriter($store, $registry),
646            $store,
647            new ResultSelectRunner(),
648            ZtdConfig::default(),
649            new FakeConnection(),
650            registry: $registry,
651        );
652
653        TransactionStatement::begin()->apply($executor->session()->transactions());
654        $store->set('users', []);
655        TransactionStatement::rollback()->apply($executor->session()->transactions());
656
657        self::assertSame([['id' => 1]], $store->get('users'));
658    }
659
660    public function testPlatformAndSessionKeepReusedDialectStateIsolated(): void
661    {
662        $platform = new \Tests\Fake\FakePlatform(new \Tests\Fake\FakeSchemaReflector([
663            'users' => 'CREATE TABLE users (id INT NOT NULL PRIMARY KEY)',
664        ]));
665        $first = new QueryExecutor(new FakeConnection(defaultRows: [['id' => 1]]), $platform);
666        $second = new QueryExecutor(new FakeConnection(defaultRows: [['id' => 2]]), $platform);
667        self::assertSame($platform, $first->platform());
668        self::assertSame($platform, $second->platform());
669        self::assertNotSame($first->session(), $second->session());
670        self::assertNotSame($first->session()->registry(), $second->session()->registry());
671        $first->session()->beginTransaction();
672        $first->execStatement('INSERT INTO users (id) VALUES (1)');
673        $second->execStatement('INSERT INTO users (id) VALUES (2)');
674        $first->session()->rollBackTransaction();
675        self::assertSame([], $first->session()->store()->get('users'));
676        self::assertSame([['id' => 2]], $second->session()->store()->get('users'));
677        self::assertSame('2', $second->session()->lastInsertId());
678    }
679
680    public function testSessionAcceptsAnExplicitCatalogWithoutReflectingThePhysicalConnection(): void
681    {
682        $connection = self::createMock(\ZtdQuery\Connection\ConnectionInterface::class);
683        $connection->expects(self::never())->method('query');
684        $session = new \ZtdQuery\Session();
685        $session->registry()->register('users', new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []));
686        $session->store()->set('users', [['id' => 7]]);
687        $executor = new QueryExecutor($connection, new \Tests\Fake\FakePlatform(), session: $session);
688        self::assertSame($session, $executor->session());
689        self::assertStringContainsString('7', $executor->rewrite('SELECT * FROM users')->sql());
690    }
691
692    public function testSessionReflectionFailurePreventsRewriterConstruction(): void
693    {
694        $platform = self::createMock(\ZtdQuery\Platform::class);
695        $platform->expects(self::once())->method('reflectSchema')->willThrowException(new DatabaseException('catalog unavailable'));
696        $platform->expects(self::never())->method('createRewriter');
697        $this->expectException(DatabaseException::class);
698        new QueryExecutor(new FakeConnection(), $platform);
699    }
700}
701