diff --git a/CHANGELOG.md b/CHANGELOG.md index 65166de83..3fefe6981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Enh #806, #964: Build `Expression` instances inside `Expression::$params` when build a query using `QueryBuilder` (@Tigrov) - Enh #766: Allow `ColumnInterface` as column type. (@Tigrov) - Bug #828: Fix `float` type when use `AbstractCommand::getRawSql()` method (@Tigrov) -- New #752, #974: Implement `ColumnSchemaInterface` classes according to the data type of database table columns +- New #752, #974, #1013: Implement `ColumnInterface` classes according to the data type of database table columns for type casting performance (@Tigrov) - Enh #829: Rename `batchInsert()` to `insertBatch()` in `DMLQueryBuilderInterface` and `CommandInterface` and change parameters from `$table, $columns, $rows` to `$table, $rows, $columns = []` (@Tigrov) @@ -115,6 +115,7 @@ - Enh #1010: Improve `Quoter::getTableNameParts()` method (@Tigrov) - Enh #1011: Refactor `TableSchemaInterface` and `AbstractSchema` (@Tigrov) - Enh #1011: Remove `AbstractTableSchema` and add `TableSchema` instead (@Tigrov) +- New #1013: Add `StringableStream` class to cast binary column values to `string` using `(string) $value` (@Tigrov) - Chg #1014: Replace `getEscapingReplacements()`/`setEscapingReplacements()` methods with `escape` constructor parameter in `Like` condition (@vjik) diff --git a/rector.php b/rector.php index 5133efe2a..a72c488b7 100644 --- a/rector.php +++ b/rector.php @@ -5,6 +5,8 @@ use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector; use Rector\Config\RectorConfig; use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector; +use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; +use Rector\Php80\Rector\Class_\StringableForToStringRector; use Rector\Php80\Rector\Ternary\GetDebugTypeRector; use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; @@ -19,9 +21,15 @@ InlineConstructorDefaultToPropertyRector::class, ]) ->withSkip([ + ClassPropertyAssignToConstructorPromotionRector::class => [ + __DIR__ . '/src/Schema/Data/StringableStream.php', + ], RestoreDefaultNullToNullableTypePropertyRector::class => [ __DIR__ . '/src/Expression/CaseExpression.php', ], + StringableForToStringRector::class => [ + __DIR__ . '/src/Schema/Data/StringableStream.php', + ], GetDebugTypeRector::class => [ __DIR__ . '/tests/AbstractColumnTest.php', ], diff --git a/src/QueryBuilder/AbstractQueryBuilder.php b/src/QueryBuilder/AbstractQueryBuilder.php index aeef10186..4c032d894 100644 --- a/src/QueryBuilder/AbstractQueryBuilder.php +++ b/src/QueryBuilder/AbstractQueryBuilder.php @@ -23,6 +23,7 @@ use Yiisoft\Db\QueryBuilder\Condition\ConditionInterface; use Yiisoft\Db\Schema\Column\ColumnFactoryInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; +use Yiisoft\Db\Schema\Data\StringableStream; use Yiisoft\Db\Schema\QuoterInterface; use Yiisoft\Db\Syntax\AbstractSqlParser; @@ -279,6 +280,7 @@ public function buildValue(mixed $value, array &$params): string GettypeResult::OBJECT => match (true) { $value instanceof Param => $this->bindParam($value, $params), $value instanceof ExpressionInterface => $this->buildExpression($value, $params), + $value instanceof StringableStream => $this->bindParam(new Param($value->getValue(), DataType::LOB), $params), $value instanceof Stringable => $this->bindParam(new Param((string) $value, DataType::STRING), $params), $value instanceof BackedEnum => is_string($value->value) ? $this->bindParam(new Param($value->value, DataType::STRING), $params) @@ -475,6 +477,7 @@ public function prepareValue(mixed $value): string $this->buildExpression($value, $params), array_map($this->prepareValue(...), $params), ), + $value instanceof StringableStream => $this->prepareBinary((string) $value), $value instanceof BackedEnum => is_string($value->value) ? $this->db->getQuoter()->quoteValue($value->value) : (string) $value->value, diff --git a/src/Schema/Column/BinaryColumn.php b/src/Schema/Column/BinaryColumn.php index 424cb6c74..931bed086 100644 --- a/src/Schema/Column/BinaryColumn.php +++ b/src/Schema/Column/BinaryColumn.php @@ -11,8 +11,10 @@ use Yiisoft\Db\Constant\ColumnType; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Constant\GettypeResult; +use Yiisoft\Db\Schema\Data\StringableStream; use function gettype; +use function is_resource; /** * Represents the metadata for a binary column. @@ -31,17 +33,19 @@ public function dbTypecast(mixed $value): mixed GettypeResult::DOUBLE => (string) $value, GettypeResult::BOOLEAN => $value ? '1' : '0', GettypeResult::OBJECT => match (true) { + $value instanceof StringableStream => new Param($value->getValue(), PDO::PARAM_LOB), $value instanceof ExpressionInterface => $value, - $value instanceof BackedEnum => (string) $value->value, - $value instanceof Stringable => (string) $value, + $value instanceof Stringable => new Param((string) $value, PDO::PARAM_LOB), + $value instanceof BackedEnum => new Param((string) $value->value, PDO::PARAM_LOB), default => $this->throwWrongTypeException($value::class), }, default => $this->throwWrongTypeException(gettype($value)), }; } - public function phpTypecast(mixed $value): mixed + public function phpTypecast(mixed $value): StringableStream|string|null { - return $value; + /** @var string|StringableStream|null */ + return is_resource($value) ? new StringableStream($value) : $value; } } diff --git a/src/Schema/Data/StringableStream.php b/src/Schema/Data/StringableStream.php new file mode 100644 index 000000000..3391c720f --- /dev/null +++ b/src/Schema/Data/StringableStream.php @@ -0,0 +1,84 @@ +value = $value; + } + + /** + * Closes the resource. + */ + public function __destruct() + { + if (is_resource($this->value)) { + fclose($this->value); + } + } + + /** + * @return string[] Prepared values for serialization. + */ + public function __serialize(): array + { + return ['value' => $this->__toString()]; + } + + /** + * @return string The result of reading the resource stream. + */ + public function __toString(): string + { + /** + * @psalm-suppress PossiblyFalsePropertyAssignmentValue, PossiblyInvalidArgument + * @var string + */ + return match (gettype($this->value)) { + GettypeResult::RESOURCE => $this->value = stream_get_contents($this->value), + GettypeResult::RESOURCE_CLOSED => throw new LogicException('Resource is closed.'), + default => $this->value, + }; + } + + /** + * @return resource|string The resource stream or the result of reading the stream. + */ + public function getValue(): mixed + { + return $this->value; + } +} diff --git a/tests/Common/CommonCommandTest.php b/tests/Common/CommonCommandTest.php index dfcd0854b..f1f3ebd3c 100644 --- a/tests/Common/CommonCommandTest.php +++ b/tests/Common/CommonCommandTest.php @@ -480,7 +480,7 @@ public function testCreateTable(): void $nameCol = $schema->getTableSchema('{{testCreateTable}}', true)->getColumn('name'); - $this->assertFalse($nameCol->isAllowNull()); + $this->assertTrue($nameCol->isNotNull()); $this->assertEquals([['id' => 1, 'bar' => 1, 'name' => 'Lilo']], $records); $db->close(); diff --git a/tests/Db/Schema/Data/ResourceStreamTest.php b/tests/Db/Schema/Data/ResourceStreamTest.php new file mode 100644 index 000000000..f9e20a53b --- /dev/null +++ b/tests/Db/Schema/Data/ResourceStreamTest.php @@ -0,0 +1,85 @@ +assertSame($resource, $stringableSteam->getValue()); + } + + public function testDestruct(): void + { + $resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb'); + $stringableSteam = new StringableStream($resource); + + $this->assertSame(GettypeResult::RESOURCE, gettype($resource)); + + unset($stringableSteam); + + $this->assertSame(GettypeResult::RESOURCE_CLOSED, gettype($resource)); + } + + public function testSerialize(): void + { + $resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb'); + $stringableSteam = new StringableStream($resource); + $serialized = serialize($stringableSteam); + + $this->assertSame('O:39:"Yiisoft\Db\Schema\Data\StringableStream":1:{s:5:"value";s:6:"string";}', $serialized); + $this->assertEquals($stringableSteam, unserialize($serialized)); + } + + public function testToString(): void + { + $resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb'); + $stringableSteam = new StringableStream($resource); + + // Can be read twice and more + $this->assertSame('string', (string) $stringableSteam); + $this->assertSame('string', (string) $stringableSteam); + } + + public function testToStringClosedResource(): void + { + $resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb'); + $stringableSteam = new StringableStream($resource); + + fclose($resource); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Resource is closed.'); + + (string) $stringableSteam; + } + + public function testGetValue(): void + { + $resource = fopen(__DIR__ . '/../../../Support/string.txt', 'rb'); + $stringableSteam = new StringableStream($resource); + + $this->assertSame($resource, $stringableSteam->getValue()); + $this->assertSame('string', (string) $stringableSteam); + $this->assertSame('string', $stringableSteam->getValue()); + } +} diff --git a/tests/Provider/ColumnProvider.php b/tests/Provider/ColumnProvider.php index 46b19b2e7..7469e2531 100644 --- a/tests/Provider/ColumnProvider.php +++ b/tests/Provider/ColumnProvider.php @@ -34,11 +34,13 @@ use Yiisoft\Db\Schema\Column\StructuredLazyColumn; use Yiisoft\Db\Schema\Data\LazyArray; use Yiisoft\Db\Schema\Data\JsonLazyArray; +use Yiisoft\Db\Schema\Data\StringableStream; use Yiisoft\Db\Schema\Data\StructuredLazyArray; use Yiisoft\Db\Tests\Support\IntEnum; use Yiisoft\Db\Tests\Support\Stringable; use Yiisoft\Db\Tests\Support\StringEnum; +use function fclose; use function fopen; class ColumnProvider @@ -147,10 +149,12 @@ public static function dbTypecastColumns(): array ['1', true], ['0', false], [new Param("\x10\x11\x12", PDO::PARAM_LOB), "\x10\x11\x12"], - ['1', IntEnum::ONE], - ['one', StringEnum::ONE], - ['string', new Stringable('string')], + [new Param('1', PDO::PARAM_LOB), IntEnum::ONE], + [new Param('one', PDO::PARAM_LOB), StringEnum::ONE], + [new Param('string', PDO::PARAM_LOB), new Stringable('string')], [$resource = fopen('php://memory', 'rb'), $resource], + [new Param($resource = fopen('php://memory', 'rb'), PDO::PARAM_LOB), new StringableStream($resource)], + [new Param("\x10\x11\x12", PDO::PARAM_LOB), new StringableStream("\x10\x11\x12")], [$expression = new Expression('expression'), $expression], ], ], @@ -473,6 +477,9 @@ public static function dbTypecastColumns(): array public static function dbTypecastColumnsWithException(): array { + $resource = fopen('php://memory', 'rb'); + fclose($resource); + return [ 'integer array' => [new IntegerColumn(), []], 'integer resource' => [new IntegerColumn(), fopen('php://memory', 'r')], @@ -485,6 +492,7 @@ public static function dbTypecastColumnsWithException(): array 'double stdClass' => [new DoubleColumn(), new stdClass()], 'string array' => [new StringColumn(), []], 'string stdClass' => [new StringColumn(), new stdClass()], + 'binary closed' => [new BinaryColumn(), $resource], 'binary array' => [new BinaryColumn(), []], 'binary stdClass' => [new BinaryColumn(), new stdClass()], 'datetime array' => [new DateTimeColumn(), []], @@ -539,7 +547,7 @@ public static function phpTypecastColumns(): array [null, null], ['', ''], ["\x10\x11\x12", "\x10\x11\x12"], - [$resource = fopen('php://memory', 'rb'), $resource], + [new StringableStream($resource = fopen('php://memory', 'rb')), $resource], ], ], 'bit' => [ diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index c8eb9237c..7297bce16 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -21,6 +21,7 @@ use Yiisoft\Db\QueryBuilder\Condition\Like; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; use Yiisoft\Db\Schema\Column\ColumnBuilder; +use Yiisoft\Db\Schema\Data\StringableStream; use Yiisoft\Db\Tests\Support\Assert; use Yiisoft\Db\Tests\Support\DbHelper; use Yiisoft\Db\Tests\Support\IntEnum; @@ -32,11 +33,6 @@ use function fopen; -/** - * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArgument - * @psalm-suppress PossiblyUndefinedArrayOffset - */ class QueryBuilderProvider { use TestTrait; @@ -1774,6 +1770,7 @@ public static function prepareValue(): array 'paramInteger' => ['1', new Param(1, DataType::INTEGER)], 'expression' => ['(1 + 2)', new Expression('(1 + 2)')], 'expression with params' => ['(1 + 2)', new Expression('(:a + :b)', [':a' => 1, 'b' => 2])], + 'ResourceStream' => ['0x737472696e67', new StringableStream(fopen(__DIR__ . '/../Support/string.txt', 'rb'))], 'Stringable' => ["'string'", new Stringable('string')], 'StringEnum' => ["'one'", StringEnum::ONE], 'IntEnum' => ['1', IntEnum::ONE], @@ -1799,9 +1796,9 @@ public static function buildValue(): array [':qp0' => new Param('string', DataType::STRING)], ], 'binary' => [ - $param = fopen(__DIR__ . '/../Support/string.txt', 'rb'), + $resource = fopen(__DIR__ . '/../Support/string.txt', 'rb'), ':qp0', - [':qp0' => new Param($param, DataType::LOB)], + [':qp0' => new Param($resource, DataType::LOB)], ], 'paramBinary' => [ $param = new Param('string', DataType::LOB), @@ -1827,6 +1824,11 @@ public static function buildValue(): array '(:a + :b)', [':a' => 1, 'b' => 2], ], + 'ResourceStream' => [ + new StringableStream($resource = fopen(__DIR__ . '/../Support/string.txt', 'rb')), + ':qp0', + [':qp0' => new Param($resource, DataType::LOB)], + ], 'Stringable' => [ new Stringable('string'), ':qp0',