Lines 100.00% 10 / 10
Functions and Methods 100.00% 2 / 2
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
SqliteLock 100.00% 10 / 10 100.00% 2 / 2 3 100.00% 1 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 withLock 100.00% 9 / 9 100.00% 1 / 1 2
1<?php
2
3declare(strict_types=1);
4
5namespace BlueprintAU\Radiant\Database\Locks;
6
7use BlueprintAU\Radiant\Database\Connections\SqliteConnection;
8use BlueprintAU\Radiant\Database\Exceptions\ConnectionException;
9
10/**
11 * SQLite lock: a write transaction.
12 *
13 * SQLite serializes writers database-wide; a writer holds the RESERVED
14 * lock for its transaction's duration, so concurrent workers block
15 * instead of racing. The transaction is opened through the connection's
16 * public API (`PDO::beginTransaction()`), which on pdo_sqlite issues a
17 * DEFERRED `BEGIN` — the RESERVED lock is taken on the first write, not
18 * at begin. A plan→apply flow writes early, and any concurrent writer
19 * blocks the whole transaction anyway, so the serialization holds; the
20 * up-front acquire of a literal `BEGIN IMMEDIATE` is not relied on.
21 *
22 * Unlike the SQL-statement adapters, this is NOT an advisory lock — it is
23 * a write transaction, so it serializes *all* database mutation for its
24 * duration. It also cannot nest: entering with a transaction already open
25 * degrades to a savepoint, which takes no RESERVED lock and voids the
26 * cross-process guarantee — so that case fails loudly instead.
27 */
28final class SqliteLock implements Lock
29{
30    /**
31     * @param  SqliteConnection  $connection
32     */
33    public function __construct(protected readonly SqliteConnection $connection) {}
34
35    /**
36     * Run the callback inside a write transaction.
37     *
38     * @template TReturn
39     *
40     * @param  callable(): TReturn  $callback
41     * @param  string  $name  Accepted for signature parity and ignored.
42     * @return TReturn
43     * @throws ConnectionException
44     * @throws \Throwable
45     */
46    #[\Override]
47    public function withLock(callable $callback, string $name): mixed
48    {
49        if ($this->connection->transactionLevel() > 0) {
50            throw new ConnectionException(
51                'SqliteLock requires a transaction-free connection: an open transaction '
52                . 'would nest as a savepoint, taking no RESERVED lock and serializing nothing. '
53                . 'Commit or roll back the outer transaction before acquiring the lock.',
54            );
55        }
56
57        return $this->connection->transaction(function () use ($callback): mixed {
58            return $callback();
59        });
60    }
61}