Skip to content

Commit

Permalink
Fix ColumnDefinitionParser (#299)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored Jan 22, 2025
1 parent 3f867e3 commit c76d52f
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 70 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- Enh #279: Separate column type constants (@Tigrov)
- New #280, #291: Realize `ColumnBuilder` class (@Tigrov)
- Enh #281: Update according changes in `ColumnSchemaInterface` (@Tigrov)
- New #282, #291: Add `ColumnDefinitionBuilder` class (@Tigrov)
- New #282, #291, #299: Add `ColumnDefinitionBuilder` class (@Tigrov)
- Bug #285: Fix `DMLQueryBuilder::insertBatch()` method (@Tigrov)
- Enh #283: Refactor `Dsn` class (@Tigrov)
- Enh #286: Use constructor to create columns and initialize properties (@Tigrov)
Expand All @@ -27,6 +27,8 @@
- Enh #296: Remove `ColumnInterface` (@Tigrov)
- Enh #298: Rename `ColumnSchemaInterface` to `ColumnInterface` (@Tigrov)
- Enh #298: Refactor `DMLQueryBuilder::prepareInsertValues()` method (@Tigrov)
- Enh #299: Add `ColumnDefinitionParser` class (@Tigrov)
- Enh #299: Convert database types to lower case (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
2 changes: 1 addition & 1 deletion src/Column/BinaryColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class BinaryColumn extends BaseBinaryColumn
{
public function dbTypecast(mixed $value): mixed
{
if ($this->getDbType() === 'BLOB') {
if ($this->getDbType() === 'blob') {
if ($value instanceof ParamInterface && is_string($value->getValue())) {
/** @var string */
$value = $value->getValue();
Expand Down
63 changes: 36 additions & 27 deletions src/Column/ColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,37 +64,46 @@ protected function buildOnUpdate(string $onUpdate): string

protected function getDbType(ColumnInterface $column): string
{
$dbType = $column->getDbType();
$size = $column->getSize();
$scale = $column->getScale();

/** @psalm-suppress DocblockTypeContradiction */
return $column->getDbType() ?? match ($column->getType()) {
ColumnType::BOOLEAN => 'number(1)',
ColumnType::BIT => match (true) {
$size === null => 'number(38)',
$size <= 126 => 'number(' . ceil(log10(2 ** $size)) . ')',
default => 'raw(' . ceil($size / 8) . ')',
return match ($dbType) {
default => $dbType,
null => match ($column->getType()) {
ColumnType::BOOLEAN => 'number(1)',
ColumnType::BIT => match (true) {
$size === null => 'number(38)',
$size <= 126 => 'number(' . ceil(log10(2 ** $size)) . ')',
default => 'raw(' . ceil($size / 8) . ')',
},
ColumnType::TINYINT => 'number(' . ($size ?? 3) . ')',
ColumnType::SMALLINT => 'number(' . ($size ?? 5) . ')',
ColumnType::INTEGER => 'number(' . ($size ?? 10) . ')',
ColumnType::BIGINT => 'number(' . ($size ?? 20) . ')',
ColumnType::FLOAT => 'binary_float',
ColumnType::DOUBLE => 'binary_double',
ColumnType::DECIMAL => 'number(' . ($size ?? 10) . ',' . ($scale ?? 0) . ')',
ColumnType::MONEY => 'number(' . ($size ?? 19) . ',' . ($scale ?? 4) . ')',
ColumnType::CHAR => 'char',
ColumnType::STRING => 'varchar2(' . ($size ?? 255) . ')',
ColumnType::TEXT => 'clob',
ColumnType::BINARY => 'blob',
ColumnType::UUID => 'raw(16)',
ColumnType::DATETIME => 'timestamp',
ColumnType::TIMESTAMP => 'timestamp',
ColumnType::DATE => 'date',
ColumnType::TIME => 'interval day(0) to second',
ColumnType::ARRAY => 'clob',
ColumnType::STRUCTURED => 'clob',
ColumnType::JSON => 'clob',
default => 'varchar2',
},
ColumnType::TINYINT => 'number(' . ($size ?? 3) . ')',
ColumnType::SMALLINT => 'number(' . ($size ?? 5) . ')',
ColumnType::INTEGER => 'number(' . ($size ?? 10) . ')',
ColumnType::BIGINT => 'number(' . ($size ?? 20) . ')',
ColumnType::FLOAT => 'binary_float',
ColumnType::DOUBLE => 'binary_double',
ColumnType::DECIMAL => 'number(' . ($size ?? 10) . ',' . ($column->getScale() ?? 0) . ')',
ColumnType::MONEY => 'number(' . ($size ?? 19) . ',' . ($column->getScale() ?? 4) . ')',
ColumnType::CHAR => 'char',
ColumnType::STRING => 'varchar2(' . ($size ?? 255) . ')',
ColumnType::TEXT => 'clob',
ColumnType::BINARY => 'blob',
ColumnType::UUID => 'raw(16)',
ColumnType::DATETIME => 'timestamp',
ColumnType::TIMESTAMP => 'timestamp',
ColumnType::DATE => 'date',
ColumnType::TIME => 'interval day(0) to second',
ColumnType::ARRAY => 'clob',
ColumnType::STRUCTURED => 'clob',
ColumnType::JSON => 'clob',
default => 'varchar2',
'timestamp with time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',
'timestamp with local time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with local time zone',
'interval day to second' => 'interval day' . ($scale !== null ? "($scale)" : '') . ' to second' . ($size !== null ? "($size)" : ''),
'interval year to month' => 'interval year' . ($scale !== null ? "($scale)" : '') . ' to month',
};
}

Expand Down
55 changes: 55 additions & 0 deletions src/Column/ColumnDefinitionParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Column;

use function preg_match;
use function preg_replace;
use function strlen;
use function strtolower;
use function substr;

/**
* Parses column definition string. For example, `string(255)` or `int unsigned`.
*/
final class ColumnDefinitionParser extends \Yiisoft\Db\Syntax\ColumnDefinitionParser
{
private const TYPE_PATTERN = '/^('
. 'timestamp\s*(?:\((\d+)\))? with(?: local)? time zone'
. '|interval year\s*(?:\((\d+)\))? to month'
. ')|('
. 'interval day\s*(?:\((\d+)\))? to second'
. '|long raw'
. '|\w*'
. ')\s*(?:\(([^)]+)\))?\s*'
. '/i';

public function parse(string $definition): array
{
preg_match(self::TYPE_PATTERN, $definition, $matches);

$type = strtolower(preg_replace('/\s*\(\d+\)/', '', $matches[4] ?? $matches[1]));
$info = ['type' => $type];

$typeDetails = $matches[6] ?? $matches[2] ?? '';

if ($typeDetails !== '') {
if ($type === 'enum') {
$info += $this->enumInfo($typeDetails);
} else {
$info += $this->sizeInfo($typeDetails);
}
}

$scale = $matches[5] ?? $matches[3] ?? '';

if ($scale !== '') {
$info += ['scale' => (int) $scale];
}

$extra = substr($definition, strlen($matches[0]));

return $info + $this->extraInfo($extra);
}
}
18 changes: 9 additions & 9 deletions src/Column/ColumnFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
use Yiisoft\Db\Schema\Column\AbstractColumnFactory;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function preg_replace;
use function rtrim;
use function strtolower;

final class ColumnFactory extends AbstractColumnFactory
{
Expand Down Expand Up @@ -40,19 +38,23 @@ final class ColumnFactory extends AbstractColumnFactory
'binary_double' => ColumnType::DOUBLE, // 64 bit
'float' => ColumnType::DOUBLE, // 126 bit
'date' => ColumnType::DATE,
'interval day to second' => ColumnType::TIME,
'timestamp' => ColumnType::TIMESTAMP,
'timestamp with time zone' => ColumnType::TIMESTAMP,
'timestamp with local time zone' => ColumnType::TIMESTAMP,
'interval day to second' => ColumnType::STRING,
'interval year to month' => ColumnType::STRING,

/** Deprecated */
'long' => ColumnType::TEXT,
];

protected function getType(string $dbType, array $info = []): string
protected function columnDefinitionParser(): ColumnDefinitionParser
{
$dbType = strtolower($dbType);
return new ColumnDefinitionParser();
}

protected function getType(string $dbType, array $info = []): string
{
if ($dbType === 'number') {
return match ($info['scale'] ?? null) {
null => ColumnType::DOUBLE,
Expand All @@ -61,10 +63,8 @@ protected function getType(string $dbType, array $info = []): string
};
}

$dbType = preg_replace('/\([^)]+\)/', '', $dbType);

if ($dbType === 'interval day to second' && isset($info['size']) && $info['size'] > 0) {
return ColumnType::STRING;
if ($dbType === 'interval day to second' && isset($info['scale']) && $info['scale'] === 0) {
return ColumnType::TIME;
}

return parent::getType($dbType, $info);
Expand Down
2 changes: 1 addition & 1 deletion src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Yiisoft\Db\Oracle;

use PDO;
use Yiisoft\Db\Command\DataType;
use Yiisoft\Db\Constant\DataType;
use Yiisoft\Db\Constant\PhpType;
use Yiisoft\Db\Driver\Pdo\AbstractPdoCommand;
use Yiisoft\Db\QueryBuilder\AbstractQueryBuilder;
Expand Down
15 changes: 14 additions & 1 deletion src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
use function implode;
use function is_array;
use function md5;
use function preg_replace;
use function serialize;
use function strtolower;

/**
* Implements the Oracle Server specific schema, supporting Oracle Server 11C and above.
Expand Down Expand Up @@ -437,7 +439,18 @@ protected function getTableSequenceName(string $tableName): string|null
*/
private function loadColumn(array $info): ColumnInterface
{
return $this->getColumnFactory()->fromDbType($info['data_type'], [
$dbType = strtolower(preg_replace('/\([^)]+\)/', '', $info['data_type']));

match ($dbType) {
'timestamp',
'timestamp with time zone',
'timestamp with local time zone',
'interval day to second',
'interval year to month' => [$info['size'], $info['data_scale']] = [$info['data_scale'], $info['size']],
default => null,
};

return $this->getColumnFactory()->fromDbType($dbType, [
'autoIncrement' => $info['identity_column'] === 'YES',
'comment' => $info['column_comment'],
'defaultValueRaw' => $info['data_default'],
Expand Down
27 changes: 27 additions & 0 deletions tests/ColumnDefinitionParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Tests;

use Yiisoft\Db\Oracle\Column\ColumnDefinitionParser;
use Yiisoft\Db\Tests\AbstractColumnDefinitionParserTest;

/**
* @group oracle
*/
final class ColumnDefinitionParserTest extends AbstractColumnDefinitionParserTest
{
protected function createColumnDefinitionParser(): ColumnDefinitionParser
{
return new ColumnDefinitionParser();
}

/**
* @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\ColumnDefinitionParserProvider::parse
*/
public function testParse(string $definition, array $expected): void
{
parent::testParse($definition, $expected);
}
}
2 changes: 1 addition & 1 deletion tests/ColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function testDbTypecastColumns(string $className, array $values): void
public function testBinaryColumn(): void
{
$binaryCol = new BinaryColumn();
$binaryCol->dbType('BLOB');
$binaryCol->dbType('blob');

$this->assertInstanceOf(Expression::class, $binaryCol->dbTypecast("\x10\x11\x12"));
$this->assertInstanceOf(
Expand Down
24 changes: 24 additions & 0 deletions tests/Provider/ColumnDefinitionParserProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Tests\Provider;

class ColumnDefinitionParserProvider extends \Yiisoft\Db\Tests\Provider\ColumnDefinitionParserProvider
{
public static function parse(): array
{
return [
...parent::parse(),
['long raw', ['type' => 'long raw']],
['interval day to second', ['type' => 'interval day to second']],
['interval day to second (2)', ['type' => 'interval day to second', 'size' => 2]],
['interval day(0) to second(2)', ['type' => 'interval day to second', 'size' => 2, 'scale' => 0]],
['timestamp with time zone', ['type' => 'timestamp with time zone']],
['timestamp (3) with time zone', ['type' => 'timestamp with time zone', 'size' => 3]],
['timestamp(3) with local time zone', ['type' => 'timestamp with local time zone', 'size' => 3]],
['interval year to month', ['type' => 'interval year to month']],
['interval year (3) to month', ['type' => 'interval year to month', 'scale' => 3]],
];
}
}
14 changes: 12 additions & 2 deletions tests/Provider/ColumnFactoryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ public static function dbTypes(): array
['binary_double', ColumnType::DOUBLE, DoubleColumn::class],
['float', ColumnType::DOUBLE, DoubleColumn::class],
['date', ColumnType::DATE, StringColumn::class],
['interval day(0) to second', ColumnType::TIME, StringColumn::class],
['timestamp', ColumnType::TIMESTAMP, StringColumn::class],
['timestamp with time zone', ColumnType::TIMESTAMP, StringColumn::class],
['timestamp with local time zone', ColumnType::TIMESTAMP, StringColumn::class],
['timestamp with local time zone', ColumnType::TIMESTAMP, StringColumn::class],
['interval day to second', ColumnType::STRING, StringColumn::class],
['interval year to month', ColumnType::STRING, StringColumn::class],
];
}

Expand All @@ -52,7 +54,15 @@ public static function definitions(): array

unset($definitions['bigint UNSIGNED']);

return $definitions;
return [
...$definitions,
['interval day to second', ColumnType::STRING, StringColumn::class, ['getDbType' => 'interval day to second']],
['interval day(0) to second', ColumnType::TIME, StringColumn::class, ['getDbType' => 'interval day to second', 'getScale' => 0]],
['interval day (0) to second(6)', ColumnType::TIME, StringColumn::class, ['getDbType' => 'interval day to second', 'getScale' => 0, 'getSize' => 6]],
['interval day to second (0)', ColumnType::STRING, StringColumn::class, ['getDbType' => 'interval day to second', 'getSize' => 0]],
['interval year to month', ColumnType::STRING, StringColumn::class, ['getDbType' => 'interval year to month']],
['interval year (2) to month', ColumnType::STRING, StringColumn::class, ['getDbType' => 'interval year to month', 'getScale' => 2]],
];
}

public static function defaultValueRaw(): array
Expand Down
4 changes: 4 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ public static function buildColumnDefinition(): array

['number(10) REFERENCES "ref_table" ("id")', ColumnBuilder::integer()->reference($referenceRestrict)],
['number(10) REFERENCES "ref_table" ("id") ON DELETE SET NULL', ColumnBuilder::integer()->reference($referenceSetNull)],
['timestamp(3) with time zone', 'timestamp (3) with time zone'],
['timestamp with local time zone', 'timestamp with local time zone'],
['interval day(5) to second(6)', 'interval day(5) to second (6)'],
['interval year(8) to month', 'interval year(8) to month'],
];
}

Expand Down
Loading

0 comments on commit c76d52f

Please sign in to comment.