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