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

Support JSON type #303

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
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
13 changes: 6 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ jobs:
--health-retries 10

steps:
- name: Configure Database.
run: docker exec -i oci bash -c "sqlplus -s system/root@XE <<< 'ALTER USER system DEFAULT TABLESPACE USERS;'"

- name: Checkout.
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Install PHP with extensions.
uses: shivammathur/setup-php@v2
Expand All @@ -73,7 +76,6 @@ jobs:
extensions: ${{ env.EXTENSIONS }}
ini-values: date.timezone='UTC'
coverage: pcov
tools: composer:v2

- name: Update composer.
run: composer self-update
Expand All @@ -90,11 +92,8 @@ jobs:
FULL_BRANCH_NAME: ${{ env.FULL_BRANCH_NAME }}
WORK_PACKAGE_URL: ${{ env.WORK_PACKAGE_URL }}

- name: Install dependencies with composer.
run: composer update --no-interaction --no-progress --optimize-autoloader --ansi

- name: Run tests with phpunit with code coverage.
run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always --display-warnings --display-deprecations
env:
YII_ORACLE_SID: XE
YII_ORACLE_DATABASE: YIITEST
Expand All @@ -104,7 +103,7 @@ jobs:
YII_ORACLE_PASSWORD: root

- name: Upload coverage to Codecov.
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
3 changes: 3 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
--health-retries 10

steps:
- name: Configure Database.
run: docker exec -i oci bash -c "sqlplus -s system/root@XE <<< 'ALTER USER system DEFAULT TABLESPACE USERS;'"

- name: Checkout.
uses: actions/checkout@v3

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- Enh #299: Convert database types to lower case (@Tigrov)
- Enh #300: Replace `DbArrayHelper::getColumn()` with `array_column()` (@Tigrov)
- New #301: Add `IndexType` class (@Tigrov)
- New #303: Support JSON type (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
10 changes: 8 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" colors="true" bootstrap="vendor/autoload.php" failOnRisky="true" failOnWarning="true" executionOrder="default" resolveDependencies="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd">
<coverage/>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/schema/10.4.xsd"
>
<php>
<ini name="error_reporting" value="-1"/>
</php>
Expand Down
8 changes: 6 additions & 2 deletions src/Column/ColumnBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
namespace Yiisoft\Db\Oracle\Column;

use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Schema\Column\ColumnInterface;

final class ColumnBuilder extends \Yiisoft\Db\Schema\Column\ColumnBuilder
{
public static function binary(int|null $size = null): ColumnInterface
public static function binary(int|null $size = null): BinaryColumn
{
return new BinaryColumn(ColumnType::BINARY, size: $size);
}

public static function json(): JsonColumn
{
return new JsonColumn(ColumnType::JSON);
}
}
28 changes: 25 additions & 3 deletions src/Column/ColumnDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@
. $this->buildExtra($column);
}

protected function buildCheck(ColumnInterface $column): string
{
$check = $column->getCheck();

if (empty($check)) {
$name = $column->getName();

if (empty($name) || version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')) {
return '';
}

return match ($column->getType()) {
ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
' CHECK (' . $this->queryBuilder->quoter()->quoteSimpleColumnName($name) . ' IS JSON)',
default => '',
};
}

return " CHECK ($check)";
}

protected function buildOnDelete(string $onDelete): string
{
return match ($onDelete = strtoupper($onDelete)) {
Expand Down Expand Up @@ -96,9 +117,10 @@
ColumnType::TIMESTAMP => 'timestamp',
ColumnType::DATE => 'date',
ColumnType::TIME => 'interval day(0) to second',
ColumnType::ARRAY => 'clob',
ColumnType::STRUCTURED => 'clob',
ColumnType::JSON => 'clob',
ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')
? 'json'

Check warning on line 122 in src/Column/ColumnDefinitionBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/Column/ColumnDefinitionBuilder.php#L122

Added line #L122 was not covered by tests
: 'clob',
default => 'varchar2',
},
'timestamp with time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',
Expand Down
7 changes: 6 additions & 1 deletion src/Column/ColumnDefinitionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class ColumnDefinitionParser extends \Yiisoft\Db\Syntax\ColumnDefinitionPa
. 'interval day\s*(?:\((\d+)\))? to second'
. '|long raw'
. '|\w*'
. ')\s*(?:\(([^)]+)\))?\s*'
. ')\s*(?:\(([^)]+)\))?(\[[\d\[\]]*\])?\s*'
. '/i';

public function parse(string $definition): array
Expand All @@ -48,6 +48,11 @@ public function parse(string $definition): array
$info += ['scale' => (int) $scale];
}

