packages/ztd-query-pdo-adapter/fuzz/Correctness/Sqlite/Target/UpdateCorrectnessTarget.php

1<?php
2
3declare(strict_types=1);
4
5namespace Fuzz\Correctness\Sqlite\Target;
6
7use Error;
8use Faker\Generator;
9use Fuzz\Correctness\ResultComparator;
10use Fuzz\Correctness\SchemaDefinition;
11use Fuzz\Correctness\Sqlite\SqliteCorrectnessHarness;
12use Fuzz\Correctness\Sqlite\SqliteSchemaAwareSqlBuilder;
13use Fuzz\Correctness\Sqlite\SqliteSchemaPool;
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 SqliteCorrectnessHarness $harness;
26    private ResultComparator $comparator;
27    private SqliteSchemaAwareSqlBuilder $sqlBuilder;
28    private Generator $faker;
29
30    /**
31     * Binds the instance to what it will work from.
32     *
33     * @param SqliteCorrectnessHarness $harness
34     * @param SqliteSchemaAwareSqlBuilder $sqlBuilder
35     * @param Generator $faker
36     */
37    public function __construct(
38        SqliteCorrectnessHarness $harness,
39        SqliteSchemaAwareSqlBuilder $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 = SqliteSchemaPool::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 ($schema->primaryKeys === [] && $e->getPrevious()?->getPrevious() instanceof \ZtdQuery\Exception\MissingPrimaryKeyException) {
85                    return;
86                }
87                if ($rawError !== null) {
88                    return;
89                }
90                throw new Error("ZTD UPDATE failed after native success\nSeed: $seed\nSQL: $sql", 0, $e);
91            }
92
93            if ($rawError !== null) {
94                throw new Error("ZTD UPDATE accepted a native-rejected query\nSeed: $seed\nSQL: $sql\n" . $rawError->getMessage(), 0, $rawError);
95            }
96
97            $this->compareTableState($schema, $seed);
98        } finally {
99            $this->harness->teardown();
100        }
101    }
102
103    /**
104     * Reads the table on both sides and fails if they disagree.
105     *
106     * @param SchemaDefinition $schema The schema
107     * @param int $seed The seed
108     *
109     * @throws Error
110     */
111    public function compareTableState(SchemaDefinition $schema, int $seed): void
112    {
113        $rawRows = $this->fetchAll($this->harness->getRawPdo(), $schema->name);
114
115        $selectSql = sprintf('SELECT * FROM "%s"', str_replace('"', '""', $schema->name));
116        $stmt = $this->harness->getZtdPdo()->query($selectSql);
117        /** @var list<Row> $ztdRows */
118        $ztdRows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
119
120        if (!$this->comparator->compareRows($rawRows, $ztdRows, $schema->primaryKeys)) {
121            throw new Error(
122                "UPDATE table state mismatch\n" .
123                "Seed: $seed\n" .
124                "Schema: {$schema->name}\n" .
125                'Raw row count: ' . count($rawRows) . "\n" .
126                'ZTD row count: ' . count($ztdRows)
127            );
128        }
129    }
130
131    /**
132     * Answers every row the connection reads.
133     *
134     * @param PDO $pdo The pdo
135     * @param string $table Table it belongs to
136     *
137     * @return list<Row> What it answers
138     */
139    public function fetchAll(PDO $pdo, string $table): array
140    {
141        $stmt = $pdo->query(sprintf('SELECT * FROM "%s"', str_replace('"', '""', $table)));
142        /** @var list<Row> $rows */
143        $rows = $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
144        return $rows;
145    }
146}
147