params = $params ?? []; if (str_starts_with($this->sql, 'SELECT')) { $this->pdo->selectedEmail = (string)($this->params['email'] ?? ''); return true; } if (str_starts_with($this->sql, 'INSERT') && $this->pdo->failOnInsert === $this->params['user']) { throw new RuntimeException('simulated partial failure'); } if (str_starts_with($this->sql, 'UPDATE')) { $this->pdo->updateParams = $this->params; } return true; } public function fetchColumn(int $column = 0): mixed { return $this->pdo->users[$this->pdo->selectedEmail] ?? false; } public function rowCount(): int { return $this->pdo->updateRowCount; } } final class NotificationFakePdo extends PDO { public array $users = ['one@example.com' => 1, 'two@example.com' => 2]; public ?string $selectedEmail = null; public mixed $failOnInsert = null; public int $updateRowCount = 1; public array $updateParams = []; public bool $rolledBack = false; public bool $inTxn = false; public function __construct() {} public function beginTransaction(): bool { $this->inTxn = true; return true; } public function inTransaction(): bool { return $this->inTxn; } public function rollBack(): bool { $this->rolledBack = true; $this->inTxn = false; return true; } public function commit(): bool { $this->inTxn = false; return true; } public function prepare(string $query, array $options = []): PDOStatement|false { return new NotificationFakeStatement($this, $query); } public function lastInsertId(?string $name = null): string { return '1'; } } function notification_persistence_assert(bool $condition, string $message): void { if (!$condition) throw new RuntimeException($message); } $pdo = new NotificationFakePdo(); $pdo->failOnInsert = 2; $queue = new NotificationQueue(); try { $queue->enqueue($pdo, [ 'type' => 'assignment_created', 'recipients' => ['one@example.com', 'two@example.com'], 'title' => 'Assigned', 'body' => 'Jobcard assigned', 'deduplication_key' => 'assignment:42', ]); throw new RuntimeException('Expected the simulated second-recipient failure.'); } catch (RuntimeException $error) { notification_persistence_assert($error->getMessage() === 'simulated partial failure', 'The simulated partial failure should be surfaced.'); } notification_persistence_assert($pdo->rolledBack, 'A partial recipient failure must roll back the whole queue transaction.'); notification_persistence_assert($queue->markRead($pdo, 7, ['notification_id' => '9']), 'Mark-read should update a user-owned row.'); notification_persistence_assert($pdo->updateParams === ['id' => 9, 'user' => 7], 'Mark-read must use schema-aligned id and user_id predicates.'); notification_persistence_assert($queue->markUnread($pdo, '7', ['id' => '9']), 'Mark-unread should update a user-owned row.'); notification_persistence_assert($pdo->updateParams === ['id' => 9, 'user' => 7], 'Mark-unread must use schema-aligned id and user_id predicates.'); printf("Notification persistence tests: 6 passed\n");