Skip to content

Commit 705193b

Browse files
committed
Range and MultiRange parser
1 parent adcb2ee commit 705193b

File tree

10 files changed

+1153
-4
lines changed

10 files changed

+1153
-4
lines changed

src/ColumnSchema.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Yiisoft\Db\Expression\JsonExpression;
1313
use Yiisoft\Db\Schema\AbstractColumnSchema;
1414
use Yiisoft\Db\Schema\SchemaInterface;
15-
1615
use function array_walk_recursive;
1716
use function bindec;
1817
use function decbin;
@@ -105,6 +104,14 @@ public function dbTypecast(mixed $value): mixed
105104
*/
106105
public function phpTypecast(mixed $value): mixed
107106
{
107+
if (is_string($value) && $rangeParser = $this->getRangeParser()) {
108+
return $rangeParser->parse($value);
109+
}
110+
111+
if (is_string($value) && $multiRangeParser = $this->getMultiRangeParser()) {
112+
return $multiRangeParser->parse($value);
113+
}
114+
108115
if ($this->dimension > 0) {
109116
if (is_string($value)) {
110117
$value = $this->getArrayParser()->parse($value);
@@ -156,6 +163,30 @@ private function getArrayParser(): ArrayParser
156163
return new ArrayParser();
157164
}
158165

166+
/**
167+
* @psalm-suppress PossiblyNullArgument
168+
*/
169+
private function getRangeParser(): ?RangeParser
170+
{
171+
if ($this->getDbType() !== null && RangeParser::isAllowedType($this->getDbType())) {
172+
return new RangeParser($this->getDbType());
173+
}
174+
175+
return null;
176+
}
177+
178+
/**
179+
* @psalm-suppress PossiblyNullArgument
180+
*/
181+
private function getMultiRangeParser(): ?MultiRangeParser
182+
{
183+
if ($this->getDbType() !== null && MultiRangeParser::isAllowedType($this->getDbType())) {
184+
return new MultiRangeParser($this->getDbType());
185+
}
186+
187+
return null;
188+
}
189+
159190
/**
160191
* @return int Get the dimension of the array.
161192
*

src/MultiRangeParser.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Pgsql;
6+
7+
use InvalidArgumentException;
8+
use function array_map;
9+
use function preg_match_all;
10+
11+
final class MultiRangeParser
12+
{
13+
private const RANGES = [
14+
Schema::TYPE_INT_MULTIRANGE => Schema::TYPE_INT_RANGE,
15+
Schema::TYPE_BIGINT_MULTIRANGE => Schema::TYPE_BIGINT_RANGE,
16+
Schema::TYPE_NUM_MULTIRANGE => Schema::TYPE_NUM_RANGE,
17+
Schema::TYPE_DATE_MULTIRANGE => Schema::TYPE_DATE_RANGE,
18+
Schema::TYPE_TS_MULTIRANGE => Schema::TYPE_TS_RANGE,
19+
Schema::TYPE_TS_TZ_MULTIRANGE => Schema::TYPE_TS_TZ_RANGE,
20+
];
21+
22+
private ?string $type = null;
23+
24+
public function __construct(?string $type = null)
25+
{
26+
$this->type = $type;
27+
}
28+
29+
public function withType(?string $type): self
30+
{
31+
$new = clone $this;
32+
$new->type = $type;
33+
34+
return $new;
35+
}
36+
37+
public function asMultiInt(): self
38+
{
39+
return $this->withType(Schema::TYPE_INT_MULTIRANGE);
40+
}
41+
42+
public function asMultiBigInt(): self
43+
{
44+
return $this->withType(Schema::TYPE_BIGINT_MULTIRANGE);
45+
}
46+
47+
public function asMultiNumeric(): self
48+
{
49+
return $this->withType(Schema::TYPE_NUM_MULTIRANGE);
50+
}
51+
52+
public function asMultiDate(): self
53+
{
54+
return $this->withType(Schema::TYPE_DATE_MULTIRANGE);
55+
}
56+
57+
public function asMultiTimestamp(): self
58+
{
59+
return $this->withType(Schema::TYPE_TS_MULTIRANGE);
60+
}
61+
62+
public function asMultiTimestampTz(): self
63+
{
64+
return $this->withType(Schema::TYPE_TS_TZ_MULTIRANGE);
65+
}
66+
67+
public function asCustom(): self
68+
{
69+
return $this->withType(null);
70+
}
71+
72+
public function parse(?string $value): ?array
73+
{
74+
if ($value === null) {
75+
return null;
76+
}
77+
78+
if ($value === '{}') {
79+
return [];
80+
}
81+
82+
if (!preg_match_all('/(([\[\(][^,]*,[^\)\]]*[\)\]]),?)+/U', $value, $matches) || $value !== '{' . implode(',', $matches[1]) . '}') {
83+
throw new InvalidArgumentException('Unsupported range format');
84+
}
85+
86+
$type = self::RANGES[$this->type] ?? $this->type;
87+
$parser = new RangeParser($type);
88+
89+
return array_map([$parser, 'parse'], $matches[1]);
90+
}
91+
92+
public static function isAllowedType(string $type): bool
93+
{
94+
return isset(self::RANGES[$type]);
95+
}
96+
}

src/RangeParser.php

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Pgsql;
6+
7+
use DateInterval;
8+
use DateTime;
9+
use InvalidArgumentException;
10+
use function preg_match;
11+
12+
final class RangeParser
13+
{
14+
private const RANGES = [
15+
Schema::TYPE_INT_RANGE,
16+
Schema::TYPE_BIGINT_RANGE,
17+
Schema::TYPE_NUM_RANGE,
18+
Schema::TYPE_TS_RANGE,
19+
Schema::TYPE_TS_TZ_RANGE,
20+
Schema::TYPE_DATE_RANGE,
21+
];
22+
23+
private ?string $type;
24+
25+
public function __construct(?string $type = null)
26+
{
27+
$this->type = $type;
28+
}
29+
30+
public function withType(?string $type): self
31+
{
32+
$new = clone $this;
33+
$new->type = $type;
34+
35+
return $new;
36+
}
37+
38+
public function asInt(): self
39+
{
40+
return $this->withType(Schema::TYPE_INT_RANGE);
41+
}
42+
43+
public function asBigInt(): self
44+
{
45+
return $this->withType(Schema::TYPE_BIGINT_RANGE);
46+
}
47+
48+
public function asNumeric(): self
49+
{
50+
return $this->withType(Schema::TYPE_NUM_RANGE);
51+
}
52+
53+
public function asDate(): self
54+
{
55+
return $this->withType(Schema::TYPE_DATE_RANGE);
56+
}
57+
58+
public function asTimestamp(): self
59+
{
60+
return $this->withType(Schema::TYPE_TS_RANGE);
61+
}
62+
63+
public function asTimestampTz(): self
64+
{
65+
return $this->withType(Schema::TYPE_TS_TZ_RANGE);
66+
}
67+
68+
public function asCustom(): self
69+
{
70+
return $this->withType(null);
71+
}
72+
73+
public function parse(?string $value): ?array
74+
{
75+
if ($value === null || $value === 'empty') {
76+
return null;
77+
}
78+
79+
if (!preg_match('/^(?P<open>\[|\()(?P<lower>[^,]*),(?P<upper>[^\)\]]*)(?P<close>\)|\])$/', $value, $matches)) {
80+
throw new InvalidArgumentException('Unsupported range format');
81+
}
82+
83+
$lower = $matches['lower'] ? trim($matches['lower'], '"') : null;
84+
$upper = $matches['upper'] ? trim($matches['upper'], '"') : null;
85+
$includeLower = $matches['open'] === '[';
86+
$includeUpper = $matches['close'] === ']';
87+
88+
if ($lower === null && $upper === null) {
89+
return [null, null];
90+
}
91+
92+
return match($this->type) {
93+
Schema::TYPE_INT_RANGE => self::parseIntRange($lower, $upper, $includeLower, $includeUpper),
94+
Schema::TYPE_BIGINT_RANGE => self::parseBigIntRange($lower, $upper, $includeLower, $includeUpper),
95+
Schema::TYPE_NUM_RANGE => self::parseNumRange($lower, $upper),
96+
Schema::TYPE_DATE_RANGE => self::parseDateRange($lower, $upper, $includeLower, $includeUpper),
97+
Schema::TYPE_TS_RANGE => self::parseTsRange($lower, $upper),
98+
Schema::TYPE_TS_TZ_RANGE => self::parseTsTzRange($lower, $upper),
99+
default => [$lower, $upper]
100+
};
101+
}
102+
103+
private static function parseIntRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
104+
{
105+
$min = $lower === null ? null : (int) $lower;
106+
$max = $upper === null ? null : (int) $upper;
107+
108+
if ($min !== null && $includeLower === false) {
109+
$min += 1;
110+
}
111+
112+
if ($max !== null && $includeUpper === false) {
113+
$max -= 1;
114+
}
115+
116+
return [$min, $max];
117+
}
118+
119+
private static function parseBigIntRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
120+
{
121+
if (PHP_INT_SIZE === 8) {
122+
return self::parseIntRange($lower, $upper, $includeLower, $includeUpper);
123+
}
124+
125+
[$min, $max] = self::parseNumRange($lower, $upper);
126+
127+
if ($min !== null && $includeLower === false) {
128+
/** @var float $min */
129+
$min += 1;
130+
}
131+
132+
if ($max !== null && $includeUpper === false) {
133+
/** @var float $max */
134+
$max -= 1;
135+
}
136+
137+
return [$min, $max];
138+
}
139+
140+
private static function parseNumRange(?string $lower, ?string $upper): array
141+
{
142+
$min = $lower === null ? null : (float) $lower;
143+
$max = $upper === null ? null : (float) $upper;
144+
145+
return [$min, $max];
146+
}
147+
148+
private static function parseDateRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
149+
{
150+
$interval = new DateInterval('P1D');
151+
$min = $lower ? DateTime::createFromFormat('Y-m-d', $lower) : null;
152+
$max = $upper ? DateTime::createFromFormat('Y-m-d', $upper) : null;
153+
154+
if ($min && $includeLower === false) {
155+
$min->add($interval);
156+
}
157+
158+
if ($max && $includeUpper === false) {
159+
$max->sub($interval);
160+
}
161+
162+
return [$min, $max];
163+
}
164+
165+
private static function parseTsRange(?string $lower, ?string $upper): array
166+
{
167+
$min = $lower ? DateTime::createFromFormat('Y-m-d H:i:s', $lower) : null;
168+
$max = $upper ? DateTime::createFromFormat('Y-m-d H:i:s', $upper) : null;
169+
170+
return [$min, $max];
171+
}
172+
173+
private static function parseTsTzRange(?string $lower, ?string $upper): array
174+
{
175+
$min = $lower ? DateTime::createFromFormat('Y-m-d H:i:sP', $lower) : null;
176+
$max = $upper ? DateTime::createFromFormat('Y-m-d H:i:sP', $upper) : null;
177+
178+
return [$min, $max];
179+
}
180+
181+
public static function isAllowedType(string $type): bool
182+
{
183+
return in_array($type, self::RANGES, true);
184+
}
185+
}

