Conversation
BREAKING CHANGES: - Removed 4-class connection hierarchy (AbstractConnection + MySQL/PG/SQLite) → Replaced with unified Connection class using match() dispatch - Removed 5-class DSN builder hierarchy (Abstract + 3 builders + Factory) → Replaced with DsnConfig immutable value object - Removed ConnectionFactory → Replaced with ConnectionManager (read/write splitting, lazy connections) - Removed DatabaseType enum → replaced with DatabaseDriver enum - Removed Helpers.php and ConnectionHelper.php NEW FEATURES: - ConnectionManager: read/write splitting, sticky-after-write, replica strategies - LazyConnection: proxy that defers PDO construction until first use - ConnectionPool: in-memory pool with health monitoring, idle eviction - 18 structured exception hierarchy with ErrorClassifier - SchemaIntrospector: centralized table/column/FK discovery with static caching - PHP 8.4: property hooks, asymmetric visibility, readonly classes TESTS: - 163 tests, 361 assertions — all green - Covers all 6 components: Types, Config, Connection, Contracts, Exceptions, Support
- Full documentation of ConnectionManager, read/write splitting, lazy connections - Connection pooling configuration and stats - Typed exception hierarchy with usage examples - Schema introspection API - DI container setup guide - v1 → v2 migration table
- basic_usage.php: ConnectionManager, CRUD, transactions, typed exceptions, lazy connections, property hooks, pool stats - advanced_usage.php: config objects, multi-connection, schema introspection, health checks, full exception hierarchy, lazy lifecycle, read/write splitting - Both examples are runnable and tested against SQLite :memory: - Added queryCount/uptimeSeconds proxy hooks to LazyConnection
PHP 8.4 interface property hooks make these observability properties part of the contract, resolving IDE lint warnings on ConnectionInterface references.
There was a problem hiding this comment.
Pull request overview
This PR appears to be a v2 (“2.x dev”) refactor of the database package, replacing the previous per-driver connection + DSN-builder + factory approach with a unified configuration model, a single Connection implementation, and a ConnectionManager that supports lazy initialization, read/write splitting, and pooling. It also reorganizes and expands the unit test suite and updates package metadata to match the new architecture.
Changes:
- Introduces new typed config/value objects (DSN/pool/read replicas) and new enums for driver/isolation/replica strategy.
- Adds new connection primitives: unified
Connection,LazyConnection,ConnectionManager, and an in-memoryConnectionPool. - Adds new support utilities (error classification, health checking, schema introspection) and rewrites tests into
tests/Unitwhile removing legacy tests.
Reviewed changes
Copilot reviewed 75 out of 76 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Unit/Types/ReadReplicaStrategyTest.php | Adds unit tests for read-replica strategy enum values/labels. |
| tests/Unit/Types/IsolationLevelTest.php | Adds unit tests for isolation level enum values/labels and enum construction. |
| tests/Unit/Types/DatabaseDriverTest.php | Adds unit tests for new DatabaseDriver enum helpers and alias parsing. |
| tests/Unit/Support/ErrorClassifierTest.php | Adds unit tests for PDOException → typed exception classification. |
| tests/Unit/Support/ConnectionPoolStatsTest.php | Adds unit tests for pool stats snapshot helpers. |
| tests/Unit/Exceptions/ExceptionHierarchyTest.php | Adds comprehensive tests for new exception hierarchy and helper constructors. |
| tests/Unit/Connection/LazyConnectionTest.php | Adds tests for lazy-connection proxy behavior and lifecycle resets. |
| tests/Unit/Connection/ConnectionTest.php | Adds tests for unified connection lifecycle, execution, transactions, and SQLite defaults. |
| tests/Unit/Connection/ConnectionPoolTest.php | Adds tests for acquire/release, exhaustion, rollback-on-release, drain, and stats. |
| tests/Unit/Connection/ConnectionManagerTest.php | Adds tests for manager defaults, named connections, disconnect/purge, stats, and sticky flags. |
| tests/Unit/Config/PoolConfigTest.php | Adds tests for pool config defaults and array parsing. |
| tests/Unit/Config/DsnConfigTest.php | Adds tests for DSN building across MySQL/PostgreSQL/SQLite and array parsing. |
| tests/Unit/Config/DatabaseConfigTest.php | Adds tests for config parsing, PDO option merging, pool parsing, timezone defaults. |
| tests/SQLiteConnectionTest.php | Removes legacy SQLite connection tests tied to old class structure. |
| tests/PostgreSQLConnectionTest.php | Removes legacy PostgreSQL connection tests tied to old class structure. |
| tests/MySQLConnectionTest.php | Removes legacy MySQL connection tests tied to old class structure. |
| tests/DSN/SQLiteDsnBuilderTest.php | Removes legacy DSN builder tests (replaced by DsnConfig). |
| tests/DSN/MySQLDsnBuilderTest.php | Removes legacy DSN builder tests (replaced by DsnConfig). |
| tests/ConnectionFactoryTest.php | Removes legacy ConnectionFactory tests (replaced by ConnectionManager). |
| tests/Cache/CacheManagerBridgeTest.php | Removes cache bridge tests (cache is now optional/suggested and excluded from PHPUnit source). |
| src/Types/ReadReplicaStrategy.php | Adds backed enum for replica selection strategies with labels. |
| src/Types/IsolationLevel.php | Adds backed enum for transaction isolation levels with labels. |
| src/Types/DatabaseType.php | Removes legacy DatabaseType enum (superseded by DatabaseDriver + configs). |
| src/Types/DatabaseDriver.php | Adds new DatabaseDriver enum with aliases, defaults, detection helpers. |
| src/Support/SchemaIntrospector.php | Adds schema introspection utilities with static caching for tables/columns/FKs/indexes. |
| src/Support/Helpers.php | Removes legacy global helper file previously autoloaded. |
| src/Support/HealthCheckResult.php | Adds immutable health check result value object. |
| src/Support/HealthChecker.php | Adds reusable health check logic for connections. |
| src/Support/ErrorClassifier.php | Adds centralized PDOException classification into typed exceptions. |
| src/Support/ConnectionPoolStats.php | Adds immutable pool stats snapshot with helper methods. |
| src/Support/ConnectionHelper.php | Removes legacy PDO creation helper with host fallback. |
| src/SQLite/Connection.php | Removes legacy SQLite-specific connection class (replaced by unified Connection). |
| src/PostgreSQL/Connection.php | Removes legacy PostgreSQL-specific connection class (replaced by unified Connection). |
| src/MySQL/Connection.php | Removes legacy MySQL-specific connection class (replaced by unified Connection). |
| src/Factory/ConnectionFactory.php | Removes legacy factory entry point (replaced by ConnectionManager). |
| src/Exceptions/TransactionException.php | Adds typed transaction exception with operation/nesting context. |
| src/Exceptions/TableNotFoundException.php | Adds typed schema exception for missing tables. |
| src/Exceptions/SyntaxException.php | Adds typed query exception for syntax errors with non-retryable semantics. |
| src/Exceptions/SchemaException.php | Adds base schema exception type. |
| src/Exceptions/QueryException.php | Adds base query exception type including SQL/params and debug SQL helper. |
| src/Exceptions/PoolException.php | Adds pool exception type with helper constructors. |
| src/Exceptions/LockTimeoutException.php | Adds typed lock-timeout exception with retryable flag. |
| src/Exceptions/ForeignKeyViolationException.php | Adds typed FK violation exception with derived flags. |
| src/Exceptions/DuplicateKeyException.php | Adds typed duplicate-key exception with helper constructor. |
| src/Exceptions/DeadlockException.php | Adds typed deadlock exception with retry helpers. |
| src/Exceptions/DatabaseException.php | Adds unified base database exception storing optional driver. |
| src/Exceptions/ConnectionLostException.php | Adds typed connection-lost exception with retryable heuristic. |
| src/Exceptions/ConnectionFailedException.php | Adds typed initial-connect failure exception with attempted-hosts context. |
| src/Exceptions/ConnectionException.php | Adds base connection exception with endpoint accessor. |
| src/Exceptions/ConfigurationException.php | Adds typed config exception with connection/key context and helpers. |
| src/Exceptions/ColumnNotFoundException.php | Adds typed schema exception for missing columns. |
| src/Exceptions/AuthenticationException.php | Adds typed authentication exception with username context helper. |
| src/Contracts/ConnectionPoolInterface.php | Adds pool interface contract. |
| src/Contracts/ConnectionManagerInterface.php | Adds manager interface contract for read/write splitting and lifecycle. |
| src/Contracts/ConnectionInterface.php | Expands connection contract (lifecycle, driver/name, observability, transactions, execute/query). |
| src/Contracts/ConnectionEventDispatcherInterface.php | Adds optional event dispatcher contract for lifecycle/query hooks. |
| src/Connection/LazyConnection.php | Adds lazy proxy connection implementing ConnectionInterface. |
| src/Connection/ConnectionPool.php | Adds in-memory connection pool implementation. |
| src/Connection/ConnectionManager.php | Adds primary connection manager with lazy creation and replica selection. |
| src/Connection/Connection.php | Adds unified concrete connection implementation with driver-specific defaults and classification. |
| src/Connection/AbstractConnection.php | Removes legacy abstract connection base. |
| src/Config/ReadReplicaConfig.php | Adds typed read-replica config and array parsing. |
| src/Config/PoolConfig.php | Adds typed pool config and array parsing. |
| src/Config/DsnConfig.php | Adds typed DSN config that builds DSNs for all supported drivers. |
| src/Config/DatabaseConfig.php | Adds typed database config with backward-compatible array parsing and PDO option defaults. |
| phpunit.xml.dist | Restricts test discovery to tests/Unit, enables fail-on-risky/warning, excludes src/Cache. |
| examples/basic_usage.php | Updates example to v2 APIs (ConnectionManager/Connection) and typed exceptions. |
| composer.json | Updates package description, dependencies, autoloading, branch alias, and PHPStan level. |
| .gitignore | Adds .phpunit.cache/ directory to ignores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $this->idle->enqueue([ | ||
| 'connection' => $connection, | ||
| 'createdAt' => microtime(true), | ||
| 'lastUsedAt' => microtime(true), | ||
| ]); |
There was a problem hiding this comment.
Connection max-lifetime enforcement is effectively disabled because createdAt is being set to microtime(true) every time a connection is released back to the pool. This means exceedsMaxLifetime() measures time since last release, not since the connection was created, so old connections will never be evicted for age. Track a stable creation timestamp per connection (e.g., store it when the connection is created and preserve it across releases, or keep metadata in a map keyed by spl_object_id($connection)).
| public static function columnExists( | ||
| ConnectionInterface $conn, | ||
| string $table, | ||
| string $column, | ||
| ?string $schema = null, | ||
| ): bool { | ||
| $key = ($schema ?: '_') . '.' . $table . '.' . $column; | ||
|
|
||
| if (array_key_exists($key, self::$columnExistsCache)) { | ||
| return self::$columnExistsCache[$key]; | ||
| } |
There was a problem hiding this comment.
columnExists() caches results globally under a key that does not include the connection identity (PDO object id) or driver, so calls across different connections/schemas can collide and return incorrect results. Incorporate at least driver + spl_object_id($conn->pdo()) + effective schema (e.g. detectSchema($conn)) into the cache key to keep the cache truly per-connection as described in the class docblock.
src/Support/SchemaIntrospector.php
Outdated
| /** @var array<string, string> pdoId → driver name */ | ||
| private static array $driverCache = []; | ||
|
|
||
| /** @var array<string, string> pdoId → schema/database name */ | ||
| private static array $schemaCache = []; | ||
|
|
There was a problem hiding this comment.
$driverCache is declared and cleared but never read or written anywhere in this class. If driver caching is no longer needed, remove it to avoid dead state; otherwise, wire it into detectDriver()/cache-key generation so it provides value.
| return match ($driver) { | ||
| DatabaseDriver::MySQL => $code === 2006 || $code === 2013 | ||
| || str_contains($message, 'server has gone away'), | ||
| DatabaseDriver::PostgreSQL => $sqlState === '57P01' | ||
| || str_contains($message, 'terminating connection'), | ||
| DatabaseDriver::SQLite => false, | ||
| }; | ||
| } | ||
|
|
||
| private static function isConnectionFailed( | ||
| DatabaseDriver $driver, | ||
| mixed $sqlState, | ||
| ?int $code, | ||
| string $message, | ||
| ): bool { | ||
| return match ($driver) { | ||
| DatabaseDriver::MySQL => $code === 2002 || $code === 2003, | ||
| DatabaseDriver::PostgreSQL => $sqlState === '08001' || $sqlState === '08006', | ||
| DatabaseDriver::SQLite => str_contains($message, 'unable to open database'), | ||
| }; |
There was a problem hiding this comment.
Several driver error detections rely on case-sensitive substring matches against PDOException messages (e.g. MySQL "server has gone away", SQLite "unable to open database"). PDO driver messages vary in capitalization, so these checks can silently fail when errorInfo codes are missing. Consider normalizing $message once (e.g. strtolower($message)) and matching against lowercase needles.
src/Config/ReadReplicaConfig.php
Outdated
| strategy: isset($config['strategy']) | ||
| ? ReadReplicaStrategy::from($config['strategy']) | ||
| : ReadReplicaStrategy::RoundRobin, | ||
| stickyAfterWrite: (bool) ($config['sticky_after_write'] ?? true), |
There was a problem hiding this comment.
ReadReplicaConfig::fromArray() reads the sticky flag from sticky_after_write, but common configs/documentation often use sticky for this behavior. As written, sticky would be ignored and there would be no way to disable stickiness unless the caller knows the internal key name. Consider supporting both keys (with one as an alias) or renaming to match the public config format consistently.
| stickyAfterWrite: (bool) ($config['sticky_after_write'] ?? true), | |
| stickyAfterWrite: (bool) ( | |
| array_key_exists('sticky', $config) | |
| ? $config['sticky'] | |
| : (array_key_exists('sticky_after_write', $config) ? $config['sticky_after_write'] : true) | |
| ), |
| private function leastConnections(array $replicas): ConnectionInterface | ||
| { | ||
| // For lazy connections, prefer those not yet initialized | ||
| $uninitialized = array_filter( | ||
| $replicas, | ||
| static fn(ConnectionInterface $c) => $c instanceof LazyConnection && !$c->initialized, | ||
| ); | ||
|
|
||
| if (!empty($uninitialized)) { | ||
| return $uninitialized[array_rand($uninitialized)]; | ||
| } | ||
|
|
||
| // All initialized — fall back to random | ||
| return $replicas[array_rand($replicas)]; | ||
| } |
There was a problem hiding this comment.
leastConnections() does not actually select the replica with the fewest connections/lowest load; once all replicas are initialized it falls back to random selection. This makes the LeastConnections strategy name misleading and can lead to unexpected behavior for users choosing it. Either implement a real least-load heuristic (e.g. track active checkouts per replica) or rename the strategy to match what it does.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99ed513d09
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| $this->idle->enqueue([ | ||
| 'connection' => $connection, | ||
| 'createdAt' => microtime(true), | ||
| 'lastUsedAt' => microtime(true), |
There was a problem hiding this comment.
Keep original created time on pooled connections
Do not reset createdAt when releasing a connection back to idle, because max-lifetime checks in acquire() use this timestamp to evict old connections. With the current assignment, every release refreshes age and long-lived connections can stay in the pool indefinitely, defeating maxLifetimeSeconds and increasing risk of stale server-side sessions.
Useful? React with 👍 / 👎.
src/Support/SchemaIntrospector.php
Outdated
| string $column, | ||
| ?string $schema = null, | ||
| ): bool { | ||
| $key = ($schema ?: '_') . '.' . $table . '.' . $column; |
There was a problem hiding this comment.
Scope column-existence cache by connection identity
Include driver/PDO/schema identity in the cache key here; as written, columnExists() caches by only schema.table.column (often _.table.column), so results from one database connection can be reused for another unrelated connection. In multi-connection apps this can return false positives/negatives for schema checks after the first lookup.
Useful? React with 👍 / 👎.
src/Config/DatabaseConfig.php
Outdated
| $dsnConfig = isset($config['dsn']) && $config['dsn'] instanceof DsnConfig | ||
| ? $config['dsn'] | ||
| : DsnConfig::fromArray($driver, $config); |
There was a problem hiding this comment.
Parse nested dsn arrays in fromArray
Handle array-valued dsn configs here instead of always falling back to the top-level flat array. Right now, passing ['dsn' => ['host' => ..., 'database' => ...]] silently ignores those DSN values, so connections may be built with missing host/database settings and target the wrong endpoint or fail to connect.
Useful? React with 👍 / 👎.
Agent-Logs-Url: https://github.com/MonkeysCloud/MonkeysLegion-Database/sessions/62f31e27-4c6f-4524-8a62-a5c62d02844b Co-authored-by: yorchperaza <2913369+yorchperaza@users.noreply.github.com>
…, RetryHandler, warmUp, security fixes, PSR-3 logger Agent-Logs-Url: https://github.com/MonkeysCloud/MonkeysLegion-Database/sessions/62f31e27-4c6f-4524-8a62-a5c62d02844b Co-authored-by: yorchperaza <2913369+yorchperaza@users.noreply.github.com>
…ed timezone regex, clearer test comment Agent-Logs-Url: https://github.com/MonkeysCloud/MonkeysLegion-Database/sessions/62f31e27-4c6f-4524-8a62-a5c62d02844b Co-authored-by: yorchperaza <2913369+yorchperaza@users.noreply.github.com>
…se-v2 feat(v2): full PHP 8.4 adoption + security, bug, and missing-feature sweep
…, lastInsertId - Add RetryHandler with truncated exponential back-off and jitter - Extract RetryConfig to own file for PSR-4 autoloading - Add ConnectionPool::warmUp() for pre-warming to minConnections - Fix createdAt tracking: max-lifetime is relative to creation, not release - Add lastInsertId() to ConnectionInterface, Connection, and LazyConnection - Add LazyConnection::innerConnection() for collaborator propagation - Fix propagateToOpenConnections to reach into resolved LazyConnection wrappers - Add logger/eventDispatcher as public properties on Connection (PHP 8.4 set hooks on ConnectionManager) - Remove duplicate class definitions from Connection.php and ConnectionManager.php - Fix RetryConfig: readonly classes cannot have property hooks - Use array_any()/array_find() in SchemaIntrospector and DatabaseDriver (PHP 8.5) - Update readme: pool warmUp, SchemaIntrospector API, RetryHandler, PSR-3 logging - 22 new tests (185 total, 400 assertions) — all green
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 80 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sslMode: isset($config['sslmode']) ? (string) $config['sslmode'] : null, | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
DsnConfig::fromArray() doesn't populate the $extra DSN parameters even though the class supports them and buildMySqlDsn()/buildPgSqlDsn() append $this->extra. This means config-supplied extra DSN params (e.g. timeouts) are silently dropped. Consider reading an extra key (or another agreed key) from the input array and passing it into the constructor.
| sslMode: isset($config['sslmode']) ? (string) $config['sslmode'] : null, | |
| ); | |
| } | |
| sslMode: isset($config['sslmode']) ? (string) $config['sslmode'] : null, | |
| extra: self::normalizeExtra($config['extra'] ?? []), | |
| ); | |
| } | |
| /** | |
| * @param mixed $extra | |
| * @return array<string, string> | |
| */ | |
| private static function normalizeExtra(mixed $extra): array | |
| { | |
| if (!is_array($extra)) { | |
| return []; | |
| } | |
| $normalized = []; | |
| foreach ($extra as $key => $value) { | |
| if (!is_string($key) && !is_int($key)) { | |
| continue; | |
| } | |
| if (!is_scalar($value) && $value !== null) { | |
| continue; | |
| } | |
| $normalized[(string) $key] = (string) ($value ?? ''); | |
| } | |
| return $normalized; | |
| } |
src/Connection/ConnectionManager.php
Outdated
| // Prefer uninitialized lazy connections (they have 0 load) | ||
| $uninitialized = array_filter( | ||
| $replicas, | ||
| static fn(ConnectionInterface $c) => $c instanceof LazyConnection && !$c->initialized, | ||
| ); | ||
|
|
||
| if (!empty($uninitialized)) { | ||
| return $uninitialized[array_rand($uninitialized)]; | ||
| } | ||
|
|
||
| // All initialized — fall back to random | ||
| return $replicas[array_rand($replicas)]; |
There was a problem hiding this comment.
ReadReplicaStrategy::LeastConnections is implemented as “prefer uninitialized, otherwise random”. Once replicas are initialized, this no longer selects the least-loaded replica, so the strategy name/contract is misleading and behavior won’t match expectations. Either track a load metric per replica (e.g. active query count / in-flight operations) and select the minimum, or rename/remove this strategy to reflect its actual behavior.
| // Prefer uninitialized lazy connections (they have 0 load) | |
| $uninitialized = array_filter( | |
| $replicas, | |
| static fn(ConnectionInterface $c) => $c instanceof LazyConnection && !$c->initialized, | |
| ); | |
| if (!empty($uninitialized)) { | |
| return $uninitialized[array_rand($uninitialized)]; | |
| } | |
| // All initialized — fall back to random | |
| return $replicas[array_rand($replicas)]; | |
| throw new ConfigurationException( | |
| 'ReadReplicaStrategy::LeastConnections requires a real per-replica load metric, ' | |
| . 'but this connection manager does not track active queries or in-flight operations. ' | |
| . 'Use a different replica strategy or implement load tracking before enabling LeastConnections.', | |
| ); |
src/Connection/ConnectionPool.php
Outdated
| use MonkeysLegion\Database\Config\DatabaseConfig; | ||
| use MonkeysLegion\Database\Config\PoolConfig; | ||
| use MonkeysLegion\Database\Contracts\ConnectionInterface; | ||
| use MonkeysLegion\Database\Contracts\ConnectionPoolInterface; | ||
| use MonkeysLegion\Database\Exceptions\PoolException; | ||
| use MonkeysLegion\Database\Support\ConnectionPoolStats; | ||
| use MonkeysLegion\Database\Support\HealthChecker; | ||
| use SplQueue; | ||
|
|
||
| /** | ||
| * MonkeysLegion Framework — Database Package | ||
| * | ||
| * In-memory connection pool with health monitoring, idle eviction, | ||
| * and max-lifetime enforcement. | ||
| * | ||
| * PHP 8.4 features used: | ||
| * • `new` in property initialiser for `SplQueue` (no constructor needed) | ||
| * • `array_any()` for concise idle-queue checks | ||
| * |
There was a problem hiding this comment.
Unused import: HealthChecker is imported but never referenced. Also the docblock claims array_any() and “new in property initialiser” are used here, but the implementation doesn’t use either. Please remove the unused import and align the docblock with the actual implementation to avoid confusion.
src/Connection/ConnectionManager.php
Outdated
| namespace MonkeysLegion\Database\Connection; | ||
|
|
||
| use MonkeysLegion\Database\Config\DatabaseConfig; | ||
| use MonkeysLegion\Database\Config\DsnConfig; |
There was a problem hiding this comment.
Unused import: MonkeysLegion\Database\Config\DsnConfig is imported but not used in this file.
| use MonkeysLegion\Database\Config\DsnConfig; |
src/Support/RetryHandler.php
Outdated
| * PHP 8.4 features used: | ||
| * • `readonly` class (`RetryConfig`) — all properties immutable after construction | ||
| * • `get` property hook on `RetryConfig::$maxDelayMs` for a clamped computed view | ||
| * • `array_any()` to test whether an exception type is in the retryable list | ||
| * |
There was a problem hiding this comment.
The docblock lists PHP 8.4 features (array_any(), a get hook on RetryConfig::$maxDelayMs, and RetryConfig being a readonly class) that the implementation does not actually use. This can mislead readers and reviewers; please update/remove these bullets to match the current code (or implement the features as described).
- DsnConfig::fromArray(): populate $extra from config with normalizeExtra() - ReadReplicaConfig: support both 'sticky' and 'sticky_after_write' keys - ConnectionManager: remove unused DsnConfig import - ConnectionManager: LeastConnections now uses queryCount as load metric (single foreach, no array_filter allocation) - ConnectionPool: remove unused HealthChecker import, fix stale docblock - RetryHandler: remove inaccurate PHP 8.4 feature claims from docblock - ErrorClassifier: normalize message to lowercase before str_contains() to prevent case-sensitivity mismatches across PDO driver versions - SchemaIntrospector: remove unused $driverCache static property - SchemaIntrospector: columnExists() cache key now includes driver + PDO object ID + schema to prevent cross-connection collisions All 185 tests pass (400 assertions).
['dsn' => ['host' => ..., 'database' => ...]] is now correctly parsed instead of silently falling back to top-level keys. Uses match(true) — single expression, no extra branching cost.
No description provided.