Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 12 additions & 42 deletions analyzer-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,7 @@ count = 1
[[issues]]
file = "src/Adapter/Profiler/Profiler.php"
code = "invalid-property-assignment-value"
message = '''Invalid type for property `$profiles`: expected `array<array-key, array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}>`, but got `array<array-key, array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': null|string, 'start': float}>`.'''
message = '''Invalid type for property `$profiles`: expected `array<array-key, array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}>`, but got `non-empty-array<array-key, array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': null|string, 'start': float}>`.'''
count = 1

[[issues]]
Expand All @@ -1194,24 +1194,6 @@ code = "missing-property-type"
message = "Property `$profiles` is missing a type hint."
count = 1

[[issues]]
file = "src/Adapter/Profiler/Profiler.php"
code = "possibly-null-operand"
message = "Right operand in arithmetic operation might be `null` (type `float|null`)."
count = 1

[[issues]]
file = "src/Adapter/Profiler/Profiler.php"
code = "possibly-undefined-int-array-index"
message = '''Possibly undefined array key `int` accessed on `array<array-key, array{'elapse': float|null, 'end': float|null, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}>`.'''
count = 1

[[issues]]
file = "src/Adapter/Profiler/Profiler.php"
code = "possibly-undefined-string-array-index"
message = '''Possibly undefined array key accessed on `array{'elapse': float|null, 'end': float, 'parameters': PhpDb\Adapter\ParameterContainer|null, 'sql': string, 'start': float}|array{'end': float}`.'''
count = 1

[[issues]]
file = "src/Adapter/Profiler/Profiler.php"
code = "unhandled-thrown-type"
Expand Down Expand Up @@ -2289,7 +2271,7 @@ 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<array-key, Stringable|null|scalar>`, but provided type `array{}|non-empty-list<mixed>` is less specific."
message = "Argument type mismatch for argument #2 of `vsprintf`: expected `array<array-key, Stringable|null|scalar>`, but provided type `list<mixed>` is less specific."
count = 1

[[issues]]
Expand Down Expand Up @@ -3321,7 +3303,13 @@ count = 1
[[issues]]
file = "src/Sql/Ddl/CreateTable.php"
code = "possibly-undefined-string-array-index"
message = "Possibly undefined array key `string('combinedBy')` accessed on `array<array-key, array<array-key, mixed>|string>`."
message = "Possibly undefined array key `string('combinedBy')` accessed on `array<array-key, array<array-key, mixed>>`."
count = 1

[[issues]]
file = "src/Sql/Ddl/CreateTable.php"
code = "possibly-undefined-string-array-index"
message = "Possibly undefined array key `string('combinedBy')` accessed on `array<array-key, string>`."
count = 1

[[issues]]
Expand Down Expand Up @@ -4353,7 +4341,7 @@ count = 1
[[issues]]
file = "src/Sql/Predicate/PredicateSet.php"
code = "less-specific-nested-argument-type"
message = "Argument type mismatch for argument #2 of `implode`: expected `array<array-key, Stringable|null|scalar>|null`, but provided type `array{}|non-empty-list<mixed>` is less specific."
message = "Argument type mismatch for argument #2 of `implode`: expected `array<array-key, Stringable|null|scalar>|null`, but provided type `list<mixed>` is less specific."
count = 1

[[issues]]
Expand All @@ -4365,7 +4353,7 @@ count = 1
[[issues]]
file = "src/Sql/Predicate/PredicateSet.php"
code = "less-specific-nested-return-statement"
message = '''Returned type `array{'spec': string, 'values': array{}|list<mixed>}` is less specific than the declared return type `array{'spec': string, 'values': array<array-key, PhpDb\Sql\ArgumentInterface>}` for function `PhpDb\Sql\Predicate\PredicateSet::getExpressionData` due to nested 'mixed'.'''
message = '''Returned type `array{'spec': string, 'values': list<mixed>}` is less specific than the declared return type `array{'spec': string, 'values': array<array-key, PhpDb\Sql\ArgumentInterface>}` for function `PhpDb\Sql\Predicate\PredicateSet::getExpressionData` due to nested 'mixed'.'''
count = 1

[[issues]]
Expand Down Expand Up @@ -4623,7 +4611,7 @@ count = 1
[[issues]]
file = "src/Sql/Select.php"
code = "invalid-property-assignment-value"
message = "Invalid type for property `$specifications`: expected `array<array-key, array<array-key, mixed>>|array<array-key, string>`, but got `array<array-key, array<array-key, mixed>|string>`."
message = "Invalid type for property `$specifications`: expected `array<array-key, array<array-key, mixed>>|array<array-key, string>`, but got `non-empty-array<array-key, array<array-key, mixed>|string>`."
count = 1

[[issues]]
Expand Down Expand Up @@ -5898,30 +5886,12 @@ code = "class-must-be-final"
message = 'Class `PhpDb\TableGateway\Feature\GlobalAdapterFeature` should be declared `final`.'
count = 1

[[issues]]
file = "src/TableGateway/Feature/GlobalAdapterFeature.php"
code = "invalid-return-statement"
message = 'Invalid return type for function `PhpDb\TableGateway\Feature\GlobalAdapterFeature::getStaticAdapter`: expected `PhpDb\Adapter\AdapterInterface`, but found `PhpDb\Adapter\AdapterInterface|null`.'
count = 1

[[issues]]
file = "src/TableGateway/Feature/GlobalAdapterFeature.php"
code = "missing-constructor"
message = 'Class `PhpDb\TableGateway\Feature\GlobalAdapterFeature` has typed properties without default values but no constructor to initialize them.'
count = 1

[[issues]]
file = "src/TableGateway/Feature/GlobalAdapterFeature.php"
code = "nullable-return-statement"
message = 'Function `PhpDb\TableGateway\Feature\GlobalAdapterFeature::getStaticAdapter` is declared to return `PhpDb\Adapter\AdapterInterface` but possibly returns a nullable value (inferred as `PhpDb\Adapter\AdapterInterface|null`).'
count = 1

[[issues]]
file = "src/TableGateway/Feature/GlobalAdapterFeature.php"
code = "possibly-undefined-string-array-index"
message = '''Possibly undefined array key `class-string('PhpDb\TableGateway\Feature\GlobalAdapterFeature')` accessed on `array<array-key, PhpDb\Adapter\AdapterInterface>`.'''
count = 1

[[issues]]
file = "src/TableGateway/Feature/MasterSlaveFeature.php"
code = "class-must-be-final"
Expand Down
40 changes: 32 additions & 8 deletions docs/book/sql-ddl/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,26 +217,43 @@ $check = new Check(

### Using Expressions in Check Constraints

Check constraints can accept either string expressions or `Expression` objects.
Check constraints accept either a string or any `ExpressionInterface` implementation such as
`PhpDb\Sql\Expression`. The name is optional; without one the constraint renders as a bare
`CHECK (...)` clause.

#### String Expressions (Simple)

For simple constraints, use strings:
A string is an expression slot by design: it is rendered verbatim as a literal SQL fragment, so
identifiers and values inside it are not quoted or escaped. Only pass trusted input. An empty
string throws `PhpDb\Sql\Exception\InvalidArgumentException` at construction.

```php
use PhpDb\Sql\Ddl\Constraint\Check;

