Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5b96d65
Range and MultiRange parser
Gerych1984 Aug 2, 2023
96868f9
Merge remote-tracking branch 'origin/master' into fork/Gerych1984/ran…
vjik Nov 10, 2025
72f9dd3
Merge remote-tracking branch 'origin/master' into fork/Gerych1984/ran…
vjik Nov 13, 2025
a979f65
range
vjik Nov 13, 2025
f4be518
avoid extra check in `phpTypecast()`
vjik Nov 14, 2025
5ecd609
Apply PHP CS Fixer and Rector changes (CI)
vjik Nov 14, 2025
17306ec
improve
vjik Nov 14, 2025
837f139
Merge remote-tracking branch 'Gerych1984/range-parser' into fork/Gery…
vjik Nov 14, 2025
39ccc9f
Don't set extra data for columns
vjik Nov 14, 2025
b3f8ff5
Introduce `RangeBoundColumnFactory`
vjik Nov 14, 2025
60da246
Add default values for `*RangeValue`
vjik Nov 14, 2025
229723a
Separated classes for each range type
vjik Nov 14, 2025
5ca6de2
fix
vjik Nov 14, 2025
4d7f787
replace `?float` to `int|float|null`
vjik Nov 14, 2025
a63012e
Update src/Column/AbstractRangeColumn.php
vjik Nov 14, 2025
1c7d231
Improve + Tests
vjik Nov 15, 2025
b421cb4
Apply PHP CS Fixer and Rector changes (CI)
vjik Nov 15, 2025
5b08a04
fix namespaces
vjik Nov 15, 2025
67d7930
improve tests
vjik Nov 15, 2025
2ef31f3
multirange
vjik Nov 15, 2025
77bf0e8
Apply PHP CS Fixer and Rector changes (CI)
vjik Nov 15, 2025
63b5d07
improve
vjik Nov 15, 2025
9c2ce00
improve
vjik Nov 15, 2025
4155e8c
improve
vjik Nov 15, 2025
0ec9cea
improve
vjik Nov 15, 2025
92b7540
Apply PHP CS Fixer and Rector changes (CI)
vjik Nov 15, 2025
e118367
fix
vjik Nov 15, 2025
eb5647e
Merge remote-tracking branch 'Gerych1984/range-parser' into range-parser
vjik Nov 15, 2025
01ea19d
improve
vjik Nov 15, 2025
d25a435
improve tests
vjik Nov 16, 2025
925d6a2
Apply PHP CS Fixer and Rector changes (CI)
vjik Nov 16, 2025
8cc3a4d
Fix multirange regexp + More tests
vjik Nov 17, 2025
3f6cf28
Update src/Column/Int8MultiRangeColumn.php
vjik Nov 17, 2025
f18ff80
Update src/Column/DateMultiRangeColumn.php
vjik Nov 17, 2025
0f5f575
Update src/Column/Int4MultiRangeColumn.php
vjik Nov 17, 2025
2573e2d
Update src/Column/NumMultiRangeColumn.php
vjik Nov 17, 2025
edfec08
Update src/Column/TsMultiRangeColumn.php
vjik Nov 17, 2025
9cc045a
Update src/Column/TsTzMultiRangeColumn.php
vjik Nov 17, 2025
634b603
fix
vjik Nov 17, 2025
4951364
improve
vjik Nov 17, 2025
4d60541
Update src/Column/AbstractMultiRangeColumn.php
vjik Nov 17, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
- Chg #447: Update expression namespaces according to changes in `yiisoft/db` package (@Tigrov)
- Bug #456: Fix typecasting bit columns' values with big size (@Tigrov)
- Chg #460: Throw exception on "unsigned" column usage (@vjik)
- New #307: Add range and multirange columns support (@vjik, @Gerych1984)

## 1.3.0 March 21, 2024

Expand Down
74 changes: 74 additions & 0 deletions src/Column/AbstractMultiRangeColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Pgsql\Expression\MultiRangeValue;
use Yiisoft\Db\Schema\Column\AbstractColumn;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function gettype;
use function is_array;
use function is_string;

