packages/ztd-query-pdo-adapter/fuzz/Robustness/ExecutionCheck.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Robustness;
6
7use Error;
8use Fuzz\Correctness\PhysicalTableSnapshot;
9use PDO;
10use PDOException;
11use ZtdQuery\Adapter\Pdo\ZtdPdo;
12use ZtdQuery\Config\UnknownSchemaBehavior;
13use ZtdQuery\Config\UnsupportedSqlBehavior;
14use ZtdQuery\Config\ZtdConfig;
15use ZtdQuery\Exception\ColumnAlreadyExistsException;
16use ZtdQuery\Exception\ColumnNotFoundException;
17use ZtdQuery\Exception\DuplicateKeyException;
18use ZtdQuery\Exception\ForeignKeyViolationException;
19use ZtdQuery\Exception\NotNullViolationException;
20use ZtdQuery\Exception\SchemaNotFoundException;
21use ZtdQuery\Exception\SqlParseException;
22use ZtdQuery\Exception\TableAlreadyExistsException;
23use ZtdQuery\Exception\UnknownSchemaException;
24use ZtdQuery\Exception\UnsupportedSqlException;
25
26/**
27 * Executes grammar-generated SQL through the public adapter and checks physical isolation.
28 * Name resolution and supported domain rejections are expected for arbitrary grammar output.
29 * Unexpected driver errors and PHP Errors remain findings with the exact input attached.
30 */
31final class ExecutionCheck
32{
33    /**
34     * Retain the native connection used to inspect physical rows.
35     */
36    public function __construct(private readonly PDO $native)
37    {
38    }
39
40    /**
41     * Execute generated SQL and compare the physical tables afterward.
42     *
43     * @throws Error When the adapter leaks an unexpected error or modifies physical rows.
44     */
45    public function verify(string $sql, string $input): void
46    {
47        $snapshots = [];
48        foreach (['users', 'orders', 'order_items', 'products'] as $table) {
49            $snapshots[$table] = PhysicalTableSnapshot::capture($this->native, $table);
50        }
51        $pdo = ZtdPdo::fromPdo($this->native, new ZtdConfig(UnsupportedSqlBehavior::Exception, UnknownSchemaBehavior::Exception));
52        try {
53            $pdo->query($sql);
54        } catch (PDOException $failure) {
55            $domainErrors = [UnsupportedSqlException::class, UnknownSchemaException::class, SchemaNotFoundException::class, ColumnNotFoundException::class, TableAlreadyExistsException::class, ColumnAlreadyExistsException::class, DuplicateKeyException::class, ForeignKeyViolationException::class, NotNullViolationException::class, SqlParseException::class];
56            for ($cause = $failure; $cause !== null; $cause = $cause->getPrevious()) {
57                if ($cause instanceof PDOException && in_array($cause->errorInfo[1] ?? null, [1040, 2002, 2006, 2013], true)) {
58                    fwrite(STDERR, 'MySQL connection failed: ' . $cause->getMessage() . PHP_EOL);
59                    exit(2);
60                }
61                foreach ($domainErrors as $domainError) {
62                    if ($cause instanceof $domainError) {
63                        return;
64                    }
65                }
66            }
67            $allowed = [
68                ['42S22', 1054], // Grammar-generated column names need not exist.
69                ['42S02', 1146], // Grammar-generated table names need not exist.
70                ['42S02', 1109], // Multi-table references may name an absent table.
71                ['42000', 1327], // Grammar-generated variables are not declared.
72            ];
73            if (!in_array([$failure->errorInfo[0] ?? null, $failure->errorInfo[1] ?? null], $allowed, true)) {
74                throw new Error("Unexpected adapter rejection\nInput: " . bin2hex($input) . "\nSQL: $sql\n" . $failure->getMessage(), 0, $failure);
75            }
76        } finally {
77            try {
78                foreach ($snapshots as $table => $snapshot) {
79                    PhysicalTableSnapshot::assertUnchanged($this->native, $table, $snapshot, $sql, crc32($input));
80                }
81            } finally {
82                if ($this->native->inTransaction()) {
83                    $this->native->rollBack();
84                }
85            }
86        }
87    }
88}
89