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