packages/ztd-query-pdo-adapter/fuzz/Correctness/Postgres/Target/UpdateCorrectnessTarget.php
1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Correctness\Postgres\Target;
6
7use Error;
8use Faker\Generator;
9use Fuzz\Correctness\Postgres\PgCorrectnessHarness;
10use Fuzz\Correctness\Postgres\PgSchemaAwareSqlBuilder;
11use Fuzz\Correctness\Postgres\PgSchemaPool;
12use Fuzz\Correctness\ResultComparator;
13use Fuzz\Correctness\SchemaDefinition;
14use PDO;
15use PDOException;
16use ZtdQuery\Connection\Exception\DatabaseException;
17use ZtdQuery\Exception\UnknownSchemaException;
18use ZtdQuery\Exception\UnsupportedSqlException;
19
20/**
21 * @phpstan-import-type Row from \Fuzz\Correctness\CorrectnessHarness
22 */
23final class UpdateCorrectnessTarget
24{
25 private PgCorrectnessHarness $harness;
26 private ResultComparator $comparator;
27 private PgSchemaAwareSqlBuilder $sqlBuilder;
28 private Generator $faker;
29
30 /**
31 * Binds the instance to what it will work from.
32 *
33 * @param PgCorrectnessHarness $harness
34 * @param PgSchemaAwareSqlBuilder $sqlBuilder
35 * @param Generator $faker
36 */
37 public function __construct(
38 PgCorrectnessHarness $harness,
39 PgSchemaAwareSqlBuilder $sqlBuilder,
40 Generator $faker
41 ) {
42 $this->harness = $harness;
43 $this->comparator = new ResultComparator();
44 $this->sqlBuilder = $sqlBuilder;
45 $this->faker = $faker;
46 }
47
48 /**
49 * @throws Error
50 */
51 public function __invoke(string $input): void
52 {
53 $seed = crc32(str_pad($input, 4, "\0"));
54 $this->faker->seed($seed);
55
56 $schema = PgSchemaPool::random($this->faker);
57 $this->harness->setup($schema, $seed);
58
59 try {
60 $sql = $this->sqlBuilder->buildUpdate($schema);
61
62 $rawError = null;
63 try {
64 $this->harness->getRawPdo()->exec($sql);
65 } catch (PDOException $e) {
66 $rawError = $e;
67 }
68
69 try {
70 $snapshot = \Fuzz\Correctness\PhysicalTableSnapshot::capture($this->harness->getRawPdo(), $schema->name);
71 try {
72 $this->harness->getZtdPdo()->exec($sql);
73 } finally {
74 \Fuzz\Correctness\PhysicalTableSnapshot::assertUnchanged($this->harness->getRawPdo(), $schema->name, $snapshot, $sql, $seed);
75 }
76 } catch (UnsupportedSqlException | UnknownSchemaException | DatabaseException | PDOException $e) {
77 if ($schema->primaryKeys === []) {
78 for ($cause = $e; $cause !== null; $cause = $cause->getPrevious()) {
79 if ($cause instanceof \ZtdQuery\Exception\MissingPrimaryKeyException) {
80 return;
81 }
82 }
83 }
84 if ($rawError !== null) {
85 return;
86 }
87 throw new Error("ZTD UPDATE failed after native success\nSeed: $seed\nSQL: $sql", 0, $e);
88 }
89
90 if ($rawError !== null) {
91 throw new Error("ZTD UPDATE accepted a native-rejected query\nSeed: $seed\nSQL: $sql\n" . $rawError->getMessage(), 0, $rawError);
92 }
93
94 $this->compareTableState($schema, $seed);
95 } finally {
96 $this->harness->teardown();
97 }
98 }
99
100 /**
101 * Reads the table on both sides and fails if they disagree.
102 *
103 * @param SchemaDefinition $schema The schema
104 * @param int $seed The seed
105 *
106 * @throws Error
107 */
108 public function compareTableState(SchemaDefinition $schema, int $seed): void
109 {
110 $rawRows = $this->fetchAll($this->harness->getRawPdo(), $schema->name);
111
112 $selectSql = sprintf('SELECT * FROM "%s"', str_replace('"', '""', $schema->name));
113 $stmt = $this->harness->getZtdPdo()->query($selectSql);
114 /** @var list<Row> $ztdRows */
115 $ztdRows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
116
117 if (!$this->comparator->compareRows($rawRows, $ztdRows, $schema->primaryKeys, $schema->columnTypes)) {
118 throw new Error(
119 "UPDATE table state mismatch\n" .
120 "Seed: $seed\n" .
121 "Schema: {$schema->name}\n" .
122 'Raw row count: ' . count($rawRows) . "\n" .
123 'ZTD row count: ' . count($ztdRows)
124 );
125 }
126 }
127
128 /**
129 * Answers every row the connection reads.
130 *
131 * @param PDO $pdo The pdo
132 * @param string $table Table it belongs to
133 *
134 * @return list<Row> What it answers
135 */
136 public function fetchAll(PDO $pdo, string $table): array
137 {
138 $stmt = $pdo->query(sprintf('SELECT * FROM "%s"', str_replace('"', '""', $table)));
139 /** @var list<Row> $rows */
140 $rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
141 return $rows;
142 }
143}
144