Skip to content

Commit

Permalink
add function info for CONCAT
Browse files Browse the repository at this point in the history
  • Loading branch information
schlndh committed Oct 27, 2023
1 parent 46cb51a commit 1f26540
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 4 deletions.
121 changes: 121 additions & 0 deletions src/Database/FunctionInfo/Concat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace MariaStan\Database\FunctionInfo;

use MariaStan\Analyser\AnalyserConditionTypeEnum;
use MariaStan\Analyser\ExprTypeResult;
use MariaStan\Ast\Expr\FunctionCall\FunctionCall;
use MariaStan\Parser\Exception\ParserException;
use MariaStan\Schema\DbType\DbType;
use MariaStan\Schema\DbType\DbTypeEnum;
use MariaStan\Schema\DbType\VarcharType;

use function array_fill;
use function count;

final class Concat implements FunctionInfo
{
/** @inheritDoc */
public function getSupportedFunctionNames(): array
{
return ['CONCAT'];
}

public function getFunctionType(): FunctionTypeEnum
{
return FunctionTypeEnum::SIMPLE;
}

public function checkSyntaxErrors(FunctionCall $functionCall): void
{
$args = $functionCall->getArguments();
$argCount = count($args);

if ($argCount > 0) {
return;
}

throw new ParserException(
FunctionInfoHelper::createArgumentCountErrorMessageMin(
$functionCall->getFunctionName(),
1,
$argCount,
),
);
}

/** @inheritDoc */
public function getInnerConditions(?AnalyserConditionTypeEnum $condition, array $arguments): array
{
if (count($arguments) === 1) {
return [$condition];
}

// TRUTHY(CONCAT(A, B)) => NOT_NULL(A) && NOT_NULL(B)
// - same for FALSY
// NULL(CONCAT(A, B)) <=> NULL(A) || NULL(B)
// NOT_NULL(CONCAT(A, B)) <=> NOT_NULL(A) && NOT_NULL(B)
$innerCondition = match ($condition) {
null, AnalyserConditionTypeEnum::NULL => $condition,
default => AnalyserConditionTypeEnum::NOT_NULL,
};

return array_fill(0, count($arguments), $innerCondition);
}

/**
* @inheritDoc
* @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
*/
public function getReturnType(
FunctionCall $functionCall,
array $argumentTypes,
?AnalyserConditionTypeEnum $condition,
): ExprTypeResult {
$leftType = $this->concatOneType($argumentTypes[0]->type);
$isNullable = $argumentTypes[0]->isNullable;
$knowledgeBase = $argumentTypes[0]->knowledgeBase;
$i = 1;
$argCount = count($argumentTypes);

while ($i < $argCount) {
$rightKnowledgeBase = $argumentTypes[$i]->knowledgeBase;
$rightType = $this->concatOneType($argumentTypes[$i]->type);
$isNullable = $isNullable || $argumentTypes[$i]->isNullable;
$i++;
$leftType = $this->concatTwoTypes($leftType, $rightType);
$knowledgeBase = $rightKnowledgeBase === null
? null
: match ($condition) {
null => null,
AnalyserConditionTypeEnum::NULL => $knowledgeBase?->or($rightKnowledgeBase),
default => $knowledgeBase?->and($rightKnowledgeBase),
};
}

return new ExprTypeResult($leftType, $isNullable, null, $knowledgeBase);
}

private function concatOneType(DbType $type): DbType
{
return match ($type::getTypeEnum()) {
DbTypeEnum::NULL, DbTypeEnum::VARCHAR => $type,
default => new VarcharType(),
};
}

private function concatTwoTypes(DbType $left, DbType $right): DbType
{
if ($left::getTypeEnum() === DbTypeEnum::NULL) {
return $left;
}

if ($right::getTypeEnum() === DbTypeEnum::NULL) {
return $right;
}

return $left;
}
}
1 change: 1 addition & 0 deletions src/Database/FunctionInfo/FunctionInfoRegistryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public function createDefaultFunctionInfos(): array
new Cast(),
new CeilFloor(),
new Coalesce(),
new Concat(),
new Count(),
new Curdate(),
new Date(),
Expand Down
23 changes: 20 additions & 3 deletions tests/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ private function provideValidFunctionCallTestData(): iterable
$selects["COALESCE(NULL, {$label1}, {$label2})"] = "SELECT COALESCE(NULL, {$value1}, {$value2})";
$selects["COALESCE({$label1}, {$label2}, int)"] = "SELECT COALESCE(NULL, {$value1}, {$value2}, 9)";
$selects["ROUND({$label1}, {$label2})"] = "SELECT ROUND({$value1}, {$value2})";
$selects["CONCAT({$label1}, {$label2})"] = "SELECT CONCAT({$value1}, {$value2})";
}