if (isset($matches[7])) {
/** @psalm-var positive-int */
$info['dimension'] = substr_count($matches[7], '[');
}

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

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

use function rtrim;
use function strcasecmp;

final class ColumnFactory extends AbstractColumnFactory
{
Expand Down Expand Up @@ -43,6 +44,7 @@ final class ColumnFactory extends AbstractColumnFactory
'timestamp with local time zone' => ColumnType::TIMESTAMP,
'interval day to second' => ColumnType::STRING,
'interval year to month' => ColumnType::STRING,
'json' => ColumnType::JSON,

/** Deprecated */
'long' => ColumnType::TEXT,
Expand All @@ -63,6 +65,10 @@ protected function getType(string $dbType, array $info = []): string
};
}

if (isset($info['check'], $info['name']) && strcasecmp($info['check'], '"' . $info['name'] . '" is json') === 0) {
return ColumnType::JSON;
}

if ($dbType === 'interval day to second' && isset($info['scale']) && $info['scale'] === 0) {
return ColumnType::TIME;
}
Expand All @@ -72,11 +78,11 @@ protected function getType(string $dbType, array $info = []): string

protected function getColumnClass(string $type, array $info = []): string
{
if ($type === ColumnType::BINARY) {
return BinaryColumn::class;
}

return parent::getColumnClass($type, $info);
return match ($type) {
ColumnType::BINARY => BinaryColumn::class,
ColumnType::JSON => JsonColumn::class,
default => parent::getColumnClass($type, $info),
};
}

protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed
Expand Down
36 changes: 36 additions & 0 deletions src/Column/JsonColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Oracle\Column;

use Yiisoft\Db\Schema\Column\AbstractJsonColumn;

use function is_resource;
use function is_string;
use function json_decode;
use function stream_get_contents;

use const JSON_THROW_ON_ERROR;

/**
* Represents a json column with eager parsing values retrieved from the database.
*/
final class JsonColumn extends AbstractJsonColumn
{
/**
* @throws \JsonException
*/
public function phpTypecast(mixed $value): mixed
{
if (is_string($value)) {
return json_decode($value, true, 512, JSON_THROW_ON_ERROR);
}

if (is_resource($value)) {
return json_decode(stream_get_contents($value), true, 512, JSON_THROW_ON_ERROR);
}

return $value;
}
}
49 changes: 28 additions & 21 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* nullable: string,
* data_default: string|null,
* constraint_type: string|null,
* check: string|null,
* column_comment: string|null,
* schema: string,
* table: string
Expand Down Expand Up @@ -331,6 +332,22 @@ protected function findColumns(TableSchemaInterface $table): bool
$tableName = $table->getName();

