Skip to content

Commit 311885f

Browse files
authored
Support JSON type (#303)
1 parent 42ea454 commit 311885f

23 files changed

+305
-104
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ jobs:
6363
--health-retries 10
6464
6565
steps:
66+
- name: Configure Database.
67+
run: docker exec -i oci bash -c "sqlplus -s system/root@XE <<< 'ALTER USER system DEFAULT TABLESPACE USERS;'"
68+
6669
- name: Checkout.
6770
uses: actions/checkout@v3
6871

.github/workflows/mutation.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ jobs:
5252
--health-retries 10
5353
5454
steps:
55+
- name: Configure Database.
56+
run: docker exec -i oci bash -c "sqlplus -s system/root@XE <<< 'ALTER USER system DEFAULT TABLESPACE USERS;'"
57+
5558
- name: Checkout.
5659
uses: actions/checkout@v3
5760

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- Enh #299: Convert database types to lower case (@Tigrov)
3232
- Enh #300: Replace `DbArrayHelper::getColumn()` with `array_column()` (@Tigrov)
3333
- New #301: Add `IndexType` class (@Tigrov)
34+
- New #303: Support JSON type (@Tigrov)
3435
- Bug #305: Explicitly mark nullable parameters (@vjik)
3536

3637
## 1.3.0 March 21, 2024

phpunit.xml.dist

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<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">
3-
<coverage/>
2+
<phpunit
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
colors="true"
5+
bootstrap="vendor/autoload.php"
6+
failOnRisky="true"
7+
failOnWarning="true"
8+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/schema/10.4.xsd"
9+
>
410
<php>
511
<ini name="error_reporting" value="-1"/>
612
</php>

src/Column/ColumnBuilder.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
namespace Yiisoft\Db\Oracle\Column;
66

77
use Yiisoft\Db\Constant\ColumnType;
8-
use Yiisoft\Db\Schema\Column\ColumnInterface;
98

109
final class ColumnBuilder extends \Yiisoft\Db\Schema\Column\ColumnBuilder
1110
{
12-
public static function binary(int|null $size = null): ColumnInterface
11+
public static function binary(int|null $size = null): BinaryColumn
1312
{
1413
return new BinaryColumn(ColumnType::BINARY, size: $size);
1514
}
15+
16+
public static function json(): JsonColumn
17+
{
18+
return new JsonColumn(ColumnType::JSON);
19+
}
1620
}

src/Column/ColumnDefinitionBuilder.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ public function build(ColumnInterface $column): string
4949
. $this->buildExtra($column);
5050
}
5151

