> */ public function results(string $table, string $sql, ...$params): array; /** * @param literal-string $sql SQL whose table is the `%i` placeholder. * @param mixed ...$params * @return array|null */ public function row(string $table, string $sql, ...$params): ?array; /** * @param array $data */ public function insert(string $table, array $data): void; /** * @param array $data * @param array $where */ public function update(string $table, array $data, array $where): void; /** * Remove rows. * * Spelled as SQL rather than as `$wpdb->delete()`'s equality-only `$where` * array because retention pruning is a RANGE — `WHERE id < %s` — which * that array cannot express at all. Taking the SQL keeps one method where * the alternative was two, and keeps the `literal-string` discipline every * other read here has: an injected column name stays unexpressible. * * **It is not `query()` wearing a narrower name.** Implementations must * refuse SQL that does not begin `DELETE FROM %i`, so the one operation * this interface deliberately cannot express — an `UPDATE` against a table * that has no update path — cannot be smuggled through it. * * @param literal-string $sql SQL beginning `DELETE FROM %i`. * @param mixed ...$params * @return int Rows removed. */ public function delete(string $table, string $sql, ...$params): int; /** * Insert a row, or increment the one already there — one statement, * atomic at the database. * * **This is the whole of `wconvert_stats`' write path**, and it is a * method rather than a pair of calls because the pair is the bug. Reading * a count in PHP and writing it back is a read-modify-write with no row * lock: two beacons arriving in the same instant both read the same number * and both write the same successor, and one Conversion is gone. That race * is exactly the one ADR 0008 accepted for Destination health *because * health is advisory* — a conversion count is not, and the race eats the * most increments on the busiest sites, which is where the numbers matter * most (ADR 0019). * * `INSERT ... ON DUPLICATE KEY UPDATE` says it in one statement, so the * database holds the row lock for the whole of it and no increment can be * lost. That is a claim about MySQL rather than about this codebase, so it * is proven against MySQL: `bin/verify-stats.php` fires two of these * concurrently on two connections and asserts the count is two. * * **It is not `query()` wearing a narrower name.** Implementations must * refuse SQL that does not begin `INSERT INTO %i`, so the `UPDATE` this * interface deliberately cannot express against a table with no update * path stays unexpressible — the `ON DUPLICATE KEY UPDATE` clause can only * ever touch the row the same statement tried to insert. * * @param literal-string $sql SQL beginning `INSERT INTO %i`. * @param mixed ...$params */ public function upsert(string $table, string $sql, ...$params): void; }