packages/ztd-query-sqlite/fuzz/Semantics/SemanticsTarget.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Semantics;
6
7use Error;
8use PDO;
9use ZtdQuery\Platform\Sqlite\Rewrite\SqliteQueryGuard;
10use ZtdQuery\Platform\Sqlite\Rewrite\SqliteRewriter;
11use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\DeleteTransformer;
12use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertTransformer;
13use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\SelectTransformer;
14use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\SqliteTransformer;
15use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\UpdateTransformer;
16use ZtdQuery\Platform\Sqlite\Schema\SqliteSchemaParser;
17use ZtdQuery\Platform\Sqlite\Shadow\SqliteMutationResolver;
18use ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter;
19use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
20use ZtdQuery\Platform\Sqlite\Sql\Value\SqliteCastRenderer;
21use ZtdQuery\Rewrite\QueryKind;
22use ZtdQuery\Schema\TableDefinitionRegistry;
23use ZtdQuery\Shadow\ShadowStore;
24
25/**
26 * Compares rewritten SELECT and simulated DML with native SQLite after every operation.
27 */
28final class SemanticsTarget
29{
30    /**
31     * Retains SQLFaker's grammar analysis between independent command sequences.
32     */
33    public function __construct(private readonly CommandSequence $sequence)
34    {
35    }
36
37    /**
38     * Runs one bounded sequence; local connections and all mutable state expire on return or failure.
39     * @throws Error
40     */
41    public function __invoke(string $input): void
42    {
43        $commands = $this->sequence->compile($input);
44        $schema = 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, score INTEGER NOT NULL)';
45        $nativeDatabase = new PDO('sqlite::memory:', options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
46        $physicalDatabase = new PDO('sqlite::memory:', options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
47        $nativeDatabase->exec($schema);
48        $physicalDatabase->exec($schema);
49        $nativeDatabase->exec("INSERT INTO users VALUES (1, 'Alice', 10), (2, 'Bob', 20)");
50        $physicalDatabase->exec("INSERT INTO users VALUES (9000, 'physical', 777)");
51        $definition = (new SqliteSchemaParser())->parse($schema);
52        if ($definition === null) {
53            throw new Error('Fixture schema could not be reflected');
54        }
55        $registry = new TableDefinitionRegistry();
56        $registry->register('users', $definition);
57        $store = new ShadowStore();
58        $store->set('users', [['id' => 1, 'name' => 'Alice', 'score' => 10], ['id' => 2, 'name' => 'Bob', 'score' => 20]]);
59        $parser = new SqliteParser();
60        $guard = new SqliteQueryGuard($parser);
61        $castRenderer = new SqliteCastRenderer();
62        $quoter = new SqliteIdentifierQuoter();
63        $selectTransformer = new SelectTransformer($castRenderer, $quoter);
64        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
65        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
66        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
67        $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
68        $schemaParser = new SqliteSchemaParser();
69        $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
70        $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
71        foreach ($commands as $sql) {
72            $native = StateComparison::query($nativeDatabase, $sql);
73            $plan = $rewriter->rewrite($sql);
74            $rows = StateComparison::query($physicalDatabase, $plan->sql());
75            if ($plan->kind() === QueryKind::READ && $rows !== $native) {
76                throw new Error('Native/rewritten result mismatch: ' . var_export([$native, $rows], true));
77            }
78            $plan->mutation()?->apply($store, $rows);
79            $rewriter->commitRewriteState();
80            StateComparison::compare($nativeDatabase, $physicalDatabase, $store);
81        }
82    }
83}
84