Skip to content

Commit

Permalink
Range and MultiRange parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerych1984 committed Aug 7, 2023
1 parent adcb2ee commit 39ec0a4
Show file tree
Hide file tree
Showing 10 changed files with 1,283 additions and 4 deletions.
33 changes: 32 additions & 1 deletion src/ColumnSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Yiisoft\Db\Expression\JsonExpression;
use Yiisoft\Db\Schema\AbstractColumnSchema;
use Yiisoft\Db\Schema\SchemaInterface;

use function array_walk_recursive;
use function bindec;
use function decbin;
Expand Down Expand Up @@ -105,6 +104,14 @@ public function dbTypecast(mixed $value): mixed
*/
public function phpTypecast(mixed $value): mixed
{
if (is_string($value) && $rangeParser = $this->getRangeParser()) {
return $rangeParser->parse($value);
}

if (is_string($value) && $multiRangeParser = $this->getMultiRangeParser()) {
return $multiRangeParser->parse($value);
}

if ($this->dimension > 0) {
if (is_string($value)) {
$value = $this->getArrayParser()->parse($value);
Expand Down Expand Up @@ -156,6 +163,30 @@ private function getArrayParser(): ArrayParser
return new ArrayParser();
}

/**
* @psalm-suppress PossiblyNullArgument
*/
private function getRangeParser(): ?RangeParser
{
if ($this->getDbType() !== null && RangeParser::isAllowedType($this->getDbType())) {
return new RangeParser($this->getDbType());
}

return null;
}

/**
* @psalm-suppress PossiblyNullArgument
*/
private function getMultiRangeParser(): ?MultiRangeParser
{
if ($this->getDbType() !== null && MultiRangeParser::isAllowedType($this->getDbType())) {
return new MultiRangeParser($this->getDbType());
}

return null;
}

/**
* @return int Get the dimension of the array.
*
Expand Down
96 changes: 96 additions & 0 deletions src/MultiRangeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql;

use InvalidArgumentException;
use function array_map;
use function preg_match_all;

final class MultiRangeParser
{
private const RANGES = [
Schema::TYPE_INT_MULTIRANGE => Schema::TYPE_INT_RANGE,
Schema::TYPE_BIGINT_MULTIRANGE => Schema::TYPE_BIGINT_RANGE,
Schema::TYPE_NUM_MULTIRANGE => Schema::TYPE_NUM_RANGE,
Schema::TYPE_DATE_MULTIRANGE => Schema::TYPE_DATE_RANGE,
Schema::TYPE_TS_MULTIRANGE => Schema::TYPE_TS_RANGE,
Schema::TYPE_TS_TZ_MULTIRANGE => Schema::TYPE_TS_TZ_RANGE,
];

private ?string $type = null;

public function __construct(?string $type = null)
{
$this->type = $type;
}

public function withType(?string $type): self
{
$new = clone $this;
$new->type = $type;

return $new;
}

public function asMultiInt(): self
{
return $this->withType(Schema::TYPE_INT_MULTIRANGE);
}

public function asMultiBigInt(): self
{
return $this->withType(Schema::TYPE_BIGINT_MULTIRANGE);
}

public function asMultiNumeric(): self
{
return $this->withType(Schema::TYPE_NUM_MULTIRANGE);
}

public function asMultiDate(): self
{
return $this->withType(Schema::TYPE_DATE_MULTIRANGE);
}

public function asMultiTimestamp(): self
{
return $this->withType(Schema::TYPE_TS_MULTIRANGE);
}

public function asMultiTimestampTz(): self
{
return $this->withType(Schema::TYPE_TS_TZ_MULTIRANGE);
}

public function asCustom(): self
{
return $this->withType(null);
}

public function parse(?string $value): ?array
{
if ($value === null) {
return null;
}

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

if (!preg_match_all('/(([\[\(][^,]*,[^\)\]]*[\)\]]),?)+/U', $value, $matches) || $value !== '{' . implode(',', $matches[1]) . '}') {
throw new InvalidArgumentException('Unsupported range format');
}

$type = self::RANGES[$this->type] ?? $this->type;
$parser = new RangeParser($type);

return array_map([$parser, 'parse'], $matches[1]);
}

public static function isAllowedType(string $type): bool
{
return isset(self::RANGES[$type]);
}
}
185 changes: 185 additions & 0 deletions src/RangeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql;

use DateInterval;
use DateTime;
use InvalidArgumentException;
use function preg_match;

final class RangeParser
{
private const RANGES = [
Schema::TYPE_INT_RANGE,
Schema::TYPE_BIGINT_RANGE,
Schema::TYPE_NUM_RANGE,
Schema::TYPE_TS_RANGE,
Schema::TYPE_TS_TZ_RANGE,
Schema::TYPE_DATE_RANGE,
];

private ?string $type;

public function __construct(?string $type = null)
{
$this->type = $type;
}

public function withType(?string $type): self
{
$new = clone $this;
$new->type = $type;

return $new;
}

public function asInt(): self
{
return $this->withType(Schema::TYPE_INT_RANGE);
}

public function asBigInt(): self
{
return $this->withType(Schema::TYPE_BIGINT_RANGE);
}

public function asNumeric(): self
{
return $this->withType(Schema::TYPE_NUM_RANGE);
}

public function asDate(): self
{
return $this->withType(Schema::TYPE_DATE_RANGE);
}

public function asTimestamp(): self
{
return $this->withType(Schema::TYPE_TS_RANGE);
}

public function asTimestampTz(): self
{
return $this->withType(Schema::TYPE_TS_TZ_RANGE);
}

public function asCustom(): self
{
return $this->withType(null);
}

public function parse(?string $value): ?array
{
if ($value === null || $value === 'empty') {
return null;
}

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

$lower = $matches['lower'] ? trim($matches['lower'], '"') : null;
$upper = $matches['upper'] ? trim($matches['upper'], '"') : null;
$includeLower = $matches['open'] === '[';
$includeUpper = $matches['close'] === ']';

if ($lower === null && $upper === null) {
return [null, null];
}

return match($this->type) {
Schema::TYPE_INT_RANGE => self::parseIntRange($lower, $upper, $includeLower, $includeUpper),
Schema::TYPE_BIGINT_RANGE => self::parseBigIntRange($lower, $upper, $includeLower, $includeUpper),
Schema::TYPE_NUM_RANGE => self::parseNumRange($lower, $upper),
Schema::TYPE_DATE_RANGE => self::parseDateRange($lower, $upper, $includeLower, $includeUpper),
Schema::TYPE_TS_RANGE => self::parseTsRange($lower, $upper),
Schema::TYPE_TS_TZ_RANGE => self::parseTsTzRange($lower, $upper),
default => [$lower, $upper]
};
}

private static function parseIntRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
{
$min = $lower === null ? null : (int) $lower;
$max = $upper === null ? null : (int) $upper;

if ($min !== null && $includeLower === false) {
$min += 1;
}

if ($max !== null && $includeUpper === false) {
$max -= 1;
}

return [$min, $max];
}

private static function parseBigIntRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
{
if (PHP_INT_SIZE === 8) {
return self::parseIntRange($lower, $upper, $includeLower, $includeUpper);
}

[$min, $max] = self::parseNumRange($lower, $upper);

if ($min !== null && $includeLower === false) {
/** @var float $min */
$min += 1;
}

if ($max !== null && $includeUpper === false) {
/** @var float $max */
$max -= 1;
}

return [$min, $max];
}

