Skip to content

Commit

Permalink
ColumnSchema classes for performance of typecasting (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored May 30, 2024
1 parent f2e0dd9 commit 85c27c5
Show file tree
Hide file tree
Showing 19 changed files with 1,058 additions and 399 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 2.0.0 under development

- Enh #336: Implement `SqlParser` and `ExpressionBuilder` driver classes (@Tigrov)
- Enh #315: Implement `ColumnSchemaInterface` classes according to the data type of database table columns
for type casting performance. Related with yiisoft/db#752 (@Tigrov)
- Chg #348: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov)

## 1.3.0 March 21, 2024
Expand Down
181 changes: 181 additions & 0 deletions src/Column/ArrayColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Traversable;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Pgsql\ArrayParser;
use Yiisoft\Db\Pgsql\Schema;
use Yiisoft\Db\Schema\Column\AbstractColumnSchema;
use Yiisoft\Db\Schema\Column\ColumnSchemaInterface;
use Yiisoft\Db\Schema\Column\DoubleColumnSchema;
use Yiisoft\Db\Schema\Column\JsonColumnSchema;
use Yiisoft\Db\Schema\Column\StringColumnSchema;
use Yiisoft\Db\Schema\SchemaInterface;

use function array_map;
use function array_walk_recursive;
use function is_array;
use function is_iterable;
use function is_string;
use function iterator_to_array;

final class ArrayColumnSchema extends AbstractColumnSchema
{
/**
* @var ColumnSchemaInterface|null The column of an array item.
*/
private ColumnSchemaInterface|null $column = null;

/**
* @var int The dimension of array, must be greater than 0.
*/
private int $dimension = 1;

public function __construct(
string $type = Schema::TYPE_ARRAY,
string|null $phpType = SchemaInterface::PHP_TYPE_ARRAY,
) {
parent::__construct($type, $phpType);
}

/**
* Set column of an array item.
*/
public function column(ColumnSchemaInterface|null $column): void
{
$this->column = $column;
}

/**
* @return ColumnSchemaInterface the column of an array item.
*/
public function getColumn(): ColumnSchemaInterface
{
if ($this->column === null) {
$type = $this->getType();
$phpType = $this->getPhpType();

$this->column = match ($type) {
Schema::TYPE_BIT => new BitColumnSchema($type, $phpType),
Schema::TYPE_STRUCTURED => new StructuredColumnSchema($type, $phpType),
SchemaInterface::TYPE_BIGINT => PHP_INT_SIZE !== 8
? new BigIntColumnSchema($type, $phpType)
: new IntegerColumnSchema($type, $phpType),
default => match ($phpType) {
SchemaInterface::PHP_TYPE_INTEGER => new IntegerColumnSchema($type, $phpType),
SchemaInterface::PHP_TYPE_DOUBLE => new DoubleColumnSchema($type, $phpType),
SchemaInterface::PHP_TYPE_BOOLEAN => new BooleanColumnSchema($type, $phpType),
SchemaInterface::PHP_TYPE_RESOURCE => new BinaryColumnSchema($type, $phpType),
SchemaInterface::PHP_TYPE_ARRAY => new JsonColumnSchema($type, $phpType),
default => new StringColumnSchema($type, $phpType),
},
};

$this->column->dbType($this->getDbType());
$this->column->enumValues($this->getEnumValues());
$this->column->precision($this->getPrecision());
$this->column->scale($this->getScale());
$this->column->size($this->getSize());
}

return $this->column;
}

/**
* Set dimension of an array, must be greater than 0.
*/
public function dimension(int $dimension): void
{
$this->dimension = $dimension;
}

/**
* @return int the dimension of the array.
*/
public function getDimension(): int
{
return $this->dimension;
}

public function dbTypecast(mixed $value): ExpressionInterface|null
{
if ($value === null || $value instanceof ExpressionInterface) {
return $value;
}

if ($this->dimension === 1 && is_array($value)) {
$value = array_map([$this->getColumn(), 'dbTypecast'], $value);
} else {
$value = $this->dbTypecastArray($value, $this->dimension);
}

return new ArrayExpression($value, $this->getDbType(), $this->dimension);
}

public function phpTypecast(mixed $value): array|null
{
if (is_string($value)) {
$value = (new ArrayParser())->parse($value);
}

if (!is_array($value)) {
return null;
}

if ($this->getType() === SchemaInterface::TYPE_STRING) {
return $value;
}

$column = $this->getColumn();

if ($this->dimension === 1 && $column->getType() !== SchemaInterface::TYPE_JSON) {
return array_map([$column, 'phpTypecast'], $value);
}

array_walk_recursive($value, function (string|null &$val) use ($column): void {
$val = $column->phpTypecast($val);
});

return $value;
}

/**
* Recursively converts array values for use in a db query.
*
* @param mixed $value The array or iterable object.
* @param int $dimension The array dimension. Should be more than 0.
*
* @return array|null Converted values.
*/
private function dbTypecastArray(mixed $value, int $dimension): array|null
{
if ($value === null) {
return null;
}

if (!is_iterable($value)) {
return [];
}

if ($dimension <= 1) {
return array_map(
[$this->getColumn(), 'dbTypecast'],
$value instanceof Traversable
? iterator_to_array($value, false)
: $value
);
}

$items = [];

foreach ($value as $val) {
$items[] = $this->dbTypecastArray($val, $dimension - 1);
}

return $items;
}
}
12 changes: 12 additions & 0 deletions src/Column/BigIntColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Schema\Column\BigIntColumnSchema as BaseBigIntColumnSchema;

final class BigIntColumnSchema extends BaseBigIntColumnSchema implements SequenceColumnSchemaInterface
{
use SequenceColumnSchemaTrait;
}
24 changes: 24 additions & 0 deletions src/Column/BinaryColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Schema\Column\BinaryColumnSchema as BaseBinaryColumnSchema;

use function hex2bin;
use function is_string;
use function str_starts_with;
use function substr;

final class BinaryColumnSchema extends BaseBinaryColumnSchema
{
public function phpTypecast(mixed $value): mixed
{
if (is_string($value) && str_starts_with($value, '\x')) {
return hex2bin(substr($value, 2));
}

return $value;
}
}
48 changes: 48 additions & 0 deletions src/Column/BitColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Pgsql\Schema;
use Yiisoft\Db\Schema\Column\AbstractColumnSchema;
use Yiisoft\Db\Schema\SchemaInterface;

use function bindec;
use function decbin;
use function gettype;
use function str_pad;

final class BitColumnSchema extends AbstractColumnSchema
{
public function __construct(
string $type = Schema::TYPE_BIT,
string|null $phpType = SchemaInterface::PHP_TYPE_INTEGER,
) {
parent::__construct($type, $phpType);
}

/** @psalm-suppress RedundantCast */
public function dbTypecast(mixed $value): string|ExpressionInterface|null
{
return match (gettype($value)) {
'integer', 'double' => str_pad(decbin((int) $value), (int) $this->getSize(), '0', STR_PAD_LEFT),
'NULL' => null,
'boolean' => $value ? '1' : '0',
'string' => $value === '' ? null : $value,
default => $value instanceof ExpressionInterface ? $value : (string) $value,
};
}

public function phpTypecast(mixed $value): int|null
{
/** @var int|string|null $value */
if (is_string($value)) {
/** @var int */
return bindec($value);
}

return $value;
}
}
19 changes: 19 additions & 0 deletions src/Column/BooleanColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Schema\Column\BooleanColumnSchema as BaseBooleanColumnSchema;

final class BooleanColumnSchema extends BaseBooleanColumnSchema
{
public function phpTypecast(mixed $value): bool|null
{
if ($value === null) {
return null;
}

return $value && $value !== 'f';
}
}
12 changes: 12 additions & 0 deletions src/Column/IntegerColumnSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Schema\Column\IntegerColumnSchema as BaseIntegerColumnSchema;

final class IntegerColumnSchema extends BaseIntegerColumnSchema implements SequenceColumnSchemaInterface
{
use SequenceColumnSchemaTrait;
}
20 changes: 20 additions & 0 deletions src/Column/SequenceColumnSchemaInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Schema\Column\ColumnSchemaInterface;

interface SequenceColumnSchemaInterface extends ColumnSchemaInterface
{
/**
* @return string|null name of an associated sequence if column is auto incremental.
*/
public function getSequenceName(): string|null;

/**
* Set the name of an associated sequence if a column is auto incremental.
*/
public function sequenceName(string|null $sequenceName): void;
}
23 changes: 23 additions & 0 deletions src/Column/SequenceColumnSchemaTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

trait SequenceColumnSchemaTrait
{
/**
* @var string|null Name of an associated sequence if column is auto incremental.
*/
private string|null $sequenceName = null;

public function getSequenceName(): string|null
{
return $this->sequenceName;
}

public function sequenceName(string|null $sequenceName = null): void
{
$this->sequenceName = $sequenceName;
}
}
Loading

0 comments on commit 85c27c5

Please sign in to comment.