Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
18 changes: 18 additions & 0 deletions src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Cast.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function parse(Parser $parser): void
return;
}

// Handle parameterized types (e.g., DECIMAL(10, 2))
if ($lexer->isNextToken($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS)) {
$parser->match($shouldUseLexer ? Lexer::T_OPEN_PARENTHESIS : TokenType::T_OPEN_PARENTHESIS);
$parameter = $parser->Literal();
Expand All @@ -61,6 +62,23 @@ public function parse(Parser $parser): void
$type .= '('.\implode(', ', $parameters).')';
}

// Handle array types by checking if the next token is '['
// Since brackets are not recognized as specific tokens, we need to check the token value
$nextTokenValue = DoctrineLexer::getLookaheadValue($lexer);
if ($nextTokenValue === '[') {
// Consume the '[' token
/** @phpstan-ignore-next-line */
$parser->match($shouldUseLexer ? Lexer::T_NONE : TokenType::T_NONE);

// Check for the closing ']' token
$nextTokenValue = DoctrineLexer::getLookaheadValue($lexer);
if ($nextTokenValue === ']') {
/** @phpstan-ignore-next-line */
$parser->match($shouldUseLexer ? Lexer::T_NONE : TokenType::T_NONE);
$type .= '[]';
}
}

$this->targetType = $type;

$parser->match($shouldUseLexer ? Lexer::T_CLOSE_PARENTHESIS : TokenType::T_CLOSE_PARENTHESIS);
Expand Down
14 changes: 14 additions & 0 deletions src/MartinGeorgiev/Utils/DoctrineLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ public static function getLookaheadType(Lexer $lexer)
return $lexer->lookahead?->type;
}

/**
* @return mixed|null
*/
public static function getLookaheadValue(Lexer $lexer)
{
if (self::isPre200($lexer)) {
// @phpstan-ignore-next-line
return $lexer->lookahead['value'] ?? null;
}

// @phpstan-ignore-next-line
return $lexer->lookahead?->value;
}