abstract class AbstractMultiRangeColumn extends AbstractColumn
{
public function dbTypecast(mixed $value): mixed
{
if ($value === null
|| is_string($value)
|| $value instanceof ExpressionInterface) {
return $value;
}

if (!is_array($value)) {
$this->throwWrongTypeException(gettype($value));
}

$rangeColumn = $this->getRangeColumn();
$ranges = [];
foreach ($value as $rawRange) {
$range = $rangeColumn->dbTypecast($rawRange);
if (!is_string($range)
&& !$range instanceof ExpressionInterface) {
$this->throwWrongTypeException(gettype($rawRange));
}
$ranges[] = $range;
}

return new MultiRangeValue(...$ranges);
}

public function phpTypecast(mixed $value): mixed
{
/**
* @var string|null $value We expect `phpTypecast()` to only receive the value that the database returns, which
* in this case is `null` or a `string`. To avoid extra checks.
*/

if ($value === null) {
return null;
}

if ($value === '{}') {
return [];
}

if (preg_match('/^{([\[\(][^,]*,[^\)\]]*[\)\]])(?:,(?1))*}$/', $value) !== 1) {
throw new NotSupportedException('Unsupported multirange format');
}

preg_match_all('/[\[\(][^,]*,[^\)\]]*[\)\]]/', $value, $matches);

$rangeColumn = $this->getRangeColumn();
return array_map(
$rangeColumn->phpTypecast(...),
$matches[0],
);
}

abstract protected function getRangeColumn(): ColumnInterface;
}
101 changes: 101 additions & 0 deletions src/Column/AbstractRangeColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use InvalidArgumentException;
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Schema\Column\AbstractColumn;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function gettype;
use function is_array;
use function is_string;
use function sprintf;

abstract class AbstractRangeColumn extends AbstractColumn
{
public function dbTypecast(mixed $value): mixed
{
if ($value === null
|| is_string($value)
|| $value instanceof ExpressionInterface) {
return $value;
}

if (!is_array($value)) {
$this->throwWrongTypeException(gettype($value));
}

if (array_keys($value) !== [0, 1]) {
throw new InvalidArgumentException(
'Value of range should be an array with two elements: lower and upper bounds.',
);
}

[$lower, $upper] = $value;
$boundColumn = $this->getBoundColumn();

return '['
. $this->prepareBoundValue($boundColumn->dbTypecast($lower))
. ','
. $this->prepareBoundValue($boundColumn->dbTypecast($upper))
. ']';
}

public function phpTypecast(mixed $value): mixed
{
/**
* @var string|null $value We expect `phpTypecast()` to only receive the value that the database returns, which
* in this case is `null` or a `string`. To avoid extra checks.
*/

if ($value === null || $value === 'empty') {
return null;
}

if (!preg_match('/^(?P<open>\[|\()(?P<lower>[^,]*),(?P<upper>[^\)\]]*)(?P<close>\)|\])$/', $value, $matches)) {
throw new NotSupportedException('Unsupported range format.');
}

return $this->createRangeValue(
$matches['lower'] === '' ? null : trim($matches['lower'], '"'),
$matches['upper'] === '' ? null : trim($matches['upper'], '"'),
$matches['open'] === '[',
$matches['close'] === ']',
);
}

abstract protected function getBoundColumn(): ColumnInterface;

/**
* @throws NotSupportedException
*/
abstract protected function createRangeValue(
?string $lower,
?string $upper,
bool $includeLower,
bool $includeUpper,
): ExpressionInterface;

/**
* @throws NotSupportedException
*/
private function prepareBoundValue(mixed $value): string
{
return match (gettype($value)) {

Check warning on line 88 in src/Column/AbstractRangeColumn.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "MatchArmRemoval": @@ @@ 'NULL' => '', 'double', 'integer' => (string) $value, 'string' => $value, - default => throw new NotSupportedException(sprintf('Range bound supports only string, int, float and null values. Got %s.', get_debug_type($value))), }; } }
'NULL' => '',
'double',
'integer' => (string) $value,
'string' => $value,
default => throw new NotSupportedException(
sprintf(
'Range bound supports only string, int, float and null values. Got %s.',
get_debug_type($value),
),
),
};
}
}
82 changes: 82 additions & 0 deletions src/Column/ColumnBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Schema\Column\ColumnInterface;
use Yiisoft\Db\Schema\Column\DateTimeColumn;
use Yiisoft\Db\Schema\Column\DoubleColumn;

final class ColumnBuilder extends \Yiisoft\Db\Schema\Column\ColumnBuilder
{
Expand All @@ -22,6 +24,11 @@
return new $className(ColumnType::BIT, size: $size);
}

