diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index d8284b60..f34d05ef 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -324,12 +324,6 @@ 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 = "incompatible-parameter-type" -message = 'Parameter `$sqlOrResource` of `PhpDb\Adapter\Driver\Pdo\AbstractPdo::createstatement()` expects type `PDOStatement|string` but parent `PhpDb\Adapter\Driver\DriverInterface::createstatement()` expects type `resource|string`' -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/AbstractPdo.php" code = "invalid-return-statement" @@ -372,24 +366,12 @@ 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-condition" -message = "This condition (type `true`) will always evaluate to true." -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 = "redundant-type-comparison" -message = "Redundant type assertion: `$sqlOrResource` is already `string`." -count = 1 - [[issues]] file = "src/Adapter/Driver/Pdo/AbstractPdo.php" code = "too-few-arguments" @@ -1512,12 +1494,24 @@ code = "less-specific-nested-argument-type" message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Delete::where`: expected `(closure(...mixed): mixed)|PhpDb\Sql\Predicate\PredicateInterface|PhpDb\Sql\Where|array|null|scalar>|string`, but provided type `array` is less specific.' count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Insert::values`: expected `PhpDb\Sql\Select|array`, but provided type `array` is less specific.' +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "less-specific-nested-argument-type" message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Select::where`: expected `(closure(...mixed): mixed)|PhpDb\Sql\Predicate\PredicateInterface|array|null|scalar>|string`, but provided type `array` is less specific.' count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Update::set`: expected `array`, but provided type `array` is less specific.' +count = 1 + [[issues]] file = "src/RowGateway/AbstractRowGateway.php" code = "less-specific-nested-argument-type" @@ -1638,6 +1632,18 @@ code = "unhandled-thrown-type" message = 'Potentially unhandled exception `PhpDb\RowGateway\Exception\RuntimeException` in `PhpDb\RowGateway\AbstractRowGateway::save`.' count = 1 +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "unhandled-thrown-type" +message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\RowGateway\AbstractRowGateway::delete`.' +count = 1 + +[[issues]] +file = "src/RowGateway/AbstractRowGateway.php" +code = "unhandled-thrown-type" +message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\RowGateway\AbstractRowGateway::save`.' +count = 1 + [[issues]] file = "src/RowGateway/Exception/InvalidArgumentException.php" code = "class-must-be-final" @@ -1675,1125 +1681,315 @@ message = 'Interface `PhpDb\RowGateway\RowGatewayInterface` is missing an `@api` count = 1 [[issues]] -file = "src/Sql/AbstractExpression.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Sql\AbstractExpression` is missing an `@api` or `@internal` annotation.' +file = "src/Sql/AbstractSql.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 [[issues]] -file = "src/Sql/AbstractPreparableSql.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Sql\AbstractPreparableSql` is missing an `@api` or `@internal` annotation.' +file = "src/Sql/AbstractSql.php" +code = "string-member-selector" +message = "This member selector uses a non-literal string type (`non-empty-string`); its specific value cannot be statically determined." count = 1 [[issues]] file = "src/Sql/AbstractSql.php" -code = "imprecise-type" -message = "Type `array` in return type of `resolveTable` is imprecise, equivalent to `array`." +code = "string-member-selector" +message = "This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined." count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `vsprintf`: expected `array`, but provided type `array|list{mixed}` is less specific." +file = "src/Sql/Insert.php" +code = "missing-return-type" +message = "Function `__isset` is missing a return type hint." count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "missing-api-or-internal" -message = 'Abstract class `PhpDb\Sql\AbstractSql` is missing an `@api` or `@internal` annotation.' +file = "src/Sql/Insert.php" +code = "missing-return-type" +message = "Function `__unset` is missing a return type hint." count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 +file = "src/Sql/Platform/AbstractPlatform.php" +code = "imprecise-type" +message = "Type `array` in property `$decorators` is imprecise, equivalent to `array`." +count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "overly-wide-return-type" -message = "Declared return type `array|null|string` for `resolvetable` has unused branches: `array`." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "invalid-property-assignment-value" +message = 'Invalid type for property `$subject`: expected `PhpDb\Sql\PreparableSqlInterface|PhpDb\Sql\SqlInterface`, but got `PhpDb\Sql\PreparableSqlInterface|PhpDb\Sql\SqlInterface|null`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "overly-wide-return-type" -message = "Declared return type `null|string` for `processexpressionparametername` has unused branches: `null`." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "missing-constructor" +message = 'Class `PhpDb\Sql\Platform\AbstractPlatform` has typed properties without default values but no constructor to initialize them.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifierInFragment`: expected `string`, but possibly received `bool|int|string`.' +file = "src/Sql/Platform/AbstractPlatform.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but possibly received `non-empty-array|null|string`.' +file = "src/Sql/Platform/AbstractPlatform.php" +code = "mixed-method-access" +message = "Attempting to access a method on a non-object type (`mixed`)." count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #1 of `count`: expected `Countable|array`, but possibly received `array|string>|string`." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "mixed-return-statement" +message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\AbstractPlatform::getSqlString`. Saw type `mixed`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #2 of `vsprintf`: expected `array`, but possibly received `array|string>|string>`." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "mixed-return-statement" +message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\AbstractPlatform::getTypeDecorator`. Saw type `mixed`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #2 of `vsprintf`: expected `array`, but possibly received `array|string>|string`." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "non-existent-method" +message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #2 of `vsprintf`: expected `array`, but possibly received `list|string>|string>`." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "non-existent-method" +message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\PreparableSqlInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-invalid-iterator" -message = "The expression provided to `foreach` (type `array|string>|string`) might not be iterable at runtime." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "non-existent-method" +message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-null-argument" -message = "Argument #1 of function `get_object_vars` is possibly `null`, but parameter type `object` does not accept it." +file = "src/Sql/Platform/AbstractPlatform.php" +code = "non-existent-method" +message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\SqlInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier` is possibly `null`, but parameter type `string` does not accept it.' +file = "src/Sql/Platform/Platform.php" +code = "class-must-be-final" +message = 'Class `PhpDb\Sql\Platform\Platform` should be declared `final`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Sql\AbstractSql::processExpressionParameterName` is possibly `null`, but parameter type `bool|float|int|string` does not accept it.' +file = "src/Sql/Platform/Platform.php" +code = "mixed-array-assignment" +message = "Unsafe array assignment on type `mixed`." count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-null-argument" -message = 'Argument #4 of method `PhpDb\Sql\AbstractSql::processExpressionParameterName` is possibly `null`, but parameter type `PhpDb\Adapter\Driver\DriverInterface` does not accept it.' +file = "src/Sql/Platform/Platform.php" +code = "mixed-return-statement" +message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\Platform::getDecorators`. Saw type `mixed`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "possibly-undefined-string-array-index" -message = 'Possibly undefined array key `string` accessed on `non-empty-array`.' +file = "src/Sql/Platform/Platform.php" +code = "mixed-return-statement" +message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\Platform::getSqlString`. Saw type `mixed`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "redundant-cast" -message = "Redundant cast to `(string)`: the expression already has this type." +file = "src/Sql/Platform/Platform.php" +code = "non-existent-method" +message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "string-member-selector" -message = "This member selector uses a non-literal string type (`non-empty-string`); its specific value cannot be statically determined." +file = "src/Sql/Platform/Platform.php" +code = "non-existent-method" +message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\PreparableSqlInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "string-member-selector" -message = "This member selector uses a non-literal string type (`string`); its specific value cannot be statically determined." +file = "src/Sql/Platform/Platform.php" +code = "non-existent-method" +message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' count = 1 [[issues]] -file = "src/Sql/AbstractSql.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\Sql\AbstractSql::buildSqlString`.' +file = "src/Sql/Platform/Platform.php" +code = "non-existent-method" +message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\SqlInterface`.' count = 1 [[issues]] -file = "src/Sql/ArgumentInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Sql\ArgumentInterface` is missing an `@api` or `@internal` annotation.' +file = "src/Sql/Platform/Platform.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string` accessed on `array`." count = 1 [[issues]] -file = "src/Sql/ArgumentType.php" -code = "invalid-enum-case-value" -message = '''Invalid case value for `PhpDb\Sql\ArgumentType::Identifier`. Expected `string`, but got `class-string('PhpDb\Sql\Argument\Identifier')`.''' +file = "src/Sql/Platform/PlatformDecoratorInterface.php" +code = "missing-api-or-internal" +message = 'Interface `PhpDb\Sql\Platform\PlatformDecoratorInterface` is missing an `@api` or `@internal` annotation.' count = 1 [[issues]] -file = "src/Sql/ArgumentType.php" -code = "invalid-enum-case-value" -message = '''Invalid case value for `PhpDb\Sql\ArgumentType::Identifiers`. Expected `string`, but got `class-string('PhpDb\Sql\Argument\Identifiers')`.''' +file = "src/Sql/Select.php" +code = "missing-return-type" +message = "Function `__clone` is missing a return type hint." count = 1 [[issues]] -file = "src/Sql/ArgumentType.php" -code = "invalid-enum-case-value" -message = '''Invalid case value for `PhpDb\Sql\ArgumentType::Literal`. Expected `string`, but got `class-string('PhpDb\Sql\Argument\Literal')`.''' +file = "src/Sql/Update.php" +code = "missing-return-type" +message = "Function `__clone` is missing a return type hint." count = 1 [[issues]] -file = "src/Sql/ArgumentType.php" -code = "invalid-enum-case-value" -message = '''Invalid case value for `PhpDb\Sql\ArgumentType::Select`. Expected `string`, but got `class-string('PhpDb\Sql\Argument\Select')`.''' +file = "src/TableGateway/AbstractTableGateway.php" +code = "clone-inside-loop" +message = "Cloning variable `$tableObject` onto itself inside a loop might not have the intended effect." count = 1 [[issues]] -file = "src/Sql/ArgumentType.php" -code = "invalid-enum-case-value" -message = '''Invalid case value for `PhpDb\Sql\ArgumentType::Value`. Expected `string`, but got `class-string('PhpDb\Sql\Argument\Value')`.''' +file = "src/TableGateway/AbstractTableGateway.php" +code = "impossible-condition" +message = "This condition (type `false`) will always evaluate to false." count = 1 [[issues]] -file = "src/Sql/ArgumentType.php" -code = "invalid-enum-case-value" -message = '''Invalid case value for `PhpDb\Sql\ArgumentType::Values`. Expected `string`, but got `class-string('PhpDb\Sql\Argument\Values')`.''' +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in parameter `$arguments` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/Sql/Combine.php" -code = "array-to-string-conversion" -message = "Potential array in right operand of string concatenation." +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in parameter `$joins` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/Sql/Combine.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Combine` should be declared `final`.' -count = 1 +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in parameter `$where` is imprecise, equivalent to `array`." +count = 3 [[issues]] -file = "src/Sql/Combine.php" -code = "invalid-array-element" -message = "Cannot use spread operator on non-iterable type `mixed`." +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in property `$columns` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/Sql/Combine.php" -code = "less-specific-nested-argument-type" -message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Select::columns`: expected `array`, but provided type `array` is less specific.' +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in property `$table` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/Sql/Combine.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in return type of `getColumns` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/Sql/Combine.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #3 of `str_replace`: expected `array|string`, but possibly received `array|null>>|null|string`." +file = "src/TableGateway/AbstractTableGateway.php" +code = "imprecise-type" +message = "Type `array` in return type of `getTable` is imprecise, equivalent to `array`." count = 1 [[issues]] -file = "src/Sql/Combine.php" -code = "possibly-null-argument" -message = "Argument #3 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." -count = 1 +file = "src/TableGateway/AbstractTableGateway.php" +code = "invalid-array-access" +message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' +count = 3 [[issues]] -file = "src/Sql/Combine.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('combine')` accessed on `array|null>>|string>`." +file = "src/TableGateway/AbstractTableGateway.php" +code = "invalid-property-assignment-value" +message = "Invalid type for property `$lastInsertValue`: expected `int`, but got `false|int|null|string`." count = 1 [[issues]] -file = "src/Sql/Ddl/AlterTable.php" +file = "src/TableGateway/AbstractTableGateway.php" code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Ddl\AlterTable::processTable`: expected `array`, but found `list{array|null|string}`.' +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::executeSelect`: expected `PhpDb\ResultSet\ResultSetInterface`, but found `PhpDb\ResultSet\ResultSetInterface|null`.' count = 1 [[issues]] -file = "src/Sql/Ddl/CreateTable.php" +file = "src/TableGateway/AbstractTableGateway.php" code = "invalid-return-statement" -message = '''Invalid return type for function `PhpDb\Sql\Ddl\CreateTable::processTable`: expected `array`, but found `list{string('')|string('TEMPORARY '), string('')|string('IF NOT EXISTS '), array|null|string}`.''' +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getFeatureSet`: expected `PhpDb\TableGateway\Feature\FeatureSet`, but found `PhpDb\TableGateway\Feature\FeatureSet|null`.' count = 1 [[issues]] -file = "src/Sql/Ddl/DropTable.php" +file = "src/TableGateway/AbstractTableGateway.php" code = "invalid-return-statement" -message = '''Invalid return type for function `PhpDb\Sql\Ddl\DropTable::processTable`: expected `array`, but found `list{string('')|string('IF EXISTS '), array|null|string}`.''' +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getResultSetPrototype`: expected `PhpDb\ResultSet\ResultSetInterface`, but found `PhpDb\ResultSet\ResultSetInterface|null`.' count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Delete` should be declared `final`.' +file = "src/TableGateway/AbstractTableGateway.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getSql`: expected `PhpDb\Sql\Sql`, but found `PhpDb\Sql\Sql|null`.' count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "impossible-null-type-comparison" -message = 'Impossible condition: variable `$this->where` (type `PhpDb\Sql\Where`) will always be `null`.' +file = "src/TableGateway/AbstractTableGateway.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getTable`: expected `PhpDb\Sql\TableIdentifier|array|string`, but found `PhpDb\Sql\TableIdentifier|array|null|string`.' count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Delete::processDelete`: expected `string`, but found `array|string`.' +file = "src/TableGateway/AbstractTableGateway.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Insert::into`: expected `PhpDb\Sql\TableIdentifier|array|string`, but provided type `PhpDb\Sql\ExpressionInterface|PhpDb\Sql\Select|PhpDb\Sql\TableIdentifier|array|null|scalar` is less specific.' count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Delete::processWhere`: expected `null|string`, but found `array|string`.' +file = "src/TableGateway/AbstractTableGateway.php" +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/Sql/Delete.php" +file = "src/TableGateway/AbstractTableGateway.php" code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `str_replace`: expected `array|string`, but provided type `array|null|string` is less specific." +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/Sql/Delete.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #3 of `str_replace`: expected `array|string`, but possibly received `array|null>>|null|string`." -count = 2 +file = "src/TableGateway/AbstractTableGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Delete::where`: expected `(closure(...mixed): mixed)|PhpDb\Sql\Predicate\PredicateInterface|PhpDb\Sql\Where|array|null|scalar>|string`, but provided type `PhpDb\Sql\Where|array|string` is less specific.' +count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "possibly-null-argument" -message = "Argument #2 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." +file = "src/TableGateway/AbstractTableGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Insert::values`: expected `PhpDb\Sql\Select|array`, but provided type `array` is less specific.' count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "possibly-null-argument" -message = "Argument #3 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." -count = 2 +file = "src/TableGateway/AbstractTableGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Select::columns`: expected `array`, but provided type `non-empty-array` is less specific.' +count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('delete')` accessed on `array|null>>|string>`." +file = "src/TableGateway/AbstractTableGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Select::where`: expected `(closure(...mixed): mixed)|PhpDb\Sql\Predicate\PredicateInterface|array|null|scalar>|string`, but provided type `PhpDb\Sql\Where|array|string` is less specific.' count = 1 [[issues]] -file = "src/Sql/Delete.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('where')` accessed on `array|null>>|string>`." -count = 1 - -[[issues]] -file = "src/Sql/Delete.php" -code = "redundant-comparison" -message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." -count = 1 - -[[issues]] -file = "src/Sql/Delete.php" -code = "redundant-logical-operation" -message = "Redundant `||` operation: left operand is always false and right operand is evaluated." -count = 1 - -[[issues]] -file = "src/Sql/Delete.php" -code = "redundant-null-coalesce" -message = "Redundant null coalesce: left-hand side can never be `null` or undefined." -count = 1 - -[[issues]] -file = "src/Sql/Expression.php" -code = "less-specific-nested-argument-type" -message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Expression::setParameters`: expected `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\ExpressionInterface|array|null|scalar>|list|null|scalar`, but provided type `PhpDb\Sql\ArgumentInterface|PhpDb\Sql\ExpressionInterface|array|list|null|scalar` is less specific.' -count = 1 - -[[issues]] -file = "src/Sql/Expression.php" -code = "possibly-null-argument" -message = "Argument #1 of function `array_unique` is possibly `null`, but parameter type `array` does not accept it." -count = 1 - -[[issues]] -file = "src/Sql/Expression.php" -code = "possibly-undefined-int-array-index" -message = "Possibly undefined array index accessed on `list>`." -count = 1 - -[[issues]] -file = "src/Sql/ExpressionInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Sql\ExpressionInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Sql/Having.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Having` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "imprecise-type" -message = "Type `array` in property `$select` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "imprecise-type" -message = "Type `array` in return type of `getRawState` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Insert::processInsert`: expected `null|string`, but found `array|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Insert::processSelect`: expected `null|string`, but found `array|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "less-specific-argument" -message = 'Argument type mismatch for argument #1 of `PhpDb\Adapter\Driver\DriverInterface::formatParameterName`: expected `string`, but provided type `array-key` is less specific.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "less-specific-argument" -message = 'Argument type mismatch for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but provided type `array-key` is less specific.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "missing-return-type" -message = "Function `__isset` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "missing-return-type" -message = "Function `__unset` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "mixed-argument" -message = '''Invalid argument type for argument #1 of `PhpDb\Sql\AbstractSql::resolveColumnValue`: expected `PhpDb\Sql\ExpressionInterface|PhpDb\Sql\Select|array{'column': PhpDb\Sql\ExpressionInterface|PhpDb\Sql\Select|bool|int|null|string, 'fromTable'?: string, 'isIdentifier'?: bool}|bool|int|null|string`, but found `mixed`.''' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/Sql/Insert.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Insert::__get`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\AbstractSql::processSubSelect`: expected `PhpDb\Sql\Select`, but possibly received `PhpDb\Sql\Select|non-empty-array`.' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-invalid-argument" -message = '''Possible argument type mismatch for argument #1 of `array_map`: expected `(callable(array-key): string)|(callable(array-key, ('S.array_map() extends mixed)): string)|null`, but possibly received `list{PhpDb\Adapter\Platform\PlatformInterface, string('quoteIdentifier')}`.''' -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #2 of `str_replace`: expected `array|string`, but possibly received `list{array|null|string, non-empty-string|string(''), string}`." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #2 of `str_replace`: expected `array|string`, but possibly received `list{array|null|string, string, string}`." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #3 of `str_replace`: expected `array|string`, but possibly received `array|null>>|null|string`." -count = 2 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-null-argument" -message = "Argument #3 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." -count = 2 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('insert')` accessed on `array|null>>|string>`." -count = 1 - -[[issues]] -file = "src/Sql/Insert.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('select')` accessed on `array|null>>|string>`." -count = 1 - -[[issues]] -file = "src/Sql/InsertIgnore.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\InsertIgnore` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Join.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Join` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Join.php" -code = "invalid-return-statement" -message = '''Invalid return type for function `PhpDb\Sql\Join::current`: expected `array{'columns': array, 'name': PhpDb\Sql\TableIdentifier|non-empty-array|string, 'on': PhpDb\Sql\Predicate\PredicateInterface|string, 'type': string}`, but found `array{'columns': array, 'name': PhpDb\Sql\TableIdentifier|non-empty-array|string, 'on': PhpDb\Sql\Predicate\PredicateInterface|string, 'type': string}|null`.''' -count = 1 - -[[issues]] -file = "src/Sql/Join.php" -code = "nullable-return-statement" -message = '''Function `PhpDb\Sql\Join::current` is declared to return `array{'columns': array, 'name': PhpDb\Sql\TableIdentifier|non-empty-array|string, 'on': PhpDb\Sql\Predicate\PredicateInterface|string, 'type': string}` but possibly returns a nullable value (inferred as `array{'columns': array, 'name': PhpDb\Sql\TableIdentifier|non-empty-array|string, 'on': PhpDb\Sql\Predicate\PredicateInterface|string, 'type': string}|null`).''' -count = 1 - -[[issues]] -file = "src/Sql/Join.php" -code = "possibly-undefined-int-array-index" -message = '''Possibly undefined array index accessed on `list, 'name': PhpDb\Sql\TableIdentifier|non-empty-array|string, 'on': PhpDb\Sql\Predicate\PredicateInterface|string, 'type': string}>`.''' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "imprecise-type" -message = "Type `array` in property `$decorators` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$subject`: expected `PhpDb\Sql\PreparableSqlInterface|PhpDb\Sql\SqlInterface`, but got `PhpDb\Sql\PreparableSqlInterface|PhpDb\Sql\SqlInterface|null`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "missing-constructor" -message = 'Class `PhpDb\Sql\Platform\AbstractPlatform` has typed properties without default values but no constructor to initialize them.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-method-access" -message = "Attempting to access a method on a non-object type (`mixed`)." -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\AbstractPlatform::getSqlString`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\AbstractPlatform::getTypeDecorator`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "non-existent-method" -message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "non-existent-method" -message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\PreparableSqlInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "non-existent-method" -message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/AbstractPlatform.php" -code = "non-existent-method" -message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\SqlInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Platform\Platform` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "mixed-array-assignment" -message = "Unsafe array assignment on type `mixed`." -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\Platform::getDecorators`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "mixed-return-statement" -message = 'Could not infer a precise return type for function `PhpDb\Sql\Platform\Platform::getSqlString`. Saw type `mixed`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "non-existent-method" -message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "non-existent-method" -message = 'Method `getSqlString` does not exist on type `PhpDb\Sql\PreparableSqlInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "non-existent-method" -message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\Platform\PlatformDecoratorInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "non-existent-method" -message = 'Method `prepareStatement` does not exist on type `PhpDb\Sql\SqlInterface`.' -count = 1 - -[[issues]] -file = "src/Sql/Platform/Platform.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string` accessed on `array`." -count = 1 - -[[issues]] -file = "src/Sql/Platform/PlatformDecoratorInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Sql\Platform\PlatformDecoratorInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Sql/PreparableSqlInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Sql\PreparableSqlInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "array-to-string-conversion" -message = "Potential array in left operand of string concatenation." -count = 2 - -[[issues]] -file = "src/Sql/Select.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Select` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "impossible-null-type-comparison" -message = 'Impossible condition: variable `$this->having` (type `PhpDb\Sql\Having`) will always be `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "impossible-null-type-comparison" -message = 'Impossible condition: variable `$this->where` (type `PhpDb\Sql\Where`) will always be `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$having`: expected `PhpDb\Sql\Having`, but got `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$joins`: expected `PhpDb\Sql\Join`, but got `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "invalid-property-assignment-value" -message = 'Invalid type for property `$where`: expected `PhpDb\Sql\Where`, but got `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "invalid-return-statement" -message = '''Invalid return type for function `PhpDb\Sql\Select::resolveTable`: expected `array{0: null|string, 1: string}`, but found `list{array|null|string, string('')|truthy-string}`.''' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "missing-return-type" -message = "Function `__clone` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 2 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-false-iterator" -message = 'Expression being iterated (type `array|false|list`) might be `false` at runtime.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but possibly received `PhpDb\Sql\ExpressionInterface|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\AbstractSql::renderTable`: expected `string`, but possibly received `array|null|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-null-argument" -message = "Argument #1 of function `strtoupper` is possibly `null`, but parameter type `string` does not accept it." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-null-argument" -message = "Argument #1 of function `trim` is possibly `null`, but parameter type `string` does not accept it." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Sql\AbstractSql::processSubSelect` is possibly `null`, but parameter type `PhpDb\Sql\Select` does not accept it.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-null-argument" -message = 'Argument #1 of method `PhpDb\Sql\AbstractSql::renderTable` is possibly `null`, but parameter type `string` does not accept it.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-null-array-index" -message = "Possibly using `null` as an array index to access elementof variable $table." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-null-operand" -message = "Possibly null left operand used in string concatenation (type `array|null|string`)." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-undefined-int-array-index" -message = "Possibly undefined array index accessed on `non-empty-list`." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key 'modifier' accessed on `array{'modifier'?: string, 'select'?: PhpDb\Sql\Select, 'type'?: string}`.''' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key 'select' accessed on `array{'modifier'?: string, 'select'?: PhpDb\Sql\Select, 'type'?: string}`.''' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key 'type' accessed on `array{'modifier'?: string('')|string('0'), 'select'?: PhpDb\Sql\Select, 'type'?: string}`.''' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-undefined-string-array-index" -message = '''Possibly undefined array key 'type' accessed on `array{'modifier'?: truthy-string, 'select'?: PhpDb\Sql\Select, 'type'?: string}`.''' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "possibly-undefined-string-array-index" -message = 'Possibly undefined array key `null|string` accessed on `array`.' -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "redundant-comparison" -message = "Redundant `!==` comparison: left-hand side is always not identical to right-hand side." -count = 3 - -[[issues]] -file = "src/Sql/Select.php" -code = "redundant-comparison" -message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." -count = 2 - -[[issues]] -file = "src/Sql/Select.php" -code = "redundant-condition" -message = "This condition (type `true`) will always evaluate to true." -count = 3 - -[[issues]] -file = "src/Sql/Select.php" -code = "redundant-logical-operation" -message = "Redundant `||` operation: left operand is always false and right operand is evaluated." -count = 2 - -[[issues]] -file = "src/Sql/Select.php" -code = "redundant-null-coalesce" -message = "Redundant null coalesce: left-hand side can never be `null` or undefined." -count = 3 - -[[issues]] -file = "src/Sql/Sql.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Sql` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Sql.php" -code = "overly-wide-return-type" -message = 'Declared return type `PhpDb\Adapter\AdapterInterface|null` for `getadapter` has unused branches: `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Sql.php" -code = "overly-wide-return-type" -message = 'Declared return type `PhpDb\Sql\Platform\PlatformDecoratorInterface|null` for `getsqlplatform` has unused branches: `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Sql.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\Sql\Sql::buildSqlString`.' -count = 1 - -[[issues]] -file = "src/Sql/Sql.php" -code = "unhandled-thrown-type" -message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\Sql\Sql::prepareStatementForSqlObject`.' -count = 1 - -[[issues]] -file = "src/Sql/SqlInterface.php" -code = "missing-api-or-internal" -message = 'Interface `PhpDb\Sql\SqlInterface` is missing an `@api` or `@internal` annotation.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Update` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "impossible-null-type-comparison" -message = 'Impossible condition: variable `$this->where` (type `PhpDb\Sql\Where`) will always be `null`.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Update::processSet`: expected `string`, but found `array|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Update::processUpdate`: expected `string`, but found `array|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\Sql\Update::processWhere`: expected `null|string`, but found `array|string`.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "less-specific-nested-argument-type" -message = "Argument type mismatch for argument #2 of `str_replace`: expected `array|string`, but provided type `array|null|string` is less specific." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "missing-return-type" -message = "Function `__clone` is missing a return type hint." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "mixed-argument" -message = '''Invalid argument type for argument #1 of `PhpDb\Sql\AbstractSql::resolveColumnValue`: expected `PhpDb\Sql\ExpressionInterface|PhpDb\Sql\Select|array{'column': PhpDb\Sql\ExpressionInterface|PhpDb\Sql\Select|bool|int|null|string, 'fromTable'?: string, 'isIdentifier'?: bool}|bool|int|null|string`, but found `mixed`.''' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 2 - -[[issues]] -file = "src/Sql/Update.php" -code = "possible-method-access-on-null" -message = "Attempting to call a method on `null`." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #3 of `Laminas\Stdlib\PriorityList::insert`: expected `int`, but possibly received `int|numeric-string`.' -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-invalid-argument" -message = "Possible argument type mismatch for argument #3 of `str_replace`: expected `array|string`, but possibly received `array|null>>|null|string`." -count = 3 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-null-argument" -message = "Argument #2 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-null-argument" -message = "Argument #3 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." -count = 3 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('set')` accessed on `array|null>>|string>`." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('update')` accessed on `array|null>>|string>`." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('where')` accessed on `array|null>>|string>`." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "redundant-comparison" -message = "Redundant `!==` comparison: left-hand side is always not identical to right-hand side." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "redundant-comparison" -message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "redundant-condition" -message = "This condition (type `true`) will always evaluate to true." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "redundant-logical-operation" -message = "Redundant `||` operation: left operand is always false and right operand is evaluated." -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "redundant-null-coalesce" -message = "Redundant null coalesce: left-hand side can never be `null` or undefined." -count = 1 - -[[issues]] -file = "src/Sql/Where.php" -code = "class-must-be-final" -message = 'Class `PhpDb\Sql\Where` should be declared `final`.' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "clone-inside-loop" -message = "Cloning variable `$tableObject` onto itself inside a loop might not have the intended effect." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "impossible-condition" -message = "This condition (type `false`) will always evaluate to false." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$arguments` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$joins` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in parameter `$where` is imprecise, equivalent to `array`." -count = 3 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in property `$columns` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in property `$table` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in return type of `getColumns` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "imprecise-type" -message = "Type `array` in return type of `getTable` is imprecise, equivalent to `array`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-array-access" -message = 'Cannot access array index on object `PhpDb\Sql\TableIdentifier` that does not implement `ArrayAccess`.' -count = 3 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-property-assignment-value" -message = "Invalid type for property `$lastInsertValue`: expected `int`, but got `false|int|null|string`." -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::executeSelect`: expected `PhpDb\ResultSet\ResultSetInterface`, but found `PhpDb\ResultSet\ResultSetInterface|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getFeatureSet`: expected `PhpDb\TableGateway\Feature\FeatureSet`, but found `PhpDb\TableGateway\Feature\FeatureSet|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getResultSetPrototype`: expected `PhpDb\ResultSet\ResultSetInterface`, but found `PhpDb\ResultSet\ResultSetInterface|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getSql`: expected `PhpDb\Sql\Sql`, but found `PhpDb\Sql\Sql|null`.' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -code = "invalid-return-statement" -message = 'Invalid return type for function `PhpDb\TableGateway\AbstractTableGateway::getTable`: expected `PhpDb\Sql\TableIdentifier|array|string`, but found `PhpDb\Sql\TableIdentifier|array|null|string`.' -count = 1 - -[[issues]] -file = "src/TableGateway/AbstractTableGateway.php" -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" -message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Delete::where`: expected `(closure(...mixed): mixed)|PhpDb\Sql\Predicate\PredicateInterface|PhpDb\Sql\Where|array|null|scalar>|string`, but provided type `PhpDb\Sql\Where|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\Sql\Select::columns`: expected `array`, but provided type `non-empty-array` 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\Sql\Select::where`: expected `(closure(...mixed): mixed)|PhpDb\Sql\Predicate\PredicateInterface|array|null|scalar>|string`, but provided type `PhpDb\Sql\Where|array|string` is less specific.' +file = "src/TableGateway/AbstractTableGateway.php" +code = "less-specific-nested-argument-type" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Update::set`: expected `array`, but provided type `array` is less specific.' count = 1 [[issues]] @@ -2818,7 +2014,7 @@ count = 2 file = "src/TableGateway/AbstractTableGateway.php" code = "mixed-argument" message = 'Invalid argument type for argument #1 of `PhpDb\Sql\Insert::into`: expected `PhpDb\Sql\TableIdentifier|array|string`, but found `mixed`.' -count = 2 +count = 1 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" @@ -2930,16 +2126,58 @@ count = 1 [[issues]] file = "src/TableGateway/AbstractTableGateway.php" -code = "possibly-undefined-string-array-index" -message = "Possibly undefined array key `string('table')` accessed on `array`." +code = "possibly-null-argument" +message = 'Argument #1 of method `PhpDb\Sql\Insert::into` is possibly `null`, but parameter type `PhpDb\Sql\TableIdentifier|array|string` does not accept it.' +count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "possibly-undefined-int-array-index" +message = 'Possibly undefined array index accessed on `list`.' count = 3 +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "possibly-undefined-string-array-index" +message = '''Possibly undefined array key `string('table')` accessed on `array|string>`.''' +count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "possibly-undefined-string-array-index" +message = '''Possibly undefined array key `string('table')` accessed on `array`.''' +count = 2 + [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "redundant-comparison" message = "Redundant `===` comparison: left-hand side is never identical to right-hand side." count = 1 +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "unhandled-thrown-type" +message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\TableGateway\AbstractTableGateway::executeDelete`.' +count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "unhandled-thrown-type" +message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\TableGateway\AbstractTableGateway::executeInsert`.' +count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "unhandled-thrown-type" +message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\TableGateway\AbstractTableGateway::executeSelect`.' +count = 1 + +[[issues]] +file = "src/TableGateway/AbstractTableGateway.php" +code = "unhandled-thrown-type" +message = 'Potentially unhandled exception `PhpDb\Sql\Exception\RuntimeException` in `PhpDb\TableGateway\AbstractTableGateway::executeUpdate`.' +count = 1 + [[issues]] file = "src/TableGateway/Exception/InvalidArgumentException.php" code = "class-must-be-final" @@ -2976,12 +2214,6 @@ code = "mixed-assignment" message = "Assigning `mixed` type to a variable may lead to unexpected behavior." count = 1 -[[issues]] -file = "src/TableGateway/Feature/SequenceFeature.php" -code = "mixed-assignment" -message = "Assigning `mixed` type to a variable may lead to unexpected behavior." -count = 1 - [[issues]] file = "src/TableGateway/TableGateway.php" code = "class-must-be-final" diff --git a/lint-baseline.toml b/lint-baseline.toml index 0bebf452..2b4cac96 100644 --- a/lint-baseline.toml +++ b/lint-baseline.toml @@ -342,24 +342,12 @@ code = "excessive-parameter-list" message = "Parameter list is too long." count = 1 -[[issues]] -file = "src/Sql/AbstractSql.php" -code = "halstead" -message = "Method has a high halstead difficulty" -count = 1 - [[issues]] file = "src/Sql/AbstractSql.php" code = "kan-defect" message = "Class has a high kan defect score." count = 1 -[[issues]] -file = "src/Sql/AbstractSql.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - [[issues]] file = "src/Sql/Combine.php" code = "cyclomatic-complexity" @@ -372,24 +360,12 @@ code = "kan-defect" message = "Class has a high kan defect score." count = 1 -[[issues]] -file = "src/Sql/Combine.php" -code = "no-array-accumulation-in-loop" -message = "Array spread in reassignment inside a loop causes O(n²) array copying." -count = 1 - [[issues]] file = "src/Sql/Insert.php" code = "cyclomatic-complexity" message = "Class has high complexity." count = 1 -[[issues]] -file = "src/Sql/Insert.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - [[issues]] file = "src/Sql/Select.php" code = "cyclomatic-complexity" @@ -400,7 +376,7 @@ count = 1 file = "src/Sql/Select.php" code = "halstead" message = "Method has a high halstead difficulty" -count = 2 +count = 1 [[issues]] file = "src/Sql/Select.php" @@ -408,36 +384,12 @@ code = "kan-defect" message = "Class has a high kan defect score." count = 1 -[[issues]] -file = "src/Sql/Select.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - -[[issues]] -file = "src/Sql/Select.php" -code = "too-many-properties" -message = "Class has too many properties." -count = 1 - [[issues]] file = "src/Sql/Update.php" code = "cyclomatic-complexity" message = "Class has high complexity." count = 1 -[[issues]] -file = "src/Sql/Update.php" -code = "halstead" -message = "Method has a high halstead difficulty" -count = 1 - -[[issues]] -file = "src/Sql/Update.php" -code = "too-many-methods" -message = "Class has too many methods." -count = 1 - [[issues]] file = "src/TableGateway/AbstractTableGateway.php" code = "cyclomatic-complexity" diff --git a/mago.toml b/mago.toml index 8aab97ce..31246c5f 100644 --- a/mago.toml +++ b/mago.toml @@ -15,9 +15,10 @@ baseline = "lint-baseline.toml" # Exception classes are message catalogues: one template and one named constructor per # condition, per the convention in docs/book/exceptions.md. The method count tracks the # number of distinct failures a component can report, so the maintainability signal does -# not apply. Predicate, AlterTable and CreateTable are fluent builders: one public method per -# clause they can emit, so their method count is the size of their public API. "test/" -# repeats the phpdb-qa-tools default, which this key replaces. +# 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. +# "test/" repeats the phpdb-qa-tools default, which this key replaces. too-many-methods = { exclude = [ "test/", "src/Sql/Exception/", @@ -27,8 +28,15 @@ too-many-methods = { exclude = [ "src/Sql/Predicate/Predicate.php", "src/Sql/Ddl/AlterTable.php", "src/Sql/Ddl/CreateTable.php", + "src/Sql/AbstractSql.php", + "src/Sql/Select.php", + "src/Sql/Insert.php", + "src/Sql/Update.php", ] } +# Select holds one property per clause of a SELECT statement. +too-many-properties = { exclude = ["src/Sql/Select.php"] } + # These constructors take one argument per part of the SQL definition they model # (a foreign key's name, columns, reference table, reference columns and two rules; a # column's name, digits, scale, nullability, default and options). They are public API, so @@ -48,6 +56,11 @@ prefer-self-return-type = { exclude = [ [analyzer] baseline = "analyzer-baseline.toml" +# ArgumentType is string-backed and each case's value is a ::class constant, which PHP accepts +# as a string. The analyzer reports the class-string type as not being a string. +ignore = [ + { code = "invalid-enum-case-value", in = ["src/Sql/ArgumentType.php"] }, +] class-initializers = [ "PhpDb\\ResultSet\\AbstractResultSet::initialize", "PhpDb\\TableGateway\\Feature\\AbstractFeature::setTableGateway", diff --git a/src/ResultSet/HydratingResultSet.php b/src/ResultSet/HydratingResultSet.php index 02c23cc9..3ea4ae43 100644 --- a/src/ResultSet/HydratingResultSet.php +++ b/src/ResultSet/HydratingResultSet.php @@ -6,6 +6,7 @@ use ArrayObject; use Laminas\Hydrator\ArraySerializableHydrator; +use Laminas\Hydrator\Exception\RuntimeException as HydratorRuntimeException; use Laminas\Hydrator\HydratorInterface; use Override; use PhpDb\ResultSet\Exception\RuntimeException; @@ -119,7 +120,7 @@ public function toArray(): array * Hydrate one row onto a clone of the prototype. * * @throws Exception\UnexpectedValueException If the row is not row data. - * @throws \Laminas\Hydrator\Exception\RuntimeException If the hydrator cannot fill the prototype. + * @throws HydratorRuntimeException If the hydrator cannot fill the prototype. */ #[Override] protected function mapRow(mixed $row): object diff --git a/src/Sql/AbstractExpression.php b/src/Sql/AbstractExpression.php index e5ea98fc..c84753c0 100644 --- a/src/Sql/AbstractExpression.php +++ b/src/Sql/AbstractExpression.php @@ -4,6 +4,9 @@ namespace PhpDb\Sql; +/** + * @api + */ abstract class AbstractExpression implements ExpressionInterface { protected ?string $specification = null; diff --git a/src/Sql/AbstractPreparableSql.php b/src/Sql/AbstractPreparableSql.php index 83313f0a..70f11f85 100644 --- a/src/Sql/AbstractPreparableSql.php +++ b/src/Sql/AbstractPreparableSql.php @@ -12,8 +12,14 @@ use function array_values; use function is_array; +/** + * @api + */ abstract class AbstractPreparableSql extends AbstractSql implements PreparableSqlInterface { + /** + * @throws Exception\RuntimeException + */ #[Override] public function prepareStatement( AdapterInterface $adapter, diff --git a/src/Sql/AbstractSql.php b/src/Sql/AbstractSql.php index 07b7a489..5ea9aaad 100644 --- a/src/Sql/AbstractSql.php +++ b/src/Sql/AbstractSql.php @@ -29,11 +29,14 @@ use function vsprintf; /** + * @api + * * @psalm-type ParameterSpecification = array|null * @psalm-type Specification = string|array> * @psalm-type SqlParameters = array>> + * @psalm-type ColumnValue = Select|ExpressionInterface|string|int|float|bool|null * @psalm-type ColumnReference = array{ - * column: Select|string|int|bool|ExpressionInterface|null, + * column: ColumnValue, * fromTable?: string, * isIdentifier?: bool, * } @@ -62,6 +65,8 @@ abstract class AbstractSql implements SqlInterface /** * {@inheritDoc} + * + * @throws Exception\RuntimeException */ #[Override] public function getSqlString(?PlatformInterface $adapterPlatform = null): string @@ -71,6 +76,9 @@ public function getSqlString(?PlatformInterface $adapterPlatform = null): string return $this->buildSqlString($adapterPlatform); } + /** + * @throws Exception\RuntimeException + */ protected function buildSqlString( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -109,62 +117,17 @@ protected function buildSqlString( protected function createSqlFromSpecificationAndParameters(array|string $specifications, array $parameters): string { if (is_string($specifications)) { - return vsprintf($specifications, $parameters); + return vsprintf($specifications, $this->flattenParameterList($parameters)); } - $parametersCount = count($parameters); - $specificationString = null; - $paramSpecs = []; - - foreach ($specifications as $candidateString => $paramSpecs) { - if (count($paramSpecs) !== $parametersCount) { - continue; - } - - $specificationString = (string) $candidateString; - break; - } - - if (null === $specificationString) { - throw Exception\RuntimeException::forUnsupportedParameterCount(); - } + [$format, $parameterSpecifications] = $this->matchSpecification($specifications, count($parameters)); $topParameters = []; - foreach ($parameters as $position => $paramsForPosition) { - if (null !== ($paramSpecs[$position]['combinedby'] ?? null)) { - $multiParamValues = []; - foreach ($paramsForPosition as $multiParamsForPosition) { - if (! is_array($multiParamsForPosition)) { - $multiParamsForPosition = [$multiParamsForPosition]; - } - - $ppCount = count($multiParamsForPosition); - - if (null === ($paramSpecs[$position][$ppCount] ?? null)) { - throw Exception\RuntimeException::forUnsupportedParameterCountOf($ppCount); - } - - $multiParamValues[] = vsprintf($paramSpecs[$position][$ppCount], $multiParamsForPosition); - } - - $topParameters[] = implode($paramSpecs[$position]['combinedby'], $multiParamValues); - continue; - } - - if (null === $paramSpecs[$position]) { - $topParameters[] = $paramsForPosition; - continue; - } - - $ppCount = count($paramsForPosition); - if (null === ($paramSpecs[$position][$ppCount] ?? null)) { - throw Exception\RuntimeException::forUnsupportedParameterCountOf($ppCount); - } - - $topParameters[] = vsprintf($paramSpecs[$position][$ppCount], $paramsForPosition); + foreach ($parameters as $position => $parameter) { + $topParameters[] = $this->renderParameter($parameterSpecifications[$position] ?? null, $parameter); } - return vsprintf($specificationString, $topParameters); + return vsprintf($format, $topParameters); } /** @@ -204,9 +167,31 @@ protected function flattenExpressionValues(array $arguments): array return $values; } + /** + * Substitute values into the numbered placeholders of a string specification + * + * @throws Exception\RuntimeException When the named specification is not a format string. + */ + protected function formatSpecification(string $name, string ...$values): string + { + $specification = $this->specifications[$name] ?? null; + + if (! is_string($specification)) { + throw Exception\RuntimeException::forStringSpecificationRequired($name); + } + + $placeholders = []; + $position = 0; + foreach ($values as $_) { + $placeholders[] = '%' . ++$position . '$s'; + } + + return str_replace($placeholders, $values, $specification); + } + protected function localizeVariables(): void { - if (! $this instanceof PlatformDecoratorInterface) { + if (! $this instanceof PlatformDecoratorInterface || null === $this->subject) { return; } @@ -217,6 +202,7 @@ protected function localizeVariables(): void /** * @staticvar int $runtimeExpressionPrefix + * @throws Exception\RuntimeException */ protected function processExpression( ExpressionInterface $expression, @@ -252,14 +238,15 @@ protected function processExpression( foreach ($expressionValues as $vIndex => $argument) { $values[] = match (true) { $argument instanceof Value => $parameterContainer instanceof ParameterContainer - ? $this->processExpressionParameterName( - $argument->getValue(), - $namedParameterPrefix, - $expressionParamIndex, - $driver, - $parameterContainer, - ) - : $platform->quoteValue((string) $argument->getValue()), + && $driver instanceof DriverInterface + ? $this->processExpressionParameterName( + $argument->getValue(), + $namedParameterPrefix, + $expressionParamIndex, + $driver, + $parameterContainer, + ) + : $platform->quoteValue((string) $argument->getValue()), $argument instanceof Identifier => $platform->quoteIdentifierInFragment($argument->getValue()), $argument instanceof Literal => $argument->getValue(), $argument instanceof Identifiers => $this->processIdentifiersArgument($argument, $platform), @@ -278,6 +265,26 @@ protected function processExpression( return vsprintf($specification, $values); } + /** + * Render each expression in a list against the platform + * + * @param array $expressions + * @return list + * @throws Exception\RuntimeException + */ + protected function processExpressionList(array $expressions, PlatformInterface $platform): array + { + $sqls = []; + foreach ($expressions as $expression) { + $sqls[] = $this->processExpression($expression, $platform); + } + + return $sqls; + } + + /** + * @throws Exception\RuntimeException + */ protected function processExpressionOrSelect( ArgumentInterface $argument, string $namedParameterPrefix, @@ -302,12 +309,12 @@ protected function processExpressionOrSelect( } protected function processExpressionParameterName( - int|float|string|bool $value, + int|float|string|bool|null $value, string $namedParameterPrefix, int &$expressionParamIndex, DriverInterface $driver, ParameterContainer $parameterContainer, - ): ?string { + ): string { $name = $namedParameterPrefix . $expressionParamIndex++; $parameterContainer->offsetSet($name, $value); @@ -331,6 +338,7 @@ protected function processIdentifiersArgument( /** * @return null|array{0: array>} Null if no joins present, array of JOIN statements otherwise + * @throws Exception\RuntimeException */ protected function processJoin( ?Join $joins, @@ -344,13 +352,12 @@ protected function processJoin( $joinSpecArgArray = []; foreach ($joins->getJoins() as $j => $join) { - $joinAs = null; - $joinNameValue = $join['name']; - $joinName = $joinNameValue; - if (is_array($joinNameValue)) { - $alias = array_key_first($joinNameValue); - $joinName = $joinNameValue[$alias]; + $joinAs = null; + $joinName = $join['name']; + if (is_array($joinName)) { + $alias = array_key_first($joinName); $joinAs = $platform->quoteIdentifier($alias); + $joinName = $joinName[$alias] ?? ''; } $joinName = match (true) { @@ -387,6 +394,9 @@ protected function processJoin( return [$joinSpecArgArray]; } + /** + * @throws Exception\RuntimeException + */ protected function processSubSelect( Select $subselect, PlatformInterface $platform, @@ -425,10 +435,11 @@ protected function renderTable(string $table, ?string $alias = null): string } /** - * @param ColumnReference|Select|string|int|bool|ExpressionInterface|null $column + * @param ColumnReference|ColumnValue $column + * @throws Exception\RuntimeException */ protected function resolveColumnValue( - Select|array|string|int|bool|ExpressionInterface|null $column, + Select|array|string|int|float|bool|ExpressionInterface|null $column, PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null, @@ -458,16 +469,20 @@ protected function resolveColumnValue( } return $isIdentifier - ? $fromTable . $platform->quoteIdentifierInFragment($column) + ? $fromTable . $platform->quoteIdentifierInFragment((string) $column) : $platform->quoteValue((string) $column); } + /** + * @return ($table is null ? null : string) + * @throws Exception\RuntimeException + */ protected function resolveTable( Select|string|TableIdentifier|null $table, PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null, - ): string|array|null { + ): ?string { $schema = null; if ($table instanceof TableIdentifier) { [$table, $schema] = $table->getTableAndSchema(); @@ -490,6 +505,56 @@ protected function resolveTable( return $table; } + /** + * @param array> $values + * @return list + * @throws Exception\RuntimeException When a value is a nested list rather than a string. + */ + private function flattenParameterList(array $values): array + { + $strings = []; + foreach ($values as $value) { + if (! is_string($value)) { + throw Exception\RuntimeException::forUnsupportedParameterCountOf(count($value)); + } + + $strings[] = $value; + } + + return $strings; + } + + /** + * @param array $parameterSpecification + * @param array> $values + * @throws Exception\RuntimeException When no format accepts the number of values. + */ + private function formatParameterList(array $parameterSpecification, array $values): string + { + $count = count($values); + $format = $parameterSpecification[$count] ?? throw Exception\RuntimeException::forUnsupportedParameterCountOf( + $count, + ); + + return vsprintf($format, $this->flattenParameterList($values)); + } + + /** + * @param array> $specifications + * @return array{string, list} + * @throws Exception\RuntimeException When no specification takes the number of parameters given. + */ + private function matchSpecification(array $specifications, int $parameterCount): array + { + foreach ($specifications as $format => $parameterSpecifications) { + if (count($parameterSpecifications) === $parameterCount) { + return [$format, $parameterSpecifications]; + } + } + + throw Exception\RuntimeException::forUnsupportedParameterCount(); + } + private function quoteJoinTableIdentifier(TableIdentifier $identifier, PlatformInterface $platform): string { [$table, $schema] = $identifier->getTableAndSchema(); @@ -499,4 +564,30 @@ private function quoteJoinTableIdentifier(TableIdentifier $identifier, PlatformI . $platform->quoteIdentifier($table) ); } + + /** + * @param ParameterSpecification $parameterSpecification + * @param string|array> $parameter + * @throws Exception\RuntimeException + */ + private function renderParameter(?array $parameterSpecification, string|array $parameter): string + { + if (null === $parameterSpecification) { + return is_string($parameter) + ? $parameter + : throw Exception\RuntimeException::forUnsupportedParameterCountOf(count($parameter)); + } + + $combinedBy = $parameterSpecification['combinedby'] ?? null; + if (null === $combinedBy) { + return $this->formatParameterList($parameterSpecification, (array) $parameter); + } + + $rendered = []; + foreach ((array) $parameter as $values) { + $rendered[] = $this->formatParameterList($parameterSpecification, (array) $values); + } + + return implode($combinedBy, $rendered); + } } diff --git a/src/Sql/Argument.php b/src/Sql/Argument.php index be2c778b..54dc26ef 100644 --- a/src/Sql/Argument.php +++ b/src/Sql/Argument.php @@ -12,6 +12,8 @@ use PhpDb\Sql\Argument\Values; /** + * @api + * * Factory for creating argument instances. */ final class Argument diff --git a/src/Sql/ArgumentInterface.php b/src/Sql/ArgumentInterface.php index 921ba0cb..160a5a91 100644 --- a/src/Sql/ArgumentInterface.php +++ b/src/Sql/ArgumentInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Sql; +/** + * @api + */ interface ArgumentInterface { public function getSpecification(): string; diff --git a/src/Sql/ArgumentType.php b/src/Sql/ArgumentType.php index 43314b02..5a3da0dd 100644 --- a/src/Sql/ArgumentType.php +++ b/src/Sql/ArgumentType.php @@ -12,6 +12,8 @@ use PhpDb\Sql\Argument\Values; /** + * @api + * * Defines types for SQL argument handling in expressions and statements. * Specifies how values should be treated during SQL generation: * - Identifier: Column names, table names, and other identifiers that require quoting diff --git a/src/Sql/Combine.php b/src/Sql/Combine.php index dd5996c3..cbd4a968 100644 --- a/src/Sql/Combine.php +++ b/src/Sql/Combine.php @@ -11,12 +11,14 @@ use function array_key_exists; use function array_keys; +use function array_merge; use function is_array; -use function str_replace; use function strtoupper; use function trim; /** + * @api + * * Combine SQL statement - allows combining multiple select statements into one * * @psalm-import-type Specification from AbstractSql @@ -61,14 +63,13 @@ public function alignColumns(): static return $this; } - $allColumns = []; + $columnLists = []; foreach ($this->combine as $combine) { - $allColumns = [ - ...$allColumns, - ...$combine['select']->getRawState(self::COLUMNS), - ]; + $columnLists[] = $combine['select']->getRawState(self::COLUMNS); } + $allColumns = array_merge(...$columnLists); + foreach ($this->combine as $combine) { $combineColumns = $combine['select']->getRawState(self::COLUMNS); $aligned = []; @@ -177,15 +178,14 @@ protected function buildSqlString( $type = 0 === $i ? '' : strtoupper( - $combine['modifier'] - ? "{$combine['type']} {$combine['modifier']}" - : $combine['type'], + '' === $combine['modifier'] + ? $combine['type'] + : "{$combine['type']} {$combine['modifier']}", ); - $select = $this->processSubSelect($combine['select'], $platform, $driver, $parameterContainer); - $sql .= str_replace( - ['%1$s', '%2$s'], - [$type, $select], - $this->specifications[self::COMBINE], + $sql .= $this->formatSpecification( + self::COMBINE, + $type, + $this->processSubSelect($combine['select'], $platform, $driver, $parameterContainer), ); } diff --git a/src/Sql/Ddl/AlterTable.php b/src/Sql/Ddl/AlterTable.php index 1113b771..eaee0bcc 100644 --- a/src/Sql/Ddl/AlterTable.php +++ b/src/Sql/Ddl/AlterTable.php @@ -6,7 +6,7 @@ use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\AbstractSql; -use PhpDb\Sql\ExpressionInterface; +use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\Literal; use PhpDb\Sql\TableIdentifier; @@ -215,28 +215,25 @@ public function setTable(string|TableIdentifier $name): static /** * @return array{0: list} + * @throws RuntimeException */ protected function processAddColumns(PlatformInterface $adapterPlatform): array { - return [array_map( - fn(ExpressionInterface $column): string => $this->processExpression($column, $adapterPlatform), - $this->addColumns, - )]; + return [$this->processExpressionList($this->addColumns, $adapterPlatform)]; } /** * @return array{0: list} + * @throws RuntimeException */ protected function processAddConstraints(PlatformInterface $adapterPlatform): array { - return [array_map( - fn(ExpressionInterface $constraint): string => $this->processExpression($constraint, $adapterPlatform), - $this->addConstraints, - )]; + return [$this->processExpressionList($this->addConstraints, $adapterPlatform)]; } /** * @return array{0: list} + * @throws RuntimeException */ protected function processChangeColumns(PlatformInterface $adapterPlatform): array { @@ -275,7 +272,10 @@ protected function processDropIndexes(PlatformInterface $adapterPlatform): array return [array_map($adapterPlatform->quoteIdentifier(...), $this->dropIndexes)]; } - /** @return string[] */ + /** + * @return string[] + * @throws RuntimeException + */ protected function processTable(PlatformInterface $adapterPlatform): array { return [$this->resolveTable($this->table, $adapterPlatform)]; diff --git a/src/Sql/Ddl/CreateTable.php b/src/Sql/Ddl/CreateTable.php index 656030f2..ee512e4a 100644 --- a/src/Sql/Ddl/CreateTable.php +++ b/src/Sql/Ddl/CreateTable.php @@ -6,6 +6,7 @@ use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\AbstractSql; +use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\Literal; use PhpDb\Sql\TableIdentifier; @@ -151,6 +152,7 @@ public function setTemporary(string|int|bool $temporary): static /** * @return string[][]|null + * @throws RuntimeException */ protected function processColumns(PlatformInterface $adapterPlatform): ?array { @@ -158,13 +160,7 @@ protected function processColumns(PlatformInterface $adapterPlatform): ?array return null; } - $sqls = []; - - foreach ($this->columns as $column) { - $sqls[] = $this->processExpression($column, $adapterPlatform); - } - - return [$sqls]; + return [$this->processExpressionList($this->columns, $adapterPlatform)]; } /** @@ -177,6 +173,7 @@ protected function processCombinedby(): ?array /** * @return string[][]|null + * @throws RuntimeException */ protected function processConstraints(PlatformInterface $adapterPlatform): ?array { @@ -184,13 +181,7 @@ protected function processConstraints(PlatformInterface $adapterPlatform): ?arra return null; } - $sqls = []; - - foreach ($this->constraints as $constraint) { - $sqls[] = $this->processExpression($constraint, $adapterPlatform); - } - - return [$sqls]; + return [$this->processExpressionList($this->constraints, $adapterPlatform)]; } /** @@ -203,6 +194,7 @@ protected function processStatementEnd(): array /** * @return string[] + * @throws RuntimeException */ protected function processTable(PlatformInterface $adapterPlatform): array { diff --git a/src/Sql/Ddl/DropTable.php b/src/Sql/Ddl/DropTable.php index 3a9bb62b..e10f4407 100644 --- a/src/Sql/Ddl/DropTable.php +++ b/src/Sql/Ddl/DropTable.php @@ -6,6 +6,7 @@ use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\AbstractSql; +use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\TableIdentifier; /** @@ -42,7 +43,10 @@ public function ifExists(bool $ifExists = true): static return $this; } - /** @return string[] */ + /** + * @return string[] + * @throws RuntimeException + */ protected function processTable(PlatformInterface $adapterPlatform): array { return [ diff --git a/src/Sql/Delete.php b/src/Sql/Delete.php index 21def71f..b8e9fefd 100644 --- a/src/Sql/Delete.php +++ b/src/Sql/Delete.php @@ -11,11 +11,12 @@ use PhpDb\Sql\Predicate\PredicateInterface; use function array_key_exists; -use function str_replace; use function strtolower; /** - * @property Where $where + * @api + * + * @property ?Where $where * @psalm-import-type Specification from AbstractSql * @psalm-import-type PredicateValue from Predicate\PredicateSet */ @@ -28,8 +29,6 @@ class Delete extends AbstractPreparableSql final public const string SPECIFICATION_WHERE = 'where'; - /** @#- */ - /** @var array */ protected array $specifications = [ self::SPECIFICATION_DELETE => 'DELETE FROM %1$s', @@ -98,18 +97,23 @@ public function where( return $this; } + /** + * @throws Exception\RuntimeException + */ protected function processDelete( PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null, ): string { - return str_replace( - '%1$s', + return $this->formatSpecification( + static::SPECIFICATION_DELETE, $this->resolveTable($this->unaliasTable($this->table), $platform, $driver, $parameterContainer), - $this->specifications[static::SPECIFICATION_DELETE], ); } + /** + * @throws Exception\RuntimeException + */ protected function processWhere( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -119,10 +123,9 @@ protected function processWhere( return null; } - return str_replace( - '%1$s', + return $this->formatSpecification( + static::SPECIFICATION_WHERE, $this->processExpression($this->where, $platform, $driver, $parameterContainer, 'where'), - $this->specifications[static::SPECIFICATION_WHERE], ); } diff --git a/src/Sql/Exception/RuntimeException.php b/src/Sql/Exception/RuntimeException.php index bb9e184e..51ecffc7 100644 --- a/src/Sql/Exception/RuntimeException.php +++ b/src/Sql/Exception/RuntimeException.php @@ -14,6 +14,8 @@ class RuntimeException extends Exception\RuntimeException 'The number of replacements in the expression' . ' does not match the number of parameters'; + final public const string STRING_SPECIFICATION_REQUIRED = 'Specification "%s" must be a format string'; + final public const string SUBJECT_NOT_IMPLEMENTING = 'The subject does not appear to implement %s,' . ' thus calling %s has no effect'; @@ -37,6 +39,11 @@ public static function forReplacementMismatch(): self return new self(self::REPLACEMENT_MISMATCH); } + public static function forStringSpecificationRequired(string $name): self + { + return new self(sprintf(self::STRING_SPECIFICATION_REQUIRED, $name)); + } + public static function forSubjectNotImplementing(string $interface, string $method): self { return new self(sprintf(self::SUBJECT_NOT_IMPLEMENTING, $interface, $method)); diff --git a/src/Sql/Expression.php b/src/Sql/Expression.php index 5820bcb1..4450c39b 100644 --- a/src/Sql/Expression.php +++ b/src/Sql/Expression.php @@ -19,6 +19,8 @@ use function str_replace; /** + * @api + * * @psalm-type ExpressionParameter = bool|string|float|int|null|list|ExpressionInterface|ArgumentInterface */ class Expression extends AbstractExpression @@ -48,9 +50,9 @@ public function __construct( if (func_num_args() > 2) { /** - * @deprecated + * Passing parameters as separate arguments is deprecated * - * @todo Make notes in documentation + * @var list $parameters */ $parameters = array_slice(func_get_args(), offset: 1); } @@ -90,7 +92,7 @@ public function getExpressionData(): array if ($count !== $parametersCount) { $matches = []; preg_match_all('/:\w*/', $specification, $matches); - if (count(array_unique($matches[0])) !== $parametersCount) { + if (count(array_unique($matches[0] ?? [])) !== $parametersCount) { throw Exception\RuntimeException::forReplacementMismatch(); } } diff --git a/src/Sql/ExpressionInterface.php b/src/Sql/ExpressionInterface.php index eff6202a..c81c54df 100644 --- a/src/Sql/ExpressionInterface.php +++ b/src/Sql/ExpressionInterface.php @@ -4,6 +4,9 @@ namespace PhpDb\Sql; +/** + * @api + */ interface ExpressionInterface { /** diff --git a/src/Sql/Having.php b/src/Sql/Having.php index 010e7418..cfb93a2f 100644 --- a/src/Sql/Having.php +++ b/src/Sql/Having.php @@ -4,4 +4,7 @@ namespace PhpDb\Sql; +/** + * @api + */ class Having extends Predicate\Predicate {} diff --git a/src/Sql/Insert.php b/src/Sql/Insert.php index d158b90f..ab76640d 100644 --- a/src/Sql/Insert.php +++ b/src/Sql/Insert.php @@ -19,10 +19,12 @@ use function implode; use function is_scalar; use function range; -use function str_replace; /** + * @api + * * @psalm-import-type Specification from AbstractSql + * @psalm-import-type ColumnValue from AbstractSql */ class Insert extends AbstractPreparableSql { @@ -48,9 +50,10 @@ class Insert extends AbstractPreparableSql /** @var TableIdentifier|string|array */ protected TableIdentifier|string|array $table = ''; - /** @var array */ + /** @var array */ protected array $columns = []; + /** @var Select|null */ protected array|Select|null $select = null; /** @@ -79,6 +82,12 @@ public function columns(array $columns): static /** * Get raw state + * + * @return TableIdentifier|string|array|list|array{ + * table: TableIdentifier|string|array, + * columns: list, + * values: list, + * } */ public function getRawState(?string $key = null): TableIdentifier|string|array { @@ -112,7 +121,7 @@ public function select(Select $select): static /** * Specify values to insert * - * @param array|Select $values + * @param array|Select $values * @param string $flag one of VALUES_MERGE or VALUES_SET; defaults to VALUES_SET * @throws Exception\InvalidArgumentException */ @@ -146,6 +155,9 @@ public function values(array|Select $values, string $flag = self::VALUES_SET): s return $this; } + /** + * @throws Exception\RuntimeException + */ protected function processInsert( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -165,15 +177,15 @@ protected function processInsert( $isPdoDriver = $driver instanceof PdoDriverInterface; foreach ($this->columns as $column => $value) { - $columns[] = $platform->quoteIdentifier($column); - if (is_scalar($value) && $parameterContainer) { + $columns[] = $platform->quoteIdentifier((string) $column); + if (is_scalar($value) && $parameterContainer && $driver instanceof DriverInterface) { // use incremental value instead of column name for PDO // @see https://github.com/zendframework/zend-db/issues/35 if ($isPdoDriver) { $column = 'c_' . $i++; } - $values[] = $driver->formatParameterName($column); + $values[] = $driver->formatParameterName((string) $column); $parameterContainer->offsetSet($column, $value); continue; } @@ -186,17 +198,17 @@ protected function processInsert( ); } - return str_replace( - ['%1$s', '%2$s', '%3$s'], - [ - $this->resolveTable($this->unaliasTable($this->table), $platform, $driver, $parameterContainer), - implode(', ', $columns), - implode(', ', $values), - ], - $this->specifications[static::SPECIFICATION_INSERT], + return $this->formatSpecification( + static::SPECIFICATION_INSERT, + $this->resolveTable($this->unaliasTable($this->table), $platform, $driver, $parameterContainer), + implode(', ', $columns), + implode(', ', $values), ); } + /** + * @throws Exception\RuntimeException + */ protected function processSelect( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -208,17 +220,16 @@ protected function processSelect( $selectSql = $this->processSubSelect($this->select, $platform, $driver, $parameterContainer); - $columns = array_map([$platform, 'quoteIdentifier'], array_keys($this->columns)); - $columns = implode(', ', $columns); - - return str_replace( - ['%1$s', '%2$s', '%3$s'], - [ - $this->resolveTable($this->unaliasTable($this->table), $platform, $driver, $parameterContainer), - $columns ? "({$columns})" : '', - $selectSql, - ], - $this->specifications[static::SPECIFICATION_SELECT], + $columns = implode(', ', array_map( + static fn(int|string $column): string => $platform->quoteIdentifier((string) $column), + array_keys($this->columns), + )); + + return $this->formatSpecification( + static::SPECIFICATION_SELECT, + $this->resolveTable($this->unaliasTable($this->table), $platform, $driver, $parameterContainer), + $columns ? "({$columns})" : '', + $selectSql, ); } @@ -237,8 +248,8 @@ private function isAssocativeArray(array $array): bool * Overloading: variable retrieval * Retrieves value by column name * + * @return ColumnValue * @throws Exception\InvalidArgumentException - * @return string */ public function __get(string $name): mixed { @@ -265,6 +276,8 @@ public function __isset(string $name) * Overloading: variable setting * * Proxies to values, using VALUES_MERGE strategy + * + * @param ColumnValue $value */ public function __set(string $name, mixed $value): void { diff --git a/src/Sql/InsertIgnore.php b/src/Sql/InsertIgnore.php index 99939792..d5875e25 100644 --- a/src/Sql/InsertIgnore.php +++ b/src/Sql/InsertIgnore.php @@ -5,6 +5,8 @@ namespace PhpDb\Sql; /** + * @api + * * @psalm-import-type Specification from AbstractSql */ class InsertIgnore extends Insert diff --git a/src/Sql/Join.php b/src/Sql/Join.php index 9f619e01..20598d96 100644 --- a/src/Sql/Join.php +++ b/src/Sql/Join.php @@ -18,6 +18,8 @@ use function key; /** + * @api + * * Aggregate JOIN specifications. * Each specification is an array with the following keys: * - name: the JOIN name @@ -77,13 +79,13 @@ public function count(): int /** * Return current join specification. * - * @return JoinSpecification + * @return JoinSpecification|null Null once iteration has passed the last join */ #[Override] #[ReturnTypeWillChange] - public function current(): array + public function current(): ?array { - return $this->joins[$this->position]; + return $this->joins[$this->position] ?? null; } /** @return list */ diff --git a/src/Sql/Literal.php b/src/Sql/Literal.php index d27974be..5adc57fb 100644 --- a/src/Sql/Literal.php +++ b/src/Sql/Literal.php @@ -8,6 +8,9 @@ use function str_replace; +/** + * @api + */ class Literal implements ExpressionInterface { public function __construct( diff --git a/src/Sql/Predicate/Predicate.php b/src/Sql/Predicate/Predicate.php index c9d2afb7..25222869 100644 --- a/src/Sql/Predicate/Predicate.php +++ b/src/Sql/Predicate/Predicate.php @@ -5,6 +5,7 @@ namespace PhpDb\Sql\Predicate; use PhpDb\Sql\ArgumentInterface; +use PhpDb\Sql\Expression as SqlExpression; use PhpDb\Sql\ExpressionInterface; use PhpDb\Sql\Predicate\Exception\InvalidArgumentException; use PhpDb\Sql\Predicate\Exception\RuntimeException; @@ -15,20 +16,20 @@ /** * @api * - * @psalm-import-type ExpressionParameter from \PhpDb\Sql\Expression + * @psalm-import-type ExpressionParameter from SqlExpression * * @property Predicate $and * @property Predicate $or * @property Predicate $AND * @property Predicate $OR * @property Predicate $nest - * @property Predicate $unnest + * @property ?Predicate $unnest * @property Predicate $NEST - * @property Predicate $UNNEST + * @property ?Predicate $UNNEST */ class Predicate extends PredicateSet { - private ?Predicate $parent = null; + private ?Predicate $unnest = null; protected ?string $nextPredicateCombineOperator = null; @@ -345,7 +346,7 @@ public function predicate(PredicateInterface $predicate): static */ public function setUnnest(?Predicate $predicate = null): void { - $this->parent = $predicate; + $this->unnest = $predicate; } /** @@ -355,12 +356,12 @@ public function setUnnest(?Predicate $predicate = null): void */ public function unnest(): self { - if (! $this->parent instanceof Predicate) { + if (! $this->unnest instanceof Predicate) { throw RuntimeException::forNotNested(); } - $parent = $this->parent; - $this->parent = null; + $parent = $this->unnest; + $this->unnest = null; return $parent; } diff --git a/src/Sql/PreparableSqlInterface.php b/src/Sql/PreparableSqlInterface.php index 3627acad..178f1993 100644 --- a/src/Sql/PreparableSqlInterface.php +++ b/src/Sql/PreparableSqlInterface.php @@ -7,6 +7,9 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\StatementContainerInterface; +/** + * @api + */ interface PreparableSqlInterface { public function prepareStatement( diff --git a/src/Sql/Select.php b/src/Sql/Select.php index cbc72b69..183750be 100644 --- a/src/Sql/Select.php +++ b/src/Sql/Select.php @@ -5,7 +5,6 @@ namespace PhpDb\Sql; use Closure; -use Override; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\Platform\PlatformInterface; @@ -24,7 +23,6 @@ use function key; use function method_exists; use function preg_split; -use function str_contains; use function strcasecmp; use function stripos; use function strtolower; @@ -32,9 +30,11 @@ use function trim; /** - * @property Where $where - * @property Having $having - * @property Join $joins + * @api + * + * @property ?Where $where + * @property ?Having $having + * @property ?Join $joins * @psalm-import-type Specification from AbstractSql * @psalm-import-type PredicateValue from Predicate\PredicateSet * @psalm-import-type JoinName from Join @@ -243,6 +243,9 @@ public function from(array|string|TableIdentifier $table): static return $this; } + /** + * @return ($key is self::COLUMNS ? ColumnList : mixed) + */ public function getRawState(?string $key = null): mixed { $rawState = [ @@ -359,7 +362,7 @@ public function offset(int|string $offset): static public function order(ExpressionInterface|array|string $order): static { $order = match (true) { - is_string($order) => str_contains($order, ',') ? preg_split('#,\s+#', $order) : (array) $order, + is_string($order) => $this->splitOrderString($order), is_array($order) => $order, default => [$order], }; @@ -391,46 +394,25 @@ public function quantifier(ExpressionInterface|string $quantifier): static */ public function reset(string $part): static { - switch ($part) { - case self::TABLE: - if ($this->tableReadOnly) { - throw Exception\InvalidArgumentException::forReadOnlyConstructorState(); - } - - $this->table = null; - break; - case self::QUANTIFIER: - $this->quantifier = null; - break; - case self::COLUMNS: - $this->columns = []; - break; - case self::JOINS: - $this->joins = null; - break; - case self::WHERE: - $this->where = null; - break; - case self::GROUP: - $this->group = null; - break; - case self::HAVING: - $this->having = null; - break; - case self::LIMIT: - $this->limit = null; - break; - case self::OFFSET: - $this->offset = null; - break; - case self::ORDER: - $this->order = []; - break; - case self::COMBINE: - $this->combine = []; - break; + if (self::TABLE === $part && $this->tableReadOnly) { + throw Exception\InvalidArgumentException::forReadOnlyConstructorState(); } + match ($part) { + self::TABLE => $this->table = null, + self::QUANTIFIER => $this->quantifier = null, + self::COLUMNS => $this->columns = [], + self::JOINS => $this->joins = null, + self::WHERE => $this->where = null, + self::GROUP => $this->group = null, + self::HAVING => $this->having = null, + self::LIMIT => $this->limit = null, + self::OFFSET => $this->offset = null, + self::ORDER => $this->order = [], + self::COMBINE => $this->combine = [], + default => null, + }; + return $this; } @@ -469,7 +451,10 @@ public function where( return $this; } - /** @return array{0: string, 1: string}|null */ + /** + * @return array{0: string, 1: string}|null + * @throws Exception\RuntimeException + */ protected function processCombine( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -479,17 +464,23 @@ protected function processCombine( return null; } - $type = $this->combine['modifier'] - ? "{$this->combine['type']} {$this->combine['modifier']}" - : $this->combine['type']; + /** @var Combination $combine */ + $combine = $this->combine; + + $type = '' === $combine['modifier'] + ? $combine['type'] + : "{$combine['type']} {$combine['modifier']}"; return [ strtoupper($type), - $this->processSubSelect($this->combine['select'], $platform, $driver, $parameterContainer), + $this->processSubSelect($combine['select'], $platform, $driver, $parameterContainer), ]; } - /** @return array{0: list}|null */ + /** + * @return array{0: list}|null + * @throws Exception\RuntimeException + */ protected function processGroup( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -516,7 +507,10 @@ protected function processGroup( return [$groups]; } - /** @return array{0: string}|null */ + /** + * @return array{0: string}|null + * @throws Exception\RuntimeException + */ protected function processHaving( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -531,7 +525,10 @@ protected function processHaving( ]; } - /** @return array{0: array>}|null */ + /** + * @return array{0: array>}|null + * @throws Exception\RuntimeException + */ protected function processJoins( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -550,7 +547,7 @@ protected function processLimit( return null; } - if ($parameterContainer instanceof ParameterContainer) { + if ($parameterContainer instanceof ParameterContainer && $driver instanceof DriverInterface) { $paramPrefix = $this->processInfo['paramPrefix']; $parameterContainer->offsetSet("{$paramPrefix}limit", $this->limit, ParameterContainer::TYPE_INTEGER); return [$driver->formatParameterName("{$paramPrefix}limit")]; @@ -569,7 +566,7 @@ protected function processOffset( return null; } - if ($parameterContainer instanceof ParameterContainer) { + if ($parameterContainer instanceof ParameterContainer && $driver instanceof DriverInterface) { $paramPrefix = $this->processInfo['paramPrefix']; $parameterContainer->offsetSet("{$paramPrefix}offset", $this->offset, ParameterContainer::TYPE_INTEGER); return [$driver->formatParameterName("{$paramPrefix}offset")]; @@ -578,7 +575,10 @@ protected function processOffset( return [$platform->quoteValue((string) $this->offset)]; } - /** @return array{0: list>}|null */ + /** + * @return array{0: list>}|null + * @throws Exception\RuntimeException + */ protected function processOrder( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -598,9 +598,9 @@ protected function processOrder( } if (is_int($k)) { - [$k, $v] = str_contains($v, ' ') - ? explode(' ', $v, limit: 2) - : [$v, self::ORDER_ASCENDING]; + $parts = explode(' ', $v, limit: 2); + $k = $parts[0]; + $v = $parts[1] ?? self::ORDER_ASCENDING; } $orders[] = [ @@ -618,6 +618,7 @@ protected function processOrder( * Process the select part * * @return list>> + * @throws Exception\RuntimeException */ protected function processSelect( PlatformInterface $platform, @@ -626,7 +627,7 @@ protected function processSelect( ): array { $expr = 1; - [$table, $fromTable] = $this->resolveTable($this->table, $platform, $driver, $parameterContainer); + [$table, $fromTable] = $this->resolveSelectTable($this->table, $platform, $driver, $parameterContainer); $columns = []; foreach ($this->columns as $columnIndexOrAs => $column) { if (self::SQL_STAR === $column) { @@ -658,7 +659,7 @@ protected function processSelect( foreach ($this->getJoins()->getJoins() as $join) { $joinName = is_array($join['name']) ? array_key_first($join['name']) : $join['name']; - $joinName = parent::resolveTable($joinName, $platform, $driver, $parameterContainer); + $joinName = $this->resolveTable($joinName, $platform, $driver, $parameterContainer); foreach ($join['columns'] as $jKey => $jColumn) { $jColumns = []; @@ -676,12 +677,15 @@ protected function processSelect( $parameterContainer, is_string($jKey) ? $jKey : 'column', ); - if (is_string($jKey)) { - $jColumns[] = $platform->quoteIdentifier($jKey); - } - - if (! is_string($jKey) && self::SQL_STAR !== $jColumn) { - $jColumns[] = $platform->quoteIdentifier($jColumn); + $jColumnAs = match (true) { + is_string($jKey) => $platform->quoteIdentifier($jKey), + self::SQL_STAR === $jColumn => null, + is_string($jColumn) => $platform->quoteIdentifier($jColumn), + default => 'Expression' . $expr++, + }; + + if (null !== $jColumnAs) { + $jColumns[] = $jColumnAs; } $columns[] = $jColumns; @@ -726,7 +730,10 @@ protected function processStatementStart(): ?array return null; } - /** @return array{0: string}|null */ + /** + * @return array{0: string}|null + * @throws Exception\RuntimeException + */ protected function processWhere( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -741,55 +748,65 @@ protected function processWhere( ]; } + private function getHaving(): Having + { + return $this->having ??= new Having(); + } + + private function getJoins(): Join + { + return $this->joins ??= new Join(); + } + + private function getWhere(): Where + { + return $this->where ??= new Where(); + } + + private function prefixFromTable(?string $fromTable, PlatformInterface $platform): string + { + return $this->prefixColumnsWithTable && null !== $fromTable && '' !== $fromTable + ? $fromTable . $platform->getIdentifierSeparator() + : ''; + } + /** * @param Select|TableReference|null $table * @return array{0: string|null, 1: string} + * @throws Exception\RuntimeException */ - #[Override] - protected function resolveTable( + private function resolveSelectTable( Select|string|array|TableIdentifier|null $table, PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null, ): array { - $alias = null; - - if (is_array($table)) { - $alias = array_key_first($table); - $table = $table[$alias]; - } - - $table = parent::resolveTable($table, $platform, $driver, $parameterContainer); + if (! is_array($table)) { + $table = $this->resolveTable($table, $platform, $driver, $parameterContainer); - $fromTable = $table; - if ($alias) { - $fromTable = $platform->quoteIdentifier($alias); - $table = $this->renderTable($table, $fromTable); + return [$table, $this->prefixFromTable($table, $platform)]; } - $fromTable = $this->prefixColumnsWithTable && $fromTable - ? $fromTable . $platform->getIdentifierSeparator() - : ''; + $alias = (string) array_key_first($table); + $fromTable = $platform->quoteIdentifier($alias); return [ - $table, - $fromTable, + $this->renderTable( + (string) $this->resolveTable($table[$alias] ?? null, $platform, $driver, $parameterContainer), + $fromTable, + ), + $this->prefixFromTable($fromTable, $platform), ]; } - private function getHaving(): Having - { - return $this->having ??= new Having(); - } - - private function getJoins(): Join + /** + * @return list + */ + private function splitOrderString(string $order): array { - return $this->joins ??= new Join(); - } + $parts = preg_split('#,\s+#', $order); - private function getWhere(): Where - { - return $this->where ??= new Where(); + return false === $parts ? [$order] : $parts; } /** @@ -801,15 +818,9 @@ private function getWhere(): Where */ public function __clone() { - if (null !== $this->where) { - $this->where = clone $this->where; - } - if (null !== $this->joins) { - $this->joins = clone $this->joins; - } - if (null !== $this->having) { - $this->having = clone $this->having; - } + $this->where = null === $this->where ? null : clone $this->where; + $this->joins = null === $this->joins ? null : clone $this->joins; + $this->having = null === $this->having ? null : clone $this->having; } /** diff --git a/src/Sql/Sql.php b/src/Sql/Sql.php index a0de5c0e..af17e312 100644 --- a/src/Sql/Sql.php +++ b/src/Sql/Sql.php @@ -7,6 +7,9 @@ use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\Driver\StatementInterface; +/** + * @api + */ class Sql { protected AdapterInterface $adapter; @@ -28,6 +31,7 @@ public function __construct( /** * @throws Exception\InvalidArgumentException + * @throws Exception\RuntimeException */ public function buildSqlString(SqlInterface $sqlObject, ?AdapterInterface $adapter = null): string { @@ -51,12 +55,12 @@ public function delete(string|TableIdentifier|null $table = null): Delete return new Delete($table ?? $this->table); } - public function getAdapter(): ?AdapterInterface + public function getAdapter(): AdapterInterface { return $this->adapter; } - public function getSqlPlatform(): ?Platform\PlatformDecoratorInterface + public function getSqlPlatform(): Platform\PlatformDecoratorInterface { return $this->sqlPlatform; } @@ -81,6 +85,9 @@ public function insert(string|TableIdentifier|null $table = null): Insert return new Insert($table ?? $this->table); } + /** + * @throws Exception\RuntimeException + */ public function prepareStatementForSqlObject( PreparableSqlInterface $sqlObject, ?StatementInterface $statement = null, diff --git a/src/Sql/SqlInterface.php b/src/Sql/SqlInterface.php index e1e4b94b..433ed032 100644 --- a/src/Sql/SqlInterface.php +++ b/src/Sql/SqlInterface.php @@ -6,6 +6,9 @@ use PhpDb\Adapter\Platform\PlatformInterface; +/** + * @api + */ interface SqlInterface { /** diff --git a/src/Sql/TableIdentifierFactory.php b/src/Sql/TableIdentifierFactory.php index 0988cbf5..1c57ae96 100644 --- a/src/Sql/TableIdentifierFactory.php +++ b/src/Sql/TableIdentifierFactory.php @@ -4,11 +4,13 @@ namespace PhpDb\Sql; +use PhpDb\ConfigProvider; + /** * Callable factory producing {@see TableIdentifier} instances with a * preconfigured table prefix and separator. * - * Register via {@see \PhpDb\ConfigProvider} and configure the prefix once + * Register via {@see ConfigProvider} and configure the prefix once * (e.g. in application config) to have every identifier created through the * factory share the same prefix — convenient for creating backup_* tables * during a migration. diff --git a/src/Sql/Update.php b/src/Sql/Update.php index d2c692cb..ee9e6020 100644 --- a/src/Sql/Update.php +++ b/src/Sql/Update.php @@ -17,12 +17,14 @@ use function is_numeric; use function is_scalar; use function is_string; -use function str_replace; use function strtolower; /** - * @property Where $where + * @api + * + * @property ?Where $where * @psalm-import-type Specification from AbstractSql + * @psalm-import-type ColumnValue from AbstractSql * @psalm-import-type PredicateValue from Predicate\PredicateSet * @psalm-import-type JoinName from Join */ @@ -62,6 +64,7 @@ class Update extends AbstractPreparableSql protected bool $emptyWhereProtection = true; + /** @var PriorityList|null */ protected ?PriorityList $set = null; protected ?Where $where = null; @@ -109,7 +112,7 @@ public function join(array|string|TableIdentifier $name, string $on, string $typ /** * Set key/value pairs to update * - * @param array $values Associative array of key values + * @param array $values Associative array of key values * @param string|int $flag One of the VALUES_* constants, or a numeric priority * @throws Exception\InvalidArgumentException */ @@ -120,7 +123,7 @@ public function set(array $values, string|int $flag = self::VALUES_SET): static $set->clear(); } - $priority = is_numeric($flag) ? $flag : 0; + $priority = is_numeric($flag) ? (int) $flag : 0; foreach ($values as $k => $v) { if (! is_string($k)) { throw Exception\InvalidArgumentException::forNonStringValueKey(); @@ -165,7 +168,10 @@ public function where( return $this; } - /** @return string[][][]|null */ + /** + * @return string[][][]|null + * @throws Exception\RuntimeException + */ protected function processJoins( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -174,6 +180,9 @@ protected function processJoins( return $this->processJoin($this->joins, $platform, $driver, $parameterContainer); } + /** + * @throws Exception\RuntimeException + */ protected function processSet( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -196,7 +205,7 @@ protected function processSet( 'column', ); $prefix .= ' = '; - if (is_scalar($value) && $parameterContainer) { + if (is_scalar($value) && $parameterContainer && $driver instanceof DriverInterface) { // use incremental value instead of column name for PDO // @see https://github.com/zendframework/zend-db/issues/35 if ($isPdoDriver) { @@ -217,25 +226,29 @@ protected function processSet( ); } - return str_replace( - '%1$s', + return $this->formatSpecification( + static::SPECIFICATION_SET, implode(', ', $setSql), - $this->specifications[static::SPECIFICATION_SET], ); } + /** + * @throws Exception\RuntimeException + */ protected function processUpdate( PlatformInterface $platform, ?DriverInterface $driver = null, ?ParameterContainer $parameterContainer = null, ): string { - return str_replace( - '%1$s', + return $this->formatSpecification( + static::SPECIFICATION_UPDATE, $this->resolveTable($this->unaliasTable($this->table), $platform, $driver, $parameterContainer), - $this->specifications[static::SPECIFICATION_UPDATE], ); } + /** + * @throws Exception\RuntimeException + */ protected function processWhere( PlatformInterface $platform, ?DriverInterface $driver = null, @@ -245,10 +258,9 @@ protected function processWhere( return null; } - return str_replace( - '%1$s', + return $this->formatSpecification( + static::SPECIFICATION_WHERE, $this->processExpression($this->where, $platform, $driver, $parameterContainer, 'where'), - $this->specifications[static::SPECIFICATION_WHERE], ); } @@ -257,6 +269,9 @@ private function getJoins(): Join return $this->joins ??= new Join(); } + /** + * @return PriorityList + */ private function getSet(): PriorityList { if (null === $this->set) { @@ -280,15 +295,9 @@ private function getWhere(): Where */ public function __clone() { - if (null !== $this->where) { - $this->where = clone $this->where; - } - if (null !== $this->joins) { - $this->joins = clone $this->joins; - } - if (null !== $this->set) { - $this->set = clone $this->set; - } + $this->where = null === $this->where ? null : clone $this->where; + $this->joins = null === $this->joins ? null : clone $this->joins; + $this->set = null === $this->set ? null : clone $this->set; } /** diff --git a/src/Sql/Where.php b/src/Sql/Where.php index e9bddaa2..11a4217e 100644 --- a/src/Sql/Where.php +++ b/src/Sql/Where.php @@ -4,4 +4,7 @@ namespace PhpDb\Sql; +/** + * @api + */ class Where extends Predicate\Predicate {} diff --git a/test/unit/Sql/AbstractSqlTest.php b/test/unit/Sql/AbstractSqlTest.php index fdfcfe01..4908dd50 100644 --- a/test/unit/Sql/AbstractSqlTest.php +++ b/test/unit/Sql/AbstractSqlTest.php @@ -15,6 +15,7 @@ use PhpDb\Sql\Argument\Identifier; use PhpDb\Sql\ArgumentInterface; use PhpDb\Sql\ArgumentType; +use PhpDb\Sql\Delete; use PhpDb\Sql\Exception\InvalidArgumentException; use PhpDb\Sql\Exception\RuntimeException; use PhpDb\Sql\Expression; @@ -23,6 +24,7 @@ use PhpDb\Sql\Predicate; use PhpDb\Sql\Select; use PhpDb\Sql\TableIdentifier; +use PhpDb\Sql\Update; use PhpDbTest\TestAsset\SelectDecorator; use PhpDbTest\TestAsset\TrustingSql92Platform; use PHPUnit\Framework\Attributes\CoversMethod; @@ -37,6 +39,7 @@ use ReflectionException; use ReflectionMethod; +use function array_values; use function count; use function current; use function key; @@ -62,6 +65,12 @@ #[CoversMethod(AbstractSql::class, 'resolveTable')] #[CoversMethod(AbstractSql::class, 'localizeVariables')] #[CoversMethod(AbstractSql::class, 'quoteJoinTableIdentifier')] +#[CoversMethod(AbstractSql::class, 'formatSpecification')] +#[CoversMethod(AbstractSql::class, 'flattenParameterList')] +#[CoversMethod(AbstractSql::class, 'formatParameterList')] +#[CoversMethod(AbstractSql::class, 'matchSpecification')] +#[CoversMethod(AbstractSql::class, 'renderParameter')] +#[CoversMethod(AbstractSql::class, 'processExpressionList')] final class AbstractSqlTest extends TestCase { protected AbstractSql&MockObject $abstractSql; @@ -117,6 +126,26 @@ public function createSqlFromSpecNonCombinedByThrowsOnUnsupportedCount(): void $method->invoke($this->abstractSql, $spec, $params); } + /** + * @throws ReflectionException + */ + #[Test] + public function createSqlFromSpecRejectsListForSlotWithoutSpecification(): void + { + $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters'); + + $spec = [ + 'SELECT %1$s FROM %2$s' => [ + [1 => '%1$s', 'combinedby' => ', '], + null, + ], + ]; + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNSUPPORTED_PARAMETER_COUNT_OF, 2)); + $method->invoke($this->abstractSql, $spec, [['col1'], ['a', 'b']]); + } + /** * @throws ReflectionException */ @@ -179,6 +208,19 @@ public function createSqlFromSpecWithNonCombinedByParam(): void static::assertSame('FROM my_table', $result); } + /** + * @throws ReflectionException + */ + #[Test] + public function createSqlFromStringSpecRejectsListParameter(): void + { + $method = new ReflectionMethod($this->abstractSql, 'createSqlFromSpecificationAndParameters'); + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf(RuntimeException::UNSUPPORTED_PARAMETER_COUNT_OF, 2)); + $method->invoke($this->abstractSql, 'LIMIT %1$s', [['a', 'b']]); + } + #[Test] public function flattenExpressionValuesViaInPredicate(): void { @@ -215,6 +257,25 @@ public function flattenExpressionValuesViaInPredicateWithParameterContainer(): v static::assertSame(3, $parameterContainer->count()); } + #[Test] + public function formatSpecificationThrowsForSpecificationThatIsNotAString(): void + { + $delete = new class('foo') extends Delete { + /** @var array|null>>|string> */ + protected array $specifications = [ + self::SPECIFICATION_DELETE => ['DELETE FROM %1$s' => [null]], + self::SPECIFICATION_WHERE => 'WHERE %1$s', + ]; + }; + + self::expectException(RuntimeException::class); + self::expectExceptionMessage(sprintf( + RuntimeException::STRING_SPECIFICATION_REQUIRED, + Delete::SPECIFICATION_DELETE, + )); + $delete->getSqlString(new TrustingSql92Platform()); + } + #[Test] public function localizeVariablesCopiesSubjectProperties(): void { @@ -229,6 +290,63 @@ public function localizeVariablesCopiesSubjectProperties(): void static::assertStringContainsString('"id"', $sql); } + /** + * @throws ReflectionException + */ + #[Test] + public function processExpressionBindsNullParameterValue(): void + { + $method = new ReflectionMethod($this->abstractSql, 'processExpression'); + $container = new ParameterContainer(); + + $sql = $method->invoke( + $this->abstractSql, + new Expression('COALESCE(?, 1)', [null]), + new TrustingSql92Platform(), + $this->mockDriver, + $container, + ); + + static::assertMatchesRegularExpression('/^COALESCE\(:expr\d+Param1, 1\)$/', (string) $sql); + static::assertSame([null], array_values($container->getNamedArray())); + } + + /** + * @throws ReflectionException + */ + #[Test] + public function processExpressionListRendersEachExpression(): void + { + $method = new ReflectionMethod($this->abstractSql, 'processExpressionList'); + + $sqls = $method->invoke( + $this->abstractSql, + ['first' => new Expression('a'), 'second' => new Expression('b')], + new TrustingSql92Platform(), + ); + + static::assertSame(['a', 'b'], $sqls); + } + + /** + * @throws ReflectionException + */ + #[Test] + public function processExpressionQuotesValueWhenContainerHasNoDriver(): void + { + $method = new ReflectionMethod($this->abstractSql, 'processExpression'); + + $sql = $method->invoke( + $this->abstractSql, + new Expression('x = ?', ['a']), + new TrustingSql92Platform(), + null, + new ParameterContainer(), + ); + + static::assertSame("x = 'a'", $sql); + } + #[Test] public function processExpressionThrowsOnUnknownArgumentType(): void { @@ -656,6 +774,32 @@ public function renderTableWithAlias(): void static::assertSame('"foo" AS "f"', $result); } + /** + * @throws ReflectionException + */ + #[Test] + public function resolveColumnValuePrefixesIdentifierWithTable(): void + { + $method = new ReflectionMethod($this->abstractSql, 'resolveColumnValue'); + + static::assertSame( + '"t"."col"', + $method->invoke( + $this->abstractSql, + ['column' => 'col', 'fromTable' => '"t".', 'isIdentifier' => true], + new TrustingSql92Platform(), + ), + ); + } + + #[Test] + public function resolveColumnValueQuotesFloatValue(): void + { + $update = (new Update('t'))->set(['a' => 1.5]); + + static::assertSame('UPDATE "t" SET "a" = \'1.5\'', $update->getSqlString(new TrustingSql92Platform())); + } + /** * @throws ReflectionException */ diff --git a/test/unit/Sql/Exception/RuntimeExceptionTest.php b/test/unit/Sql/Exception/RuntimeExceptionTest.php index 01a8996d..57f965b2 100644 --- a/test/unit/Sql/Exception/RuntimeExceptionTest.php +++ b/test/unit/Sql/Exception/RuntimeExceptionTest.php @@ -17,6 +17,7 @@ #[Group('unit')] #[CoversMethod(RuntimeException::class, 'forReplacementMismatch')] +#[CoversMethod(RuntimeException::class, 'forStringSpecificationRequired')] #[CoversMethod(RuntimeException::class, 'forSubjectNotImplementing')] #[CoversMethod(RuntimeException::class, 'forSubjectNotPreparableSqlInterface')] #[CoversMethod(RuntimeException::class, 'forSubjectNotSqlInterface')] @@ -33,6 +34,11 @@ public static function namedConstructorProvider(): array [], RuntimeException::REPLACEMENT_MISMATCH, ], + 'string specification required' => [ + 'forStringSpecificationRequired', + ['delete'], + RuntimeException::STRING_SPECIFICATION_REQUIRED, + ], 'subject not implementing' => [ 'forSubjectNotImplementing', [SqlInterface::class, 'getSqlString()'], diff --git a/test/unit/Sql/InsertTest.php b/test/unit/Sql/InsertTest.php index 905afe63..4d6b8d42 100644 --- a/test/unit/Sql/InsertTest.php +++ b/test/unit/Sql/InsertTest.php @@ -364,6 +364,17 @@ public function processInsertWithPdoDriverUsesDriverColumnQuoting(): void static::assertStringContainsString(':c_1', $sql); } + #[Test] + public function quotesFloatValue(): void + { + $insert = (new Insert('t'))->values(['a' => 1.5]); + + static::assertSame( + 'INSERT INTO "t" ("a") VALUES (\'1.5\')', + $insert->getSqlString(new TrustingSql92Platform()), + ); + } + #[Test] #[CoversNothing] public function specificationconstantsCouldBeOverridedByExtensionInGetSqlString(): void diff --git a/test/unit/Sql/JoinTest.php b/test/unit/Sql/JoinTest.php index 9e004491..a485551f 100644 --- a/test/unit/Sql/JoinTest.php +++ b/test/unit/Sql/JoinTest.php @@ -55,6 +55,16 @@ public function countsJoinedTables(): void static::assertCount($join->count(), $join->getJoins()); } + #[Test] + public function currentReturnsNullPastTheLastJoin(): void + { + $join = new Join(); + $join->join('foo', 'a = b'); + $join->next(); + + static::assertNull($join->current()); + } + #[Test] public function currentReturnsTheCurrentJoinSpecification(): void { diff --git a/test/unit/Sql/SelectTest.php b/test/unit/Sql/SelectTest.php index eeabdc93..0daf0ce8 100644 --- a/test/unit/Sql/SelectTest.php +++ b/test/unit/Sql/SelectTest.php @@ -36,6 +36,7 @@ use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; use ReflectionException; +use ReflectionMethod; use ReflectionObject; use TypeError; @@ -72,7 +73,12 @@ #[CoversMethod(Select::class, 'processCombine')] #[CoversMethod(Select::class, 'processStatementStart')] #[CoversMethod(Select::class, 'processStatementEnd')] -#[CoversMethod(Select::class, 'resolveTable')] +#[CoversMethod(Select::class, 'resolveSelectTable')] +#[CoversMethod(Select::class, 'splitOrderString')] +#[CoversMethod(Select::class, 'prefixFromTable')] +#[CoversMethod(Select::class, 'getWhere')] +#[CoversMethod(Select::class, 'getHaving')] +#[CoversMethod(Select::class, 'getJoins')] final class SelectTest extends TestCase { use AdapterTestTrait; @@ -1214,6 +1220,17 @@ public function join(): void static::assertSame('bar', $joins2->getJoins()[1]['name']); } + #[Test] + public function joinAliasesUnkeyedExpressionColumn(): void + { + $select = (new Select('a'))->join('b', 'a.id = b.id', [new Expression('COUNT(*)')]); + + static::assertSame( + 'SELECT "a".*, COUNT(*) AS Expression1 FROM "a" INNER JOIN "b" ON "a"."id" = "b"."id"', + $select->getSqlString(new TrustingSql92Platform()), + ); + } + #[Test] #[TestDox(': unit test: test limit()')] public function limit(): void @@ -1248,6 +1265,18 @@ public function limitExceptionOnInvalidParameter(): void $select->limit('foobar'); } + #[Test] + public function limitIsQuotedWhenContainerHasNoDriver(): void + { + $select = (new Select('a'))->limit(5); + $method = new ReflectionMethod($select, 'processLimit'); + + static::assertSame( + ["'5'"], + $method->invoke($select, new TrustingSql92Platform(), null, new ParameterContainer()), + ); + } + #[Test] #[TestDox('unit test: Test __get() returns expected objects magically')] public function magicAccessor(): void @@ -1264,6 +1293,17 @@ public function magicGetJoinsReturnsJoinInstance(): void static::assertInstanceOf(Join::class, $select->joins); } + #[Test] + public function numbersEachUnkeyedJoinExpression(): void + { + $select = (new Select('a'))->join('b', 'a.id = b.id', [new Expression('MIN(x)'), new Expression('MAX(x)')]); + + static::assertSame( + 'SELECT "a".*, MIN(x) AS Expression1, MAX(x) AS Expression2 FROM "a" INNER JOIN "b" ON "a"."id" = "b"."id"', + $select->getSqlString(new TrustingSql92Platform()), + ); + } + #[Test] #[TestDox(': unit test: test offset()')] public function offset(): void @@ -1357,6 +1397,17 @@ public function orderCorrectlySplitsParameter(): void ); } + #[Test] + public function orderSplitsCommaSeparatedString(): void + { + $select = (new Select('a'))->order('x DESC, y'); + + static::assertSame( + 'SELECT "a".* FROM "a" ORDER BY "x" DESC, "y" ASC', + $select->getSqlString(new TrustingSql92Platform()), + ); + } + #[Test] public function orderWithAssociativeArray(): void { @@ -1497,6 +1548,24 @@ public function quantifierParameterExpressionInterface(): void ); } + #[Test] + public function repeatedClauseCallsAccumulate(): void + { + $select = (new Select('a'))->join('b', 'a.id = b.id', []) + ->join('c', 'a.id = c.id', []) + ->where('x = 1') + ->where('y = 2') + ->group('x') + ->having('x > 1') + ->having('y > 2'); + + static::assertSame( + 'SELECT "a".* FROM "a" INNER JOIN "b" ON "a"."id" = "b"."id" INNER JOIN "c" ON "a"."id" = "c"."id"' + . ' WHERE x = 1 AND y = 2 GROUP BY "x" HAVING x > 1 AND y > 2', + $select->getSqlString(new TrustingSql92Platform()), + ); + } + #[Test] #[TestDox('unit test: Test reset() resets internal stat of Select object, based on input')] public function reset(): void diff --git a/test/unit/Sql/UpdateTest.php b/test/unit/Sql/UpdateTest.php index 17182db4..d608d871 100644 --- a/test/unit/Sql/UpdateTest.php +++ b/test/unit/Sql/UpdateTest.php @@ -59,6 +59,7 @@ #[CoversMethod(Update::class, 'processWhere')] #[CoversMethod(Update::class, 'processJoins')] #[CoversMethod(AbstractPreparableSql::class, 'unaliasTable')] +#[CoversMethod(Update::class, 'getJoins')] final class UpdateTest extends TestCase { use AdapterTestTrait; @@ -337,6 +338,14 @@ public function joinWithTableIdentifier(): void static::assertStringContainsString('JOIN "schema"."bar"', $sql); } + #[Test] + public function numericFlagSetsPriorityAboveDefault(): void + { + $update = (new Update('t'))->set(['a' => new Expression('1')])->set(['b' => new Expression('2')], 1); + + static::assertSame('UPDATE "t" SET "b" = 2, "a" = 1', $update->getSqlString(new TrustingSql92Platform())); + } + #[Test] #[Group('Laminas-240')] public function passingMultipleKeyValueInWhereClause(): void @@ -420,6 +429,19 @@ public function processSetWithPdoDriverUsesDriverColumnQuoting(): void static::assertStringContainsString(':c_1', $sql); } + #[Test] + public function repeatedJoinCallsAccumulate(): void + { + $update = (new Update('a'))->set(['x' => new Expression('1')]) + ->join('b', 'a.id = b.id') + ->join('c', 'a.id = c.id'); + + static::assertSame( + 'UPDATE "a" INNER JOIN "b" ON "a"."id" = "b"."id" INNER JOIN "c" ON "a"."id" = "c"."id" SET "x" = 1', + $update->getSqlString(new TrustingSql92Platform()), + ); + } + #[Test] public function set(): void {