Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@

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

use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\TokenType;
use MartinGeorgiev\Utils\DoctrineOrm;

/**
* Implementation of PostgreSQL json field retrieval, filtered by key (using ->).
*
* Supports both string keys for object property access and integer indices for array element access:
* - JSON_GET_FIELD(json_column, 'property_name') -> json_column->'property_name'
* - JSON_GET_FIELD(json_column, 0) -> json_column->0
*
* @see https://www.postgresql.org/docs/9.4/static/functions-json.html
* @since 0.1
*
Expand All @@ -17,7 +27,24 @@ class JsonGetField extends BaseFunction
protected function customizeFunction(): void
{
$this->setFunctionPrototype('(%s -> %s)');
$this->addNodeMapping('StringPrimary');
$this->addNodeMapping('StringPrimary');
}

protected function feedParserWithNodes(Parser $parser): void
{
$shouldUseLexer = DoctrineOrm::isPre219();

$nodeForJsonDocumentName = $parser->StringPrimary();
$parser->match($shouldUseLexer ? Lexer::T_COMMA : TokenType::T_COMMA);

// Second parameter can be either an index or a property name
try {
$nodeForJsonIndexOrPropertyName = $parser->ArithmeticPrimary();
} catch (QueryException) {
// If ArithmeticPrimary fails (e.g., when encountering a property name rather than an index), try StringPrimary
$nodeForJsonIndexOrPropertyName = $parser->StringPrimary();
}

/* @phpstan-ignore-next-line assign.propertyType */
$this->nodes = [$nodeForJsonDocumentName, $nodeForJsonIndexOrPropertyName];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
/**
* Implementation of PostgreSQL json field retrieval as integer, filtered by key (using ->> and type casting to BIGINT).
*
* Supports both string keys for object property access and integer indices for array element access:
* - JSON_GET_FIELD_AS_INTEGER(json_column, 'property_name') -> CAST(json_column->>'property_name' as BIGINT)
* - JSON_GET_FIELD_AS_INTEGER(json_column, 0) -> CAST(json_column->>0 as BIGINT)
*
* @see https://www.postgresql.org/docs/9.4/static/functions-json.html
* @since 0.3
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonGetFieldAsInteger extends BaseFunction
class JsonGetFieldAsInteger extends JsonGetField
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('CAST(%s ->> %s as BIGINT)');
$this->addNodeMapping('StringPrimary');
$this->addNodeMapping('StringPrimary');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
/**
* Implementation of PostgreSQL json field retrieval as text, filtered by key (using ->>).
*
* Supports both string keys for object property access and integer indices for array element access:
* - JSON_GET_FIELD_AS_TEXT(json_column, 'property_name') -> json_column->>'property_name'
* - JSON_GET_FIELD_AS_TEXT(json_column, 0) -> json_column->>0
*
* @see https://www.postgresql.org/docs/9.4/static/functions-json.html
* @since 0.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonGetFieldAsText extends BaseFunction
class JsonGetFieldAsText extends JsonGetField
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('(%s ->> %s)');
$this->addNodeMapping('StringPrimary');
$this->addNodeMapping('StringPrimary');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

namespace Tests\Integration\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger;

class JsonGetFieldAsIntegerTest extends JsonTestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_GET_FIELD' => JsonGetField::class,
'JSON_GET_FIELD_AS_INTEGER' => JsonGetFieldAsInteger::class,
];
}
Expand All @@ -22,6 +24,18 @@ public function test_json_get_field_as_integer(): void
$this->assertSame(30, $result[0]['result']);
}

public function test_json_get_field_as_integer_with_index(): void
{
// First, let's insert test data with numeric arrays
$this->connection->executeStatement(
\sprintf("UPDATE %s.containsjsons SET object1 = '{\"scores\": [85, 92, 78]}' WHERE id = 1", self::DATABASE_SCHEMA)
);

$dql = "SELECT JSON_GET_FIELD_AS_INTEGER(JSON_GET_FIELD(t.object1, 'scores'), 1) as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame(92, $result[0]['result']);
}

public function test_json_get_field_as_integer_empty_object(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_INTEGER(t.object1, 'age') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 4";
Expand All @@ -36,7 +50,7 @@ public function test_json_get_field_as_integer_null_value(): void
$this->assertNull($result[0]['result']);
}

public function test_json_get_field_as_integer_nonexistent_field(): void
public function test_json_get_field_as_integer_nonexistent_property_name(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_INTEGER(t.object1, 'nonexistent') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Tests\Integration\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText;

class JsonGetFieldAsTextTest extends JsonTestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_GET_FIELD' => JsonGetField::class,
'JSON_GET_FIELD_AS_TEXT' => JsonGetFieldAsText::class,
];
}

public function test_json_get_field_as_text_with_property_name(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_TEXT(t.object1, 'name') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame('John', $result[0]['result']);
}

public function test_json_get_field_as_text_with_index(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_TEXT(JSON_GET_FIELD(t.object1, 'tags'), 0) as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame('developer', $result[0]['result']);
}

public function test_json_get_field_as_text_nested_access(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_TEXT(JSON_GET_FIELD(t.object1, 'address'), 'city') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame('New York', $result[0]['result']);
}

public function test_json_get_field_as_text_with_null_value(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_TEXT(t.object1, 'age') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 5";
$result = $this->executeDqlQuery($dql);
$this->assertNull($result[0]['result']);
}

public function test_json_get_field_as_text_with_nonexistent_index(): void
{
$dql = "SELECT JSON_GET_FIELD_AS_TEXT(JSON_GET_FIELD(t.object1, 'tags'), 10) as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertNull($result[0]['result']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Tests\Integration\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;

class JsonGetFieldTest extends JsonTestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_GET_FIELD' => JsonGetField::class,
];
}

public function test_json_get_field_with_property_name(): void
{
$dql = "SELECT JSON_GET_FIELD(t.object1, 'name') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame('"John"', $result[0]['result']);
}

public function test_json_get_field_with_index(): void
{
$dql = "SELECT JSON_GET_FIELD(JSON_GET_FIELD(t.object1, 'tags'), 0) as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame('"developer"', $result[0]['result']);
}

public function test_json_get_field_nested_object_access(): void
{
$dql = "SELECT JSON_GET_FIELD(JSON_GET_FIELD(t.object1, 'address'), 'city') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertSame('"New York"', $result[0]['result']);
}

public function test_json_get_field_with_empty_array(): void
{
$dql = "SELECT JSON_GET_FIELD(JSON_GET_FIELD(t.object1, 'tags'), 0) as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 3";
$result = $this->executeDqlQuery($dql);
$this->assertNull($result[0]['result']);
}

public function test_json_get_field_with_nonexistent_index(): void
{
$dql = "SELECT JSON_GET_FIELD(JSON_GET_FIELD(t.object1, 'tags'), 10) as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertNull($result[0]['result']);
}

public function test_json_get_field_with_nonexistent_property_name(): void
{
$dql = "SELECT JSON_GET_FIELD(t.object1, 'nonexistent') as result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsJsons t WHERE t.id = 1";
$result = $this->executeDqlQuery($dql);
$this->assertNull($result[0]['result']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@
namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger;

class JsonGetFieldAsIntegerTest extends TestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_GET_FIELD' => JsonGetField::class,
'JSON_GET_FIELD_AS_INTEGER' => JsonGetFieldAsInteger::class,
];
}

protected function getExpectedSqlStatements(): array
{
return [
"SELECT CAST(c0_.object1 ->> 'rank' as BIGINT) AS sclr_0 FROM ContainsJsons c0_",
'extracts field as integer' => "SELECT CAST(c0_.object1 ->> 'rank' as BIGINT) AS sclr_0 FROM ContainsJsons c0_",
'extracts array element as integer' => 'SELECT CAST(c0_.object1 ->> 0 as BIGINT) AS sclr_0 FROM ContainsJsons c0_',
'extracts nested array element as integer' => "SELECT CAST((c0_.object1 -> 'scores') ->> 1 as BIGINT) AS sclr_0 FROM ContainsJsons c0_",
];
}

protected function getDqlStatements(): array
{
return [
\sprintf("SELECT JSON_GET_FIELD_AS_INTEGER(e.object1, 'rank') FROM %s e", ContainsJsons::class),
'extracts field as integer' => \sprintf("SELECT JSON_GET_FIELD_AS_INTEGER(e.object1, 'rank') FROM %s e", ContainsJsons::class),
'extracts array element as integer' => \sprintf('SELECT JSON_GET_FIELD_AS_INTEGER(e.object1, 0) FROM %s e', ContainsJsons::class),
'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),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@
namespace Tests\Unit\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText;

class JsonGetFieldAsTextTest extends TestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_GET_FIELD' => JsonGetField::class,
'JSON_GET_FIELD_AS_TEXT' => JsonGetFieldAsText::class,
];
}

protected function getExpectedSqlStatements(): array
{
return [
"SELECT (c0_.object1 ->> 'country') AS sclr_0 FROM ContainsJsons c0_",
'extracts field as text' => "SELECT (c0_.object1 ->> 'country') AS sclr_0 FROM ContainsJsons c0_",
'extracts array element as text' => 'SELECT (c0_.object1 ->> 0) AS sclr_0 FROM ContainsJsons c0_',
'extracts nested array element as text' => "SELECT ((c0_.object1 -> 'tags') ->> 1) AS sclr_0 FROM ContainsJsons c0_",
];
}

protected function getDqlStatements(): array
{
return [
\sprintf("SELECT JSON_GET_FIELD_AS_TEXT(e.object1, 'country') FROM %s e", ContainsJsons::class),
'extracts field as text' => \sprintf("SELECT JSON_GET_FIELD_AS_TEXT(e.object1, 'country') FROM %s e", ContainsJsons::class),
'extracts array element as text' => \sprintf('SELECT JSON_GET_FIELD_AS_TEXT(e.object1, 0) FROM %s e', ContainsJsons::class),
'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),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ protected function getExpectedSqlStatements(): array
return [
'extracts top-level field from json' => "SELECT (c0_.object1 -> 'key') AS sclr_0 FROM ContainsJsons c0_",
'extracts nested field from json' => "SELECT ((c0_.object1 -> 'nested') -> 'key') AS sclr_0 FROM ContainsJsons c0_",
'extracts array element by index' => 'SELECT (c0_.object1 -> 0) AS sclr_0 FROM ContainsJsons c0_',
'extracts nested array element by index' => "SELECT ((c0_.object1 -> 'tags') -> 1) AS sclr_0 FROM ContainsJsons c0_",
'extracts field from array element by index' => "SELECT ((c0_.object1 -> 0) -> 'name') AS sclr_0 FROM ContainsJsons c0_",
];
}

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