packages/ztd-query-core/tests/Unit/Simulator/StatementSimulatorTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Simulator;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use RuntimeException;
11use Tests\Fake\ExceptionThrowingRewriter;
12use Tests\Fake\FixedRewriter;
13use ZtdQuery\Config\ZtdConfig;
14use ZtdQuery\Connection\ConnectionInterface;
15use ZtdQuery\Connection\Exception\DatabaseException;
16use ZtdQuery\Connection\ResultSet;
17use ZtdQuery\Connection\StatementInterface;
18use ZtdQuery\Exception\UnsupportedSqlException;
19use ZtdQuery\QueryExecutor;
20use ZtdQuery\ResultSelectRunner;
21use ZtdQuery\Rewrite\QueryKind;
22use ZtdQuery\Rewrite\RewritePlan;
23use ZtdQuery\RewriteRefusal;
24use ZtdQuery\Schema\Key\CandidateKeySet;
25use ZtdQuery\Schema\TableDefinitionRegistry;
26use ZtdQuery\Shadow\Mutation\MutationImpact;
27use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
28use ZtdQuery\Shadow\ReferentialIntegrityEnforcer;
29use ZtdQuery\Shadow\ShadowStore;
30use ZtdQuery\Shadow\ShadowTransactions;
31use ZtdQuery\Simulator\StatementSimulator;
32
33#[UsesClass(ZtdConfig::class)]
34#[UsesClass(\ZtdQuery\Session::class)]
35#[UsesClass(\ZtdQuery\Schema\ViewDefinitionSet::class)]
36#[UsesClass(DatabaseException::class)]
37#[UsesClass(UnsupportedSqlException::class)]
38#[UsesClass(RewritePlan::class)]
39#[UsesClass(ResultSelectRunner::class)]
40#[UsesClass(ResultSet::class)]
41#[UsesClass(InsertMutation::class)]
42#[UsesClass(MutationImpact::class)]
43#[UsesClass(CandidateKeySet::class)]
44#[UsesClass(TableDefinitionRegistry::class)]
45#[UsesClass(ShadowStore::class)]
46#[UsesClass(ShadowTransactions::class)]
47#[UsesClass(ReferentialIntegrityEnforcer::class)]
48#[UsesClass(QueryExecutor::class)]
49#[CoversClass(StatementSimulator::class)]
50#[UsesClass(\ZtdQuery\Shadow\ForeignKeyCascade::class)]
51#[UsesClass(\ZtdQuery\Shadow\ForeignKeyEnds::class)]
52#[UsesClass(\ZtdQuery\Shadow\ForeignKeyIntegrity::class)]
53#[UsesClass(\ZtdQuery\Shadow\Mutation\ConflictSearch::class)]
54#[UsesClass(\ZtdQuery\Shadow\Mutation\RowConstraints::class)]
55#[UsesClass(\ZtdQuery\Shadow\ParentKeyLookup::class)]
56#[UsesClass(\ZtdQuery\Shadow\Row\RowMultiset::class)]
57#[UsesClass(\ZtdQuery\Shadow\ShadowApplication::class)]
58#[UsesClass(\ZtdQuery\Shadow\TableTransitions::class)]
59#[UsesClass(RewriteRefusal::class)]
60#[UsesClass(\ZtdQuery\Schema\RowSet::class)]
61final class StatementSimulatorTest extends TestCase
62{
63    public function testUnsupportedSqlThrows(): void
64    {
65        $shadowStore = new ShadowStore();
66        $connection = static::createStub(ConnectionInterface::class);
67        $executor = \Tests\Fake\QueryExecutorBuilder::create(
68            new ExceptionThrowingRewriter(new UnsupportedSqlException('DROP TABLE users', 'Unsupported')),
69            $shadowStore,
70            new ResultSelectRunner(),
71            ZtdConfig::default(),
72            $connection
73        );
74        $simulator = new StatementSimulator($executor);
75
76        $this->expectException(DatabaseException::class);
77
78        $simulator->simulate('DROP TABLE users', fn () => false);
79    }
80
81    public function testReadStatementReturnsRowCount(): void
82    {
83        $shadowStore = new ShadowStore();
84        $connection = static::createStub(ConnectionInterface::class);
85        $executor = \Tests\Fake\QueryExecutorBuilder::create(
86            new FixedRewriter(new RewritePlan('SELECT 1 AS id', QueryKind::READ)),
87            $shadowStore,
88            new ResultSelectRunner(),
89            ZtdConfig::default(),
90            $connection
91        );
92        $simulator = new StatementSimulator($executor);
93
94        $statement = static::createStub(StatementInterface::class);
95        $statement->method('rowCount')->willReturn(1);
96
97        $result = $simulator->simulate('SELECT 1 AS id', function (string $sql) use ($statement) {
98            return $statement;
99        });
100
101        self::assertSame(1, $result);
102    }
103
104    public function testReadStatementReturnsFalseWhenExecutorFails(): void
105    {
106        $shadowStore = new ShadowStore();
107        $connection = static::createStub(ConnectionInterface::class);
108        $executor = \Tests\Fake\QueryExecutorBuilder::create(
109            new FixedRewriter(new RewritePlan('SELECT 1 AS id', QueryKind::READ)),
110            $shadowStore,
111            new ResultSelectRunner(),
112            ZtdConfig::default(),
113            $connection
114        );
115        $simulator = new StatementSimulator($executor);
116
117        $result = $simulator->simulate('SELECT 1 AS id', fn () => false);
118
119        self::assertFalse($result);
120    }
121
122    public function testWriteStatementUpdatesShadowStore(): void
123    {
124        $store = new ShadowStore();
125        $connection = static::createStub(ConnectionInterface::class);
126
127        $resultStatement = static::createStub(StatementInterface::class);
128        $resultStatement->method('fetchAll')->willReturn([
129            ['id' => 2, 'name' => 'Bob'],
130        ]);
131        $connection->method('query')->willReturn($resultStatement);
132
133        $executor = \Tests\Fake\QueryExecutorBuilder::create(
134            new FixedRewriter(new RewritePlan('SELECT 2 AS id, \'Bob\' AS name', QueryKind::WRITE_SIMULATED, new InsertMutation('users'))),
135            $store,
136            new ResultSelectRunner(),
137            ZtdConfig::default(),
138            $connection
139        );
140        $simulator = new StatementSimulator($executor);
141
142        $executorStatement = static::createStub(StatementInterface::class);
143        $executorStatement->method('fetchAll')->willReturn([
144            ['id' => 2, 'name' => 'Bob'],
145        ]);
146
147        $result = $simulator->simulate('INSERT INTO users VALUES (2, \'Bob\')', function (string $sql) use ($executorStatement) {
148            return $executorStatement;
149        });
150
151        self::assertSame(1, $result);
152        $rows = $store->get('users');
153        self::assertCount(1, $rows);
154        self::assertSame('Bob', $rows[0]['name']);
155        self::assertSame(2, $rows[0]['id']);
156    }
157
158    public function testSimulateRefusesAWriteThePlanCarriesNoMutationFor(): void
159    {
160        $shadowStore = new ShadowStore();
161        $connection = static::createStub(ConnectionInterface::class);
162        $executor = \Tests\Fake\QueryExecutorBuilder::create(
163            new FixedRewriter(new RewritePlan('SELECT 1 AS id', QueryKind::WRITE_SIMULATED)),
164            $shadowStore,
165            new ResultSelectRunner(),
166            ZtdConfig::default(),
167            $connection
168        );
169        $simulator = new StatementSimulator($executor);
170
171        $this->expectException(RuntimeException::class);
172        $this->expectExceptionMessage('Missing shadow mutation');
173
174        $simulator->simulate('UPDATE users SET name = \'Bob\' WHERE id = 1', fn () => false);
175    }
176}
177