src/Schema.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
use Yiisoft\Db\Schema\Builder\ColumnInterface;
2121
use Yiisoft\Db\Schema\ColumnSchemaInterface;
2222
use Yiisoft\Db\Schema\TableSchemaInterface;
23-
2423
use function array_merge;
2524
use function array_unique;
2625
use function array_values;
@@ -86,6 +85,23 @@ final class Schema extends AbstractPdoSchema
8685
*/
8786
public const TYPE_BIT = 'bit';
8887

88+
/**
89+
* Define the abstract range columns types
90+
* @see https://www.postgresql.org/docs/current/rangetypes.html
91+
*/
92+
public const TYPE_INT_RANGE = 'int4range';
93+
public const TYPE_BIGINT_RANGE = 'int8range';
94+
public const TYPE_NUM_RANGE = 'numrange';
95+
public const TYPE_TS_RANGE = 'tsrange';
96+
public const TYPE_TS_TZ_RANGE = 'tstzrange';
97+
public const TYPE_DATE_RANGE = 'daterange';
98+
public const TYPE_INT_MULTIRANGE = 'int4multirange';
99+
public const TYPE_BIGINT_MULTIRANGE = 'int8multirange';
100+
public const TYPE_NUM_MULTIRANGE = 'nummultirange';
101+
public const TYPE_TS_MULTIRANGE = 'tsmultirange';
102+
public const TYPE_TS_TZ_MULTIRANGE = 'tstzmultirange';
103+
public const TYPE_DATE_MULTIRANGE = 'datemultirange';
104+
89105
/**
90106
* @var array The mapping from physical column types (keys) to abstract column types (values).
91107
*

0 commit comments

Comments
 (0)