Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Composite types #303

Merged
merged 40 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9a25d99
Support Composite types
Tigrov Jul 26, 2023
97863dc
Update comment and test
Tigrov Jul 26, 2023
913ea1f
Fix styleci issues
Tigrov Jul 26, 2023
8d88a2a
Fix psalm issues
Tigrov Jul 26, 2023
17beaef
Fill skipped items for indexed `$value`
Tigrov Jul 27, 2023
7e24fd4
Add CompositeExpressionInterface
Tigrov Jul 27, 2023
d2975b5
Add tests and fixes
Tigrov Jul 28, 2023
42ff006
Remove CompositeExpressionInterface
Tigrov Jul 28, 2023
6cc2920
Remove TODO retrieve columns from schema
Tigrov Jul 28, 2023
3f6c680
Move to folder `Composite`
Tigrov Jul 28, 2023
47c0622
Apply fixes from StyleCI
Tigrov Jul 28, 2023
d0c21d3
Add tests and fixes
Tigrov Jul 28, 2023
fd67929
Merge branch 'yiisoft:master' into support_composite_types
Tigrov Jul 29, 2023
a455ab8
Remove check of `CompositeExpression` type
Tigrov Jul 28, 2023
65a388b
Refactor
Tigrov Jul 29, 2023
32645ec
Apply fixes from StyleCI
Tigrov Jul 29, 2023
09be4f6
Update tests and comments
Tigrov Jul 29, 2023
ae6f41c
Update comments
Tigrov Jul 29, 2023
d5f295e
Fix typo in `$compositeParser`
Tigrov Jul 30, 2023
4119262
Refactor ColumnSchema.php (#302)
Tigrov Jul 29, 2023
7ef91fd
Revert "Refactor ColumnSchema.php (#302)"
Tigrov Jul 30, 2023
b4bde58
Merge remote-tracking branch 'origin/master' into support_composite_t…
Tigrov Jul 30, 2023
ec137b7
Improve initialization of ColumnSchema type
Tigrov Aug 1, 2023
0747938
Mark `CompositeParser` class as `final`
Tigrov Aug 1, 2023
c6ba36f
Merge branch 'master' into support_composite_types
Tigrov Sep 6, 2023
3a3f47b
Update according to #297
Tigrov Sep 6, 2023
0f872e3
Improve `dbTypecastArray()`
Tigrov Sep 6, 2023
1a06c70
Add test for excessive elements
Tigrov Sep 6, 2023
feb58a1
Add line to CHANGELOG.md
Tigrov Sep 6, 2023
edb4f17
Rename $fieldName to $compositeColumnName
Tigrov Sep 6, 2023
90e9dbf
Double array of null values
Tigrov Sep 6, 2023
12cd867
Update test
Tigrov Sep 6, 2023
f39f761
Merge branch 'master' into support_composite_types
Tigrov Nov 14, 2023
c06f2b1
Apply suggestions from code review
Tigrov Nov 14, 2023
f9cfa6a
Merge branch 'master' into support_composite_types
Tigrov Jan 8, 2024
da586d2
Psalm suppress `MixedAssignment`
Tigrov Jan 8, 2024
77fa5ab
Remove `null` type from `ColumnSchema::$columns`
Tigrov Jan 8, 2024
0f6c122
Update CHANGELOG.md, improve
Tigrov Jan 8, 2024
d85acfb
Merge branch 'master' into support_composite_types
Tigrov Jan 9, 2024
a5e1dbb
Remove `@psalm-var mixed` annotations
Tigrov Jan 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Bug #316, #6: Support table view constraints (@Tigrov)
- Enh #324: Change property `Schema::$typeMap` to constant `Schema::TYPE_MAP` (@Tigrov)
- Enh #303: Support composite types (@Tigrov)

## 1.2.0 November 12, 2023

Expand All @@ -26,7 +27,7 @@
- Enh #294: Refactoring of `Schema::normalizeDefaultValue()` method (@Tigrov)
- Bug #287: Fix `bit` type (@Tigrov)
- Bug #295: Fix multiline and single quote in default string value, add support for PostgreSQL 9.4 parentheses around negative numeric default values (@Tigrov)
- Bug #296: Prevent posible issues with array default values `('{one,two}'::text[])::varchar[]`, remove `ArrayParser::parseString()` (@Tigrov)
- Bug #296: Prevent possible issues with array default values `('{one,two}'::text[])::varchar[]`, remove `ArrayParser::parseString()` (@Tigrov)

## 1.0.0 April 12, 2023

Expand Down
3 changes: 3 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
</issueHandlers>
</psalm>
114 changes: 114 additions & 0 deletions src/Builder/CompositeExpressionBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Builder;

use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidArgumentException;
use Yiisoft\Db\Exception\InvalidConfigException;
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Pgsql\Composite\CompositeExpression;
use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;

use function implode;

/**
* Builds expressions for {@see CompositeExpression} for PostgreSQL Server.
*/
final class CompositeExpressionBuilder implements ExpressionBuilderInterface
{
public function __construct(private QueryBuilderInterface $queryBuilder)
{
}

/**
* The method builds the raw SQL from the expression that won't be additionally escaped or quoted.
*
* @param CompositeExpression $expression The expression build.
* @param array $params The binding parameters.
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
*
* @return string The raw SQL that won't be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = []): string
{
$value = $expression->getValue();

if (empty($value)) {
return 'NULL';
}

if ($value instanceof QueryInterface) {
[$sql, $params] = $this->queryBuilder->build($value, $params);
return "($sql)" . $this->getTypeHint($expression);
}

/** @psalm-var string[] $placeholders */
$placeholders = $this->buildPlaceholders($expression, $params);

if (empty($placeholders)) {
return 'NULL';
}

return 'ROW(' . implode(', ', $placeholders) . ')' . $this->getTypeHint($expression);
}

/**
* Builds a placeholder array out of $expression values.
*
* @param array $params The binding parameters.
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
*/
private function buildPlaceholders(CompositeExpression $expression, array &$params): array
{
$value = $expression->getNormalizedValue();

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

$placeholders = [];
$columns = $expression->getColumns();

/** @psalm-var int|string $columnName */
foreach ($value as $columnName => $item) {
if (isset($columns[$columnName])) {
$item = $columns[$columnName]->dbTypecast($item);
}

if ($item instanceof ExpressionInterface) {
$placeholders[] = $this->queryBuilder->buildExpression($item, $params);
} else {
$placeholders[] = $this->queryBuilder->bindParam($item, $params);
}
}

return $placeholders;
}

/**
* @return string The typecast expression based on {@see type}.
*/
private function getTypeHint(CompositeExpression $expression): string
{
$type = $expression->getType();

if ($type === null) {
return '';
}

return '::' . $type;
}
}
122 changes: 117 additions & 5 deletions src/ColumnSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Expression\JsonExpression;
use Yiisoft\Db\Pgsql\Composite\CompositeExpression;
use Yiisoft\Db\Pgsql\Composite\CompositeParser;
use Yiisoft\Db\Schema\AbstractColumnSchema;
use Yiisoft\Db\Schema\ColumnSchemaInterface;
use Yiisoft\Db\Schema\SchemaInterface;

