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