0
0
mirror of https://github.com/nextcloud/server.git synced 2024-11-14 20:36:50 +00:00
nextcloud_server/lib/private/DB/QueryBuilder/Partitioned/JoinCondition.php
Louis Chemineau 2574cbfa61
chore: Apply php:cs recommendations
Signed-off-by: Louis Chemineau <louis@chmn.me>
2024-08-28 10:44:18 +02:00

174 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\DB\QueryBuilder\Partitioned;
use OC\DB\QueryBuilder\CompositeExpression;
use OC\DB\QueryBuilder\QueryFunction;
use OCP\DB\QueryBuilder\IQueryFunction;
/**
* Utility class for working with join conditions
*/
class JoinCondition {
public function __construct(
public string|IQueryFunction $fromColumn,
public ?string $fromAlias,
public string|IQueryFunction $toColumn,
public ?string $toAlias,
public array $fromConditions,
public array $toConditions,
) {
if (is_string($this->fromColumn) && str_starts_with($this->fromColumn, '(')) {
$this->fromColumn = new QueryFunction($this->fromColumn);
}
if (is_string($this->toColumn) && str_starts_with($this->toColumn, '(')) {
$this->toColumn = new QueryFunction($this->toColumn);
}
}
/**
* @param JoinCondition[] $conditions
* @return JoinCondition
*/
public static function merge(array $conditions): JoinCondition {
$fromColumn = '';
$toColumn = '';
$fromAlias = null;
$toAlias = null;
$fromConditions = [];
$toConditions = [];
foreach ($conditions as $condition) {
if (($condition->fromColumn && $fromColumn) || ($condition->toColumn && $toColumn)) {
throw new InvalidPartitionedQueryException("Can't join from {$condition->fromColumn} to {$condition->toColumn} as it already join froms {$fromColumn} to {$toColumn}");
}
if ($condition->fromColumn) {
$fromColumn = $condition->fromColumn;
}
if ($condition->toColumn) {
$toColumn = $condition->toColumn;
}
if ($condition->fromAlias) {
$fromAlias = $condition->fromAlias;
}
if ($condition->toAlias) {
$toAlias = $condition->toAlias;
}
$fromConditions = array_merge($fromConditions, $condition->fromConditions);
$toConditions = array_merge($toConditions, $condition->toConditions);
}
return new JoinCondition($fromColumn, $fromAlias, $toColumn, $toAlias, $fromConditions, $toConditions);
}
/**
* @param null|string|CompositeExpression $condition
* @param string $join
* @param string $alias
* @param string $fromAlias
* @return JoinCondition
* @throws InvalidPartitionedQueryException
*/
public static function parse($condition, string $join, string $alias, string $fromAlias): JoinCondition {
if ($condition === null) {
throw new InvalidPartitionedQueryException("Can't join on $join without a condition");
}
$result = self::parseSubCondition($condition, $join, $alias, $fromAlias);
if (!$result->fromColumn || !$result->toColumn) {
throw new InvalidPartitionedQueryException("No join condition found from $fromAlias to $alias");
}
return $result;
}
private static function parseSubCondition($condition, string $join, string $alias, string $fromAlias): JoinCondition {
if ($condition instanceof CompositeExpression) {
if ($condition->getType() === CompositeExpression::TYPE_OR) {
throw new InvalidPartitionedQueryException("Cannot join on $join with an OR expression");
}
return self::merge(array_map(function ($subCondition) use ($join, $alias, $fromAlias) {
return self::parseSubCondition($subCondition, $join, $alias, $fromAlias);
}, $condition->getParts()));
}
$condition = (string)$condition;
$isSubCondition = self::isExtraCondition($condition);
if ($isSubCondition) {
if (self::mentionsAlias($condition, $fromAlias)) {
return new JoinCondition('', null, '', null, [$condition], []);
} else {
return new JoinCondition('', null, '', null, [], [$condition]);
}
}
$condition = str_replace('`', '', $condition);
// expect a condition in the form of 'alias1.column1 = alias2.column2'
if (!str_contains($condition, ' = ')) {
throw new InvalidPartitionedQueryException("Can only join on $join with an `eq` condition");
}
$parts = explode(' = ', $condition, 2);
$parts = array_map(function (string $part) {
return self::clearConditionPart($part);
}, $parts);
if (!self::isSingleCondition($parts[0]) || !self::isSingleCondition($parts[1])) {
throw new InvalidPartitionedQueryException("Can only join on $join with a single condition");
}
if (self::mentionsAlias($parts[0], $fromAlias)) {
return new JoinCondition($parts[0], self::getAliasForPart($parts[0]), $parts[1], self::getAliasForPart($parts[1]), [], []);
} elseif (self::mentionsAlias($parts[1], $fromAlias)) {
return new JoinCondition($parts[1], self::getAliasForPart($parts[1]), $parts[0], self::getAliasForPart($parts[0]), [], []);
} else {
throw new InvalidPartitionedQueryException("join condition for $join needs to explicitly refer to the table by alias");
}
}
private static function isSingleCondition(string $condition): bool {
return !(str_contains($condition, ' OR ') || str_contains($condition, ' AND '));
}
private static function getAliasForPart(string $part): ?string {
if (str_contains($part, ' ')) {
return uniqid('join_alias_');
} else {
return null;
}
}
private static function clearConditionPart(string $part): string {
if (str_starts_with($part, 'CAST(')) {
// pgsql/mysql cast
$part = substr($part, strlen('CAST('));
[$part] = explode(' AS ', $part);
} elseif (str_starts_with($part, 'to_number(to_char(')) {
// oracle cast to int
$part = substr($part, strlen('to_number(to_char('), -2);
} elseif (str_starts_with($part, 'to_number(to_char(')) {
// oracle cast to string
$part = substr($part, strlen('to_char('), -1);
}
return $part;
}
/**
* Check that a condition is an extra limit on the from/to part, and not the join condition
*
* This is done by checking that only one of the halves of the condition references a column
*/
private static function isExtraCondition(string $condition): bool {
$parts = explode(' ', $condition, 2);
return str_contains($parts[0], '`') xor str_contains($parts[1], '`');
}
private static function mentionsAlias(string $condition, string $alias): bool {
return str_contains($condition, "$alias.");
}
}