Skip to content

Commit

Permalink
Tests are passing
Browse files Browse the repository at this point in the history
  • Loading branch information
arogachev committed Jan 15, 2024
1 parent db89274 commit 75818c0
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 73 deletions.
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -23,7 +23,7 @@
"ext-pdo": "*",
"php": "^8.1",
"yiisoft/db": "^1.0",
"yiisoft/rbac": "dev-master"
"yiisoft/rbac": "dev-master#cba39d8471677970282fdad302271c858f5c7912"
},
"require-dev": {
"ext-pdo_sqlite": "*",
Expand Down
12 changes: 12 additions & 0 deletions src/AssignmentsStorage.php
Expand Up @@ -128,6 +128,18 @@ public function userHasItem(string $userId, array $itemNames): bool
->exists();
}

public function filterUserItemNames(string $userId, array $itemNames): array
{
/** @var array{itemName: string} $rows */
$rows = (new Query($this->database))
->select('itemName')
->from($this->tableName)
->where(['userId' => $userId, 'itemName' => $itemNames])
->all();

return array_column($rows, 'itemName');
}

public function add(Assignment $assignment): void
{
$this
Expand Down
82 changes: 64 additions & 18 deletions src/ItemTreeTraversal/CteItemTreeTraversal.php
Expand Up @@ -20,6 +20,7 @@
* @internal
*
* @psalm-import-type RawItem from ItemsStorage
* @psalm-import-type AccessTree from ItemTreeTraversalInterface
*/
abstract class CteItemTreeTraversal implements ItemTreeTraversalInterface
{
Expand Down Expand Up @@ -49,34 +50,60 @@ public function getParentRows(string $name): array
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll();
}

public function getChildrenRows(string $name): array
public function getAccessTree(string $name): array
{
$baseOuterQuery = (new Query($this->database))->select('item.*')->where(['!=', 'item.name', $name]);
$baseOuterQuery = (new Query($this->database))->select(['item.*', 'parent_of.children']);
$cteSelectRelationQuery = (new Query($this->database))
->select(['parent', new Expression($this->getTrimConcatChildrenExpression())])
->from("$this->childrenTableName AS item_child_recursive")
->innerJoin('parent_of', [
"item_child_recursive.child" => new Expression('{{parent_of}}.[[child_name]]'),
]);
$cteSelectItemQuery = (new Query($this->database))
->select(['name', new Expression($this->getEmptyChildrenExpression())])
->from($this->tableName)
->where(['name' => $name])
->union($cteSelectRelationQuery, all: true);
$quoter = $this->database->getQuoter();
$outerQuery = $baseOuterQuery
->withQuery(
$cteSelectItemQuery,
$quoter->quoteTableName('parent_of') . '(' . $quoter->quoteColumnName('child_name') . ',' .
$quoter->quoteColumnName('children') . ')',
recursive: $this->useRecursiveInWith,
)
->from('parent_of')
->leftJoin(
['item' => $this->tableName],
['item.name' => new Expression('{{parent_of}}.[[child_name]]')],
);

/** @psalm-var AccessTree */
return $outerQuery->createCommand()->queryAll();
}

public function getChildrenRows(string|array $names): array
{
$baseOuterQuery = $this->getChildrenBaseOuterQuery($names);

/** @psalm-var RawItem[] */
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
return $this->getRowsCommand($names, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
}

public function getChildPermissionRows(string $name): array
public function getChildPermissionRows(string|array $names): array
{
$baseOuterQuery = (new Query($this->database))
->select('item.*')
->where(['!=', 'item.name', $name])
->andWhere(['item.type' => Item::TYPE_PERMISSION]);
$baseOuterQuery = $this->getChildrenBaseOuterQuery($names)->andWhere(['item.type' => Item::TYPE_PERMISSION]);

/** @psalm-var RawItem[] */
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
return $this->getRowsCommand($names, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
}

public function getChildRoleRows(string $name): array
public function getChildRoleRows(string|array $names): array
{
$baseOuterQuery = (new Query($this->database))
->select('item.*')
->where(['!=', 'item.name', $name])
->andWhere(['item.type' => Item::TYPE_ROLE]);
$baseOuterQuery = $this->getChildrenBaseOuterQuery($names)->andWhere(['item.type' => Item::TYPE_ROLE]);

/** @psalm-var RawItem[] */
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
return $this->getRowsCommand($names, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
}

public function hasChild(string $parentName, string $childName): bool
Expand All @@ -96,8 +123,27 @@ public function hasChild(string $parentName, string $childName): bool
return $result !== false;
}

/**
* @infection-ignore-all
* - ProtectedVisibility.
*
* @psalm-return non-empty-string
*/
protected function getEmptyChildrenExpression(): string
{
return "''";
}

/**
* @psalm-return non-empty-string
*/
protected function getTrimConcatChildrenExpression(): string
{
return "TRIM(',' FROM CONCAT(children, ',', item_child_recursive.child))";
}

private function getRowsCommand(
string $name,
string|array $names,
QueryInterface $baseOuterQuery,
bool $areParents = true,
): CommandInterface {
Expand All @@ -124,7 +170,7 @@ private function getRowsCommand(
$cteSelectItemQuery = (new Query($this->database))
->select('name')
->from($this->tableName)
->where(['name' => $name])
->where(['name' => $names])
->union($cteSelectRelationQuery, all: true);
$quoter = $this->database->getQuoter();
$outerQuery = $baseOuterQuery
Expand All @@ -149,7 +195,7 @@ private function getChildrenBaseOuterQuery(string|array $names): QueryInterface
{
$baseOuterQuery = (new Query($this->database))->select('item.*')->distinct();
if (is_string($names)) {
return $baseOuterQuery->where(['!=', 'item.name', $names])
return $baseOuterQuery->where(['!=', 'item.name', $names]);
}

return $baseOuterQuery->where(['not in', 'item.name', $names]);
Expand Down
5 changes: 5 additions & 0 deletions src/ItemTreeTraversal/MssqlCteItemTreeTraversal.php
Expand Up @@ -12,4 +12,9 @@
final class MssqlCteItemTreeTraversal extends CteItemTreeTraversal
{
protected bool $useRecursiveInWith = false;

protected function getEmptyChildrenExpression(): string
{
return "CAST('' AS NVARCHAR(MAX))";
}
}
98 changes: 72 additions & 26 deletions src/ItemTreeTraversal/MysqlItemTreeTraversal.php
Expand Up @@ -19,6 +19,7 @@
* @internal
*
* @psalm-import-type RawItem from ItemsStorage
* @psalm-import-type AccessTree from ItemTreeTraversalInterface
*/
final class MysqlItemTreeTraversal implements ItemTreeTraversalInterface
{
Expand All @@ -42,9 +43,8 @@ public function getParentRows(string $name): array
{
$sql = "SELECT DISTINCT item.* FROM (
SELECT @r AS child_name,
(SELECT @r := parent FROM $this->childrenTableName WHERE child = child_name LIMIT 1) AS parent,
@l := @l + 1 AS level
FROM (SELECT @r := :name, @l := 0) val, $this->childrenTableName
(SELECT @r := parent FROM $this->childrenTableName WHERE child = child_name LIMIT 1) AS parent
FROM (SELECT @r := :name) val, $this->childrenTableName
) s
LEFT JOIN $this->tableName AS item ON item.name = s.child_name
WHERE item.name != :name";
Expand All @@ -56,34 +56,45 @@ public function getParentRows(string $name): array
->queryAll();
}

public function getChildrenRows(string $name): array
public function getAccessTree(string $name): array
{
$baseOuterQuery = (new Query($this->database))->select([new Expression('item.*')])->distinct();
$sql = "SELECT item.*, access_tree_base.children FROM (
SELECT child_name, MIN(TRIM(BOTH ',' FROM TRIM(BOTH child_name FROM raw_children))) as children FROM (
SELECT @r AS child_name, @path := concat(@path, ',', @r) as raw_children,
(SELECT @r := parent FROM $this->childrenTableName WHERE child = child_name LIMIT 1) AS parent
FROM (SELECT @r := :name, @path := '') val, $this->childrenTableName
) raw_access_tree_base
GROUP BY child_name
) access_tree_base
LEFT JOIN $this->tableName AS item ON item.name = access_tree_base.child_name";

/** @psalm-var AccessTree */
return $this
->database
->createCommand($sql, [':name' => $name])
->queryAll();
}

public function getChildrenRows(string|array $names): array
{
/** @psalm-var RawItem[] */
return $this->getChildrenRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll();
return $this->getChildrenRowsCommand($names, baseOuterQuery: $this->getChildrenBaseOuterQuery())->queryAll();
}

public function getChildPermissionRows(string $name): array
public function getChildPermissionRows(string|array $names): array
{
$baseOuterQuery = (new Query($this->database))
->select([new Expression('item.*')])
->distinct()
->where(['item.type' => Item::TYPE_PERMISSION]);
$baseOuterQuery = $this->getChildrenBaseOuterQuery()->where(['item.type' => Item::TYPE_PERMISSION]);

/** @psalm-var RawItem[] */
return $this->getChildrenRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll();
return $this->getChildrenRowsCommand($names, baseOuterQuery: $baseOuterQuery)->queryAll();
}

public function getChildRoleRows(string $name): array
public function getChildRoleRows(string|array $names): array
{
$baseOuterQuery = (new Query($this->database))
->select([new Expression('item.*')])
->distinct()
->where(['item.type' => Item::TYPE_ROLE]);
$baseOuterQuery = $this->getChildrenBaseOuterQuery()->where(['item.type' => Item::TYPE_ROLE]);

/** @psalm-var RawItem[] */
return $this->getChildrenRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll();
return $this->getChildrenRowsCommand($names, baseOuterQuery: $baseOuterQuery)->queryAll();
}

public function hasChild(string $parentName, string $childName): bool
Expand All @@ -97,17 +108,52 @@ public function hasChild(string $parentName, string $childName): bool
return $result !== false;
}

private function getChildrenRowsCommand(string $name, QueryInterface $baseOuterQuery): CommandInterface
/**
* @param string|string[] $names
*/
private function getChildrenRowsCommand(string|array $names, QueryInterface $baseOuterQuery): CommandInterface
{
$names = (array) $names;
$fromSql = "SELECT DISTINCT child
FROM (SELECT * FROM $this->childrenTableName ORDER by parent) item_child_sorted,
(SELECT @pv := :name) init
WHERE find_in_set(parent, @pv) AND length(@pv := concat(@pv, ',', child))";
FROM (SELECT * FROM $this->childrenTableName ORDER by parent) item_child_sorted,\n";
$where = '';
$excludedNamesStr = '';
$parameters = [];
$lastNameIndex = array_key_last($names);

foreach ($names as $index => $name) {
$fromSql .= "(SELECT @pv$index := :name$index) init$index";
$excludedNamesStr .= "@pv$index";

if ($index !== $lastNameIndex) {
$fromSql .= ',';
$excludedNamesStr .= ', ';
}

$fromSql .= "\n";

if ($index !== 0) {
$where .= ' OR ';
}

$where .= "(find_in_set(parent, @pv$index) AND length(@pv$index := concat(@pv$index, ',', child)))";

$parameters[":name$index"] = $name;
}

$where = "($where) AND child NOT IN ($excludedNamesStr)";
$fromSql .= "WHERE $where";
$outerQuery = $baseOuterQuery
->from(['s' => "($fromSql)"])
->leftJoin(['item' => $this->tableName], ['item.name' => new Expression('s.child')])
->addParams([':name' => $name]);
->from(new Expression("($fromSql) s"))
->leftJoin(['item' => $this->tableName], ['item.name' => new Expression('s.child')]);
/** @psalm-var non-empty-string $outerQuerySql */
$outerQuerySql = $outerQuery;

return $outerQuery->createCommand();
return $this->database->createCommand($outerQuerySql, $parameters);
}

private function getChildrenBaseOuterQuery(): QueryInterface
{
return (new Query($this->database))->select('item.*')->distinct();
}
}
4 changes: 4 additions & 0 deletions src/ItemTreeTraversal/SqliteCteItemTreeTraversal.php
Expand Up @@ -12,4 +12,8 @@
*/
final class SqliteCteItemTreeTraversal extends CteItemTreeTraversal
{
protected function getTrimConcatChildrenExpression(): string
{
return "TRIM(children || ',' || item_child_recursive.child, ',')";
}
}

0 comments on commit 75818c0

Please sign in to comment.