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

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Semantics;
6
7use Error;
8use PDO;
9use ZtdQuery\Shadow\ShadowStore;
10
11/**
12 * Checks native results, shadow rows and physical isolation for a command sequence.
13 */
14final class StateComparison
15{
16    /**
17     * Executes a query while treating connection failures as campaign failures.
18     *
19     * @return list<array<string, mixed>>
20     * @throws Error
21     */
22    public static function query(PDO $connection, string $sql): array
23    {
24        $statement = $connection->query($sql);
25        if ($statement === false) {
26            throw new Error('SQLite query failed: ' . $sql);
27        }
28
29        $rows = [];
30        while (($row = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
31            if (!is_array($row)) {
32                throw new Error('SQLite returned a non-associative row');
33            }
34            $columns = [];
35            foreach ($row as $column => $value) {
36                if (!is_string($column)) {
37                    throw new Error('SQLite returned an unnamed result column');
38                }
39                $columns[$column] = $value;
40            }
41            $rows[] = $columns;
42        }
43
44        return $rows;
45    }
46
47    /**
48     * Checks row values and physical isolation after every command, including no-op writes.
49     *
50     * @throws Error
51     */
52    public static function compare(PDO $nativeDatabase, PDO $physicalDatabase, ShadowStore $store): void
53    {
54        $native = self::query($nativeDatabase, 'SELECT id, name, score FROM users ORDER BY id');
55        $shadow = $store->get('users');
56        array_multisort(array_column($shadow, 'id'), SORT_ASC, SORT_NUMERIC, $shadow);
57        $native = array_map(static function (array $row): array {
58            ksort($row);
59            return $row;
60        }, $native);
61        $shadow = array_map(static function (array $row): array {
62            ksort($row);
63            return $row;
64        }, $shadow);
65        if ($native !== $shadow) {
66            throw new Error('Native/shadow state mismatch: ' . var_export([$native, $shadow], true));
67        }
68        $physical = self::query($physicalDatabase, 'SELECT id, name, score FROM users');
69        if ($physical !== [['id' => 9000, 'name' => 'physical', 'score' => 777]]) {
70            throw new Error('Physical database was modified: ' . var_export($physical, true));
71        }
72    }
73}
74