yii2-arangodb/mirzaev/yii2/arangodb/Query.php

1710 lines
49 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace mirzaev\yii2\arangodb;
use Yii;
use yii\base\Component;
use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
use yii\db\QueryInterface;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use ArangoDBClient\Document;
use ArangoDBClient\Statement;
use Exception;
class Query extends Component implements QueryInterface
{
const PARAM_PREFIX = 'qp';
const DEBUG = true;
public $separator = " ";
protected $conditionBuilders = [
'NOT' => 'genNotCondition',
'AND' => 'genAndCondition',
'OR' => 'genAndCondition',
'IN' => 'genInCondition',
'LIKE' => 'genLikeCondition',
'BETWEEN' => 'genBetweenCondition'
];
protected $conditionMap = [
'NOT' => '!',
'AND' => '&&',
'OR' => '||',
'IN' => 'in',
'LIKE' => 'LIKE',
];
public $select = [];
public string|array $for;
public string|array $in;
public string $collection;
public array $lets = [];
/**
* Массив коллекций вершин и направлений для их обхода
*
* [
* ["INBOUND" => "collection1"],
* ["OUTBOUND" => "collection2"],
* ["ANY" => "collection2"]
* ]
*/
public array $traversals = [];
public $foreach = [];
public $where = [];
public $limit;
public $offset;
/**
* Поиск
*
* [свойство => его значение]
*/
public array $search;
/**
* Тип поиска
*/
public string $searchType = 'START';
public $orderBy;
public $indexBy;
public $params = [];
public $options = [];
/**
* @param array $options
* @param null|Connection $db
* @return null|Statement
*/
private function getStatement($options = [], $db = null)
{
if ($db === null) {
$db = Yii::$app->get('arangodb');
}
return $db->getStatement($options);
}
/**
* @param $aql
* @param $params
* @return array [$aql, $params]
*/
private static function prepareBindVars($aql, array $params)
{
$search = [];
$replace = [];
foreach ($params as $key => $value) {
if (is_array($value)) {
$search[] = "@$key";
$replace[] = json_encode($value);
unset($params[$key]);
}
}
if (count($search)) {
$aql = str_replace($search, $replace, $aql);
}
return [$aql, $params];
}
/**
* @param null|Connection $db
* @param array $options
* @return null|Statement
*/
public function createCommand($db = null, $options = [])
{
list($aql, $params) = $this->genQuery($this);
$options = ArrayHelper::merge(
$options,
[
'query' => $aql,
'bindVars' => $params,
]
);
return $this->getStatement($options, $db);
}
/**
* @param $aql
* @param array $bindValues
* @param array $params
* @return array
* @throws Exception
*/
public function execute($aql, $bindValues = [], $params = [])
{
list($aql, $bindValues) = self::prepareBindVars($aql, $bindValues);
$options = [
'query' => $aql,
'bindVars' => $bindValues,
];
$options = ArrayHelper::merge($params, $options);
$statement = $this->getStatement($options);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
return $this->prepareResult($cursor->getAll());
}
/**
* @param $fields
* @return $this
*/
public function select($fields): self
{
$this->select = $fields;
return $this;
}
/**
* @param $collection
* @return $this
*/
public function collection(string $collection): self
{
$this->collection = $collection;
return $this;
}
/**
*/
public function for(string|array $for): self
{
$this->for = $for;
return $this;
}
/**
*/
public function in(string|array $in): self
{
$this->in = $in;
return $this;
}
/**
*/
public function search(array $text, string $type = 'START'): self
{
$this->search = $text;
$this->searchType = $type;
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 = 'ANY'): static
{
$this->traversals[] = [
match (strtoupper($direction)) {
'INBOUND', 'OUTBOUND', 'ANY' => $direction,
default => 'ANY'
}
=> $vertex
];
return $this;
}
/**
* Проверка типа и конвертация
*/
protected static function checkArrayAndConvert(string|array $text): string
{
if (is_array($text)) {
return self::convertArrayToString($text);
}
return $text;
}
/**
* Конвертация в строку
*/
protected static function convertArrayToString(array $text): string
{
// Очистка
array_walk($text, 'trim');
// Конвертация
return implode(", ", $text);
}
/**
* Генерация AQL конструкции "FOR"
*
* Примеры:
* 1. "FOR account"
* 2. "FOR account, account_edge_supply"
*/
protected static function genFor(string|array $for): string
{
if (is_array($for)) {
// Если передан массив, то конвертировать в строку
$for = self::convertArrayToString($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 = []): 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 static function quoteCollectionName(string|array $name): string
{
if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
return $name;
}
if (strpos($name, '.') === false) {
return $name;
}
$parts = explode('.', $name);
foreach ($parts as $i => $part) {
$parts[$i] = $part;
}
return implode('.', $parts);
}
/**
* @param $name
* @return string
*/
public function quoteColumnName(string $name)
{
if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
return $name;
}
if (($pos = strrpos($name, '.')) !== false) {
$prefix = substr($name, 0, $pos);
$prefix = $this->quoteCollectionName($prefix) . '.';
$name = substr($name, $pos + 1);
} else {
$prefix = $this->quoteCollectionName($this->collection) . '.';
}
return $prefix . $name;
}
/**
* Генерация AQL кода "FOR IN"
*
* @param $condition
* @param $params
* @return string
*/
protected function genForeach(array $conditions): ?string
{
// Инициализация
$aql = '';
foreach ($conditions as $condition) {
// Перебор выражений
genForeach_recursion:
foreach ($condition as $FOR => $IN) {
// Инициализация операндов
if (is_int($FOR) && is_array($IN)) {
// Вложенный массив (неожиданные входные данные)
// Реинициализация
$condition = $IN;
// Перебор вложенного массива
goto genForeach_recursion;
}
$aql .= "FOR $FOR IN $IN ";
}
}
// Постобработка
$aql = trim($aql);
return $aql;
}
/**
* @param $condition
* @param $params
* @return string
*/
protected function genWhere($condition, array &$params)
{
return ($where = $this->genCondition($condition, $params)) ? 'FILTER ' . $where : '';
}
/**
* @param $condition
* @param $params
* @return string
*
* @todo Разобраться с этим говном
*/
protected function genCondition($condition, array &$params)
{
if (!is_array($condition)) {
return (string) $condition;
} elseif (empty($condition)) {
return '';
}
/**
* @todo Переписать под новую архитектуру
*/
if (isset($condition[0]) && is_string($condition[0]) && count($condition) > 1) {
// Формат: operator, operand 1, operand 2, ...
$operator = strtoupper($condition[0]);
if (isset($this->conditionBuilders[$operator])) {
$method = $this->conditionBuilders[$operator];
array_shift($condition);
return $this->$method($operator, $condition, $params);
} else {
throw new InvalidArgumentException('Found unknown operator in query: ' . $operator);
}
} else {
// hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->genHashCondition($condition, $params);
}
}
/**
* @param $condition
* @param $params
* @return string
* @throws Exception
*/
protected function genHashCondition(array $condition, array &$params)
{
$parts = [];
foreach ($condition as $key => $value) {
// Перебор выражений
// Инициализация
$operator = $condition['operator'] ?? '==';
if (is_int($key) && is_array($value)) {
// Обычный массив (вложенное выражение)
// Начало рекурсивного поиска выражений
recursive_expressions_search:
// Реинициализация
$operator = $value['operator'] ?? $operator;
foreach ($value as $key => $value) {
// Перебор выражений в сложном режиме
if (is_int($key) && is_string($value)) {
// $key = $value;
// $value = null;
} else if (is_int($key) && is_array($value)) {
// Многомерный массив
// Рекурсивный поиск выражений
goto recursive_expressions_search;
}
// Защита алгоритма от использования параметров как продолжения выражения
$break = true;
// Генерация
$this->filterHashCondition($key, $value, $params, $parts, $operator);
}
} else {
// Ассоциативный массив (простой режим)
if (isset($break)) {
// Обработка завершена дальше в цикле идут параметры
break;
}
// Генерация
$this->filterHashCondition($key, $value, $params, $parts, $operator);
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') && (', $parts) . ')';
}
/**
* @todo Можно добавить проверку операторов
*/
protected function filterHashCondition(mixed $column, mixed $value, array &$params, array &$parts, string $operator = '==')
{
if (is_array($value) || $value instanceof Query) {
// Правый операнд передан как массив или инстанция Query
// IN condition
$parts[] = $this->genInCondition('IN', [$column, $value], $params);
} else if (is_int($column) && is_string($value)) {
// Передача без ключа массива (строка с готовым выражением)
$parts[] = $value;
} else {
// Правый операнд передан как строка (подразумевается)
if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column);
}
if ($value === null) {
// Конвертация null
// Генерация
$parts[] = "$column $operator null";
} else {
// Обычная обработка параметра
$phName = self::PARAM_PREFIX . count($params);
// Генерация
$parts[] = "$column $operator @$phName";
$params[$phName] = $value;
}
}
}
/**
* @param $operator
* @param $operands
* @param $params
* @return string
*/
protected function genAndCondition($operator, $operands, &$params)
{
$parts = [];
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->genCondition($operand, $params);
}
if ($operand !== '') {
$parts[] = $operand;
}
}
if (!empty($parts)) {
return '(' . implode(") {$this->conditionMap[$operator]} (", $parts) . ')';
} else {
return '';
}
}
/**
* @param $operator
* @param $operands
* @param $params
* @return string
*/
protected function genNotCondition($operator, $operands, &$params)
{
if (count($operands) != 1) {
throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
}
$operand = reset($operands);
if (is_array($operand)) {
$operand = $this->genCondition($operand, $params);
}
if ($operand === '') {
return '';
}
return "{$this->conditionMap[$operator]} ($operand)";
}
/**
* @param $operator
* @param $operands
* @param $params
* @return string
* @throws Exception
*/
protected function genInCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
if ($values === [] || $column === []) {
return $operator === 'IN' ? '0==1' : '';
}
if ($values instanceof Query) {
// sub-query
list($sql, $params) = $this->genQuery($values, $params);
$column = $column;
if (is_array($column)) {
foreach ($column as $i => $col) {
if (strpos($col, '(') === false) {
$column[$i] = $this->quoteColumnName($col);
}
}
return '(' . implode(', ', $column) . ") {$this->conditionMap[$operator]} ($sql)";
} else {
if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column);
}
return "$column {$this->conditionMap[$operator]} ($sql)";
}
}
$values = (array) $values;
if (count($column) > 1) {
return $this->genCompositeInCondition($operator, $column, $values, $params);
}
if (is_array($column)) {
$column = reset($column);
}
foreach ($values as $i => $value) {
if (is_array($value)) {
$value = isset($value[$column]) ? $value[$column] : null;
}
if ($value === null) {
$values[$i] = 'null';
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
$values[$i] = "@$phName";
}
}
if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column);
}
if (count($values) > 1) {
return "$column {$this->conditionMap[$operator]} [" . implode(', ', $values) . ']';
} else {
$operator = $operator === 'IN' ? '==' : '!=';
return $column . $operator . reset($values);
}
}
/**
* @param $operator
* @param $columns
* @param $values
* @param $params
* @return string
*/
protected function genCompositeInCondition($operator, $columns, $values, &$params)
{
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $column) {
if (isset($value[$column])) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value[$column];
$vs[] = "@$phName";
} else {
$vs[] = 'null';
}
}
$vss[] = '(' . implode(', ', $vs) . ')';
}
foreach ($columns as $i => $column) {
if (strpos($column, '(') === false) {
$columns[$i] = $this->quoteColumnName($column);
}
}
return '(' . implode(', ', $columns) . ") {$this->conditionMap[$operator]} [" . implode(', ', $vss) . ']';
}
/**
* Creates an SQL expressions with the `BETWEEN` operator.
* @param string $operator the operator to use
* @param array $operands the first operand is the column name. The second and third operands
* describe the interval that column value should be in.
* @param array $params the binding parameters to be populated
* @return string the generated AQL expression
* @throws InvalidArgumentException if wrong number of operands have been given.
*/
public function genBetweenCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new InvalidArgumentException("Operator '$operator' requires three operands.");
}
list($column, $value1, $value2) = $operands;
if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column);
}
$phName1 = self::PARAM_PREFIX . count($params);
$params[$phName1] = $value1;
$phName2 = self::PARAM_PREFIX . count($params);
$params[$phName2] = $value2;
return "$column >= @$phName1 && $column <= @$phName2";
}
/**
* @param $operator
* @param $condition
* @param $params
* @return string
*/
protected function genLikeCondition($operator, $condition, &$params)
{
if (!(isset($condition[0]) && isset($condition[1]))) {
throw new InvalidArgumentException("You must set 'column' and 'pattern' params");
}
$caseInsensitive = isset($condition[2]) ? (bool)$condition[2] : false;
return $this->conditionMap[$operator]
. '('
. $this->quoteColumnName($condition[0])
. ', "'
. $condition[1]
. '", '
. ($caseInsensitive ? 'TRUE' : 'FALSE')
. ')';
}
/**
* @param $columns
* @return string
*/
protected function genOrderBy($columns)
{
if (empty($columns)) {
return '';
}
$orders = [];
foreach ($columns as $name => $direction) {
$orders[] = $this->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
}
return 'SORT ' . implode(', ', $orders);
}
/**
* @param $limit
* @return bool
*/
protected function hasLimit($limit)
{
return is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0;
}
/**
* @param $offset
* @return bool
*/
protected function hasOffset($offset)
{
return is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0';
}
/**
* @param $limit
* @param $offset
* @return string
*/
protected function genLimit($limit, $offset)
{
$aql = '';
if ($this->hasLimit($limit)) {
$aql = 'LIMIT ' . ($this->hasOffset($offset) ? $offset : '0') . ',' . $limit;
}
return $aql;
}
/**
* @param $columns
* @param $params
* @return string
*/
protected function genSelect($columns, &$params) // А нахуй здесь params ещё и ссылкой? Потом проверить
{
if ($columns === null || empty($columns)) {
return 'RETURN ' . $this->collection;
}
if (!is_array($columns)) {
return 'RETURN ' . $columns;
}
return 'RETURN ' . self::convertArray2Aql($columns, $this->collection);
}
/**
* @param null $query
* @param array $params
* @return array
*
* @todo Оптимизировать и создать регулируемую очередь выполнения
*/
protected function genQuery($query = null, array $params = [])
{
// Инициализация
$query ?? $query = $this;
$query->in ?? $query->in = $query->collection ?? throw new Exception('Не найдена коллекция');
$query->for ?? $query->for = $query->in;
$query->collection ?? $query->collection = self::checkArrayAndConvert($query->for);
$params = array_merge($params, $query->params);
$clauses = [
$query::genFor($query->for),
$query::genIn($query->in, $query->traversals),
$query::genLet($query->lets),
$query->genForeach($query->foreach),
$query->genWhere($query->where, $params),
isset($query->search) ? $query->genSearch($query->search, $query->searchType) : null,
$query->genOrderBy($query->orderBy, $params),
$query->genLimit($query->limit, $query->offset, $params),
$query->genSelect($query->select, $params),
];
$aql = implode($query->separator, array_filter($clauses));
return self::prepareBindVars($aql, $params);
}
/**
* @param Statement $statement
* @return string
*/
protected static function getRawAql($statement)
{
$query = $statement->getQuery();
$values = $statement->getBindVars();
$search = [];
$replace = [];
foreach ($values as $key => $value) {
$search[] = "/@\b$key\b/";
$replace[] = is_string($value) ? "\"$value\"" : json_encode($value);
}
if (count($search)) {
$query = preg_replace($search, $replace, $query);
}
return $query;
}
/**
* @param null $db
* @return array
* @throws Exception
*/
public function all($db = null)
{
$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');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
return $this->prepareResult($cursor->getAll());
}
/**
* @param null $db
*/
public function one($db = null)
{
$this->limit(1);
$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');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
$result = $this->prepareResult($cursor->getAll());
return empty($result) ? false : $result[0];
}
/**
* @param $columns
* @param array $params
* @param null $db
*
* @return bool
*
* @throws Exception
*/
public function insert($columns, $params = [], $db = null)
{
// Инициализация
$this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция');
$query->collection ?? $query->collection = self::checkArrayAndConvert($query->for);
$data = Serializer::encode($columns);
$clauses = [
"INSERT $data IN {$this->quoteCollectionName($this->collection)}",
$this->genOptions(),
];
$aql = implode($this->separator, array_filter($clauses));
$params = ArrayHelper::merge(
$params,
[
'query' => $aql,
]
);
$statement = $this->getStatement($params, $db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::insert');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::insert');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::insert');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::insert');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
return true;
}
/**
* @param $columns
* @param array $params
* @param null $db
*
* @return bool
*
* @throws Exception
*/
public function update($columns, $params = [], $db = null)
{
// Инициализация
$this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция');
$this->for ?? $this->for = $this->in;
$query->collection ?? $query->collection = self::checkArrayAndConvert($query->for);
$clauses = [
static::genFor($this->for),
static::genIn($this->in, $this->traversals),
$this->genWhere($this->where, $params),
$this->genUpdate($this->in, $columns),
$this->genOptions(),
];
$aql = implode($this->separator, array_filter($clauses));
$params = ArrayHelper::merge(
$params,
[
'query' => $aql,
'bindVars' => $params,
]
);
$statement = $this->getStatement($params, $db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::update');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::update');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::update');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::update');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
$meta = $cursor->getMetadata();
return isset($meta['extra']['operations']['executed']) ?
$meta['extra']['operations']['executed'] :
true;
}
/**
* @param $collection
* @param array $condition
* @param array $params
* @param null $db
* @return bool
* @throws Exception
*/
public function remove($params = [], $db = null)
{
// Инициализация
$this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция');
$this->for ?? $this->for = $this->in;
$query->collection ?? $query->collection = self::checkArrayAndConvert($query->for);
$clauses = [
static::genFor($this->for),
static::genIn($this->in, $this->traversals),
$this->genWhere($this->where, $params),
$this->genRemove($this->in),
$this->genOptions(),
];
$aql = implode($this->separator, array_filter($clauses));
$params = ArrayHelper::merge(
$params,
[
'query' => $aql,
'bindVars' => $params,
]
);
$statement = $this->getStatement($params, $db);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::remove');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::remove');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::remove');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::remove');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
$meta = $cursor->getMetadata();
return isset($meta['extra']['operations']['executed']) ?
$meta['extra']['operations']['executed'] :
true;
}
/**
* @param $collection
* @param $columns
* @return string
*/
protected function genUpdate($collection, $columns)
{
return 'UPDATE ' . $collection . ' WITH '
. Serializer::encode($columns) . ' IN '
. $this->quoteCollectionName($collection);
}
/**
* @param $collection
* @return string
*/
protected function genRemove($collection)
{
return 'REMOVE ' . $collection . ' IN ' . $collection;
}
/**
* @param $collection
* @param $columns
* @return string
*/
protected function genSearch(array $expression, string $type = 'START'): string
{
$query = 'SEARCH ';
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 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
*/
public function prepareResult($rows)
{
$result = [];
if (isset($rows[0]) && $rows[0] instanceof Document) {
if ($this->indexBy === null) {
foreach ($rows as $row) {
$result[] = $row->getAll();
}
} else {
foreach ($rows as $row) {
if (is_string($this->indexBy)) {
$key = $row->{$this->indexBy};
} else {
$key = call_user_func($this->indexBy, $row);
}
$result[$key] = $row->getAll();
}
}
} else {
$result = $rows;
}
return $result;
}
/**
* @param string $q
* @param null $db
* @return int
* @throws Exception
* @throws \triagens\ArangoDb\ClientException
*/
public function count($q = '*', $db = null)
{
$this->select = '1';
$this->limit(1);
$this->offset(0);
$statement = $this->createCommand($db);
$statement->setFullCount(true);
$statement->setBatchSize(1);
$token = $this->getRawAql($statement);
Yii::info($token, 'mirzaev\yii2\arangodb\Query::query');
try {
Yii::beginProfile($token, 'mirzaev\yii2\arangodb\Query::query');
$cursor = $statement->execute();
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
} catch (Exception $ex) {
Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query');
throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex);
}
return $cursor->getFullCount();
}
/**
* @param null $db
* @return bool
* @throws Exception
*/
public function exists($db = null)
{
$record = $this->one($db);
return !empty($record);
}
/**
* @param callable|string $column
* @return $this|static
*/
public function indexBy($column)
{
$this->indexBy = $column;
return $this;
}
/**
* Перебор
*
* FOR НАЗВАНИЕ IN ПЕРЕМЕННАЯ
*
* @param array $expression ['НАЗВАНИЕ' => 'ПЕРЕМЕННАЯ']
*/
public function foreach(array $expression)
{
$this->foreach = match (true) {
empty($this->foreach) => [$expression],
default => $this->foreach []= [$expression]
};
return $this;
}
/**
* @param array $expression
*/
public function where($expression, $operator = 'AND')
{
$this->where = match (true) {
is_null($this->where) => $expression,
default => [$operator, $this->where, $expression]
};
return $this;
}
/**
*/
public function let(string $name, mixed $value): static
{
$this->lets[$name] = $value;
return $this;
}
/**
* @param array|string $condition
* @param array $params
* @return $this|static
*/
public function andWhere($condition, $params = [])
{
if (is_null($this->where)) {
$this->where = $condition;
} else {
$this->where = ['AND', $this->where, $condition];
}
$this->params($params);
return $this;
}
/**
* @param array|string $condition
* @param array $params
* @return $this|static
*/
public function orWhere($condition, $params = [])
{
if ($this->where === null) {
$this->where = $condition;
} else {
$this->where = ['OR', $this->where, $condition];
}
$this->params($params);
return $this;
}
/**
* Sets the WHERE part of the query but ignores [[isEmpty()|empty operands]].
*
* 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 gening query conditions based on filter values entered by users.
*
* The following code shows the difference between this method and [[where()]]:
*
* ```php
* // WHERE `age`=:age
* $query->filterWhere(['name' => null, 'age' => 20]);
* // WHERE `age`=:age
* $query->where(['age' => 20]);
* // WHERE `name` IS NULL AND `age`=:age
* $query->where(['name' => null, 'age' => 20]);
* ```
*
* Note that unlike [[where()]], you cannot pass binding parameters to this method.
*
* @param array $condition the conditions that should be put in the WHERE part.
* See [[where()]] on how to specify this parameter.
* @return static the query object itself.
* @see where()
* @see andFilterWhere()
* @see orFilterWhere()
*/
public function filterWhere(array $condition)
{
$condition = $this->filterCondition($condition);
if ($condition !== []) {
$this->where($condition);
}
return $this;
}
public function filterStartsWith(array $expression): string
{
// Генерация
foreach ($expression as $key => $value) {
if (isset($return)) {
$return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")";
} else {
$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%\")";
}
}
return $return;
}
/**
* Adds an additional WHERE condition to the existing one but ignores [[isEmpty()|empty operands]].
* The new condition and the existing one will be joined using the 'AND' operator.
*
* 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 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.
* @return static the query object itself.
* @see filterWhere()
* @see orFilterWhere()
*/
public function andFilterWhere(array $condition)
{
$condition = $this->filterCondition($condition);
if ($condition !== []) {
$this->andWhere($condition);
}
return $this;
}
/**
* Adds an additional WHERE condition to the existing one but ignores [[isEmpty()|empty operands]].
* The new condition and the existing one will be joined using the 'OR' operator.
*
* 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 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.
* @return static the query object itself.
* @see filterWhere()
* @see andFilterWhere()
*/
public function orFilterWhere(array $condition)
{
$condition = $this->filterCondition($condition);
if ($condition !== []) {
$this->orWhere($condition);
}
return $this;
}
/**
* Returns a value indicating whether the give value is "empty".
*
* The value is considered "empty", if one of the following conditions is satisfied:
*
* - it is `null`,
* - an empty string (`''`),
* - a string containing only whitespace characters,
* - or an empty array.
*
* @param mixed $value
* @return boolean if the value is empty
*/
protected function isEmpty($value)
{
return $value === '' || $value === [] || $value === null || is_string($value) && (trim($value) === '' || trim($value, '%') === '');
}
/**
* Removes [[isEmpty()|empty operands]] from the given query condition.
*
* @param array $condition the original condition
* @return array the condition with [[isEmpty()|empty operands]] removed.
* @throws NotSupportedException if the condition operator is not supported
*/
protected function filterCondition($condition)
{
if (!is_array($condition)) {
return $condition;
}
if (!isset($condition[0])) {
// hash format: 'column1' => 'value1', 'column2' => 'value2', ...
foreach ($condition as $name => $value) {
if ($this->isEmpty($value)) {
unset($condition[$name]);
}
}
return $condition;
}
// operator format: operator, operand 1, operand 2, ...
$operator = array_shift($condition);
switch (strtoupper($operator)) {
case 'NOT':
case 'AND':
case 'OR':
foreach ($condition as $i => $operand) {
$subCondition = $this->filterCondition($operand);
if ($this->isEmpty($subCondition)) {
unset($condition[$i]);
} else {
$condition[$i] = $subCondition;
}
}
if (empty($condition)) {
return [];
}
break;
case 'IN':
case 'LIKE':
if (array_key_exists(1, $condition) && $this->isEmpty($condition[1])) {
return [];
}
break;
case 'BETWEEN':
if ((array_key_exists(1, $condition) && $this->isEmpty($condition[1]))
|| (array_key_exists(2, $condition) && $this->isEmpty($condition[2]))
) {
return [];
}
break;
default:
throw new NotSupportedException("Operator not supported: $operator");
}
array_unshift($condition, $operator);
return $condition;
}
/**
* @param array|string $columns
* @return $this|static
*/
public function orderBy($columns)
{
$this->orderBy = $this->normalizeOrderBy($columns);
return $this;
}
/**
* @param array|string $columns
* @return $this|static
*/
public function addOrderBy($columns)
{
$columns = $this->normalizeOrderBy($columns);
if ($this->orderBy === null) {
$this->orderBy = $columns;
} else {
$this->orderBy = array_merge($this->orderBy, $columns);
}
return $this;
}
/**
* @param int $limit
* @return $this|static
*/
public function limit($limit)
{
// Если $limit === 0 то $limit = null
$limit === 0 and $limit = null;
$this->limit = $limit;
return $this;
}
/**
* @param int $offset
* @return $this|static
*/
public function offset($offset)
{
$this->offset = $offset;
return $this;
}
/**
* @param $columns
* @return array
*/
protected function normalizeOrderBy($columns)
{
if (is_array($columns)) {
return $columns;
} else {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
$result = [];
foreach ($columns as $column) {
if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
$result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC;
} else {
$result[$column] = SORT_ASC;
}
}
return $result;
}
}
/**
* Sets the parameters to be bound to the query.
* @param array $params list of query parameter values indexed by parameter placeholders.
* For example, `[':name' => 'Dan', ':age' => 31]`.
* @return static the query object itself
* @see params()
*/
public function params(array ...$params)
{
foreach ($params as $params) {
// Перебор параметров
$this->params = match (true) {
empty($this->params) => $params,
default => (function ($params) {
// Инициализация
$return = [];
foreach ($params as $name => $value) {
// Перебор параметров
if (is_integer($name)) {
// Обычный массив
$return[] = $value;
} else {
// Ассоциативный массив
$return[$name] = $value;
}
}
return array_merge($this->params, $return);
})($params)
};
}
return $this;
}
/**
* @param $options
* @return $this
*/
public function options($options)
{
$this->options = $options;
return $this;
}
/**
* @param $options
* @return $this
*/
public function addOptions($options)
{
if (!empty($options)) {
if (empty($this->options)) {
$this->params = $options;
} else {
foreach ($options as $name => $value) {
if (is_integer($name)) {
$this->options[] = $value;
} else {
$this->options[$name] = $value;
}
}
}
}
return $this;
}
public function emulateExecution($value = true)
{
}
}