Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
121 / 121
100.00% covered (success)
100.00%
99 / 99
58.82% covered (warning)
58.82%
60 / 102
100.00% covered (success)
100.00%
29 / 29
CRAP
100.00% covered (success)
100.00%
1 / 1
Database
100.00% covered (success)
100.00%
121 / 121
100.00% covered (success)
100.00%
99 / 99
58.82% covered (warning)
58.82%
60 / 102
100.00% covered (success)
100.00%
29 / 29
340.09
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
8 / 8
37.50% covered (danger)
37.50%
3 / 8
100.00% covered (success)
100.00%
1 / 1
7.91
 getFetchMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connected
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPdoDriver
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSqlDirs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadScript
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
12 / 12
60.00% covered (warning)
60.00%
6 / 10
100.00% covered (success)
100.00%
1 / 1
8.30
 placeholderCompiler
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compilePlaceholders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connect
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 disconnect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
6 / 6
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
4.25
 reconnect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ping
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 reset
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
7 / 7
80.00% covered (warning)
80.00%
4 / 5
100.00% covered (success)
100.00%
1 / 1
4.13
 checkAfterFailure
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 rollBackOpenTransaction
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 markSuspect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 quote
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 begin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 commit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rollback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 transactionCall
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getConn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requirePdo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 checkReuse
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
15 / 15
21.05% covered (danger)
21.05%
8 / 38
100.00% covered (success)
100.00%
1 / 1
39.49
 drop
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 markConnected
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 touchConnection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Quma;
6
7use Celema\Quma\Exception\ConnectionFailed;
8use Closure;
9use PDO;
10use PDOException;
11use RuntimeException;
12use Throwable;
13
14/**
15 * @api
16 * @psalm-no-seal-properties
17 */
18class Database
19{
20    public readonly bool $debug;
21    protected ?PDO $pdo = null;
22    protected ?int $connectedAt = null;
23    protected ?int $lastUsedAt = null;
24    /** Whether an operation failed on the connection since the last reset. */
25    protected bool $suspect = false;
26    /** @var array<string, LoadedScript> */
27    protected array $compiledScripts = [];
28
29    public function __construct(
30        protected readonly Connection $conn,
31    ) {
32        $this->debug = Debug::enabled();
33    }
34
35    public function __get(string $key): Folder
36    {
37        Util::assertPathSegment($key, 'SQL folder name');
38
39        $exists = false;
40
41        foreach ($this->conn->config->sql as $path) {
42            $exists = is_dir($path . DIRECTORY_SEPARATOR . $key);
43
44            if ($exists) {
45                break;
46            }
47        }
48
49        if (!$exists) {
50            throw new RuntimeException('The SQL folder does not exist: ' . $key);
51        }
52
53        return new Folder($this, $key);
54    }
55
56    public function getFetchMode(): int
57    {
58        return $this->conn->config->pdo->fetchMode;
59    }
60
61    public function connected(): bool
62    {
63        return $this->pdo !== null;
64    }
65
66    public function getPdoDriver(): string
67    {
68        return $this->conn->config->driver;
69    }
70
71    /** @return list<non-empty-string> */
72    public function getSqlDirs(): array
73    {
74        return $this->conn->config->sql;
75    }
76
77    public function loadScript(string $path, bool $isTemplate): LoadedScript
78    {
79        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
80
81        if (array_key_exists($key, $this->compiledScripts)) {
82            return $this->compiledScripts[$key];
83        }
84
85        if ($isTemplate) {
86            if (!is_readable($path)) {
87                throw new RuntimeException('Could not read SQL script: ' . $path);
88            }
89
90            $script = new LoadedScript($path, $path, compile: $this->placeholderCompiler());
91            $this->compiledScripts[$key] = $script;
92
93            return $script;
94        }
95
96        $source = file_get_contents($path);
97
98        if ($source === false) {
99            throw new RuntimeException('Could not read SQL script: ' . $path);
100        }
101
102        $compiled = $this->compilePlaceholders($source, $path);
103        $script = new LoadedScript($compiled, $path);
104        $this->compiledScripts[$key] = $script;
105
106        return $script;
107    }
108
109    /** @return Closure(string, string): string */
110    private function placeholderCompiler(): Closure
111    {
112        return $this->compilePlaceholders(...);
113    }
114
115    private function compilePlaceholders(string $source, string $path): string
116    {
117        return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source;
118    }
119
120    public function connect(): static
121    {
122        if ($this->pdo !== null) {
123            return $this;
124        }
125
126        $config = $this->conn->config;
127
128        try {
129            $pdo = new PDO(
130                $config->dsn,
131                $config->pdo->username,
132                $config->pdo->password,
133                $config->pdo->effectiveOptions($config->driver),
134            );
135        } catch (PDOException $e) {
136            throw ConnectionFailed::from($e);
137        }
138
139        $this->pdo = $pdo;
140        $this->markConnected();
141
142        return $this;
143    }
144
145    public function disconnect(): void
146    {
147        if ($this->pdo !== null) {
148            try {
149                if ($this->pdo->inTransaction()) {
150                    $this->pdo->rollBack();
151                }
152            } catch (Throwable) {
153                // @mago-expect lint:no-empty-catch-clause Rollback failures are intentionally ignored during teardown.
154            }
155        }
156
157        $this->drop();
158    }
159
160    public function reconnect(): static
161    {
162        $this->disconnect();
163
164        return $this->connect();
165    }
166
167    public function ping(): bool
168    {
169        if ($this->pdo === null) {
170            return false;
171        }
172
173        try {
174            $stmt = $this->pdo->query('SELECT 1');
175
176            if ($stmt === false) {
177                return false;
178            }
179
180            $this->touchConnection();
181
182            return $stmt->fetchColumn() !== false;
183        } catch (Throwable) {
184            return false;
185        }
186    }
187
188    /**
189     * Brings the connection back to a clean state after a unit of work, such
190     * as a request in a long-running worker: an open transaction is rolled
191     * back. A connection that cannot be rolled back is dropped instead of
192     * throwing; the next statement connects anew.
193     *
194     * If an operation failed during the unit of work, the connection is
195     * pinged and dropped if it is broken. The failure may have been an
196     * ordinary SQL error or a lost connection, and MySQL does not reflect a
197     * lost connection in its transaction state, so without the ping a
198     * connection used continuously would never be replaced.
199     *
200     * Returns whether an open connection is kept for reuse.
201     */
202    public function reset(): bool
203    {
204        if ($this->pdo === null) {
205            return false;
206        }
207
208        if ($this->rollBackOpenTransaction($this->pdo) && $this->checkAfterFailure()) {
209            return true;
210        }
211
212        $this->drop();
213
214        return false;
215    }
216
217    /**
218     * Pings the connection if an operation failed on it since the last
219     * reset. Returns whether the connection can be kept.
220     */
221    private function checkAfterFailure(): bool
222    {
223        if (!$this->suspect) {
224            return true;
225        }
226
227        $this->suspect = false;
228
229        return $this->ping();
230    }
231
232    /** Returns false if the rollback failed. */
233    private function rollBackOpenTransaction(PDO $pdo): bool
234    {
235        try {
236            if ($pdo->inTransaction()) {
237                $pdo->rollBack();
238                // Only the rollback reached the server; a reset without one
239                // must not hide a long idle period from the idle check.
240                $this->touchConnection();
241            }
242
243            return true;
244        } catch (Throwable) {
245            return false;
246        }
247    }
248
249    /**
250     * Records that an operation failed on the given connection, so that the
251     * next reset() verifies it. A query may still hold a connection that was
252     * replaced since; its failures do not concern the current one.
253     *
254     * @internal
255     */
256    public function markSuspect(PDO $pdo): void
257    {
258        if ($pdo === $this->pdo) {
259            $this->suspect = true;
260        }
261    }
262
263    public function quote(string $value): string
264    {
265        $quoted = $this->requirePdo()->quote($value);
266
267        if ($quoted === false) {
268            throw new RuntimeException("PDO driver '{$this->conn->config->driver}' cannot quote values");
269        }
270
271        return $quoted;
272    }
273
274    public function begin(): bool
275    {
276        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->beginTransaction());
277    }
278
279    public function commit(): bool
280    {
281        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->commit());
282    }
283
284    public function rollback(): bool
285    {
286        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->rollBack());
287    }
288
289    /** @param Closure(PDO): bool $call */
290    private function transactionCall(Closure $call): bool
291    {
292        $pdo = $this->requirePdo();
293
294        try {
295            return $call($pdo);
296        } catch (PDOException $e) {
297            $this->markSuspect($pdo);
298
299            throw $e;
300        }
301    }
302
303    public function getConn(): PDO
304    {
305        return $this->requirePdo();
306    }
307
308    protected function requirePdo(): PDO
309    {
310        $this->checkReuse();
311        $this->connect();
312        $pdo = $this->pdo;
313
314        if ($pdo !== null) {
315            $this->touchConnection();
316
317            return $pdo;
318        }
319
320        throw new RuntimeException('Database connection not initialized');
321    }
322
323    /**
324     * Before an existing connection is used after a pause, replace it if it
325     * reached the maximum age, or ping it if it was idle long enough and drop
326     * it if it is broken. Never inside a transaction: a new connection would
327     * silently lose the transaction's work, so a statement on a broken
328     * connection fails instead. Statements themselves are never retried.
329     */
330    protected function checkReuse(): void
331    {
332        if ($this->pdo === null || $this->pdo->inTransaction()) {
333            return;
334        }
335
336        $config = $this->conn->config;
337        $now = time();
338
339        if ($config->maxConnectionAge > 0 && ($now - ($this->connectedAt ?? $now)) >= $config->maxConnectionAge) {
340            $this->drop();
341
342            return;
343        }
344
345        if (
346            $config->pingAfterIdle > 0
347            && ($now - ($this->lastUsedAt ?? $now)) >= $config->pingAfterIdle
348            && !$this->ping()
349        ) {
350            $this->drop();
351        }
352    }
353
354    protected function drop(): void
355    {
356        $this->pdo = null;
357        $this->connectedAt = null;
358        $this->lastUsedAt = null;
359        $this->suspect = false;
360    }
361
362    protected function markConnected(): void
363    {
364        $now = time();
365        $this->connectedAt = $now;
366        $this->lastUsedAt = $now;
367    }
368
369    protected function touchConnection(): void
370    {
371        if ($this->pdo !== null) {
372            $this->lastUsedAt = time();
373        }
374    }
375
376    public function execute(string $query, mixed ...$args): Query
377    {
378        return new Query($this, $query, new Args($args), null);
379    }
380}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Database->__construct
30        protected readonly Connection $conn,
31    ) {
32        $this->debug = Debug::enabled();
33    }
Database->__get
35    public function __get(string $key): Folder
36    {
37        Util::assertPathSegment($key, 'SQL folder name');
38
39        $exists = false;
40
41        foreach ($this->conn->config->sql as $path) {
41        foreach ($this->conn->config->sql as $path) {
42            $exists = is_dir($path . DIRECTORY_SEPARATOR . $key);
43
44            if ($exists) {
45                break;
41        foreach ($this->conn->config->sql as $path) {
41        foreach ($this->conn->config->sql as $path) {
42            $exists = is_dir($path . DIRECTORY_SEPARATOR . $key);
43
44            if ($exists) {
45                break;
46            }
47        }
48
49        if (!$exists) {
50            throw new RuntimeException('The SQL folder does not exist: ' . $key);
53        return new Folder($this, $key);
54    }
Database->begin
276        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->beginTransaction());
277    }
Database->checkAfterFailure
223        if (!$this->suspect) {
224            return true;
227        $this->suspect = false;
228
229        return $this->ping();
230    }
Database->checkReuse
332        if ($this->pdo === null || $this->pdo->inTransaction()) {
332        if ($this->pdo === null || $this->pdo->inTransaction()) {
332        if ($this->pdo === null || $this->pdo->inTransaction()) {
333            return;
336        $config = $this->conn->config;
337        $now = time();
338
339        if ($config->maxConnectionAge > 0 && ($now - ($this->connectedAt ?? $now)) >= $config->maxConnectionAge) {
339        if ($config->maxConnectionAge > 0 && ($now - ($this->connectedAt ?? $now)) >= $config->maxConnectionAge) {
339        if ($config->maxConnectionAge > 0 && ($now - ($this->connectedAt ?? $now)) >= $config->maxConnectionAge) {
340            $this->drop();
341
342            return;
346            $config->pingAfterIdle > 0
347            && ($now - ($this->lastUsedAt ?? $now)) >= $config->pingAfterIdle
347            && ($now - ($this->lastUsedAt ?? $now)) >= $config->pingAfterIdle
348            && !$this->ping()
348            && !$this->ping()
350            $this->drop();
351        }
352    }
352    }
Database->commit
281        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->commit());
282    }
Database->compilePlaceholders
115    private function compilePlaceholders(string $source, string $path): string
116    {
117        return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source;
118    }
Database->connect
122        if ($this->pdo !== null) {
123            return $this;
126        $config = $this->conn->config;
127
128        try {
129            $pdo = new PDO(
135        } catch (PDOException $e) {
136            throw ConnectionFailed::from($e);
139        $this->pdo = $pdo;
140        $this->markConnected();
141
142        return $this;
143    }
Database->connected
63        return $this->pdo !== null;
64    }
Database->disconnect
147        if ($this->pdo !== null) {
148            try {
149                if ($this->pdo->inTransaction()) {
150                    $this->pdo->rollBack();
150                    $this->pdo->rollBack();
152            } catch (Throwable) {
157        $this->drop();
158    }
Database->drop
356        $this->pdo = null;
357        $this->connectedAt = null;
358        $this->lastUsedAt = null;
359        $this->suspect = false;
360    }
Database->execute
376    public function execute(string $query, mixed ...$args): Query
377    {
378        return new Query($this, $query, new Args($args), null);
379    }
Database->getConn
305        return $this->requirePdo();
306    }
Database->getFetchMode
58        return $this->conn->config->pdo->fetchMode;
59    }
Database->getPdoDriver
68        return $this->conn->config->driver;
69    }
Database->getSqlDirs
74        return $this->conn->config->sql;
75    }
Database->loadScript
77    public function loadScript(string $path, bool $isTemplate): LoadedScript
78    {
79        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
79        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
79        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
79        $key = ($isTemplate ? 'tpql:' : 'sql:') . $path;
80
81        if (array_key_exists($key, $this->compiledScripts)) {
82            return $this->compiledScripts[$key];
85        if ($isTemplate) {
86            if (!is_readable($path)) {
87                throw new RuntimeException('Could not read SQL script: ' . $path);
90            $script = new LoadedScript($path, $path, compile: $this->placeholderCompiler());
91            $this->compiledScripts[$key] = $script;
92
93            return $script;
96        $source = file_get_contents($path);
97
98        if ($source === false) {
99            throw new RuntimeException('Could not read SQL script: ' . $path);
102        $compiled = $this->compilePlaceholders($source, $path);
103        $script = new LoadedScript($compiled, $path);
104        $this->compiledScripts[$key] = $script;
105
106        return $script;
107    }
Database->markConnected
364        $now = time();
365        $this->connectedAt = $now;
366        $this->lastUsedAt = $now;
367    }
Database->markSuspect
256    public function markSuspect(PDO $pdo): void
257    {
258        if ($pdo === $this->pdo) {
259            $this->suspect = true;
260        }
261    }
261    }
Database->ping
169        if ($this->pdo === null) {
170            return false;
173        try {
174            $stmt = $this->pdo->query('SELECT 1');
175
176            if ($stmt === false) {
177                return false;
180            $this->touchConnection();
181
182            return $stmt->fetchColumn() !== false;
183        } catch (Throwable) {
184            return false;
185        }
186    }
Database->placeholderCompiler
112        return $this->compilePlaceholders(...);
113    }
Database->quote
263    public function quote(string $value): string
264    {
265        $quoted = $this->requirePdo()->quote($value);
266
267        if ($quoted === false) {
268            throw new RuntimeException("PDO driver '{$this->conn->config->driver}' cannot quote values");
271        return $quoted;
272    }
Database->reconnect
162        $this->disconnect();
163
164        return $this->connect();
165    }
Database->requirePdo
310        $this->checkReuse();
311        $this->connect();
312        $pdo = $this->pdo;
313
314        if ($pdo !== null) {
315            $this->touchConnection();
316
317            return $pdo;
320        throw new RuntimeException('Database connection not initialized');
321    }
Database->reset
204        if ($this->pdo === null) {
205            return false;
208        if ($this->rollBackOpenTransaction($this->pdo) && $this->checkAfterFailure()) {
208        if ($this->rollBackOpenTransaction($this->pdo) && $this->checkAfterFailure()) {
208        if ($this->rollBackOpenTransaction($this->pdo) && $this->checkAfterFailure()) {
209            return true;
212        $this->drop();
213
214        return false;
215    }
Database->rollBackOpenTransaction
233    private function rollBackOpenTransaction(PDO $pdo): bool
234    {
235        try {
236            if ($pdo->inTransaction()) {
237                $pdo->rollBack();
238                // Only the rollback reached the server; a reset without one
239                // must not hide a long idle period from the idle check.
240                $this->touchConnection();
241            }
242
243            return true;
243            return true;
244        } catch (Throwable) {
245            return false;
246        }
247    }
Database->rollback
286        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->rollBack());
287    }
Database->touchConnection
371        if ($this->pdo !== null) {
372            $this->lastUsedAt = time();
373        }
374    }
374    }
Database->transactionCall
290    private function transactionCall(Closure $call): bool
291    {
292        $pdo = $this->requirePdo();
293
294        try {
295            return $call($pdo);
296        } catch (PDOException $e) {
297            $this->markSuspect($pdo);
298
299            throw $e;
300        }
301    }
compilePlaceholders
115    private function compilePlaceholders(string $source, string $path): string
116    {
117        return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source;
118    }
{closure:/workspace/celema/quma/src/Database.php:276-276}
276        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->beginTransaction());
{closure:/workspace/celema/quma/src/Database.php:281-281}
281        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->commit());
{closure:/workspace/celema/quma/src/Database.php:286-286}
286        return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->rollBack());