$selects["TRIM({$label1})"] = "SELECT TRIM({$value1})";
Expand All @@ -852,6 +853,7 @@ private function provideValidFunctionCallTestData(): iterable
$selects["FLOOR({$label1})"] = "SELECT FLOOR({$value1})";
$selects["CEIL({$label1})"] = "SELECT CEIL({$value1})";
$selects["CEILING({$label1})"] = "SELECT CEILING({$value1})";
$selects["CONCAT({$label1})"] = "SELECT CONCAT({$value1})";
}

// TODO: figure out the context in which the function is called and adjust the return type accordingly.
Expand Down Expand Up @@ -1368,9 +1370,15 @@ public function testValid(string $query, array $params = [], array $explicitFiel

foreach ($forceNullsForColumns as $col => $mustBeNull) {
if ($mustBeNull) {
$this->assertNull($row[$col]);
$this->assertNull(
$row[$col],
'Analyser thinks that column is always NULL, but it contains non NULL value.',
);
} else {
$this->assertNotNull($row[$col]);
$this->assertNotNull(
$row[$col],
'Analyser thinks that column is not nullable, but it contains null.',
);
}
}

Expand Down Expand Up @@ -1607,6 +1615,11 @@ public function provideValidNullabilityTestData(): iterable
'COALESCE(col_vchar)',
'COALESCE(col_vchar IS NULL)',
'COALESCE(NULL, NULL, col_vchar)',
'CONCAT(col_vchar)',
'CONCAT(col_vchar, NULL) IS NULL',
'CONCAT(col_vchar) IS NOT NULL',
'CONCAT(col_vchar, col_int) IS NOT NULL',
'CONCAT(col_vchar, col_int) IS NULL',
];

foreach (['COALESCE', 'IFNULL', 'NVL'] as $coalesceFn) {
Expand Down Expand Up @@ -1811,7 +1824,11 @@ public function testValidNullability(string $query, array $params = []): void
$this->assertNotNull($result->resultFields);
$this->assertSameSize($result->resultFields, $fields);
$rowCount = count($rows);
$this->assertGreaterThanOrEqual(1, $rowCount, 'The query did not return any results.');
$this->assertGreaterThanOrEqual(
1,
$rowCount,
'Nullability test query has to return at least 1 row, but it did not return any.',
);
$nullCountsByColumn = array_fill_keys(array_keys($rows[0]), 0);

foreach ($rows as $row) {
Expand Down
9 changes: 8 additions & 1 deletion tests/code/Parser/MariaDbParser/invalid/function_calls.test
Original file line number Diff line number Diff line change
Expand Up @@ -1223,4 +1223,11 @@ SELECT CEIL()
MariaStan\Parser\Exception\ParserException
Function CEIL takes 1 argument, got 0.
#####
1582: Incorrect parameter count in the call to native function 'CEIL'
1582: Incorrect parameter count in the call to native function 'CEIL'
-----
SELECT CONCAT()
-----
MariaStan\Parser\Exception\ParserException
Function CONCAT takes at least 1 argument, got 0.
#####
1582: Incorrect parameter count in the call to native function 'CONCAT'
47 changes: 47 additions & 0 deletions tests/code/Parser/MariaDbParser/valid/function_calls.test
Original file line number Diff line number Diff line change
Expand Up @@ -6317,4 +6317,51 @@ MariaStan\Ast\Query\SelectQuery\SimpleSelectQuery
)
[isDistinct] => false
[isSqlCalcFoundRows] => false
)
-----
SELECT CONCAT(1), CONCAT("A", "B")
-----
MariaStan\Ast\Query\SelectQuery\SimpleSelectQuery
(
[select] => Array
(
[0] => MariaStan\Ast\SelectExpr\RegularExpr
(
[expr] => MariaStan\Ast\Expr\FunctionCall\StandardFunctionCall
(
[name] => CONCAT
[arguments] => Array
(
[0] => MariaStan\Ast\Expr\LiteralInt
(
[value] => 1
)
)
[isDistinct] => false
)
)
[1] => MariaStan\Ast\SelectExpr\RegularExpr
(
[expr] => MariaStan\Ast\Expr\FunctionCall\StandardFunctionCall
(
[name] => CONCAT
[arguments] => Array
(
[0] => MariaStan\Ast\Expr\LiteralString
(
[value] => A
[firstConcatPart] => A
)
[1] => MariaStan\Ast\Expr\LiteralString
(
[value] => B
[firstConcatPart] => B
)
)
[isDistinct] => false
)
)
)
[isDistinct] => false
[isSqlCalcFoundRows] => false
)

0 comments on commit 1f26540

Please sign in to comment.