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
179 changes: 156 additions & 23 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -659,15 +659,7 @@ private function buildJoinsClause(ParsedQuery $grouped, array &$joinFilterWhereC
default => throw new UnsupportedException('Unsupported join method: ' . $joinQuery->getMethod()->value),
};
$isCrossJoin = $joinType === JoinType::Cross || $joinType === JoinType::Natural;

$joinValues = $joinQuery->getValues();
if ($isCrossJoin) {
/** @var string $joinAlias */
$joinAlias = $joinValues[0] ?? '';
} else {
/** @var string $joinAlias */
$joinAlias = $joinValues[3] ?? '';
}
$joinAlias = $joinQuery->getJoinAlias();
$effectiveJoinTable = $joinAlias !== '' ? $joinAlias : $joinTable;

foreach ($this->joinFilterHooks as $hook) {
Expand Down Expand Up @@ -1301,6 +1293,7 @@ public function compileFilter(Query $query): string
Method::Exists => $this->compileExists($query),
Method::NotExists => $this->compileNotExists($query),
Method::Raw => $this->compileRaw($query),
Method::On => $this->compileOn($query),
default => throw new UnsupportedException('Unsupported filter type: ' . $method->value),
};
}
Expand Down Expand Up @@ -1483,6 +1476,15 @@ public function compileJoin(Query $query): string
return $type . ' ' . $table;
}

if ($query->isNestedJoin()) {
$alias = $query->getJoinAlias();
if ($alias !== '') {
$table .= ' AS ' . $this->quote($alias);
}

return $type . ' ' . $table . ' ON ' . \implode(' AND ', $this->compileJoinOn($query));
}

if (empty($values)) {
return $type . ' ' . $table;
}
Expand Down Expand Up @@ -1511,6 +1513,53 @@ public function compileJoin(Query $query): string
return $type . ' ' . $table . ' ON ' . $left . ' ' . $operator . ' ' . $right;
}

/**
* @return list<string>
*/
private function compileJoinOn(Query $query): array
{
$onQueries = $query->getJoinOnQueries();
if ($onQueries === []) {
throw new ValidationException('Join ON requires at least one condition');
}

$parts = [];

foreach ($onQueries as $onQuery) {
$this->assertJoinOnPredicate($onQuery);
if ($onQuery->getMethod() === Method::On) {
$parts[] = $this->compileOn($onQuery);
continue;
}

$parts[] = $this->compileFilter($onQuery);
}

return $parts;
}

private function compileOn(Query $query): string
{
$values = $query->getValues();
/** @var string $leftCol */
$leftCol = $values[0] ?? '';
/** @var string $operator */
$operator = $values[1] ?? '=';
/** @var string $rightCol */
$rightCol = $values[2] ?? '';

if ($leftCol === '' || $rightCol === '') {
throw new ValidationException('Join ON requires left and right columns');
}

$allowedOperators = ['=', '!=', '<', '>', '<=', '>=', '<>'];
if (! \in_array($operator, $allowedOperators, true)) {
throw new ValidationException('Invalid join operator: ' . $operator);
}

return $this->resolveAndWrap($leftCol) . ' ' . $operator . ' ' . $this->resolveAndWrap($rightCol);
}

