diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 506207a44..3e7637504 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -1,1065 +1,117 @@ variant = "loose" [[issues]] -file = "src/Adapter/Adapter.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Adapter` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Adapter.php" -code = "imprecise-type" -message = "Type `array` in parameter `$initialParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Adapter.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parametersOrQueryMode` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Adapter.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Adapter.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/Adapter/Adapter.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteValue`: expected `string`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/Adapter/Adapter.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterAwareInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\AdapterAwareInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/AdapterAwareTrait.php" -code = "missing-api-or-internal" -message = 'Trait `PhpDb\Adapter\AdapterAwareTrait` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "imprecise-type" -message = "Type `array` in parameter `$initialParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parametersOrQueryMode` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\AdapterInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `FUNCTION_FORMAT_PARAMETER_NAME` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `FUNCTION_QUOTE_IDENTIFIER` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `FUNCTION_QUOTE_VALUE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `PREPARE_TYPE_NAMED` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `PREPARE_TYPE_POSITIONAL` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `QUERY_MODE_EXECUTE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `QUERY_MODE_PREPARE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-constant-type" -message = "Class constant `VALUE_QUOTE_SEPARATOR` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterInterface.php" -code = "missing-return-type" -message = "Function `getHelpers` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/AbstractConnection.php" -code = "imprecise-type" -message = "Type `array` in parameter `$connectionParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/AbstractConnection.php" -code = "imprecise-type" -message = "Type `array` in property `$connectionParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/AbstractConnection.php" -code = "imprecise-type" -message = "Type `array` in return type of `getConnectionParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/AbstractConnection.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Adapter\Driver\AbstractConnection` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/AbstractConnection.php" -code = "missing-property-type" -message = "Property `$resource` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/AbstractConnection.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Adapter\Driver\AbstractConnection::getResource`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ConnectionInterface.php" -code = "imprecise-type" -message = "Type `array` in parameter `$connectionParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ConnectionInterface.php" -code = "imprecise-type" -message = "Type `array` in return type of `getConnectionParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ConnectionInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\ConnectionInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ConnectionInterface.php" -code = "missing-return-type" -message = "Function `getResource` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/DriverAwareInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\DriverAwareInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/DriverInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\DriverInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/DriverInterface.php" -code = "missing-constant-type" -message = "Class constant `PARAMETERIZATION_NAMED` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/DriverInterface.php" -code = "missing-constant-type" -message = "Class constant `PARAMETERIZATION_POSITIONAL` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/DriverInterface.php" -code = "missing-parameter-type" -message = "Parameter `$resource` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/DriverInterface.php" -code = "missing-parameter-type" -message = "Parameter `$sqlOrResource` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/AbstractFeature.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Adapter\Driver\Feature\AbstractFeature` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\Feature\DriverFeatureInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "imprecise-type" -message = "Type `array` in parameter `$features` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "invalid-override-attribute" -message = 'Invalid `#[Override]` attribute on `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::addfeature`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "invalid-override-attribute" -message = 'Invalid `#[Override]` attribute on `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::addfeatures`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "invalid-override-attribute" -message = 'Invalid `#[Override]` attribute on `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::getfeature`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::addFeature`: expected `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderInterface`, but found `$this(phpdb\adapter\driver\feature\driverfeatureprovidertrait&PhpDb\Adapter\Driver\DriverInterface)`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::addFeatures`: expected `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderInterface`, but found `$this(phpdb\adapter\driver\feature\driverfeatureprovidertrait)`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "missing-api-or-internal" -message = 'Trait `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "mixed-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::addFeature`: expected `PhpDb\Adapter\Driver\Feature\DriverFeatureInterface`, but found `mixed`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait::addFeature`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "impossible-condition" -message = "This condition (type `false`) will always evaluate to false." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "impossible-type-comparison" -message = "Impossible type assertion: `$name` of type `string` can never be `int`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Adapter\Driver\Pdo\AbstractPdo::getConnection`: expected `PhpDb\Adapter\Driver\PdoConnectionInterface`, but found `(PhpDb\Adapter\Driver\PdoConnectionInterface&PhpDb\Adapter\Driver\AbstractConnection&PhpDb\Adapter\Driver\PdoDriverAwareInterface)|PDO`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Adapter\Driver\Pdo\AbstractPdo` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "non-existent-method" -message = "Method `getLastGeneratedValue` does not exist on type `PDO`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "non-existent-method" -message = "Method `getResource` does not exist on type `PDO`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "non-existent-method" -message = "Method `isConnected` does not exist on type `PDO`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "overly-wide-return-type" -message = 'Declared return type `PhpDb\Adapter\Driver\ResultInterface|null` for `getresultprototype` has unused branches: `null`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "redundant-logical-operation" -message = "Redundant `&&` operation: left operand is always falsy and right operand is not evaluated." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "too-few-arguments" -message = "Too few arguments provided for method `PDO::connect`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdo::createStatement`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdo::formatParameterName`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "imprecise-type" -message = "Type `array` in parameter `$connectionParameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "invalid-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Driver\DriverInterface::createResult`: expected `resource`, but found `PDOStatement`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "mixed-argument" -message = "Invalid argument type for argument #1 of `strtolower`: expected `string`, but found `mixed`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 7 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "too-many-arguments" -message = 'Too many arguments provided for method `PhpDb\Adapter\Driver\DriverInterface::createResult`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection::beginTransaction`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection::commit`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection::execute`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection::rollback`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdoConnection.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection::setResource`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Driver\Pdo\Result` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "incompatible-return-type" -message = 'Return type `mixed` of `PhpDb\Adapter\Driver\Pdo\Result::next()` is incompatible with parent return type `void` of `Iterator::next()`' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-constant-type" -message = "Class constant `STATEMENT_MODE_FORWARD` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-constant-type" -message = "Class constant `STATEMENT_MODE_SCROLLABLE` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-constant-type" -message = "Class constant `VALID_FETCH_MODES` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-parameter-type" -message = "Parameter `$generatedValue` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$currentComplete` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$currentData` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$fetchMode` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$generatedValue` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$options` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$position` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-property-type" -message = "Property `$resource` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "missing-template-parameter" -message = "Too few template arguments for `Iterator`: expected at least 2, but found 0." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `Exception` in `PhpDb\Adapter\Driver\Pdo\Result::getQueryResult`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::count`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::current`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::getAffectedRows`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::getFieldCount`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::isQueryResult`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::next`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Result::rewind`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Driver\Pdo\Statement` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "imprecise-type" -message = "Type `array` in parameter `$options` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "invalid-argument" -message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Driver\DriverInterface::createResult`: expected `resource`, but found `PDOStatement|false|null`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "invalid-property-assignment-value" -message = "Invalid type for property `$sql`: expected `string`, but got `null|string`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "possibly-false-argument" -message = 'Argument #1 of method `PhpDb\Adapter\Driver\DriverInterface::createResult` is possibly `false`, but parameter type `resource` does not accept it.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Adapter\Driver\DriverInterface::createResult` is possibly `null`, but parameter type `resource` does not accept it.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Statement::bindParametersFromContainer`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PDOException` in `PhpDb\Adapter\Driver\Pdo\Statement::prepare`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Driver\Pdo\Statement::bindParametersFromContainer`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Driver\Pdo\Statement::execute`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "uninitialized-property" -message = 'Property `$driver` is not initialized in the constructor of class `PhpDb\Adapter\Driver\Pdo\Statement`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "uninitialized-property" -message = 'Property `$isQuery` is not initialized in the constructor of class `PhpDb\Adapter\Driver\Pdo\Statement`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "uninitialized-property" -message = 'Property `$pdo` is not initialized in the constructor of class `PhpDb\Adapter\Driver\Pdo\Statement`.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/PdoConnectionInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\PdoConnectionInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/PdoDriverAwareInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\PdoDriverAwareInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/PdoDriverInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\PdoDriverInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ResultInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\ResultInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ResultInterface.php" -code = "missing-template-parameter" -message = "Too few template arguments for `Iterator`: expected at least 2, but found 0." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/StatementInterface.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/StatementInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Driver\StatementInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Driver/StatementInterface.php" -code = "missing-return-type" -message = "Function `getResource` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Exception/ErrorException.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Exception\ErrorException` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Exception/InvalidArgumentException.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Exception\InvalidArgumentException` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Exception/InvalidConnectionParametersException.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Exception\InvalidConnectionParametersException` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Exception/InvalidConnectionParametersException.php" -code = "imprecise-type" -message = "Type `array` in parameter `$parameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Exception/InvalidConnectionParametersException.php" -code = "imprecise-type" -message = "Type `array` in property `$parameters` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Exception/InvalidQueryException.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Exception\InvalidQueryException` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\ParameterContainer` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "imprecise-type" -message = "Type `array` in parameter `$data` is imprecise, equivalent to `array`." -count = 2 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "incompatible-parameter-name" -message = 'Parameter #1 of `PhpDb\Adapter\ParameterContainer::offsetunset()` is named `$name` but parent `ArrayAccess::offsetunset()` names it `$offset`' -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "incompatible-parameter-type" -message = 'Parameter `$name` of `PhpDb\Adapter\ParameterContainer::offsetexists()` expects type `int|string` but parent `ArrayAccess::offsetexists()` expects type `mixed`' -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "incompatible-parameter-type" -message = 'Parameter `$name` of `PhpDb\Adapter\ParameterContainer::offsetget()` expects type `int|string` but parent `ArrayAccess::offsetget()` expects type `mixed`' -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "incompatible-parameter-type" -message = 'Parameter `$name` of `PhpDb\Adapter\ParameterContainer::offsetset()` expects type `int|null|string` but parent `ArrayAccess::offsetset()` expects type `mixed`' +file = "src/Adapter/AdapterInterface.php" +code = "missing-return-type" +message = "Function `getHelpers` is missing a return type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_AUTO` is missing a type hint." +file = "src/Adapter/Driver/AbstractConnection.php" +code = "missing-property-type" +message = "Property `$resource` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_BINARY` is missing a type hint." +file = "src/Adapter/Driver/AbstractConnection.php" +code = "mixed-return-statement" +message = 'Could not infer a precise return type for function `PhpDb\Adapter\Driver\AbstractConnection::getResource`. Saw type `mixed`.' count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_DOUBLE` is missing a type hint." +file = "src/Adapter/Driver/ConnectionInterface.php" +code = "missing-return-type" +message = "Function `getResource` is missing a return type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_INTEGER` is missing a type hint." +file = "src/Adapter/Driver/Pdo/AbstractPdo.php" +code = "overly-wide-return-type" +message = 'Declared return type `PhpDb\Adapter\Driver\ResultInterface|null` for `getresultprototype` has unused branches: `null`.' count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_LOB` is missing a type hint." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "incompatible-return-type" +message = 'Return type `mixed` of `PhpDb\Adapter\Driver\Pdo\Result::next()` is incompatible with parent return type `void` of `Iterator::next()`' count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_NULL` is missing a type hint." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-parameter-type" +message = "Parameter `$generatedValue` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-constant-type" -message = "Class constant `TYPE_STRING` is missing a type hint." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$currentComplete` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-template-parameter" -message = "Too few template arguments for `ArrayAccess`: expected at least 2, but found 0." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$currentData` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "missing-template-parameter" -message = "Too few template arguments for `Iterator`: expected at least 2, but found 0." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$fetchMode` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$generatedValue` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #1 of `ltrim`: expected `string`, but possibly received `int|string`." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$options` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-null-argument" -message = "Argument #1 of function `array_key_exists` is possibly `null`, but parameter type `bool|float|int|string` does not accept it." -count = 4 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-null-array-index" -message = "Possibly using `null` as an array index to access element." -count = 2 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-null-array-index" -message = "Possibly using `null` as an array index to access elementof variable $this->data." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$position` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-null-array-index" -message = "Possibly using `null` as an array index to access elementof variable $this->errata." +file = "src/Adapter/Driver/Pdo/Result.php" +code = "missing-property-type" +message = "Property `$resource` is missing a type hint." count = 1 [[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-null-array-index" -message = "Possibly using `null` as an array index to access elementof variable $this->maxLength." +file = "src/Adapter/Driver/StatementInterface.php" +code = "missing-return-type" +message = "Function `getResource` is missing a return type hint." count = 1 [[issues]] file = "src/Adapter/ParameterContainer.php" -code = "possibly-undefined-int-array-index" -message = "Possibly undefined array key `int` accessed on `array`." -count = 8 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `int|string` accessed on `array`." +code = "incompatible-parameter-name" +message = 'Parameter #1 of `PhpDb\Adapter\ParameterContainer::offsetexists()` is named `$name` but parent `ArrayAccess::offsetexists()` names it `$offset`' count = 1 [[issues]] file = "src/Adapter/ParameterContainer.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string` accessed on `array`." -count = 2 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string` accessed on `array`." +code = "incompatible-parameter-name" +message = 'Parameter #1 of `PhpDb\Adapter\ParameterContainer::offsetget()` is named `$name` but parent `ArrayAccess::offsetget()` names it `$offset`' count = 1 [[issues]] file = "src/Adapter/ParameterContainer.php" -code = "redundant-comparison" -message = "Redundant `===` comparison: left-hand side is always identical to right-hand side." +code = "incompatible-parameter-name" +message = 'Parameter #1 of `PhpDb\Adapter\ParameterContainer::offsetset()` is named `$name` but parent `ArrayAccess::offsetset()` names it `$offset`' count = 1 [[issues]] file = "src/Adapter/ParameterContainer.php" -code = "redundant-condition" -message = "This condition (type `true`) will always evaluate to true." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "imprecise-type" -message = "Type `array` in parameter `$additionalSafeWords` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Adapter\Platform\AbstractPlatform::getQuoteIdentifierSymbol`: expected `string`, but found `null|string`.' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Adapter\Platform\AbstractPlatform` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "missing-constant-type" -message = "Class constant `SAFE_WORDS` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "mixed-argument" -message = "Invalid argument type for argument #1 of `strtolower`: expected `string`, but found `mixed`." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "nullable-return-statement" -message = 'Function `PhpDb\Adapter\Platform\AbstractPlatform::getQuoteIdentifierSymbol` is declared to return `string` but possibly returns a nullable value (inferred as `null|string`).' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-false-iterator" -message = "Expression being iterated (type `false|list`) might be `false` at runtime." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-null-argument" -message = "Argument #1 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." -count = 2 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-null-operand" -message = "Possibly null left operand used in string concatenation (type `null|string`)." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-null-operand" -message = "Possibly null middle operand used in string concatenation (type `null|string`)." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-null-operand" -message = "Possibly null right operand used in string concatenation (type `null|string`)." -count = 2 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-undefined-int-array-index" -message = "Possibly undefined array key `int(0)` accessed on `array`." -count = 4 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "possibly-undefined-int-array-index" -message = "Possibly undefined array key `int(1)` accessed on `array`." -count = 2 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Platform\AbstractPlatform::quoteValue`.' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/PlatformInterface.php" -code = "imprecise-type" -message = "Type `array` in parameter `$additionalSafeWords` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/PlatformInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Platform\PlatformInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/Sql92.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Platform\Sql92` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/Sql92.php" -code = "missing-constant-type" -message = "Class constant `PLATFORM_NAME` is missing a type hint." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/Sql92.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Platform\Sql92::quoteValue`.' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\Profiler\Profiler` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "falsable-return-statement" -message = '''Function `PhpDb\Adapter\Profiler\Profiler::getLastProfile` is declared to return `array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|null` but possibly returns 'false' (inferred as `array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|false`).''' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "invalid-property-assignment-value" -message = '''Invalid type for property `$profiles`: expected `array`, but got `non-empty-array`.''' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "invalid-return-statement" -message = '''Invalid return type for function `PhpDb\Adapter\Profiler\Profiler::getLastProfile`: expected `array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|null`, but found `array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|false`.''' +code = "incompatible-parameter-name" +message = 'Parameter #1 of `PhpDb\Adapter\ParameterContainer::offsetunset()` is named `$name` but parent `ArrayAccess::offsetunset()` names it `$offset`' count = 1 [[issues]] @@ -1074,48 +126,6 @@ code = "missing-property-type" message = "Property `$profiles` is missing a type hint." count = 1 -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Adapter\Exception\RuntimeException` in `PhpDb\Adapter\Profiler\Profiler::profilerFinish`.' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/ProfilerAwareInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Profiler\ProfilerAwareInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/ProfilerInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\Profiler\ProfilerInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/SchemaAwareInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\SchemaAwareInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Adapter/StatementContainer.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Adapter\StatementContainer` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Adapter/StatementContainer.php" -code = "incompatible-parameter-type" -message = 'Parameter `$sql` of `PhpDb\Adapter\StatementContainer::setsql()` expects type `string` but parent `PhpDb\Adapter\StatementContainerInterface::setsql()` expects type `null|string`' -count = 1 - -[[issues]] -file = "src/Adapter/StatementContainerInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Adapter\StatementContainerInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - [[issues]] file = "src/ConfigProvider.php" code = "imprecise-type" @@ -1890,12 +900,6 @@ code = "less-specific-argument" message = 'Argument type mismatch for argument #2 of `PhpDb\Sql\Sql::__construct`: expected `PhpDb\Sql\TableIdentifier|array|null|string`, but provided type `PhpDb\Sql\TableIdentifier|array|string` is less specific.' count = 1 -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "less-specific-nested-argument-type" -message = 'Argument type mismatch for argument #1 of `PhpDb\ResultSet\ResultSetInterface::initialize`: expected `iterable`, but provided type `PhpDb\Adapter\Driver\ResultInterface|null` is less specific.' -count = 1 - [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "less-specific-nested-argument-type" diff --git a/lint-baseline.toml b/lint-baseline.toml index 893a1b21f..19faa6c46 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -1,209 +1,11 @@ variant = "loose" -[[issues]] -file = "src/Adapter/Adapter.php" -code = "prefer-first-class-callable" -message = "Use first-class callable syntax `...` instead of a closure." -count = 2 - -[[issues]] -file = "src/Adapter/AdapterAwareInterface.php" -code = "strict-types" -message = "Missing `declare(strict_types=1);` statement at the beginning of the file." -count = 1 - -[[issues]] -file = "src/Adapter/AdapterAwareTrait.php" -code = "strict-types" -message = "Missing `declare(strict_types=1);` statement at the beginning of the file." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/ConnectionInterface.php" -code = "too-many-methods" -message = "Interface has too many methods." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "literal-named-argument" -message = "Literal argument `':'` should be passed as a named argument for clarity." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/AbstractPdo.php" -code = "tagged-todo" -message = "TODO should be tagged with (@username) or (#issue)." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "halstead" -message = "Method has a high halstead difficulty" -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "literal-named-argument" -message = "Literal argument `true` should be passed as a named argument for clarity." -count = 2 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Result.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "literal-named-argument" -message = "Literal argument `':'` should be passed as a named argument for clarity." -count = 1 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 2 - -[[issues]] -file = "src/Adapter/Driver/Pdo/Statement.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 1 - [[issues]] file = "src/Adapter/ParameterContainer.php" code = "cyclomatic-complexity" message = "Class has high complexity." count = 1 -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "halstead" -message = "Method has a high halstead difficulty" -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "kan-defect" -message = "Class has a high kan defect score." -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "literal-named-argument" -message = "Literal argument `':'` should be passed as a named argument for clarity." -count = 1 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "literal-named-argument" -message = "Literal argument `1` should be passed as a named argument for clarity." -count = 2 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 2 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "no-else-clause" -message = "Avoid `elseif` clauses." -count = 3 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 11 - -[[issues]] -file = "src/Adapter/ParameterContainer.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "literal-named-argument" -message = '''Literal argument `"\x00\n\r\\'\"\x1a"` should be passed as a named argument for clarity.''' -count = 2 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "literal-named-argument" -message = '''Literal argument `'\\"'` should be passed as a named argument for clarity.''' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/AbstractPlatform.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 2 - -[[issues]] -file = "src/Adapter/Platform/PlatformInterface.php" -code = "too-many-methods" -message = "Interface has too many methods." -count = 1 - -[[issues]] -file = "src/Adapter/Platform/Sql92.php" -code = "literal-named-argument" -message = '''Literal argument `"\x00\n\r\\'\"\x1a"` should be passed as a named argument for clarity.''' -count = 1 - -[[issues]] -file = "src/Adapter/Platform/Sql92.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "no-else-clause" -message = "Avoid `else` clauses." -count = 1 - -[[issues]] -file = "src/Adapter/Profiler/Profiler.php" -code = "no-isset" -message = "Use of the `isset` construct." -count = 1 - -[[issues]] -file = "src/Adapter/SchemaAwareInterface.php" -code = "tagged-todo" -message = "TODO should be tagged with (@username) or (#issue)." -count = 1 - -[[issues]] -file = "src/Adapter/StatementContainer.php" -code = "strict-types" -message = "Missing `declare(strict_types=1);` statement at the beginning of the file." -count = 1 - [[issues]] file = "src/Container/AbstractAdapterInterfaceFactory.php" code = "no-empty" diff --git a/mago.toml b/mago.toml index 31246c5f0..b803bde02 100644 --- a/mago.toml +++ b/mago.toml @@ -18,9 +18,18 @@ baseline = "lint-baseline.toml" # not apply. Predicate, AlterTable, CreateTable, Select, Insert and Update are fluent builders: # one public method per clause they can emit, so their method count is the size of their # public API. AbstractSql is their base and carries one protected hook per clause it renders. +# ConnectionInterface and PlatformInterface are the contracts every driver and platform package +# implements, so their method count is the size of that contract. Pdo\Result implements Iterator, +# Countable and ResultInterface, and ParameterContainer implements Iterator, ArrayAccess and +# Countable beside its errata and max-length accessors: in both, the count is fixed by the +# interfaces and the public API. # "test/" repeats the phpdb-qa-tools default, which this key replaces. too-many-methods = { exclude = [ "test/", + "src/Adapter/Driver/ConnectionInterface.php", + "src/Adapter/Driver/Pdo/Result.php", + "src/Adapter/ParameterContainer.php", + "src/Adapter/Platform/PlatformInterface.php", "src/Sql/Exception/", "src/Sql/Predicate/Exception/", "src/TableGateway/Exception/", @@ -62,6 +71,8 @@ ignore = [ { code = "invalid-enum-case-value", in = ["src/Sql/ArgumentType.php"] }, ] class-initializers = [ + "PhpDb\\Adapter\\Driver\\Pdo\\Statement::initialize", + "PhpDb\\Adapter\\Driver\\Pdo\\Statement::setDriver", "PhpDb\\ResultSet\\AbstractResultSet::initialize", "PhpDb\\TableGateway\\Feature\\AbstractFeature::setTableGateway", "PhpDb\\RowGateway\\Feature\\AbstractFeature::setRowGateway", diff --git a/src/Adapter/Adapter.php b/src/Adapter/Adapter.php index 7e2a42b8f..c507941b0 100644 --- a/src/Adapter/Adapter.php +++ b/src/Adapter/Adapter.php @@ -12,6 +12,9 @@ use function is_array; use function strtolower; +/** + * @api + */ class Adapter implements AdapterInterface, Profiler\ProfilerAwareInterface, SchemaAwareInterface { /** @@ -30,6 +33,8 @@ public function __construct( /** * Create statement + * + * @param ParameterContainer|array $initialParameters */ #[Override] public function createStatement( @@ -83,6 +88,15 @@ public function getDriver(): Driver\DriverInterface /** * {@inheritDoc} + * + * Each argument names a helper, and the match is strict, so an argument that is not one of + * the FUNCTION_* names, whatever its type, adds no helper. + * + * Every helper names its parameter $value so callers can pass it by name, which is why + * quoteIdentifier(), whose own parameter is $identifier, sits behind a forwarding closure. + * + * @mago-expect analysis:mixed-assignment + * @mago-expect lint:prefer-first-class-callable */ #[Override] public function getHelpers() @@ -90,17 +104,15 @@ public function getHelpers() $functions = []; $platform = $this->platform; foreach (func_get_args() as $arg) { - switch ($arg) { - case self::FUNCTION_QUOTE_IDENTIFIER: - $functions[] = static function ($value) use ($platform) { - return $platform->quoteIdentifier($value); - }; - break; - case self::FUNCTION_QUOTE_VALUE: - $functions[] = static function ($value) use ($platform) { - return $platform->quoteValue($value); - }; - break; + $function = match ($arg) { + self::FUNCTION_QUOTE_IDENTIFIER => static fn(string $value): string => $platform->quoteIdentifier( + $value, + ), + self::FUNCTION_QUOTE_VALUE => $platform->quoteValue(...), + default => null, + }; + if (null !== $function) { + $functions[] = $function; } } return $functions; @@ -129,6 +141,8 @@ public function getQueryResultSetPrototype(): ResultSet\ResultSetInterface * * Always prepares the statement; never executes it. Use executeQuery() * to run the returned statement. + * + * @param ParameterContainer|array $parameters */ #[Override] public function prepareQuery( @@ -152,6 +166,8 @@ public function prepareQuery( * * @deprecated Use prepareQuery() and executeQuery() instead. query() will be removed in a future version. * + * @param ParameterContainer|array|string $parametersOrQueryMode + * * @throws Exception\InvalidArgumentException * @throws Exception\RuntimeException When execution did not produce a result. * @throws PhpException diff --git a/src/Adapter/AdapterAwareInterface.php b/src/Adapter/AdapterAwareInterface.php index 6dcbd6607..f2d621d83 100644 --- a/src/Adapter/AdapterAwareInterface.php +++ b/src/Adapter/AdapterAwareInterface.php @@ -1,7 +1,12 @@ $initialParameters + */ public function createStatement( ?string $initialSql = null, ParameterContainer|array $initialParameters = [], @@ -58,12 +63,17 @@ public function getQueryResultSetPrototype(): ResultSet\ResultSetInterface; /** * Prepares a statement for the given SQL without executing it. + * + * @param ParameterContainer|array $parameters */ public function prepareQuery( string $sql, ParameterContainer|array $parameters = [], ): Driver\StatementInterface; + /** + * @param ParameterContainer|array|string $parametersOrQueryMode + */ public function query( string $sql, ParameterContainer|array|string $parametersOrQueryMode = self::QUERY_MODE_PREPARE, diff --git a/src/Adapter/Driver/AbstractConnection.php b/src/Adapter/Driver/AbstractConnection.php index ac50195f5..515c1c686 100644 --- a/src/Adapter/Driver/AbstractConnection.php +++ b/src/Adapter/Driver/AbstractConnection.php @@ -8,8 +8,14 @@ use PhpDb\Adapter\Profiler\ProfilerAwareInterface; use PhpDb\Adapter\Profiler\ProfilerInterface; +/** + * @api + * + * @psalm-import-type ConnectionParameters from ConnectionInterface + */ abstract class AbstractConnection implements ConnectionInterface, ProfilerAwareInterface { + /** @var ConnectionParameters */ protected array $connectionParameters = []; protected ?string $driverName; @@ -34,7 +40,11 @@ public function disconnect(): ConnectionInterface return $this; } - /** Get connection parameters */ + /** + * Get connection parameters + * + * @return ConnectionParameters + */ #[Override] public function getConnectionParameters(): array { @@ -53,7 +63,7 @@ public function getProfiler(): ?ProfilerInterface } /** - * @return resource|null + * @return resource|object|null */ #[Override] public function getResource() @@ -72,6 +82,7 @@ public function inTransaction(): bool return $this->inTransaction; } + /** @param ConnectionParameters $connectionParameters */ #[Override] public function setConnectionParameters(array $connectionParameters): ConnectionInterface { diff --git a/src/Adapter/Driver/ConnectionInterface.php b/src/Adapter/Driver/ConnectionInterface.php index 51511428e..ed9382d72 100644 --- a/src/Adapter/Driver/ConnectionInterface.php +++ b/src/Adapter/Driver/ConnectionInterface.php @@ -6,6 +6,11 @@ use PhpDb\Adapter\SchemaAwareInterface; +/** + * @api + * + * @psalm-type ConnectionParameters = array + */ interface ConnectionInterface extends SchemaAwareInterface { public function beginTransaction(): self; @@ -18,6 +23,7 @@ public function disconnect(): self; public function execute(string $sql): ?ResultInterface; + /** @return ConnectionParameters */ public function getConnectionParameters(): array; /** @@ -41,5 +47,6 @@ public function isConnected(): bool; public function rollback(): self; + /** @param ConnectionParameters $connectionParameters */ public function setConnectionParameters(array $connectionParameters): self; } diff --git a/src/Adapter/Driver/DriverAwareInterface.php b/src/Adapter/Driver/DriverAwareInterface.php index 2fc7a6f20..601171788 100644 --- a/src/Adapter/Driver/DriverAwareInterface.php +++ b/src/Adapter/Driver/DriverAwareInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Adapter\Driver; +/** + * @api + */ interface DriverAwareInterface { /** Implementation should provide a fluent interface */ diff --git a/src/Adapter/Driver/DriverInterface.php b/src/Adapter/Driver/DriverInterface.php index eb3fd59e3..e5007c146 100644 --- a/src/Adapter/Driver/DriverInterface.php +++ b/src/Adapter/Driver/DriverInterface.php @@ -6,10 +6,13 @@ use PhpDb\Exception; +/** + * @api + */ interface DriverInterface { - public const PARAMETERIZATION_POSITIONAL = 'positional'; - public const PARAMETERIZATION_NAMED = 'named'; + public const string PARAMETERIZATION_POSITIONAL = 'positional'; + public const string PARAMETERIZATION_NAMED = 'named'; /** * Check environment @@ -21,16 +24,16 @@ public function checkEnvironment(): bool; /** * Create result * - * @param resource $resource + * @param resource|object $resource The driver's native result handle */ - public function createResult($resource): ResultInterface; + public function createResult(mixed $resource): ResultInterface; /** * Create statement * - * @param resource|string $sqlOrResource + * @param resource|object|string|null $sqlOrResource */ - public function createStatement($sqlOrResource = null): StatementInterface; + public function createStatement(mixed $sqlOrResource = null): StatementInterface; /** Format parameter name */ public function formatParameterName(string $name, ?string $type = null): string; diff --git a/src/Adapter/Driver/Feature/AbstractFeature.php b/src/Adapter/Driver/Feature/AbstractFeature.php index 2872ec8f6..e86c71f01 100644 --- a/src/Adapter/Driver/Feature/AbstractFeature.php +++ b/src/Adapter/Driver/Feature/AbstractFeature.php @@ -7,6 +7,9 @@ use Override; use PhpDb\Adapter\Driver\DriverInterface; +/** + * @api + */ abstract class AbstractFeature implements DriverFeatureInterface { protected DriverInterface $driver; diff --git a/src/Adapter/Driver/Feature/DriverFeatureInterface.php b/src/Adapter/Driver/Feature/DriverFeatureInterface.php index 216aa07d9..dee71cace 100644 --- a/src/Adapter/Driver/Feature/DriverFeatureInterface.php +++ b/src/Adapter/Driver/Feature/DriverFeatureInterface.php @@ -6,4 +6,7 @@ use PhpDb\Adapter\Driver\DriverAwareInterface; +/** + * @api + */ interface DriverFeatureInterface extends DriverAwareInterface {} diff --git a/src/Adapter/Driver/Feature/DriverFeatureProviderInterface.php b/src/Adapter/Driver/Feature/DriverFeatureProviderInterface.php index f03c2bdd9..10ec06634 100644 --- a/src/Adapter/Driver/Feature/DriverFeatureProviderInterface.php +++ b/src/Adapter/Driver/Feature/DriverFeatureProviderInterface.php @@ -7,13 +7,15 @@ use PhpDb\Adapter\Driver\DriverInterface; /** + * @api + * * @property array $features */ interface DriverFeatureProviderInterface { public function addFeature(DriverFeatureInterface $feature): self; - /** @param DriverFeatureInterface[] $features */ + /** @param array $features */ public function addFeatures(array $features): self; /** Get feature by class FQCN. */ diff --git a/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php b/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php index 665e0559c..f6652ccd1 100644 --- a/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php +++ b/src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php @@ -4,7 +4,6 @@ namespace PhpDb\Adapter\Driver\Feature; -use Override; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Exception\RuntimeException; @@ -14,6 +13,8 @@ * This trait can be used in any driver that needs to support features. * Primarily used in the Pdo driver, but can be adapted for others. * + * @api + * * @phpstan-ignore trait.unused */ trait DriverFeatureProviderTrait @@ -21,30 +22,47 @@ trait DriverFeatureProviderTrait /** @var array */ protected array $features = []; - #[Override] + /** @throws RuntimeException When the composing class is not a feature-providing driver. */ public function addFeature(DriverFeatureInterface $feature): DriverFeatureProviderInterface { - if (! $this instanceof DriverInterface) { - throw RuntimeException::forUncomposableTrait(__TRAIT__, DriverInterface::class); - } + $provider = $this->asFeatureProvidingDriver(); - $feature->setDriver($this); + $feature->setDriver($provider); $this->features[$feature::class] = $feature; - return $this; + return $provider; } - #[Override] + /** + * @param array $features + * + * @throws RuntimeException When the composing class is not a feature-providing driver. + */ public function addFeatures(array $features): DriverFeatureProviderInterface { + $provider = $this->asFeatureProvidingDriver(); + foreach ($features as $feature) { $this->addFeature($feature); } - return $this; + return $provider; } - #[Override] public function getFeature(string $name): DriverFeatureInterface|false { return $this->features[$name] ?? false; } + + /** @throws RuntimeException When the composing class is not a feature-providing driver. */ + private function asFeatureProvidingDriver(): DriverInterface&DriverFeatureProviderInterface + { + if (! $this instanceof DriverInterface) { + throw RuntimeException::forUncomposableTrait(__TRAIT__, DriverInterface::class); + } + + if (! $this instanceof DriverFeatureProviderInterface) { + throw RuntimeException::forUncomposableTrait(__TRAIT__, DriverFeatureProviderInterface::class); + } + + return $this; + } } diff --git a/src/Adapter/Driver/Pdo/AbstractPdo.php b/src/Adapter/Driver/Pdo/AbstractPdo.php index de5d80c9b..37a30e555 100644 --- a/src/Adapter/Driver/Pdo/AbstractPdo.php +++ b/src/Adapter/Driver/Pdo/AbstractPdo.php @@ -18,12 +18,14 @@ use PhpDb\Adapter\Profiler\ProfilerInterface; use function extension_loaded; -use function is_int; use function is_numeric; use function is_string; use function ltrim; use function preg_match; +/** + * @api + */ abstract class AbstractPdo implements PdoDriverInterface, ProfilerAwareInterface { protected (PdoConnectionInterface&AbstractConnection&PdoDriverAwareInterface)|PDO $connection; @@ -51,9 +53,10 @@ public function checkEnvironment(): bool } /** - * todo: this needs improved + * @param resource|object|string|null $sqlOrResource A resource or object other than a PDOStatement + * is ignored, as with null * - * @param PDOStatement|resource|string|null $sqlOrResource A non-PDO resource is ignored, as with null + * @throws Exception\RuntimeException When the connection is a bare PDO instance. */ #[Override] public function createStatement($sqlOrResource = null): StatementInterface @@ -61,57 +64,70 @@ public function createStatement($sqlOrResource = null): StatementInterface /** @var Statement $statement */ $statement = clone $this->statementPrototype; if ($sqlOrResource instanceof PDOStatement) { - $statement->setResource($sqlOrResource); - } else { - if (is_string($sqlOrResource)) { - $statement->setSql($sqlOrResource); - } - if (! $this->connection->isConnected()) { - $this->connection->connect(); - } - /** @var PDO $resource */ - $resource = $this->connection->getResource(); - $statement->initialize($resource); + return $statement->setResource($sqlOrResource); } - return $statement; + + if (is_string($sqlOrResource)) { + $statement->setSql($sqlOrResource); + } + + $connection = $this->getPdoConnection(); + if (! $connection->isConnected()) { + $connection->connect(); + } + + /** @var PDO $resource */ + $resource = $connection->getResource(); + + return $statement->initialize($resource); } /** * {@inheritDoc} + * + * An integer name is named only when the type asks for it; with no type, a numeric name is + * positional. PDO rejects named parameters outside [a-zA-Z0-9_], see + * https://bugs.php.net/bug.php?id=43130. + * + * The analyzer reads $name as the string that DriverInterface declares, so it reports the + * cast that an integer name needs as redundant. + * + * @throws Exception\RuntimeException When a named parameter contains invalid characters. + * + * @mago-expect analysis:redundant-cast */ #[Override] public function formatParameterName(string|int $name, ?string $type = null): string { - if (null === $type && ! is_numeric($name) || self::PARAMETERIZATION_NAMED === $type) { - // proposed fix for passing $name as int with type self::PARAMETERIZATION_NAMED - if (is_int($name) && self::PARAMETERIZATION_NAMED === $type) { - $name = (string) $name; - } - // end proposed fix - $name = ltrim($name, ':'); - // @see https://bugs.php.net/bug.php?id=43130 - if (preg_match('/[^a-zA-Z0-9_]/', $name)) { - throw Exception\RuntimeException::forInvalidPdoParam($name); - } - return ":{$name}"; + $isPositional = null === $type ? is_numeric($name) : self::PARAMETERIZATION_NAMED !== $type; + if ($isPositional) { + return '?'; } - return '?'; + $name = ltrim((string) $name, characters: ':'); + if (1 === preg_match('/[^a-zA-Z0-9_]/', $name)) { + throw Exception\RuntimeException::forInvalidPdoParam($name); + } + + return ":{$name}"; } + /** @throws Exception\RuntimeException When the connection is a bare PDO instance. */ #[Override] public function getConnection(): PdoConnectionInterface { - return $this->connection; + return $this->getPdoConnection(); } /** * {@inheritDoc} + * + * @throws Exception\RuntimeException When the connection is a bare PDO instance. */ #[Override] public function getLastGeneratedValue(?string $name = null): string|int|false|null { - return $this->connection->getLastGeneratedValue($name); + return $this->getPdoConnection()->getLastGeneratedValue($name); } /** @@ -148,4 +164,17 @@ public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface } return $this; } + + /** + * The connection property also admits a bare PDO instance, which the driver methods + * cannot use: it has no isConnected(), getResource() or getLastGeneratedValue(). + * + * @throws Exception\RuntimeException When the connection is a bare PDO instance. + */ + private function getPdoConnection(): PdoConnectionInterface&AbstractConnection&PdoDriverAwareInterface + { + return $this->connection instanceof PDO + ? throw Exception\RuntimeException::forBarePdoConnection() + : $this->connection; + } } diff --git a/src/Adapter/Driver/Pdo/AbstractPdoConnection.php b/src/Adapter/Driver/Pdo/AbstractPdoConnection.php index 1e3f314e4..d49fac17a 100644 --- a/src/Adapter/Driver/Pdo/AbstractPdoConnection.php +++ b/src/Adapter/Driver/Pdo/AbstractPdoConnection.php @@ -6,6 +6,7 @@ use Override; use PDO; +use PDOException; use PhpDb\Adapter\Driver\AbstractConnection; use PhpDb\Adapter\Driver\ConnectionInterface; use PhpDb\Adapter\Driver\PdoConnectionInterface; @@ -18,6 +19,11 @@ use function strtolower; +/** + * @api + * + * @psalm-import-type ConnectionParameters from ConnectionInterface + */ abstract class AbstractPdoConnection extends AbstractConnection implements PdoConnectionInterface, PdoDriverAwareInterface @@ -29,16 +35,19 @@ abstract class AbstractPdoConnection extends AbstractConnection implements /** @var ?PDO $resource */ protected $resource; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException When connecting did not produce a PDO resource. + * @throws PDOException When the driver cannot start a transaction. + */ #[Override] public function beginTransaction(): ConnectionInterface { - if (! $this->isConnected()) { - $this->connect(); - } + $resource = $this->getConnectedResource(); if (0 === $this->nestedTransactionsCount) { - $this->resource->beginTransaction(); + $resource->beginTransaction(); $this->inTransaction = true; } @@ -47,13 +56,16 @@ public function beginTransaction(): ConnectionInterface return $this; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + * @throws Exception\RuntimeException When connecting did not produce a PDO resource. + * @throws PDOException When the driver cannot commit. + */ #[Override] public function commit(): ConnectionInterface { - if (! $this->isConnected()) { - $this->connect(); - } + $resource = $this->getConnectedResource(); if ($this->inTransaction) { $this->nestedTransactionsCount -= 1; @@ -64,7 +76,7 @@ public function commit(): ConnectionInterface * after issuing a SET autocommit=0; we have to commit too. */ if (0 === $this->nestedTransactionsCount) { - $this->resource->commit(); + $resource->commit(); $this->inTransaction = false; } @@ -74,28 +86,33 @@ public function commit(): ConnectionInterface /** * {@inheritDoc} * + * The SQL is passed to createResult() as a second argument for drivers that accept a + * context there, such as the SQLite driver; DriverInterface declares only the first. + * * @throws Exception\InvalidQueryException + * @throws Exception\RuntimeException When connecting did not produce a PDO resource or no driver is set. + * @throws PDOException When the driver raises one while running the query. + * + * @mago-expect analysis:too-many-arguments */ #[Override] public function execute($sql): ?ResultInterface { - if (! $this->isConnected()) { - $this->connect(); - } + $resource = $this->getConnectedResource(); $this->profiler?->profilerStart($sql); - $resultResource = $this->resource->query($sql); + $resultResource = $resource->query($sql); $this->profiler?->profilerFinish(); if (false === $resultResource) { - $errorInfo = $this->resource->errorInfo(); + $errorInfo = $resource->errorInfo(); throw Exception\InvalidQueryException::forDriverError($errorInfo[2]); } /** @phpstan-ignore arguments.count */ - return $this->driver->createResult($resultResource, $sql); + return $this->getDriver()->createResult($resultResource, $sql); } /** @@ -120,20 +137,25 @@ public function isConnected(): bool return $this->resource instanceof PDO; } - /** Prepare a statement */ + /** + * Prepare a statement + * + * @throws Exception\RuntimeException When no driver is set. + */ public function prepare(?string $sql = null): StatementInterface { if (! $this->isConnected()) { $this->connect(); } - return $this->driver->createStatement($sql); + return $this->getDriver()->createStatement($sql); } /** * {@inheritDoc} * * @throws Exception\RuntimeException + * @throws PDOException When the driver cannot roll back. */ #[Override] public function rollback(): ConnectionInterface @@ -146,7 +168,7 @@ public function rollback(): ConnectionInterface throw Exception\RuntimeException::forRollbackWithoutTransaction(); } - $this->resource->rollBack(); + $this->getConnectedResource()->rollBack(); $this->inTransaction = false; $this->nestedTransactionsCount = 0; @@ -154,6 +176,7 @@ public function rollback(): ConnectionInterface return $this; } + /** @param ConnectionParameters $connectionParameters */ #[Override] public function setConnectionParameters(array $connectionParameters): ConnectionInterface { @@ -170,11 +193,34 @@ public function setDriver(PdoDriverInterface $driver): PdoDriverAwareInterface return $this; } + /** @throws PDOException When the driver name cannot be read from the resource. */ public function setResource(PDO $resource): PdoConnectionInterface { $this->resource = $resource; - $this->driverName = strtolower($this->resource->getAttribute(PDO::ATTR_DRIVER_NAME)); + $this->driverName = strtolower((string) $resource->getAttribute(PDO::ATTR_DRIVER_NAME)); return $this; } + + /** + * Connect if needed and return the PDO resource. + * + * @throws Exception\RuntimeException When connecting did not produce a PDO resource. + */ + private function getConnectedResource(): PDO + { + if (! $this->isConnected()) { + $this->connect(); + } + + return $this->resource instanceof PDO + ? $this->resource + : throw Exception\RuntimeException::forMissingConnectionResource(); + } + + /** @throws Exception\RuntimeException When no driver is set. */ + private function getDriver(): PdoDriverInterface + { + return $this->driver ?? throw Exception\RuntimeException::forMissingDriver(); + } } diff --git a/src/Adapter/Driver/Pdo/Result.php b/src/Adapter/Driver/Pdo/Result.php index 41460dd4c..84f078fa3 100644 --- a/src/Adapter/Driver/Pdo/Result.php +++ b/src/Adapter/Driver/Pdo/Result.php @@ -5,9 +5,11 @@ namespace PhpDb\Adapter\Driver\Pdo; use Closure; +use Exception as PhpException; use Iterator; use Override; use PDO; +use PDOException; use PDOStatement; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Exception; @@ -19,10 +21,15 @@ use function in_array; use function is_int; +/** + * @api + * + * @implements Iterator + */ class Result implements Iterator, ResultInterface { - public const STATEMENT_MODE_SCROLLABLE = 'scrollable'; - public const STATEMENT_MODE_FORWARD = 'forward'; + public const string STATEMENT_MODE_SCROLLABLE = 'scrollable'; + public const string STATEMENT_MODE_FORWARD = 'forward'; protected string $statementMode = self::STATEMENT_MODE_FORWARD; @@ -32,9 +39,9 @@ class Result implements Iterator, ResultInterface /** * @internal * - * @var array + * @var list */ - public const VALID_FETCH_MODES = [ + public const array VALID_FETCH_MODES = [ PDO::FETCH_LAZY, // 1 PDO::FETCH_ASSOC, // 2 PDO::FETCH_NUM, // 3 @@ -95,26 +102,22 @@ public function buffer(): void {} * Count * * @return int + * + * @throws PDOException When the driver raises one while counting rows. */ #[ReturnTypeWillChange] #[Override] public function count() { - if (is_int($this->rowCount)) { - return $this->rowCount; - } - if ($this->rowCount instanceof Closure) { - $this->rowCount = (int) ($this->rowCount)(); - } else { - $this->rowCount = (int) $this->resource->rowCount(); - } - return $this->rowCount; + return $this->rowCount = $this->resolveRowCount(); } /** * Get the data * * @return mixed + * + * @throws PDOException When the driver raises one while fetching. */ #[ReturnTypeWillChange] #[Override] @@ -131,6 +134,8 @@ public function current() /** * {@inheritdoc} + * + * @throws PDOException When the driver raises one while counting rows. */ #[Override] public function getAffectedRows(): int @@ -145,6 +150,8 @@ public function getFetchMode(): int /** * {@inheritdoc} + * + * @throws PDOException When the driver raises one while counting columns. */ #[Override] public function getFieldCount(): int @@ -162,6 +169,8 @@ public function getGeneratedValue(): string|int|false|null * {@inheritdoc} * * @throws Exception\RuntimeException + * @throws PDOException When the driver raises one while counting columns. + * @throws PhpException When the result set rejects this result as its data source. */ #[Override] public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface @@ -219,6 +228,8 @@ public function isBuffered(): bool /** * {@inheritdoc} + * + * @throws PDOException When the driver raises one while counting columns. */ #[Override] public function isQueryResult(): bool @@ -242,6 +253,8 @@ public function key() * Next * * @return mixed + * + * @throws PDOException When the driver raises one while fetching. */ #[ReturnTypeWillChange] #[Override] @@ -255,6 +268,7 @@ public function next() /** * @throws Exception\RuntimeException + * @throws PDOException When the driver raises one while fetching. * @return void */ #[ReturnTypeWillChange] @@ -276,7 +290,7 @@ public function rewind() */ public function setFetchMode(int $fetchMode): void { - if (! in_array($fetchMode, self::VALID_FETCH_MODES, true)) { + if (! in_array($fetchMode, self::VALID_FETCH_MODES, strict: true)) { throw Exception\InvalidArgumentException::forInvalidFetchMode(); } @@ -285,7 +299,7 @@ public function setFetchMode(int $fetchMode): void public function setStatementMode(string $statementMode = self::STATEMENT_MODE_FORWARD): void { - if (! in_array($statementMode, [self::STATEMENT_MODE_SCROLLABLE, self::STATEMENT_MODE_FORWARD], true)) { + if (! in_array($statementMode, [self::STATEMENT_MODE_SCROLLABLE, self::STATEMENT_MODE_FORWARD], strict: true)) { throw Exception\InvalidArgumentException::forInvalidStatementMode(); } @@ -303,4 +317,16 @@ public function valid() { return false !== $this->currentData; } + + /** @throws PDOException When the driver raises one while counting rows. */ + private function resolveRowCount(): int + { + $rowCount = $this->rowCount; + + return match (true) { + is_int($rowCount) => $rowCount, + $rowCount instanceof Closure => (int) $rowCount(), + default => $this->resource->rowCount(), + }; + } } diff --git a/src/Adapter/Driver/Pdo/Statement.php b/src/Adapter/Driver/Pdo/Statement.php index 79ace82c7..87372d84f 100644 --- a/src/Adapter/Driver/Pdo/Statement.php +++ b/src/Adapter/Driver/Pdo/Statement.php @@ -20,10 +20,14 @@ use function implode; use function is_array; +use function is_bool; use function is_int; use function ltrim; use function preg_match; +/** + * @api + */ class Statement implements StatementInterface, PdoDriverAwareInterface, ProfilerAwareInterface { protected PDO $pdo; @@ -34,7 +38,7 @@ class Statement implements StatementInterface, PdoDriverAwareInterface, Profiler protected string $sql = ''; - protected bool $isQuery; + protected bool $isQuery = false; protected bool $parametersBound = false; @@ -42,12 +46,21 @@ class Statement implements StatementInterface, PdoDriverAwareInterface, Profiler protected bool $isPrepared = false; + /** + * @param array $options + */ public function __construct( protected ParameterContainer $parameterContainer = new ParameterContainer(), protected array $options = [], ) {} - /** @throws Exception\InvalidQueryException */ + /** + * @param ParameterContainer|array|null $parameters + * + * @throws Exception\InvalidQueryException + * @throws Exception\RuntimeException + * @throws PDOException When the driver raises one while preparing or binding. + */ #[Override] public function execute(array|ParameterContainer|null $parameters = null): ?ResultInterface { @@ -93,7 +106,7 @@ public function execute(array|ParameterContainer|null $parameters = null): ?Resu $this->profiler?->profilerFinish(); - return $this->driver->createResult($this->resource); + return $this->driver->createResult($resource); } #[Override] @@ -134,7 +147,10 @@ public function isPrepared(): bool return $this->isPrepared; } - /** @throws Exception\RuntimeException */ + /** + * @throws Exception\RuntimeException + * @throws PDOException When the driver raises one while preparing. + */ #[Override] public function prepare(?string $sql = null): StatementInterface { @@ -186,15 +202,25 @@ public function setResource(PDOStatement $pdoStatement): StatementInterface return $this; } - /** Set sql */ + /** Set sql; a null SQL string is stored as an empty one */ #[Override] public function setSql(?string $sql): StatementContainerInterface { - $this->sql = $sql; + $this->sql = $sql ?? ''; return $this; } - /** Bind parameters from container */ + /** + * Bind parameters from container + * + * Parameter values are bound by reference and can be of any type, so the loop and + * PDOStatement::bindParam() both assign mixed values. + * + * @throws Exception\RuntimeException + * @throws PDOException When the driver rejects a binding. + * + * @mago-expect analysis:mixed-assignment + */ protected function bindParametersFromContainer(): void { $resource = $this->getPreparedResource(); @@ -202,32 +228,11 @@ protected function bindParametersFromContainer(): void $errata = $this->parameterContainer->getErrataIterator()->getArrayCopy(); foreach ($parameters as $name => &$value) { - if (isset($errata[$name])) { - $type = match ($errata[$name]) { - ParameterContainer::TYPE_INTEGER => PDO::PARAM_INT, - ParameterContainer::TYPE_NULL => PDO::PARAM_NULL, - ParameterContainer::TYPE_LOB => PDO::PARAM_LOB, - default => PDO::PARAM_STR, - }; - } else { - $type = match (true) { - is_int($value) => PDO::PARAM_INT, - false === $value, true === $value => PDO::PARAM_BOOL, - null === $value => PDO::PARAM_NULL, - default => PDO::PARAM_STR, - }; - } - - if (is_int($name)) { - $parameter = $name + 1; - } else { - if (! preg_match('/^:?[a-zA-Z0-9_]+$/', $name)) { - throw Exception\RuntimeException::forInvalidPdoParam($name); - } - $parameter = ':' . ltrim($name, ':'); - } - - $resource->bindParam($parameter, $value, $type); + $resource->bindParam( + $this->toPdoParameter($name), + $value, + $this->toPdoType($errata[$name] ?? null, $value), + ); } $this->parametersBound = true; @@ -241,6 +246,39 @@ private function getPreparedResource(): PDOStatement : throw Exception\RuntimeException::forUnpreparedStatement(); } + /** @throws Exception\RuntimeException When a named parameter contains invalid characters. */ + private function toPdoParameter(int|string $name): int|string + { + if (is_int($name)) { + return $name + 1; + } + + if (1 !== preg_match('/^:?[a-zA-Z0-9_]+$/', $name)) { + throw Exception\RuntimeException::forInvalidPdoParam($name); + } + + return ':' . ltrim($name, characters: ':'); + } + + private function toPdoType(mixed $errata, mixed $value): int + { + if (null !== $errata) { + return match ($errata) { + ParameterContainer::TYPE_INTEGER => PDO::PARAM_INT, + ParameterContainer::TYPE_NULL => PDO::PARAM_NULL, + ParameterContainer::TYPE_LOB => PDO::PARAM_LOB, + default => PDO::PARAM_STR, + }; + } + + return match (true) { + is_int($value) => PDO::PARAM_INT, + is_bool($value) => PDO::PARAM_BOOL, + null === $value => PDO::PARAM_NULL, + default => PDO::PARAM_STR, + }; + } + /** Perform a deep clone */ public function __clone(): void { diff --git a/src/Adapter/Driver/PdoConnectionInterface.php b/src/Adapter/Driver/PdoConnectionInterface.php index c91486c6b..d577eb919 100644 --- a/src/Adapter/Driver/PdoConnectionInterface.php +++ b/src/Adapter/Driver/PdoConnectionInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Adapter\Driver; +/** + * @api + */ interface PdoConnectionInterface extends ConnectionInterface { public function getDsn(): string; diff --git a/src/Adapter/Driver/PdoDriverAwareInterface.php b/src/Adapter/Driver/PdoDriverAwareInterface.php index 636e26f8e..87c067358 100644 --- a/src/Adapter/Driver/PdoDriverAwareInterface.php +++ b/src/Adapter/Driver/PdoDriverAwareInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Adapter\Driver; +/** + * @api + */ interface PdoDriverAwareInterface { /** Implementation should provide a fluent interface */ diff --git a/src/Adapter/Driver/PdoDriverInterface.php b/src/Adapter/Driver/PdoDriverInterface.php index ef3326a48..6afba3d4d 100644 --- a/src/Adapter/Driver/PdoDriverInterface.php +++ b/src/Adapter/Driver/PdoDriverInterface.php @@ -4,4 +4,7 @@ namespace PhpDb\Adapter\Driver; +/** + * @api + */ interface PdoDriverInterface extends DriverInterface {} diff --git a/src/Adapter/Driver/ResultInterface.php b/src/Adapter/Driver/ResultInterface.php index 75be8c068..8c050c91f 100644 --- a/src/Adapter/Driver/ResultInterface.php +++ b/src/Adapter/Driver/ResultInterface.php @@ -9,6 +9,11 @@ use PhpDb\Adapter\Exception; use PhpDb\ResultSet\ResultSetInterface; +/** + * @api + * + * @extends Iterator + */ interface ResultInterface extends Countable, Iterator { /** diff --git a/src/Adapter/Driver/StatementInterface.php b/src/Adapter/Driver/StatementInterface.php index 7c44898b6..de6c2d3c0 100644 --- a/src/Adapter/Driver/StatementInterface.php +++ b/src/Adapter/Driver/StatementInterface.php @@ -7,9 +7,16 @@ use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\StatementContainerInterface; +/** + * @api + */ interface StatementInterface extends StatementContainerInterface { - /** Execute */ + /** + * Execute + * + * @param ParameterContainer|array|null $parameters + */ public function execute(ParameterContainer|array|null $parameters = null): ?ResultInterface; /** diff --git a/src/Adapter/Exception/ErrorException.php b/src/Adapter/Exception/ErrorException.php index 0eef9de17..b1f186d13 100644 --- a/src/Adapter/Exception/ErrorException.php +++ b/src/Adapter/Exception/ErrorException.php @@ -6,4 +6,7 @@ use PhpDb\Exception; +/** + * @api + */ class ErrorException extends Exception\ErrorException {} diff --git a/src/Adapter/Exception/InvalidArgumentException.php b/src/Adapter/Exception/InvalidArgumentException.php index d7267f770..1d943b484 100644 --- a/src/Adapter/Exception/InvalidArgumentException.php +++ b/src/Adapter/Exception/InvalidArgumentException.php @@ -6,6 +6,9 @@ use PhpDb\Exception; +/** + * @api + */ class InvalidArgumentException extends Exception\InvalidArgumentException { final public const string INCORRECT_FLAG = 'Flag incorrectly set'; diff --git a/src/Adapter/Exception/InvalidConnectionParametersException.php b/src/Adapter/Exception/InvalidConnectionParametersException.php index 0c94dd564..2a255e360 100644 --- a/src/Adapter/Exception/InvalidConnectionParametersException.php +++ b/src/Adapter/Exception/InvalidConnectionParametersException.php @@ -4,10 +4,15 @@ namespace PhpDb\Adapter\Exception; +/** + * @api + */ class InvalidConnectionParametersException extends RuntimeException { + /** @var array */ protected array $parameters; + /** @param array $parameters The rejected connection parameters */ public function __construct(string $message, array $parameters) { parent::__construct($message); diff --git a/src/Adapter/Exception/InvalidQueryException.php b/src/Adapter/Exception/InvalidQueryException.php index 54aa2bbdb..daa592ebb 100644 --- a/src/Adapter/Exception/InvalidQueryException.php +++ b/src/Adapter/Exception/InvalidQueryException.php @@ -8,6 +8,9 @@ use function sprintf; +/** + * @api + */ class InvalidQueryException extends UnexpectedValueException { /** The driver's own error text, passed through verbatim. */ diff --git a/src/Adapter/Exception/RuntimeException.php b/src/Adapter/Exception/RuntimeException.php index d0361c55d..88db28ecd 100644 --- a/src/Adapter/Exception/RuntimeException.php +++ b/src/Adapter/Exception/RuntimeException.php @@ -14,6 +14,10 @@ class RuntimeException extends Exception\RuntimeException final public const string DISCONNECTED_ROLLBACK = 'Must be connected before you can rollback'; + final public const string BARE_PDO_CONNECTION = + 'A bare PDO instance cannot serve as the driver connection;' + . ' wrap it in a PdoConnectionInterface implementation'; + /** The driver's own error text, passed through verbatim. */ final public const string DRIVER_ERROR = '%s'; @@ -25,6 +29,10 @@ class RuntimeException extends Exception\RuntimeException 'The PDO param "%s" contains invalid characters.' . ' Only alphabetic characters, digits, and underscores (_) are allowed.'; + final public const string MISSING_CONNECTION_RESOURCE = 'Connecting did not produce a PDO resource'; + + final public const string MISSING_DRIVER = 'No driver has been set on this connection'; + final public const string MISSING_DSN = 'The DSN has not been set or constructed from parameters' . ' in connect() for this Connection'; @@ -59,6 +67,11 @@ public static function forAlreadyPrepared(): self return new self(self::ALREADY_PREPARED); } + public static function forBarePdoConnection(): self + { + return new self(self::BARE_PDO_CONNECTION); + } + public static function forDisconnectedRollback(): self { return new self(self::DISCONNECTED_ROLLBACK); @@ -79,6 +92,16 @@ public static function forInvalidPdoParam(string $name): self return new self(sprintf(self::INVALID_PDO_PARAM, $name)); } + public static function forMissingConnectionResource(): self + { + return new self(self::MISSING_CONNECTION_RESOURCE); + } + + public static function forMissingDriver(): self + { + return new self(self::MISSING_DRIVER); + } + public static function forMissingDsn(): self { return new self(self::MISSING_DSN); diff --git a/src/Adapter/ParameterContainer.php b/src/Adapter/ParameterContainer.php index e8ecdd4d1..7372afdee 100644 --- a/src/Adapter/ParameterContainer.php +++ b/src/Adapter/ParameterContainer.php @@ -21,17 +21,24 @@ use function ltrim; use function next; use function reset; +use function str_starts_with; use function substr; +/** + * @api + * + * @implements Iterator + * @implements ArrayAccess + */ class ParameterContainer implements Iterator, ArrayAccess, Countable { - public const TYPE_AUTO = 'auto'; - public const TYPE_NULL = 'null'; - public const TYPE_DOUBLE = 'double'; - public const TYPE_INTEGER = 'integer'; - public const TYPE_BINARY = 'binary'; - public const TYPE_STRING = 'string'; - public const TYPE_LOB = 'lob'; + public const string TYPE_AUTO = 'auto'; + public const string TYPE_NULL = 'null'; + public const string TYPE_DOUBLE = 'double'; + public const string TYPE_INTEGER = 'integer'; + public const string TYPE_BINARY = 'binary'; + public const string TYPE_STRING = 'string'; + public const string TYPE_LOB = 'lob'; /** * Data @@ -62,6 +69,8 @@ class ParameterContainer implements Iterator, ArrayAccess, Countable /** * Constructor + * + * @param array $data */ public function __construct(array $data = []) { @@ -159,30 +168,32 @@ public function next(): void #[ReturnTypeWillChange] public function offsetExists(mixed $name): bool { - return isset($this->data[$name]); + return null !== ($this->data[$name] ?? null); } /** * Offset get * + * A name that is not set is looked up again through the name mapping, without its + * leading colon. + * * @param string|int $name */ #[Override] #[ReturnTypeWillChange] public function offsetGet(mixed $name): mixed { - if (isset($this->data[$name])) { + if (null !== ($this->data[$name] ?? null)) { return $this->data[$name]; } - $normalizedName = ltrim($name, ':'); - if ( - isset($this->nameMapping[$normalizedName], $this->data[$this->nameMapping[$normalizedName]]) - ) { - return $this->data[$this->nameMapping[$normalizedName]]; + if (! is_string($name)) { + return null; } - return null; + $mappedName = $this->nameMapping[ltrim($name, characters: ':')] ?? null; + + return null === $mappedName ? null : $this->data[$mappedName] ?? null; } /** @@ -192,13 +203,9 @@ public function offsetGet(mixed $name): mixed */ public function offsetGetErrata(string|int $name): mixed { - if (is_int($name)) { - $name = $this->positions[$name]; - } - if (! array_key_exists($name, $this->data)) { - throw Exception\InvalidArgumentException::forMissingData(); - } - return $this->errata[$name]; + $name = $this->requireDataName($name); + + return $this->errata[$name] ?? null; } /** @@ -208,13 +215,9 @@ public function offsetGetErrata(string|int $name): mixed */ public function offsetGetMaxLength(string|int $name): mixed { - if (is_int($name)) { - $name = $this->positions[$name]; - } - if (! array_key_exists($name, $this->data)) { - throw Exception\InvalidArgumentException::forMissingData(); - } - return $this->maxLength[$name]; + $name = $this->requireDataName($name); + + return $this->maxLength[$name] ?? null; } /** @@ -222,10 +225,9 @@ public function offsetGetMaxLength(string|int $name): mixed */ public function offsetHasErrata(string|int $name): bool { - if (is_int($name)) { - $name = $this->positions[$name]; - } - return isset($this->errata[$name]); + $name = $this->nameAt($name); + + return null !== $name && null !== ($this->errata[$name] ?? null); } /** @@ -233,89 +235,82 @@ public function offsetHasErrata(string|int $name): bool */ public function offsetHasMaxLength(string|int $name): bool { - if (is_int($name)) { - $name = $this->positions[$name]; - } - return isset($this->maxLength[$name]); + $name = $this->nameAt($name); + + return null !== $name && null !== ($this->maxLength[$name] ?? null); } /** * Offset set * - * @param string|int|null $name + * An integer name addresses an existing position, or adds a parameter named after it. A + * string name may be given with a leading colon, and either form follows the name mapping. + * A null name appends a parameter named after the current count. Any other name is rejected. + * + * @param mixed $name * @throws Exception\InvalidArgumentException */ #[Override] #[ReturnTypeWillChange] public function offsetSet(mixed $name, mixed $value, mixed $errata = null, mixed $maxLength = null): void { - $isNewPosition = true; - - if (is_int($name)) { - if (isset($this->positions[$name])) { - $isNewPosition = false; - $name = $this->positions[$name]; - } else { - $name = (string) $name; - } - } elseif (is_string($name)) { - if (':' === $name[0]) { - $normalizedName = substr($name, 1); - if (isset($this->nameMapping[$normalizedName])) { - $name = $this->nameMapping[$normalizedName]; - } - } elseif (isset($this->nameMapping[$name])) { - $name = $this->nameMapping[$name]; - } - - $isNewPosition = ! isset($this->data[$name]); - - if (is_string($value) && isset($value[0]) && ':' === $value[0]) { - $this->nameMapping[substr($value, 1)] = $name; - } - } elseif (null === $name) { - $name = (string) count($this->data); - } else { - throw Exception\InvalidArgumentException::forInvalidKeyType(); + $key = match (true) { + is_int($name) => $this->positions[$name] ?? (string) $name, + is_string($name) => $this->nameMapping[str_starts_with($name, ':') ? substr($name, offset: 1) : $name] + ?? $name, + null === $name => (string) count($this->data), + default => throw Exception\InvalidArgumentException::forInvalidKeyType(), + }; + + $isNewPosition = is_int($name) + ? ! array_key_exists($name, $this->positions) + : null === ($this->data[$key] ?? null); + + if (is_string($name) && is_string($value) && str_starts_with($value, ':')) { + $this->nameMapping[substr($value, offset: 1)] = $key; } if ($isNewPosition) { - $this->positions[] = $name; + $this->positions[] = $key; } - $this->data[$name] = $value; + $this->data[$key] = $value; if (null !== $errata) { - $this->errata[$name] = $errata; + $this->errata[$key] = $errata; } if (null !== $maxLength) { - $this->maxLength[$name] = $maxLength; + $this->maxLength[$key] = $maxLength; } } /** * Offset set errata + * + * @throws Exception\InvalidArgumentException When an integer name is not a known position. */ public function offsetSetErrata(string|int $name, mixed $errata): void { - if (is_int($name)) { - $name = $this->positions[$name]; - } - $this->errata[$name] = $errata; + $this->errata[$this->requireName($name)] = $errata; } /** * Offset set max length + * + * @throws Exception\InvalidArgumentException When an integer name is not a known position. */ public function offsetSetMaxLength(string|int $name, mixed $maxLength): void { - if (is_int($name)) { - $name = $this->positions[$name]; - } - $this->maxLength[$name] = $maxLength; + $this->maxLength[$this->requireName($name)] = $maxLength; } + /** + * Bind a parameter to another by reference. Referencing a name that is not set creates it + * as null, so the two stay bound when either is set later. + * + * @mago-expect analysis:possibly-undefined-string-array-index + */ public function offsetSetReference(string|int $name, string|int $from): void { $this->data[$name] = &$this->data[$from]; @@ -323,13 +318,16 @@ public function offsetSetReference(string|int $name, string|int $from): void /** * Offset unset + * + * An integer name addresses a position when one exists, and is used as the name otherwise. + * The name keeps its position, so later positions do not shift. */ #[Override] #[ReturnTypeWillChange] public function offsetUnset(mixed $name): void { - if (is_int($name) && isset($this->positions[$name])) { - $name = $this->positions[$name]; + if (is_int($name)) { + $name = $this->positions[$name] ?? $name; } unset($this->data[$name]); } @@ -341,10 +339,8 @@ public function offsetUnset(mixed $name): void */ public function offsetUnsetErrata(string|int $name): void { - if (is_int($name)) { - $name = $this->positions[$name]; - } - if (! array_key_exists($name, $this->errata)) { + $name = $this->nameAt($name); + if (null === $name || ! array_key_exists($name, $this->errata)) { throw Exception\InvalidArgumentException::forMissingData(); } $this->errata[$name] = null; @@ -357,10 +353,8 @@ public function offsetUnsetErrata(string|int $name): void */ public function offsetUnsetMaxLength(string|int $name): void { - if (is_int($name)) { - $name = $this->positions[$name]; - } - if (! array_key_exists($name, $this->maxLength)) { + $name = $this->nameAt($name); + if (null === $name || ! array_key_exists($name, $this->maxLength)) { throw Exception\InvalidArgumentException::forMissingData(); } $this->maxLength[$name] = null; @@ -378,11 +372,17 @@ public function rewind(): void /** * Set from array + * + * Values are parameter values of any type. + * + * @param array $data + * + * @mago-expect analysis:mixed-assignment */ public function setFromArray(array $data): static { - foreach ($data as $n => $v) { - $this->offsetSet($n, $v); + foreach ($data as $name => $value) { + $this->offsetSet($name, $value); } return $this; } @@ -396,4 +396,26 @@ public function valid(): bool { return current($this->data) !== false; } + + /** Resolve an integer name to the parameter name at that position; null when there is none. */ + private function nameAt(string|int $name): ?string + { + return is_int($name) ? $this->positions[$name] ?? null : $name; + } + + /** @throws Exception\InvalidArgumentException When the name does not address a parameter. */ + private function requireDataName(string|int $name): string + { + $name = $this->requireName($name); + + return array_key_exists($name, $this->data) + ? $name + : throw Exception\InvalidArgumentException::forMissingData(); + } + + /** @throws Exception\InvalidArgumentException When an integer name is not a known position. */ + private function requireName(string|int $name): string + { + return $this->nameAt($name) ?? throw Exception\InvalidArgumentException::forMissingData(); + } } diff --git a/src/Adapter/Platform/AbstractPlatform.php b/src/Adapter/Platform/AbstractPlatform.php index dd09a77b5..77c5e5da3 100644 --- a/src/Adapter/Platform/AbstractPlatform.php +++ b/src/Adapter/Platform/AbstractPlatform.php @@ -20,11 +20,13 @@ use const PREG_SPLIT_NO_EMPTY; /** + * @api + * * @property Driver\DriverInterface|Driver\PdoDriverInterface|PDO $driver */ abstract class AbstractPlatform implements PlatformInterface { - /** @var string[] */ + /** @var array{string, string} The opening and closing identifier quotes */ protected array $quoteIdentifier = ['"', '"']; protected string $quoteIdentifierTo = '\''; @@ -34,7 +36,7 @@ abstract class AbstractPlatform implements PlatformInterface protected string $quoteIdentifierFragmentPattern = '/([^0-9,a-zA-Z$_:])/i'; /** @var array */ - private const SAFE_WORDS = ['*' => true, ' ' => true, '.' => true, 'as' => true]; + private const array SAFE_WORDS = ['*' => true, ' ' => true, '.' => true, 'as' => true]; /** * {@inheritDoc} @@ -86,11 +88,13 @@ public function quoteIdentifier(string $identifier): string #[Override] public function quoteIdentifierChain(array|string $identifierChain): string { - return '"' . implode('"."', (array) str_replace('"', '\\"', $identifierChain)) . '"'; + return '"' . implode('"."', (array) str_replace('"', replace: '\\"', subject: $identifierChain)) . '"'; } /** * {@inheritDoc} + * + * @param array $additionalSafeWords */ #[Override] public function quoteIdentifierInFragment(string $identifier, array $additionalSafeWords = []): string @@ -111,18 +115,14 @@ public function quoteIdentifierInFragment(string $identifier, array $additionalS PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY, ); - $quoteStart = $this->quoteIdentifier[0]; - $quoteEnd = $this->quoteIdentifier[1]; - $quoteTo = $this->quoteIdentifierTo; - $result = ''; - - foreach ($parts as $part) { - $lowerPart = strtolower($part); - if (isset($safeWords[$lowerPart])) { - $result .= $part; - } else { - $result .= $quoteStart . str_replace($quoteStart, $quoteTo, $part) . $quoteEnd; - } + [$quoteStart, $quoteEnd] = $this->quoteIdentifier; + $quoteTo = $this->quoteIdentifierTo; + $result = ''; + + foreach (false === $parts ? [] : $parts as $part) { + $result .= $safeWords[strtolower($part)] ?? false + ? $part + : $quoteStart . str_replace($quoteStart, $quoteTo, $part) . $quoteEnd; } return $result; @@ -134,19 +134,21 @@ public function quoteIdentifierInFragment(string $identifier, array $additionalS #[Override] public function quoteTrustedValue(int|float|string|bool $value): ?string { - return '\'' . addcslashes((string) $value, "\x00\n\r\\'\"\x1a") . '\''; + return '\'' . addcslashes((string) $value, characters: "\x00\n\r\\'\"\x1a") . '\''; } /** * {@inheritDoc} + * + * @throws Exception\RuntimeException When no driver is available to quote safely. */ #[Override] public function quoteValue(string $value): string { - if (! isset($this->driver)) { + if (null === ($this->driver ?? null)) { throw Exception\RuntimeException::forVulnerablePlatformQuote(static::class, __METHOD__); } - return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + return '\'' . addcslashes($value, characters: "\x00\n\r\\'\"\x1a") . '\''; } /** diff --git a/src/Adapter/Platform/PlatformInterface.php b/src/Adapter/Platform/PlatformInterface.php index 4a11ba25f..1ca43e0ff 100644 --- a/src/Adapter/Platform/PlatformInterface.php +++ b/src/Adapter/Platform/PlatformInterface.php @@ -6,6 +6,9 @@ use PhpDb\Sql\Platform\PlatformDecoratorInterface; +/** + * @api + */ interface PlatformInterface { /** @@ -50,6 +53,8 @@ public function quoteIdentifierChain(array|string $identifierChain): string; /** * Quote identifier in fragment + * + * @param array $additionalSafeWords */ public function quoteIdentifierInFragment(string $identifier, array $additionalSafeWords = []): string; diff --git a/src/Adapter/Platform/Sql92.php b/src/Adapter/Platform/Sql92.php index 61ab06081..d8e26dab5 100644 --- a/src/Adapter/Platform/Sql92.php +++ b/src/Adapter/Platform/Sql92.php @@ -11,9 +11,12 @@ use function addcslashes; +/** + * @api + */ class Sql92 extends AbstractPlatform { - final public const PLATFORM_NAME = 'SQL92'; + final public const string PLATFORM_NAME = 'SQL92'; /** * {@inheritDoc} @@ -35,13 +38,15 @@ public function getSqlPlatformDecorator(): PlatformDecoratorInterface /** * {@inheritDoc} + * + * @throws Exception\RuntimeException When no driver is available to quote safely. */ #[Override] public function quoteValue(string $value): string { - if (! isset($this->driver)) { + if (null === ($this->driver ?? null)) { throw Exception\RuntimeException::forVulnerablePlatformQuote(static::class, __METHOD__); } - return '\'' . addcslashes($value, "\x00\n\r\\'\"\x1a") . '\''; + return '\'' . addcslashes($value, characters: "\x00\n\r\\'\"\x1a") . '\''; } } diff --git a/src/Adapter/Profiler/Profiler.php b/src/Adapter/Profiler/Profiler.php index da67fecb7..d4b5e82f5 100644 --- a/src/Adapter/Profiler/Profiler.php +++ b/src/Adapter/Profiler/Profiler.php @@ -10,13 +10,16 @@ use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\StatementContainerInterface; -use function end; +use function array_key_exists; +use function array_key_last; use function is_string; use function microtime; /** + * @api + * * @phpstan-type ProfileShape array{ - * sql: string, + * sql: string|null, * parameters: ParameterContainer|null, * start: float, * end: float|null, @@ -37,7 +40,9 @@ class Profiler implements ProfilerInterface */ public function getLastProfile(): ?array { - return end($this->profiles); + $lastIndex = array_key_last($this->profiles); + + return null === $lastIndex ? null : $this->profiles[$lastIndex] ?? null; } /** @@ -50,11 +55,13 @@ public function getProfiles(): array /** * @return $this Provides a fluent interface + * + * @throws Exception\RuntimeException When no profile has been started. */ #[Override] public function profilerFinish(): ProfilerInterface { - if (! isset($this->profiles[$this->currentIndex])) { + if (! array_key_exists($this->currentIndex, $this->profiles)) { throw Exception\RuntimeException::forUnstartedProfile(__FUNCTION__); } $current = &$this->profiles[$this->currentIndex]; @@ -71,26 +78,17 @@ public function profilerFinish(): ProfilerInterface #[Override] public function profilerStart(string|StatementContainerInterface $target): ProfilerInterface { - $profileInformation = [ - 'sql' => '', - 'parameters' => null, - 'start' => microtime(true), + $start = microtime(true); + $container = is_string($target) ? null : $target->getParameterContainer(); + + $this->profiles[$this->currentIndex] = [ + 'sql' => is_string($target) ? $target : $target->getSql(), + 'parameters' => null === $container ? null : clone $container, + 'start' => $start, 'end' => null, 'elapse' => null, ]; - if (is_string($target)) { - $profileInformation['sql'] = $target; - } else { - $profileInformation['sql'] = $target->getSql(); - $container = $target->getParameterContainer(); - if (null !== $container) { - $profileInformation['parameters'] = clone $container; - } - } - - $this->profiles[$this->currentIndex] = $profileInformation; - return $this; } } diff --git a/src/Adapter/Profiler/ProfilerAwareInterface.php b/src/Adapter/Profiler/ProfilerAwareInterface.php index eeb13aacc..a22a47cbe 100644 --- a/src/Adapter/Profiler/ProfilerAwareInterface.php +++ b/src/Adapter/Profiler/ProfilerAwareInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Adapter\Profiler; +/** + * @api + */ interface ProfilerAwareInterface { /** Implementation should provide a fluent interface */ diff --git a/src/Adapter/Profiler/ProfilerInterface.php b/src/Adapter/Profiler/ProfilerInterface.php index 448a363e5..805cd34d0 100644 --- a/src/Adapter/Profiler/ProfilerInterface.php +++ b/src/Adapter/Profiler/ProfilerInterface.php @@ -6,6 +6,9 @@ use PhpDb\Adapter\StatementContainerInterface; +/** + * @api + */ interface ProfilerInterface { /** diff --git a/src/Adapter/SchemaAwareInterface.php b/src/Adapter/SchemaAwareInterface.php index 4c4ff1d8e..71c669dc8 100644 --- a/src/Adapter/SchemaAwareInterface.php +++ b/src/Adapter/SchemaAwareInterface.php @@ -4,12 +4,13 @@ namespace PhpDb\Adapter; +/** + * @api + */ interface SchemaAwareInterface { /** * Get current schema - * - * todo: narrow this to string|false when version bumps to PHP 8.2 minimum */ public function getCurrentSchema(): string|false; } diff --git a/src/Adapter/StatementContainer.php b/src/Adapter/StatementContainer.php index e44986b9f..6baf74ae7 100644 --- a/src/Adapter/StatementContainer.php +++ b/src/Adapter/StatementContainer.php @@ -1,9 +1,14 @@ sql = $sql; + $this->sql = $sql ?? ''; return $this; } } diff --git a/src/Adapter/StatementContainerInterface.php b/src/Adapter/StatementContainerInterface.php index 59ca305bd..35126f5af 100644 --- a/src/Adapter/StatementContainerInterface.php +++ b/src/Adapter/StatementContainerInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Adapter; +/** + * @api + */ interface StatementContainerInterface { /** Get parameter container */ diff --git a/test/unit/Adapter/AdapterTest.php b/test/unit/Adapter/AdapterTest.php index ca137f235..ac5cfe4f9 100644 --- a/test/unit/Adapter/AdapterTest.php +++ b/test/unit/Adapter/AdapterTest.php @@ -20,6 +20,7 @@ use PhpDb\ResultSet\ResultSetInterface; use PhpDbTest\TestAsset\TemporaryResultSet; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; @@ -52,6 +53,17 @@ final class AdapterTest extends TestCase protected Adapter $adapter; + /** + * @return array + */ + public static function helperProvider(): array + { + return [ + 'quote identifier' => [Adapter::FUNCTION_QUOTE_IDENTIFIER, 'quoteIdentifier'], + 'quote value' => [Adapter::FUNCTION_QUOTE_VALUE, 'quoteValue'], + ]; + } + #[Test] public function constructorWithProfilerDelegatesToSetProfiler(): void { @@ -68,6 +80,19 @@ public function constructorWithProfilerDelegatesToSetProfiler(): void static::assertSame($profilerMock, $adapter->getProfiler()); } + #[Test] + public function constructorWithProfilerPassesItToAProfilerAwareDriver(): void + { + $profiler = $this->createStub(Profiler\ProfilerInterface::class); + $driver = $this->createMockForIntersectionOfInterfaces([ + DriverInterface::class, + Profiler\ProfilerAwareInterface::class, + ]); + $driver->expects(self::once())->method('setProfiler')->with($profiler); + + new Adapter($driver, $this->createStub(PlatformInterface::class), profiler: $profiler); + } + #[Test] #[TestDox('unit test: Test createStatement() produces a statement object')] public function createStatementDelegatesToDriver(): void @@ -75,6 +100,19 @@ public function createStatementDelegatesToDriver(): void static::assertSame($this->mockStatement, $this->adapter->createStatement()); } + #[Test] + public function createStatementSetsTheInitialParametersOnTheStatement(): void + { + $this->mockStatement + ->expects(self::once()) + ->method('setParameterContainer') + ->with(self::callback( + static fn(ParameterContainer $container): bool => ['id' => 5] === $container->getNamedArray(), + )); + + $this->adapter->createStatement('SELECT 1', ['id' => 5]); + } + #[Test] #[TestDox('unit test: Test executeQuery() returns the raw result without wrapping query results')] public function executeQueryReturnsRawResultWithoutWrappingQueryResults(): void @@ -147,6 +185,31 @@ public function getDriverReturnsDriver(): void static::assertSame($this->mockDriver, $this->adapter->getDriver()); } + /** + * @param non-empty-string $platformMethod + */ + #[Test] + #[DataProvider('helperProvider')] + public function getHelpersFunctionsAcceptValueAsNamedArgument(string $helper, string $platformMethod): void + { + $this->mockPlatform + ->method($platformMethod) + ->with('test') + ->willReturn('"test"'); + + $functions = $this->adapter->getHelpers($helper); + + static::assertSame('"test"', $functions[0](value: 'test')); + } + + #[Test] + public function getHelpersIgnoresUnknownAndNonStringArguments(): void + { + $functions = $this->adapter->getHelpers('unknown', true, 1, Adapter::FUNCTION_QUOTE_IDENTIFIER); + + static::assertCount(1, $functions); + } + #[Test] public function getHelpersQuoteIdentifierClosureCallsPlatform(): void { diff --git a/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php b/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php index 66f450404..4cf0f3d5a 100644 --- a/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php +++ b/test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php @@ -10,6 +10,7 @@ use PhpDb\Adapter\Driver\Feature\DriverFeatureProviderTrait; use PhpDb\Adapter\Exception\RuntimeException; use PhpDbTest\Adapter\Driver\TestAsset\TestFeatureDriver; +use PhpDbTest\Adapter\Driver\TestAsset\TestNonProviderFeatureDriver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; @@ -21,8 +22,17 @@ #[CoversMethod(DriverFeatureProviderTrait::class, 'addFeature')] #[CoversMethod(DriverFeatureProviderTrait::class, 'addFeatures')] #[CoversMethod(DriverFeatureProviderTrait::class, 'getFeature')] +#[CoversMethod(DriverFeatureProviderTrait::class, 'asFeatureProvidingDriver')] final class DriverFeatureProviderTraitTest extends TestCase { + #[Test] + public function addFeatureReturnsTheDriver(): void + { + $driver = new TestFeatureDriver(); + + static::assertSame($driver, $driver->addFeature($this->createMock(DriverFeatureInterface::class))); + } + #[Test] public function addFeaturesAddsMultipleFeatures(): void { @@ -47,6 +57,48 @@ public function addFeatureSetsDriverAndStoresFeature(): void static::assertSame($feature, $driver->getFeature($feature::class)); } + #[Test] + public function addFeaturesReturnsTheDriver(): void + { + $driver = new TestFeatureDriver(); + + static::assertSame($driver, $driver->addFeatures([$this->createMock(DriverFeatureInterface::class)])); + } + + #[Test] + public function addFeaturesThrowsWhenUsedOutsideDriverInterfaceEvenWithNoFeatures(): void + { + $nonDriver = new class implements DriverFeatureProviderInterface { + use DriverFeatureProviderTrait; + }; + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf( + RuntimeException::UNCOMPOSABLE_TRAIT, + DriverFeatureProviderTrait::class, + DriverInterface::class, + )); + + $nonDriver->addFeatures([]); + } + + #[Test] + public function addFeatureThrowsWhenTheDriverIsNotAFeatureProvider(): void + { + $driver = new TestNonProviderFeatureDriver(); + $feature = $this->createMock(DriverFeatureInterface::class); + $feature->expects(self::never())->method('setDriver'); + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf( + RuntimeException::UNCOMPOSABLE_TRAIT, + DriverFeatureProviderTrait::class, + DriverFeatureProviderInterface::class, + )); + + $driver->addFeature($feature); + } + #[Test] public function addFeatureThrowsWhenUsedOutsideDriverInterface(): void { diff --git a/test/unit/Adapter/Driver/Pdo/ConnectionTest.php b/test/unit/Adapter/Driver/Pdo/ConnectionTest.php index 81fae63e1..35f0e5f64 100644 --- a/test/unit/Adapter/Driver/Pdo/ConnectionTest.php +++ b/test/unit/Adapter/Driver/Pdo/ConnectionTest.php @@ -4,6 +4,7 @@ namespace PhpDbTest\Adapter\Driver\Pdo; +use Closure; use Exception; use Override; use PDO; @@ -16,7 +17,9 @@ use PhpDbTest\Adapter\Driver\Pdo\TestAsset\SqliteMemoryPdo; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestConnection; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestPdo; +use PhpDbTest\Adapter\Driver\Pdo\TestAsset\UnconnectableConnection; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -32,11 +35,23 @@ #[CoversMethod(AbstractPdoConnection::class, 'commit')] #[CoversMethod(AbstractPdoConnection::class, 'setResource')] #[CoversMethod(AbstractPdoConnection::class, 'prepare')] +#[CoversMethod(AbstractPdoConnection::class, 'getConnectedResource')] +#[CoversMethod(AbstractPdoConnection::class, 'getDriver')] #[Group('unit')] final class ConnectionTest extends TestCase { protected TestConnection $connection; + /** @return array */ + public static function connectedOperationProvider(): array + { + return [ + 'begin transaction' => [static fn(AbstractPdoConnection $c): mixed => $c->beginTransaction()], + 'commit' => [static fn(AbstractPdoConnection $c): mixed => $c->commit()], + 'execute' => [static fn(AbstractPdoConnection $c): mixed => $c->execute('SELECT 1')], + ]; + } + #[Test] public function beginTransactionAutoConnectsWhenNotConnected(): void { @@ -127,6 +142,17 @@ public function executeCallsProfilerStartAndFinish(): void $connection->execute('SELECT 1'); } + #[Test] + public function executeThrowsWhenNoDriverIsSet(): void + { + $connection = new TestConnection(new SqliteMemoryPdo()); + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::MISSING_DRIVER); + + $connection->execute('SELECT 1'); + } + #[Test] public function fluentSetDriver(): void { @@ -183,6 +209,29 @@ public function prepareAutoConnectsAndReturnsStatement(): void static::assertInstanceOf(Statement::class, $statement); } + #[Test] + public function prepareConnectsBeforeCreatingTheStatement(): void + { + $connection = new TestConnection(['dsn' => 'sqlite::memory:']); + $driver = $this->createStub(PdoDriverInterface::class); + $connection->setDriver($driver); + + $connection->prepare('SELECT 1'); + + static::assertTrue($connection->isConnected()); + } + + #[Test] + public function prepareThrowsWhenNoDriverIsSet(): void + { + $connection = new TestConnection(new SqliteMemoryPdo()); + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::MISSING_DRIVER); + + $connection->prepare('SELECT 1'); + } + /** * Test getResource method tries to connect to the database, it should never return null */ @@ -203,6 +252,39 @@ public function setConnectionParametersStoresParams(): void static::assertSame($params, $this->connection->getConnectionParameters()); } + #[Test] + public function setResourceConnectsWithTheGivenResource(): void + { + $connection = new TestConnection([]); + $pdo = new SqliteMemoryPdo(); + + static::assertSame($connection, $connection->setResource($pdo)); + static::assertSame($pdo, $connection->getResource()); + } + + #[Test] + public function setResourceStoresTheLowercaseDriverName(): void + { + $pdo = $this->createStub(PDO::class); + $pdo->method('getAttribute')->willReturn('SQLite'); + + $connection = new TestConnection([]); + $connection->setResource($pdo); + + static::assertSame('sqlite', $connection->getDriverName()); + } + + /** @param Closure(AbstractPdoConnection): mixed $operation */ + #[Test] + #[DataProvider('connectedOperationProvider')] + public function throwsWhenConnectingDoesNotProduceAPdoResource(Closure $operation): void + { + self::expectException(RuntimeException::class); + self::expectExceptionMessage(RuntimeException::MISSING_CONNECTION_RESOURCE); + + $operation(new UnconnectableConnection()); + } + /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. diff --git a/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php b/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php index f16face93..d719a0714 100644 --- a/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php +++ b/test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php @@ -9,6 +9,7 @@ use PhpDb\Adapter\Driver\ConnectionInterface; use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Exception\RuntimeException; +use PhpDbTest\Adapter\Driver\Pdo\TestAsset\SqliteMemoryPdo; use PhpDbTest\TestAsset\ConnectionWrapper; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; @@ -41,6 +42,29 @@ public function beginTransactionSetsInTransactionAtTrue(): void static::assertTrue($this->wrapper->inTransaction()); } + #[Test] + public function beginTransactionStartsATransactionOnThePdoResource(): void + { + $pdo = new SqliteMemoryPdo(); + $connection = new ConnectionWrapper($pdo); + + $connection->beginTransaction(); + + static::assertTrue($pdo->inTransaction()); + } + + #[Test] + public function commitCommitsThePdoResourceTransaction(): void + { + $pdo = new SqliteMemoryPdo(); + $connection = new ConnectionWrapper($pdo); + $connection->beginTransaction(); + + $connection->commit(); + + static::assertFalse($pdo->inTransaction()); + } + #[Test] public function commitReturnsInstanceOfConnection(): void { @@ -133,6 +157,18 @@ public function rollbackReturnsInstanceOfConnection(): void static::assertInstanceOf(ConnectionInterface::class, $this->wrapper->rollback()); } + #[Test] + public function rollbackRollsBackThePdoResourceTransaction(): void + { + $pdo = new SqliteMemoryPdo(); + $connection = new ConnectionWrapper($pdo); + $connection->beginTransaction(); + + $connection->rollback(); + + static::assertFalse($pdo->inTransaction()); + } + #[Test] public function rollbackSetsInTransactionAtFalse(): void { diff --git a/test/unit/Adapter/Driver/Pdo/PdoTest.php b/test/unit/Adapter/Driver/Pdo/PdoTest.php index caa6368cd..3a448073d 100644 --- a/test/unit/Adapter/Driver/Pdo/PdoTest.php +++ b/test/unit/Adapter/Driver/Pdo/PdoTest.php @@ -4,14 +4,17 @@ namespace PhpDbTest\Adapter\Driver\Pdo; +use Closure; use Error; use Override; use PDOStatement; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\Feature\DriverFeatureInterface; use PhpDb\Adapter\Driver\Pdo\AbstractPdo; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; +use PhpDb\Adapter\Exception\RuntimeException as AdapterRuntimeException; use PhpDb\Adapter\Profiler\ProfilerInterface; use PhpDb\Exception\RuntimeException; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\SqliteMemoryPdo; @@ -33,11 +36,22 @@ #[CoversMethod(AbstractPdo::class, 'setProfiler')] #[CoversMethod(AbstractPdo::class, 'getProfiler')] #[CoversMethod(AbstractPdo::class, 'formatParameterName')] +#[CoversMethod(AbstractPdo::class, 'getPdoConnection')] #[Group('unit')] final class PdoTest extends TestCase { protected TestPdo $pdo; + /** @return array */ + public static function connectionOperationProvider(): array + { + return [ + 'create statement' => [static fn(AbstractPdo $pdo): mixed => $pdo->createStatement('SELECT 1')], + 'get connection' => [static fn(AbstractPdo $pdo): mixed => $pdo->getConnection()], + 'get last generated id' => [static fn(AbstractPdo $pdo): mixed => $pdo->getLastGeneratedValue()], + ]; + } + /** @psalm-return array */ public static function getInvalidParamName(): array { @@ -93,6 +107,37 @@ public function constructorSetsDriverOnConnection(): void static::assertSame($connection, $pdo->getConnection()); } + #[Test] + public function createStatementConnectsADisconnectedConnection(): void + { + $connection = $this->createMock(AbstractPdoConnection::class); + $connection->method('isConnected')->willReturn(false); + $connection->method('getResource')->willReturn(new SqliteMemoryPdo()); + $connection->expects(self::once())->method('connect'); + + (new TestPdo($connection))->createStatement('SELECT 1'); + } + + #[Test] + public function createStatementDoesNotReconnectAConnectedConnection(): void + { + $connection = $this->createMock(AbstractPdoConnection::class); + $connection->method('isConnected')->willReturn(true); + $connection->method('getResource')->willReturn(new SqliteMemoryPdo()); + $connection->expects(self::never())->method('connect'); + + (new TestPdo($connection))->createStatement('SELECT 1'); + } + + #[Test] + public function createStatementReturnsACopyOfThePrototype(): void + { + $prototype = new Statement(); + $pdo = new TestPdo(new TestConnection(new SqliteMemoryPdo()), $prototype); + + static::assertNotSame($prototype, $pdo->createStatement('SELECT 1')); + } + #[Test] public function createStatementWithNullConnectsAndInitializes(): void { @@ -219,6 +264,17 @@ public function setProfilerPropagatesProfilerToConnectionAndStatement(): void static::assertSame($profiler, $statement->getProfiler()); } + /** @param Closure(AbstractPdo): mixed $operation */ + #[Test] + #[DataProvider('connectionOperationProvider')] + public function throwsForABarePdoConnection(Closure $operation): void + { + self::expectException(AdapterRuntimeException::class); + self::expectExceptionMessage(AdapterRuntimeException::BARE_PDO_CONNECTION); + + $operation(new TestPdo(new SqliteMemoryPdo())); + } + /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. diff --git a/test/unit/Adapter/Driver/Pdo/ResultTest.php b/test/unit/Adapter/Driver/Pdo/ResultTest.php index e4f9cd1a8..11cde0e9d 100644 --- a/test/unit/Adapter/Driver/Pdo/ResultTest.php +++ b/test/unit/Adapter/Driver/Pdo/ResultTest.php @@ -22,6 +22,7 @@ #[CoversMethod(Result::class, 'current')] #[CoversMethod(Result::class, 'count')] +#[CoversMethod(Result::class, 'resolveRowCount')] #[CoversMethod(Result::class, 'initialize')] #[CoversMethod(Result::class, 'isBuffered')] #[CoversMethod(Result::class, 'getFetchMode')] diff --git a/test/unit/Adapter/Driver/Pdo/StatementTest.php b/test/unit/Adapter/Driver/Pdo/StatementTest.php index 572c9065b..4db3cedd7 100644 --- a/test/unit/Adapter/Driver/Pdo/StatementTest.php +++ b/test/unit/Adapter/Driver/Pdo/StatementTest.php @@ -15,6 +15,7 @@ use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\Profiler\ProfilerInterface; +use PhpDbTest\Adapter\Driver\Pdo\TestAsset\BindingObservableStatement; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\SqliteMemoryPdo; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestConnection; use PhpDbTest\Adapter\Driver\Pdo\TestAsset\TestPdo; @@ -38,6 +39,8 @@ #[CoversMethod(Statement::class, 'isPrepared')] #[CoversMethod(Statement::class, 'execute')] #[CoversMethod(Statement::class, 'bindParametersFromContainer')] +#[CoversMethod(Statement::class, 'toPdoParameter')] +#[CoversMethod(Statement::class, 'toPdoType')] #[CoversMethod(Statement::class, 'setProfiler')] #[CoversMethod(Statement::class, 'getProfiler')] #[CoversMethod(Statement::class, 'initialize')] @@ -49,6 +52,21 @@ final class StatementTest extends TestCase { protected Statement $statement; + /** @return array */ + public static function boundTypeProvider(): array + { + return [ + 'integer errata' => [42, ParameterContainer::TYPE_INTEGER, 'integer'], + 'null errata' => ['data', ParameterContainer::TYPE_NULL, 'null'], + 'lob errata' => ['data', ParameterContainer::TYPE_LOB, 'blob'], + 'other errata binds a string' => [42, ParameterContainer::TYPE_BINARY, 'text'], + 'integer value' => [42, null, 'integer'], + 'boolean value' => [true, null, 'integer'], + 'null value' => [null, null, 'null'], + 'string value' => ['42', null, 'text'], + ]; + } + /** @return array */ public static function invalidParameterNameProvider(): array { @@ -62,6 +80,18 @@ public static function invalidParameterNameProvider(): array ]; } + #[Test] + public function bindingMarksTheParametersAsBound(): void + { + $statement = new BindingObservableStatement(new ParameterContainer(['val' => 1])); + $statement->initialize(new SqliteMemoryPdo()); + $statement->prepare('SELECT :val'); + + $statement->bind(); + + static::assertTrue($statement->hasBoundParameters()); + } + #[Test] public function bindParametersDetectsBooleanValueType(): void { @@ -193,6 +223,56 @@ public function bindParametersWithPositionalIntegers(): void static::assertInstanceOf(Result::class, $result); } + #[Test] + public function bindsAColonPrefixedNameWithASingleColon(): void + { + $pdo = new SqliteMemoryPdo(); + $this->statement->setDriver(new TestPdo(new TestConnection($pdo))); + $this->statement->initialize($pdo); + $this->statement->setSql('SELECT :val AS t'); + + $container = new ParameterContainer(); + $container->offsetSet(':val', 'bound'); + $this->statement->setParameterContainer($container); + + $row = $this->statement->execute()?->current(); + + static::assertSame('bound', is_array($row) ? $row['t'] ?? null : null); + } + + #[Test] + public function bindsANullValueAsPdoNull(): void + { + $resource = $this->createMock(PDOStatement::class); + $resource->expects(self::once())->method('bindParam')->with(':val', null, PDO::PARAM_NULL); + + $statement = new BindingObservableStatement(new ParameterContainer(['val' => null])); + $statement->setResource($resource); + + $statement->bind(); + } + + #[Test] + #[DataProvider('boundTypeProvider')] + public function bindsEachParameterWithThePdoTypeForItsErrataOrValue( + mixed $value, + ?string $errata, + string $expectedType, + ): void { + $pdo = new SqliteMemoryPdo(); + $this->statement->setDriver(new TestPdo(new TestConnection($pdo))); + $this->statement->initialize($pdo); + $this->statement->setSql('SELECT typeof(:val) AS t'); + + $container = new ParameterContainer(); + $container->offsetSet('val', $value, $errata); + $this->statement->setParameterContainer($container); + + $row = $this->statement->execute()?->current(); + + static::assertSame($expectedType, is_array($row) ? $row['t'] ?? null : null); + } + #[Test] public function cloneClonesParameterContainerWhenSet(): void { @@ -224,6 +304,19 @@ public function cloneResetsState(): void ); } + #[Test] + public function cloneResetsTheBoundState(): void + { + $statement = new BindingObservableStatement(new ParameterContainer(['val' => 1])); + $statement->initialize(new SqliteMemoryPdo()); + $statement->prepare('SELECT :val'); + $statement->bind(); + + $clone = clone $statement; + + static::assertFalse($clone->hasBoundParameters()); + } + #[Test] public function constructorAcceptsParameterContainerAndOptions(): void { @@ -383,6 +476,20 @@ public function executeWithArrayParametersMergesIntoContainer(): void static::assertInstanceOf(Result::class, $result); } + #[Test] + public function executeWithNoParametersDoesNotBind(): void + { + $pdo = new SqliteMemoryPdo(); + $statement = new BindingObservableStatement(); + $statement->setDriver(new TestPdo(new TestConnection($pdo))); + $statement->initialize($pdo); + $statement->setSql('SELECT 1'); + + $statement->execute(); + + static::assertFalse($statement->hasBoundParameters()); + } + #[Test] public function executeWithParameterContainerSetsContainer(): void { @@ -563,6 +670,15 @@ public function setSql(): void static::assertSame('SELECT 1', $this->statement->getSql()); } + #[Test] + public function setSqlStoresNullAsAnEmptyString(): void + { + $this->statement->setSql('SELECT 1'); + $this->statement->setSql(null); + + static::assertSame('', $this->statement->getSql()); + } + /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. diff --git a/test/unit/Adapter/Driver/Pdo/TestAsset/BindingObservableStatement.php b/test/unit/Adapter/Driver/Pdo/TestAsset/BindingObservableStatement.php new file mode 100644 index 000000000..eb9990f5d --- /dev/null +++ b/test/unit/Adapter/Driver/Pdo/TestAsset/BindingObservableStatement.php @@ -0,0 +1,23 @@ +bindParametersFromContainer(); + } + + public function hasBoundParameters(): bool + { + return $this->parametersBound; + } +} diff --git a/test/unit/Adapter/Driver/Pdo/TestAsset/UnconnectableConnection.php b/test/unit/Adapter/Driver/Pdo/TestAsset/UnconnectableConnection.php new file mode 100644 index 000000000..cfb8813cd --- /dev/null +++ b/test/unit/Adapter/Driver/Pdo/TestAsset/UnconnectableConnection.php @@ -0,0 +1,33 @@ + [ + 'forBarePdoConnection', + [], + RuntimeException::BARE_PDO_CONNECTION, + ], 'disconnected rollback' => [ 'forDisconnectedRollback', [], @@ -63,6 +71,16 @@ public static function namedConstructorProvider(): array ['bad-name'], RuntimeException::INVALID_PDO_PARAM, ], + 'missing connection resource' => [ + 'forMissingConnectionResource', + [], + RuntimeException::MISSING_CONNECTION_RESOURCE, + ], + 'missing driver' => [ + 'forMissingDriver', + [], + RuntimeException::MISSING_DRIVER, + ], 'missing dsn' => [ 'forMissingDsn', [], diff --git a/test/unit/Adapter/ParameterContainerTest.php b/test/unit/Adapter/ParameterContainerTest.php index 532e3c8ea..474e8391c 100644 --- a/test/unit/Adapter/ParameterContainerTest.php +++ b/test/unit/Adapter/ParameterContainerTest.php @@ -4,10 +4,12 @@ namespace PhpDbTest\Adapter; +use Closure; use Override; use PhpDb\Adapter\Exception\InvalidArgumentException; use PhpDb\Adapter\ParameterContainer; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; @@ -38,11 +40,43 @@ #[CoversMethod(ParameterContainer::class, '__construct')] #[CoversMethod(ParameterContainer::class, 'offsetSetReference')] #[CoversMethod(ParameterContainer::class, 'getPositionalArray')] +#[CoversMethod(ParameterContainer::class, 'nameAt')] +#[CoversMethod(ParameterContainer::class, 'requireName')] +#[CoversMethod(ParameterContainer::class, 'requireDataName')] #[Group('unit')] final class ParameterContainerTest extends TestCase { protected ParameterContainer $parameterContainer; + /** @return array */ + public static function unknownPositionProvider(): array + { + return [ + 'get errata' => [static fn(ParameterContainer $c): mixed => $c->offsetGetErrata(5)], + 'get max length' => [static fn(ParameterContainer $c): mixed => $c->offsetGetMaxLength(5)], + 'set errata' => [ + static function (ParameterContainer $c): void { + $c->offsetSetErrata(5, ParameterContainer::TYPE_INTEGER); + }, + ], + 'set max length' => [ + static function (ParameterContainer $c): void { + $c->offsetSetMaxLength(5, 10); + }, + ], + 'unset errata' => [ + static function (ParameterContainer $c): void { + $c->offsetUnsetErrata(5); + }, + ], + 'unset max length' => [ + static function (ParameterContainer $c): void { + $c->offsetUnsetMaxLength(5); + }, + ], + ]; + } + #[Test] public function constructorWithDataPopulatesContainer(): void { @@ -128,6 +162,14 @@ public function offsetExists(): void static::assertFalse(isset($this->parameterContainer['bar'])); } + #[Test] + public function offsetExistsIsFalseForANullValue(): void + { + $container = new ParameterContainer(['foo' => null]); + + static::assertFalse($container->offsetExists('foo')); + } + #[Test] #[TestDox('unit test: Test offsetGet() returns proper values via method call and array access')] public function offsetGet(): void @@ -157,6 +199,14 @@ public function offsetGetErrataByPositionalIndex(): void static::assertSame(ParameterContainer::TYPE_INTEGER, $container->offsetGetErrata(0)); } + #[Test] + public function offsetGetErrataReturnsNullWhenTheParameterHasNoErrata(): void + { + $container = new ParameterContainer(['foo' => 'bar']); + + static::assertNull($container->offsetGetErrata('foo')); + } + #[Test] public function offsetGetErrataThrowsWhenNameDoesNotExist(): void { @@ -177,6 +227,14 @@ public function offsetGetMaxLengthByPositionalIndex(): void static::assertSame(50, $container->offsetGetMaxLength(0)); } + #[Test] + public function offsetGetMaxLengthReturnsNullWhenTheParameterHasNoMaxLength(): void + { + $container = new ParameterContainer(['foo' => 'bar']); + + static::assertNull($container->offsetGetMaxLength('foo')); + } + #[Test] public function offsetGetMaxLengthThrowsWhenNameDoesNotExist(): void { @@ -188,6 +246,34 @@ public function offsetGetMaxLengthThrowsWhenNameDoesNotExist(): void $container->offsetGetMaxLength('nonexistent'); } + #[Test] + public function offsetGetResolvesAColonPrefixedNameThroughTheNameMapping(): void + { + $container = new ParameterContainer(); + $container->offsetSet('c_0', ':alias'); + $container->offsetSet('c_0', 'value'); + + static::assertSame('value', $container->offsetGet(':alias')); + } + + #[Test] + public function offsetGetReturnsNullForAMappedNameWhoseParameterIsNotSet(): void + { + $container = new ParameterContainer(); + $container->offsetSet('c_0', ':alias'); + $container->offsetUnset('c_0'); + + static::assertNull($container->offsetGet('alias')); + } + + #[Test] + public function offsetGetReturnsNullForAnIntegerNameThatIsNotSet(): void + { + $container = new ParameterContainer(['foo' => 'bar']); + + static::assertNull($container->offsetGet(5)); + } + #[Test] #[TestDox('unit test: Test offsetHasErrata() will check if errata exists for a particular key')] public function offsetHasErrata(): void @@ -205,6 +291,15 @@ public function offsetHasErrataByPositionalIndex(): void static::assertTrue($container->offsetHasErrata(0)); } + #[Test] + public function offsetHasErrataIsFalseForAnUnknownPosition(): void + { + $container = new ParameterContainer(['foo' => 'bar']); + $container->offsetSetErrata('foo', ParameterContainer::TYPE_INTEGER); + + static::assertFalse($container->offsetHasErrata(5)); + } + #[Test] #[TestDox('unit test: Test offsetHasMaxLength() will check if errata exists for a particular key')] public function offsetHasMaxLength(): void @@ -222,6 +317,15 @@ public function offsetHasMaxLengthByPositionalIndex(): void static::assertTrue($container->offsetHasMaxLength(0)); } + #[Test] + public function offsetHasMaxLengthIsFalseForAnUnknownPosition(): void + { + $container = new ParameterContainer(['foo' => 'bar']); + $container->offsetSetMaxLength('foo', 10); + + static::assertFalse($container->offsetHasMaxLength(5)); + } + #[Test] #[TestDox('unit test: Test offsetSet() works via method call and array access')] public function offsetSet(): void @@ -282,6 +386,16 @@ public function offsetSetAndGetMaxLength(): void static::assertSame(100, $this->parameterContainer->offsetGetMaxLength('foo')); } + #[Test] + public function offsetSetAtAKnownPositionDoesNotAddAPosition(): void + { + $container = new ParameterContainer(['a' => 'one']); + $container->offsetSet(0, 'two'); + $container->offsetSet(1, 'three'); + + static::assertSame(['a' => 'two', 1 => 'three'], $container->getNamedArray()); + } + #[Test] #[TestDox('unit test: Test offsetSetErrata() will persist errata data')] public function offsetSetErrata(): void @@ -398,6 +512,16 @@ public function offsetUnsetErrataThrowsWhenNameDoesNotExist(): void $container->offsetUnsetErrata('foo'); } + #[Test] + public function offsetUnsetKeepsThePositionOfTheName(): void + { + $container = new ParameterContainer(['a' => 'one', 'b' => 'two']); + $container->offsetUnset(0); + $container->offsetSet(0, 'again'); + + static::assertSame(['b' => 'two', 'a' => 'again'], $container->getNamedArray()); + } + #[Test] #[TestDox('unit test: Test offsetUnsetMaxLength() will unset data for a particular key')] public function offsetUnsetMaxLength(): void @@ -428,6 +552,19 @@ public function offsetUnsetMaxLengthThrowsWhenNameDoesNotExist(): void $container->offsetUnsetMaxLength('foo'); } + /** @param Closure(ParameterContainer): mixed $call */ + #[Test] + #[DataProvider('unknownPositionProvider')] + public function rejectsAnIntegerNameThatIsNotAPosition(Closure $call): void + { + $container = new ParameterContainer(['foo' => 'bar']); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage(InvalidArgumentException::MISSING_DATA); + + $call($container); + } + #[Test] #[TestDox('unit test: Test rewind() resets the iterators pointer')] public function rewind(): void diff --git a/test/unit/Adapter/Platform/Sql92Test.php b/test/unit/Adapter/Platform/Sql92Test.php index 3be65869e..a124e9282 100644 --- a/test/unit/Adapter/Platform/Sql92Test.php +++ b/test/unit/Adapter/Platform/Sql92Test.php @@ -75,6 +75,12 @@ public function getQuoteIdentifierSymbol(): void static::assertSame('"', $this->platform->getQuoteIdentifierSymbol()); } + #[Test] + public function getQuoteIdentifierSymbolReturnsTheOpeningQuote(): void + { + static::assertSame('[', (new TestPlatform(quoteIdentifier: ['[', ']']))->getQuoteIdentifierSymbol()); + } + #[Test] public function getQuoteValueSymbol(): void { @@ -95,6 +101,12 @@ public function quoteIdentifierChain(): void static::assertSame('"schema"."identifier"', $this->platform->quoteIdentifierChain(['schema', 'identifier'])); } + #[Test] + public function quoteIdentifierChainEscapesDoubleQuotes(): void + { + static::assertSame('"a\\"b"."c"', $this->platform->quoteIdentifierChain(['a"b', 'c'])); + } + #[Test] public function quoteIdentifierInFragment(): void { @@ -126,6 +138,14 @@ public function quoteIdentifierInFragment(): void ); } + #[Test] + public function quoteIdentifierInFragmentReplacesTheOpeningQuoteInEachPart(): void + { + $platform = new TestPlatform(quoteIdentifier: ['$', '#']); + + static::assertSame("\$a'b#.\$c#", $platform->quoteIdentifierInFragment('a$b.c')); + } + #[Test] public function quoteIdentifierInFragmentReturnsUnquotedWhenQuotingDisabled(): void { @@ -142,6 +162,14 @@ public function quoteIdentifierReturnsUnquotedWhenQuotingDisabled(): void static::assertSame('test', $platform->quoteIdentifier('test')); } + #[Test] + public function quoteIdentifierWrapsInTheOpeningAndClosingQuotesAndReplacesTheOpeningQuote(): void + { + $platform = new TestPlatform(quoteIdentifier: ['[', ']']); + + static::assertSame("[a'b]c]", $platform->quoteIdentifier('a[b]c')); + } + #[Test] public function quoteTrustedValueEscapesSpecialCharacters(): void { diff --git a/test/unit/Adapter/Platform/TestAsset/TestPlatform.php b/test/unit/Adapter/Platform/TestAsset/TestPlatform.php index 9b0a2308d..e46c1b72c 100644 --- a/test/unit/Adapter/Platform/TestAsset/TestPlatform.php +++ b/test/unit/Adapter/Platform/TestAsset/TestPlatform.php @@ -12,8 +12,10 @@ final class TestPlatform extends AbstractPlatform { + /** @param array{string, string} $quoteIdentifier */ public function __construct( protected ?DriverInterface $driver = null, + protected array $quoteIdentifier = ['"', '"'], ) {} #[Override] diff --git a/test/unit/Adapter/Profiler/ProfilerTest.php b/test/unit/Adapter/Profiler/ProfilerTest.php index 805289c3c..d435e380f 100644 --- a/test/unit/Adapter/Profiler/ProfilerTest.php +++ b/test/unit/Adapter/Profiler/ProfilerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use function array_map; use function sprintf; #[CoversMethod(Profiler::class, 'profilerStart')] @@ -25,6 +26,26 @@ final class ProfilerTest extends TestCase { protected Profiler $profiler; + #[Test] + public function eachFinishedProfileIsKeptSeparately(): void + { + $this->profiler->profilerStart('SELECT 1'); + $this->profiler->profilerFinish(); + $this->profiler->profilerStart('SELECT 2'); + $this->profiler->profilerFinish(); + + static::assertSame([0 => 'SELECT 1', 1 => 'SELECT 2'], array_map( + static fn(array $profile): ?string => $profile['sql'], + $this->profiler->getProfiles(), + )); + } + + #[Test] + public function getLastProfileReturnsNullBeforeAnyProfileStarts(): void + { + static::assertNull($this->profiler->getLastProfile()); + } + #[Test] public function getLastProfileReturnsSqlAndTimings(): void { @@ -38,6 +59,16 @@ public function getLastProfileReturnsSqlAndTimings(): void static::assertIsFloat($profile['elapse']); } + #[Test] + public function getLastProfileReturnsTheMostRecentProfile(): void + { + $this->profiler->profilerStart('SELECT 1'); + $this->profiler->profilerFinish(); + $this->profiler->profilerStart('SELECT 2'); + + static::assertSame('SELECT 2', $this->profiler->getLastProfile()['sql'] ?? null); + } + #[Test] public function getProfilesReturnsAllRecordedProfiles(): void { @@ -49,6 +80,17 @@ public function getProfilesReturnsAllRecordedProfiles(): void static::assertCount(2, $this->profiler->getProfiles()); } + #[Test] + public function profilerFinishRecordsTheElapsedTime(): void + { + $this->profiler->profilerStart('SELECT 1'); + $this->profiler->profilerFinish(); + + $profile = $this->profiler->getLastProfile(); + + static::assertSame(($profile['end'] ?? 0.0) - ($profile['start'] ?? 0.0), $profile['elapse'] ?? null); + } + #[Test] public function profilerFinishThrowsWithoutStart(): void { diff --git a/test/unit/Adapter/StatementContainerTest.php b/test/unit/Adapter/StatementContainerTest.php index 447a62414..452d0810c 100644 --- a/test/unit/Adapter/StatementContainerTest.php +++ b/test/unit/Adapter/StatementContainerTest.php @@ -57,4 +57,14 @@ public function setAndGetSql(): void static::assertSame($container, $result); static::assertSame('test', $container->getSql()); } + + #[Test] + public function setSqlStoresNullAsAnEmptyString(): void + { + $container = new StatementContainer('test'); + + $container->setSql(null); + + static::assertSame('', $container->getSql()); + } }