private static function parseNumRange(?string $lower, ?string $upper): array
{
$min = $lower === null ? null : (float) $lower;
$max = $upper === null ? null : (float) $upper;

return [$min, $max];
}

private static function parseDateRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
{
$interval = new DateInterval('P1D');
$min = $lower ? DateTime::createFromFormat('Y-m-d', $lower) : null;
$max = $upper ? DateTime::createFromFormat('Y-m-d', $upper) : null;

if ($min && $includeLower === false) {
$min->add($interval);
}

if ($max && $includeUpper === false) {
$max->sub($interval);
}

return [$min, $max];
}

private static function parseTsRange(?string $lower, ?string $upper): array
{
$min = $lower ? DateTime::createFromFormat('Y-m-d H:i:s', $lower) : null;
$max = $upper ? DateTime::createFromFormat('Y-m-d H:i:s', $upper) : null;

return [$min, $max];
}

private static function parseTsTzRange(?string $lower, ?string $upper): array
{
$min = $lower ? DateTime::createFromFormat('Y-m-d H:i:sP', $lower) : null;
$max = $upper ? DateTime::createFromFormat('Y-m-d H:i:sP', $upper) : null;

return [$min, $max];
}

public static function isAllowedType(string $type): bool
{
return in_array($type, self::RANGES, true);
}
}
18 changes: 17 additions & 1 deletion src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Yiisoft\Db\Schema\Builder\ColumnInterface;
use Yiisoft\Db\Schema\ColumnSchemaInterface;
use Yiisoft\Db\Schema\TableSchemaInterface;

use function array_merge;
use function array_unique;
use function array_values;
Expand Down Expand Up @@ -86,6 +85,23 @@ final class Schema extends AbstractPdoSchema
*/
public const TYPE_BIT = 'bit';

/**
* Define the abstract range columns types
* @see https://www.postgresql.org/docs/current/rangetypes.html
*/
public const TYPE_INT_RANGE = 'int4range';
public const TYPE_BIGINT_RANGE = 'int8range';
public const TYPE_NUM_RANGE = 'numrange';
public const TYPE_TS_RANGE = 'tsrange';
public const TYPE_TS_TZ_RANGE = 'tstzrange';
public const TYPE_DATE_RANGE = 'daterange';
public const TYPE_INT_MULTIRANGE = 'int4multirange';
public const TYPE_BIGINT_MULTIRANGE = 'int8multirange';
public const TYPE_NUM_MULTIRANGE = 'nummultirange';
public const TYPE_TS_MULTIRANGE = 'tsmultirange';
public const TYPE_TS_TZ_MULTIRANGE = 'tstzmultirange';
public const TYPE_DATE_MULTIRANGE = 'datemultirange';

/**
* @var array The mapping from physical column types (keys) to abstract column types (values).
*
Expand Down
Loading

0 comments on commit 39ec0a4

Please sign in to comment.