diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eea3494e..7792ed168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Chg #483: Bump `yiisoft/db` version to `^2.0.1` (@vjik) - Bug #484: Fix building range-type column definitions (@Tigrov) - Enh #489: Clarify return type of `phpTypecast()` methods for range columns (@Tigrov) +- New #492: Add `getBounds()` method to range values (@Tigrov) ## 2.0.1 February 07, 2026 diff --git a/src/Expression/DateRangeValue.php b/src/Expression/DateRangeValue.php index 9fa76d548..8e4955427 100644 --- a/src/Expression/DateRangeValue.php +++ b/src/Expression/DateRangeValue.php @@ -15,4 +15,22 @@ public function __construct( public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} + + /** + * Returns the lower and upper bounds of a range, inclusive. + * + * @psalm-return array{0: ?DateTimeImmutable, 1: ?DateTimeImmutable} + */ + public function getBounds(): array + { + $lower = $this->lower === null || $this->includeLower + ? $this->lower + : $this->lower->modify('+1 day'); + + $upper = $this->upper === null || $this->includeUpper + ? $this->upper + : $this->upper->modify('-1 day'); + + return [$lower, $upper]; + } } diff --git a/src/Expression/Int4RangeValue.php b/src/Expression/Int4RangeValue.php index d65c6efde..1ff9ab9cc 100644 --- a/src/Expression/Int4RangeValue.php +++ b/src/Expression/Int4RangeValue.php @@ -14,4 +14,22 @@ public function __construct( public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} + + /** + * Returns the lower and upper bounds of a range, inclusive. + * + * @psalm-return array{0: ?int, 1: ?int} + */ + public function getBounds(): array + { + $lower = $this->lower === null || $this->includeLower + ? $this->lower + : $this->lower + 1; + + $upper = $this->upper === null || $this->includeUpper + ? $this->upper + : $this->upper - 1; + + return [$lower, $upper]; + } } diff --git a/src/Expression/Int8RangeValue.php b/src/Expression/Int8RangeValue.php index 903425569..6d017f150 100644 --- a/src/Expression/Int8RangeValue.php +++ b/src/Expression/Int8RangeValue.php @@ -4,8 +4,12 @@ namespace Yiisoft\Db\Pgsql\Expression; +use RuntimeException; use Yiisoft\Db\Expression\ExpressionInterface; +use const PHP_INT_MAX; +use const PHP_INT_MIN; + final class Int8RangeValue implements ExpressionInterface { public function __construct( @@ -14,4 +18,34 @@ public function __construct( public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} + + /** + * Returns the lower and upper bounds of a range, inclusive. + * + * @psalm-return array{0: int|string|null, 1: int|string|null} + */ + public function getBounds(): array + { + $lower = $this->lower === null || $this->includeLower + ? $this->lower + : ( + PHP_INT_MIN < $this->lower && $this->lower < PHP_INT_MAX + ? (int) $this->lower + 1 + : throw new RuntimeException( + 'Lower bound cannot be determined from the excluded value of a bigint range.', + ) + ); + + $upper = $this->upper === null || $this->includeUpper + ? $this->upper + : ( + PHP_INT_MIN < $this->upper && $this->upper < PHP_INT_MAX + ? (int) $this->upper - 1 + : throw new RuntimeException( + 'Upper bound cannot be determined from the excluded value of a bigint range.', + ) + ); + + return [$lower, $upper]; + } } diff --git a/src/Expression/NumRangeValue.php b/src/Expression/NumRangeValue.php index 1f5d3f234..341ae14cc 100644 --- a/src/Expression/NumRangeValue.php +++ b/src/Expression/NumRangeValue.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Pgsql\Expression; +use RuntimeException; use Yiisoft\Db\Expression\ExpressionInterface; final class NumRangeValue implements ExpressionInterface @@ -14,4 +15,26 @@ public function __construct( public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} + + /** + * Returns the lower and upper bounds of a range, inclusive. + * + * @psalm-return array{0: int|float|null, 1: int|float|null} + */ + public function getBounds(): array + { + $lower = $this->lower === null || $this->includeLower + ? $this->lower + : throw new RuntimeException( + 'Lower bound cannot be determined from the excluded value of a numeric range.', + ); + + $upper = $this->upper === null || $this->includeUpper + ? $this->upper + : throw new RuntimeException( + 'Upper bound cannot be determined from the excluded value of a numeric range.', + ); + + return [$lower, $upper]; + } } diff --git a/src/Expression/TsRangeValue.php b/src/Expression/TsRangeValue.php index 50594c8e9..7349df134 100644 --- a/src/Expression/TsRangeValue.php +++ b/src/Expression/TsRangeValue.php @@ -15,4 +15,22 @@ public function __construct( public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} + + /** + * Returns the lower and upper bounds of a range, inclusive. + * + * @psalm-return array{0: ?DateTimeImmutable, 1: ?DateTimeImmutable} + */ + public function getBounds(): array + { + $lower = $this->lower === null || $this->includeLower + ? $this->lower + : $this->lower->modify('+1 second'); + + $upper = $this->upper === null || $this->includeUpper + ? $this->upper + : $this->upper->modify('-1 second'); + + return [$lower, $upper]; + } } diff --git a/src/Expression/TsTzRangeValue.php b/src/Expression/TsTzRangeValue.php index 7f08c3aab..0112d6cbf 100644 --- a/src/Expression/TsTzRangeValue.php +++ b/src/Expression/TsTzRangeValue.php @@ -15,4 +15,22 @@ public function __construct( public readonly bool $includeLower = true, public readonly bool $includeUpper = true, ) {} + + /** + * Returns the lower and upper bounds of a range, inclusive. + * + * @psalm-return array{0: ?DateTimeImmutable, 1: ?DateTimeImmutable} + */ + public function getBounds(): array + { + $lower = $this->lower === null || $this->includeLower + ? $this->lower + : $this->lower->modify('+1 second'); + + $upper = $this->upper === null || $this->includeUpper + ? $this->upper + : $this->upper->modify('-1 second'); + + return [$lower, $upper]; + } } diff --git a/tests/RangeValueTest.php b/tests/RangeValueTest.php new file mode 100644 index 000000000..cdf264022 --- /dev/null +++ b/tests/RangeValueTest.php @@ -0,0 +1,145 @@ + [ + new Int4RangeValue(1, 5), + [1, 5], + ]; + yield 'int4 exclusive' => [ + new Int4RangeValue(1, 5, false, false), + [2, 4], + ]; + yield 'int4 unbounded' => [ + new Int4RangeValue(null, null, false, false), + [null, null], + ]; + yield 'int8 inclusive beyond bounds' => [ + new Int8RangeValue(PHP_INT_MIN, PHP_INT_MAX), + [PHP_INT_MIN, PHP_INT_MAX], + ]; + yield 'int8 exclusive' => [ + new Int8RangeValue('1', '5', false, false), + [2, 4], + ]; + yield 'int8 unbounded' => [ + new Int8RangeValue(null, null, false, false), + [null, null], + ]; + yield 'num inclusive' => [ + new NumRangeValue(1.5, 5.5), + [1.5, 5.5], + ]; + yield 'num unbounded' => [ + new NumRangeValue(null, null, false, false), + [null, null], + ]; + yield 'date inclusive' => [ + new DateRangeValue(new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-05')), + [new DateTimeImmutable('2024-01-01'), new DateTimeImmutable('2024-01-05')], + ]; + yield 'date exclusive' => [ + new DateRangeValue( + new DateTimeImmutable('2024-01-01'), + new DateTimeImmutable('2024-01-05'), + false, + false, + ), + [new DateTimeImmutable('2024-01-02'), new DateTimeImmutable('2024-01-04')], + ]; + yield 'timestamp exclusive' => [ + new TsRangeValue( + new DateTimeImmutable('2024-01-01 10:00:00'), + new DateTimeImmutable('2024-01-01 15:00:00'), + false, + false, + ), + [new DateTimeImmutable('2024-01-01 10:00:01'), new DateTimeImmutable('2024-01-01 14:59:59')], + ]; + yield 'timestamp with time zone exclusive' => [ + new TsTzRangeValue( + new DateTimeImmutable('2024-01-01 10:00:00+02:00'), + new DateTimeImmutable('2024-01-01 15:00:00+02:00'), + false, + false, + ), + [new DateTimeImmutable('2024-01-01 10:00:01+02:00'), new DateTimeImmutable('2024-01-01 14:59:59+02:00')], + ]; + } + + #[DataProvider('boundsProvider')] + public function testGetBounds(object $rangeValue, array $expected): void + { + $this->assertBoundsSame($expected, $rangeValue->getBounds()); + } + + public static function boundsExceptionProvider(): iterable + { + yield 'int8 lower below minimum' => [ + new Int8RangeValue(PHP_INT_MIN, null, false), + 'Lower bound cannot be determined from the excluded value of a bigint range.', + ]; + yield 'int8 lower at maximum' => [ + new Int8RangeValue(PHP_INT_MAX, null, false), + 'Lower bound cannot be determined from the excluded value of a bigint range.', + ]; + yield 'int8 upper above maximum' => [ + new Int8RangeValue(null, PHP_INT_MAX, true, false), + 'Upper bound cannot be determined from the excluded value of a bigint range.', + ]; + yield 'int8 upper at minimum' => [ + new Int8RangeValue(null, PHP_INT_MIN, true, false), + 'Upper bound cannot be determined from the excluded value of a bigint range.', + ]; + yield 'num lower exclusive' => [ + new NumRangeValue(1.5, null, false), + 'Lower bound cannot be determined from the excluded value of a numeric range.', + ]; + yield 'num upper exclusive' => [ + new NumRangeValue(null, 5.5, true, false), + 'Upper bound cannot be determined from the excluded value of a numeric range.', + ]; + } + + #[DataProvider('boundsExceptionProvider')] + public function testGetBoundsException(object $rangeValue, string $expectedMessage): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage($expectedMessage); + + $rangeValue->getBounds(); + } + + private function assertBoundsSame(array $expected, array $actual): void + { + $this->assertCount(2, $actual); + + foreach ($expected as $key => $expectedValue) { + if ($expectedValue instanceof DateTimeImmutable) { + $this->assertEquals($expectedValue, $actual[$key]); + } else { + $this->assertSame($expectedValue, $actual[$key]); + } + } + } +}