Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions src/Expression/DateRangeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
18 changes: 18 additions & 0 deletions src/Expression/Int4RangeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
34 changes: 34 additions & 0 deletions src/Expression/Int8RangeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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];
}
}
23 changes: 23 additions & 0 deletions src/Expression/NumRangeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Db\Pgsql\Expression;

use RuntimeException;
use Yiisoft\Db\Expression\ExpressionInterface;

final class NumRangeValue implements ExpressionInterface
Expand All @@ -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];
}
}
18 changes: 18 additions & 0 deletions src/Expression/TsRangeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
18 changes: 18 additions & 0 deletions src/Expression/TsTzRangeValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
145 changes: 145 additions & 0 deletions tests/RangeValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Tests;

use DateTimeImmutable;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use RuntimeException;
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 const PHP_INT_MAX;
use const PHP_INT_MIN;

final class RangeValueTest extends TestCase
{
public static function boundsProvider(): iterable
{
yield 'int4 inclusive' => [
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]);
}
}
}
}