public static function decimal(?int $size = 10, ?int $scale = 0): DoubleColumn
{
return new DoubleColumn(ColumnType::DECIMAL, scale: $scale, size: $size);
}

public static function tinyint(?int $size = null): IntegerColumn
{
return new IntegerColumn(ColumnType::TINYINT, size: $size);
Expand All @@ -39,7 +46,7 @@

public static function bigint(?int $size = null, bool $unsigned = false): BigIntColumn|IntegerColumn
{
return PHP_INT_SIZE === 4 || $unsigned

Check warning on line 49 in src/Column/ColumnBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "LogicalOr": @@ @@ } public static function bigint(?int $size = null, bool $unsigned = false): BigIntColumn|IntegerColumn { - return PHP_INT_SIZE === 4 || $unsigned ? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned) : new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned); + return PHP_INT_SIZE === 4 && $unsigned ? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned) : new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned); } public static function binary(?int $size = null): BinaryColumn {

Check warning on line 49 in src/Column/ColumnBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ } public static function bigint(?int $size = null, bool $unsigned = false): BigIntColumn|IntegerColumn { - return PHP_INT_SIZE === 4 || $unsigned ? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned) : new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned); + return PHP_INT_SIZE === 5 || $unsigned ? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned) : new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned); } public static function binary(?int $size = null): BinaryColumn {

Check warning on line 49 in src/Column/ColumnBuilder.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ } public static function bigint(?int $size = null, bool $unsigned = false): BigIntColumn|IntegerColumn { - return PHP_INT_SIZE === 4 || $unsigned ? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned) : new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned); + return PHP_INT_SIZE === 3 || $unsigned ? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned) : new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned); } public static function binary(?int $size = null): BinaryColumn {
? new BigIntColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned)
: new IntegerColumn(ColumnType::BIGINT, size: $size, unsigned: $unsigned);
}
Expand All @@ -58,4 +65,79 @@
{
return new StructuredColumn(ColumnType::STRUCTURED, dbType: $dbType, columns: $columns);
}

public static function date(): DateTimeColumn
{
return new DateTimeColumn(ColumnType::DATE);
}

public static function datetimeWithTimezone(?int $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::DATETIMETZ, size: $size);
}

public static function datetime(?int $size = 0): DateTimeColumn
{
return new DateTimeColumn(ColumnType::DATETIME, size: $size);
}

public static function int4Range(): Int4RangeColumn
{
return new Int4RangeColumn();
}

public static function int8Range(): Int8RangeColumn
{
return new Int8RangeColumn();
}

public static function numRange(): NumRangeColumn
{
return new NumRangeColumn();
}

public static function tsRange(): TsRangeColumn
{
return new TsRangeColumn();
}

public static function tsTzRange(): TsTzRangeColumn
{
return new TsTzRangeColumn();
}

public static function dateRange(): DateRangeColumn
{
return new DateRangeColumn();
}

public static function int4MultiRange(): Int4MultiRangeColumn
{
return new Int4MultiRangeColumn();
}

public static function int8MultiRange(): Int8MultiRangeColumn
{
return new Int8MultiRangeColumn();
}

public static function numMultiRange(): NumMultiRangeColumn
{
return new NumMultiRangeColumn();
}

public static function tsMultiRange(): TsMultiRangeColumn
{
return new TsMultiRangeColumn();
}

public static function tsTzMultiRange(): TsTzMultiRangeColumn
{
return new TsTzMultiRangeColumn();
}

public static function dateMultiRange(): DateMultiRangeColumn
{
return new DateMultiRangeColumn();
}
}
47 changes: 45 additions & 2 deletions src/Column/ColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType;
use Yiisoft\Db\Schema\Column\AbstractColumnFactory;
use Yiisoft\Db\Schema\Column\ColumnInterface;

