From 5b96d6504addcc591b9a1a240b38d0fdd9fcbead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Wed, 2 Aug 2023 21:31:56 +0300 Subject: [PATCH 01/37] Range and MultiRange parser --- src/ColumnSchema.php | 33 ++- src/MultiRangeParser.php | 96 +++++++ src/RangeParser.php | 185 +++++++++++++ src/Schema.php | 18 +- tests/ColumnSchemaTest.php | 46 ++- tests/MultiRangeParserTest.php | 389 ++++++++++++++++++++++++++ tests/Provider/SchemaProvider.php | 84 ++++++ tests/RangeParserTest.php | 446 ++++++++++++++++++++++++++++++ tests/Support/Fixture/pgsql.sql | 8 +- tests/Support/Fixture/pgsql14.sql | 9 + 10 files changed, 1310 insertions(+), 4 deletions(-) create mode 100644 src/MultiRangeParser.php create mode 100644 src/RangeParser.php create mode 100644 tests/MultiRangeParserTest.php create mode 100644 tests/RangeParserTest.php create mode 100644 tests/Support/Fixture/pgsql14.sql diff --git a/src/ColumnSchema.php b/src/ColumnSchema.php index db39d788b..33ed35177 100644 --- a/src/ColumnSchema.php +++ b/src/ColumnSchema.php @@ -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; @@ -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); @@ -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. * diff --git a/src/MultiRangeParser.php b/src/MultiRangeParser.php new file mode 100644 index 000000000..08624c4ad --- /dev/null +++ b/src/MultiRangeParser.php @@ -0,0 +1,96 @@ + 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]); + } +} diff --git a/src/RangeParser.php b/src/RangeParser.php new file mode 100644 index 000000000..4ce66a2b6 --- /dev/null +++ b/src/RangeParser.php @@ -0,0 +1,185 @@ +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\[|\()(?P[^,]*),(?P[^\)\]]*)(?P\)|\])$/', $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); + } +} diff --git a/src/Schema.php b/src/Schema.php index f40a3fa36..cf6cbb2d6 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -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; @@ -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). * diff --git a/tests/ColumnSchemaTest.php b/tests/ColumnSchemaTest.php index 2c8195acb..0f989547f 100644 --- a/tests/ColumnSchemaTest.php +++ b/tests/ColumnSchemaTest.php @@ -16,8 +16,10 @@ use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Schema\SchemaInterface; - +use function str_repeat; use function stream_get_contents; +use function version_compare; +use const PHP_INT_SIZE; /** * @group pgsql @@ -59,6 +61,9 @@ public function testPhpTypeCast(): void 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], 'jsonb_col' => new JsonExpression(new ArrayExpression([1, 2, 3])), 'jsonarray_col' => [new ArrayExpression([[',', 'null', true, 'false', 'f']], SchemaInterface::TYPE_JSON)], + 'intrange_col' => new Expression("'[3,7)'::int4range"), + 'bigintrange_col' => new Expression("'[2147483648,2147483649]'::int8range"), + 'numrange_col' => new Expression("'(1.1,1.5)'::numrange"), ] ); $command->execute(); @@ -81,6 +86,9 @@ public function testPhpTypeCast(): void $jsonColPhpType = $tableSchema->getColumn('json_col')?->phpTypecast($query['json_col']); $jsonBColPhpType = $tableSchema->getColumn('jsonb_col')?->phpTypecast($query['jsonb_col']); $jsonArrayColPhpType = $tableSchema->getColumn('jsonarray_col')?->phpTypecast($query['jsonarray_col']); + $intRangePhpType = $tableSchema->getColumn('intrange_col')->phpTypecast($query['intrange_col']); + $bigIntRangephpType = $tableSchema->getColumn('bigintrange_col')->phpTypecast($query['bigintrange_col']); + $numRangePhpType = $tableSchema->getColumn('numrange_col')->phpTypecast($query['numrange_col']); $this->assertSame(1, $intColPhpTypeCast); $this->assertSame(str_repeat('x', 100), $charColPhpTypeCast); @@ -97,10 +105,46 @@ public function testPhpTypeCast(): void $this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $jsonColPhpType); $this->assertSame(['1', '2', '3'], $jsonBColPhpType); $this->assertSame([[[',', 'null', true, 'false', 'f']]], $jsonArrayColPhpType); + $this->assertSame([3, 6], $intRangePhpType); + $this->assertSame(PHP_INT_SIZE === 8 ? [2147483648, 2147483649] : [2147483648.0, 2147483649.0], $bigIntRangephpType); + $this->assertSame([1.1, 1.5], $numRangePhpType); $db->close(); } + public function testPhpTypeCaseMultiRange(): void + { + if (version_compare($this->getConnection()->getServerVersion(), '14.0', '<')) { + $this->markTestSkipped('PostgreSQL < 14.0 does not support multi range columns.'); + } + + $this->setFixture('pgsql14.sql'); + $db = $this->getConnection(true); + + $command = $db->createCommand(); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('table_with_multirange'); + $command->insert( + 'table_with_multirange', + [ + 'int4multirange_col' => new Expression("'{[3,7), [8,9)}'::int4multirange"), + 'int8multirange_col' => new Expression("'{[2147483648,2147483649]}'::int8multirange"), + 'nummultirange_col' => new Expression("'{[10.5,15.2],(20.7,21),(38.1,39.3]}'::nummultirange"), + 'datemultirange_col' => new Expression("'{[2020-12-01,2021-01-01],[2020-12-02,2021-01-03)}'::datemultirange"), + ] + ); + $command->execute(); + $query = (new Query($db))->from('table_with_multirange')->one(); + + $this->assertNotNull($tableSchema); + + $intColPhpTypeCast = $tableSchema->getColumn('int4multirange_col')?->phpTypecast($query['int4multirange_col']); + $floatColPhpTypeCast = $tableSchema->getColumn('nummultirange_col')?->phpTypecast($query['nummultirange_col']); + + $this->assertSame([[3,6], [8,8]], $intColPhpTypeCast); + $this->assertSame([[10.5, 15.2], [20.7, 21.0], [38.1, 39.3]], $floatColPhpTypeCast); + } + /** * @throws JsonException */ diff --git a/tests/MultiRangeParserTest.php b/tests/MultiRangeParserTest.php new file mode 100644 index 000000000..fa98d179a --- /dev/null +++ b/tests/MultiRangeParserTest.php @@ -0,0 +1,389 @@ +parse($value); + + $this->assertSame($expected, $result); + } + + public static function intRangeDataProvider(): array + { + return [ + [ + [[3, 6]], + '{[3,7)}', + ], + + [ + [[3,6], [8,8]], + '{[3,7),[8,9)}' + ], + ]; + } + + /** + * @dataProvider intRangeDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testIntMultiRange(array $expected, string $value): void + { + $parser = new MultiRangeParser(); + $result = $parser->asMultiInt()->parse($value); + + $this->assertSame($expected, $result); + } + + public static function dateRangeDataProvider(): array + { + return [ + [ + [ + [ + new DateTime('2020-12-01'), + new DateTime('2021-01-01'), + ], + + [ + new DateTime('2020-12-01'), + new DateTime('2021-01-02'), + ], + + [ + new DateTime('2021-01-01'), + new DateTime('2021-01-02'), + ], + + [ + null, + new DateTime('2021-01-02'), + ], + + [ + new DateTime('2020-12-01'), + null, + ], + ], + '{[2020-12-01,2021-01-01],[2020-12-01,2021-01-03),(2020-12-31,2021-01-03),(,2021-01-03),[2020-12-01,)}', + ], + ]; + } + + /** + * @dataProvider dateRangeDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testDateMultiRange(array $expected, string $value): void + { + $parser = new MultiRangeParser(); + $result = $parser->asMultiDate()->parse($value); + + $this->assertCount(count($expected), $result); + + foreach ($expected as $i => $date) { + $this->assertCount(2, $result[$i]); + + $lower = $date[0]; + $upper = $date[1]; + $min = $result[$i][0]; + $max = $result[$i][1]; + + if ($lower === null) { + $this->assertNull($min); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $min); + $this->assertSame($lower->format('Y-m-d'), $min->format('Y-m-d')); + } + + if ($upper === null) { + $this->assertNull($max); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $max); + $this->assertSame($upper->format('Y-m-d'), $max->format('Y-m-d')); + } + } + } + + public static function numRangeDataProvider(): array + { + return [ + [ + [ + [10.5, 20.7], + [null, 39.3], + [11.2, null], + [null, null], + ], + '{[10.5,20.7],(,39.3],(11.2,),[,]}', + ], + ]; + } + + /** + * @dataProvider numRangeDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testNumMultiRange(array $expected, string $value): void + { + $parser = new MultiRangeParser(); + $result = $parser->asMultiNumeric()->parse($value); + + $this->assertSame($expected, $result); + } + + public static function bigIntDataProvider(): array + { + return [ + [ + [ + [ + PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', + PHP_INT_SIZE === 8 ? (int) '2147483649' : (float) '2147483649', + ], + [ + null, + PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', + ], + ], + '{[2147483648,2147483649],[,2147483649)}', + ], + ]; + } + + /** + * @dataProvider bigIntDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testBigIntMultiRange(array $expected, string $value): void + { + $parser = new MultiRangeParser(); + $result = $parser->asMultiBigInt()->parse($value); + + $this->assertSame($expected, $result); + } + + public static function timestampDataProvider(): array + { + return [ + [ + [ + [ + new DateTime('2023-08-07 13:00:00'), + new DateTime('2023-08-07 13:30:00'), + ], + [ + null, + new DateTime('2023-08-07 13:30:00'), + ], + [ + new DateTime('2023-08-07 13:00:00'), + null, + ], + ], + + '{[2023-08-07 13:00:00,2023-08-07 13:30:00],[,2023-08-07 13:30:00],[2023-08-07 13:00:00,]}' + ], + ]; + } + + /** + * @dataProvider timestampDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testTimestampMultiRange(array $expected, string $value): void + { + $parser = new MultiRangeParser(); + $result = $parser->asMultiTimestamp()->parse($value); + + $this->assertCount(count($expected), $result); + + foreach ($expected as $i => $date) { + + $res = $result[$i]; + + $this->assertCount(count($date), $res); + + $lower = $res[0]; + $upper = $res[1]; + $min = $date[0]; + $max = $date[1]; + + if ($min === null) { + $this->assertNull($lower); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $lower); + $this->assertSame($min->format('U'), $lower->format('U')); + } + + if ($max === null) { + $this->assertNull($upper); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $upper); + $this->assertSame($max->format('U'), $upper->format('U')); + } + } + } + + public static function timestampTzDataProvider(): array + { + return [ + [ + [ + [ + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), + ], + + [ + null, + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), + ], + + [ + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), + null, + ], + ], + + '{[2023-08-07 13:00:00+03,2023-08-07 13:30:00+03],[,2023-08-07 13:30:00+03],[2023-08-07 13:00:00+03,]}' + ], + ]; + } + + /** + * @dataProvider timestampTzDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testTimestampTzMultiRange(array $expected, string $value): void + { + $parser = new MultiRangeParser(); + $result = $parser->asMultiTimestampTz()->parse($value); + + $this->assertCount(count($expected), $result); + + foreach ($expected as $i => $date) { + + $res = $result[$i]; + + $this->assertCount(count($date), $res); + + $lower = $res[0]; + $upper = $res[1]; + $min = $date[0]; + $max = $date[1]; + + if ($min === null) { + $this->assertNull($lower); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $lower); + $this->assertSame($min->format('U'), $lower->format('U')); + } + + if ($max === null) { + $this->assertNull($upper); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $upper); + $this->assertSame($max->format('U'), $upper->format('U')); + } + } + } + + public static function customDataProvider(): array + { + return [ + [ + [ + ['a', 'f'], + ['from', 'to'], + [null, 'to'], + ['from', null], + ], + '{[a,f],[from,to],[,to],[from,]}', + ] + ]; + } + + /** + * @dataProvider customDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testCustomMultiRange(array $expected, string $value): void + { + $parser = (new MultiRangeParser())->asCustom(); + $result = $parser->parse($value); + + $this->assertSame($expected, $result); + } + + public static function exceptionDataProvider(): array + { + return [ + ['{,2147483649)}'], + ['test'], + ['{[2147483648,2147483649,[,2147483649)}}'] + ]; + } + + /** + * @dataProvider exceptionDataProvider + * @param string $value + * @return void + */ + public function testExceptions(string $value): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported range format'); + + (new RangeParser())->asCustom()->parse($value); + } +} diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 62ee902c0..ae6e97a7b 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -358,6 +358,90 @@ public static function columns(): array 'defaultValue' => null, 'dimension' => 1, ], + 'intrange_col' => [ + 'type' => 'string', + 'dbType' => 'int4range', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 0, + ], + 'bigintrange_col' => [ + 'type' => 'string', + 'dbType' => 'int8range', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 0, + ], + 'numrange_col' => [ + 'type' => 'string', + 'dbType' => 'numrange', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 0, + ], + 'daterange_col' => [ + 'type' => 'string', + 'dbType' => 'daterange', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 0, + ], + 'tsrange_col' => [ + 'type' => 'string', + 'dbType' => 'tsrange', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 0, + ], + 'tstzrange_col' => [ + 'type' => 'string', + 'dbType' => 'tstzrange', + 'phpType' => 'string', + 'primaryKey' => false, + 'allowNull' => true, + 'autoIncrement' => false, + 'enumValues' => null, + 'size' => null, + 'precision' => null, + 'scale' => null, + 'defaultValue' => null, + 'dimension' => 0, + ], ], 'tableName' => 'type', ], diff --git a/tests/RangeParserTest.php b/tests/RangeParserTest.php new file mode 100644 index 000000000..7ae5b1a8a --- /dev/null +++ b/tests/RangeParserTest.php @@ -0,0 +1,446 @@ +parse($value); + + $this->assertNull($result); + $this->assertNull($parser->asCustom()->parse($value)); + + foreach (self::TYPES as $type) { + $this->assertNull($parser->withType($type)->parse($value)); + + switch ($type) { + case Schema::TYPE_INT_RANGE: + $this->assertNull($parser->asInt()->parse($value)); + break; + case Schema::TYPE_BIGINT_RANGE: + $this->assertNull($parser->asBigInt()->parse($value)); + break; + case Schema::TYPE_NUM_RANGE: + $this->assertNull($parser->asNumeric()->parse($value)); + break; + case Schema::TYPE_DATE_RANGE: + $this->assertNull($parser->asDate()->parse($value)); + break; + case Schema::TYPE_TS_RANGE: + $this->assertNull($parser->asTimestamp()->parse($value)); + break; + case Schema::TYPE_TS_TZ_RANGE: + $this->assertNull($parser->asTimestampTz()->parse($value)); + break; + } + } + } + + public static function intRangeDataProvider(): array + { + return [ + [ + [1, 10], + '[1,11)', + ], + + [ + [100, 120], + '[100,120]', + ], + + [ + [null, null], + '(,)', + ], + + [ + [0, 5], + '(-1,6)', + ], + + [ + [5, null], + '[5,)', + ], + + [ + [null, 7], + '[,8)', + ], + ]; + } + + /** + * @dataProvider intRangeDataProvider + * @return void + * @throws \Throwable + * @throws \Yiisoft\Db\Exception\Exception + * @throws \Yiisoft\Db\Exception\InvalidConfigException + */ + public function testIntRangeParser(array $expected, string $value): void + { + $parser = new RangeParser(); + $result = $parser->asInt()->parse($value); + + $this->assertSame($expected, $result); + } + + public static function bigIntDataProvider(): array + { + return [ + [ + [ + PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', + PHP_INT_SIZE === 8 ? (int) '2147483649' : (float) '2147483649', + ], + '[2147483648,2147483649]', + ], + + [ + [ + null, + PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', + ], + '[,2147483649)', + ], + ]; + } + + /** + * @dataProvider bigIntDataProvider + * @param string $value + * @return void + */ + public function testBigIntRange(array $expected, string $value): void + { + $parser = (new RangeParser())->asBigInt(); + $result = $parser->parse($value); + + $this->assertSame($expected, $result); + } + + public static function numRangeDataProvider(): array + { + return [ + [ + [10.5, 20.7], + '[10.5,20.7]', + ], + + [ + [null, 39.3], + '(,39.3]', + ], + + [ + [11.2, null], + '(11.2,)', + ], + + [ + [null, null], + '[,]', + ], + ]; + } + + /** + * @dataProvider numRangeDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testNumRangeParser(array $expected, string $value): void + { + $parser = new RangeParser(); + $result = $parser->asNumeric()->parse($value); + + $this->assertSame($expected, $result); + } + + + public static function dateRangeDataProvider(): array + { + return [ + [ + new DateTime('2020-12-01'), + new DateTime('2021-01-01'), + '[2020-12-01,2021-01-01]', + ], + + [ + new DateTime('2020-12-01'), + new DateTime('2021-01-02'), + '[2020-12-01,2021-01-03)', + ], + + [ + new DateTime('2021-01-01'), + new DateTime('2021-01-02'), + '(2020-12-31,2021-01-03)', + ], + + [ + null, + new DateTime('2021-01-02'), + '(,2021-01-03)', + ], + + [ + new DateTime('2020-12-01'), + null, + '[2020-12-01,)', + ], + ]; + } + + /** + * @dataProvider dateRangeDataProvider + * @param DateTimeInterface|null $lower + * @param DateTimeInterface|null $upper + * @param string $value + * @return void + */ + public function testDateRangeParser(?DateTimeInterface $lower, ?DateTimeInterface $upper, string $value): void + { + $parser = new RangeParser(Schema::TYPE_DATE_RANGE); + $result = $parser->parse($value); + + $this->assertCount(2, $result); + + $min = $result[0]; + $max = $result[1]; + + if ($lower === null) { + $this->assertNull($min); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $min); + $this->assertSame($lower->format('Y-m-d'), $min->format('Y-m-d')); + } + + if ($upper === null) { + $this->assertNull($max); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $max); + $this->assertSame($upper->format('Y-m-d'), $max->format('Y-m-d')); + } + } + + public static function timestampDataProvider(): array + { + return [ + [ + [ + new DateTime('2023-08-07 13:00:00'), + new DateTime('2023-08-07 13:30:00'), + ], + '[2023-08-07 13:00:00,2023-08-07 13:30:00]' + ], + [ + [ + null, + new DateTime('2023-08-07 13:30:00'), + ], + '[,2023-08-07 13:30:00]' + ], + [ + [ + new DateTime('2023-08-07 13:00:00'), + null, + ], + '[2023-08-07 13:00:00,]' + ], + ]; + } + + /** + * @dataProvider timestampDataProvider + * @param DateTime[]|null[] $expected + * @param string $value + * @return void + */ + public function testTimestampRange(array $expected, string $value): void + { + $parser = new RangeParser(); + $result = $parser->asTimestamp()->parse($value); + + $this->assertCount(2, $result); + + $lower = $result[0]; + $upper = $result[1]; + $min = $expected[0]; + $max = $expected[1]; + + if ($min === null) { + $this->assertNull($lower); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $lower); + $this->assertSame($min->format('U'), $lower->format('U')); + } + + if ($max === null) { + $this->assertNull($upper); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $upper); + $this->assertSame($max->format('U'), $upper->format('U')); + } + } + + public static function timestampTzDataProvider(): array + { + return [ + [ + [ + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), + ], + '[2023-08-07 13:00:00+03,2023-08-07 13:30:00+03]' + ], + [ + [ + null, + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), + ], + '[,2023-08-07 13:30:00+03]' + ], + [ + [ + DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), + null, + ], + '[2023-08-07 13:00:00+03,]' + ], + ]; + } + + /** + * @dataProvider timestampTzDataProvider + * @param DateTimeInterface[]|null[] $expected + * @param string $value + * @return void + */ + public function testTimestampTzRange(array $expected, string $value): void + { + $parser = new RangeParser(); + $result = $parser->asTimestampTz()->parse($value); + + $this->assertCount(2, $result); + + $lower = $result[0]; + $upper = $result[1]; + $min = $expected[0]; + $max = $expected[1]; + + if ($min === null) { + $this->assertNull($lower); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $lower); + $this->assertSame($min->format('U'), $lower->format('U')); + } + + if ($max === null) { + $this->assertNull($upper); + } else { + $this->assertInstanceOf(DateTimeInterface::class, $upper); + $this->assertSame($max->format('U'), $upper->format('U')); + } + } + + public static function customDataProvider(): array + { + return [ + [ + ['a', 'f'], + '[a,f]', + ], + + [ + ['from', 'to'], + '[from,to]', + ], + + [ + [null, 'to'], + '[,to]', + ], + + [ + ['from', null], + '[from,]', + ] + ]; + } + + /** + * @dataProvider customDataProvider + * @param array $expected + * @param string $value + * @return void + */ + public function testCustomRange(array $expected, string $value): void + { + $parser = new RangeParser('my_custom_type'); + $result = $parser->parse($value); + $customResult = $parser->asCustom()->parse($value); + + $this->assertSame($expected, $result); + $this->assertSame($expected, $customResult); + } + + public static function exceptionDataProvider(): array + { + return [ + ['(10,15'], + ['test'], + ['{10,15}'], + ['11,]'], + ]; + } + + /** + * @dataProvider exceptionDataProvider + * @param string $value + * @return void + */ + public function testArgumentException(string $value): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported range format'); + + (new RangeParser())->parse($value); + } +} diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index 97df69307..7b012ea6f 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -162,7 +162,13 @@ CREATE TABLE "type" ( textarray2_col text[][], json_col json DEFAULT '{"a":1}', jsonb_col jsonb, - jsonarray_col json[] + jsonarray_col json[], + intrange_col int4range, + bigintrange_col int8range, + numrange_col numrange, + daterange_col daterange, + tsrange_col tsrange, + tstzrange_col tstzrange ); CREATE TABLE "bool_values" ( diff --git a/tests/Support/Fixture/pgsql14.sql b/tests/Support/Fixture/pgsql14.sql new file mode 100644 index 000000000..1a7fc0992 --- /dev/null +++ b/tests/Support/Fixture/pgsql14.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS "table_with_multirange" CASCADE; + +CREATE TABLE "table_with_multirange" ( + id serial not null primary key, + int4multirange_col int4multirange, + int8multirange_col int8multirange, + nummultirange_col nummultirange, + datemultirange_col datemultirange +); From a979f6536ebb4d5b6e5321a76d4dfc91d8ff9e0b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 13 Nov 2025 17:36:19 +0300 Subject: [PATCH 02/37] range --- src/Column/ColumnBuilder.php | 52 +++ src/Column/ColumnFactory.php | 19 +- src/Column/RangeColumn.php | 164 +++++++ src/Constant/PgsqlColumnType.php | 10 + src/Expression/Builder/RangeValueBuilder.php | 52 +++ src/Expression/DateRangeValue.php | 18 + src/Expression/Int4RangeValue.php | 17 + src/Expression/Int8RangeValue.php | 17 + src/Expression/NumRangeValue.php | 17 + src/Expression/TsRangeValue.php | 18 + src/Expression/TsTzRangeValue.php | 18 + src/MultiRangeParser.php | 96 ---- src/RangeParser.php | 185 -------- tests/MultiRangeParserTest.php | 389 ---------------- tests/Provider/SchemaProvider.php | 7 + tests/RangeParserTest.php | 446 ------------------- tests/SchemaTest.php | 2 +- 17 files changed, 408 insertions(+), 1119 deletions(-) create mode 100644 src/Column/RangeColumn.php create mode 100644 src/Constant/PgsqlColumnType.php create mode 100644 src/Expression/Builder/RangeValueBuilder.php create mode 100644 src/Expression/DateRangeValue.php create mode 100644 src/Expression/Int4RangeValue.php create mode 100644 src/Expression/Int8RangeValue.php create mode 100644 src/Expression/NumRangeValue.php create mode 100644 src/Expression/TsRangeValue.php create mode 100644 src/Expression/TsTzRangeValue.php delete mode 100644 src/MultiRangeParser.php delete mode 100644 src/RangeParser.php delete mode 100644 tests/MultiRangeParserTest.php delete mode 100644 tests/RangeParserTest.php diff --git a/src/Column/ColumnBuilder.php b/src/Column/ColumnBuilder.php index cf0d4c1ff..60522eed3 100644 --- a/src/Column/ColumnBuilder.php +++ b/src/Column/ColumnBuilder.php @@ -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 { @@ -22,6 +24,11 @@ public static function bit(?int $size = null): BitColumn|BigBitColumn 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); @@ -58,4 +65,49 @@ public static function structured(?string $dbType = null, array $columns = []): { 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(): RangeColumn + { + return new RangeColumn(dbType: 'int4range'); + } + + public static function int8Range(): RangeColumn + { + return new RangeColumn(dbType: 'int8range'); + } + + public static function numRange(): RangeColumn + { + return new RangeColumn(dbType: 'numrange'); + } + + public static function tsRange(): RangeColumn + { + return new RangeColumn(dbType: 'tsrange'); + } + + public static function tsTzRange(): RangeColumn + { + return new RangeColumn(dbType: 'tstzrange'); + } + + public static function dateRange(): RangeColumn + { + return new RangeColumn(dbType: 'daterange'); + } } diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index 8068888d1..ac46a3a43 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -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; @@ -23,8 +24,7 @@ final class ColumnFactory extends AbstractColumnFactory * * @link https://www.postgresql.org/docs/current/datatype.html#DATATYPE-TABLE * - * @var string[] - * @psalm-var array + * @inheritdoc */ protected const TYPE_MAP = [ 'bool' => ColumnType::BOOLEAN, @@ -91,6 +91,12 @@ final class ColumnFactory extends AbstractColumnFactory 'xml' => ColumnType::STRING, 'json' => ColumnType::JSON, 'jsonb' => ColumnType::JSON, + 'int4range' => PgsqlColumnType::RANGE, + 'int8range' => PgsqlColumnType::RANGE, + 'numrange' => PgsqlColumnType::RANGE, + 'tsrange' => PgsqlColumnType::RANGE, + 'tstzrange' => PgsqlColumnType::RANGE, + 'daterange' => PgsqlColumnType::RANGE, ]; public function fromType(string $type, array $info = []): ColumnInterface @@ -129,10 +135,19 @@ protected function getColumnClass(string $type, array $info = []): string ColumnType::BINARY => BinaryColumn::class, ColumnType::ARRAY => ArrayColumn::class, ColumnType::STRUCTURED => StructuredColumn::class, + PgsqlColumnType::RANGE => RangeColumn::class, default => parent::getColumnClass($type, $info), }; } + protected function isType(string $type): bool + { + return match ($type) { + PgsqlColumnType::RANGE => true, + default => parent::isType($type), + }; + } + protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed { /** @var string $value */ diff --git a/src/Column/RangeColumn.php b/src/Column/RangeColumn.php new file mode 100644 index 000000000..759b2da21 --- /dev/null +++ b/src/Column/RangeColumn.php @@ -0,0 +1,164 @@ +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.', + ); + } + + $dbType = $this->getDbType(); + [$lower, $upper] = $value; + + $column = match ($dbType) { + 'int4range' => ColumnBuilder::integer(), + 'int8range' => ColumnBuilder::bigint(), + 'numrange' => ColumnBuilder::decimal(), + 'tsrange' => ColumnBuilder::datetime(), + 'tstzrange' => ColumnBuilder::datetimeWithTimezone(), + 'daterange' => ColumnBuilder::date(), + default => $this->throwNotSupportedDbType($dbType), + }; + + return '[' + . $this->dbTypecastBound($column->dbTypecast($lower)) + . ',' + . $this->dbTypecastBound($column->dbTypecast($upper)) + . ')'; + } + + public function phpTypecast(mixed $value): mixed + { + if ($value === null || $value === 'empty') { + return null; + } + + if (!is_string($value)) { + $this->throwWrongTypeException(gettype($value)); + } + + if (!preg_match('/^(?P\[|\()(?P[^,]*),(?P[^\)\]]*)(?P\)|\])$/', $value, $matches)) { + throw new NotSupportedException('Unsupported range format.'); + } + + $dbType = $this->getDbType(); + $lower = $matches['lower'] ? trim($matches['lower'], '"') : null; + $upper = $matches['upper'] ? trim($matches['upper'], '"') : null; + $includeLower = $matches['open'] === '['; + $includeUpper = $matches['close'] === ']'; + + switch ($dbType) { + case 'int4range': + $column = ColumnBuilder::integer(); + return new Int4RangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + case 'int8range': + $column = ColumnBuilder::bigint(); + return new Int8RangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + case 'numrange': + $column = ColumnBuilder::decimal(); + return new NumRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + case 'tsrange': + $column = ColumnBuilder::datetime(); + return new TsRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + case 'tstzrange': + $column = ColumnBuilder::datetimeWithTimezone(); + return new TsTzRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + case 'daterange': + $column = ColumnBuilder::date(); + return new DateRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + default: + $this->throwNotSupportedDbType($dbType); + } + } + + /** + * @throws NotSupportedException + */ + private function dbTypecastBound(ExpressionInterface|float|int|string|null $value): string + { + return match (gettype($value)) { + 'NULL' => 'NULL', + 'double', + 'integer' => (string) $value, + 'string' => $value, + 'object' => throw new NotSupportedException('Range bound as expression is not supported.'), + }; + } + + /** + * @throws NotSupportedException + */ + private function throwNotSupportedDbType(?string $dbType): never + { + throw new NotSupportedException( + sprintf( + 'Unsupported range type: %s', + $dbType ?? 'unknown', + ), + ); + } +} diff --git a/src/Constant/PgsqlColumnType.php b/src/Constant/PgsqlColumnType.php new file mode 100644 index 000000000..b6ce88c61 --- /dev/null +++ b/src/Constant/PgsqlColumnType.php @@ -0,0 +1,10 @@ + + */ +final class RangeValueBuilder implements ExpressionBuilderInterface +{ + public function __construct( + private readonly QueryBuilderInterface $queryBuilder, + ) {} + + public function build(ExpressionInterface $expression, array &$params = []): string + { + $column = match ($expression::class) { + Int4RangeValue::class => ColumnBuilder::integer(), + Int8RangeValue::class => ColumnBuilder::bigint(), + NumRangeValue::class => ColumnBuilder::decimal(), + TsRangeValue::class => ColumnBuilder::datetime(), + TsTzRangeValue::class => ColumnBuilder::datetimeWithTimezone(), + DateRangeValue::class => ColumnBuilder::date(), + }; + + return ($expression->includeLower ? '[' : '(') + . $this->queryBuilder->buildValue( + $column->dbTypecast($expression->lower), + $params, + ) + . ', ' + . $this->queryBuilder->buildValue( + $column->dbTypecast($expression->upper), + $params, + ) + . ($expression->includeUpper ? ']' : ')'); + } +} diff --git a/src/Expression/DateRangeValue.php b/src/Expression/DateRangeValue.php new file mode 100644 index 000000000..c083b69c3 --- /dev/null +++ b/src/Expression/DateRangeValue.php @@ -0,0 +1,18 @@ + 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]); - } -} diff --git a/src/RangeParser.php b/src/RangeParser.php deleted file mode 100644 index 4ce66a2b6..000000000 --- a/src/RangeParser.php +++ /dev/null @@ -1,185 +0,0 @@ -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\[|\()(?P[^,]*),(?P[^\)\]]*)(?P\)|\])$/', $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); - } -} diff --git a/tests/MultiRangeParserTest.php b/tests/MultiRangeParserTest.php deleted file mode 100644 index fa98d179a..000000000 --- a/tests/MultiRangeParserTest.php +++ /dev/null @@ -1,389 +0,0 @@ -parse($value); - - $this->assertSame($expected, $result); - } - - public static function intRangeDataProvider(): array - { - return [ - [ - [[3, 6]], - '{[3,7)}', - ], - - [ - [[3,6], [8,8]], - '{[3,7),[8,9)}' - ], - ]; - } - - /** - * @dataProvider intRangeDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testIntMultiRange(array $expected, string $value): void - { - $parser = new MultiRangeParser(); - $result = $parser->asMultiInt()->parse($value); - - $this->assertSame($expected, $result); - } - - public static function dateRangeDataProvider(): array - { - return [ - [ - [ - [ - new DateTime('2020-12-01'), - new DateTime('2021-01-01'), - ], - - [ - new DateTime('2020-12-01'), - new DateTime('2021-01-02'), - ], - - [ - new DateTime('2021-01-01'), - new DateTime('2021-01-02'), - ], - - [ - null, - new DateTime('2021-01-02'), - ], - - [ - new DateTime('2020-12-01'), - null, - ], - ], - '{[2020-12-01,2021-01-01],[2020-12-01,2021-01-03),(2020-12-31,2021-01-03),(,2021-01-03),[2020-12-01,)}', - ], - ]; - } - - /** - * @dataProvider dateRangeDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testDateMultiRange(array $expected, string $value): void - { - $parser = new MultiRangeParser(); - $result = $parser->asMultiDate()->parse($value); - - $this->assertCount(count($expected), $result); - - foreach ($expected as $i => $date) { - $this->assertCount(2, $result[$i]); - - $lower = $date[0]; - $upper = $date[1]; - $min = $result[$i][0]; - $max = $result[$i][1]; - - if ($lower === null) { - $this->assertNull($min); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $min); - $this->assertSame($lower->format('Y-m-d'), $min->format('Y-m-d')); - } - - if ($upper === null) { - $this->assertNull($max); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $max); - $this->assertSame($upper->format('Y-m-d'), $max->format('Y-m-d')); - } - } - } - - public static function numRangeDataProvider(): array - { - return [ - [ - [ - [10.5, 20.7], - [null, 39.3], - [11.2, null], - [null, null], - ], - '{[10.5,20.7],(,39.3],(11.2,),[,]}', - ], - ]; - } - - /** - * @dataProvider numRangeDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testNumMultiRange(array $expected, string $value): void - { - $parser = new MultiRangeParser(); - $result = $parser->asMultiNumeric()->parse($value); - - $this->assertSame($expected, $result); - } - - public static function bigIntDataProvider(): array - { - return [ - [ - [ - [ - PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', - PHP_INT_SIZE === 8 ? (int) '2147483649' : (float) '2147483649', - ], - [ - null, - PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', - ], - ], - '{[2147483648,2147483649],[,2147483649)}', - ], - ]; - } - - /** - * @dataProvider bigIntDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testBigIntMultiRange(array $expected, string $value): void - { - $parser = new MultiRangeParser(); - $result = $parser->asMultiBigInt()->parse($value); - - $this->assertSame($expected, $result); - } - - public static function timestampDataProvider(): array - { - return [ - [ - [ - [ - new DateTime('2023-08-07 13:00:00'), - new DateTime('2023-08-07 13:30:00'), - ], - [ - null, - new DateTime('2023-08-07 13:30:00'), - ], - [ - new DateTime('2023-08-07 13:00:00'), - null, - ], - ], - - '{[2023-08-07 13:00:00,2023-08-07 13:30:00],[,2023-08-07 13:30:00],[2023-08-07 13:00:00,]}' - ], - ]; - } - - /** - * @dataProvider timestampDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testTimestampMultiRange(array $expected, string $value): void - { - $parser = new MultiRangeParser(); - $result = $parser->asMultiTimestamp()->parse($value); - - $this->assertCount(count($expected), $result); - - foreach ($expected as $i => $date) { - - $res = $result[$i]; - - $this->assertCount(count($date), $res); - - $lower = $res[0]; - $upper = $res[1]; - $min = $date[0]; - $max = $date[1]; - - if ($min === null) { - $this->assertNull($lower); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $lower); - $this->assertSame($min->format('U'), $lower->format('U')); - } - - if ($max === null) { - $this->assertNull($upper); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $upper); - $this->assertSame($max->format('U'), $upper->format('U')); - } - } - } - - public static function timestampTzDataProvider(): array - { - return [ - [ - [ - [ - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), - ], - - [ - null, - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), - ], - - [ - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), - null, - ], - ], - - '{[2023-08-07 13:00:00+03,2023-08-07 13:30:00+03],[,2023-08-07 13:30:00+03],[2023-08-07 13:00:00+03,]}' - ], - ]; - } - - /** - * @dataProvider timestampTzDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testTimestampTzMultiRange(array $expected, string $value): void - { - $parser = new MultiRangeParser(); - $result = $parser->asMultiTimestampTz()->parse($value); - - $this->assertCount(count($expected), $result); - - foreach ($expected as $i => $date) { - - $res = $result[$i]; - - $this->assertCount(count($date), $res); - - $lower = $res[0]; - $upper = $res[1]; - $min = $date[0]; - $max = $date[1]; - - if ($min === null) { - $this->assertNull($lower); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $lower); - $this->assertSame($min->format('U'), $lower->format('U')); - } - - if ($max === null) { - $this->assertNull($upper); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $upper); - $this->assertSame($max->format('U'), $upper->format('U')); - } - } - } - - public static function customDataProvider(): array - { - return [ - [ - [ - ['a', 'f'], - ['from', 'to'], - [null, 'to'], - ['from', null], - ], - '{[a,f],[from,to],[,to],[from,]}', - ] - ]; - } - - /** - * @dataProvider customDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testCustomMultiRange(array $expected, string $value): void - { - $parser = (new MultiRangeParser())->asCustom(); - $result = $parser->parse($value); - - $this->assertSame($expected, $result); - } - - public static function exceptionDataProvider(): array - { - return [ - ['{,2147483649)}'], - ['test'], - ['{[2147483648,2147483649,[,2147483649)}}'] - ]; - } - - /** - * @dataProvider exceptionDataProvider - * @param string $value - * @return void - */ - public function testExceptions(string $value): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unsupported range format'); - - (new RangeParser())->asCustom()->parse($value); - } -} diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 56fd4de35..ca5e02cea 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -14,6 +14,7 @@ use Yiisoft\Db\Pgsql\Column\BitColumn; use Yiisoft\Db\Pgsql\Column\BooleanColumn; use Yiisoft\Db\Pgsql\Column\IntegerColumn; +use Yiisoft\Db\Pgsql\Column\RangeColumn; use Yiisoft\Db\Schema\Column\DateTimeColumn; use Yiisoft\Db\Schema\Column\DoubleColumn; use Yiisoft\Db\Schema\Column\JsonColumn; @@ -191,6 +192,12 @@ public static function columns(): array notNull: false, ), ), + 'intrange_col' => new RangeColumn(dbType: 'int4range'), + 'bigintrange_col' => new RangeColumn(dbType: 'int8range'), + 'numrange_col' => new RangeColumn(dbType: 'numrange'), + 'daterange_col' => new RangeColumn(dbType: 'daterange'), + 'tsrange_col' => new RangeColumn(dbType: 'tsrange'), + 'tstzrange_col' => new RangeColumn(dbType: 'tstzrange'), ], 'tableName' => 'type', ], diff --git a/tests/RangeParserTest.php b/tests/RangeParserTest.php deleted file mode 100644 index 7ae5b1a8a..000000000 --- a/tests/RangeParserTest.php +++ /dev/null @@ -1,446 +0,0 @@ -parse($value); - - $this->assertNull($result); - $this->assertNull($parser->asCustom()->parse($value)); - - foreach (self::TYPES as $type) { - $this->assertNull($parser->withType($type)->parse($value)); - - switch ($type) { - case Schema::TYPE_INT_RANGE: - $this->assertNull($parser->asInt()->parse($value)); - break; - case Schema::TYPE_BIGINT_RANGE: - $this->assertNull($parser->asBigInt()->parse($value)); - break; - case Schema::TYPE_NUM_RANGE: - $this->assertNull($parser->asNumeric()->parse($value)); - break; - case Schema::TYPE_DATE_RANGE: - $this->assertNull($parser->asDate()->parse($value)); - break; - case Schema::TYPE_TS_RANGE: - $this->assertNull($parser->asTimestamp()->parse($value)); - break; - case Schema::TYPE_TS_TZ_RANGE: - $this->assertNull($parser->asTimestampTz()->parse($value)); - break; - } - } - } - - public static function intRangeDataProvider(): array - { - return [ - [ - [1, 10], - '[1,11)', - ], - - [ - [100, 120], - '[100,120]', - ], - - [ - [null, null], - '(,)', - ], - - [ - [0, 5], - '(-1,6)', - ], - - [ - [5, null], - '[5,)', - ], - - [ - [null, 7], - '[,8)', - ], - ]; - } - - /** - * @dataProvider intRangeDataProvider - * @return void - * @throws \Throwable - * @throws \Yiisoft\Db\Exception\Exception - * @throws \Yiisoft\Db\Exception\InvalidConfigException - */ - public function testIntRangeParser(array $expected, string $value): void - { - $parser = new RangeParser(); - $result = $parser->asInt()->parse($value); - - $this->assertSame($expected, $result); - } - - public static function bigIntDataProvider(): array - { - return [ - [ - [ - PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', - PHP_INT_SIZE === 8 ? (int) '2147483649' : (float) '2147483649', - ], - '[2147483648,2147483649]', - ], - - [ - [ - null, - PHP_INT_SIZE === 8 ? (int) '2147483648' : (float) '2147483648', - ], - '[,2147483649)', - ], - ]; - } - - /** - * @dataProvider bigIntDataProvider - * @param string $value - * @return void - */ - public function testBigIntRange(array $expected, string $value): void - { - $parser = (new RangeParser())->asBigInt(); - $result = $parser->parse($value); - - $this->assertSame($expected, $result); - } - - public static function numRangeDataProvider(): array - { - return [ - [ - [10.5, 20.7], - '[10.5,20.7]', - ], - - [ - [null, 39.3], - '(,39.3]', - ], - - [ - [11.2, null], - '(11.2,)', - ], - - [ - [null, null], - '[,]', - ], - ]; - } - - /** - * @dataProvider numRangeDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testNumRangeParser(array $expected, string $value): void - { - $parser = new RangeParser(); - $result = $parser->asNumeric()->parse($value); - - $this->assertSame($expected, $result); - } - - - public static function dateRangeDataProvider(): array - { - return [ - [ - new DateTime('2020-12-01'), - new DateTime('2021-01-01'), - '[2020-12-01,2021-01-01]', - ], - - [ - new DateTime('2020-12-01'), - new DateTime('2021-01-02'), - '[2020-12-01,2021-01-03)', - ], - - [ - new DateTime('2021-01-01'), - new DateTime('2021-01-02'), - '(2020-12-31,2021-01-03)', - ], - - [ - null, - new DateTime('2021-01-02'), - '(,2021-01-03)', - ], - - [ - new DateTime('2020-12-01'), - null, - '[2020-12-01,)', - ], - ]; - } - - /** - * @dataProvider dateRangeDataProvider - * @param DateTimeInterface|null $lower - * @param DateTimeInterface|null $upper - * @param string $value - * @return void - */ - public function testDateRangeParser(?DateTimeInterface $lower, ?DateTimeInterface $upper, string $value): void - { - $parser = new RangeParser(Schema::TYPE_DATE_RANGE); - $result = $parser->parse($value); - - $this->assertCount(2, $result); - - $min = $result[0]; - $max = $result[1]; - - if ($lower === null) { - $this->assertNull($min); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $min); - $this->assertSame($lower->format('Y-m-d'), $min->format('Y-m-d')); - } - - if ($upper === null) { - $this->assertNull($max); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $max); - $this->assertSame($upper->format('Y-m-d'), $max->format('Y-m-d')); - } - } - - public static function timestampDataProvider(): array - { - return [ - [ - [ - new DateTime('2023-08-07 13:00:00'), - new DateTime('2023-08-07 13:30:00'), - ], - '[2023-08-07 13:00:00,2023-08-07 13:30:00]' - ], - [ - [ - null, - new DateTime('2023-08-07 13:30:00'), - ], - '[,2023-08-07 13:30:00]' - ], - [ - [ - new DateTime('2023-08-07 13:00:00'), - null, - ], - '[2023-08-07 13:00:00,]' - ], - ]; - } - - /** - * @dataProvider timestampDataProvider - * @param DateTime[]|null[] $expected - * @param string $value - * @return void - */ - public function testTimestampRange(array $expected, string $value): void - { - $parser = new RangeParser(); - $result = $parser->asTimestamp()->parse($value); - - $this->assertCount(2, $result); - - $lower = $result[0]; - $upper = $result[1]; - $min = $expected[0]; - $max = $expected[1]; - - if ($min === null) { - $this->assertNull($lower); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $lower); - $this->assertSame($min->format('U'), $lower->format('U')); - } - - if ($max === null) { - $this->assertNull($upper); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $upper); - $this->assertSame($max->format('U'), $upper->format('U')); - } - } - - public static function timestampTzDataProvider(): array - { - return [ - [ - [ - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), - ], - '[2023-08-07 13:00:00+03,2023-08-07 13:30:00+03]' - ], - [ - [ - null, - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:30:00+03'), - ], - '[,2023-08-07 13:30:00+03]' - ], - [ - [ - DateTime::createFromFormat('Y-m-d H:i:sP', '2023-08-07 13:00:00+03'), - null, - ], - '[2023-08-07 13:00:00+03,]' - ], - ]; - } - - /** - * @dataProvider timestampTzDataProvider - * @param DateTimeInterface[]|null[] $expected - * @param string $value - * @return void - */ - public function testTimestampTzRange(array $expected, string $value): void - { - $parser = new RangeParser(); - $result = $parser->asTimestampTz()->parse($value); - - $this->assertCount(2, $result); - - $lower = $result[0]; - $upper = $result[1]; - $min = $expected[0]; - $max = $expected[1]; - - if ($min === null) { - $this->assertNull($lower); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $lower); - $this->assertSame($min->format('U'), $lower->format('U')); - } - - if ($max === null) { - $this->assertNull($upper); - } else { - $this->assertInstanceOf(DateTimeInterface::class, $upper); - $this->assertSame($max->format('U'), $upper->format('U')); - } - } - - public static function customDataProvider(): array - { - return [ - [ - ['a', 'f'], - '[a,f]', - ], - - [ - ['from', 'to'], - '[from,to]', - ], - - [ - [null, 'to'], - '[,to]', - ], - - [ - ['from', null], - '[from,]', - ] - ]; - } - - /** - * @dataProvider customDataProvider - * @param array $expected - * @param string $value - * @return void - */ - public function testCustomRange(array $expected, string $value): void - { - $parser = new RangeParser('my_custom_type'); - $result = $parser->parse($value); - $customResult = $parser->asCustom()->parse($value); - - $this->assertSame($expected, $result); - $this->assertSame($expected, $customResult); - } - - public static function exceptionDataProvider(): array - { - return [ - ['(10,15'], - ['test'], - ['{10,15}'], - ['11,]'], - ]; - } - - /** - * @dataProvider exceptionDataProvider - * @param string $value - * @return void - */ - public function testArgumentException(string $value): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Unsupported range format'); - - (new RangeParser())->parse($value); - } -} diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index e17866cd2..67a3b80e3 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -75,7 +75,7 @@ public function testColumnTypeMapNoExist(): void $command->dropTable('type_range')->execute(); } - $command->createTable('type_range', ['id' => 'int', 'during tsrange'])->execute(); + $command->createTable('type_range', ['id' => 'int', 'during point'])->execute(); $table = $schema->getTableSchema('type_range', true); From f4be518eb93f5cfcc8800b5ed1e0e80a15275e92 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 10:26:22 +0300 Subject: [PATCH 03/37] avoid extra check in `phpTypecast()` --- src/Column/RangeColumn.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Column/RangeColumn.php b/src/Column/RangeColumn.php index 759b2da21..b262857fc 100644 --- a/src/Column/RangeColumn.php +++ b/src/Column/RangeColumn.php @@ -63,14 +63,15 @@ public function dbTypecast(mixed $value): mixed 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 (!is_string($value)) { - $this->throwWrongTypeException(gettype($value)); - } - if (!preg_match('/^(?P\[|\()(?P[^,]*),(?P[^\)\]]*)(?P\)|\])$/', $value, $matches)) { throw new NotSupportedException('Unsupported range format.'); } From 5ecd609dbb8c5915edbc65fbf1f2b90075f99139 Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Fri, 14 Nov 2025 07:26:58 +0000 Subject: [PATCH 04/37] Apply PHP CS Fixer and Rector changes (CI) --- src/Column/RangeColumn.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Column/RangeColumn.php b/src/Column/RangeColumn.php index b262857fc..21ba4c423 100644 --- a/src/Column/RangeColumn.php +++ b/src/Column/RangeColumn.php @@ -18,7 +18,6 @@ use function gettype; use function is_array; -use function is_string; use function sprintf; final class RangeColumn extends AbstractColumn From 17306ece5d57fee0af27e8763d110617233347fd Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 10:34:40 +0300 Subject: [PATCH 05/37] improve --- src/Column/RangeColumn.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Column/RangeColumn.php b/src/Column/RangeColumn.php index b262857fc..3e8d6e8b4 100644 --- a/src/Column/RangeColumn.php +++ b/src/Column/RangeColumn.php @@ -77,8 +77,8 @@ public function phpTypecast(mixed $value): mixed } $dbType = $this->getDbType(); - $lower = $matches['lower'] ? trim($matches['lower'], '"') : null; - $upper = $matches['upper'] ? trim($matches['upper'], '"') : null; + $lower = $matches['lower'] === '' ? null : trim($matches['lower'], '"'); + $upper = $matches['upper'] === '' ? null : trim($matches['upper'], '"'); $includeLower = $matches['open'] === '['; $includeUpper = $matches['close'] === ']'; From 39ccc9f035f368c4072cee76278b715ef3695807 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 10:41:20 +0300 Subject: [PATCH 06/37] Don't set extra data for columns --- src/Column/RangeColumn.php | 12 ++++++------ src/Expression/Builder/RangeValueBuilder.php | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Column/RangeColumn.php b/src/Column/RangeColumn.php index 3033df8c7..f8950b408 100644 --- a/src/Column/RangeColumn.php +++ b/src/Column/RangeColumn.php @@ -46,9 +46,9 @@ public function dbTypecast(mixed $value): mixed $column = match ($dbType) { 'int4range' => ColumnBuilder::integer(), 'int8range' => ColumnBuilder::bigint(), - 'numrange' => ColumnBuilder::decimal(), - 'tsrange' => ColumnBuilder::datetime(), - 'tstzrange' => ColumnBuilder::datetimeWithTimezone(), + 'numrange' => ColumnBuilder::decimal(null, null), + 'tsrange' => ColumnBuilder::datetime(null), + 'tstzrange' => ColumnBuilder::datetimeWithTimezone(null), 'daterange' => ColumnBuilder::date(), default => $this->throwNotSupportedDbType($dbType), }; @@ -99,7 +99,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'numrange': - $column = ColumnBuilder::decimal(); + $column = ColumnBuilder::decimal(null, null); return new NumRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -107,7 +107,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'tsrange': - $column = ColumnBuilder::datetime(); + $column = ColumnBuilder::datetime(null); return new TsRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -115,7 +115,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'tstzrange': - $column = ColumnBuilder::datetimeWithTimezone(); + $column = ColumnBuilder::datetimeWithTimezone(null); return new TsTzRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), diff --git a/src/Expression/Builder/RangeValueBuilder.php b/src/Expression/Builder/RangeValueBuilder.php index e9b5f679e..85a51d5fe 100644 --- a/src/Expression/Builder/RangeValueBuilder.php +++ b/src/Expression/Builder/RangeValueBuilder.php @@ -31,9 +31,9 @@ public function build(ExpressionInterface $expression, array &$params = []): str $column = match ($expression::class) { Int4RangeValue::class => ColumnBuilder::integer(), Int8RangeValue::class => ColumnBuilder::bigint(), - NumRangeValue::class => ColumnBuilder::decimal(), - TsRangeValue::class => ColumnBuilder::datetime(), - TsTzRangeValue::class => ColumnBuilder::datetimeWithTimezone(), + NumRangeValue::class => ColumnBuilder::decimal(null, null), + TsRangeValue::class => ColumnBuilder::datetime(null), + TsTzRangeValue::class => ColumnBuilder::datetimeWithTimezone(null), DateRangeValue::class => ColumnBuilder::date(), }; From b3f8ff5d833bdcdf564ca6b51fdd2132614c70ba Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 14:15:25 +0300 Subject: [PATCH 07/37] Introduce `RangeBoundColumnFactory` --- src/Column/RangeBoundColumnFactory.php | 45 ++++++++++++++++++++ src/Column/RangeColumn.php | 24 +++++------ src/Expression/Builder/RangeValueBuilder.php | 14 +++--- 3 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 src/Column/RangeBoundColumnFactory.php diff --git a/src/Column/RangeBoundColumnFactory.php b/src/Column/RangeBoundColumnFactory.php new file mode 100644 index 000000000..6008dbb8e --- /dev/null +++ b/src/Column/RangeBoundColumnFactory.php @@ -0,0 +1,45 @@ + ColumnBuilder::integer(), - 'int8range' => ColumnBuilder::bigint(), - 'numrange' => ColumnBuilder::decimal(null, null), - 'tsrange' => ColumnBuilder::datetime(null), - 'tstzrange' => ColumnBuilder::datetimeWithTimezone(null), - 'daterange' => ColumnBuilder::date(), + 'int4range' => RangeBoundColumnFactory::int4(), + 'int8range' => RangeBoundColumnFactory::int8(), + 'numrange' => RangeBoundColumnFactory::num(), + 'tsrange' => RangeBoundColumnFactory::ts(), + 'tstzrange' => RangeBoundColumnFactory::tsTz(), + 'daterange' => RangeBoundColumnFactory::date(), default => $this->throwNotSupportedDbType($dbType), }; @@ -83,7 +83,7 @@ public function phpTypecast(mixed $value): mixed switch ($dbType) { case 'int4range': - $column = ColumnBuilder::integer(); + $column = RangeBoundColumnFactory::int4(); return new Int4RangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -91,7 +91,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'int8range': - $column = ColumnBuilder::bigint(); + $column = RangeBoundColumnFactory::int8(); return new Int8RangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -99,7 +99,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'numrange': - $column = ColumnBuilder::decimal(null, null); + $column = RangeBoundColumnFactory::num(); return new NumRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -107,7 +107,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'tsrange': - $column = ColumnBuilder::datetime(null); + $column = RangeBoundColumnFactory::ts(); return new TsRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -115,7 +115,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'tstzrange': - $column = ColumnBuilder::datetimeWithTimezone(null); + $column = RangeBoundColumnFactory::tstz(); return new TsTzRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), @@ -123,7 +123,7 @@ public function phpTypecast(mixed $value): mixed $includeUpper, ); case 'daterange': - $column = ColumnBuilder::date(); + $column = RangeBoundColumnFactory::date(); return new DateRangeValue( $column->phpTypecast($lower), $column->phpTypecast($upper), diff --git a/src/Expression/Builder/RangeValueBuilder.php b/src/Expression/Builder/RangeValueBuilder.php index 85a51d5fe..1f3cf9e57 100644 --- a/src/Expression/Builder/RangeValueBuilder.php +++ b/src/Expression/Builder/RangeValueBuilder.php @@ -6,7 +6,7 @@ use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\Pgsql\Column\ColumnBuilder; +use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\DateRangeValue; use Yiisoft\Db\Pgsql\Expression\Int4RangeValue; use Yiisoft\Db\Pgsql\Expression\Int8RangeValue; @@ -29,12 +29,12 @@ public function __construct( public function build(ExpressionInterface $expression, array &$params = []): string { $column = match ($expression::class) { - Int4RangeValue::class => ColumnBuilder::integer(), - Int8RangeValue::class => ColumnBuilder::bigint(), - NumRangeValue::class => ColumnBuilder::decimal(null, null), - TsRangeValue::class => ColumnBuilder::datetime(null), - TsTzRangeValue::class => ColumnBuilder::datetimeWithTimezone(null), - DateRangeValue::class => ColumnBuilder::date(), + Int4RangeValue::class => RangeBoundColumnFactory::int4(), + Int8RangeValue::class => RangeBoundColumnFactory::int8(), + NumRangeValue::class => RangeBoundColumnFactory::num(), + TsRangeValue::class => RangeBoundColumnFactory::ts(), + TsTzRangeValue::class => RangeBoundColumnFactory::tsTz(), + DateRangeValue::class => RangeBoundColumnFactory::date(), }; return ($expression->includeLower ? '[' : '(') From 60da2468f1558ba1a36a98869bf6b01a0c8ea5d9 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 14:19:57 +0300 Subject: [PATCH 08/37] Add default values for `*RangeValue` --- src/Expression/DateRangeValue.php | 8 ++++---- src/Expression/Int4RangeValue.php | 8 ++++---- src/Expression/Int8RangeValue.php | 8 ++++---- src/Expression/NumRangeValue.php | 8 ++++---- src/Expression/TsRangeValue.php | 8 ++++---- src/Expression/TsTzRangeValue.php | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Expression/DateRangeValue.php b/src/Expression/DateRangeValue.php index c083b69c3..9fa76d548 100644 --- a/src/Expression/DateRangeValue.php +++ b/src/Expression/DateRangeValue.php @@ -10,9 +10,9 @@ final class DateRangeValue implements ExpressionInterface { public function __construct( - public readonly ?DateTimeImmutable $lower, - public readonly ?DateTimeImmutable $upper, - public readonly bool $includeLower, - public readonly bool $includeUpper, + public readonly ?DateTimeImmutable $lower = null, + public readonly ?DateTimeImmutable $upper = null, + public readonly bool $includeLower = true, + public readonly bool $includeUpper = true, ) {} } diff --git a/src/Expression/Int4RangeValue.php b/src/Expression/Int4RangeValue.php index 02ebcf15f..d65c6efde 100644 --- a/src/Expression/Int4RangeValue.php +++ b/src/Expression/Int4RangeValue.php @@ -9,9 +9,9 @@ final class Int4RangeValue implements ExpressionInterface { public function __construct( - public readonly ?int $lower, - public readonly ?int $upper, - public readonly bool $includeLower, - public readonly bool $includeUpper, + public readonly ?int $lower = null, + public readonly ?int $upper = null, + public readonly bool $includeLower = true, + public readonly bool $includeUpper = true, ) {} } diff --git a/src/Expression/Int8RangeValue.php b/src/Expression/Int8RangeValue.php index b79b2a4e8..903425569 100644 --- a/src/Expression/Int8RangeValue.php +++ b/src/Expression/Int8RangeValue.php @@ -9,9 +9,9 @@ final class Int8RangeValue implements ExpressionInterface { public function __construct( - public readonly int|string|null $lower, - public readonly int|string|null $upper, - public readonly bool $includeLower, - public readonly bool $includeUpper, + public readonly int|string|null $lower = null, + public readonly int|string|null $upper = null, + public readonly bool $includeLower = true, + public readonly bool $includeUpper = true, ) {} } diff --git a/src/Expression/NumRangeValue.php b/src/Expression/NumRangeValue.php index 626f4fad1..f29a10bba 100644 --- a/src/Expression/NumRangeValue.php +++ b/src/Expression/NumRangeValue.php @@ -9,9 +9,9 @@ final class NumRangeValue implements ExpressionInterface { public function __construct( - public readonly ?float $lower, - public readonly ?float $upper, - public readonly bool $includeLower, - public readonly bool $includeUpper, + public readonly ?float $lower = null, + public readonly ?float $upper = null, + public readonly bool $includeLower = true, + public readonly bool $includeUpper = true, ) {} } diff --git a/src/Expression/TsRangeValue.php b/src/Expression/TsRangeValue.php index 06dc22bc3..50594c8e9 100644 --- a/src/Expression/TsRangeValue.php +++ b/src/Expression/TsRangeValue.php @@ -10,9 +10,9 @@ final class TsRangeValue implements ExpressionInterface { public function __construct( - public readonly ?DateTimeImmutable $lower, - public readonly ?DateTimeImmutable $upper, - public readonly bool $includeLower, - public readonly bool $includeUpper, + public readonly ?DateTimeImmutable $lower = null, + public readonly ?DateTimeImmutable $upper = null, + public readonly bool $includeLower = true, + public readonly bool $includeUpper = true, ) {} } diff --git a/src/Expression/TsTzRangeValue.php b/src/Expression/TsTzRangeValue.php index b8fde1049..7f08c3aab 100644 --- a/src/Expression/TsTzRangeValue.php +++ b/src/Expression/TsTzRangeValue.php @@ -10,9 +10,9 @@ final class TsTzRangeValue implements ExpressionInterface { public function __construct( - public readonly ?DateTimeImmutable $lower, - public readonly ?DateTimeImmutable $upper, - public readonly bool $includeLower, - public readonly bool $includeUpper, + public readonly ?DateTimeImmutable $lower = null, + public readonly ?DateTimeImmutable $upper = null, + public readonly bool $includeLower = true, + public readonly bool $includeUpper = true, ) {} } From 229723a439caa262a863aa40cace24c09dd41e15 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 15:11:59 +0300 Subject: [PATCH 09/37] Separated classes for each range type --- src/Column/AbstractRangeColumn.php | 98 +++++++++++ src/Column/ColumnBuilder.php | 24 +-- src/Column/ColumnFactory.php | 26 ++- src/Column/DateRangeColumn.php | 30 ++++ src/Column/Int4RangeColumn.php | 29 ++++ src/Column/Int8RangeColumn.php | 29 ++++ src/Column/NumRangeColumn.php | 30 ++++ src/Column/RangeColumn.php | 164 ------------------ src/Column/TsRangeColumn.php | 30 ++++ src/Column/TsTzRangeColumn.php | 30 ++++ src/Constant/PgsqlColumnType.php | 7 +- .../Builder/AbstractRangeValueBuilder.php | 55 ++++++ .../Builder/DateRangeValueBuilder.php | 34 ++++ .../Builder/Int4RangeValueBuilder.php | 34 ++++ .../Builder/Int8RangeValueBuilder.php | 34 ++++ .../Builder/NumRangeValueBuilder.php | 34 ++++ src/Expression/Builder/RangeValueBuilder.php | 52 ------ .../Builder/TsRangeValueBuilder.php | 34 ++++ .../Builder/TsTzRangeValueBuilder.php | 34 ++++ tests/Provider/SchemaProvider.php | 19 +- 20 files changed, 583 insertions(+), 244 deletions(-) create mode 100644 src/Column/AbstractRangeColumn.php create mode 100644 src/Column/DateRangeColumn.php create mode 100644 src/Column/Int4RangeColumn.php create mode 100644 src/Column/Int8RangeColumn.php create mode 100644 src/Column/NumRangeColumn.php delete mode 100644 src/Column/RangeColumn.php create mode 100644 src/Column/TsRangeColumn.php create mode 100644 src/Column/TsTzRangeColumn.php create mode 100644 src/Expression/Builder/AbstractRangeValueBuilder.php create mode 100644 src/Expression/Builder/DateRangeValueBuilder.php create mode 100644 src/Expression/Builder/Int4RangeValueBuilder.php create mode 100644 src/Expression/Builder/Int8RangeValueBuilder.php create mode 100644 src/Expression/Builder/NumRangeValueBuilder.php delete mode 100644 src/Expression/Builder/RangeValueBuilder.php create mode 100644 src/Expression/Builder/TsRangeValueBuilder.php create mode 100644 src/Expression/Builder/TsTzRangeValueBuilder.php diff --git a/src/Column/AbstractRangeColumn.php b/src/Column/AbstractRangeColumn.php new file mode 100644 index 000000000..e2e60cf9a --- /dev/null +++ b/src/Column/AbstractRangeColumn.php @@ -0,0 +1,98 @@ +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\[|\()(?P[^,]*),(?P[^\)\]]*)(?P\)|\])$/', $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)) { + 'NULL' => '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), + ), + ), + }; + } +} diff --git a/src/Column/ColumnBuilder.php b/src/Column/ColumnBuilder.php index 60522eed3..292aa8202 100644 --- a/src/Column/ColumnBuilder.php +++ b/src/Column/ColumnBuilder.php @@ -81,33 +81,33 @@ public static function datetime(?int $size = 0): DateTimeColumn return new DateTimeColumn(ColumnType::DATETIME, size: $size); } - public static function int4Range(): RangeColumn + public static function int4Range(): Int4RangeColumn { - return new RangeColumn(dbType: 'int4range'); + return new Int4RangeColumn(); } - public static function int8Range(): RangeColumn + public static function int8Range(): Int8RangeColumn { - return new RangeColumn(dbType: 'int8range'); + return new Int8RangeColumn(); } - public static function numRange(): RangeColumn + public static function numRange(): NumRangeColumn { - return new RangeColumn(dbType: 'numrange'); + return new NumRangeColumn(); } - public static function tsRange(): RangeColumn + public static function tsRange(): TsRangeColumn { - return new RangeColumn(dbType: 'tsrange'); + return new TsRangeColumn(); } - public static function tsTzRange(): RangeColumn + public static function tsTzRange(): TsTzRangeColumn { - return new RangeColumn(dbType: 'tstzrange'); + return new TsTzRangeColumn(); } - public static function dateRange(): RangeColumn + public static function dateRange(): DateRangeColumn { - return new RangeColumn(dbType: 'daterange'); + return new DateRangeColumn(); } } diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index ac46a3a43..4cc597bab 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -91,12 +91,12 @@ final class ColumnFactory extends AbstractColumnFactory 'xml' => ColumnType::STRING, 'json' => ColumnType::JSON, 'jsonb' => ColumnType::JSON, - 'int4range' => PgsqlColumnType::RANGE, - 'int8range' => PgsqlColumnType::RANGE, - 'numrange' => PgsqlColumnType::RANGE, - 'tsrange' => PgsqlColumnType::RANGE, - 'tstzrange' => PgsqlColumnType::RANGE, - 'daterange' => PgsqlColumnType::RANGE, + 'int4range' => PgsqlColumnType::INT4RANGE, + 'int8range' => PgsqlColumnType::INT8RANGE, + 'numrange' => PgsqlColumnType::NUMRANGE, + 'tsrange' => PgsqlColumnType::TSRANGE, + 'tstzrange' => PgsqlColumnType::TSTZRANGE, + 'daterange' => PgsqlColumnType::DATERANGE, ]; public function fromType(string $type, array $info = []): ColumnInterface @@ -135,7 +135,12 @@ protected function getColumnClass(string $type, array $info = []): string ColumnType::BINARY => BinaryColumn::class, ColumnType::ARRAY => ArrayColumn::class, ColumnType::STRUCTURED => StructuredColumn::class, - PgsqlColumnType::RANGE => RangeColumn::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, default => parent::getColumnClass($type, $info), }; } @@ -143,7 +148,12 @@ protected function getColumnClass(string $type, array $info = []): string protected function isType(string $type): bool { return match ($type) { - PgsqlColumnType::RANGE => true, + PgsqlColumnType::INT4RANGE, + PgsqlColumnType::INT8RANGE, + PgsqlColumnType::NUMRANGE, + PgsqlColumnType::TSRANGE, + PgsqlColumnType::TSTZRANGE, + PgsqlColumnType::DATERANGE => true, default => parent::isType($type), }; } diff --git a/src/Column/DateRangeColumn.php b/src/Column/DateRangeColumn.php new file mode 100644 index 000000000..b85666ed9 --- /dev/null +++ b/src/Column/DateRangeColumn.php @@ -0,0 +1,30 @@ +getBoundColumn(); + return new DateRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/Int4RangeColumn.php b/src/Column/Int4RangeColumn.php new file mode 100644 index 000000000..866c73d30 --- /dev/null +++ b/src/Column/Int4RangeColumn.php @@ -0,0 +1,29 @@ +getBoundColumn(); + return new Int4RangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/Int8RangeColumn.php b/src/Column/Int8RangeColumn.php new file mode 100644 index 000000000..465367f97 --- /dev/null +++ b/src/Column/Int8RangeColumn.php @@ -0,0 +1,29 @@ +getBoundColumn(); + return new Int8RangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/NumRangeColumn.php b/src/Column/NumRangeColumn.php new file mode 100644 index 000000000..f0610e9fb --- /dev/null +++ b/src/Column/NumRangeColumn.php @@ -0,0 +1,30 @@ +getBoundColumn(); + return new NumRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/RangeColumn.php b/src/Column/RangeColumn.php deleted file mode 100644 index d41404e05..000000000 --- a/src/Column/RangeColumn.php +++ /dev/null @@ -1,164 +0,0 @@ -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.', - ); - } - - $dbType = $this->getDbType(); - [$lower, $upper] = $value; - - $column = match ($dbType) { - 'int4range' => RangeBoundColumnFactory::int4(), - 'int8range' => RangeBoundColumnFactory::int8(), - 'numrange' => RangeBoundColumnFactory::num(), - 'tsrange' => RangeBoundColumnFactory::ts(), - 'tstzrange' => RangeBoundColumnFactory::tsTz(), - 'daterange' => RangeBoundColumnFactory::date(), - default => $this->throwNotSupportedDbType($dbType), - }; - - return '[' - . $this->dbTypecastBound($column->dbTypecast($lower)) - . ',' - . $this->dbTypecastBound($column->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\[|\()(?P[^,]*),(?P[^\)\]]*)(?P\)|\])$/', $value, $matches)) { - throw new NotSupportedException('Unsupported range format.'); - } - - $dbType = $this->getDbType(); - $lower = $matches['lower'] === '' ? null : trim($matches['lower'], '"'); - $upper = $matches['upper'] === '' ? null : trim($matches['upper'], '"'); - $includeLower = $matches['open'] === '['; - $includeUpper = $matches['close'] === ']'; - - switch ($dbType) { - case 'int4range': - $column = RangeBoundColumnFactory::int4(); - return new Int4RangeValue( - $column->phpTypecast($lower), - $column->phpTypecast($upper), - $includeLower, - $includeUpper, - ); - case 'int8range': - $column = RangeBoundColumnFactory::int8(); - return new Int8RangeValue( - $column->phpTypecast($lower), - $column->phpTypecast($upper), - $includeLower, - $includeUpper, - ); - case 'numrange': - $column = RangeBoundColumnFactory::num(); - return new NumRangeValue( - $column->phpTypecast($lower), - $column->phpTypecast($upper), - $includeLower, - $includeUpper, - ); - case 'tsrange': - $column = RangeBoundColumnFactory::ts(); - return new TsRangeValue( - $column->phpTypecast($lower), - $column->phpTypecast($upper), - $includeLower, - $includeUpper, - ); - case 'tstzrange': - $column = RangeBoundColumnFactory::tstz(); - return new TsTzRangeValue( - $column->phpTypecast($lower), - $column->phpTypecast($upper), - $includeLower, - $includeUpper, - ); - case 'daterange': - $column = RangeBoundColumnFactory::date(); - return new DateRangeValue( - $column->phpTypecast($lower), - $column->phpTypecast($upper), - $includeLower, - $includeUpper, - ); - default: - $this->throwNotSupportedDbType($dbType); - } - } - - /** - * @throws NotSupportedException - */ - private function dbTypecastBound(ExpressionInterface|float|int|string|null $value): string - { - return match (gettype($value)) { - 'NULL' => 'NULL', - 'double', - 'integer' => (string) $value, - 'string' => $value, - 'object' => throw new NotSupportedException('Range bound as expression is not supported.'), - }; - } - - /** - * @throws NotSupportedException - */ - private function throwNotSupportedDbType(?string $dbType): never - { - throw new NotSupportedException( - sprintf( - 'Unsupported range type: %s', - $dbType ?? 'unknown', - ), - ); - } -} diff --git a/src/Column/TsRangeColumn.php b/src/Column/TsRangeColumn.php new file mode 100644 index 000000000..e0595ffe6 --- /dev/null +++ b/src/Column/TsRangeColumn.php @@ -0,0 +1,30 @@ +getBoundColumn(); + return new TsRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/TsTzRangeColumn.php b/src/Column/TsTzRangeColumn.php new file mode 100644 index 000000000..a60fa99a5 --- /dev/null +++ b/src/Column/TsTzRangeColumn.php @@ -0,0 +1,30 @@ +getBoundColumn(); + return new TsTzRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Constant/PgsqlColumnType.php b/src/Constant/PgsqlColumnType.php index b6ce88c61..01aa6993b 100644 --- a/src/Constant/PgsqlColumnType.php +++ b/src/Constant/PgsqlColumnType.php @@ -6,5 +6,10 @@ final class PgsqlColumnType { - public const RANGE = 'range'; + public const INT4RANGE = 'int4range'; + public const INT8RANGE = 'int8range'; + public const NUMRANGE = 'numrange'; + public const TSRANGE = 'tsrange'; + public const TSTZRANGE = 'tstzrange'; + public const DATERANGE = 'daterange'; } diff --git a/src/Expression/Builder/AbstractRangeValueBuilder.php b/src/Expression/Builder/AbstractRangeValueBuilder.php new file mode 100644 index 000000000..41ba5e093 --- /dev/null +++ b/src/Expression/Builder/AbstractRangeValueBuilder.php @@ -0,0 +1,55 @@ + + */ +abstract class AbstractRangeValueBuilder implements ExpressionBuilderInterface +{ + public function __construct( + private readonly QueryBuilderInterface $queryBuilder, + ) {} + + abstract protected function getBoundColumn(): ColumnInterface; + + /** + * @throws NotSupportedException + */ + final protected function buildRange( + mixed $lower, + mixed $upper, + bool $includeLower, + bool $includeUpper, + array &$params, + ): string { + $column = $this->getBoundColumn(); + return ($includeLower ? '[' : '(') + . $this->prepareBoundValue($lower, $column, $params) + . ', ' + . $this->prepareBoundValue($upper, $column, $params) + . ($includeUpper ? ']' : ')'); + } + + /** + * @throws NotSupportedException + */ + private function prepareBoundValue(mixed $value, ColumnInterface $boundColumn, array &$params): string + { + $value = $boundColumn->dbTypecast($value); + if ($value === null) { + return ''; + } + + return $this->queryBuilder->buildValue($value, $params); + } +} diff --git a/src/Expression/Builder/DateRangeValueBuilder.php b/src/Expression/Builder/DateRangeValueBuilder.php new file mode 100644 index 000000000..c747898d5 --- /dev/null +++ b/src/Expression/Builder/DateRangeValueBuilder.php @@ -0,0 +1,34 @@ + + */ +final class DateRangeValueBuilder extends AbstractRangeValueBuilder +{ + public function build(ExpressionInterface $expression, array &$params = []): string + { + return $this->buildRange( + $expression->lower, + $expression->upper, + $expression->includeLower, + $expression->includeUpper, + $params, + ); + } + + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::date(); + } +} diff --git a/src/Expression/Builder/Int4RangeValueBuilder.php b/src/Expression/Builder/Int4RangeValueBuilder.php new file mode 100644 index 000000000..6cff68e72 --- /dev/null +++ b/src/Expression/Builder/Int4RangeValueBuilder.php @@ -0,0 +1,34 @@ + + */ +final class Int4RangeValueBuilder extends AbstractRangeValueBuilder +{ + public function build(ExpressionInterface $expression, array &$params = []): string + { + return $this->buildRange( + $expression->lower, + $expression->upper, + $expression->includeLower, + $expression->includeUpper, + $params, + ); + } + + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::int4(); + } +} diff --git a/src/Expression/Builder/Int8RangeValueBuilder.php b/src/Expression/Builder/Int8RangeValueBuilder.php new file mode 100644 index 000000000..5f5edb5bb --- /dev/null +++ b/src/Expression/Builder/Int8RangeValueBuilder.php @@ -0,0 +1,34 @@ + + */ +final class Int8RangeValueBuilder extends AbstractRangeValueBuilder +{ + public function build(ExpressionInterface $expression, array &$params = []): string + { + return $this->buildRange( + $expression->lower, + $expression->upper, + $expression->includeLower, + $expression->includeUpper, + $params, + ); + } + + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::int8(); + } +} diff --git a/src/Expression/Builder/NumRangeValueBuilder.php b/src/Expression/Builder/NumRangeValueBuilder.php new file mode 100644 index 000000000..4f89ef424 --- /dev/null +++ b/src/Expression/Builder/NumRangeValueBuilder.php @@ -0,0 +1,34 @@ + + */ +final class NumRangeValueBuilder extends AbstractRangeValueBuilder +{ + public function build(ExpressionInterface $expression, array &$params = []): string + { + return $this->buildRange( + $expression->lower, + $expression->upper, + $expression->includeLower, + $expression->includeUpper, + $params, + ); + } + + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::num(); + } +} diff --git a/src/Expression/Builder/RangeValueBuilder.php b/src/Expression/Builder/RangeValueBuilder.php deleted file mode 100644 index 1f3cf9e57..000000000 --- a/src/Expression/Builder/RangeValueBuilder.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -final class RangeValueBuilder implements ExpressionBuilderInterface -{ - public function __construct( - private readonly QueryBuilderInterface $queryBuilder, - ) {} - - public function build(ExpressionInterface $expression, array &$params = []): string - { - $column = match ($expression::class) { - Int4RangeValue::class => RangeBoundColumnFactory::int4(), - Int8RangeValue::class => RangeBoundColumnFactory::int8(), - NumRangeValue::class => RangeBoundColumnFactory::num(), - TsRangeValue::class => RangeBoundColumnFactory::ts(), - TsTzRangeValue::class => RangeBoundColumnFactory::tsTz(), - DateRangeValue::class => RangeBoundColumnFactory::date(), - }; - - return ($expression->includeLower ? '[' : '(') - . $this->queryBuilder->buildValue( - $column->dbTypecast($expression->lower), - $params, - ) - . ', ' - . $this->queryBuilder->buildValue( - $column->dbTypecast($expression->upper), - $params, - ) - . ($expression->includeUpper ? ']' : ')'); - } -} diff --git a/src/Expression/Builder/TsRangeValueBuilder.php b/src/Expression/Builder/TsRangeValueBuilder.php new file mode 100644 index 000000000..4327580b1 --- /dev/null +++ b/src/Expression/Builder/TsRangeValueBuilder.php @@ -0,0 +1,34 @@ + + */ +final class TsRangeValueBuilder extends AbstractRangeValueBuilder +{ + public function build(ExpressionInterface $expression, array &$params = []): string + { + return $this->buildRange( + $expression->lower, + $expression->upper, + $expression->includeLower, + $expression->includeUpper, + $params, + ); + } + + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::ts(); + } +} diff --git a/src/Expression/Builder/TsTzRangeValueBuilder.php b/src/Expression/Builder/TsTzRangeValueBuilder.php new file mode 100644 index 000000000..0dcaa3676 --- /dev/null +++ b/src/Expression/Builder/TsTzRangeValueBuilder.php @@ -0,0 +1,34 @@ + + */ +final class TsTzRangeValueBuilder extends AbstractRangeValueBuilder +{ + public function build(ExpressionInterface $expression, array &$params = []): string + { + return $this->buildRange( + $expression->lower, + $expression->upper, + $expression->includeLower, + $expression->includeUpper, + $params, + ); + } + + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::tsTz(); + } +} diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index ca5e02cea..1bb6d2222 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -13,8 +13,13 @@ use Yiisoft\Db\Pgsql\Column\BinaryColumn; use Yiisoft\Db\Pgsql\Column\BitColumn; use Yiisoft\Db\Pgsql\Column\BooleanColumn; +use Yiisoft\Db\Pgsql\Column\DateRangeColumn; +use Yiisoft\Db\Pgsql\Column\Int4RangeColumn; +use Yiisoft\Db\Pgsql\Column\Int8RangeColumn; use Yiisoft\Db\Pgsql\Column\IntegerColumn; -use Yiisoft\Db\Pgsql\Column\RangeColumn; +use Yiisoft\Db\Pgsql\Column\NumRangeColumn; +use Yiisoft\Db\Pgsql\Column\TsRangeColumn; +use Yiisoft\Db\Pgsql\Column\TsTzRangeColumn; use Yiisoft\Db\Schema\Column\DateTimeColumn; use Yiisoft\Db\Schema\Column\DoubleColumn; use Yiisoft\Db\Schema\Column\JsonColumn; @@ -192,12 +197,12 @@ public static function columns(): array notNull: false, ), ), - 'intrange_col' => new RangeColumn(dbType: 'int4range'), - 'bigintrange_col' => new RangeColumn(dbType: 'int8range'), - 'numrange_col' => new RangeColumn(dbType: 'numrange'), - 'daterange_col' => new RangeColumn(dbType: 'daterange'), - 'tsrange_col' => new RangeColumn(dbType: 'tsrange'), - 'tstzrange_col' => new RangeColumn(dbType: 'tstzrange'), + 'intrange_col' => new Int4RangeColumn(dbType: 'int4range'), + 'bigintrange_col' => new Int8RangeColumn(dbType: 'int8range'), + 'numrange_col' => new NumRangeColumn(dbType: 'numrange'), + 'daterange_col' => new DateRangeColumn(dbType: 'daterange'), + 'tsrange_col' => new TsRangeColumn(dbType: 'tsrange'), + 'tstzrange_col' => new TsTzRangeColumn(dbType: 'tstzrange'), ], 'tableName' => 'type', ], From 5ca6de2b18056c7015a5ac76b35779105303a045 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 15:16:15 +0300 Subject: [PATCH 10/37] fix --- src/Column/AbstractRangeColumn.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Column/AbstractRangeColumn.php b/src/Column/AbstractRangeColumn.php index e2e60cf9a..4e2f2b1aa 100644 --- a/src/Column/AbstractRangeColumn.php +++ b/src/Column/AbstractRangeColumn.php @@ -83,7 +83,7 @@ abstract protected function createRangeValue( private function prepareBoundValue(mixed $value): string { return match (gettype($value)) { - 'NULL' => 'NULL', + 'NULL' => '', 'double', 'integer' => (string) $value, 'string' => $value, From 4d7f787ab6f45ca49d85637b294404e8b16e7b8b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 17:50:32 +0300 Subject: [PATCH 11/37] replace `?float` to `int|float|null` --- src/Expression/NumRangeValue.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Expression/NumRangeValue.php b/src/Expression/NumRangeValue.php index f29a10bba..1f5d3f234 100644 --- a/src/Expression/NumRangeValue.php +++ b/src/Expression/NumRangeValue.php @@ -9,8 +9,8 @@ final class NumRangeValue implements ExpressionInterface { public function __construct( - public readonly ?float $lower = null, - public readonly ?float $upper = null, + public readonly int|float|null $lower = null, + public readonly int|float|null $upper = null, public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} From a63012e8be3f463b1829260bc8f489169820e5c4 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 14 Nov 2025 17:54:30 +0300 Subject: [PATCH 12/37] Update src/Column/AbstractRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/AbstractRangeColumn.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Column/AbstractRangeColumn.php b/src/Column/AbstractRangeColumn.php index 4e2f2b1aa..5a5d13a14 100644 --- a/src/Column/AbstractRangeColumn.php +++ b/src/Column/AbstractRangeColumn.php @@ -39,7 +39,7 @@ public function dbTypecast(mixed $value): mixed . $this->prepareBoundValue($boundColumn->dbTypecast($lower)) . ',' . $this->prepareBoundValue($boundColumn->dbTypecast($upper)) - . ')'; + . ']'; } public function phpTypecast(mixed $value): mixed From 1c7d231db29d73bb6aa980fa239f01376322c5c2 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 13:01:10 +0300 Subject: [PATCH 13/37] Improve + Tests --- src/Column/AbstractRangeColumn.php | 5 +- src/DQLQueryBuilder.php | 18 +++ .../Builder/AbstractRangeValueBuilder.php | 22 ++- .../Builder/DateRangeValueBuilder.php | 1 - .../Builder/Int4RangeValueBuilder.php | 1 - .../Builder/Int8RangeValueBuilder.php | 1 - .../Builder/NumRangeValueBuilder.php | 1 - .../Builder/TsRangeValueBuilder.php | 1 - .../Builder/TsTzRangeValueBuilder.php | 1 - tests/Column/DateRangeColumnTest.php | 123 ++++++++++++++ tests/Column/Int4RangeColumnTest.php | 89 +++++++++++ tests/Column/Int8RangeColumnTest.php | 89 +++++++++++ tests/Column/NumRangeColumnTest.php | 89 +++++++++++ tests/Column/TsRangeColumnTest.php | 150 ++++++++++++++++++ tests/Column/TsTzRangeColumnTest.php | 150 ++++++++++++++++++ 15 files changed, 721 insertions(+), 20 deletions(-) create mode 100644 tests/Column/DateRangeColumnTest.php create mode 100644 tests/Column/Int4RangeColumnTest.php create mode 100644 tests/Column/Int8RangeColumnTest.php create mode 100644 tests/Column/NumRangeColumnTest.php create mode 100644 tests/Column/TsRangeColumnTest.php create mode 100644 tests/Column/TsTzRangeColumnTest.php diff --git a/src/Column/AbstractRangeColumn.php b/src/Column/AbstractRangeColumn.php index 5a5d13a14..85a242667 100644 --- a/src/Column/AbstractRangeColumn.php +++ b/src/Column/AbstractRangeColumn.php @@ -12,13 +12,16 @@ 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 || $value instanceof ExpressionInterface) { + if ($value === null + || is_string($value) + || $value instanceof ExpressionInterface) { return $value; } diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index dce9aeccf..23a16145b 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -17,6 +17,18 @@ use Yiisoft\Db\Pgsql\Builder\LikeBuilder; use Yiisoft\Db\Pgsql\Builder\StructuredValueBuilder; use Yiisoft\Db\Pgsql\Builder\JsonValueBuilder; +use Yiisoft\Db\Pgsql\Expression\Builder\DateRangeValueBuilder; +use Yiisoft\Db\Pgsql\Expression\Builder\Int4RangeValueBuilder; +use Yiisoft\Db\Pgsql\Expression\Builder\Int8RangeValueBuilder; +use Yiisoft\Db\Pgsql\Expression\Builder\NumRangeValueBuilder; +use Yiisoft\Db\Pgsql\Expression\Builder\TsRangeValueBuilder; +use Yiisoft\Db\Pgsql\Expression\Builder\TsTzRangeValueBuilder; +use Yiisoft\Db\Pgsql\Expression\DateRangeValue; +use Yiisoft\Db\Pgsql\Expression\Int4RangeValue; +use Yiisoft\Db\Pgsql\Expression\Int8RangeValue; +use Yiisoft\Db\Pgsql\Expression\NumRangeValue; +use Yiisoft\Db\Pgsql\Expression\TsRangeValue; +use Yiisoft\Db\Pgsql\Expression\TsTzRangeValue; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; use Yiisoft\Db\QueryBuilder\Condition\Like; use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlaps; @@ -41,6 +53,12 @@ protected function defaultExpressionBuilders(): array NotLike::class => LikeBuilder::class, CaseX::class => CaseXBuilder::class, ArrayMerge::class => ArrayMergeBuilder::class, + DateRangeValue::class => DateRangeValueBuilder::class, + Int4RangeValue::class => Int4RangeValueBuilder::class, + Int8RangeValue::class => Int8RangeValueBuilder::class, + NumRangeValue::class => NumRangeValueBuilder::class, + TsRangeValue::class => TsRangeValueBuilder::class, + TsTzRangeValue::class => TsTzRangeValueBuilder::class, ]; } } diff --git a/src/Expression/Builder/AbstractRangeValueBuilder.php b/src/Expression/Builder/AbstractRangeValueBuilder.php index 41ba5e093..210ff6903 100644 --- a/src/Expression/Builder/AbstractRangeValueBuilder.php +++ b/src/Expression/Builder/AbstractRangeValueBuilder.php @@ -7,7 +7,6 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; -use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; use Yiisoft\Db\Schema\Column\ColumnInterface; /** @@ -16,10 +15,6 @@ */ abstract class AbstractRangeValueBuilder implements ExpressionBuilderInterface { - public function __construct( - private readonly QueryBuilderInterface $queryBuilder, - ) {} - abstract protected function getBoundColumn(): ColumnInterface; /** @@ -30,26 +25,27 @@ final protected function buildRange( mixed $upper, bool $includeLower, bool $includeUpper, - array &$params, ): string { $column = $this->getBoundColumn(); - return ($includeLower ? '[' : '(') - . $this->prepareBoundValue($lower, $column, $params) - . ', ' - . $this->prepareBoundValue($upper, $column, $params) - . ($includeUpper ? ']' : ')'); + return '\'' + . ($includeLower ? '[' : '(') + . $this->prepareBoundValue($lower, $column) + . ',' + . $this->prepareBoundValue($upper, $column) + . ($includeUpper ? ']' : ')') + . '\''; } /** * @throws NotSupportedException */ - private function prepareBoundValue(mixed $value, ColumnInterface $boundColumn, array &$params): string + private function prepareBoundValue(mixed $value, ColumnInterface $boundColumn): string { $value = $boundColumn->dbTypecast($value); if ($value === null) { return ''; } - return $this->queryBuilder->buildValue($value, $params); + return (string) $value; } } diff --git a/src/Expression/Builder/DateRangeValueBuilder.php b/src/Expression/Builder/DateRangeValueBuilder.php index c747898d5..a31ef269e 100644 --- a/src/Expression/Builder/DateRangeValueBuilder.php +++ b/src/Expression/Builder/DateRangeValueBuilder.php @@ -23,7 +23,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $expression->upper, $expression->includeLower, $expression->includeUpper, - $params, ); } diff --git a/src/Expression/Builder/Int4RangeValueBuilder.php b/src/Expression/Builder/Int4RangeValueBuilder.php index 6cff68e72..372394307 100644 --- a/src/Expression/Builder/Int4RangeValueBuilder.php +++ b/src/Expression/Builder/Int4RangeValueBuilder.php @@ -23,7 +23,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $expression->upper, $expression->includeLower, $expression->includeUpper, - $params, ); } diff --git a/src/Expression/Builder/Int8RangeValueBuilder.php b/src/Expression/Builder/Int8RangeValueBuilder.php index 5f5edb5bb..49ba45da9 100644 --- a/src/Expression/Builder/Int8RangeValueBuilder.php +++ b/src/Expression/Builder/Int8RangeValueBuilder.php @@ -23,7 +23,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $expression->upper, $expression->includeLower, $expression->includeUpper, - $params, ); } diff --git a/src/Expression/Builder/NumRangeValueBuilder.php b/src/Expression/Builder/NumRangeValueBuilder.php index 4f89ef424..fb90a116c 100644 --- a/src/Expression/Builder/NumRangeValueBuilder.php +++ b/src/Expression/Builder/NumRangeValueBuilder.php @@ -23,7 +23,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $expression->upper, $expression->includeLower, $expression->includeUpper, - $params, ); } diff --git a/src/Expression/Builder/TsRangeValueBuilder.php b/src/Expression/Builder/TsRangeValueBuilder.php index 4327580b1..bbb94c4b0 100644 --- a/src/Expression/Builder/TsRangeValueBuilder.php +++ b/src/Expression/Builder/TsRangeValueBuilder.php @@ -23,7 +23,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $expression->upper, $expression->includeLower, $expression->includeUpper, - $params, ); } diff --git a/src/Expression/Builder/TsTzRangeValueBuilder.php b/src/Expression/Builder/TsTzRangeValueBuilder.php index 0dcaa3676..4a4a78362 100644 --- a/src/Expression/Builder/TsTzRangeValueBuilder.php +++ b/src/Expression/Builder/TsTzRangeValueBuilder.php @@ -23,7 +23,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $expression->upper, $expression->includeLower, $expression->includeUpper, - $params, ); } diff --git a/tests/Column/DateRangeColumnTest.php b/tests/Column/DateRangeColumnTest.php new file mode 100644 index 000000000..6663310c2 --- /dev/null +++ b/tests/Column/DateRangeColumnTest.php @@ -0,0 +1,123 @@ +createConnection(['[2024-01-01,2024-01-10)', '(2024-01-20,2024-01-30]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ) + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/Int4RangeColumnTest.php b/tests/Column/Int4RangeColumnTest.php new file mode 100644 index 000000000..ba1e79e8a --- /dev/null +++ b/tests/Column/Int4RangeColumnTest.php @@ -0,0 +1,89 @@ +createConnection(['[1,10)', '(20,30]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ) + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/Int8RangeColumnTest.php b/tests/Column/Int8RangeColumnTest.php new file mode 100644 index 000000000..9de26bd13 --- /dev/null +++ b/tests/Column/Int8RangeColumnTest.php @@ -0,0 +1,89 @@ +createConnection(['[1,10)', '(20,30]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ) + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/NumRangeColumnTest.php b/tests/Column/NumRangeColumnTest.php new file mode 100644 index 000000000..9fa5e702c --- /dev/null +++ b/tests/Column/NumRangeColumnTest.php @@ -0,0 +1,89 @@ +createConnection(['[1.5,10.5)', '(20.5,30.5]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ) + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php new file mode 100644 index 000000000..3b35c4cbd --- /dev/null +++ b/tests/Column/TsRangeColumnTest.php @@ -0,0 +1,150 @@ +createConnection(['["2024-01-01 10:00:00","2024-01-01 20:00:00")', '("2024-01-01 20:00:00","2024-01-01 23:00:00"]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $db->close(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ) + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/TsTzRangeColumnTest.php b/tests/Column/TsTzRangeColumnTest.php new file mode 100644 index 000000000..eb890554b --- /dev/null +++ b/tests/Column/TsTzRangeColumnTest.php @@ -0,0 +1,150 @@ +createConnection(['["2024-01-01 10:00:00+00","2024-01-01 20:00:00+00")', '("2024-01-01 20:00:00+00","2024-01-01 23:00:00+00"]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ) + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} From b421cb4d5ccd6a41992e66e8329953e9e78fc09e Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Sat, 15 Nov 2025 10:01:57 +0000 Subject: [PATCH 14/37] Apply PHP CS Fixer and Rector changes (CI) --- tests/Column/DateRangeColumnTest.php | 24 ++++++++-------- tests/Column/Int4RangeColumnTest.php | 2 +- tests/Column/Int8RangeColumnTest.php | 2 +- tests/Column/NumRangeColumnTest.php | 2 +- tests/Column/TsRangeColumnTest.php | 42 ++++++++++++++-------------- tests/Column/TsTzRangeColumnTest.php | 42 ++++++++++++++-------------- 6 files changed, 57 insertions(+), 57 deletions(-) diff --git a/tests/Column/DateRangeColumnTest.php b/tests/Column/DateRangeColumnTest.php index 6663310c2..7a74df886 100644 --- a/tests/Column/DateRangeColumnTest.php +++ b/tests/Column/DateRangeColumnTest.php @@ -25,8 +25,8 @@ public static function dataBase(): iterable '[2024-01-01,2024-01-06)', new DateRangeValue( new DateTimeImmutable('2024-01-01'), - new DateTimeImmutable('2024-01-05') - ) + new DateTimeImmutable('2024-01-05'), + ), ]; yield [ '[2024-01-02,2024-01-05)', @@ -34,31 +34,31 @@ public static function dataBase(): iterable new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-05'), false, - false - ) + false, + ), ]; yield ['[2024-01-01,)', '[2024-01-01,)']; yield [ '[2024-01-01,)', - new DateRangeValue(new DateTimeImmutable('2024-01-01'), null) + new DateRangeValue(new DateTimeImmutable('2024-01-01'), null), ]; yield [ '[2024-01-01,)', - new DateRangeValue(new DateTimeImmutable('2024-01-01'), null, true, false) + new DateRangeValue(new DateTimeImmutable('2024-01-01'), null, true, false), ]; yield ['(,2024-01-11)', '(,2024-01-10]']; yield [ '(,2024-01-11)', - new DateRangeValue(null, new DateTimeImmutable('2024-01-10'), false, true) + new DateRangeValue(null, new DateTimeImmutable('2024-01-10'), false, true), ]; yield ['(,)', '(,)']; yield [ '(,)', - new DateRangeValue(null, null) + new DateRangeValue(null, null), ]; yield [ '(,)', - new DateRangeValue(null, null, false, false) + new DateRangeValue(null, null, false, false), ]; yield ['empty', '(2024-01-07,2024-01-07)']; yield ['empty', 'empty']; @@ -68,8 +68,8 @@ public static function dataBase(): iterable new DateTimeImmutable('2024-01-10'), new DateTimeImmutable('2024-01-10'), false, - false - ) + false, + ), ]; } @@ -111,7 +111,7 @@ private function createConnection(array $values = []): Connection array_map( static fn(string $value): string => "('$value')", $values, - ) + ), ); $db ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") diff --git a/tests/Column/Int4RangeColumnTest.php b/tests/Column/Int4RangeColumnTest.php index ba1e79e8a..49befdfb0 100644 --- a/tests/Column/Int4RangeColumnTest.php +++ b/tests/Column/Int4RangeColumnTest.php @@ -77,7 +77,7 @@ private function createConnection(array $values = []): Connection array_map( static fn(string $value): string => "('$value')", $values, - ) + ), ); $db ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") diff --git a/tests/Column/Int8RangeColumnTest.php b/tests/Column/Int8RangeColumnTest.php index 9de26bd13..6454510c7 100644 --- a/tests/Column/Int8RangeColumnTest.php +++ b/tests/Column/Int8RangeColumnTest.php @@ -77,7 +77,7 @@ private function createConnection(array $values = []): Connection array_map( static fn(string $value): string => "('$value')", $values, - ) + ), ); $db ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") diff --git a/tests/Column/NumRangeColumnTest.php b/tests/Column/NumRangeColumnTest.php index 9fa5e702c..d14b526be 100644 --- a/tests/Column/NumRangeColumnTest.php +++ b/tests/Column/NumRangeColumnTest.php @@ -77,7 +77,7 @@ private function createConnection(array $values = []): Connection array_map( static fn(string $value): string => "('$value')", $values, - ) + ), ); $db ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") diff --git a/tests/Column/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php index 3b35c4cbd..69e0a1e22 100644 --- a/tests/Column/TsRangeColumnTest.php +++ b/tests/Column/TsRangeColumnTest.php @@ -19,26 +19,26 @@ public static function dataBase(): iterable { yield [ '["2024-01-01 10:00:00","2024-01-01 15:00:00"]', - '["2024-01-01 10:00:00","2024-01-01 15:00:00"]' + '["2024-01-01 10:00:00","2024-01-01 15:00:00"]', ]; yield [ '("2024-01-01 10:00:00","2024-01-01 15:00:00")', - '("2024-01-01 10:00:00","2024-01-01 15:00:00")' + '("2024-01-01 10:00:00","2024-01-01 15:00:00")', ]; yield [ '["2024-01-01 10:00:00","2024-01-01 15:00:00")', - '["2024-01-01 10:00:00","2024-01-01 15:00:00")' + '["2024-01-01 10:00:00","2024-01-01 15:00:00")', ]; yield [ '("2024-01-01 10:00:00","2024-01-01 15:00:00"]', - '("2024-01-01 10:00:00","2024-01-01 15:00:00"]' + '("2024-01-01 10:00:00","2024-01-01 15:00:00"]', ]; yield [ '["2024-01-01 10:00:00","2024-01-01 15:00:00"]', new TsRangeValue( new DateTimeImmutable('2024-01-01 10:00:00'), - new DateTimeImmutable('2024-01-01 15:00:00') - ) + new DateTimeImmutable('2024-01-01 15:00:00'), + ), ]; yield [ '("2024-01-01 10:00:00","2024-01-01 15:00:00")', @@ -46,48 +46,48 @@ public static function dataBase(): iterable new DateTimeImmutable('2024-01-01 10:00:00'), new DateTimeImmutable('2024-01-01 15:00:00'), false, - false - ) + false, + ), ]; yield [ '["2024-01-01 10:00:00",)', - '["2024-01-01 10:00:00",)' + '["2024-01-01 10:00:00",)', ]; yield [ '["2024-01-01 10:00:00",)', - new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), null) + new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), null), ]; yield [ '["2024-01-01 10:00:00",)', - new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), null, true, false) + new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), null, true, false), ]; yield [ '(,"2024-01-10 10:00:00"]', - '(,"2024-01-10 10:00:00"]' + '(,"2024-01-10 10:00:00"]', ]; yield [ '(,"2024-01-10 10:00:00"]', - new TsRangeValue(null, new DateTimeImmutable('2024-01-10 10:00:00'), false, true) + new TsRangeValue(null, new DateTimeImmutable('2024-01-10 10:00:00'), false, true), ]; yield [ '(,)', - '(,)' + '(,)', ]; yield [ '(,)', - new TsRangeValue(null, null) + new TsRangeValue(null, null), ]; yield [ '(,)', - new TsRangeValue(null, null, false, false) + new TsRangeValue(null, null, false, false), ]; yield [ 'empty', - '("2024-01-07 10:00:00","2024-01-07 10:00:00")' + '("2024-01-07 10:00:00","2024-01-07 10:00:00")', ]; yield [ 'empty', - 'empty' + 'empty', ]; yield [ 'empty', @@ -95,8 +95,8 @@ public static function dataBase(): iterable new DateTimeImmutable('2024-01-10 10:00:00'), new DateTimeImmutable('2024-01-10 10:00:00'), false, - false - ) + false, + ), ]; } @@ -138,7 +138,7 @@ private function createConnection(array $values = []): Connection array_map( static fn(string $value): string => "('$value')", $values, - ) + ), ); $db ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") diff --git a/tests/Column/TsTzRangeColumnTest.php b/tests/Column/TsTzRangeColumnTest.php index eb890554b..f42bf3976 100644 --- a/tests/Column/TsTzRangeColumnTest.php +++ b/tests/Column/TsTzRangeColumnTest.php @@ -19,26 +19,26 @@ public static function dataBase(): iterable { yield [ '["2024-01-01 10:00:00+00","2024-01-01 10:00:00+00"]', - '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+05"]' + '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+05"]', ]; yield [ '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")', - '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")' + '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")', ]; yield [ '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")', - '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")' + '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")', ]; yield [ '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"]', - '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"]' + '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"]', ]; yield [ '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"]', new TsTzRangeValue( new DateTimeImmutable('2024-01-01 10:00:00+00'), - new DateTimeImmutable('2024-01-01 15:00:00+00') - ) + new DateTimeImmutable('2024-01-01 15:00:00+00'), + ), ]; yield [ '("2024-01-01 10:00:00+00","2024-01-01 15:00:00+00")', @@ -46,48 +46,48 @@ public static function dataBase(): iterable new DateTimeImmutable('2024-01-01 10:00:00+00'), new DateTimeImmutable('2024-01-01 15:00:00+00'), false, - false - ) + false, + ), ]; yield [ '["2024-01-01 10:00:00+00",)', - '["2024-01-01 10:00:00+00",)' + '["2024-01-01 10:00:00+00",)', ]; yield [ '["2024-01-01 10:00:00+00",)', - new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), null) + new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), null), ]; yield [ '["2024-01-01 10:00:00+00",)', - new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), null, true, false) + new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), null, true, false), ]; yield [ '(,"2024-01-10 10:00:00+00"]', - '(,"2024-01-10 10:00:00+00"]' + '(,"2024-01-10 10:00:00+00"]', ]; yield [ '(,"2024-01-10 10:00:00+00"]', - new TsTzRangeValue(null, new DateTimeImmutable('2024-01-10 10:00:00+00'), false, true) + new TsTzRangeValue(null, new DateTimeImmutable('2024-01-10 10:00:00+00'), false, true), ]; yield [ '(,)', - '(,)' + '(,)', ]; yield [ '(,)', - new TsTzRangeValue(null, null) + new TsTzRangeValue(null, null), ]; yield [ '(,)', - new TsTzRangeValue(null, null, false, false) + new TsTzRangeValue(null, null, false, false), ]; yield [ 'empty', - '("2024-01-07 10:00:00+00","2024-01-07 10:00:00+00")' + '("2024-01-07 10:00:00+00","2024-01-07 10:00:00+00")', ]; yield [ 'empty', - 'empty' + 'empty', ]; yield [ 'empty', @@ -95,8 +95,8 @@ public static function dataBase(): iterable new DateTimeImmutable('2024-01-10 10:00:00+00'), new DateTimeImmutable('2024-01-10 10:00:00+00'), false, - false - ) + false, + ), ]; } @@ -138,7 +138,7 @@ private function createConnection(array $values = []): Connection array_map( static fn(string $value): string => "('$value')", $values, - ) + ), ); $db ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") From 5b08a04362a7625479a78dd16834eac00923ebd1 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 13:04:13 +0300 Subject: [PATCH 15/37] fix namespaces --- tests/Column/DateRangeColumnTest.php | 2 +- tests/Column/Int4RangeColumnTest.php | 2 +- tests/Column/Int8RangeColumnTest.php | 2 +- tests/Column/NumRangeColumnTest.php | 2 +- tests/Column/TsRangeColumnTest.php | 2 +- tests/Column/TsTzRangeColumnTest.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Column/DateRangeColumnTest.php b/tests/Column/DateRangeColumnTest.php index 7a74df886..88301d42d 100644 --- a/tests/Column/DateRangeColumnTest.php +++ b/tests/Column/DateRangeColumnTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Column; +namespace Yiisoft\Db\Pgsql\Tests\Column; use DateTimeImmutable; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Column/Int4RangeColumnTest.php b/tests/Column/Int4RangeColumnTest.php index 49befdfb0..a5a37171e 100644 --- a/tests/Column/Int4RangeColumnTest.php +++ b/tests/Column/Int4RangeColumnTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Column; +namespace Yiisoft\Db\Pgsql\Tests\Column; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Column/Int8RangeColumnTest.php b/tests/Column/Int8RangeColumnTest.php index 6454510c7..c7ec1395e 100644 --- a/tests/Column/Int8RangeColumnTest.php +++ b/tests/Column/Int8RangeColumnTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Column; +namespace Yiisoft\Db\Pgsql\Tests\Column; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Column/NumRangeColumnTest.php b/tests/Column/NumRangeColumnTest.php index d14b526be..070d7ff03 100644 --- a/tests/Column/NumRangeColumnTest.php +++ b/tests/Column/NumRangeColumnTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Column; +namespace Yiisoft\Db\Pgsql\Tests\Column; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Column/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php index 69e0a1e22..a79f77e00 100644 --- a/tests/Column/TsRangeColumnTest.php +++ b/tests/Column/TsRangeColumnTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Column; +namespace Yiisoft\Db\Pgsql\Tests\Column; use DateTimeImmutable; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Column/TsTzRangeColumnTest.php b/tests/Column/TsTzRangeColumnTest.php index f42bf3976..f7ad73bbe 100644 --- a/tests/Column/TsTzRangeColumnTest.php +++ b/tests/Column/TsTzRangeColumnTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Column; +namespace Yiisoft\Db\Pgsql\Tests\Column; use DateTimeImmutable; use PHPUnit\Framework\Attributes\DataProvider; From 67d79300a76ebc647352041b0e0934f16c461ae3 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 14:32:44 +0300 Subject: [PATCH 16/37] improve tests --- tests/ArrayValueBuilderTest.php | 2 ++ tests/JsonValueBuilderTest.php | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/tests/ArrayValueBuilderTest.php b/tests/ArrayValueBuilderTest.php index e0c992e85..c64071a7f 100644 --- a/tests/ArrayValueBuilderTest.php +++ b/tests/ArrayValueBuilderTest.php @@ -178,5 +178,7 @@ public function testBuild( $this->assertSame($expected, $builder->build($expression, $params)); Assert::arraysEquals($expectedParams, $params); + + $db->close(); } } diff --git a/tests/JsonValueBuilderTest.php b/tests/JsonValueBuilderTest.php index b54c6569b..b88b6b6be 100644 --- a/tests/JsonValueBuilderTest.php +++ b/tests/JsonValueBuilderTest.php @@ -59,6 +59,8 @@ public function testBuild(mixed $value, string $expected): void $this->assertSame(':qp0', $builder->build($expression, $params)); $this->assertEquals([':qp0' => new Param($expected, DataType::STRING)], $params); + + $db->close(); } public function testBuildArrayValue(): void @@ -78,6 +80,8 @@ public function testBuildArrayValue(): void $this->assertSame('array_to_json(ARRAY[1,2,3]::int[])::jsonb', $builder->build($expression, $params)); $this->assertSame([], $params); + + $db->close(); } public function testBuildNull(): void @@ -91,6 +95,8 @@ public function testBuildNull(): void $this->assertSame('NULL', $builder->build($expression, $params)); $this->assertSame([], $params); + + $db->close(); } public function testBuildQueryExpression(): void @@ -109,6 +115,8 @@ public function testBuildQueryExpression(): void $this->assertSame('(SELECT "json_field" FROM "json_table")::jsonb', $builder->build($expression, $params)); $this->assertSame([], $params); + + $db->close(); } public function testBuildWithType(): void @@ -122,5 +130,7 @@ public function testBuildWithType(): void $this->assertSame(':qp0::jsonb', $builder->build($expression, $params)); $this->assertEquals([':qp0' => new Param('[1,2,3]', DataType::STRING)], $params); + + $db->close(); } } From 2ef31f3073624a2a7f179b8943744dce989fb35f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 15:02:59 +0300 Subject: [PATCH 17/37] multirange --- CHANGELOG.md | 1 + src/Column/AbstractMultiRangeColumn.php | 72 +++++++++ src/Column/ColumnBuilder.php | 30 ++++ src/Column/ColumnFactory.php | 20 ++- src/Column/DateMultiRangeColumn.php | 15 ++ src/Column/Int4MultiRangeColumn.php | 15 ++ src/Column/Int8MultiRangeColumn.php | 15 ++ src/Column/NumMultiRangeColumn.php | 15 ++ src/Column/TsMultiRangeColumn.php | 15 ++ src/Column/TsTzMultiRangeColumn.php | 15 ++ src/Constant/PgsqlColumnType.php | 6 + src/DQLQueryBuilder.php | 3 + .../Builder/MultiRangeValueBuilder.php | 37 +++++ src/Expression/MultiRangeValue.php | 22 +++ tests/Column/DateMultiRangeColumnTest.php | 151 ++++++++++++++++++ tests/Column/Int4MultiRangeColumnTest.php | 99 ++++++++++++ tests/Column/Int8MultiRangeColumnTest.php | 99 ++++++++++++ tests/Column/NumMultiRangeColumnTest.php | 99 ++++++++++++ tests/Column/TsMultiRangeColumnTest.php | 126 +++++++++++++++ tests/Column/TsTzMultiRangeColumnTest.php | 126 +++++++++++++++ tests/Support/Fixture/pgsql14.sql | 9 -- tests/Support/TestTrait.php | 11 +- 22 files changed, 990 insertions(+), 11 deletions(-) create mode 100644 src/Column/AbstractMultiRangeColumn.php create mode 100644 src/Column/DateMultiRangeColumn.php create mode 100644 src/Column/Int4MultiRangeColumn.php create mode 100644 src/Column/Int8MultiRangeColumn.php create mode 100644 src/Column/NumMultiRangeColumn.php create mode 100644 src/Column/TsMultiRangeColumn.php create mode 100644 src/Column/TsTzMultiRangeColumn.php create mode 100644 src/Expression/Builder/MultiRangeValueBuilder.php create mode 100644 src/Expression/MultiRangeValue.php create mode 100644 tests/Column/DateMultiRangeColumnTest.php create mode 100644 tests/Column/Int4MultiRangeColumnTest.php create mode 100644 tests/Column/Int8MultiRangeColumnTest.php create mode 100644 tests/Column/NumMultiRangeColumnTest.php create mode 100644 tests/Column/TsMultiRangeColumnTest.php create mode 100644 tests/Column/TsTzMultiRangeColumnTest.php delete mode 100644 tests/Support/Fixture/pgsql14.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 4511e5b9f..2c74183b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Column/AbstractMultiRangeColumn.php b/src/Column/AbstractMultiRangeColumn.php new file mode 100644 index 000000000..d800810df --- /dev/null +++ b/src/Column/AbstractMultiRangeColumn.php @@ -0,0 +1,72 @@ +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_all('/^{(([\[\(][^,]*,[^\)\]]*[\)\]]),?)+}$/U', $value, $matches)) { + throw new NotSupportedException('Unsupported multirange format'); + } + + $rangeColumn = $this->getRangeColumn(); + return array_map( + static fn(string $range): mixed => $rangeColumn->phpTypecast($range), + $matches[1], + ); + } + + abstract protected function getRangeColumn(): ColumnInterface; +} diff --git a/src/Column/ColumnBuilder.php b/src/Column/ColumnBuilder.php index 292aa8202..85daba878 100644 --- a/src/Column/ColumnBuilder.php +++ b/src/Column/ColumnBuilder.php @@ -110,4 +110,34 @@ 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(); + } } diff --git a/src/Column/ColumnFactory.php b/src/Column/ColumnFactory.php index 4cc597bab..4664f1059 100644 --- a/src/Column/ColumnFactory.php +++ b/src/Column/ColumnFactory.php @@ -97,6 +97,12 @@ final class ColumnFactory extends AbstractColumnFactory '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 @@ -141,6 +147,12 @@ protected function getColumnClass(string $type, array $info = []): string 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), }; } @@ -153,7 +165,13 @@ protected function isType(string $type): bool PgsqlColumnType::NUMRANGE, PgsqlColumnType::TSRANGE, PgsqlColumnType::TSTZRANGE, - PgsqlColumnType::DATERANGE => true, + PgsqlColumnType::DATERANGE, + PgsqlColumnType::INT4MULTIRANGE, + PgsqlColumnType::INT8MULTIRANGE, + PgsqlColumnType::NUMMULTIRANGE, + PgsqlColumnType::TSMULTIRANGE, + PgsqlColumnType::TSTZMULTIRANGE, + PgsqlColumnType::DATEMULTIRANGE => true, default => parent::isType($type), }; } diff --git a/src/Column/DateMultiRangeColumn.php b/src/Column/DateMultiRangeColumn.php new file mode 100644 index 000000000..ebf284272 --- /dev/null +++ b/src/Column/DateMultiRangeColumn.php @@ -0,0 +1,15 @@ + NumRangeValueBuilder::class, TsRangeValue::class => TsRangeValueBuilder::class, TsTzRangeValue::class => TsTzRangeValueBuilder::class, + MultiRangeValue::class => MultiRangeValueBuilder::class, ]; } } diff --git a/src/Expression/Builder/MultiRangeValueBuilder.php b/src/Expression/Builder/MultiRangeValueBuilder.php new file mode 100644 index 000000000..f69d94c61 --- /dev/null +++ b/src/Expression/Builder/MultiRangeValueBuilder.php @@ -0,0 +1,37 @@ + + */ +final class MultiRangeValueBuilder implements ExpressionBuilderInterface +{ + public function __construct( + private readonly QueryBuilderInterface $queryBuilder, + ) { + } + + public function build(ExpressionInterface $expression, array &$params = []): string + { + $ranges = array_map( + fn(string|ExpressionInterface $range): string => trim( + $range instanceof ExpressionInterface + ? $this->queryBuilder->prepareValue($range) + : $range, + '\'', + ), + $expression->ranges, + ); + return '\'{' . implode(',', $ranges) . '}\''; + } +} diff --git a/src/Expression/MultiRangeValue.php b/src/Expression/MultiRangeValue.php new file mode 100644 index 000000000..08af9fab2 --- /dev/null +++ b/src/Expression/MultiRangeValue.php @@ -0,0 +1,22 @@ + $ranges + */ + public readonly array $ranges; + + public function __construct( + string|ExpressionInterface ...$ranges + ) { + $this->ranges = $ranges; + } +} diff --git a/tests/Column/DateMultiRangeColumnTest.php b/tests/Column/DateMultiRangeColumnTest.php new file mode 100644 index 000000000..bc6c8124b --- /dev/null +++ b/tests/Column/DateMultiRangeColumnTest.php @@ -0,0 +1,151 @@ +createConnection(['{[2024-01-01,2024-01-10),[2024-01-20,2024-01-30]}', '{[2024-02-01,2024-02-10)}']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(minVersion: '14.0'); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ), + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/Int4MultiRangeColumnTest.php b/tests/Column/Int4MultiRangeColumnTest.php new file mode 100644 index 000000000..79565aeda --- /dev/null +++ b/tests/Column/Int4MultiRangeColumnTest.php @@ -0,0 +1,99 @@ +createConnection(['{[1,10),[20,30]}', '{[40,50)}']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(minVersion: '14.0'); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ), + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/Int8MultiRangeColumnTest.php b/tests/Column/Int8MultiRangeColumnTest.php new file mode 100644 index 000000000..3136fb875 --- /dev/null +++ b/tests/Column/Int8MultiRangeColumnTest.php @@ -0,0 +1,99 @@ +createConnection(['{[1,10),[20,30]}', '{[40,50)}']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(minVersion: '14.0'); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ), + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/NumMultiRangeColumnTest.php b/tests/Column/NumMultiRangeColumnTest.php new file mode 100644 index 000000000..7baf7970c --- /dev/null +++ b/tests/Column/NumMultiRangeColumnTest.php @@ -0,0 +1,99 @@ +createConnection(['{[1.5,10.5),[20.5,30.5]}', '{[40.5,50.5)}']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(minVersion: '14.0'); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ), + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/TsMultiRangeColumnTest.php b/tests/Column/TsMultiRangeColumnTest.php new file mode 100644 index 000000000..05d7c4ea3 --- /dev/null +++ b/tests/Column/TsMultiRangeColumnTest.php @@ -0,0 +1,126 @@ +createConnection(['{["2024-01-01 10:00:00","2024-01-01 20:00:00"),["2024-01-02 10:00:00","2024-01-02 20:00:00"]}', '{["2024-01-03 10:00:00","2024-01-03 20:00:00")}']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(minVersion: '14.0'); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ), + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Column/TsTzMultiRangeColumnTest.php b/tests/Column/TsTzMultiRangeColumnTest.php new file mode 100644 index 000000000..7bde86a8f --- /dev/null +++ b/tests/Column/TsTzMultiRangeColumnTest.php @@ -0,0 +1,126 @@ +createConnection(['{["2024-01-01 10:00:00+00","2024-01-01 20:00:00+00"),["2024-01-02 10:00:00+00","2024-01-02 20:00:00+00"]}', '{["2024-01-03 10:00:00+00","2024-01-03 20:00:00+00")}']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $db->close(); + + $this->assertIsArray($result); + $this->assertSame($expectedColumnValue, $result['col']); + } + + /** + * @psalm-param list $values + */ + private function createConnection(array $values = []): Connection + { + $db = $this->getConnection(minVersion: '14.0'); + + $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); + $db->createCommand( + <<execute(); + + if ($values !== []) { + $valuesClause = implode( + ', ', + array_map( + static fn(string $value): string => "('$value')", + $values, + ), + ); + $db + ->createCommand("INSERT INTO tbl_test (col) VALUES $valuesClause") + ->execute(); + } + + return $db; + } +} diff --git a/tests/Support/Fixture/pgsql14.sql b/tests/Support/Fixture/pgsql14.sql deleted file mode 100644 index 1a7fc0992..000000000 --- a/tests/Support/Fixture/pgsql14.sql +++ /dev/null @@ -1,9 +0,0 @@ -DROP TABLE IF EXISTS "table_with_multirange" CASCADE; - -CREATE TABLE "table_with_multirange" ( - id serial not null primary key, - int4multirange_col int4multirange, - int8multirange_col int8multirange, - nummultirange_col nummultirange, - datemultirange_col datemultirange -); diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index beca25178..bca6c834e 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -26,10 +26,19 @@ public static function setUpBeforeClass(): void $db->close(); } - protected function getConnection(bool $fixture = false): Connection + protected function getConnection(bool $fixture = false, ?string $minVersion = null): Connection { $db = new Connection($this->getDriver(), DbHelper::getSchemaCache()); + if ($minVersion !== null) { + $currentVersion = $db->getServerInfo()->getVersion(); + if (version_compare($currentVersion, $minVersion, '<')) { + $this->markTestSkipped( + "This test requires at least PostgreSQL version $minVersion. Current version is $currentVersion." + ); + } + } + if ($fixture) { DbHelper::loadFixture($db, __DIR__ . "/Fixture/$this->fixture"); } From 77bf0e8861166786ceff16555ef7950d41a23e63 Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:03:42 +0000 Subject: [PATCH 18/37] Apply PHP CS Fixer and Rector changes (CI) --- src/Column/AbstractMultiRangeColumn.php | 2 +- .../Builder/MultiRangeValueBuilder.php | 3 +- src/Expression/MultiRangeValue.php | 2 +- tests/Column/DateMultiRangeColumnTest.php | 36 +++++++++---------- tests/Column/Int4MultiRangeColumnTest.php | 4 +-- tests/Column/Int8MultiRangeColumnTest.php | 4 +-- tests/Column/NumMultiRangeColumnTest.php | 4 +-- tests/Column/TsMultiRangeColumnTest.php | 6 ++-- tests/Column/TsTzMultiRangeColumnTest.php | 6 ++-- tests/Support/TestTrait.php | 2 +- 10 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/Column/AbstractMultiRangeColumn.php b/src/Column/AbstractMultiRangeColumn.php index d800810df..c583a7642 100644 --- a/src/Column/AbstractMultiRangeColumn.php +++ b/src/Column/AbstractMultiRangeColumn.php @@ -63,7 +63,7 @@ public function phpTypecast(mixed $value): mixed $rangeColumn = $this->getRangeColumn(); return array_map( - static fn(string $range): mixed => $rangeColumn->phpTypecast($range), + $rangeColumn->phpTypecast(...), $matches[1], ); } diff --git a/src/Expression/Builder/MultiRangeValueBuilder.php b/src/Expression/Builder/MultiRangeValueBuilder.php index f69d94c61..885cd2f62 100644 --- a/src/Expression/Builder/MultiRangeValueBuilder.php +++ b/src/Expression/Builder/MultiRangeValueBuilder.php @@ -18,8 +18,7 @@ final class MultiRangeValueBuilder implements ExpressionBuilderInterface { public function __construct( private readonly QueryBuilderInterface $queryBuilder, - ) { - } + ) {} public function build(ExpressionInterface $expression, array &$params = []): string { diff --git a/src/Expression/MultiRangeValue.php b/src/Expression/MultiRangeValue.php index 08af9fab2..b1cce7aea 100644 --- a/src/Expression/MultiRangeValue.php +++ b/src/Expression/MultiRangeValue.php @@ -15,7 +15,7 @@ final class MultiRangeValue implements ExpressionInterface public readonly array $ranges; public function __construct( - string|ExpressionInterface ...$ranges + string|ExpressionInterface ...$ranges, ) { $this->ranges = $ranges; } diff --git a/tests/Column/DateMultiRangeColumnTest.php b/tests/Column/DateMultiRangeColumnTest.php index bc6c8124b..01d92d64c 100644 --- a/tests/Column/DateMultiRangeColumnTest.php +++ b/tests/Column/DateMultiRangeColumnTest.php @@ -20,84 +20,84 @@ public static function dataBase(): iterable { yield [ '{[2024-01-01,2024-01-06),[2024-02-01,2024-02-10)}', - '{[2024-01-01,2024-01-05],[2024-02-01,2024-02-10)}' + '{[2024-01-01,2024-01-05],[2024-02-01,2024-02-10)}', ]; yield [ '{[2024-01-02,2024-01-05),[2024-02-01,2024-02-10)}', - '{(2024-01-01,2024-01-05),[2024-02-01,2024-02-10)}' + '{(2024-01-01,2024-01-05),[2024-02-01,2024-02-10)}', ]; yield [ '{[2024-01-01,2024-01-05),[2024-02-01,2024-02-10)}', - '{[2024-01-01,2024-01-05),[2024-02-01,2024-02-10)}' + '{[2024-01-01,2024-01-05),[2024-02-01,2024-02-10)}', ]; yield [ '{[2024-01-02,2024-01-06),[2024-02-01,2024-02-10)}', - '{(2024-01-01,2024-01-05],[2024-02-01,2024-02-10)}' + '{(2024-01-01,2024-01-05],[2024-02-01,2024-02-10)}', ]; yield [ '{[2024-01-01,2024-01-06),[2024-03-01,2024-03-10)}', - '{[2024-01-01,2024-01-05],[2024-03-01,2024-03-10)}' + '{[2024-01-01,2024-01-05],[2024-03-01,2024-03-10)}', ]; yield [ '{[2024-01-01,)}', - '{[2024-01-01,)}' + '{[2024-01-01,)}', ]; yield [ '{(,2024-01-11)}', - '{(,2024-01-10]}' + '{(,2024-01-10]}', ]; yield [ '{(,)}', - '{(,)}' + '{(,)}', ]; yield [ '{}', - '{empty}' + '{empty}', ]; yield [ '{}', - '{}' + '{}', ]; yield [ '{[2024-01-01,2024-01-06),[2024-02-01,2024-02-05),[2024-03-01,2024-03-05)}', - '{[2024-01-01,2024-01-05],[2024-02-01,2024-02-05),[2024-03-01,2024-03-05)}' + '{[2024-01-01,2024-01-05],[2024-02-01,2024-02-05),[2024-03-01,2024-03-05)}', ]; yield [ '{[2024-01-01,2024-01-06),[2024-02-01,2024-02-05),[2024-03-01,2024-03-05)}', - ['[2024-01-01,2024-01-05]', '[2024-02-01,2024-02-05)', '[2024-03-01,2024-03-05)'] + ['[2024-01-01,2024-01-05]', '[2024-02-01,2024-02-05)', '[2024-03-01,2024-03-05)'], ]; yield [ '{}', - [] + [], ]; yield [ '{[2024-01-01,2024-01-08)}', [ new DateRangeValue(new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-05')), new DateRangeValue(new DateTimeImmutable('2024-01-02'), new DateTimeImmutable('2024-01-08'), false, false), - ] + ], ]; yield [ '{[2024-01-01,2024-01-04),[2024-02-01,2024-02-04)}', [ new DateRangeValue(new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-03')), new DateRangeValue(new DateTimeImmutable('2024-02-01'), new DateTimeImmutable('2024-02-03')), - ] + ], ]; yield [ '{[2024-01-01,2024-01-06),[2024-02-01,2024-02-05),[2024-03-01,2024-03-05)}', - new MultiRangeValue('[2024-01-01,2024-01-05]', '[2024-02-01,2024-02-05)', '[2024-03-01,2024-03-05)') + new MultiRangeValue('[2024-01-01,2024-01-05]', '[2024-02-01,2024-02-05)', '[2024-03-01,2024-03-05)'), ]; yield [ '{}', - new MultiRangeValue() + new MultiRangeValue(), ]; yield [ '{[2024-01-01,2024-01-08)}', new MultiRangeValue( new DateRangeValue(new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-05')), new DateRangeValue(new DateTimeImmutable('2024-01-02'), new DateTimeImmutable('2024-01-08'), false, false), - ) + ), ]; } diff --git a/tests/Column/Int4MultiRangeColumnTest.php b/tests/Column/Int4MultiRangeColumnTest.php index 79565aeda..0c7813236 100644 --- a/tests/Column/Int4MultiRangeColumnTest.php +++ b/tests/Column/Int4MultiRangeColumnTest.php @@ -35,7 +35,7 @@ public static function dataBase(): iterable [ new Int4RangeValue(1, 5), new Int4RangeValue(2, 8, false, false), - ] + ], ]; yield ['{[1,4),[7,10)}', [new Int4RangeValue(1, 3), new Int4RangeValue(7, 9)]]; yield ['{[1,6),[10,15),[20,25)}', new MultiRangeValue('[1,5]', '[10,15)', '[20,25)')]; @@ -45,7 +45,7 @@ public static function dataBase(): iterable new MultiRangeValue( new Int4RangeValue(1, 5), new Int4RangeValue(2, 8, false, false), - ) + ), ]; } diff --git a/tests/Column/Int8MultiRangeColumnTest.php b/tests/Column/Int8MultiRangeColumnTest.php index 3136fb875..afdca9766 100644 --- a/tests/Column/Int8MultiRangeColumnTest.php +++ b/tests/Column/Int8MultiRangeColumnTest.php @@ -35,7 +35,7 @@ public static function dataBase(): iterable [ new Int8RangeValue(1, 5), new Int8RangeValue(2, 8, false, false), - ] + ], ]; yield ['{[1,4),[7,10)}', [new Int8RangeValue(1, 3), new Int8RangeValue(7, 9)]]; yield ['{[1,6),[10,15),[20,25)}', new MultiRangeValue('[1,5]', '[10,15)', '[20,25)')]; @@ -45,7 +45,7 @@ public static function dataBase(): iterable new MultiRangeValue( new Int8RangeValue(1, 5), new Int8RangeValue(2, 8, false, false), - ) + ), ]; } diff --git a/tests/Column/NumMultiRangeColumnTest.php b/tests/Column/NumMultiRangeColumnTest.php index 7baf7970c..c1446c9e8 100644 --- a/tests/Column/NumMultiRangeColumnTest.php +++ b/tests/Column/NumMultiRangeColumnTest.php @@ -35,7 +35,7 @@ public static function dataBase(): iterable [ new NumRangeValue(1.5, 5.5), new NumRangeValue(2.5, 8.5, false, false), - ] + ], ]; yield ['{[1.5,3.5],[7.5,9.5]}', [new NumRangeValue(1.5, 3.5), new NumRangeValue(7.5, 9.5)]]; yield ['{[1.5,5.5],[10.5,15.5),[20.5,25.5)}', new MultiRangeValue('[1.5,5.5]', '[10.5,15.5)', '[20.5,25.5)')]; @@ -45,7 +45,7 @@ public static function dataBase(): iterable new MultiRangeValue( new NumRangeValue(1.5, 5.5), new NumRangeValue(2.5, 8.5, false, false), - ) + ), ]; } diff --git a/tests/Column/TsMultiRangeColumnTest.php b/tests/Column/TsMultiRangeColumnTest.php index 05d7c4ea3..e042fb83c 100644 --- a/tests/Column/TsMultiRangeColumnTest.php +++ b/tests/Column/TsMultiRangeColumnTest.php @@ -53,14 +53,14 @@ public static function dataBase(): iterable [ new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), new DateTimeImmutable('2024-01-01 15:00:00')), new TsRangeValue(new DateTimeImmutable('2024-01-02 10:00:00'), new DateTimeImmutable('2024-01-02 15:00:00'), false, false), - ] + ], ]; yield [ '{["2024-01-01 10:00:00","2024-01-01 13:00:00"],["2024-01-02 10:00:00","2024-01-02 13:00:00"]}', [ new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), new DateTimeImmutable('2024-01-01 13:00:00')), new TsRangeValue(new DateTimeImmutable('2024-01-02 10:00:00'), new DateTimeImmutable('2024-01-02 13:00:00')), - ] + ], ]; yield [ '{["2024-01-01 10:00:00","2024-01-01 15:00:00"],["2024-01-02 10:00:00","2024-01-02 15:00:00"),["2024-01-03 10:00:00","2024-01-03 15:00:00")}', @@ -72,7 +72,7 @@ public static function dataBase(): iterable new MultiRangeValue( new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), new DateTimeImmutable('2024-01-01 15:00:00')), new TsRangeValue(new DateTimeImmutable('2024-01-02 10:00:00'), new DateTimeImmutable('2024-01-02 15:00:00'), false, false), - ) + ), ]; } diff --git a/tests/Column/TsTzMultiRangeColumnTest.php b/tests/Column/TsTzMultiRangeColumnTest.php index 7bde86a8f..0a1cffac1 100644 --- a/tests/Column/TsTzMultiRangeColumnTest.php +++ b/tests/Column/TsTzMultiRangeColumnTest.php @@ -53,14 +53,14 @@ public static function dataBase(): iterable [ new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), new DateTimeImmutable('2024-01-01 15:00:00+00')), new TsTzRangeValue(new DateTimeImmutable('2024-01-02 10:00:00+00'), new DateTimeImmutable('2024-01-02 15:00:00+00'), false, false), - ] + ], ]; yield [ '{["2024-01-01 10:00:00+00","2024-01-01 13:00:00+00"],["2024-01-02 10:00:00+00","2024-01-02 13:00:00+00"]}', [ new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), new DateTimeImmutable('2024-01-01 13:00:00+00')), new TsTzRangeValue(new DateTimeImmutable('2024-01-02 10:00:00+00'), new DateTimeImmutable('2024-01-02 13:00:00+00')), - ] + ], ]; yield [ '{["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"],["2024-01-02 10:00:00+00","2024-01-02 15:00:00+00"),["2024-01-03 10:00:00+00","2024-01-03 15:00:00+00")}', @@ -72,7 +72,7 @@ public static function dataBase(): iterable new MultiRangeValue( new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), new DateTimeImmutable('2024-01-01 15:00:00+00')), new TsTzRangeValue(new DateTimeImmutable('2024-01-02 10:00:00+00'), new DateTimeImmutable('2024-01-02 15:00:00+00'), false, false), - ) + ), ]; } diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index bca6c834e..31289930c 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -34,7 +34,7 @@ protected function getConnection(bool $fixture = false, ?string $minVersion = nu $currentVersion = $db->getServerInfo()->getVersion(); if (version_compare($currentVersion, $minVersion, '<')) { $this->markTestSkipped( - "This test requires at least PostgreSQL version $minVersion. Current version is $currentVersion." + "This test requires at least PostgreSQL version $minVersion. Current version is $currentVersion.", ); } } From 63b5d07d22fbac190453b5714ad55bf85c60bf4d Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 23:20:35 +0300 Subject: [PATCH 19/37] improve --- tests/StructuredValueBuilderTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/StructuredValueBuilderTest.php b/tests/StructuredValueBuilderTest.php index 1e78fee26..f9c3e014f 100644 --- a/tests/StructuredValueBuilderTest.php +++ b/tests/StructuredValueBuilderTest.php @@ -146,5 +146,7 @@ public function testBuild( $this->assertSame($expected, $builder->build($expression, $params)); Assert::arraysEquals($expectedParams, $params); + + $db->close(); } } From 9c2ce0025da2ecef75cf6ddd861973f95b9f64a9 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 23:23:58 +0300 Subject: [PATCH 20/37] improve --- tests/Column/TsRangeColumnTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Column/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php index a79f77e00..fdbd1bef2 100644 --- a/tests/Column/TsRangeColumnTest.php +++ b/tests/Column/TsRangeColumnTest.php @@ -107,10 +107,10 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); - $db->close(); - $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + $db->close(); + $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } From 4155e8c57c9273173dbd840576c68c7db7838425 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 15 Nov 2025 23:36:40 +0300 Subject: [PATCH 21/37] improve --- tests/CommandTest.php | 6 +++++- tests/QueryBuilderTest.php | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index f23ab96d7..0fe69eb40 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -225,7 +225,11 @@ public function testInsertReturningPksUuid(): void public function testShowDatabases(): void { - $this->assertSame([self::getDatabaseName()], self::getDb()->createCommand()->showDatabases()); + $db = self::getDb(); + + $this->assertSame([self::getDatabaseName()], $db->createCommand()->showDatabases()); + + $db->close(); } #[DataProviderExternal(CommandProvider::class, 'createIndex')] diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 510c00489..d3ef46927 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -653,6 +653,8 @@ public function testArrayMergeWithTypeWithOrdering( $result = $db->select($arrayMerge)->scalar(); $this->assertSame($expectedResult, $result); + + $db->close(); } #[DataProviderExternal(QueryBuilderProvider::class, 'upsertWithMultiOperandFunctions')] From 0ec9cea286f0ba278d6ff67919273e9d5fa55aaa Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 16 Nov 2025 01:10:54 +0300 Subject: [PATCH 22/37] improve --- tests/CommandTest.php | 13 +++++++++---- tests/Provider/QueryBuilderProvider.php | 2 +- tests/QueryBuilderTest.php | 23 +++++++++-------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 0fe69eb40..e43e3c742 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\Db\Pgsql\Tests; use PHPUnit\Framework\Attributes\DataProviderExternal; +use Throwable; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Tests\Provider\CommandProvider; @@ -28,13 +29,17 @@ public function testAddDefaultValue(): void $command = $db->createCommand(); - $this->expectException(NotSupportedException::class); - $this->expectExceptionMessage( + $exception = null; + try { + $command->addDefaultValue('{{table}}', '{{name}}', 'column', 'value'); + } catch (Throwable $exception) {} + + $this->assertInstanceOf(NotSupportedException::class, $exception); + $this->assertSame( 'Yiisoft\Db\Pgsql\DDLQueryBuilder::addDefaultValue is not supported by PostgreSQL.', + $exception->getMessage(), ); - $command->addDefaultValue('{{table}}', '{{name}}', 'column', 'value'); - $db->close(); } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index bfd2ec6d5..d09de7a43 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -457,7 +457,7 @@ public static function buildColumnDefinition(): array ]; $db = self::getDb(); - $serverVersion = self::getDb()->getServerInfo()->getVersion(); + $serverVersion = $db->getServerInfo()->getVersion(); $db->close(); if (version_compare($serverVersion, '13', '<')) { diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index d3ef46927..99a5572d6 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\Attributes\TestWith; +use Throwable; use Yiisoft\Db\Constant\DataType; use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; use Yiisoft\Db\Exception\IntegrityException; @@ -55,8 +56,6 @@ public function testAddDefaultValue(): void ); $qb->addDefaultValue('T_constraints_1', 'CN_pk', 'C_default', 1); - - $db->close(); } #[DataProviderExternal(QueryBuilderProvider::class, 'alterColumn')] @@ -135,7 +134,6 @@ public function testBuildWithWhereExists(string $cond, string $expectedQuerySql) public function testCheckIntegrity(): void { $db = $this->getConnection(); - $qb = $db->getQueryBuilder(); $this->assertSame( @@ -162,20 +160,23 @@ public function testCheckIntegrityExecute(): void $db->createCommand()->checkIntegrity('public', 'item')->execute(); - $this->expectException(IntegrityException::class); - $this->expectExceptionMessage( + $exception = null; + try { + $command->execute(); + } catch (Throwable $exception) {} + + $this->assertInstanceOf(IntegrityException::class, $exception); + $this->assertSame( 'SQLSTATE[23503]: Foreign key violation: 7 ERROR: insert or update on table "item" violates foreign key constraint "item_category_id_fkey"', + $exception->getMessage(), ); - $command->execute(); - $db->close(); } public function testCreateTable(): void { $db = $this->getConnection(); - $qb = $db->getQueryBuilder(); $this->assertSame( @@ -212,7 +213,6 @@ public function testDelete(string $table, array|string $condition, string $expec public function testDropCommentFromColumn(): void { $db = $this->getConnection(true); - $qb = $db->getQueryBuilder(); $this->assertSame( @@ -228,7 +228,6 @@ public function testDropCommentFromColumn(): void public function testDropDefaultValue(): void { $db = $this->getConnection(true); - $qb = $db->getQueryBuilder(); $this->expectException(NotSupportedException::class); @@ -237,14 +236,11 @@ public function testDropDefaultValue(): void ); $qb->dropDefaultValue('T_constraints_1', 'CN_pk'); - - $db->close(); } public function testDropIndex(): void { $db = $this->getConnection(); - $qb = $db->getQueryBuilder(); $this->assertSame( @@ -369,7 +365,6 @@ public function testResetSequencePgsql12(): void $this->setFixture('pgsql12.sql'); $db = $this->getConnection(true); - $qb = $db->getQueryBuilder(); $this->assertSame( From 92b75404a4cc5e9e57ee198e97cfbc2ec382b27a Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:11:39 +0000 Subject: [PATCH 23/37] Apply PHP CS Fixer and Rector changes (CI) --- tests/CommandTest.php | 3 ++- tests/QueryBuilderTest.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index e43e3c742..987154863 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -32,7 +32,8 @@ public function testAddDefaultValue(): void $exception = null; try { $command->addDefaultValue('{{table}}', '{{name}}', 'column', 'value'); - } catch (Throwable $exception) {} + } catch (Throwable $exception) { + } $this->assertInstanceOf(NotSupportedException::class, $exception); $this->assertSame( diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 99a5572d6..fb4df9005 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -163,7 +163,8 @@ public function testCheckIntegrityExecute(): void $exception = null; try { $command->execute(); - } catch (Throwable $exception) {} + } catch (Throwable $exception) { + } $this->assertInstanceOf(IntegrityException::class, $exception); $this->assertSame( From e118367f4863c99bbf285ccb117c70f8f5817f4e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 16 Nov 2025 01:45:58 +0300 Subject: [PATCH 24/37] fix --- tests/QueryBuilderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 99a5572d6..118563d27 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -166,7 +166,7 @@ public function testCheckIntegrityExecute(): void } catch (Throwable $exception) {} $this->assertInstanceOf(IntegrityException::class, $exception); - $this->assertSame( + $this->assertStringStartsWith( 'SQLSTATE[23503]: Foreign key violation: 7 ERROR: insert or update on table "item" violates foreign key constraint "item_category_id_fkey"', $exception->getMessage(), ); From 01ea19d57be3ec7ba00ab977d1680e63ae14558e Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 16 Nov 2025 01:58:21 +0300 Subject: [PATCH 25/37] improve --- tests/QueryBuilderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index e19081f4d..c0ba76b3e 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -636,7 +636,7 @@ public function testArrayMergeWithTypeWithOrdering( [2, 1, 3], new ArrayValue([6, 5, 7]), $stringParam, - self::getDb()->select(new ArrayValue([10, 9])), + $db->select(new ArrayValue([10, 9])), ))->type($type)->ordered(); $params = []; From d25a435d387662fd7710a9c4e3cc74d706c578c6 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 16 Nov 2025 19:34:14 +0300 Subject: [PATCH 26/37] improve tests --- tests/Column/DateMultiRangeColumnTest.php | 6 +- tests/Column/DateRangeColumnTest.php | 5 +- tests/Column/Int4MultiRangeColumnTest.php | 6 +- tests/Column/Int4RangeColumnTest.php | 5 +- tests/Column/Int8MultiRangeColumnTest.php | 6 +- tests/Column/Int8RangeColumnTest.php | 5 +- tests/Column/NumMultiRangeColumnTest.php | 6 +- tests/Column/NumRangeColumnTest.php | 5 +- tests/Column/TsMultiRangeColumnTest.php | 6 +- tests/Column/TsRangeColumnTest.php | 5 +- tests/Column/TsTzMultiRangeColumnTest.php | 6 +- tests/Column/TsTzRangeColumnTest.php | 5 +- tests/CommandTest.php | 9 ++- tests/Support/TestTrait.php | 64 +++++---------------- tests/TestConnection.php | 69 +++++++++++++++++++++++ 15 files changed, 118 insertions(+), 90 deletions(-) create mode 100644 tests/TestConnection.php diff --git a/tests/Column/DateMultiRangeColumnTest.php b/tests/Column/DateMultiRangeColumnTest.php index 01d92d64c..939c03d74 100644 --- a/tests/Column/DateMultiRangeColumnTest.php +++ b/tests/Column/DateMultiRangeColumnTest.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Pgsql\Expression\DateRangeValue; use Yiisoft\Db\Pgsql\Expression\MultiRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class DateMultiRangeColumnTest extends TestCase { @@ -110,8 +111,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -121,7 +120,8 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(minVersion: '14.0'); + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('14.0'); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/DateRangeColumnTest.php b/tests/Column/DateRangeColumnTest.php index 88301d42d..07d5a8fb4 100644 --- a/tests/Column/DateRangeColumnTest.php +++ b/tests/Column/DateRangeColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Expression\DateRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class DateRangeColumnTest extends TestCase { @@ -82,8 +83,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -93,7 +92,7 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(); + $db = TestConnection::get(); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/Int4MultiRangeColumnTest.php b/tests/Column/Int4MultiRangeColumnTest.php index 0c7813236..6c577a90f 100644 --- a/tests/Column/Int4MultiRangeColumnTest.php +++ b/tests/Column/Int4MultiRangeColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Expression\Int4RangeValue; use Yiisoft\Db\Pgsql\Expression\MultiRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class Int4MultiRangeColumnTest extends TestCase { @@ -58,8 +59,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -69,7 +68,8 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(minVersion: '14.0'); + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('14.0'); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/Int4RangeColumnTest.php b/tests/Column/Int4RangeColumnTest.php index a5a37171e..3a58ab5e5 100644 --- a/tests/Column/Int4RangeColumnTest.php +++ b/tests/Column/Int4RangeColumnTest.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Expression\Int4RangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class Int4RangeColumnTest extends TestCase { @@ -48,8 +49,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -59,7 +58,7 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(); + $db = TestConnection::get(); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/Int8MultiRangeColumnTest.php b/tests/Column/Int8MultiRangeColumnTest.php index afdca9766..6929947f6 100644 --- a/tests/Column/Int8MultiRangeColumnTest.php +++ b/tests/Column/Int8MultiRangeColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Expression\Int8RangeValue; use Yiisoft\Db\Pgsql\Expression\MultiRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class Int8MultiRangeColumnTest extends TestCase { @@ -58,8 +59,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -69,7 +68,8 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(minVersion: '14.0'); + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('14.0'); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/Int8RangeColumnTest.php b/tests/Column/Int8RangeColumnTest.php index c7ec1395e..b2a2021bf 100644 --- a/tests/Column/Int8RangeColumnTest.php +++ b/tests/Column/Int8RangeColumnTest.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Expression\Int8RangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class Int8RangeColumnTest extends TestCase { @@ -48,8 +49,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -59,7 +58,7 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(); + $db = TestConnection::get(); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/NumMultiRangeColumnTest.php b/tests/Column/NumMultiRangeColumnTest.php index c1446c9e8..ceb2346d6 100644 --- a/tests/Column/NumMultiRangeColumnTest.php +++ b/tests/Column/NumMultiRangeColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Expression\MultiRangeValue; use Yiisoft\Db\Pgsql\Expression\NumRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class NumMultiRangeColumnTest extends TestCase { @@ -58,8 +59,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -69,7 +68,8 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(minVersion: '14.0'); + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('14.0'); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/NumRangeColumnTest.php b/tests/Column/NumRangeColumnTest.php index 070d7ff03..8544bb1f2 100644 --- a/tests/Column/NumRangeColumnTest.php +++ b/tests/Column/NumRangeColumnTest.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Expression\NumRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class NumRangeColumnTest extends TestCase { @@ -48,8 +49,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -59,7 +58,7 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(); + $db = TestConnection::get(); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/TsMultiRangeColumnTest.php b/tests/Column/TsMultiRangeColumnTest.php index e042fb83c..d2aa382e8 100644 --- a/tests/Column/TsMultiRangeColumnTest.php +++ b/tests/Column/TsMultiRangeColumnTest.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Pgsql\Expression\MultiRangeValue; use Yiisoft\Db\Pgsql\Expression\TsRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class TsMultiRangeColumnTest extends TestCase { @@ -85,8 +86,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -96,7 +95,8 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(minVersion: '14.0'); + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('14.0'); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php index fdbd1bef2..dcee5f403 100644 --- a/tests/Column/TsRangeColumnTest.php +++ b/tests/Column/TsRangeColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Expression\TsRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class TsRangeColumnTest extends TestCase { @@ -109,8 +110,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -120,7 +119,7 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(); + $db = TestConnection::get(); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/TsTzMultiRangeColumnTest.php b/tests/Column/TsTzMultiRangeColumnTest.php index 0a1cffac1..efaeeef72 100644 --- a/tests/Column/TsTzMultiRangeColumnTest.php +++ b/tests/Column/TsTzMultiRangeColumnTest.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Pgsql\Expression\MultiRangeValue; use Yiisoft\Db\Pgsql\Expression\TsTzRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class TsTzMultiRangeColumnTest extends TestCase { @@ -85,8 +86,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -96,7 +95,8 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(minVersion: '14.0'); + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('14.0'); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/Column/TsTzRangeColumnTest.php b/tests/Column/TsTzRangeColumnTest.php index f7ad73bbe..3bf12807c 100644 --- a/tests/Column/TsTzRangeColumnTest.php +++ b/tests/Column/TsTzRangeColumnTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Expression\TsTzRangeValue; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Pgsql\Tests\TestConnection; final class TsTzRangeColumnTest extends TestCase { @@ -109,8 +110,6 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); - $db->close(); - $this->assertIsArray($result); $this->assertSame($expectedColumnValue, $result['col']); } @@ -120,7 +119,7 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void */ private function createConnection(array $values = []): Connection { - $db = $this->getConnection(); + $db = TestConnection::get(); $db->createCommand('DROP TABLE IF EXISTS tbl_test')->execute(); $db->createCommand( diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 987154863..e0a2b4bb6 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -231,11 +231,14 @@ public function testInsertReturningPksUuid(): void public function testShowDatabases(): void { - $db = self::getDb(); + $db = TestConnection::get(); - $this->assertSame([self::getDatabaseName()], $db->createCommand()->showDatabases()); + $databases = $db->createCommand()->showDatabases(); - $db->close(); + $this->assertSame( + [TestConnection::databaseName()], + $databases, + ); } #[DataProviderExternal(CommandProvider::class, 'createIndex')] diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index 31289930c..6550e3827 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -7,6 +7,7 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Driver; use Yiisoft\Db\Pgsql\Dsn; +use Yiisoft\Db\Pgsql\Tests\TestConnection; use Yiisoft\Db\Tests\Support\DbHelper; use function preg_replace; @@ -26,18 +27,9 @@ public static function setUpBeforeClass(): void $db->close(); } - protected function getConnection(bool $fixture = false, ?string $minVersion = null): Connection + protected function getConnection(bool $fixture = false): Connection { - $db = new Connection($this->getDriver(), DbHelper::getSchemaCache()); - - if ($minVersion !== null) { - $currentVersion = $db->getServerInfo()->getVersion(); - if (version_compare($currentVersion, $minVersion, '<')) { - $this->markTestSkipped( - "This test requires at least PostgreSQL version $minVersion. Current version is $currentVersion.", - ); - } - } + $db = TestConnection::create($this->getDsn()); if ($fixture) { DbHelper::loadFixture($db, __DIR__ . "/Fixture/$this->fixture"); @@ -48,25 +40,13 @@ protected function getConnection(bool $fixture = false, ?string $minVersion = nu protected static function getDb(): Connection { - $dsn = (string) new Dsn( - host: self::getHost(), - databaseName: self::getDatabaseName(), - port: self::getPort(), - ); - $driver = new Driver($dsn, self::getUsername(), self::getPassword()); - $driver->charset('utf8'); - - return new Connection($driver, DbHelper::getSchemaCache()); + return TestConnection::create(); } protected function getDsn(): string { if ($this->dsn === '') { - $this->dsn = (string) new Dsn( - host: self::getHost(), - databaseName: self::getDatabaseName(), - port: self::getPort(), - ); + $this->dsn = TestConnection::dsn(); } return $this->dsn; @@ -94,34 +74,16 @@ protected function setFixture(string $fixture): void protected function getDriver(): Driver { - $driver = new Driver($this->getDsn(), self::getUsername(), self::getPassword()); - $driver->charset('utf8'); - - return $driver; - } - - private static function getDatabaseName(): string - { - return getenv('YII_PGSQL_DATABASE') ?: 'yiitest'; - } - - private static function getHost(): string - { - return getenv('YII_PGSQL_HOST') ?: '127.0.0.1'; + return TestConnection::createDriver(); } - private static function getPort(): string + protected function ensureMinPostgreSqlVersion(string $version): void { - return getenv('YII_PGSQL_PORT') ?: '5432'; - } - - private static function getUsername(): string - { - return getenv('YII_PGSQL_USER') ?: 'root'; - } - - private static function getPassword(): string - { - return getenv('YII_PGSQL_PASSWORD') ?: 'root'; + $currentVersion = TestConnection::get()->getServerInfo()->getVersion(); + if (version_compare($currentVersion, $version, '<')) { + $this->markTestSkipped( + "This test requires at least PostgreSQL version $version. Current version is $currentVersion.", + ); + } } } diff --git a/tests/TestConnection.php b/tests/TestConnection.php new file mode 100644 index 000000000..0d47b40f1 --- /dev/null +++ b/tests/TestConnection.php @@ -0,0 +1,69 @@ +getSchema()->refresh(); + return $db; + } + + public static function dsn(): string + { + return self::$dsn ??= (string) new Dsn( + host: self::host(), + databaseName: self::databaseName(), + port: self::port(), + ); + } + + public static function create(?string $dsn = null): Connection + { + return new Connection(self::createDriver($dsn), DbHelper::getSchemaCache()); + } + + public static function createDriver(?string $dsn = null): Driver + { + $driver = new Driver($dsn ?? self::dsn(), self::username(), self::password()); + $driver->charset('utf8'); + return $driver; + } + + public static function databaseName(): string + { + return getenv('YII_PGSQL_DATABASE') ?: 'yiitest'; + } + + private static function host(): string + { + return getenv('YII_PGSQL_HOST') ?: '127.0.0.1'; + } + + private static function port(): string + { + return getenv('YII_PGSQL_PORT') ?: '5432'; + } + + private static function username(): string + { + return getenv('YII_PGSQL_USER') ?: 'root'; + } + + private static function password(): string + { + return getenv('YII_PGSQL_PASSWORD') ?: 'root'; + } +} From 925d6a25ed77c5989a6976c40b7042b651d7cc4c Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:34:59 +0000 Subject: [PATCH 27/37] Apply PHP CS Fixer and Rector changes (CI) --- tests/Support/TestTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index 6550e3827..1f996d937 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -6,7 +6,6 @@ use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Driver; -use Yiisoft\Db\Pgsql\Dsn; use Yiisoft\Db\Pgsql\Tests\TestConnection; use Yiisoft\Db\Tests\Support\DbHelper; From 8cc3a4d79a8c2740fc104680a208056f2dcaa241 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:09:27 +0300 Subject: [PATCH 28/37] Fix multirange regexp + More tests --- src/Column/AbstractMultiRangeColumn.php | 6 +++-- tests/Column/DateMultiRangeColumnTest.php | 29 +++++++++++++++++++++++ tests/Column/DateRangeColumnTest.php | 27 +++++++++++++++++++++ tests/Column/Int4MultiRangeColumnTest.php | 29 +++++++++++++++++++++++ tests/Column/Int4RangeColumnTest.php | 27 +++++++++++++++++++++ tests/Column/Int8MultiRangeColumnTest.php | 29 +++++++++++++++++++++++ tests/Column/Int8RangeColumnTest.php | 27 +++++++++++++++++++++ tests/Column/NumMultiRangeColumnTest.php | 29 +++++++++++++++++++++++ tests/Column/NumRangeColumnTest.php | 27 +++++++++++++++++++++ tests/Column/TsMultiRangeColumnTest.php | 29 +++++++++++++++++++++++ tests/Column/TsRangeColumnTest.php | 27 +++++++++++++++++++++ tests/Column/TsTzMultiRangeColumnTest.php | 29 +++++++++++++++++++++++ tests/Column/TsTzRangeColumnTest.php | 27 +++++++++++++++++++++ 13 files changed, 340 insertions(+), 2 deletions(-) diff --git a/src/Column/AbstractMultiRangeColumn.php b/src/Column/AbstractMultiRangeColumn.php index c583a7642..bcfb76de9 100644 --- a/src/Column/AbstractMultiRangeColumn.php +++ b/src/Column/AbstractMultiRangeColumn.php @@ -57,14 +57,16 @@ public function phpTypecast(mixed $value): mixed return []; } - if (!preg_match_all('/^{(([\[\(][^,]*,[^\)\]]*[\)\]]),?)+}$/U', $value, $matches)) { + if (preg_match('/^{([\[\(][^,]*,[^\)\]]*[\)\]],?)+}$/', $value) !== 1) { throw new NotSupportedException('Unsupported multirange format'); } + preg_match_all('/[\[\(][^,]*,[^\)\]]*[\)\]]/', $value, $matches); + $rangeColumn = $this->getRangeColumn(); return array_map( $rangeColumn->phpTypecast(...), - $matches[1], + $matches[0], ); } diff --git a/tests/Column/DateMultiRangeColumnTest.php b/tests/Column/DateMultiRangeColumnTest.php index 939c03d74..8fbe943ee 100644 --- a/tests/Column/DateMultiRangeColumnTest.php +++ b/tests/Column/DateMultiRangeColumnTest.php @@ -115,6 +115,35 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [[], '{}']; + yield [ + [ + new DateRangeValue(new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-06'), true, false), + new DateRangeValue(new DateTimeImmutable('2024-01-10'), new DateTimeImmutable('2024-01-20'), true, false), + ], + '{[2024-01-01,2024-01-05],[2024-01-10,2024-01-20)}', + ]; + yield [ + [ + new DateRangeValue(null, new DateTimeImmutable('2024-01-06'), false, false), + new DateRangeValue(new DateTimeImmutable('2024-01-10'), null, true, false), + ], + '{[,2024-01-05],[2024-01-10,)}', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(array $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/DateRangeColumnTest.php b/tests/Column/DateRangeColumnTest.php index 07d5a8fb4..9ea3eb452 100644 --- a/tests/Column/DateRangeColumnTest.php +++ b/tests/Column/DateRangeColumnTest.php @@ -87,6 +87,33 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [null, 'empty']; + yield [ + new DateRangeValue(new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-06'), true, false), + '[2024-01-01,2024-01-05]', + ]; + yield [ + new DateRangeValue(null, new DateTimeImmutable('2024-01-06'), false, false), + '(,2024-01-05]', + ]; + yield [ + new DateRangeValue(new DateTimeImmutable('2024-01-06'), null, true, false), + '(2024-01-05,)', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(mixed $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/Int4MultiRangeColumnTest.php b/tests/Column/Int4MultiRangeColumnTest.php index 6c577a90f..4b6a607b6 100644 --- a/tests/Column/Int4MultiRangeColumnTest.php +++ b/tests/Column/Int4MultiRangeColumnTest.php @@ -63,6 +63,35 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [[], '{}']; + yield [ + [ + new Int4RangeValue(1, 6, true, false), + new Int4RangeValue(10, 20, true, false), + ], + '{[1,5],[10,20)}', + ]; + yield [ + [ + new Int4RangeValue(null, 6, false, false), + new Int4RangeValue(10, null, true, false), + ], + '{[,5],[10,)}', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(array $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/Int4RangeColumnTest.php b/tests/Column/Int4RangeColumnTest.php index 3a58ab5e5..3f2ece877 100644 --- a/tests/Column/Int4RangeColumnTest.php +++ b/tests/Column/Int4RangeColumnTest.php @@ -53,6 +53,33 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [null, 'empty']; + yield [ + new Int4RangeValue(1, 6, true, false), + '[1,5]', + ]; + yield [ + new Int4RangeValue(null, 6, false, false), + '(,5]', + ]; + yield [ + new Int4RangeValue(6, null, true, false), + '(5,)', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(mixed $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/Int8MultiRangeColumnTest.php b/tests/Column/Int8MultiRangeColumnTest.php index 6929947f6..09f1cef3e 100644 --- a/tests/Column/Int8MultiRangeColumnTest.php +++ b/tests/Column/Int8MultiRangeColumnTest.php @@ -63,6 +63,35 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [[], '{}']; + yield [ + [ + new Int8RangeValue(1, 6, true, false), + new Int8RangeValue(10, 20, true, false), + ], + '{[1,5],[10,20)}', + ]; + yield [ + [ + new Int8RangeValue(null, 6, false, false), + new Int8RangeValue(10, null, true, false), + ], + '{[,5],[10,)}', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(array $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/Int8RangeColumnTest.php b/tests/Column/Int8RangeColumnTest.php index b2a2021bf..efffb0567 100644 --- a/tests/Column/Int8RangeColumnTest.php +++ b/tests/Column/Int8RangeColumnTest.php @@ -53,6 +53,33 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [null, 'empty']; + yield [ + new Int8RangeValue(1, 6, true, false), + '[1,5]', + ]; + yield [ + new Int8RangeValue(null, 6, false, false), + '(,5]', + ]; + yield [ + new Int8RangeValue(6, null, true, false), + '(5,)', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(mixed $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/NumMultiRangeColumnTest.php b/tests/Column/NumMultiRangeColumnTest.php index ceb2346d6..1b187afaf 100644 --- a/tests/Column/NumMultiRangeColumnTest.php +++ b/tests/Column/NumMultiRangeColumnTest.php @@ -63,6 +63,35 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [[], '{}']; + yield [ + [ + new NumRangeValue(1.5, 5.5, true, true), + new NumRangeValue(10.5, 20.5, true, false), + ], + '{[1.5,5.5],[10.5,20.5)}', + ]; + yield [ + [ + new NumRangeValue(null, 5.5, false, true), + new NumRangeValue(10.5, null, true, false), + ], + '{[,5.5],[10.5,)}', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(array $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/NumRangeColumnTest.php b/tests/Column/NumRangeColumnTest.php index 8544bb1f2..f1b43885e 100644 --- a/tests/Column/NumRangeColumnTest.php +++ b/tests/Column/NumRangeColumnTest.php @@ -53,6 +53,33 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [null, 'empty']; + yield [ + new NumRangeValue(1.5, 5.5, true, true), + '[1.5,5.5]', + ]; + yield [ + new NumRangeValue(null, 5.5, false, true), + '(,5.5]', + ]; + yield [ + new NumRangeValue(5.5, null, false, false), + '(5.5,)', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(mixed $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/TsMultiRangeColumnTest.php b/tests/Column/TsMultiRangeColumnTest.php index d2aa382e8..3dff98d15 100644 --- a/tests/Column/TsMultiRangeColumnTest.php +++ b/tests/Column/TsMultiRangeColumnTest.php @@ -90,6 +90,35 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [[], '{}']; + yield [ + [ + new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), new DateTimeImmutable('2024-01-01 15:00:00'), true, true), + new TsRangeValue(new DateTimeImmutable('2024-01-02 10:00:00'), new DateTimeImmutable('2024-01-02 15:00:00'), true, false), + ], + '{["2024-01-01 10:00:00","2024-01-01 15:00:00"],["2024-01-02 10:00:00","2024-01-02 15:00:00")}', + ]; + yield [ + [ + new TsRangeValue(null, new DateTimeImmutable('2024-01-01 15:00:00'), false, true), + new TsRangeValue(new DateTimeImmutable('2024-01-02 10:00:00'), null, true, false), + ], + '{[,"2024-01-01 15:00:00"],["2024-01-02 10:00:00",)}', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(array $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php index dcee5f403..ed994930b 100644 --- a/tests/Column/TsRangeColumnTest.php +++ b/tests/Column/TsRangeColumnTest.php @@ -114,6 +114,33 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [null, 'empty']; + yield [ + new TsRangeValue(new DateTimeImmutable('2024-01-01 10:00:00'), new DateTimeImmutable('2024-01-01 15:00:00'), true, true), + '["2024-01-01 10:00:00","2024-01-01 15:00:00"]', + ]; + yield [ + new TsRangeValue(null, new DateTimeImmutable('2024-01-01 15:00:00'), false, true), + '(,"2024-01-01 15:00:00"]', + ]; + yield [ + new TsRangeValue(new DateTimeImmutable('2024-01-01 15:00:00'), null, false, false), + '("2024-01-01 15:00:00",)', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(mixed $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/TsTzMultiRangeColumnTest.php b/tests/Column/TsTzMultiRangeColumnTest.php index efaeeef72..f38f4c6d7 100644 --- a/tests/Column/TsTzMultiRangeColumnTest.php +++ b/tests/Column/TsTzMultiRangeColumnTest.php @@ -90,6 +90,35 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [[], '{}']; + yield [ + [ + new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), new DateTimeImmutable('2024-01-01 15:00:00+00'), true, true), + new TsTzRangeValue(new DateTimeImmutable('2024-01-02 10:00:00+00'), new DateTimeImmutable('2024-01-02 15:00:00+00'), true, false), + ], + '{["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"],["2024-01-02 10:00:00+00","2024-01-02 15:00:00+00")}', + ]; + yield [ + [ + new TsTzRangeValue(null, new DateTimeImmutable('2024-01-01 15:00:00+00'), false, true), + new TsTzRangeValue(new DateTimeImmutable('2024-01-02 10:00:00+00'), null, true, false), + ], + '{[,"2024-01-01 15:00:00+00"],["2024-01-02 10:00:00+00",)}', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(array $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ diff --git a/tests/Column/TsTzRangeColumnTest.php b/tests/Column/TsTzRangeColumnTest.php index 3bf12807c..cffa3aeb0 100644 --- a/tests/Column/TsTzRangeColumnTest.php +++ b/tests/Column/TsTzRangeColumnTest.php @@ -114,6 +114,33 @@ public function testBase(mixed $expectedColumnValue, mixed $value): void $this->assertSame($expectedColumnValue, $result['col']); } + public static function dataPhpTypecast(): iterable + { + yield 'empty' => [null, 'empty']; + yield [ + new TsTzRangeValue(new DateTimeImmutable('2024-01-01 10:00:00+00'), new DateTimeImmutable('2024-01-01 15:00:00+00'), true, true), + '["2024-01-01 10:00:00+00","2024-01-01 15:00:00+00"]', + ]; + yield [ + new TsTzRangeValue(null, new DateTimeImmutable('2024-01-01 15:00:00+00'), false, true), + '(,"2024-01-01 15:00:00+00"]', + ]; + yield [ + new TsTzRangeValue(new DateTimeImmutable('2024-01-01 15:00:00+00'), null, false, false), + '("2024-01-01 15:00:00+00",)', + ]; + } + + #[DataProvider('dataPhpTypecast')] + public function testPhpTypecast(mixed $expected, string $value): void + { + $db = $this->createConnection([$value]); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 1])->withTypecasting()->one(); + + $this->assertEquals($expected, $result['col']); + } + /** * @psalm-param list $values */ From 3f6cf28cb5d6020379a641b4f12ef279ea67fe8c Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:10:19 +0300 Subject: [PATCH 29/37] Update src/Column/Int8MultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/Int8MultiRangeColumn.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Column/Int8MultiRangeColumn.php b/src/Column/Int8MultiRangeColumn.php index 14eef5406..089b4acec 100644 --- a/src/Column/Int8MultiRangeColumn.php +++ b/src/Column/Int8MultiRangeColumn.php @@ -8,6 +8,8 @@ final class Int8MultiRangeColumn extends AbstractMultiRangeColumn { + protected const DEFAULT_TYPE = PgsqlColumnType::INT8MULTIRANGE; + protected function getRangeColumn(): ColumnInterface { return new Int8RangeColumn(); From f18ff8004f49ef9a0b8aec9a865ef1f31862cbfa Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:10:34 +0300 Subject: [PATCH 30/37] Update src/Column/DateMultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/DateMultiRangeColumn.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Column/DateMultiRangeColumn.php b/src/Column/DateMultiRangeColumn.php index ebf284272..1731b11fd 100644 --- a/src/Column/DateMultiRangeColumn.php +++ b/src/Column/DateMultiRangeColumn.php @@ -8,6 +8,8 @@ final class DateMultiRangeColumn extends AbstractMultiRangeColumn { + protected const DEFAULT_TYPE = PgsqlColumnType::DATEMULTIRANGE; + protected function getRangeColumn(): ColumnInterface { return new DateRangeColumn(); From 0f5f5752d355533835153d8772039803bea63259 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:10:47 +0300 Subject: [PATCH 31/37] Update src/Column/Int4MultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/Int4MultiRangeColumn.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Column/Int4MultiRangeColumn.php b/src/Column/Int4MultiRangeColumn.php index 2291ff8fb..b14870c24 100644 --- a/src/Column/Int4MultiRangeColumn.php +++ b/src/Column/Int4MultiRangeColumn.php @@ -8,6 +8,8 @@ final class Int4MultiRangeColumn extends AbstractMultiRangeColumn { + protected const DEFAULT_TYPE = PgsqlColumnType::INT4MULTIRANGE; + protected function getRangeColumn(): ColumnInterface { return new Int4RangeColumn(); From 2573e2d8ec5251ad225a6a998ac26823db6a6897 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:10:59 +0300 Subject: [PATCH 32/37] Update src/Column/NumMultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/NumMultiRangeColumn.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Column/NumMultiRangeColumn.php b/src/Column/NumMultiRangeColumn.php index e7d0ec9c5..759b76922 100644 --- a/src/Column/NumMultiRangeColumn.php +++ b/src/Column/NumMultiRangeColumn.php @@ -8,6 +8,8 @@ final class NumMultiRangeColumn extends AbstractMultiRangeColumn { + protected const DEFAULT_TYPE = PgsqlColumnType::NUMMULTIRANGE; + protected function getRangeColumn(): ColumnInterface { return new NumRangeColumn(); From edfec0896a0c6d5ae7eef3f563111c045eb1d83b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:11:17 +0300 Subject: [PATCH 33/37] Update src/Column/TsMultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/TsMultiRangeColumn.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Column/TsMultiRangeColumn.php b/src/Column/TsMultiRangeColumn.php index 343685cb1..7f96688a5 100644 --- a/src/Column/TsMultiRangeColumn.php +++ b/src/Column/TsMultiRangeColumn.php @@ -8,6 +8,8 @@ final class TsMultiRangeColumn extends AbstractMultiRangeColumn { + protected const DEFAULT_TYPE = PgsqlColumnType::TSMULTIRANGE; + protected function getRangeColumn(): ColumnInterface { return new TsRangeColumn(); From 9cc045a00d62607c9efa9550e11ad226dcb3c480 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:11:34 +0300 Subject: [PATCH 34/37] Update src/Column/TsTzMultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/TsTzMultiRangeColumn.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Column/TsTzMultiRangeColumn.php b/src/Column/TsTzMultiRangeColumn.php index c34028208..67119606a 100644 --- a/src/Column/TsTzMultiRangeColumn.php +++ b/src/Column/TsTzMultiRangeColumn.php @@ -8,6 +8,8 @@ final class TsTzMultiRangeColumn extends AbstractMultiRangeColumn { + protected const DEFAULT_TYPE = PgsqlColumnType::TSTZMULTIRANGE; + protected function getRangeColumn(): ColumnInterface { return new TsTzRangeColumn(); From 634b603f0b7976e272e9e47ec868f480698832f0 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:17:13 +0300 Subject: [PATCH 35/37] fix --- src/Column/DateMultiRangeColumn.php | 1 + src/Column/Int4MultiRangeColumn.php | 1 + src/Column/Int8MultiRangeColumn.php | 1 + src/Column/NumMultiRangeColumn.php | 1 + src/Column/TsMultiRangeColumn.php | 1 + src/Column/TsTzMultiRangeColumn.php | 1 + 6 files changed, 6 insertions(+) diff --git a/src/Column/DateMultiRangeColumn.php b/src/Column/DateMultiRangeColumn.php index 1731b11fd..138130562 100644 --- a/src/Column/DateMultiRangeColumn.php +++ b/src/Column/DateMultiRangeColumn.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType; use Yiisoft\Db\Schema\Column\ColumnInterface; final class DateMultiRangeColumn extends AbstractMultiRangeColumn diff --git a/src/Column/Int4MultiRangeColumn.php b/src/Column/Int4MultiRangeColumn.php index b14870c24..80ecc0a55 100644 --- a/src/Column/Int4MultiRangeColumn.php +++ b/src/Column/Int4MultiRangeColumn.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType; use Yiisoft\Db\Schema\Column\ColumnInterface; final class Int4MultiRangeColumn extends AbstractMultiRangeColumn diff --git a/src/Column/Int8MultiRangeColumn.php b/src/Column/Int8MultiRangeColumn.php index 089b4acec..5aedc5a68 100644 --- a/src/Column/Int8MultiRangeColumn.php +++ b/src/Column/Int8MultiRangeColumn.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType; use Yiisoft\Db\Schema\Column\ColumnInterface; final class Int8MultiRangeColumn extends AbstractMultiRangeColumn diff --git a/src/Column/NumMultiRangeColumn.php b/src/Column/NumMultiRangeColumn.php index 759b76922..d0e2381a0 100644 --- a/src/Column/NumMultiRangeColumn.php +++ b/src/Column/NumMultiRangeColumn.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType; use Yiisoft\Db\Schema\Column\ColumnInterface; final class NumMultiRangeColumn extends AbstractMultiRangeColumn diff --git a/src/Column/TsMultiRangeColumn.php b/src/Column/TsMultiRangeColumn.php index 7f96688a5..13333dc56 100644 --- a/src/Column/TsMultiRangeColumn.php +++ b/src/Column/TsMultiRangeColumn.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType; use Yiisoft\Db\Schema\Column\ColumnInterface; final class TsMultiRangeColumn extends AbstractMultiRangeColumn diff --git a/src/Column/TsTzMultiRangeColumn.php b/src/Column/TsTzMultiRangeColumn.php index 67119606a..38019a382 100644 --- a/src/Column/TsTzMultiRangeColumn.php +++ b/src/Column/TsTzMultiRangeColumn.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Column; +use Yiisoft\Db\Pgsql\Constant\PgsqlColumnType; use Yiisoft\Db\Schema\Column\ColumnInterface; final class TsTzMultiRangeColumn extends AbstractMultiRangeColumn From 4951364521ebb5610314c623e1cd733cfc08e8f5 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 11:55:52 +0300 Subject: [PATCH 36/37] improve --- .../Builder/AbstractRangeValueBuilder.php | 31 +++++++++---------- .../Builder/DateRangeValueBuilder.php | 11 ------- .../Builder/Int4RangeValueBuilder.php | 11 ------- .../Builder/Int8RangeValueBuilder.php | 11 ------- .../Builder/NumRangeValueBuilder.php | 11 ------- .../Builder/TsRangeValueBuilder.php | 11 ------- .../Builder/TsTzRangeValueBuilder.php | 11 ------- 7 files changed, 15 insertions(+), 82 deletions(-) diff --git a/src/Expression/Builder/AbstractRangeValueBuilder.php b/src/Expression/Builder/AbstractRangeValueBuilder.php index 210ff6903..948e14208 100644 --- a/src/Expression/Builder/AbstractRangeValueBuilder.php +++ b/src/Expression/Builder/AbstractRangeValueBuilder.php @@ -7,35 +7,34 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\Pgsql\Expression\DateRangeValue; +use Yiisoft\Db\Pgsql\Expression\Int4RangeValue; +use Yiisoft\Db\Pgsql\Expression\Int8RangeValue; +use Yiisoft\Db\Pgsql\Expression\NumRangeValue; +use Yiisoft\Db\Pgsql\Expression\TsRangeValue; +use Yiisoft\Db\Pgsql\Expression\TsTzRangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; /** - * @template T as ExpressionInterface + * @template T as DateRangeValue|Int4RangeValue|Int8RangeValue|NumRangeValue|TsRangeValue|TsTzRangeValue * @implements ExpressionBuilderInterface */ abstract class AbstractRangeValueBuilder implements ExpressionBuilderInterface { - abstract protected function getBoundColumn(): ColumnInterface; - - /** - * @throws NotSupportedException - */ - final protected function buildRange( - mixed $lower, - mixed $upper, - bool $includeLower, - bool $includeUpper, - ): string { + final public function build(ExpressionInterface $expression, array &$params = []): string + { $column = $this->getBoundColumn(); return '\'' - . ($includeLower ? '[' : '(') - . $this->prepareBoundValue($lower, $column) + . ($expression->includeLower ? '[' : '(') + . $this->prepareBoundValue($expression->lower, $column) . ',' - . $this->prepareBoundValue($upper, $column) - . ($includeUpper ? ']' : ')') + . $this->prepareBoundValue($expression->upper, $column) + . ($expression->includeUpper ? ']' : ')') . '\''; } + abstract protected function getBoundColumn(): ColumnInterface; + /** * @throws NotSupportedException */ diff --git a/src/Expression/Builder/DateRangeValueBuilder.php b/src/Expression/Builder/DateRangeValueBuilder.php index a31ef269e..c66261a1f 100644 --- a/src/Expression/Builder/DateRangeValueBuilder.php +++ b/src/Expression/Builder/DateRangeValueBuilder.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Pgsql\Expression\Builder; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\DateRangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -16,16 +15,6 @@ */ final class DateRangeValueBuilder extends AbstractRangeValueBuilder { - public function build(ExpressionInterface $expression, array &$params = []): string - { - return $this->buildRange( - $expression->lower, - $expression->upper, - $expression->includeLower, - $expression->includeUpper, - ); - } - protected function getBoundColumn(): ColumnInterface { return RangeBoundColumnFactory::date(); diff --git a/src/Expression/Builder/Int4RangeValueBuilder.php b/src/Expression/Builder/Int4RangeValueBuilder.php index 372394307..0558b64eb 100644 --- a/src/Expression/Builder/Int4RangeValueBuilder.php +++ b/src/Expression/Builder/Int4RangeValueBuilder.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Pgsql\Expression\Builder; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\Int4RangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -16,16 +15,6 @@ */ final class Int4RangeValueBuilder extends AbstractRangeValueBuilder { - public function build(ExpressionInterface $expression, array &$params = []): string - { - return $this->buildRange( - $expression->lower, - $expression->upper, - $expression->includeLower, - $expression->includeUpper, - ); - } - protected function getBoundColumn(): ColumnInterface { return RangeBoundColumnFactory::int4(); diff --git a/src/Expression/Builder/Int8RangeValueBuilder.php b/src/Expression/Builder/Int8RangeValueBuilder.php index 49ba45da9..a2f66648a 100644 --- a/src/Expression/Builder/Int8RangeValueBuilder.php +++ b/src/Expression/Builder/Int8RangeValueBuilder.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Pgsql\Expression\Builder; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\Int8RangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -16,16 +15,6 @@ */ final class Int8RangeValueBuilder extends AbstractRangeValueBuilder { - public function build(ExpressionInterface $expression, array &$params = []): string - { - return $this->buildRange( - $expression->lower, - $expression->upper, - $expression->includeLower, - $expression->includeUpper, - ); - } - protected function getBoundColumn(): ColumnInterface { return RangeBoundColumnFactory::int8(); diff --git a/src/Expression/Builder/NumRangeValueBuilder.php b/src/Expression/Builder/NumRangeValueBuilder.php index fb90a116c..025d9b329 100644 --- a/src/Expression/Builder/NumRangeValueBuilder.php +++ b/src/Expression/Builder/NumRangeValueBuilder.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Pgsql\Expression\Builder; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\NumRangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -16,16 +15,6 @@ */ final class NumRangeValueBuilder extends AbstractRangeValueBuilder { - public function build(ExpressionInterface $expression, array &$params = []): string - { - return $this->buildRange( - $expression->lower, - $expression->upper, - $expression->includeLower, - $expression->includeUpper, - ); - } - protected function getBoundColumn(): ColumnInterface { return RangeBoundColumnFactory::num(); diff --git a/src/Expression/Builder/TsRangeValueBuilder.php b/src/Expression/Builder/TsRangeValueBuilder.php index bbb94c4b0..4792ba1a3 100644 --- a/src/Expression/Builder/TsRangeValueBuilder.php +++ b/src/Expression/Builder/TsRangeValueBuilder.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Pgsql\Expression\Builder; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\TsRangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -16,16 +15,6 @@ */ final class TsRangeValueBuilder extends AbstractRangeValueBuilder { - public function build(ExpressionInterface $expression, array &$params = []): string - { - return $this->buildRange( - $expression->lower, - $expression->upper, - $expression->includeLower, - $expression->includeUpper, - ); - } - protected function getBoundColumn(): ColumnInterface { return RangeBoundColumnFactory::ts(); diff --git a/src/Expression/Builder/TsTzRangeValueBuilder.php b/src/Expression/Builder/TsTzRangeValueBuilder.php index 4a4a78362..ee9557ad4 100644 --- a/src/Expression/Builder/TsTzRangeValueBuilder.php +++ b/src/Expression/Builder/TsTzRangeValueBuilder.php @@ -4,7 +4,6 @@ namespace Yiisoft\Db\Pgsql\Expression\Builder; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column\RangeBoundColumnFactory; use Yiisoft\Db\Pgsql\Expression\TsTzRangeValue; use Yiisoft\Db\Schema\Column\ColumnInterface; @@ -16,16 +15,6 @@ */ final class TsTzRangeValueBuilder extends AbstractRangeValueBuilder { - public function build(ExpressionInterface $expression, array &$params = []): string - { - return $this->buildRange( - $expression->lower, - $expression->upper, - $expression->includeLower, - $expression->includeUpper, - ); - } - protected function getBoundColumn(): ColumnInterface { return RangeBoundColumnFactory::tsTz(); From 4d60541d88b5b206edd6d3c12f1df32ec74c4987 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Mon, 17 Nov 2025 14:35:52 +0300 Subject: [PATCH 37/37] Update src/Column/AbstractMultiRangeColumn.php Co-authored-by: Sergei Tigrov --- src/Column/AbstractMultiRangeColumn.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Column/AbstractMultiRangeColumn.php b/src/Column/AbstractMultiRangeColumn.php index bcfb76de9..05b1e9a8d 100644 --- a/src/Column/AbstractMultiRangeColumn.php +++ b/src/Column/AbstractMultiRangeColumn.php @@ -57,7 +57,7 @@ public function phpTypecast(mixed $value): mixed return []; } - if (preg_match('/^{([\[\(][^,]*,[^\)\]]*[\)\]],?)+}$/', $value) !== 1) { + if (preg_match('/^{([\[\(][^,]*,[^\)\]]*[\)\]])(?:,(?1))*}$/', $value) !== 1) { throw new NotSupportedException('Unsupported multirange format'); }