/**
* @return mixed|null
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<?php

declare(strict_types=1);

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

use Doctrine\DBAL\Exception\DriverException;
use Doctrine\ORM\Query\QueryException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Cast;
use Tests\Integration\MartinGeorgiev\TestCase;

class CastTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createTestTableForTextFixture();
$this->createTestTableForArrayFixture();
$this->createTestTableForNumericFixture();
}

protected function getStringFunctions(): array
{
return [
'CAST' => Cast::class,
];
}

public function test_cast_text_to_integer(): void
{
$dql = 'SELECT CAST(t.text1 AS INTEGER) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertSame(123, $result[0]['result']);
}

public function test_cast_text_to_text(): void
{
$dql = 'SELECT CAST(t.text1 AS TEXT) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertSame('123', $result[0]['result']);
}

public function test_cast_text_to_boolean(): void
{
$dql = 'SELECT CAST(t.text2 AS BOOLEAN) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertTrue($result[0]['result']);
}

public function test_cast_text_to_decimal(): void
{
$dql = 'SELECT CAST(t.text1 AS DECIMAL(10, 2)) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertEquals('123.00', $result[0]['result']);
}

public function test_cast_array_to_text_array(): void
{
$dql = 'SELECT CAST(a.integerArray AS TEXT[]) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsArrays a WHERE a.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertIsString($result[0]['result']);
static::assertStringContainsString('{', $result[0]['result']);
}

public function test_cast_boolean_array_to_integer_array(): void
{
$dql = 'SELECT CAST(a.boolArray AS INTEGER[]) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsArrays a WHERE a.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertIsString($result[0]['result']);
static::assertStringContainsString('{', $result[0]['result']);
}

public function test_cast_with_where_condition(): void
{
$dql = 'SELECT t.id FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE CAST(t.text1 AS INTEGER) > 100';
$result = $this->executeDqlQuery($dql);
static::assertNotEmpty($result);
}

public function test_cast_in_complex_query(): void
{
$dql = 'SELECT t.id, CAST(t.text1 AS INTEGER) as casted_text FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id IN (1, 2, 3)';
$result = $this->executeDqlQuery($dql);
static::assertNotEmpty($result);
}

public function test_cast_numeric_to_integer(): void
{
$dql = 'SELECT CAST(n.decimal1 AS INTEGER) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsNumerics n WHERE n.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertSame(11, $result[0]['result']); // PostgreSQL rounds 10.5 to 11
}

public function test_cast_numeric_to_decimal(): void
{
$dql = 'SELECT CAST(n.integer1 AS DECIMAL(10, 2)) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsNumerics n WHERE n.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertEquals('10.00', $result[0]['result']);
}

public function test_cast_throws_with_invalid_type(): void
{
$this->expectException(DriverException::class);
$dql = "SELECT CAST('invalid' AS INVALID_TYPE) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1";
$this->executeDqlQuery($dql);
}

public function test_cast_throws_with_null_input(): void
{
$this->expectException(QueryException::class);
$dql = 'SELECT CAST(NULL AS INTEGER) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsTexts t WHERE t.id = 1';
$this->executeDqlQuery($dql);
}

public function test_cast_lowercase_array_types(): void
{
$dql = 'SELECT CAST(a.integerArray AS int[]) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsArrays a WHERE a.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertIsString($result[0]['result']);
static::assertStringContainsString('{', $result[0]['result']);
}

public function test_cast_mixed_case_array_types(): void
{
$dql = 'SELECT CAST(a.integerArray AS Text[]) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsArrays a WHERE a.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertIsString($result[0]['result']);
static::assertStringContainsString('{', $result[0]['result']);
}

public function test_cast_parameterized_decimal_array(): void
{
$dql = 'SELECT CAST(a.integerArray AS DECIMAL(10, 2)[]) AS result FROM Fixtures\\MartinGeorgiev\\Doctrine\\Entity\\ContainsArrays a WHERE a.id = 1';
$result = $this->executeDqlQuery($dql);
static::assertIsString($result[0]['result']);
static::assertStringContainsString('{', $result[0]['result']);
}

private function createTestTableForTextFixture(): void
{
$tableName = 'containstexts';

$this->dropTestTableIfItExists($tableName);

$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
$sql = \sprintf('
CREATE TABLE %s (
id SERIAL PRIMARY KEY,
text1 TEXT,
text2 TEXT
)
', $fullTableName);

$this->connection->executeStatement($sql);

$sql = \sprintf('
INSERT INTO %s.containstexts (text1, text2) VALUES
(\'123\', \'true\'),
(\'456\', \'false\'),
(\'789\', \'1\')
', self::DATABASE_SCHEMA);
$this->connection->executeStatement($sql);
}

private function createTestTableForArrayFixture(): void
{
$tableName = 'containsarrays';

$this->dropTestTableIfItExists($tableName);

$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
$sql = \sprintf('
CREATE TABLE %s (
id SERIAL PRIMARY KEY,
textarray TEXT[],
smallintarray SMALLINT[],
integerarray INTEGER[],
bigintarray BIGINT[],
boolarray BOOLEAN[]
)
', $fullTableName);

$this->connection->executeStatement($sql);

$sql = \sprintf('
INSERT INTO %s.containsarrays (textarray, smallintarray, integerarray, bigintarray, boolarray) VALUES
(\'{"apple", "banana", "cherry"}\', \'{1, 2, 3}\', \'{10, 20, 30}\', \'{100, 200, 300}\', \'{true, false, true}\'),
(\'{"dog", "cat", "bird"}\', \'{4, 5, 6}\', \'{40, 50, 60}\', \'{400, 500, 600}\', \'{false, true, false}\')
', self::DATABASE_SCHEMA);
$this->connection->executeStatement($sql);
}

private function createTestTableForNumericFixture(): void
{
$tableName = 'containsnumerics';

$this->dropTestTableIfItExists($tableName);

$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
$sql = \sprintf('
CREATE TABLE %s (
id SERIAL PRIMARY KEY,
integer1 INTEGER,
integer2 INTEGER,
bigint1 BIGINT,
bigint2 BIGINT,
decimal1 DECIMAL,
decimal2 DECIMAL
)
', $fullTableName);

$this->connection->executeStatement($sql);

$sql = \sprintf('
INSERT INTO %s.containsnumerics (integer1, integer2, bigint1, bigint2, decimal1, decimal2) VALUES
(10, 20, 1000, 2000, 10.5, 20.5)
', self::DATABASE_SCHEMA);
$this->connection->executeStatement($sql);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ protected function getExpectedSqlStatements(): array
'cast as jsonb' => 'SELECT cast(c0_.text1 as JSONB) AS sclr_0 FROM ContainsTexts c0_',
'cast as boolean' => 'SELECT cast(c0_.text1 as BOOLEAN) AS sclr_0 FROM ContainsTexts c0_',
'cast with precision' => 'SELECT cast(c0_.text1 as DECIMAL(10, 2)) AS sclr_0 FROM ContainsTexts c0_',
'cast as integer array' => 'SELECT cast(c0_.text1 as INTEGER[]) AS sclr_0 FROM ContainsTexts c0_',
'cast as text array' => 'SELECT cast(c0_.text1 as TEXT[]) AS sclr_0 FROM ContainsTexts c0_',
'cast as boolean array' => 'SELECT cast(c0_.text1 as BOOLEAN[]) AS sclr_0 FROM ContainsTexts c0_',
'cast as decimal array' => 'SELECT cast(c0_.text1 as DECIMAL(10, 2)[]) AS sclr_0 FROM ContainsTexts c0_',
];
}

Expand All @@ -37,6 +41,10 @@ protected function getDqlStatements(): array
'cast as jsonb' => \sprintf('SELECT CAST(e.text1 AS JSONB) FROM %s e', ContainsTexts::class),
'cast as boolean' => \sprintf('SELECT CAST(e.text1 AS BOOLEAN) FROM %s e', ContainsTexts::class),
'cast with precision' => \sprintf('SELECT CAST(e.text1 AS DECIMAL(10, 2)) FROM %s e', ContainsTexts::class),
'cast as integer array' => \sprintf('SELECT CAST(e.text1 AS INTEGER[]) FROM %s e', ContainsTexts::class),
'cast as text array' => \sprintf('SELECT CAST(e.text1 AS TEXT[]) FROM %s e', ContainsTexts::class),
'cast as boolean array' => \sprintf('SELECT CAST(e.text1 AS BOOLEAN[]) FROM %s e', ContainsTexts::class),
'cast as decimal array' => \sprintf('SELECT CAST(e.text1 AS DECIMAL(10, 2)[]) FROM %s e', ContainsTexts::class),
];
}
}
Loading