packages/ztd-query-pdo-adapter/fuzz/Correctness/Sqlite/Target/DeleteCorrectnessTarget.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 DeleteCorrectnessTarget
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->buildDelete($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 DELETE failed after native success\nSeed: $seed\nSQL: $sql", 0, $e);
91 }
92
93 if ($rawError !== null) {
94 throw new Error("ZTD DELETE 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 "DELETE 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