52+
protected function buildCheck(ColumnInterface $column): string
53+
{
54+
$check = $column->getCheck();
55+
56+
if (empty($check)) {
57+
$name = $column->getName();
58+
59+
if (empty($name) || version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')) {
60+
return '';
61+
}
62+
63+
return match ($column->getType()) {
64+
ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
65+
' CHECK (' . $this->queryBuilder->quoter()->quoteSimpleColumnName($name) . ' IS JSON)',
66+
default => '',
67+
};
68+
}
69+
70+
return " CHECK ($check)";
71+
}
72+
5273
protected function buildOnDelete(string $onDelete): string
5374
{
5475
return match ($onDelete = strtoupper($onDelete)) {
@@ -96,9 +117,10 @@ protected function getDbType(ColumnInterface $column): string
96117
ColumnType::TIMESTAMP => 'timestamp',
97118
ColumnType::DATE => 'date',
98119
ColumnType::TIME => 'interval day(0) to second',
99-
ColumnType::ARRAY => 'clob',
100-
ColumnType::STRUCTURED => 'clob',
101-
ColumnType::JSON => 'clob',
120+
ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
121+
version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')
122+
? 'json'
123+
: 'clob',
102124
default => 'varchar2',
103125
},
104126
'timestamp with time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',

src/Column/ColumnDefinitionParser.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class ColumnDefinitionParser extends \Yiisoft\Db\Syntax\ColumnDefinitionPa
2222
. 'interval day\s*(?:\((\d+)\))? to second'
2323
. '|long raw'
2424
. '|\w*'
25-
. ')\s*(?:\(([^)]+)\))?\s*'
25+
. ')\s*(?:\(([^)]+)\))?(\[[\d\[\]]*\])?\s*'
2626
. '/i';
2727

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

51+
if (isset($matches[7])) {
52+
/** @psalm-var positive-int */
53+
$info['dimension'] = substr_count($matches[7], '[');
54+
}
55+
5156
$extra = substr($definition, strlen($matches[0]));
5257

5358
return $info + $this->extraInfo($extra);

src/Column/ColumnFactory.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Yiisoft\Db\Schema\Column\ColumnInterface;
1010

1111
use function rtrim;
12+
use function strcasecmp;
1213

1314
final class ColumnFactory extends AbstractColumnFactory
1415
{
@@ -43,6 +44,7 @@ final class ColumnFactory extends AbstractColumnFactory
4344
'timestamp with local time zone' => ColumnType::TIMESTAMP,
4445
'interval day to second' => ColumnType::STRING,
4546
'interval year to month' => ColumnType::STRING,
47+
'json' => ColumnType::JSON,
4648

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

68+
if (isset($info['check'], $info['name']) && strcasecmp($info['check'], '"' . $info['name'] . '" is json') === 0) {
69+
return ColumnType::JSON;
70+
}
71+
6672
if ($dbType === 'interval day to second' && isset($info['scale']) && $info['scale'] === 0) {
6773
return ColumnType::TIME;
6874
}
@@ -72,11 +78,11 @@ protected function getType(string $dbType, array $info = []): string
7278

7379
protected function getColumnClass(string $type, array $info = []): string
7480
{
75-
if ($type === ColumnType::BINARY) {
76-
return BinaryColumn::class;
77-
}
78-
79-
return parent::getColumnClass($type, $info);
81+
return match ($type) {
82+
ColumnType::BINARY => BinaryColumn::class,
83+
ColumnType::JSON => JsonColumn::class,
84+
default => parent::getColumnClass($type, $info),
85+
};
8086
}
8187

8288
protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed

src/Column/JsonColumn.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Db\Oracle\Column;
6+
7+
use Yiisoft\Db\Schema\Column\AbstractJsonColumn;
8+
9+
use function is_resource;
10+
use function is_string;
11+
use function json_decode;
12+
use function stream_get_contents;
13+
14+
use const JSON_THROW_ON_ERROR;
15+
16+
/**
17+
* Represents a JSON column with eager parsing values retrieved from the database.
18+
*/
19+
final class JsonColumn extends AbstractJsonColumn
20+
{
21+
/**
22+
* @throws \JsonException
23+
*/
24+
public function phpTypecast(mixed $value): mixed
25+
{
26+
if (is_string($value)) {
27+
return json_decode($value, true, 512, JSON_THROW_ON_ERROR);
28+
}
29+
30+
if (is_resource($value)) {
31+
return json_decode(stream_get_contents($value), true, 512, JSON_THROW_ON_ERROR);
32+
}
33+
34+
return $value;
35+
}
36+
}

src/Schema.php

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
* nullable: string,
4646
* data_default: string|null,
4747
* constraint_type: string|null,
48+
* check: string|null,
4849
* column_comment: string|null,
4950
* schema: string,
5051
* table: string
@@ -331,6 +332,22 @@ protected function findColumns(TableSchemaInterface $table): bool
331332
$tableName = $table->getName();
332333

333334
$sql = <<<SQL
335+
WITH C AS (
336+
SELECT AC.CONSTRAINT_TYPE, AC.SEARCH_CONDITION, ACC.COLUMN_NAME
337+
FROM ALL_CONSTRAINTS AC
338+
INNER JOIN ALL_CONS_COLUMNS ACC
339+
ON ACC.OWNER = AC.OWNER
340+
AND ACC.TABLE_NAME = AC.TABLE_NAME
341+
AND ACC.CONSTRAINT_NAME = AC.CONSTRAINT_NAME
342+
LEFT JOIN ALL_CONS_COLUMNS ACC2
343+
ON ACC2.OWNER = AC.OWNER
344+
AND ACC2.TABLE_NAME = AC.TABLE_NAME
345+
AND ACC2.CONSTRAINT_NAME = AC.CONSTRAINT_NAME
346+
AND ACC2.COLUMN_NAME != ACC.COLUMN_NAME
347+
WHERE AC.OWNER = :schemaName2
348+
AND AC.TABLE_NAME = :tableName2
349+
AND (AC.CONSTRAINT_TYPE = 'P' OR AC.CONSTRAINT_TYPE IN ('U', 'C') AND ACC2.COLUMN_NAME IS NULL)
350+
)
334351
SELECT
335352
A.COLUMN_NAME,
336353
A.DATA_TYPE,
@@ -339,7 +356,8 @@ protected function findColumns(TableSchemaInterface $table): bool
339356
(CASE WHEN A.CHAR_LENGTH > 0 THEN A.CHAR_LENGTH ELSE A.DATA_PRECISION END) AS "size",
340357
A.NULLABLE,
341358
A.DATA_DEFAULT,
342-
AC.CONSTRAINT_TYPE,
359+
C.CONSTRAINT_TYPE,
360+
C2.SEARCH_CONDITION AS "check",
343361
COM.COMMENTS AS COLUMN_COMMENT
344362
FROM ALL_TAB_COLUMNS A
345363
INNER JOIN ALL_OBJECTS B
@@ -349,26 +367,12 @@ protected function findColumns(TableSchemaInterface $table): bool
349367
ON COM.OWNER = A.OWNER
350368
AND COM.TABLE_NAME = A.TABLE_NAME
351369
AND COM.COLUMN_NAME = A.COLUMN_NAME
352-
LEFT JOIN ALL_CONSTRAINTS AC
353-
ON AC.OWNER = A.OWNER
354-
AND AC.TABLE_NAME = A.TABLE_NAME
355-
AND (AC.CONSTRAINT_TYPE = 'P'
356-
OR AC.CONSTRAINT_TYPE = 'U'
357-
AND (
358-
SELECT COUNT(*)
359-
FROM ALL_CONS_COLUMNS UCC
360-
WHERE UCC.CONSTRAINT_NAME = AC.CONSTRAINT_NAME
361-
AND UCC.TABLE_NAME = AC.TABLE_NAME
362-
AND UCC.OWNER = AC.OWNER
363-
) = 1
364-
)
365-
AND AC.CONSTRAINT_NAME IN (
366-
SELECT ACC.CONSTRAINT_NAME
367-
FROM ALL_CONS_COLUMNS ACC
368-
WHERE ACC.OWNER = A.OWNER
369-
AND ACC.TABLE_NAME = A.TABLE_NAME
370-
AND ACC.COLUMN_NAME = A.COLUMN_NAME
371-
)
370+
LEFT JOIN C
371+
ON C.COLUMN_NAME = A.COLUMN_NAME
372+
AND C.CONSTRAINT_TYPE IN ('P', 'U')
373+
LEFT JOIN C C2
374+
ON C2.COLUMN_NAME = A.COLUMN_NAME
375+
AND C2.CONSTRAINT_TYPE = 'C'
372376
WHERE A.OWNER = :schemaName
373377
AND A.TABLE_NAME = :tableName
374378
AND B.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
@@ -377,7 +381,9 @@ protected function findColumns(TableSchemaInterface $table): bool
377381

378382
$columns = $this->db->createCommand($sql, [
379383
':schemaName' => $schemaName,
384+
':schemaName2' => $schemaName,
380385
':tableName' => $tableName,
386+
':tableName2' => $tableName,
381387
])->queryAll();
382388

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

454460
return $this->getColumnFactory()->fromDbType($dbType, [
455461
'autoIncrement' => $info['identity_column'] === 'YES',
462+
'check' => $info['check'],
456463
'comment' => $info['column_comment'],
457464
'defaultValueRaw' => $info['data_default'],
458465
'name' => $info['column_name'],

0 commit comments

Comments
 (0)