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