Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
121 / 121 |
|
100.00% |
99 / 99 |
|
58.82% |
60 / 102 |
|
100.00% |
29 / 29 |
CRAP | |
100.00% |
1 / 1 |
| Database | |
100.00% |
121 / 121 |
|
100.00% |
99 / 99 |
|
58.82% |
60 / 102 |
|
100.00% |
29 / 29 |
340.09 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| __get | |
100.00% |
9 / 9 |
|
100.00% |
8 / 8 |
|
37.50% |
3 / 8 |
|
100.00% |
1 / 1 |
7.91 | |||
| getFetchMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| connected | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getPdoDriver | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getSqlDirs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| loadScript | |
100.00% |
16 / 16 |
|
100.00% |
12 / 12 |
|
60.00% |
6 / 10 |
|
100.00% |
1 / 1 |
8.30 | |||
| placeholderCompiler | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| compilePlaceholders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| connect | |
100.00% |
14 / 14 |
|
100.00% |
6 / 6 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| disconnect | |
100.00% |
5 / 5 |
|
100.00% |
6 / 6 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
4.25 | |||
| reconnect | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| ping | |
100.00% |
9 / 9 |
|
100.00% |
7 / 7 |
|
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| reset | |
100.00% |
6 / 6 |
|
100.00% |
7 / 7 |
|
80.00% |
4 / 5 |
|
100.00% |
1 / 1 |
4.13 | |||
| checkAfterFailure | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| rollBackOpenTransaction | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| markSuspect | |
100.00% |
2 / 2 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| quote | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| begin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| commit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| rollback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| transactionCall | |
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| getConn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| requirePdo | |
100.00% |
7 / 7 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| checkReuse | |
100.00% |
11 / 11 |
|
100.00% |
15 / 15 |
|
21.05% |
8 / 38 |
|
100.00% |
1 / 1 |
39.49 | |||
| drop | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| markConnected | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| touchConnection | |
100.00% |
2 / 2 |
|
100.00% |
3 / 3 |
|
50.00% |
1 / 2 |
|
100.00% |
1 / 1 |
2.50 | |||
| execute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Quma; |
| 6 | |
| 7 | use Celema\Quma\Exception\ConnectionFailed; |
| 8 | use Closure; |
| 9 | use PDO; |
| 10 | use PDOException; |
| 11 | use RuntimeException; |
| 12 | use Throwable; |
| 13 | |
| 14 | /** |
| 15 | * @api |
| 16 | * @psalm-no-seal-properties |
| 17 | */ |
| 18 | class 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 | } |
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.
| 30 | protected readonly Connection $conn, |
| 31 | ) { |
| 32 | $this->debug = Debug::enabled(); |
| 33 | } |
| 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 | } |
| 276 | return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->beginTransaction()); |
| 277 | } |
| 223 | if (!$this->suspect) { |
| 224 | return true; |
| 227 | $this->suspect = false; |
| 228 | |
| 229 | return $this->ping(); |
| 230 | } |
| 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 | } |
| 281 | return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->commit()); |
| 282 | } |
| 115 | private function compilePlaceholders(string $source, string $path): string |
| 116 | { |
| 117 | return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source; |
| 118 | } |
| 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 | } |
| 63 | return $this->pdo !== null; |
| 64 | } |
| 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 | } |
| 356 | $this->pdo = null; |
| 357 | $this->connectedAt = null; |
| 358 | $this->lastUsedAt = null; |
| 359 | $this->suspect = false; |
| 360 | } |
| 376 | public function execute(string $query, mixed ...$args): Query |
| 377 | { |
| 378 | return new Query($this, $query, new Args($args), null); |
| 379 | } |
| 305 | return $this->requirePdo(); |
| 306 | } |
| 58 | return $this->conn->config->pdo->fetchMode; |
| 59 | } |
| 68 | return $this->conn->config->driver; |
| 69 | } |
| 74 | return $this->conn->config->sql; |
| 75 | } |
| 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 | } |
| 364 | $now = time(); |
| 365 | $this->connectedAt = $now; |
| 366 | $this->lastUsedAt = $now; |
| 367 | } |
| 256 | public function markSuspect(PDO $pdo): void |
| 257 | { |
| 258 | if ($pdo === $this->pdo) { |
| 259 | $this->suspect = true; |
| 260 | } |
| 261 | } |
| 261 | } |
| 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 | } |
| 112 | return $this->compilePlaceholders(...); |
| 113 | } |
| 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 | } |
| 162 | $this->disconnect(); |
| 163 | |
| 164 | return $this->connect(); |
| 165 | } |
| 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 | } |
| 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 | } |
| 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 | } |
| 286 | return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->rollBack()); |
| 287 | } |
| 371 | if ($this->pdo !== null) { |
| 372 | $this->lastUsedAt = time(); |
| 373 | } |
| 374 | } |
| 374 | } |
| 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 | } |
| 115 | private function compilePlaceholders(string $source, string $path): string |
| 116 | { |
| 117 | return $this->conn->config->placeholders?->compileSql($source, $path) ?? $source; |
| 118 | } |
| 276 | return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->beginTransaction()); |
| 281 | return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->commit()); |
| 286 | return $this->transactionCall(static fn(PDO $pdo): bool => $pdo->rollBack()); |