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