packages/ztd-query-mysql/fuzz/Robustness/Target/RobustnessTarget.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Robustness\Target;
6
7use Error;
8use Fuzz\Robustness\Invariant\ShadowStoreConsistencyChecker;
9use ZtdQuery\Exception\SimulationException;
10use ZtdQuery\Exception\UnknownSchemaException;
11use ZtdQuery\Exception\UnsupportedSqlException;
12use ZtdQuery\Platform\MySql\Rewrite\MySqlQueryGuard;
13use ZtdQuery\Platform\MySql\Rewrite\MySqlRewriter;
14use ZtdQuery\Platform\MySql\Rewrite\Transformer\DeleteTransformer;
15use ZtdQuery\Platform\MySql\Rewrite\Transformer\InsertTransformer;
16use ZtdQuery\Platform\MySql\Rewrite\Transformer\MySqlTransformer;
17use ZtdQuery\Platform\MySql\Rewrite\Transformer\ReplaceTransformer;
18use ZtdQuery\Platform\MySql\Rewrite\Transformer\SelectTransformer;
19use ZtdQuery\Platform\MySql\Rewrite\Transformer\UpdateTransformer;
20use ZtdQuery\Platform\MySql\Schema\MySqlSchemaParser;
21use ZtdQuery\Platform\MySql\Shadow\MySqlMutationResolver;
22use ZtdQuery\Platform\MySql\Sql\MySqlParser;
23use ZtdQuery\Schema\TableDefinitionRegistry;
24use ZtdQuery\Shadow\ShadowStore;
25
26/**
27 * Exercises classification, rewrite invariants and simulated writes in isolated state.
28 */
29final class RobustnessTarget
30{
31    /**
32     * Apply metadata mutations and verify store structure after every outcome.
33     *
34     * @throws Error
35     */
36    public function __invoke(string $sql): void
37    {
38        (new ClassifyTarget())($sql);
39        (new RewriteTarget())($sql);
40        $parser = new MySqlParser();
41        $schemaParser = new MySqlSchemaParser($parser);
42        $guard = new MySqlQueryGuard($parser);
43        $store = new ShadowStore();
44        $registry = new TableDefinitionRegistry();
45        $schemas = [
46            'users' => 'CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255), status VARCHAR(50))',
47            'orders' => 'CREATE TABLE orders (id INT PRIMARY KEY, user_id INT NOT NULL, amount DECIMAL(10,2), created_at DATETIME)',
48            'order_items' => 'CREATE TABLE order_items (order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, PRIMARY KEY (order_id, product_id))',
49            'products' => 'CREATE TABLE products (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10,2), category VARCHAR(100))',
50            'events' => 'CREATE TABLE events (id INT PRIMARY KEY, event_date DATE) PARTITION BY RANGE (YEAR(event_date)) (PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025), PARTITION pmax VALUES LESS THAN MAXVALUE)',
51        ];
52
53        foreach ($schemas as $tableName => $createSql) {
54            $definition = $schemaParser->parse($createSql);
55            if ($definition !== null) {
56                $registry->register($tableName, $definition);
57            }
58        }
59        $rowsByTable = [
60            'users' => [
61                ['id' => '1', 'name' => 'Alice', 'email' => 'alice@example.com', 'status' => 'active'],
62                ['id' => '2', 'name' => 'Bob', 'email' => 'bob@example.com', 'status' => 'pending'],
63                ['id' => '3', 'name' => 'Charlie', 'email' => null, 'status' => 'active'],
64            ],
65            'orders' => [
66                ['id' => '1', 'user_id' => '1', 'amount' => '100.00', 'created_at' => '2024-01-01 00:00:00'],
67                ['id' => '2', 'user_id' => '2', 'amount' => '250.50', 'created_at' => '2024-01-02 12:30:00'],
68            ],
69            'order_items' => [
70                ['order_id' => '1', 'product_id' => '1', 'quantity' => '2'],
71                ['order_id' => '1', 'product_id' => '2', 'quantity' => '1'],
72                ['order_id' => '2', 'product_id' => '1', 'quantity' => '3'],
73            ],
74            'products' => [
75                ['id' => '1', 'name' => 'Widget', 'price' => '19.99', 'category' => 'tools'],
76                ['id' => '2', 'name' => 'Gadget', 'price' => '49.99', 'category' => 'electronics'],
77            ],
78            'events' => [
79                ['id' => '1', 'event_date' => '2023-06-01'],
80                ['id' => '2', 'event_date' => '2024-06-01'],
81            ],
82        ];
83        foreach ($rowsByTable as $table => $rows) {
84            $store->set($table, $rows);
85        }
86        $select = new SelectTransformer();
87        $update = new UpdateTransformer($parser, $select);
88        $delete = new DeleteTransformer($parser, $select);
89        $transformer = new MySqlTransformer($parser, $select, new InsertTransformer($parser, $select), $update, $delete, new ReplaceTransformer($parser, $select));
90        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $update, $delete);
91        $rewriter = new MySqlRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
92        try {
93            try {
94                $plan = $rewriter->rewrite($sql);
95            } catch (UnsupportedSqlException | UnknownSchemaException) {
96                return;
97            }
98            $mutation = $plan->mutation();
99            if ($mutation === null) {
100                return;
101            }
102            try {
103                /**
104                 * @throws SimulationException Domain rejection declared by concrete shadow mutations.
105                 */
106                $mutation->apply($store, []);
107            } catch (SimulationException) {
108                return;
109            } finally {
110                $violation = (new ShadowStoreConsistencyChecker($store))->check($sql);
111                if ($violation !== null) {
112                    throw new Error((string) $violation);
113                }
114            }
115        } finally {
116            $store->clear();
117            foreach ($registry->getAll() as $name => $definition) {
118                $registry->unregister($name);
119            }
120        }
121    }
122}
123