orm: support subquery in join on, ref

This commit is contained in:
Yuri Kuznetsov
2025-06-09 13:46:00 +03:00
parent 8feaadc162
commit 34df8c5f63
2 changed files with 152 additions and 133 deletions
@@ -2492,28 +2492,28 @@ abstract class BaseQueryComposer implements QueryComposer
protected function getWherePartItem(
Entity $entity,
mixed $leftKey,
mixed $value,
mixed $right,
array &$params,
int $level,
bool $noCustomWhere = false
): ?string {
if (is_int($leftKey) && is_string($value)) {
return $this->convertComplexExpression($entity, $value, false, $params);
if (is_int($leftKey) && is_string($right)) {
return $this->convertComplexExpression($entity, $right, false, $params);
}
$field = $leftKey;
$left = $leftKey;
if (is_int($field)) {
$field = 'AND';
if (is_int($left)) {
$left = 'AND';
}
if ($leftKey === 'NOT') {
$field = 'AND';
$left = 'AND';
}
if (in_array($field, self::SQL_OPERATORS)) {
$internalPart = $this->getWherePart($entity, $value, $field, $params, $level + 1);
if (in_array($left, self::SQL_OPERATORS)) {
$internalPart = $this->getWherePart($entity, $right, $left, $params, $level + 1);
if (!$internalPart && $internalPart !== '0') {
return null;
@@ -2526,11 +2526,11 @@ abstract class BaseQueryComposer implements QueryComposer
return "(" . $internalPart . ")";
}
if ($field === self::EXISTS_OPERATOR) {
if ($value instanceof Select) {
$subQueryPart = $this->composeSelect($value);
} else if (is_array($value)) {
$subQueryPart = $this->createSelectQueryInternal($value);
if ($left === self::EXISTS_OPERATOR) {
if ($right instanceof Select) {
$subQueryPart = $this->composeSelect($right);
} else if (is_array($right)) {
$subQueryPart = $this->createSelectQueryInternal($right);
} else {
throw new RuntimeException("Bad EXISTS usage in where-clause.");
}
@@ -2541,75 +2541,99 @@ abstract class BaseQueryComposer implements QueryComposer
$isComplex = false;
$isNotValue = false;
if (str_ends_with($field, ':')) {
$field = substr($field, 0, strlen($field) - 1);
if (str_ends_with($left, ':')) {
$left = substr($left, 0, strlen($left) - 1);
$isNotValue = true;
}
[$field, $operator, $operatorOrm] = $this->splitWhereLeftItem($field);
[$left, $operator, $operatorOrm] = $this->splitWhereLeftItem($left);
$leftPart = null;
if (Util::isComplexExpression($field)) {
$leftPart = $this->convertComplexExpression($entity, $field, false, $params);
if (Util::isComplexExpression($left)) {
$leftPart = $this->convertComplexExpression($entity, $left, false, $params);
$isComplex = true;
}
if (!$isComplex) {
if (!$entity->hasAttribute($field)) {
if (!$entity->hasAttribute($left)) {
return $this->quote(false);
}
$operatorKey = $this->getWhereOperatorKey(
$operator,
$operatorOrm,
$value,
$entity->getAttributeType($field)
$right,
$entity->getAttributeType($left)
);
if (
!$noCustomWhere &&
$this->getAttributeParam($entity, $field, 'where') &&
isset($this->getAttributeParam($entity, $field, 'where')[$operatorKey])
$this->getAttributeParam($entity, $left, 'where') &&
isset($this->getAttributeParam($entity, $left, 'where')[$operatorKey])
) {
$whereDefs = $this->getAttributeParam($entity, $field, 'where')[$operatorKey];
$whereDefs = $this->getAttributeParam($entity, $left, 'where')[$operatorKey];
return $this->getWherePartItemCustom($entity, $value, $whereDefs, $params, $level);
return $this->getWherePartItemCustom($entity, $right, $whereDefs, $params, $level);
}
$leftPart = $this->getWherePartItemAttributeLeftPart($entity, $field, $params);
$leftPart = $this->getWherePartItemAttributeLeftPart($entity, $left, $params);
}
return $this->addRightWherePartItem(
leftPart: $leftPart,
operatorOrm: $operatorOrm,
operator: $operator,
right: $right,
entity: $entity,
isNotValue: $isNotValue,
params: $params,
);
}
/**
* @param array<string, mixed> $params
*/
private function addRightWherePartItem(
?string $leftPart,
string $operatorOrm,
string $operator,
mixed $right,
Entity $entity,
bool $isNotValue,
array $params,
): string {
if ($leftPart === null) {
return $this->quote(false);
}
if ($operatorOrm === '=s' || $operatorOrm === '!=s') {
if ($value instanceof Select) {
$subSql = $this->composeSelect($value);
if ($right instanceof Select) {
$subSql = $this->composeSelect($right);
return "$leftPart $operator ($subSql)";
}
if (!is_array($value)) {
if (!is_array($right)) {
throw new RuntimeException("Bad `=s` operator usage, value must be sub-query.");
}
$subQuerySelectParams = !empty($value['selectParams']) ?
$value['selectParams'] :
$value;
$subQuerySelectParams = !empty($right['selectParams']) ?
$right['selectParams'] :
$right;
if (
!isset($subQuerySelectParams['from']) &&
!isset($subQuerySelectParams['fromQuery'])
) {
// 'entityType' is for backward compatibility.
$subQuerySelectParams['from'] = $value['entityType'] ?? $entity->getEntityType();
$subQuerySelectParams['from'] = $right['entityType'] ?? $entity->getEntityType();
}
if (!empty($value['withDeleted'])) {
if (!empty($right['withDeleted'])) {
$subQuerySelectParams['withDeleted'] = true;
}
@@ -2618,12 +2642,12 @@ abstract class BaseQueryComposer implements QueryComposer
return "$leftPart $operator ($subSql)";
}
if ($value instanceof Select) {
if ($right instanceof Select) {
if ($operatorOrm === '*' || $operatorOrm === '!*') {
throw new RuntimeException("LIKE operator is not compatible with sub-query.");
}
$subQueryPart = $this->composeSelect($value);
$subQueryPart = $this->composeSelect($right);
return "$leftPart $operator ($subQueryPart)";
}
@@ -2632,14 +2656,14 @@ abstract class BaseQueryComposer implements QueryComposer
throw new RuntimeException("ANY/ALL operators can be used only with sub-query.");
}
if ($value instanceof Expression) {
if ($right instanceof Expression) {
$isNotValue = true;
$value = $value->getValue();
$right = $right->getValue();
}
if (is_array($value)) {
$valuePartList = $value;
if (is_array($right)) {
$valuePartList = $right;
foreach ($valuePartList as $k => $v) {
$valuePartList[$k] = $this->quote($v);
@@ -2663,16 +2687,16 @@ abstract class BaseQueryComposer implements QueryComposer
}
if ($isNotValue) {
if (is_null($value)) {
if (is_null($right)) {
return $leftPart;
}
$expressionSql = $this->convertComplexExpression($entity, $value, false, $params);
$expressionSql = $this->convertComplexExpression($entity, $right, false, $params);
return "$leftPart $operator $expressionSql";
}
if (is_null($value)) {
if (is_null($right)) {
if ($operator === '=') {
return "$leftPart IS NULL";
}
@@ -2684,7 +2708,7 @@ abstract class BaseQueryComposer implements QueryComposer
return $this->quote(false);
}
$valuePart = $this->quote($value);
$valuePart = $this->quote($right);
return "$leftPart $operator $valuePart";
}
@@ -3031,40 +3055,58 @@ abstract class BaseQueryComposer implements QueryComposer
/**
* @param array<string, mixed> $params
* @param string $alias
* @param mixed $left
* @param mixed $right
* @noinspection PhpDeprecationInspection
*/
protected function buildJoinConditionStatement(
Entity $entity,
string $alias,
$left,
$right,
mixed $leftKey,
mixed $right,
array $params,
bool $noLeftAlias = false
): string {
$sql = '';
// @todo Unify with `getWherePartItem`. If reasonable.
if (is_array($right) && (is_int($left) || in_array($left, ['AND', 'OR']))) {
$left = $leftKey;
if (is_int($left)) {
$left = 'AND';
}
if ($leftKey === 'NOT') {
$left = 'AND';
}
if (in_array($left, self::SQL_OPERATORS)) {
$logicalOperator = 'AND';
if ($left === 'OR') {
$logicalOperator = 'OR';
}
$sqlList = [];
$parts = [];
foreach ($right as $k => $v) {
$sqlList[] = $this->buildJoinConditionStatement($entity, $alias, $k, $v, $params, $noLeftAlias);
$part = $this->buildJoinConditionStatement($entity, $alias, $k, $v, $params, $noLeftAlias);
if ($part === '') {
continue;
}
$parts[] = $part;
}
$sql = implode(' ' . $logicalOperator . ' ', $sqlList);
$sql = implode(' ' . $logicalOperator . ' ', $parts);
if (count($sqlList) > 1) {
if (count($parts) > 1) {
$sql = '(' . $sql . ')';
}
if ($leftKey === 'NOT') {
return "NOT ($sql)";
}
return $sql;
}
@@ -3073,98 +3115,43 @@ abstract class BaseQueryComposer implements QueryComposer
if (str_ends_with($left, ':')) {
$left = substr($left, 0, strlen($left) - 1);
$isNotValue = true;
}
[$left, $operator] = $this->splitWhereLeftItem($left);
[$left, $operator, $operatorOrm] = $this->splitWhereLeftItem($left);
$leftPart = null;
if (Util::isComplexExpression($left)) {
$isComplex = true;
$stub = [];
$sql .= $this->convertComplexExpression($entity, $left, false, $stub);
// Differs from where logic.
$stub = []; // @todo Revise the need.
$leftPart = $this->convertComplexExpression($entity, $left, false, $stub);
}
if (!$isComplex) {
if (strpos($left, '.') > 0) {
list($leftAlias, $attribute) = explode('.', $left);
// Differs from where logic.
/** @noinspection PhpDeprecationInspection */
$leftAlias = $this->sanitize($leftAlias);
/** @noinspection PhpDeprecationInspection */
$column = $this->toDb($this->sanitize($attribute));
} else {
/** @noinspection PhpDeprecationInspection */
$column = $this->toDb($this->sanitize($left));
$column = $this->toDb($this->sanitize($left));
/** @noinspection PhpDeprecationInspection */
$leftAlias = $noLeftAlias ?
$this->getFromAlias($params, $entity->getEntityType()) :
$this->sanitize($alias);
}
$leftAlias = $noLeftAlias ?
$this->getFromAlias($params, $entity->getEntityType()) :
$this->sanitize($alias);
$sql .= $this->quoteColumn("$leftAlias.$column");
$leftPart = $this->quoteColumn("$leftAlias.$column");
}
if ($right instanceof Expression) {
$isNotValue = true;
$right = $right->getValue();
}
if (is_array($right)) {
$arr = [];
foreach ($right as $item) {
$arr[] = $this->quote($item);
}
$operator = $operator === '<>' ? 'NOT IN' : 'IN';
if (count($arr)) {
$sql .= " " . $operator . " (" . implode(', ', $arr) . ")";
return $sql;
}
if ($operator === 'IN') {
$sql .= " IS NULL";
return $sql;
}
$sql .= " IS NOT NULL";
return $sql;
}
$value = $right;
if ($isNotValue) {
if (is_null($value)) {
return $sql;
}
$rightPart = $this->convertComplexExpression($entity, $value, false, $params);
$sql .= " " . $operator . " " . $rightPart;
return $sql;
}
if (is_null($value)) {
if ($operator === '=') {
$sql .= " IS NULL";
} else if ($operator === '<>') {
$sql .= " IS NOT NULL";
}
return $sql;
}
$sql .= " " . $operator . " " . $this->quote($value);
return $sql;
return $this->addRightWherePartItem(
leftPart: $leftPart,
operatorOrm: $operatorOrm,
operator: $operator,
right: $right,
entity: $entity,
isNotValue: $isNotValue,
params: $params,
);
}
/**
@@ -1090,6 +1090,38 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
);
}
public function testJoinSubQueryInOn(): void
{
$sql =
"SELECT post.id AS `id` FROM `post` " .
"JOIN `post` AS `p` ON p.id IN (SELECT p1.id AS `id` FROM `post` AS `p1` LIMIT 0, 1)";
$select = SelectBuilder::create()
->select('id')
->from('Post')
->join(
Join::createWithTableTarget('Post', 'p')
->withConditions(
Condition::in(
Expression::column('p.id'),
SelectBuilder::create()
->select('id')
->from('Post', 'p1')
->limit(0, 1)
->withDeleted()
->build()
)
)
)
->withDeleted()
->build();
$this->assertEquals(
$sql,
$this->query->composeSelect($select)
);
}
public function testJoinSubQueryException1(): void
{
$this->expectException(LogicException::class);