Skip to content

Commit

Permalink
WIP: fix nullability for trivial subqueries
Browse files Browse the repository at this point in the history
  • Loading branch information
schlndh committed Apr 21, 2023
1 parent fa9d86e commit fe494d3
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/Analyser/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function analyzeQuery(string $query): AnalyserResult
[new AnalyserError("Couldn't parse query: '{$queryShort}'. Error: {$e->getMessage()}")],
null,
null,
null,
);
}

Expand Down
9 changes: 9 additions & 0 deletions src/Analyser/AnalyserKnowledgeBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ public function or(self $other): self
return new self($mergedColumnNullability, null);
}

public function removeTruthiness(): self
{
if ($this->truthiness === null) {
return $this;
}

return new self($this->columnNullability, null);
}

private static function tryTrivialAnd(self $a, self $b): ?self
{
if ($a->truthiness === true) {
Expand Down
1 change: 1 addition & 0 deletions src/Analyser/AnalyserResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function __construct(
public readonly array $errors,
public readonly ?int $positionalPlaceholderCount,
public readonly ?array $referencedSymbols,
public readonly ?QueryResultRowCountRange $rowCountRange,
) {
}
}
70 changes: 53 additions & 17 deletions src/Analyser/AnalyserState.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@ public function __construct(
/** @throws AnalyserException */
public function analyse(): AnalyserResult
{
$rowCountRange = null;

switch ($this->queryAst::getQueryType()) {
case QueryTypeEnum::SELECT:
assert($this->queryAst instanceof SelectQuery);
$fields = $this->dispatchAnalyseSelectQuery($this->queryAst);
[$fields, $rowCountRange] = $this->dispatchAnalyseSelectQuery($this->queryAst);
break;
case QueryTypeEnum::INSERT:
// fallthrough intentional
Expand Down Expand Up @@ -115,6 +117,7 @@ public function analyse(): AnalyserResult
[new AnalyserError("Unsupported query: {$this->queryAst::getQueryType()->value}")],
null,
null,
null,
);
}

Expand All @@ -124,11 +127,17 @@ public function analyse(): AnalyserResult
$referencedSymbols = array_merge($referencedSymbols, $columns);
}

return new AnalyserResult($fields, $this->errors, $this->positionalPlaceholderCount, $referencedSymbols);
return new AnalyserResult(
$fields,
$this->errors,
$this->positionalPlaceholderCount,
$referencedSymbols,
$rowCountRange,
);
}

/**
* @return array<QueryResultField>
* @return array{0: array<QueryResultField>, 1: ?QueryResultRowCountRange}
* @throws AnalyserException
*/
private function dispatchAnalyseSelectQuery(SelectQuery $select): array
Expand All @@ -149,12 +158,12 @@ private function dispatchAnalyseSelectQuery(SelectQuery $select): array
default:
$this->errors[] = new AnalyserError("Unhandled SELECT type {$select::getSelectQueryType()->value}");

return [];
return [[], null];
}
}

/**
* @return array<QueryResultField>
* @return array{0: array<QueryResultField>, 1: ?QueryResultRowCountRange}
* @throws AnalyserException
*/
private function analyseWithSelectQuery(WithSelectQuery $select): array
Expand Down Expand Up @@ -205,7 +214,7 @@ private function analyseWithSelectQuery(WithSelectQuery $select): array
}

/**
* @return array<QueryResultField>
* @return array{0: array<QueryResultField>, 1: ?QueryResultRowCountRange}
* @throws AnalyserException
*/
private function analyseCombinedSelectQuery(CombinedSelectQuery $select): array
Expand Down Expand Up @@ -262,11 +271,11 @@ private function analyseCombinedSelectQuery(CombinedSelectQuery $select): array
$this->resolveExprType($select->limit->offset);
}

return $fields;
return [$fields, null];
}

/**
* @return array<QueryResultField>
* @return array{0: array<QueryResultField>, 1: ?QueryResultRowCountRange}
* @throws AnalyserException
*/
private function analyseSingleSelectQuery(SimpleSelectQuery $select): array
Expand All @@ -281,6 +290,8 @@ private function analyseSingleSelectQuery(SimpleSelectQuery $select): array
}
}

$whereResult = null;

if ($select->where) {
$whereResult = $this->resolveExprType($select->where, AnalyserConditionTypeEnum::TRUTHY);
$this->columnResolver->addKnowledge($whereResult->knowledgeBase);
Expand Down Expand Up @@ -351,7 +362,15 @@ private function analyseSingleSelectQuery(SimpleSelectQuery $select): array
$this->resolveExprType($select->limit->offset);
}

return $fields;
$rowCount = null;

if ($select->from === null && $select->having === null) {
$rowCount = QueryResultRowCountRange::createFixed(1)
->applyWhere($whereResult)
->applyLimitClause($select->limit);
}

return [$fields, $rowCount];
}

