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

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Robustness\Target;
6
7use Error;
8use Fuzz\Robustness\Invariant\ClassifyRewriteAgreementChecker;
9use Fuzz\Robustness\Invariant\RewritePlanConsistencyChecker;
10use ZtdQuery\Platform\MySql\Rewrite\MySqlQueryGuard;
11use ZtdQuery\Platform\MySql\Rewrite\MySqlRewriter;
12use ZtdQuery\Platform\MySql\Rewrite\Transformer\DeleteTransformer;
13use ZtdQuery\Platform\MySql\Rewrite\Transformer\InsertTransformer;
14use ZtdQuery\Platform\MySql\Rewrite\Transformer\MySqlTransformer;
15use ZtdQuery\Platform\MySql\Rewrite\Transformer\ReplaceTransformer;
16use ZtdQuery\Platform\MySql\Rewrite\Transformer\SelectTransformer;
17use ZtdQuery\Platform\MySql\Rewrite\Transformer\UpdateTransformer;
18use ZtdQuery\Platform\MySql\Schema\MySqlSchemaParser;
19use ZtdQuery\Platform\MySql\Shadow\MySqlMutationResolver;
20use ZtdQuery\Platform\MySql\Sql\MySqlParser;
21use ZtdQuery\Schema\TableDefinitionRegistry;
22use ZtdQuery\Shadow\ShadowStore;
23
24/**
25 * Checks plan invariants and agreement between classification and rewriting.
26 */
27final class RewriteTarget
28{
29    /**
30     * Verify one statement with independent, disposable shadow state.
31     *
32     * @throws Error
33     */
34    public function __invoke(string $sql): void
35    {
36        $parser = new MySqlParser();
37        $schemaParser = new MySqlSchemaParser($parser);
38        $guard = new MySqlQueryGuard($parser);
39        $store = new ShadowStore();
40        $registry = new TableDefinitionRegistry();
41        $schemas = [
42            'users' => 'CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255), status VARCHAR(50))',
43            'orders' => 'CREATE TABLE orders (id INT PRIMARY KEY, user_id INT NOT NULL, amount DECIMAL(10,2), created_at DATETIME)',
44            '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))',
45            'products' => 'CREATE TABLE products (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10,2), category VARCHAR(100))',
46            '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)',
47        ];
48
49        foreach ($schemas as $tableName => $createSql) {
50            $definition = $schemaParser->parse($createSql);
51            if ($definition !== null) {
52                $registry->register($tableName, $definition);
53            }
54        }
55        $rowsByTable = [
56            'users' => [
57                ['id' => '1', 'name' => 'Alice', 'email' => 'alice@example.com', 'status' => 'active'],
58                ['id' => '2', 'name' => 'Bob', 'email' => 'bob@example.com', 'status' => 'pending'],
59                ['id' => '3', 'name' => 'Charlie', 'email' => null, 'status' => 'active'],
60            ],
61            'orders' => [
62                ['id' => '1', 'user_id' => '1', 'amount' => '100.00', 'created_at' => '2024-01-01 00:00:00'],
63                ['id' => '2', 'user_id' => '2', 'amount' => '250.50', 'created_at' => '2024-01-02 12:30:00'],
64            ],
65            'order_items' => [
66                ['order_id' => '1', 'product_id' => '1', 'quantity' => '2'],
67                ['order_id' => '1', 'product_id' => '2', 'quantity' => '1'],
68                ['order_id' => '2', 'product_id' => '1', 'quantity' => '3'],
69            ],
70            'products' => [
71                ['id' => '1', 'name' => 'Widget', 'price' => '19.99', 'category' => 'tools'],
72                ['id' => '2', 'name' => 'Gadget', 'price' => '49.99', 'category' => 'electronics'],
73            ],
74            'events' => [
75                ['id' => '1', 'event_date' => '2023-06-01'],
76                ['id' => '2', 'event_date' => '2024-06-01'],
77            ],
78        ];
79        foreach ($rowsByTable as $table => $rows) {
80            $store->set($table, $rows);
81        }
82        $select = new SelectTransformer();
83        $update = new UpdateTransformer($parser, $select);
84        $delete = new DeleteTransformer($parser, $select);
85        $transformer = new MySqlTransformer($parser, $select, new InsertTransformer($parser, $select), $update, $delete, new ReplaceTransformer($parser, $select));
86        $mutationResolver = new MySqlMutationResolver($store, $registry, $schemaParser, $update, $delete);
87        $rewriter = new MySqlRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
88        try {
89            $checks = [
90                new RewritePlanConsistencyChecker($rewriter),
91                new ClassifyRewriteAgreementChecker($guard, $rewriter),
92            ];
93            foreach ($checks as $check) {
94                $violation = $check->check($sql);
95                if ($violation !== null) {
96                    throw new Error((string) $violation);
97                }
98            }
99        } finally {
100            $store->clear();
101            foreach ($registry->getAll() as $name => $definition) {
102                $registry->unregister($name);
103            }
104        }
105    }
106}
107