use function array_walk_recursive;
Expand Down Expand Up @@ -58,6 +61,12 @@ final class ColumnSchema extends AbstractColumnSchema
*/
private string|null $sequenceName = null;

/**
* @var ColumnSchemaInterface[] Columns metadata of the composite type.
* @psalm-var array<string, ColumnSchemaInterface>
*/
private array $columns = [];

/**
* Converts the input value according to {@see type} and {@see dbType} for use in a db query.
*
Expand All @@ -69,14 +78,63 @@ final class ColumnSchema extends AbstractColumnSchema
*/
public function dbTypecast(mixed $value): mixed
{
if ($value === null || $value instanceof ExpressionInterface) {
return $value;
}

if ($this->dimension > 0) {
if ($value === null || $value instanceof ExpressionInterface) {
return $value;
}

if ($this->getType() === Schema::TYPE_COMPOSITE) {
$value = $this->dbTypecastArray($value, $this->dimension);
}

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

return $this->dbTypecastValue($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 [];
}

$items = [];

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

return $items;
}

/**
* Converts the input value for use in a db query.
*/
private function dbTypecastValue(mixed $value): mixed
{
if ($value === null || $value instanceof ExpressionInterface) {
return $value;
}

return match ($this->getType()) {
SchemaInterface::TYPE_JSON => new JsonExpression($value, $this->getDbType()),

Expand All @@ -88,6 +146,8 @@ public function dbTypecast(mixed $value): mixed
? str_pad(decbin($value), (int) $this->getSize(), '0', STR_PAD_LEFT)
: (string) $value,

Schema::TYPE_COMPOSITE => new CompositeExpression($value, $this->getDbType(), $this->columns),

default => $this->typecast($value),
};
}
Expand Down Expand Up @@ -115,7 +175,6 @@ public function phpTypecast(mixed $value): mixed
}

array_walk_recursive($value, function (mixed &$val) {
/** @psalm-var mixed $val */
$val = $this->phpTypecastValue($val);
});

Expand Down Expand Up @@ -144,10 +203,42 @@ private function phpTypecastValue(mixed $value): mixed
SchemaInterface::TYPE_JSON
=> json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR),

Schema::TYPE_COMPOSITE => $this->phpTypecastComposite($value),

default => parent::phpTypecast($value),
};
}

/**
* Converts the input value according to the composite type after retrieval from the database.
*/
private function phpTypecastComposite(mixed $value): array|null
{
if (is_string($value)) {
$value = (new CompositeParser())->parse($value);
}

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

$fields = [];
$columnNames = array_keys($this->columns);

/** @psalm-var int|string $columnName */
foreach ($value as $columnName => $item) {
$columnName = $columnNames[$columnName] ?? $columnName;

if (isset($this->columns[$columnName])) {
$item = $this->columns[$columnName]->phpTypecast($item);
}

$fields[$columnName] = $item;
}

return $fields;
}

/**
* Creates instance of ArrayParser.
*/
Expand Down Expand Up @@ -191,4 +282,25 @@ public function sequenceName(string|null $sequenceName): void
{
$this->sequenceName = $sequenceName;
}

/**
* Set columns of the composite type.
*
* @param ColumnSchemaInterface[] $columns The metadata of the composite type columns.
* @psalm-param array<string, ColumnSchemaInterface> $columns
*/
public function columns(array $columns): void
{
$this->columns = $columns;
}

/**
* Get the metadata of the composite type columns.
*
* @return ColumnSchemaInterface[]
*/
public function getColumns(): array
{
return $this->columns;
}
}