// Simple string expression
// Named
$check = new Check('age >= 18', 'check_adult');
$check = new Check('price > 0', 'check_positive_price');
$check = new Check("status IN ('active', 'pending', 'completed')", 'check_valid_status');

// Unnamed
$check = new Check('price > 0');
```

**Generated SQL:**

```sql
CONSTRAINT "check_adult" CHECK (age >= 18)
CHECK (price > 0)
```

#### Expression Objects (Advanced)

For complex or parameterized constraints, use `Expression` objects:
An `Expression` is rendered through the platform, so its identifier and value arguments are
quoted. Values are always inlined into the DDL statement: DDL is never prepared, and MySQL rejects
parameter markers inside a `CHECK` clause.

```php
use PhpDb\Sql\Argument\Identifier;
use PhpDb\Sql\Expression;
use PhpDb\Sql\Ddl\Constraint\Check;

Expand All @@ -247,12 +264,19 @@ $expr = new Expression(
);
$check = new Check($expr, 'check_valid_age_range');

// Complex expression
// Expression with quoted identifiers
$expr = new Expression(
'discount_percent BETWEEN ? AND ?',
[0, 100]
'? > ?',
[new Identifier('end_date'), new Identifier('start_date')]
);
$check = new Check($expr, 'check_discount_range');
$check = new Check($expr, 'check_date_range');
```

**Generated SQL:**

```sql
CONSTRAINT "check_valid_age_range" CHECK (age >= '18' AND age <= '120')
CONSTRAINT "check_date_range" CHECK ("end_date" > "start_date")
```

## Indexes
Expand Down
10 changes: 6 additions & 4 deletions src/Metadata/Source/AbstractSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
$schema = $this->defaultSchema;
}

$this->loadColumnData($table, $schema);

Check warning on line 127 in src/Metadata/Source/AbstractSource.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ $schema = $this->defaultSchema; } - $this->loadColumnData($table, $schema); + $info = $this->data['columns'][$schema][$table][$columnName] ?? null; if (null === $info) {

$info = $this->data['columns'][$schema][$table][$columnName] ?? null;
if (null === $info) {
Expand All @@ -142,10 +142,10 @@
$column->setCharacterOctetLength(
$info['character_octet_length'] ? (int) $info['character_octet_length'] : null,
);
$column->setNumericPrecision(

Check warning on line 145 in src/Metadata/Source/AbstractSource.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ $column->setCharacterOctetLength( $info['character_octet_length'] ? (int) $info['character_octet_length'] : null, ); - $column->setNumericPrecision( - $info['numeric_precision'] ? (int) $info['numeric_precision'] : null, - ); + $column->setNumericScale( $info['numeric_scale'] ? (int) $info['numeric_scale'] : null, );
$info['numeric_precision'] ? (int) $info['numeric_precision'] : null,
);
$column->setNumericScale(

Check warning on line 148 in src/Metadata/Source/AbstractSource.php

View workflow job for this annotation

GitHub Actions / qa / Mutation testing

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ $column->setNumericPrecision( $info['numeric_precision'] ? (int) $info['numeric_precision'] : null, ); - $column->setNumericScale( - $info['numeric_scale'] ? (int) $info['numeric_scale'] : null, - ); + $column->setNumericUnsigned($info['numeric_unsigned']); $column->setErratas($info['erratas']);
$info['numeric_scale'] ? (int) $info['numeric_scale'] : null,
);
$column->setNumericUnsigned($info['numeric_unsigned']);
Expand Down Expand Up @@ -693,15 +693,17 @@
* Prepare data hierarchy
*
* The by-reference walk builds arbitrary depths of the hierarchy, which
* cannot be expressed against the MetadataData shape.
* cannot be expressed against the MetadataData shape. The reference is
* re-typed as a plain array so the analyzer does not re-derive the shape
* on every iteration, which does not terminate in useful time on Mago
* 1.47.5 and later.
*
* @mago-expect analysis:possibly-undefined-string-array-index
* @mago-expect analysis:possibly-undefined-int-array-index
* @mago-expect analysis:possibly-null-array-access
* @mago-expect analysis:mixed-assignment
* @mago-expect lint:no-isset
*/
protected function prepareDataHierarchy(string $type, string ...$keys): void
{
/** @var array<array-key, mixed> $data */
$data = &$this->data;
foreach ([$type, ...$keys] as $key) {
if (! isset($data[$key])) {
Expand Down
23 changes: 18 additions & 5 deletions src/Sql/Ddl/Constraint/Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use Override;
use PhpDb\Sql\Argument\Identifier;
use PhpDb\Sql\Argument\Literal;
use PhpDb\Sql\Exception\InvalidArgumentException;
use PhpDb\Sql\ExpressionInterface;

use function implode;
use function sprintf;

class Check extends AbstractConstraint
{
Expand All @@ -18,10 +20,17 @@ class Check extends AbstractConstraint
protected string $specification = 'CHECK (%s)';

/**
* @param string|ExpressionInterface $expression
* A string expression is rendered verbatim as a literal SQL fragment. An ExpressionInterface
* expression is rendered through the platform, so its identifiers and values are quoted.
*
* @throws InvalidArgumentException When the expression is an empty string.
*/
public function __construct($expression, ?string $name)
public function __construct(string|ExpressionInterface $expression, ?string $name = null)
{
if ('' === $expression) {
throw new InvalidArgumentException('Check constraint expression must not be an empty string.');
}

parent::__construct(null, $name);

$this->expression = $expression;
Expand All @@ -31,6 +40,10 @@ public function __construct($expression, ?string $name)
#[Override]
public function getExpressionData(): array
{
$checkData = $this->expression instanceof ExpressionInterface
? $this->expression->getExpressionData()
: ['spec' => '%s', 'values' => [new Literal($this->expression)]];

$specParts = [];
$values = [];

Expand All @@ -39,9 +52,9 @@ public function getExpressionData(): array
$values[] = new Identifier($this->name);
}

if ('' !== $this->expression) {
$specParts[] = $this->specification;
$values[] = new Literal($this->expression);
$specParts[] = sprintf($this->specification, $checkData['spec']);
foreach ($checkData['values'] as $value) {
$values[] = $value;
}

return [
Expand Down
8 changes: 4 additions & 4 deletions src/TableGateway/AbstractTableGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ protected function executeSelect(Select $select): ResultSetInterface

if (
isset($selectState['columns'])
&& [Select::SQL_STAR] === $selectState['columns']
&& [] !== $this->columns
&& [Select::SQL_STAR] === $selectState['columns']
&& [] !== $this->columns
) {
$select->columns($this->columns);
}
Expand Down Expand Up @@ -430,8 +430,8 @@ public function __clone(): void
$this->table = clone $this->table;
} elseif (
is_array($this->table)
&& count($this->table) === 1
&& is_object(reset($this->table))
&& count($this->table) === 1
&& is_object(reset($this->table))
) {
foreach ($this->table as &$tableObject) {
$tableObject = clone $tableObject;
Expand Down
Loading
Loading