Expand All @@ -23,8 +24,7 @@
*
* @link https://www.postgresql.org/docs/current/datatype.html#DATATYPE-TABLE
*
* @var string[]
* @psalm-var array<string, ColumnType::*>
* @inheritdoc
*/
protected const TYPE_MAP = [
'bool' => ColumnType::BOOLEAN,
Expand Down Expand Up @@ -91,6 +91,18 @@
'xml' => ColumnType::STRING,
'json' => ColumnType::JSON,
'jsonb' => ColumnType::JSON,
'int4range' => PgsqlColumnType::INT4RANGE,
'int8range' => PgsqlColumnType::INT8RANGE,
'numrange' => PgsqlColumnType::NUMRANGE,
'tsrange' => PgsqlColumnType::TSRANGE,
'tstzrange' => PgsqlColumnType::TSTZRANGE,
'daterange' => PgsqlColumnType::DATERANGE,
'int4multirange' => PgsqlColumnType::INT4MULTIRANGE,
'int8multirange' => PgsqlColumnType::INT8MULTIRANGE,
'nummultirange' => PgsqlColumnType::NUMMULTIRANGE,
'tsmultirange' => PgsqlColumnType::TSMULTIRANGE,
'tstzmultirange' => PgsqlColumnType::TSTZMULTIRANGE,
'datemultirange' => PgsqlColumnType::DATEMULTIRANGE,
];

public function fromType(string $type, array $info = []): ColumnInterface
Expand Down Expand Up @@ -129,17 +141,48 @@
ColumnType::BINARY => BinaryColumn::class,
ColumnType::ARRAY => ArrayColumn::class,
ColumnType::STRUCTURED => StructuredColumn::class,
PgsqlColumnType::INT4RANGE => Int4RangeColumn::class,
PgsqlColumnType::INT8RANGE => Int8RangeColumn::class,
PgsqlColumnType::NUMRANGE => NumRangeColumn::class,
PgsqlColumnType::TSRANGE => TsRangeColumn::class,
PgsqlColumnType::TSTZRANGE => TsTzRangeColumn::class,
PgsqlColumnType::DATERANGE => DateRangeColumn::class,
PgsqlColumnType::INT4MULTIRANGE => Int4MultiRangeColumn::class,
PgsqlColumnType::INT8MULTIRANGE => Int8MultiRangeColumn::class,
PgsqlColumnType::NUMMULTIRANGE => NumMultiRangeColumn::class,
PgsqlColumnType::TSMULTIRANGE => TsMultiRangeColumn::class,
PgsqlColumnType::TSTZMULTIRANGE => TsTzMultiRangeColumn::class,
PgsqlColumnType::DATEMULTIRANGE => DateMultiRangeColumn::class,
default => parent::getColumnClass($type, $info),
};
}

protected function isType(string $type): bool
{
return match ($type) {
PgsqlColumnType::INT4RANGE,
PgsqlColumnType::INT8RANGE,
PgsqlColumnType::NUMRANGE,
PgsqlColumnType::TSRANGE,
PgsqlColumnType::TSTZRANGE,
PgsqlColumnType::DATERANGE,
PgsqlColumnType::INT4MULTIRANGE,
PgsqlColumnType::INT8MULTIRANGE,
PgsqlColumnType::NUMMULTIRANGE,
PgsqlColumnType::TSMULTIRANGE,
PgsqlColumnType::TSTZMULTIRANGE,
PgsqlColumnType::DATEMULTIRANGE => true,
default => parent::isType($type),
};
}

protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed
{
/** @var string $value */
$value = preg_replace("/::[^:']+$/", '$1', $defaultValue);

if (str_starts_with($value, "B'") && $value[-1] === "'") {
return $column->phpTypecast(substr($value, 2, -1));

Check warning on line 185 in src/Column/ColumnFactory.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "UnwrapSubstr": @@ @@ /** @var string $value */ $value = preg_replace("/::[^:']+\$/", '$1', $defaultValue); if (str_starts_with($value, "B'") && $value[-1] === "'") { - return $column->phpTypecast(substr($value, 2, -1)); + return $column->phpTypecast($value); } $value = parent::normalizeNotNullDefaultValue($value, $column); if ($value instanceof Expression) {
}

$value = parent::normalizeNotNullDefaultValue($value, $column);
Expand Down
18 changes: 18 additions & 0 deletions src/Column/DateMultiRangeColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Column;

use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType;
use Yiisoft\Db\Schema\Column\ColumnInterface;

final class DateMultiRangeColumn extends AbstractMultiRangeColumn
{
Comment thread
vjik marked this conversation as resolved.
protected const DEFAULT_TYPE = PgsqlColumnType::DATEMULTIRANGE;

protected function getRangeColumn(): ColumnInterface
{
return new DateRangeColumn();
}
}
Loading
Loading