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