Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Range parsers with tests b #93

Closed
Closed
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ composer.phar
# phpunit itself is not needed
phpunit.phar
phpunit.result.cache
.phpunit.result.cache

# local phpunit config
/phpunit.xml
Expand Down
14 changes: 14 additions & 0 deletions src/ColumnSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public function dbTypecast($value)
*/
public function phpTypecast($value)
{
if (RangeParser::isAllowedType($this->getType())) {
return $this->getRangeParser()->parse($value);
}

if ($this->dimension > 0) {
if (!is_array($value)) {
$value = $this->getArrayParser()->parse($value);
Expand Down Expand Up @@ -141,6 +145,16 @@ protected function getArrayParser(): ArrayParser
return new ArrayParser();
}

/**
* Creates instance of RangeParser.
*
* @return RangeParser
*/
protected function getRangeParser(): RangeParser
{
return new RangeParser($this->getType());
}

/**
* @return int Get the dimension of array. Defaults to 0, means this column is not an array.
*/
Expand Down
201 changes: 201 additions & 0 deletions src/RangeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql;

use DateInterval;
use DateTime;
use InvalidArgumentException;
use function preg_match;

final class RangeParser
{
private const RANGES = [
Schema::TYPE_INT_4_RANGE,
Schema::TYPE_INT_8_RANGE,
Schema::TYPE_NUM_RANGE,
Schema::TYPE_TS_RANGE,
Schema::TYPE_TS_TZ_RANGE,
Schema::TYPE_DATE_RANGE,
];

private ?string $type = null;

public function __construct(?string $type = null)
{
if ($type !== null) {
if (self::isAllowedType($type)) {
$this->type = $type;
} else {
throw new InvalidArgumentException('Unsupported range type "' . $type . '"');
}
}
}

public function parse(?string $value): ?array
{
if ($value === null) {
return null;
}

if (!preg_match('/^(?P<open>\[|\()(?P<lower>[^,]*),(?P<upper>[^\)\]]*)(?P<close>\)|\])$/', $value, $matches)) {
throw new InvalidArgumentException();
}

$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];
}

$type = $this->type ?? self::parseType($lower, $upper);

switch ($type) {
case Schema::TYPE_INT_4_RANGE:
return self::parseIntRange($lower, $upper, $includeLower, $includeUpper);
case Schema::TYPE_INT_8_RANGE:
return self::parseBigIntRange($lower, $upper, $includeLower, $includeUpper);
case Schema::TYPE_NUM_RANGE:
return self::parseNumRange($lower, $upper, $includeLower, $includeUpper);
case Schema::TYPE_DATE_RANGE:
return self::parseDateRange($lower, $upper, $includeLower, $includeUpper);
case Schema::TYPE_TS_RANGE:
return self::parseTsRange($lower, $upper, $includeLower, $includeUpper);
case Schema::TYPE_TS_TZ_RANGE:
return self::parseTsTzRange($lower, $upper, $includeLower, $includeUpper);
default:
return null;
}
}

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, $includeLower, $includeUpper);

if ($min !== null && $includeLower === false) {
$min += 1;
}

if ($max !== null && $includeUpper === false) {
$max -= 1;
}

return [$min, $max];
}

private static function parseNumRange(?string $lower, ?string $upper, bool $includeLower, bool $includeUpper): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool $includeLower, bool $includeUpper they are not used within the method.

Copy link
Author

@Gerych1984 Gerych1984 Nov 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix that and add new short immutable methods like withType, asInt, asNum etc at nearest time

{
$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, bool $includeLower, bool $includeUpper): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool $includeLower, bool $includeUpper they are not used within the method.

{
$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, bool $includeLower, bool $includeUpper): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool $includeLower, bool $includeUpper they are not used within the method.

{
$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);
}

/**
* Find range type from value format
*
* @param string $lower
* @param string $upper
*
* @return string|null
*/
private static function parseType(?string $lower, ?string $upper): ?string
{
if ($lower !== null && $upper !== null) {
if (filter_var($lower, FILTER_VALIDATE_INT) && filter_var($upper, FILTER_VALIDATE_INT)) {
return Schema::TYPE_INT_4_RANGE;
}

if (filter_var($lower, FILTER_VALIDATE_FLOAT) && filter_var($upper, FILTER_VALIDATE_FLOAT)) {
return Schema::TYPE_NUM_RANGE;
}
}

$value = $lower ?? $upper;

if (filter_var($value, FILTER_VALIDATE_INT)) {
return Schema::TYPE_INT_4_RANGE;
}


if (filter_var($value, FILTER_VALIDATE_FLOAT)) {
return Schema::TYPE_NUM_RANGE;
}

if (DateTime::createFromFormat('Y-m-d', $value)) {
return Schema::TYPE_DATE_RANGE;
}

if (DateTime::createFromFormat('Y-m-d H:i:s', $value)) {
return Schema::TYPE_TS_RANGE;
}

if (DateTime::createFromFormat('Y-m-d H:i:sP', $value)) {
return Schema::TYPE_TS_TZ_RANGE;
}

return null;
}
}
12 changes: 12 additions & 0 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ final class Schema extends AbstractSchema implements ConstraintFinderInterface
use ViewFinderTrait;

public const TYPE_JSONB = 'jsonb';
public const TYPE_INT_4_RANGE = 'int4range';
public const TYPE_INT_8_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';

/**
* @var array<array-key, string> mapping from physical column types (keys) to abstract column types (values).
Expand Down Expand Up @@ -159,6 +165,12 @@ final class Schema extends AbstractSchema implements ConstraintFinderInterface
'json' => self::TYPE_JSON,
'jsonb' => self::TYPE_JSON,
'xml' => self::TYPE_STRING,
'int4range' => self::TYPE_INT_4_RANGE,
'int8range' => self::TYPE_INT_8_RANGE,
'numrange' => self::TYPE_NUM_RANGE,
'tsrange' => self::TYPE_TS_RANGE,
'tstzrange' => self::TYPE_TS_TZ_RANGE,
'daterange' => self::TYPE_DATE_RANGE,
];

/**
Expand Down
12 changes: 12 additions & 0 deletions tests/Fixture/postgres.sql
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ DROP TABLE IF EXISTS "T_constraints_2";
DROP TABLE IF EXISTS "T_constraints_1";
DROP TABLE IF EXISTS "T_upsert";
DROP TABLE IF EXISTS "T_upsert_1";
DROP TABLE IF EXISTS "ranges";

DROP SCHEMA IF EXISTS "schema1" CASCADE;
DROP SCHEMA IF EXISTS "schema2" CASCADE;
Expand Down Expand Up @@ -424,3 +425,14 @@ CREATE TABLE "T_upsert_1"
(
"a" INT NOT NULL PRIMARY KEY
);

CREATE TABLE "ranges"
(
"id" SERIAL NOT NULL PRIMARY KEY,
"int_range" int4range,
"bigint_range" int8range,
"num_range" numrange,
"ts_range" tsrange,
"ts_tz_range" tstzrange,
"date_range" daterange
);
Loading