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