Добавление LET, Traversal. рефакторинг

This commit is contained in:
RedHood 2021-01-31 11:36:27 +10:00
parent 5faa656ede
commit bf44832914
4 changed files with 355 additions and 106 deletions

View File

@ -20,7 +20,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
parent::__construct($config);
}
protected function buildQuery($query = null, $params = [])
protected function genQuery($query = null, $params = [])
{
if ($this->primaryModel !== null) {
// lazy loading
@ -46,7 +46,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
}
return parent::buildQuery($query, $params);
return parent::genQuery($query, $params);
}
private function createModels($rows)
@ -104,6 +104,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
{
$statement = $this->createCommand($db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
@ -114,6 +115,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
throw new \Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
if (!empty($rows)) {
$models = $this->createModels($rows);
if (!empty($this->with)) {

View File

@ -107,8 +107,8 @@ abstract class ActiveRecord extends BaseActiveRecord
public static function find()
{
/** @var ActiveQuery $query */
$query = \Yii::createObject(ActiveQuery::className(), [get_called_class()]);
$query->from(static::collectionName())->select(static::collectionName());
$query = Yii::createObject(ActiveQuery::class, [get_called_class()]);
$query->collection(static::collectionName())->select(static::collectionName());
return $query;
}
@ -123,6 +123,14 @@ abstract class ActiveRecord extends BaseActiveRecord
$row = $row->getAll();
}
if (!is_iterable($row)) {
// Если нельзя обработать полученные данные
// Например, если прочитан NULL, то далее будет ошибка при обработке в foreach
// Реинициализация
$row = [];
}
parent::populateRecord($record, $row);
}

View File

@ -2,9 +2,9 @@
namespace mirzaev\yii2\arangodb;
use yii\base\Object;
use yii\base\BaseObject;
class AqlExpression extends Object
class AqlExpression extends BaseObject
{
/**
* @var string the AQL expression represented by this object

View File

@ -19,16 +19,17 @@ use Exception;
class Query extends Component implements QueryInterface
{
const PARAM_PREFIX = 'qp';
const DEBUG = true;
public $separator = " ";
protected $conditionBuilders = [
'NOT' => 'buildNotCondition',
'AND' => 'buildAndCondition',
'OR' => 'buildAndCondition',
'IN' => 'buildInCondition',
'LIKE' => 'buildLikeCondition',
'BETWEEN' => 'buildBetweenCondition',
'NOT' => 'genNotCondition',
'AND' => 'genAndCondition',
'OR' => 'genAndCondition',
'IN' => 'genInCondition',
'LIKE' => 'genLikeCondition',
'BETWEEN' => 'genBetweenCondition'
];
protected $conditionMap = [
@ -41,9 +42,26 @@ class Query extends Component implements QueryInterface
public $select = [];
public $from;
public string|array $for;
public $where;
public string|array $in;
public string $collection;
public array $vars = [];
/**
* Массив коллекций вершин и направлений для их обхода
*
* [
* ["INBOUND" => "collection1"],
* ["OUTBOUND" => "collection2"],
* ["ANY" => "collection2"]
* ]
*/
public array $traversals = [];
public $where = [];
public $limit;
@ -103,7 +121,7 @@ class Query extends Component implements QueryInterface
*/
public function createCommand($db = null, $options = [])
{
list($aql, $params) = $this->buildQuery($this);
list($aql, $params) = $this->genQuery($this);
$options = ArrayHelper::merge(
$options,
@ -162,28 +180,120 @@ class Query extends Component implements QueryInterface
* @param $collection
* @return $this
*/
public function from($collection)
public function collection(string $collection)
{
$this->from = $collection;
$this->collection = $collection;
return $this;
}
/**
* @param $collection
* @return string
*/
protected function buildFrom($collection)
public function for(string|array $for)
{
$collection = trim($collection);
return $collection ? "FOR $collection IN $collection" : '';
$this->for = $for;
return $this;
}
/**
* @param $name
*/
public function in(string|array $in)
{
$this->in = $in;
return $this;
}
/**
* Обойти коллекцию вершин по направлению
*
* Генерация AQL выражения
*
* @see https://www.arangodb.com/docs/3.7/aql/operations-let.html
*
* @param mixed $vertex Коллекция вершин из которой требуется обход
* @param string $direction Направление ('INBOUND', 'OUTBOUND', 'ANY')
*/
public function traversal(string $vertex, string $direction = 'INBOUND'): static
{
$this->traversals[] = [
match ($direction) {
'INBOUND', 'OUTBOUND', 'ANY' => $direction,
default => null
}
=> $vertex
];
return $this;
}
/**
* Генерация AQL конструкции "FOR"
*
* Примеры:
* 1. "FOR account"
* 2. "FOR account, account_edge_supply"
*
*/
protected static function genFor(string|array $for): string
{
if (is_array($for)) {
// Если передан массив, то конвертировать в строку
// Очистка элементов через trim()
array_walk($for, 'trim');
// Конвертация
$for = implode(", ", $for);
}
// Генерация
return "FOR $for";
}
/**
* Генерация AQL конструкции "IN"
*
* Примеры:
* 1. "IN account"
* 2. "IN INBOUND supply account_edge_supply"
*/
protected static function genIn(string|array|null $in, array $traversals = null): string
{
if (is_array($in)) {
// Если передан массив, то конвертировать в строку
// Очистка элементов через trim()
array_walk($in, 'trim');
// Конвертация
$in = implode(", ", $in);
}
$expression = '';
foreach ($traversals as $traversal) {
foreach ($traversal as $direction => $vertex) {
if ($aql = static::genTraversal($direction, $vertex)) {
$expression .= $aql . ', ';
}
}
}
$expression = trim($expression, ', ');
// Если сгенерированное выражение не пустое, то добавить пробел
$expression = !empty($expression) ? $expression . ' ' : null;
// Генерация
return "IN $expression" . $in;
}
/**
* @param string|array $name Название
* @return string
*/
public function quoteCollectionName($name)
public static function quoteCollectionName(string|array $name): string
{
if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
return $name;
@ -203,7 +313,7 @@ class Query extends Component implements QueryInterface
* @param $name
* @return string
*/
public function quoteColumnName($name)
public function quoteColumnName(string $name)
{
if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
return $name;
@ -213,7 +323,7 @@ class Query extends Component implements QueryInterface
$prefix = $this->quoteCollectionName($prefix) . '.';
$name = substr($name, $pos + 1);
} else {
$prefix = $this->quoteCollectionName($this->from) . '.';
$prefix = $this->quoteCollectionName($this->collection) . '.';
}
return $prefix . $name;
@ -224,19 +334,21 @@ class Query extends Component implements QueryInterface
* @param $params
* @return string
*/
protected function buildWhere($condition, &$params)
protected function genWhere($condition, &$params)
{
$where = $this->buildCondition($condition, $params);
$where = $this->genCondition($condition, $params);
return $where === '' ? '' : 'FILTER ' . $where;
return $where ? 'FILTER ' . $where : '';
}
/**
* @param $condition
* @param $params
* @return string
*
* @todo Разобраться с этим говном
*/
protected function buildCondition($condition, &$params)
protected function genCondition($condition, &$params)
{
if (!is_array($condition)) {
return (string) $condition;
@ -254,7 +366,7 @@ class Query extends Component implements QueryInterface
throw new InvalidParamException('Found unknown operator in query: ' . $operator);
}
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($condition, $params);
return $this->genHashCondition($condition, $params);
}
}
@ -264,13 +376,13 @@ class Query extends Component implements QueryInterface
* @return string
* @throws Exception
*/
protected function buildHashCondition($condition, &$params)
protected function genHashCondition($condition, &$params)
{
$parts = [];
foreach ($condition as $column => $value) {
if (is_array($value) || $value instanceof Query) {
// IN condition
$parts[] = $this->buildInCondition('IN', [$column, $value], $params);
$parts[] = $this->genInCondition('IN', [$column, $value], $params);
} else {
if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column);
@ -293,12 +405,12 @@ class Query extends Component implements QueryInterface
* @param $params
* @return string
*/
protected function buildAndCondition($operator, $operands, &$params)
protected function genAndCondition($operator, $operands, &$params)
{
$parts = [];
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->buildCondition($operand, $params);
$operand = $this->genCondition($operand, $params);
}
if ($operand !== '') {
$parts[] = $operand;
@ -317,7 +429,7 @@ class Query extends Component implements QueryInterface
* @param $params
* @return string
*/
protected function buildNotCondition($operator, $operands, &$params)
protected function genNotCondition($operator, $operands, &$params)
{
if (count($operands) != 1) {
throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
@ -325,7 +437,7 @@ class Query extends Component implements QueryInterface
$operand = reset($operands);
if (is_array($operand)) {
$operand = $this->buildCondition($operand, $params);
$operand = $this->genCondition($operand, $params);
}
if ($operand === '') {
return '';
@ -341,7 +453,7 @@ class Query extends Component implements QueryInterface
* @return string
* @throws Exception
*/
protected function buildInCondition($operator, $operands, &$params)
protected function genInCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
@ -355,7 +467,7 @@ class Query extends Component implements QueryInterface
if ($values instanceof Query) {
// sub-query
list($sql, $params) = $this->buildQuery($values, $params);
list($sql, $params) = $this->genQuery($values, $params);
$column = (array)$column;
if (is_array($column)) {
foreach ($column as $i => $col) {
@ -375,7 +487,7 @@ class Query extends Component implements QueryInterface
$values = (array) $values;
if (count($column) > 1) {
return $this->buildCompositeInCondition($operator, $column, $values, $params);
return $this->genCompositeInCondition($operator, $column, $values, $params);
}
if (is_array($column)) {
@ -412,7 +524,7 @@ class Query extends Component implements QueryInterface
* @param $params
* @return string
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
protected function genCompositeInCondition($operator, $columns, $values, &$params)
{
$vss = [];
foreach ($values as $value) {
@ -446,7 +558,7 @@ class Query extends Component implements QueryInterface
* @return string the generated AQL expression
* @throws InvalidParamException if wrong number of operands have been given.
*/
public function buildBetweenCondition($operator, $operands, &$params)
public function genBetweenCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new InvalidParamException("Operator '$operator' requires three operands.");
@ -471,7 +583,7 @@ class Query extends Component implements QueryInterface
* @param $params
* @return string
*/
protected function buildLikeCondition($operator, $condition, &$params)
protected function genLikeCondition($operator, $condition, &$params)
{
if (!(isset($condition[0]) && isset($condition[1]))) {
throw new InvalidParamException("You must set 'column' and 'pattern' params");
@ -491,7 +603,7 @@ class Query extends Component implements QueryInterface
* @param $columns
* @return string
*/
protected function buildOrderBy($columns)
protected function genOrderBy($columns)
{
if (empty($columns)) {
return '';
@ -527,7 +639,7 @@ class Query extends Component implements QueryInterface
* @param $offset
* @return string
*/
protected function buildLimit($limit, $offset)
protected function genLimit($limit, $offset)
{
$aql = '';
if ($this->hasLimit($limit)) {
@ -542,22 +654,17 @@ class Query extends Component implements QueryInterface
* @param $params
* @return string
*/
protected function buildSelect($columns, &$params) // А нахуй здесь params ещё и ссылкой? Потом проверить
protected function genSelect($columns, &$params) // А нахуй здесь params ещё и ссылкой? Потом проверить
{
if ($columns === null || empty($columns)) {
return 'RETURN ' . $this->from;
return 'RETURN ' . $this->collection;
}
if (!is_array($columns)) {
return 'RETURN ' . $columns;
}
$names = '';
foreach ($columns as $name => $column) {
$names .= "\"$name\": $this->from.$column, ";
}
return 'RETURN {' . trim($names, ', ') . '}';
return 'RETURN ' . self::convertArray2Aql($columns, $this->collection);
}
/**
@ -565,24 +672,20 @@ class Query extends Component implements QueryInterface
* @param array $params
* @return array
*/
protected function buildQuery($query = null, $params = [])
protected function genQuery($query = null, $params = [])
{
$query = isset($query) ? $query : $this;
if ($query->where === null) {
$where = [];
} else {
$where = $query->where;
}
$params = empty($params) ? $query->params : array_merge($params, $query->params);
$clauses = [
$this->buildFrom($query->from),
$this->buildWhere($where, $params),
$this->buildOrderBy($query->orderBy, $params),
$this->buildLimit($query->limit, $query->offset, $params),
$this->buildSelect($query->select, $params),
static::genFor($query->for ?? $query->collection),
static::genIn($query->in ?? $query->collection, $query->traversals),
static::genLet($query->vars),
$this->genWhere($query->where, $params),
$this->genOrderBy($query->orderBy, $params),
$this->genLimit($query->limit, $query->offset, $params),
$this->genSelect($query->select, $params),
];
$aql = implode($query->separator, array_filter($clauses));
@ -622,6 +725,7 @@ class Query extends Component implements QueryInterface
{
$statement = $this->createCommand($db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
@ -671,7 +775,7 @@ class Query extends Component implements QueryInterface
$clauses = [
"INSERT $doc IN {$this->quoteCollectionName($collection)}",
$this->buildOptions(),
$this->genOptions(),
];
$aql = implode($this->separator, array_filter($clauses));
@ -708,12 +812,13 @@ class Query extends Component implements QueryInterface
*/
public function update($collection, $columns, $condition = [], $params = [], $db = null)
{
$this->from($collection);
$this->collection = $collection;
$clauses = [
$this->buildFrom($collection),
$this->buildWhere($condition, $params),
$this->buildUpdate($collection, $columns),
$this->buildOptions(),
$this->genFor($collection),
$this->genIn($collection),
$this->genWhere($condition, $params),
$this->genUpdate($collection, $columns),
$this->genOptions(),
];
$aql = implode($this->separator, array_filter($clauses));
@ -744,31 +849,33 @@ class Query extends Component implements QueryInterface
}
/**
* @param $collection
* @param $columns
* @param array $condition
* Представление
*
* Работа с представлениями
*
* @see https://www.arangodb.com/docs/3.7/http/views.html
*
* @param string $collection
* @param string|array $vars
* @param array $expression
* @param string $type
* @param array $params
* @param null $db
* @return bool
* @throws Exception
* @param Connection $db
*/
public function search(string|array $collection, string|array $vars, array $expression, int $type = 0, $params = [], $db = null): array
public function view(string $collection, string|array|null $vars = null, array $expression = null, string $type = null, array $params = [], $db = null): array
{
$this->from($collection);
$this->collection = $collection;
$clauses = [
$this->buildFrom($collection),
$this->buildSearch($expression, $type),
$this->buildLimit($this->limit, 0),
$this->buildOptions(),
$this->buildSelect($vars, $params)
$this->genFor($collection),
$this->genIn($collection),
$this->genSearch($expression, $type),
$this->genLimit($this->limit, 0),
$this->genOptions(),
$this->genSelect($vars, $params)
];
$aql = implode($this->separator, array_filter($clauses));
$fp = fopen('debug.txt', 'a');
fwrite($fp, print_r($aql, true) . PHP_EOL);
fclose($fp);
$params = ArrayHelper::merge(
$params,
[
@ -791,6 +898,25 @@ class Query extends Component implements QueryInterface
return $this->prepareResult($cursor->getAll());
}
/**
* Поиск ребра
*
* Работа с представлениями
*/
// public function searchEdge(string $collection, string $_from, string $_to, string|array|null $vars = null, string $direction = 'INBOUND', array $expression = null, array $params = [], $db = null)
// {
// $this->collection = $collection;
// $clauses = [
// $this->genFor($_from),
// $this->genLet($collection, $this->genFor([$_from, $collection], [$_to, $collection], $direction), $params),
// $this->genLimit($this->limit, 0),
// $this->genOptions(),
// $this->genSelect($vars, $params)
// ];
// $aql = implode($this->separator, array_filter($clauses));
// }
/**
* @param $collection
* @param array $condition
@ -801,12 +927,13 @@ class Query extends Component implements QueryInterface
*/
public function remove($collection, $condition = [], $params = [], $db = null)
{
$this->from($collection);
$this->collection = $collection;
$clauses = [
$this->buildFrom($collection),
$this->buildWhere($condition, $params),
$this->buildRemove($collection),
$this->buildOptions(),
$this->genFor($collection),
$this->genIn($collection),
$this->genWhere($condition, $params),
$this->genRemove($collection),
$this->genOptions(),
];
$aql = implode($this->separator, array_filter($clauses));
@ -841,7 +968,7 @@ class Query extends Component implements QueryInterface
* @param $columns
* @return string
*/
protected function buildUpdate($collection, $columns)
protected function genUpdate($collection, $columns)
{
return 'UPDATE ' . $collection . ' WITH '
. Serializer::encode($columns) . ' IN '
@ -852,7 +979,7 @@ class Query extends Component implements QueryInterface
* @param $collection
* @return string
*/
protected function buildRemove($collection)
protected function genRemove($collection)
{
return 'REMOVE ' . $collection . ' IN ' . $collection;
}
@ -862,24 +989,108 @@ class Query extends Component implements QueryInterface
* @param $columns
* @return string
*/
protected function buildSearch(array $expression, int $type = 0): string
protected function genSearch(array $expression, string $type = 'START'): string
{
$query = 'SEARCH ';
return match ($type) {
1 => $query . $this->filterStartsWith($expression),
return match (strtoupper($type)) {
'START', 'STARTS', 'STARTS_WITH' => $query . $this->filterStartsWith($expression),
'CONTAINS', 'LIKE' => $query . $this->filterContains($expression),
default => $query . Serializer::encode($expression)
};
}
/**
* Присвоение переменной значения
*
* Генерация AQL выражения
*
* @see https://www.arangodb.com/docs/3.7/aql/operations-let.html
*
* @param array $vars Ключ - переменная, значение - её значение
*/
protected static function genLet(array $vars): ?string
{
// Инициализация
$result = '';
// Конвертация
foreach ($vars as $name => $value) {
if (
$value[0] === '('
|| $value[0] === '"' && substr($value, -1) === '"'
|| $value[0] === "'" && substr($value, -1) === "'"
) {
$condition = $value;
} else {
$condition = '"' . $value . '"';
}
$result .= 'LET ' . $name . ' = ' . $condition . ', ';
}
return trim($result, ', ');
}
/**
* Обойти коллекцию вершин по направлению
*
* Генерация AQL выражения
*
* @see https://www.arangodb.com/docs/3.7/aql/operations-let.html
*
* @param string $direction Направление
* @param mixed $vertex Коллекция вершин из которой требуется обход
*/
protected static function genTraversal(string $direction, string $vertex): ?string
{
return $direction . ' ' . $vertex;
}
/**
* @return string
*/
protected function buildOptions()
protected function genOptions()
{
return empty($this->options) ? '' : ' OPTIONS ' . Json::encode($this->options);
}
/**
* Конвертер Array -> AQL (string)
*
* Примеры:
* 1. {"id": product_search._key, "catn": product_search.catn}
* 2. {"name": login, "phone": number}
* 3. {"users": users}
* 4. {}
*
* @param array $target Массив для конвертации
* @param string|null Название коллекции к которой привязывать значения массива
*/
protected static function convertArray2Aql(array $target, string|null $collection = null): string
{
// Инициализация
$result = '';
// Конвертация
foreach ($target as $name => $value) {
$result .= "\"$name\": ";
if (is_null($collection)) {
// Коллекция не отправлена
$result .= "$value, ";
} else {
// Коллекция отправлена
$result .= "$collection.$value, ";
}
}
return '{' . trim($result, ', ') . '}';
}
/**
* @param Document[] $rows
* @return array
@ -955,6 +1166,7 @@ class Query extends Component implements QueryInterface
public function indexBy($column)
{
$this->indexBy = $column;
return $this;
}
@ -963,10 +1175,19 @@ class Query extends Component implements QueryInterface
* @param array $params
* @return $this|static
*/
public function where($condition, $params = [])
public function where($condition)
{
$this->where = $condition;
$this->addParams($params);
return $this;
}
/**
*/
public function let(string $name, mixed $value): static
{
$this->vars[$name] = $value;
return $this;
}
@ -1007,7 +1228,7 @@ class Query extends Component implements QueryInterface
*
* This method is similar to [[where()]]. The main difference is that this method will
* remove [[isEmpty()|empty query operands]]. As a result, this method is best suited
* for building query conditions based on filter values entered by users.
* for gening query conditions based on filter values entered by users.
*
* The following code shows the difference between this method and [[where()]]:
*
@ -1046,9 +1267,26 @@ class Query extends Component implements QueryInterface
// Генерация
foreach ($expression as $key => $value) {
if ($return) {
$return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->from) . ".$key, \"$value\")";
$return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
} else {
$return = 'STARTS_WITH(' . $this->quoteCollectionName($this->from) . ".$key, \"$value\")";
$return = 'STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
}
}
return $return;
}
public function filterContains(array $expression): string
{
// Инициализация
$return = [];
// Генерация
foreach ($expression as $key => $value) {
if ($return) {
$return .= ' OR LIKE(' . $this->quoteCollectionName($this->collection) . ".$key, \"%$value%\")";
} else {
$return = 'LIKE(' . $this->quoteCollectionName($this->collection) . ".$key, \"%$value%\")";
}
}
@ -1061,7 +1299,7 @@ class Query extends Component implements QueryInterface
*
* This method is similar to [[andWhere()]]. The main difference is that this method will
* remove [[isEmpty()|empty query operands]]. As a result, this method is best suited
* for building query conditions based on filter values entered by users.
* for gening query conditions based on filter values entered by users.
*
* @param array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
@ -1084,7 +1322,7 @@ class Query extends Component implements QueryInterface
*
* This method is similar to [[orWhere()]]. The main difference is that this method will
* remove [[isEmpty()|empty query operands]]. As a result, this method is best suited
* for building query conditions based on filter values entered by users.
* for gening query conditions based on filter values entered by users.
*
* @param array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
@ -1287,6 +1525,7 @@ class Query extends Component implements QueryInterface
}
}
}
return $this;
}