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