protected function compileJoinWithBuilder(Query $query, JoinBuilder $joinBuilder): string
{
$type = match ($query->getMethod()) {
Expand All @@ -1524,16 +1573,7 @@ protected function compileJoinWithBuilder(Query $query, JoinBuilder $joinBuilder
};

$table = $this->quote($query->getAttribute());
$values = $query->getValues();

// Handle alias
if ($query->getMethod() === Method::CrossJoin || $query->getMethod() === Method::NaturalJoin) {
/** @var string $alias */
$alias = $values[0] ?? '';
} else {
/** @var string $alias */
$alias = $values[3] ?? '';
}
$alias = $query->getJoinAlias();

if ($alias !== '') {
$table .= ' AS ' . $this->quote($alias);
Expand Down Expand Up @@ -1905,6 +1945,8 @@ public function toAst(): Select
{
$grouped = Query::groupByType($this->pendingQueries);

$this->prepareAliasQualification($grouped);

$columns = $this->buildAstColumns($grouped);
$from = $this->buildAstFrom();
$joins = $this->buildAstJoins($grouped);
Expand Down Expand Up @@ -1977,6 +2019,10 @@ private function columnNameToAstExpression(string $col): Expression
return new Column($parts[1], $parts[0]);
}

if ($this->qualify && ! isset($this->aggregationAliases[$col])) {
return new Column($col, $this->alias);
}

return new Column($col);
}

Expand Down Expand Up @@ -2040,19 +2086,21 @@ private function buildAstJoins(ParsedQuery $grouped): array
$isCrossOrNatural = $joinMethod === Method::CrossJoin || $joinMethod === Method::NaturalJoin;

if ($isCrossOrNatural) {
/** @var string $joinAlias */
$joinAlias = $values[0] ?? '';
$joinAlias = $joinQuery->getJoinAlias();
$tableRef = new Table($table, $joinAlias !== '' ? $joinAlias : null);
$joins[] = new AstJoinClause($type, $tableRef, null);
} elseif ($joinQuery->isNestedJoin()) {
$joinAlias = $joinQuery->getJoinAlias();
$tableRef = new Table($table, $joinAlias !== '' ? $joinAlias : null);
$joins[] = new AstJoinClause($type, $tableRef, $this->nestedJoinOnToAst($joinQuery));
} else {
/** @var string $leftCol */
$leftCol = $values[0] ?? '';
/** @var string $operator */
$operator = $values[1] ?? '=';
/** @var string $rightCol */
$rightCol = $values[2] ?? '';
/** @var string $joinAlias */
$joinAlias = $values[3] ?? '';
$joinAlias = $joinQuery->getJoinAlias();

$tableRef = new Table($table, $joinAlias !== '' ? $joinAlias : null);

Expand Down Expand Up @@ -2112,11 +2160,96 @@ private function queryToAstExpression(Query $query): Expression
Method::NotEndsWith => new Binary(new Column($attr), 'NOT LIKE', new Literal('%' . $this->toScalar($values[0] ?? ''))),
Method::And => $this->buildLogicalAstExpression($query, 'AND'),
Method::Or => $this->buildLogicalAstExpression($query, 'OR'),
Method::On => $this->buildOnAstExpression($query),
Method::Raw => new Raw($attr),
default => new Raw($attr !== '' ? $attr : '1 = 1'),
};
}

private function nestedJoinOnToAst(Query $joinQuery): Expression
{
$onQueries = $joinQuery->getJoinOnQueries();
if ($onQueries === []) {
throw new ValidationException('Join ON requires at least one condition');
}

$exprs = [];
foreach ($onQueries as $onQuery) {
$this->assertJoinOnPredicate($onQuery);
$exprs[] = $this->queryToAstExpression($onQuery);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

return $this->combineAstExpressions($exprs, 'AND');
}

private function assertJoinOnPredicate(Query $query): void
{
$method = $query->getMethod();
$allowed = match ($method) {
Method::On,
Method::Equal,
Method::NotEqual,
Method::GreaterThan,
Method::GreaterThanEqual,
Method::LessThan,
Method::LessThanEqual,
Method::Between,
Method::NotBetween,
Method::IsNull,
Method::IsNotNull,
Method::Contains,
Method::ContainsAny,
Method::NotContains,
Method::StartsWith,
Method::NotStartsWith,
Method::EndsWith,
Method::NotEndsWith,
Method::And,
Method::Or => true,
default => false,
};

if (! $allowed) {
throw new ValidationException('Unsupported join ON condition: ' . $method->value);
}

if ($method !== Method::And && $method !== Method::Or) {
return;
}

foreach ($query->getValues() as $child) {
if ($child instanceof Query) {
$this->assertJoinOnPredicate($child);
}
}
}

private function buildOnAstExpression(Query $query): Expression
{
$values = $query->getValues();
/** @var string $leftCol */
$leftCol = $values[0] ?? '';
/** @var string $operator */
$operator = $values[1] ?? '=';
/** @var string $rightCol */
$rightCol = $values[2] ?? '';

if ($leftCol === '' || $rightCol === '') {
throw new ValidationException('Join ON requires left and right columns');
}

$allowedOperators = ['=', '!=', '<', '>', '<=', '>=', '<>'];
if (! \in_array($operator, $allowedOperators, true)) {
throw new ValidationException('Invalid join operator: ' . $operator);
}

return new Binary(
$this->columnNameToAstExpression($leftCol),
$operator,
$this->columnNameToAstExpression($rightCol),
Comment thread
greptile-apps[bot] marked this conversation as resolved.
);
}

private function toLiteral(mixed $value): Literal
{
if ($value === null || \is_string($value) || \is_int($value) || \is_float($value) || \is_bool($value)) {
Expand Down
1 change: 1 addition & 0 deletions src/Query/Method.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ enum Method: string
case CrossJoin = 'crossJoin';
case FullOuterJoin = 'fullOuterJoin';
case NaturalJoin = 'naturalJoin';
case On = 'on';

// Union
case Union = 'union';
Expand Down
Loading
Loading