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