packages/ztd-query-mysqli-adapter/fuzz/Robustness/Target/ExecutionTarget.php
1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Robustness\Target;
6
7use Error;
8use mysqli;
9use mysqli_result;
10use mysqli_sql_exception;
11use SqlFaker\Generation\Choice\BytePlanCompiler;
12use SqlFaker\Generation\Choice\PlanBuilder;
13use SqlFaker\Generation\Plan\GenerationPlan;
14use SqlFaker\MySqlProvider;
15use ZtdQuery\Adapter\Mysqli\ZtdMysqli;
16use ZtdQuery\Adapter\Mysqli\ZtdMysqliException;
17
18/**
19 * Replays grammar plans through a fresh adapter and verifies physical isolation.
20 */
21final class ExecutionTarget
22{
23 private PlanBuilder $planner;
24
25 /**
26 * @var GenerationPlan<true>
27 */
28 private GenerationPlan $constraint;
29
30 /**
31 * Bind the immutable grammar and the disposable native fixture connection.
32 *
33 * Building the planner takes most of a second, so it is built once rather than per input.
34 */
35 public function __construct(private MySqlProvider $provider, private mysqli $native)
36 {
37 $this->planner = $provider->planner();
38 $this->constraint = GenerationPlan::fromRule('simple_statement_or_begin')->requiringNonEmpty();
39 }
40
41 /**
42 * Run one reproducible grammar case with no state shared by adapter instances.
43 *
44 * @throws Error When execution leaks an unexpected error or changes physical rows.
45 */
46 public function __invoke(string $input): void
47 {
48 $plan = (new BytePlanCompiler())->compile($input, $this->planner, $this->constraint);
49 $sql = $this->provider->generate($plan);
50 $ztd = ZtdMysqli::fromMysqli($this->native);
51 try {
52 $result = $ztd->query($sql);
53 if ($result instanceof mysqli_result) {
54 $result->free();
55 }
56 } catch (ZtdMysqliException) {
57 // Unsupported grammar and unknown schema are explicit adapter rejections.
58 } catch (mysqli_sql_exception $exception) {
59 // Grammar identifiers need not exist in the fixed schema.
60 if (!in_array($exception->getCode(), [1054, 1146, 1109, 1327], true)) {
61 throw new Error("Unexpected mysqli error\nPHP: " . PHP_VERSION . "\nMySQL: " . $this->native->server_info . "\nInput: " . bin2hex($input) . "\nSQL: $sql", 0, $exception);
62 }
63 }
64 try {
65 /** @throws mysqli_sql_exception */
66 $physical = $this->native->query('SELECT id, name FROM users ORDER BY id');
67 if (!$physical instanceof mysqli_result || mysqli_fetch_all($physical, MYSQLI_ASSOC) !== [['id' => '1', 'name' => 'Alice'], ['id' => '2', 'name' => 'Bob']]) {
68 throw new Error("Physical table changed\nInput: " . bin2hex($input) . "\nSQL: $sql");
69 }
70 $physical->free();
71 } finally {
72 $this->native->rollback();
73 $this->native->autocommit(true);
74 }
75 }
76}
77