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