Skip to content

Commit 9e70791

Browse files
feat(#401): improve JSON field extraction by adding index support
1 parent 1276f8d commit 9e70791

File tree

6 files changed

+59
-12
lines changed

6 files changed

+59
-12
lines changed

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonGetField.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@
44

55
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

7+
use Doctrine\ORM\Query\Lexer;
8+
use Doctrine\ORM\Query\Parser;
9+
use Doctrine\ORM\Query\QueryException;
10+
use Doctrine\ORM\Query\TokenType;
11+
use MartinGeorgiev\Utils\DoctrineOrm;
12+
713
/**
814
* Implementation of PostgreSQL json field retrieval, filtered by key (using ->).
915
*
16+
* Supports both string keys for object property access and integer indices for array element access:
17+
* - JSON_GET_FIELD(json_column, 'property_name') -> json_column->'property_name'
18+
* - JSON_GET_FIELD(json_column, 0) -> json_column->0
19+
*
1020
* @see https://www.postgresql.org/docs/9.4/static/functions-json.html
1121
* @since 0.1
1222
*
@@ -17,7 +27,22 @@ class JsonGetField extends BaseFunction
1727
protected function customizeFunction(): void
1828
{
1929
$this->setFunctionPrototype('(%s -> %s)');
20-
$this->addNodeMapping('StringPrimary');
21-
$this->addNodeMapping('StringPrimary');
30+
}
31+
32+
protected function feedParserWithNodes(Parser $parser): void
33+
{
34+
$shouldUseLexer = DoctrineOrm::isPre219();
35+
36+
// Parse first parameter (always StringPrimary for the JSON column)
37+
$this->nodes[0] = $parser->StringPrimary();
38+
$parser->match($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA);
39+
40+
// Parse second parameter - try ArithmeticPrimary first, then StringPrimary
41+
try {
42+
$this->nodes[1] = $parser->ArithmeticPrimary();
43+
} catch (QueryException) {
44+
// If ArithmeticPrimary fails (e.g., when encountering a string), try StringPrimary
45+
$this->nodes[1] = $parser->StringPrimary();
46+
}
2247
}
2348
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonGetFieldAsInteger.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
/**
88
* Implementation of PostgreSQL json field retrieval as integer, filtered by key (using ->> and type casting to BIGINT).
99
*
10+
* Supports both string keys for object property access and integer indices for array element access:
11+
* - JSON_GET_FIELD_AS_INTEGER(json_column, 'property_name') -> CAST(json_column->>'property_name' as BIGINT)
12+
* - JSON_GET_FIELD_AS_INTEGER(json_column, 0) -> CAST(json_column->>0 as BIGINT)
13+
*
1014
* @see https://www.postgresql.org/docs/9.4/static/functions-json.html
1115
* @since 0.3
1216
*
1317
* @author Martin Georgiev <martin.georgiev@gmail.com>
1418
*/
15-
class JsonGetFieldAsInteger extends BaseFunction
19+
class JsonGetFieldAsInteger extends JsonGetField
1620
{
1721
protected function customizeFunction(): void
1822
{
1923
$this->setFunctionPrototype('CAST(%s ->> %s as BIGINT)');
20-
$this->addNodeMapping('StringPrimary');
21-
$this->addNodeMapping('StringPrimary');
2224
}
2325
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonGetFieldAsText.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
/**
88
* Implementation of PostgreSQL json field retrieval as text, filtered by key (using ->>).
99
*
10+
* Supports both string keys for object property access and integer indices for array element access:
11+
* - JSON_GET_FIELD_AS_TEXT(json_column, 'property_name') -> json_column->>'property_name'
12+
* - JSON_GET_FIELD_AS_TEXT(json_column, 0) -> json_column->>0
13+
*
1014
* @see https://www.postgresql.org/docs/9.4/static/functions-json.html
1115
* @since 0.1
1216
*
1317
* @author Martin Georgiev <martin.georgiev@gmail.com>
1418
*/
15-
class JsonGetFieldAsText extends BaseFunction
19+
class JsonGetFieldAsText extends JsonGetField
1620
{
1721
protected function customizeFunction(): void
1822
{
1923
$this->setFunctionPrototype('(%s ->> %s)');
20-
$this->addNodeMapping('StringPrimary');
21-
$this->addNodeMapping('StringPrimary');
2224
}
2325
}

tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonGetFieldAsIntegerTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,34 @@
55
namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;
89
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger;
910

1011
class JsonGetFieldAsIntegerTest extends TestCase
1112
{
1213
protected function getStringFunctions(): array
1314
{
1415
return [
16+
'JSON_GET_FIELD' => JsonGetField::class,
1517
'JSON_GET_FIELD_AS_INTEGER' => JsonGetFieldAsInteger::class,
1618
];
1719
}
1820

1921
protected function getExpectedSqlStatements(): array
2022
{
2123
return [
22-
"SELECT CAST(c0_.object1 ->> 'rank' as BIGINT) AS sclr_0 FROM ContainsJsons c0_",
24+
'extracts field as integer' => "SELECT CAST(c0_.object1 ->> 'rank' as BIGINT) AS sclr_0 FROM ContainsJsons c0_",
25+
'extracts array element as integer' => 'SELECT CAST(c0_.object1 ->> 0 as BIGINT) AS sclr_0 FROM ContainsJsons c0_',
26+
'extracts nested array element as integer' => "SELECT CAST((c0_.object1 -> 'scores') ->> 1 as BIGINT) AS sclr_0 FROM ContainsJsons c0_",
2327
];
2428
}
2529

2630
protected function getDqlStatements(): array
2731
{
2832
return [
29-
\sprintf("SELECT JSON_GET_FIELD_AS_INTEGER(e.object1, 'rank') FROM %s e", ContainsJsons::class),
33+
'extracts field as integer' => \sprintf("SELECT JSON_GET_FIELD_AS_INTEGER(e.object1, 'rank') FROM %s e", ContainsJsons::class),
34+
'extracts array element as integer' => \sprintf('SELECT JSON_GET_FIELD_AS_INTEGER(e.object1, 0) FROM %s e', ContainsJsons::class),
35+
'extracts nested array element as integer' => \sprintf("SELECT JSON_GET_FIELD_AS_INTEGER(JSON_GET_FIELD(e.object1, 'scores'), 1) FROM %s e", ContainsJsons::class),
3036
];
3137
}
3238
}

tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonGetFieldAsTextTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,34 @@
55
namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;
89
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText;
910

1011
class JsonGetFieldAsTextTest extends TestCase
1112
{
1213
protected function getStringFunctions(): array
1314
{
1415
return [
16+
'JSON_GET_FIELD' => JsonGetField::class,
1517
'JSON_GET_FIELD_AS_TEXT' => JsonGetFieldAsText::class,
1618
];
1719
}
1820

1921
protected function getExpectedSqlStatements(): array
2022
{
2123
return [
22-
"SELECT (c0_.object1 ->> 'country') AS sclr_0 FROM ContainsJsons c0_",
24+
'extracts field as text' => "SELECT (c0_.object1 ->> 'country') AS sclr_0 FROM ContainsJsons c0_",
25+
'extracts array element as text' => 'SELECT (c0_.object1 ->> 0) AS sclr_0 FROM ContainsJsons c0_',
26+
'extracts nested array element as text' => "SELECT ((c0_.object1 -> 'tags') ->> 1) AS sclr_0 FROM ContainsJsons c0_",
2327
];
2428
}
2529

2630
protected function getDqlStatements(): array
2731
{
2832
return [
29-
\sprintf("SELECT JSON_GET_FIELD_AS_TEXT(e.object1, 'country') FROM %s e", ContainsJsons::class),
33+
'extracts field as text' => \sprintf("SELECT JSON_GET_FIELD_AS_TEXT(e.object1, 'country') FROM %s e", ContainsJsons::class),
34+
'extracts array element as text' => \sprintf('SELECT JSON_GET_FIELD_AS_TEXT(e.object1, 0) FROM %s e', ContainsJsons::class),
35+
'extracts nested array element as text' => \sprintf("SELECT JSON_GET_FIELD_AS_TEXT(JSON_GET_FIELD(e.object1, 'tags'), 1) FROM %s e", ContainsJsons::class),
3036
];
3137
}
3238
}

tests/Unit/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonGetFieldTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ protected function getExpectedSqlStatements(): array
2121
return [
2222
'extracts top-level field from json' => "SELECT (c0_.object1 -> 'key') AS sclr_0 FROM ContainsJsons c0_",
2323
'extracts nested field from json' => "SELECT ((c0_.object1 -> 'nested') -> 'key') AS sclr_0 FROM ContainsJsons c0_",
24+
'extracts array element by index' => 'SELECT (c0_.object1 -> 0) AS sclr_0 FROM ContainsJsons c0_',
25+
'extracts nested array element by index' => "SELECT ((c0_.object1 -> 'tags') -> 1) AS sclr_0 FROM ContainsJsons c0_",
26+
'extracts field from array element by index' => "SELECT ((c0_.object1 -> 0) -> 'name') AS sclr_0 FROM ContainsJsons c0_",
2427
];
2528
}
2629

@@ -29,6 +32,9 @@ protected function getDqlStatements(): array
2932
return [
3033
'extracts top-level field from json' => \sprintf("SELECT JSON_GET_FIELD(e.object1, 'key') FROM %s e", ContainsJsons::class),
3134
'extracts nested field from json' => \sprintf("SELECT JSON_GET_FIELD(JSON_GET_FIELD(e.object1, 'nested'), 'key') FROM %s e", ContainsJsons::class),
35+
'extracts array element by index' => \sprintf('SELECT JSON_GET_FIELD(e.object1, 0) FROM %s e', ContainsJsons::class),
36+
'extracts nested array element by index' => \sprintf("SELECT JSON_GET_FIELD(JSON_GET_FIELD(e.object1, 'tags'), 1) FROM %s e", ContainsJsons::class),
37+
'extracts field from array element by index' => \sprintf("SELECT JSON_GET_FIELD(JSON_GET_FIELD(e.object1, 0), 'name') FROM %s e", ContainsJsons::class),
3238
];
3339
}
3440
}

0 commit comments

Comments
 (0)