$sql = <<<SQL
WITH C AS (
SELECT AC.CONSTRAINT_TYPE, AC.SEARCH_CONDITION, ACC.COLUMN_NAME
FROM ALL_CONSTRAINTS AC
INNER JOIN ALL_CONS_COLUMNS ACC
ON ACC.OWNER = AC.OWNER
AND ACC.TABLE_NAME = AC.TABLE_NAME
AND ACC.CONSTRAINT_NAME = AC.CONSTRAINT_NAME
LEFT JOIN ALL_CONS_COLUMNS ACC2
ON ACC2.OWNER = AC.OWNER
AND ACC2.TABLE_NAME = AC.TABLE_NAME
AND ACC2.CONSTRAINT_NAME = AC.CONSTRAINT_NAME
AND ACC2.COLUMN_NAME != ACC.COLUMN_NAME
WHERE AC.OWNER = :schemaName2
AND AC.TABLE_NAME = :tableName2
AND (AC.CONSTRAINT_TYPE = 'P' OR AC.CONSTRAINT_TYPE IN ('U', 'C') AND ACC2.COLUMN_NAME IS NULL)
)
SELECT
A.COLUMN_NAME,
A.DATA_TYPE,
Expand All @@ -339,7 +356,8 @@ protected function findColumns(TableSchemaInterface $table): bool
(CASE WHEN A.CHAR_LENGTH > 0 THEN A.CHAR_LENGTH ELSE A.DATA_PRECISION END) AS "size",
A.NULLABLE,
A.DATA_DEFAULT,
AC.CONSTRAINT_TYPE,
C.CONSTRAINT_TYPE,
C2.SEARCH_CONDITION AS "check",
COM.COMMENTS AS COLUMN_COMMENT
FROM ALL_TAB_COLUMNS A
INNER JOIN ALL_OBJECTS B
Expand All @@ -349,26 +367,12 @@ protected function findColumns(TableSchemaInterface $table): bool
ON COM.OWNER = A.OWNER
AND COM.TABLE_NAME = A.TABLE_NAME
AND COM.COLUMN_NAME = A.COLUMN_NAME
LEFT JOIN ALL_CONSTRAINTS AC
ON AC.OWNER = A.OWNER
AND AC.TABLE_NAME = A.TABLE_NAME
AND (AC.CONSTRAINT_TYPE = 'P'
OR AC.CONSTRAINT_TYPE = 'U'
AND (
SELECT COUNT(*)
FROM ALL_CONS_COLUMNS UCC
WHERE UCC.CONSTRAINT_NAME = AC.CONSTRAINT_NAME
AND UCC.TABLE_NAME = AC.TABLE_NAME
AND UCC.OWNER = AC.OWNER
) = 1
)
AND AC.CONSTRAINT_NAME IN (
SELECT ACC.CONSTRAINT_NAME
FROM ALL_CONS_COLUMNS ACC
WHERE ACC.OWNER = A.OWNER
AND ACC.TABLE_NAME = A.TABLE_NAME
AND ACC.COLUMN_NAME = A.COLUMN_NAME
)
LEFT JOIN C
ON C.COLUMN_NAME = A.COLUMN_NAME
AND C.CONSTRAINT_TYPE IN ('P', 'U')
LEFT JOIN C C2
ON C2.COLUMN_NAME = A.COLUMN_NAME
AND C2.CONSTRAINT_TYPE = 'C'
WHERE A.OWNER = :schemaName
AND A.TABLE_NAME = :tableName
AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
Expand All @@ -377,7 +381,9 @@ protected function findColumns(TableSchemaInterface $table): bool

$columns = $this->db->createCommand($sql, [
':schemaName' => $schemaName,
':schemaName2' => $schemaName,
':tableName' => $tableName,
':tableName2' => $tableName,
])->queryAll();

if ($columns === []) {
Expand Down Expand Up @@ -453,6 +459,7 @@ private function loadColumn(array $info): ColumnInterface

return $this->getColumnFactory()->fromDbType($dbType, [
'autoIncrement' => $info['identity_column'] === 'YES',
'check' => $info['check'],
'comment' => $info['column_comment'],
'defaultValueRaw' => $info['data_default'],
'name' => $info['column_name'],
Expand Down
21 changes: 7 additions & 14 deletions tests/ColumnFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPUnit\Framework\Attributes\DataProviderExternal;
use Yiisoft\Db\Oracle\Tests\Provider\ColumnFactoryProvider;
use Yiisoft\Db\Oracle\Tests\Support\TestTrait;
use Yiisoft\Db\Schema\Column\ColumnInterface;
use Yiisoft\Db\Tests\AbstractColumnFactoryTest;

/**
Expand All @@ -23,23 +24,15 @@ public function testFromDbType(string $dbType, string $expectedType, string $exp
}

#[DataProviderExternal(ColumnFactoryProvider::class, 'definitions')]
public function testFromDefinition(
string $definition,
string $expectedType,
string $expectedInstanceOf,
array $expectedMethodResults = []
): void {
parent::testFromDefinition($definition, $expectedType, $expectedInstanceOf, $expectedMethodResults);
public function testFromDefinition(string $definition, ColumnInterface $expected): void
{
parent::testFromDefinition($definition, $expected);
}

#[DataProviderExternal(ColumnFactoryProvider::class, 'pseudoTypes')]
public function testFromPseudoType(
string $pseudoType,
string $expectedType,
string $expectedInstanceOf,
array $expectedMethodResults = []
): void {
parent::testFromPseudoType($pseudoType, $expectedType, $expectedInstanceOf, $expectedMethodResults);
public function testFromPseudoType(string $pseudoType, ColumnInterface $expected): void
{
parent::testFromPseudoType($pseudoType, $expected);
}

#[DataProviderExternal(ColumnFactoryProvider::class, 'types')]
Expand Down
Loading
Loading