/**
Expand Down Expand Up @@ -736,6 +755,7 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
case Expr\ExprTypeEnum::UNARY_OP:
assert($expr instanceof Expr\UnaryOp);
$innerCondition = $condition;
$isTruthinessUncertain = false;

if ($innerCondition !== null && $expr->operation === Expr\UnaryOpTypeEnum::LOGIC_NOT) {
$innerCondition = match ($innerCondition) {
Expand All @@ -750,6 +770,7 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
AnalyserConditionTypeEnum::NULL => AnalyserConditionTypeEnum::NULL,
default => AnalyserConditionTypeEnum::NOT_NULL,
};
$isTruthinessUncertain = true;
}

$resolvedInnerExpr = $this->resolveExprType($expr->expression, $innerCondition);
Expand All @@ -765,12 +786,13 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
default => new Schema\DbType\IntType(),
};

return new ExprTypeResult(
$type,
$resolvedInnerExpr->isNullable,
null,
$resolvedInnerExpr->knowledgeBase,
);
$knowledgeBase = $resolvedInnerExpr->knowledgeBase;

if ($isTruthinessUncertain) {
$knowledgeBase = $knowledgeBase?->removeTruthiness();
}

return new ExprTypeResult($type, $resolvedInnerExpr->isNullable, null, $knowledgeBase);
case Expr\ExprTypeEnum::BINARY_OP:
assert($expr instanceof Expr\BinaryOp);
$innerConditionLeft = $innerConditionRight = null;
Expand All @@ -796,6 +818,7 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
Expr\BinaryOpTypeEnum::INT_DIVISION,
Expr\BinaryOpTypeEnum::MODULO,
];
$isTruthinessUncertain = true;

// TODO: handle $condition for <=>, REGEXP
if (
Expand Down Expand Up @@ -841,6 +864,11 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
// For now, we can only determine that none of the operands can be NULL.
default => [AnalyserConditionTypeEnum::NOT_NULL, AnalyserConditionTypeEnum::TRUTHY],
};

if ($condition === AnalyserConditionTypeEnum::NOT_NULL) {
$isTruthinessUncertain = false;
}

$kbCombinineWithAnd = true;
} elseif (
$expr->operation === Expr\BinaryOpTypeEnum::LOGIC_XOR
Expand Down Expand Up @@ -962,19 +990,27 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
: $leftResult->knowledgeBase->or($rightResult->knowledgeBase);
}

// TODO: In many cases I'm relaxing the condition, because I can't check the real condition statically.
// E.g. TRUTHY(5 = 1) becomes TRUTHY(5 IS NOT NULL AND 1 IS NOT NULL). So I have to remove truthiness
// in these cases.
if ($isTruthinessUncertain) {
$knowledgeBase = $knowledgeBase?->removeTruthiness();
}

return new ExprTypeResult($type, $isNullable, null, $knowledgeBase);
case Expr\ExprTypeEnum::SUBQUERY:
assert($expr instanceof Expr\Subquery);
// TODO: handle $condition
$subqueryAnalyser = $this->getSubqueryAnalyser($expr->query);
$result = $subqueryAnalyser->analyse();
$canBeEmpty = ($result->rowCountRange?->min ?? 0) === 0;

if ($result->resultFields === null) {
return new ExprTypeResult(
new Schema\DbType\MixedType(),
// TODO: Change it to false if we can statically determine that the query will always return
// a result: e.g. SELECT 1
true,
$canBeEmpty,
);
}

Expand All @@ -984,7 +1020,7 @@ private function resolveExprType(Expr\Expr $expr, ?AnalyserConditionTypeEnum $co
// TODO: Change it to false if we can statically determine that the query will always return
// a result: e.g. SELECT 1
// TODO: Fix this for "x IN (SELECT ...)".
true,
$canBeEmpty || $result->resultFields[0]->exprType->isNullable,
);
}

Expand Down
79 changes: 79 additions & 0 deletions src/Analyser/QueryResultRowCountRange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace MariaStan\Analyser;

use MariaStan\Ast\Expr\ExprTypeEnum;
use MariaStan\Ast\Expr\LiteralInt;
use MariaStan\Ast\Limit;

use function assert;
use function max;
use function min;

final class QueryResultRowCountRange
{
/**
* @phpstan-param positive-int|0 $min
* @phpstan-param positive-int|0|null $max null => unlimited
*/
public function __construct(public readonly int $min, public readonly ?int $max)
{
}

/** @phpstan-param positive-int|0 $count */
public static function createFixed(int $count): self
{
return new self($count, $count);
}

public function applyLimitClause(?Limit $limit): self
{
if ($limit === null || $this->min === 0) {
return $this;
}

$count = $limit->count;
$offset = $limit->offset;
$newMin = $this->min;
$newMax = $this->max;

if ($offset !== null) {
if ($offset::getExprType() === ExprTypeEnum::LITERAL_INT) {
assert($offset instanceof LiteralInt);
assert($offset->value >= 0);
$newMin = max(0, $newMin - $offset->value);

if ($newMax !== null) {
$newMax = max(0, $newMax - $offset->value);
}
} else {
$newMin = 0;
}
}

if ($count::getExprType() === ExprTypeEnum::LITERAL_INT) {
assert($count instanceof LiteralInt);
assert($count->value >= 0);
$newMin = min($newMin, $count->value);
$newMax = min($newMax ?? $count->value, $count->value);
} else {
$newMin = 0;
}

return new self($newMin, $newMax);
}

public function applyWhere(?ExprTypeResult $whereResult): self
{
if (
$whereResult === null
|| ($whereResult->knowledgeBase !== null && $whereResult->knowledgeBase->truthiness)
) {
return $this;
}

return new self(0, $this->max);
}
}
Loading

0 comments on commit fe494d3

Please sign in to comment.