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..05b1e9a8d --- /dev/null +++ b/src/Column/AbstractMultiRangeColumn.php @@ -0,0 +1,74 @@ +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('/^{([\[\(][^,]*,[^\)\]]*[\)\]])(?:,(?1))*}$/', $value) !== 1) { + throw new NotSupportedException('Unsupported multirange format'); + } + + preg_match_all('/[\[\(][^,]*,[^\)\]]*[\)\]]/', $value, $matches); + + $rangeColumn = $this->getRangeColumn(); + return array_map( + $rangeColumn->phpTypecast(...), + $matches[0], + ); + } + + abstract protected function getRangeColumn(): ColumnInterface; +} diff --git a/src/Column/AbstractRangeColumn.php b/src/Column/AbstractRangeColumn.php new file mode 100644 index 000000000..85a242667 --- /dev/null +++ b/src/Column/AbstractRangeColumn.php @@ -0,0 +1,101 @@ +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' => '', + '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 cf0d4c1ff..85daba878 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,79 @@ 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(): Int4RangeColumn + { + return new Int4RangeColumn(); + } + + public static function int8Range(): Int8RangeColumn + { + return new Int8RangeColumn(); + } + + public static function numRange(): NumRangeColumn + { + return new NumRangeColumn(); + } + + public static function tsRange(): TsRangeColumn + { + return new TsRangeColumn(); + } + + public static function tsTzRange(): TsTzRangeColumn + { + return new TsTzRangeColumn(); + } + + 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 8068888d1..4664f1059 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,18 @@ final class ColumnFactory extends AbstractColumnFactory 'xml' => ColumnType::STRING, 'json' => ColumnType::JSON, 'jsonb' => ColumnType::JSON, + 'int4range' => PgsqlColumnType::INT4RANGE, + 'int8range' => PgsqlColumnType::INT8RANGE, + 'numrange' => PgsqlColumnType::NUMRANGE, + '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 @@ -129,10 +141,41 @@ protected function getColumnClass(string $type, array $info = []): string ColumnType::BINARY => BinaryColumn::class, ColumnType::ARRAY => ArrayColumn::class, ColumnType::STRUCTURED => StructuredColumn::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, + 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), }; } + protected function isType(string $type): bool + { + return match ($type) { + PgsqlColumnType::INT4RANGE, + PgsqlColumnType::INT8RANGE, + PgsqlColumnType::NUMRANGE, + PgsqlColumnType::TSRANGE, + PgsqlColumnType::TSTZRANGE, + PgsqlColumnType::DATERANGE, + PgsqlColumnType::INT4MULTIRANGE, + PgsqlColumnType::INT8MULTIRANGE, + PgsqlColumnType::NUMMULTIRANGE, + PgsqlColumnType::TSMULTIRANGE, + PgsqlColumnType::TSTZMULTIRANGE, + PgsqlColumnType::DATEMULTIRANGE => true, + default => parent::isType($type), + }; + } + protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed { /** @var string $value */ diff --git a/src/Column/DateMultiRangeColumn.php b/src/Column/DateMultiRangeColumn.php new file mode 100644 index 000000000..138130562 --- /dev/null +++ b/src/Column/DateMultiRangeColumn.php @@ -0,0 +1,18 @@ +getBoundColumn(); + return new DateRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/Int4MultiRangeColumn.php b/src/Column/Int4MultiRangeColumn.php new file mode 100644 index 000000000..80ecc0a55 --- /dev/null +++ b/src/Column/Int4MultiRangeColumn.php @@ -0,0 +1,18 @@ +getBoundColumn(); + return new Int4RangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/Int8MultiRangeColumn.php b/src/Column/Int8MultiRangeColumn.php new file mode 100644 index 000000000..5aedc5a68 --- /dev/null +++ b/src/Column/Int8MultiRangeColumn.php @@ -0,0 +1,18 @@ +getBoundColumn(); + return new Int8RangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/NumMultiRangeColumn.php b/src/Column/NumMultiRangeColumn.php new file mode 100644 index 000000000..d0e2381a0 --- /dev/null +++ b/src/Column/NumMultiRangeColumn.php @@ -0,0 +1,18 @@ +getBoundColumn(); + return new NumRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} 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 @@ +getBoundColumn(); + return new TsRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Column/TsTzMultiRangeColumn.php b/src/Column/TsTzMultiRangeColumn.php new file mode 100644 index 000000000..38019a382 --- /dev/null +++ b/src/Column/TsTzMultiRangeColumn.php @@ -0,0 +1,18 @@ +getBoundColumn(); + return new TsTzRangeValue( + $column->phpTypecast($lower), + $column->phpTypecast($upper), + $includeLower, + $includeUpper, + ); + } +} diff --git a/src/Constant/PgsqlColumnType.php b/src/Constant/PgsqlColumnType.php new file mode 100644 index 000000000..439a7f53e --- /dev/null +++ b/src/Constant/PgsqlColumnType.php @@ -0,0 +1,21 @@ + 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, + MultiRangeValue::class => MultiRangeValueBuilder::class, ]; } } diff --git a/src/Expression/Builder/AbstractRangeValueBuilder.php b/src/Expression/Builder/AbstractRangeValueBuilder.php new file mode 100644 index 000000000..948e14208 --- /dev/null +++ b/src/Expression/Builder/AbstractRangeValueBuilder.php @@ -0,0 +1,50 @@ + + */ +abstract class AbstractRangeValueBuilder implements ExpressionBuilderInterface +{ + final public function build(ExpressionInterface $expression, array &$params = []): string + { + $column = $this->getBoundColumn(); + return '\'' + . ($expression->includeLower ? '[' : '(') + . $this->prepareBoundValue($expression->lower, $column) + . ',' + . $this->prepareBoundValue($expression->upper, $column) + . ($expression->includeUpper ? ']' : ')') + . '\''; + } + + abstract protected function getBoundColumn(): ColumnInterface; + + /** + * @throws NotSupportedException + */ + private function prepareBoundValue(mixed $value, ColumnInterface $boundColumn): string + { + $value = $boundColumn->dbTypecast($value); + if ($value === null) { + return ''; + } + + return (string) $value; + } +} diff --git a/src/Expression/Builder/DateRangeValueBuilder.php b/src/Expression/Builder/DateRangeValueBuilder.php new file mode 100644 index 000000000..c66261a1f --- /dev/null +++ b/src/Expression/Builder/DateRangeValueBuilder.php @@ -0,0 +1,22 @@ + + */ +final class DateRangeValueBuilder extends AbstractRangeValueBuilder +{ + 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..0558b64eb --- /dev/null +++ b/src/Expression/Builder/Int4RangeValueBuilder.php @@ -0,0 +1,22 @@ + + */ +final class Int4RangeValueBuilder extends AbstractRangeValueBuilder +{ + 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..a2f66648a --- /dev/null +++ b/src/Expression/Builder/Int8RangeValueBuilder.php @@ -0,0 +1,22 @@ + + */ +final class Int8RangeValueBuilder extends AbstractRangeValueBuilder +{ + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::int8(); + } +} diff --git a/src/Expression/Builder/MultiRangeValueBuilder.php b/src/Expression/Builder/MultiRangeValueBuilder.php new file mode 100644 index 000000000..885cd2f62 --- /dev/null +++ b/src/Expression/Builder/MultiRangeValueBuilder.php @@ -0,0 +1,36 @@ + + */ +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/Builder/NumRangeValueBuilder.php b/src/Expression/Builder/NumRangeValueBuilder.php new file mode 100644 index 000000000..025d9b329 --- /dev/null +++ b/src/Expression/Builder/NumRangeValueBuilder.php @@ -0,0 +1,22 @@ + + */ +final class NumRangeValueBuilder extends AbstractRangeValueBuilder +{ + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::num(); + } +} diff --git a/src/Expression/Builder/TsRangeValueBuilder.php b/src/Expression/Builder/TsRangeValueBuilder.php new file mode 100644 index 000000000..4792ba1a3 --- /dev/null +++ b/src/Expression/Builder/TsRangeValueBuilder.php @@ -0,0 +1,22 @@ + + */ +final class TsRangeValueBuilder extends AbstractRangeValueBuilder +{ + 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..ee9557ad4 --- /dev/null +++ b/src/Expression/Builder/TsTzRangeValueBuilder.php @@ -0,0 +1,22 @@ + + */ +final class TsTzRangeValueBuilder extends AbstractRangeValueBuilder +{ + protected function getBoundColumn(): ColumnInterface + { + return RangeBoundColumnFactory::tsTz(); + } +} diff --git a/src/Expression/DateRangeValue.php b/src/Expression/DateRangeValue.php new file mode 100644 index 000000000..9fa76d548 --- /dev/null +++ b/src/Expression/DateRangeValue.php @@ -0,0 +1,18 @@ + $ranges + */ + public readonly array $ranges; + + public function __construct( + string|ExpressionInterface ...$ranges, + ) { + $this->ranges = $ranges; + } +} diff --git a/src/Expression/NumRangeValue.php b/src/Expression/NumRangeValue.php new file mode 100644 index 000000000..1f5d3f234 --- /dev/null +++ b/src/Expression/NumRangeValue.php @@ -0,0 +1,17 @@ +assertSame($expected, $builder->build($expression, $params)); Assert::arraysEquals($expectedParams, $params); + + $db->close(); } } diff --git a/tests/Column/DateMultiRangeColumnTest.php b/tests/Column/DateMultiRangeColumnTest.php new file mode 100644 index 000000000..8fbe943ee --- /dev/null +++ b/tests/Column/DateMultiRangeColumnTest.php @@ -0,0 +1,180 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('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/DateRangeColumnTest.php b/tests/Column/DateRangeColumnTest.php new file mode 100644 index 000000000..9ea3eb452 --- /dev/null +++ b/tests/Column/DateRangeColumnTest.php @@ -0,0 +1,149 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + + $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..4b6a607b6 --- /dev/null +++ b/tests/Column/Int4MultiRangeColumnTest.php @@ -0,0 +1,128 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('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/Int4RangeColumnTest.php b/tests/Column/Int4RangeColumnTest.php new file mode 100644 index 000000000..3f2ece877 --- /dev/null +++ b/tests/Column/Int4RangeColumnTest.php @@ -0,0 +1,115 @@ +createConnection(['[1,10)', '(20,30]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + + $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..09f1cef3e --- /dev/null +++ b/tests/Column/Int8MultiRangeColumnTest.php @@ -0,0 +1,128 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('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/Int8RangeColumnTest.php b/tests/Column/Int8RangeColumnTest.php new file mode 100644 index 000000000..efffb0567 --- /dev/null +++ b/tests/Column/Int8RangeColumnTest.php @@ -0,0 +1,115 @@ +createConnection(['[1,10)', '(20,30]']); + + $db->createCommand()->insert('tbl_test', ['col' => $value])->execute(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + + $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..1b187afaf --- /dev/null +++ b/tests/Column/NumMultiRangeColumnTest.php @@ -0,0 +1,128 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('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/NumRangeColumnTest.php b/tests/Column/NumRangeColumnTest.php new file mode 100644 index 000000000..f1b43885e --- /dev/null +++ b/tests/Column/NumRangeColumnTest.php @@ -0,0 +1,115 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + + $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..3dff98d15 --- /dev/null +++ b/tests/Column/TsMultiRangeColumnTest.php @@ -0,0 +1,155 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('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/TsRangeColumnTest.php b/tests/Column/TsRangeColumnTest.php new file mode 100644 index 000000000..ed994930b --- /dev/null +++ b/tests/Column/TsRangeColumnTest.php @@ -0,0 +1,176 @@ +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(); + + $result = $db->select('col')->from('tbl_test')->where(['id' => 3])->one(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + + $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..f38f4c6d7 --- /dev/null +++ b/tests/Column/TsTzMultiRangeColumnTest.php @@ -0,0 +1,155 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + $this->ensureMinPostgreSqlVersion('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/TsTzRangeColumnTest.php b/tests/Column/TsTzRangeColumnTest.php new file mode 100644 index 000000000..cffa3aeb0 --- /dev/null +++ b/tests/Column/TsTzRangeColumnTest.php @@ -0,0 +1,176 @@ +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(); + + $this->assertIsArray($result); + $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 + */ + private function createConnection(array $values = []): Connection + { + $db = TestConnection::get(); + + $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/CommandTest.php b/tests/CommandTest.php index f23ab96d7..e0a2b4bb6 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,18 @@ 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(); } @@ -225,7 +231,14 @@ public function testInsertReturningPksUuid(): void public function testShowDatabases(): void { - $this->assertSame([self::getDatabaseName()], self::getDb()->createCommand()->showDatabases()); + $db = TestConnection::get(); + + $databases = $db->createCommand()->showDatabases(); + + $this->assertSame( + [TestConnection::databaseName()], + $databases, + ); } #[DataProviderExternal(CommandProvider::class, 'createIndex')] 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(); } } 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/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 56fd4de35..1bb6d2222 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -13,7 +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\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; @@ -191,6 +197,12 @@ public static function columns(): array notNull: false, ), ), + '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', ], diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 510c00489..c0ba76b3e 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,24 @@ 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->assertStringStartsWith( '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 +214,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 +229,6 @@ public function testDropCommentFromColumn(): void public function testDropDefaultValue(): void { $db = $this->getConnection(true); - $qb = $db->getQueryBuilder(); $this->expectException(NotSupportedException::class); @@ -237,14 +237,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 +366,6 @@ public function testResetSequencePgsql12(): void $this->setFixture('pgsql12.sql'); $db = $this->getConnection(true); - $qb = $db->getQueryBuilder(); $this->assertSame( @@ -640,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 = []; @@ -653,6 +649,8 @@ public function testArrayMergeWithTypeWithOrdering( $result = $db->select($arrayMerge)->scalar(); $this->assertSame($expectedResult, $result); + + $db->close(); } #[DataProviderExternal(QueryBuilderProvider::class, 'upsertWithMultiOperandFunctions')] 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); 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(); } } diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index de2a9f360..1a93364e9 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -166,7 +166,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/TestTrait.php b/tests/Support/TestTrait.php index beca25178..1f996d937 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -6,7 +6,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; @@ -28,7 +28,7 @@ public static function setUpBeforeClass(): void protected function getConnection(bool $fixture = false): Connection { - $db = new Connection($this->getDriver(), DbHelper::getSchemaCache()); + $db = TestConnection::create($this->getDsn()); if ($fixture) { DbHelper::loadFixture($db, __DIR__ . "/Fixture/$this->fixture"); @@ -39,25 +39,13 @@ protected function getConnection(bool $fixture = false): Connection 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; @@ -85,34 +73,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'; + } +}