packages/ztd-query-mysqli-adapter/fuzz/Correctness/MysqliCorrectnessHarness.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Correctness;
6
7use Faker\Factory;
8use Faker\Generator;
9use mysqli;
10use RuntimeException;
11use SqlFixture\FixtureProvider;
12use ZtdQuery\Adapter\Mysqli\ZtdMysqli;
13use ZtdQuery\Config\UnknownSchemaBehavior;
14use ZtdQuery\Config\UnsupportedSqlBehavior;
15use ZtdQuery\Config\ZtdConfig;
16
17/**
18 * Resets native and simulated tables to the same synthetic fixture state.
19 */
20final class MysqliCorrectnessHarness
21{
22    private mysqli $rawMysqli;
23    private ?ZtdMysqli $ztdMysqli = null;
24    private ?SchemaDefinition $currentSchema = null;
25    private string $host;
26    private int $port;
27    private string $dbName;
28    private string $user;
29    private string $pass;
30    private Generator $faker;
31    private FixtureProvider $fixtureProvider;
32
33
34    /**
35     * Bind the connection, schema generator and deterministic input dependencies.
36     */
37    public function __construct(string $host, int $port, string $dbName, string $user, string $pass)
38    {
39        $this->host = $host;
40        $this->port = $port;
41        $this->dbName = $dbName;
42        $this->user = $user;
43        $this->pass = $pass;
44        $this->rawMysqli = new mysqli($host, $user, $pass, $dbName, $port);
45        $this->faker = Factory::create();
46        $this->faker->addProvider(new FixedDateTimeProvider());
47        $this->fixtureProvider = new FixtureProvider($this->faker);
48    }
49
50    /**
51     * Set up both connections with the same schema and data.
52     *
53     * @return array<int, array<string, mixed>> The fixture rows inserted
54     */
55    public function setup(SchemaDefinition $schema, int $seed, int $rowCount = 3): array
56    {
57        $this->currentSchema = $schema;
58        $this->faker->seed($seed);
59
60        $this->rawMysqli->query("DROP TABLE IF EXISTS `{$schema->name}`");
61        $this->rawMysqli->query($schema->sql);
62
63        $fixtureRows = [];
64        for ($i = 0; $i < $rowCount; $i++) {
65            $row = $this->fixtureProvider->fixture($schema->sql);
66            if (count($schema->primaryKeys) === 1 && $schema->primaryKeys[0] === 'id') {
67                $row['id'] = $i + 1;
68            }
69            if ($schema->name === 'composite_pk') {
70                $row['order_id'] = $i + 1;
71                $row['product_id'] = ($i + 1) * 10;
72            }
73            $fixtureRows[] = $row;
74        }
75
76        foreach ($fixtureRows as $row) {
77            (new FixtureRowWriter())->insertRow($this->rawMysqli, $schema->name, $row);
78        }
79
80        $this->ztdMysqli = new ZtdMysqli(
81            $this->host,
82            $this->user,
83            $this->pass,
84            $this->dbName,
85            $this->port,
86            null,
87            new ZtdConfig(UnsupportedSqlBehavior::Ignore, UnknownSchemaBehavior::Exception)
88        );
89
90        $this->ztdMysqli->query($schema->sql);
91        foreach ($fixtureRows as $row) {
92            $columns = array_keys($row);
93            $values = array_map(function ($v) {
94                if ($v === null) {
95                    return 'NULL';
96                }
97                if (is_int($v) || is_float($v)) {
98                    return (string) $v;
99                }
100                if (is_bool($v)) {
101                    return $v ? '1' : '0';
102                }
103                assert(is_string($v));
104                return "'" . addslashes($v) . "'";
105            }, array_values($row));
106            $sql = sprintf(
107                'INSERT INTO `%s` (%s) VALUES (%s)',
108                $schema->name,
109                implode(', ', array_map(fn ($c) => "`$c`", $columns)),
110                implode(', ', $values)
111            );
112            $this->ztdMysqli->query($sql);
113        }
114
115        return $fixtureRows;
116    }
117
118    /**
119     * Remove the active physical table and discard its simulated session.
120     */
121    public function teardown(): void
122    {
123        if ($this->currentSchema !== null) {
124            $this->rawMysqli->query("DROP TABLE IF EXISTS `{$this->currentSchema->name}`");
125        }
126        $this->ztdMysqli = null;
127        $this->currentSchema = null;
128    }
129
130    /**
131     * Return the native connection that serves as the differential oracle.
132     */
133    public function getRawMysqli(): mysqli
134    {
135        return $this->rawMysqli;
136    }
137
138    /**
139     * Return the adapter initialized for the current scenario.
140     *
141     * @throws RuntimeException If setup has not created a session.
142     */
143    public function getZtdMysqli(): ZtdMysqli
144    {
145        if ($this->ztdMysqli === null) {
146            throw new RuntimeException('ZtdMysqli not initialized. Call setup() first.');
147        }
148        return $this->ztdMysqli;
149    }
150
151
152    /**
153     * Return the schema owned by the active scenario, if any.
154     */
155    public function getCurrentSchema(): ?SchemaDefinition
156    {
157        return $this->currentSchema;
158    }
159
160
161}
162