packages/ztd-query-pdo-adapter/fuzz/Correctness/Sqlite/SqliteCorrectnessHarness.php
1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Correctness\Sqlite;
6
7use Faker\Factory;
8use Faker\Generator;
9use Fuzz\Correctness\SchemaDefinition;
10use PDO;
11use RuntimeException;
12use ZtdQuery\Adapter\Pdo\ZtdPdo;
13use ZtdQuery\Config\UnknownSchemaBehavior;
14use ZtdQuery\Config\UnsupportedSqlBehavior;
15use ZtdQuery\Config\ZtdConfig;
16
17/**
18 * @phpstan-import-type Row from \Fuzz\Correctness\CorrectnessHarness
19 */
20final class SqliteCorrectnessHarness
21{
22 private PDO $rawPdo;
23 private ?ZtdPdo $ztdPdo = null;
24 private ?SchemaDefinition $currentSchema = null;
25 private Generator $faker;
26
27 /** @var list<Row> */
28 private array $fixtureRows = [];
29
30 /**
31 * Binds the instance to what it will work from.
32 *
33 */
34 public function __construct()
35 {
36 $this->rawPdo = new PDO('sqlite::memory:', null, null, [
37 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
38 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
39 ]);
40 $this->faker = Factory::create();
41 $this->faker->addProvider(new \Fuzz\Correctness\FixedDateTimeProvider());
42 }
43
44 /**
45 * Set up both connections with the same schema and data.
46 *
47 * @return list<Row> The fixture rows inserted
48 */
49 public function setup(SchemaDefinition $schema, int $seed, int $rowCount = 3): array
50 {
51 $this->currentSchema = $schema;
52 $this->faker->seed($seed);
53
54 $this->rawPdo->exec("DROP TABLE IF EXISTS \"{$schema->name}\"");
55 $this->rawPdo->exec($schema->sql);
56
57 $this->fixtureRows = [];
58 for ($i = 0; $i < $rowCount; $i++) {
59 $row = $this->generateFixtureRow($schema, $i);
60 $this->fixtureRows[] = $row;
61 }
62
63 foreach ($this->fixtureRows as $row) {
64 $this->insertRow($this->rawPdo, $schema->name, $row);
65 }
66
67 $this->ztdPdo = ZtdPdo::fromPdo(
68 $this->rawPdo,
69 new ZtdConfig(UnsupportedSqlBehavior::Ignore, UnknownSchemaBehavior::Exception)
70 );
71
72 $this->ztdPdo->exec($schema->sql);
73 foreach ($this->fixtureRows as $row) {
74 $columns = array_keys($row);
75 $values = array_map(function ($v) {
76 if ($v === null) {
77 return 'NULL';
78 }
79 if (is_int($v) || is_float($v)) {
80 return (string) $v;
81 }
82 if (is_bool($v)) {
83 return $v ? '1' : '0';
84 }
85 return "'" . str_replace("'", "''", $v) . "'";
86 }, array_values($row));
87 $sql = sprintf(
88 'INSERT INTO "%s" (%s) VALUES (%s)',
89 str_replace('"', '""', $schema->name),
90 implode(', ', array_map(fn ($c) => '"' . str_replace('"', '""', $c) . '"', $columns)),
91 implode(', ', $values)
92 );
93 $this->ztdPdo->exec($sql);
94 }
95
96 return $this->fixtureRows;
97 }
98
99 /**
100 * Teardown.
101 *
102 */
103 public function teardown(): void
104 {
105 if ($this->currentSchema !== null) {
106 $this->rawPdo->exec("DROP TABLE IF EXISTS \"{$this->currentSchema->name}\"");
107 }
108 $this->ztdPdo = null;
109 $this->currentSchema = null;
110 $this->fixtureRows = [];
111 }
112
113 /**
114 * Answers raw pdo.
115 *
116 * @return PDO
117 */
118 public function getRawPdo(): PDO
119 {
120 return $this->rawPdo;
121 }
122
123 /**
124 * @throws RuntimeException
125 */
126 public function getZtdPdo(): ZtdPdo
127 {
128 if ($this->ztdPdo === null) {
129 throw new RuntimeException('ZtdPdo not initialized. Call setup() first.');
130 }
131 return $this->ztdPdo;
132 }
133
134 /**
135 * @return list<Row>
136 */
137 public function getFixtureRows(): array
138 {
139 return $this->fixtureRows;
140 }
141
142 /**
143 * Answers current schema.
144 *
145 * @return ?SchemaDefinition
146 */
147 public function getCurrentSchema(): ?SchemaDefinition
148 {
149 return $this->currentSchema;
150 }
151
152 /**
153 * Answers one fixture row for the schema, made from the index so a run repeats.
154 *
155 * @param SchemaDefinition $schema The schema
156 * @param int $index Where to read
157 *
158 * @return Row What it answers
159 */
160 public function generateFixtureRow(SchemaDefinition $schema, int $index): array
161 {
162 $row = [];
163 foreach ($schema->columns as $col) {
164 $colLower = strtolower($col);
165
166 if ($col === 'id' || str_ends_with($colLower, '_id')) {
167 $row[$col] = $index + 1;
168 } elseif (str_contains($colLower, 'real') || str_contains($colLower, 'float') || str_contains($colLower, 'double')) {
169 $row[$col] = round($this->faker->randomFloat(2, 0, 999), 2);
170 } elseif (str_contains($colLower, 'int') || str_contains($colLower, 'quantity') || str_contains($colLower, 'numeric')) {
171 $row[$col] = $this->faker->numberBetween(1, 100);
172 } elseif (str_contains($colLower, 'blob')) {
173 $row[$col] = $this->faker->lexify('????');
174 } else {
175 $row[$col] = $this->faker->lexify('????');
176 }
177 }
178
179 if ($schema->name === 'composite_pk') {
180 $row['order_id'] = $index + 1;
181 $row['product_id'] = ($index + 1) * 10;
182 }
183
184 return $row;
185 }
186
187 /**
188 * Writes one fixture row into the table both sides read.
189 *
190 * @param PDO $pdo The pdo
191 * @param string $table Table it belongs to
192 * @param Row $row Row to read
193 */
194 public function insertRow(PDO $pdo, string $table, array $row): void
195 {
196 $columns = array_keys($row);
197 $placeholders = array_fill(0, count($columns), '?');
198 $sql = sprintf(
199 'INSERT INTO "%s" (%s) VALUES (%s)',
200 str_replace('"', '""', $table),
201 implode(', ', array_map(fn ($c) => '"' . str_replace('"', '""', $c) . '"', $columns)),
202 implode(', ', $placeholders)
203 );
204 $values = array_map(function ($v) {
205 if (is_bool($v)) {
206 return $v ? 1 : 0;
207 }
208 return $v;
209 }, array_values($row));
210 $stmt = $pdo->prepare($sql);
211 $stmt->execute($values);
212 }
213}
214