diff --git a/mirzaev/yii2/arangodb/ActiveQuery.php b/mirzaev/yii2/arangodb/ActiveQuery.php index 60be2c2..b574cc5 100644 --- a/mirzaev/yii2/arangodb/ActiveQuery.php +++ b/mirzaev/yii2/arangodb/ActiveQuery.php @@ -103,9 +103,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface 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(); diff --git a/mirzaev/yii2/arangodb/Migration.php b/mirzaev/yii2/arangodb/Migration.php index a786c2a..6eec329 100644 --- a/mirzaev/yii2/arangodb/Migration.php +++ b/mirzaev/yii2/arangodb/Migration.php @@ -23,7 +23,7 @@ abstract class Migration extends Component implements MigrationInterface { parent::init(); - $this->db = Instance::ensure($this->db, Connection::class; + $this->db = Instance::ensure($this->db, Connection::class); } public function execute($aql, $bindValues = [], $params = []) diff --git a/mirzaev/yii2/arangodb/Query.php b/mirzaev/yii2/arangodb/Query.php index 5ef1a1b..31eb97d 100644 --- a/mirzaev/yii2/arangodb/Query.php +++ b/mirzaev/yii2/arangodb/Query.php @@ -74,12 +74,19 @@ class Query extends Component implements QueryInterface * * [свойство => его значение] */ - public array $search; + public array|string $search; /** - * Тип поиска + * Фильтр + * + * [свойство => его значение] */ - public string $searchType = 'START'; + public array|string $filter; + + /** + * Отладка + */ + public bool $debug = false; public $orderBy; @@ -145,6 +152,11 @@ class Query extends Component implements QueryInterface ] ); + if ($this->debug) { + var_dump($aql); + die; + } + return $this->getStatement($options, $db); } @@ -221,22 +233,27 @@ class Query extends Component implements QueryInterface /** */ - public function search(array $text, string $type = 'START'): self + public function search(array|string $expressions): self { - $this->search = $text; - $this->searchType = $type; + $this->search = $expressions; return $this; } + /** + */ + public function filter(array|string $expressions): self + { + $this->filter = $expressions; + + return $this; + } /** * Обойти коллекцию вершин по направлению * * Генерация AQL выражения * - * @see https://www.arangodb.com/docs/3.7/aql/operations-let.html - * * @param mixed $vertex Коллекция вершин из которой требуется обход * @param string $direction Направление ('INBOUND', 'OUTBOUND', 'ANY') */ @@ -253,24 +270,43 @@ class Query extends Component implements QueryInterface 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)) { // Если передан массив, то конвертировать в строку - // Очистка элементов через trim() - array_walk($for, 'trim'); - - // Конвертация - $for = implode(", ", $for); + $for = self::convertArrayToString($for); } // Генерация @@ -368,19 +404,17 @@ class Query extends Component implements QueryInterface foreach ($conditions as $condition) { // Перебор выражений - genForeach_recursion: - foreach ($condition as $FOR => $IN) { // Инициализация операндов if (is_int($FOR) && is_array($IN)) { // Вложенный массив (неожиданные входные данные) - // Реинициализация - $condition = $IN; + // !!! Вход в рекурсию !!! + // Обработка вложенного массива + $aql .= ' ' . $this->genForeach([$IN]); - // Перебор вложенного массива - goto genForeach_recursion; + continue; } $aql .= "FOR $FOR IN $IN "; @@ -751,8 +785,13 @@ class Query extends Component implements QueryInterface return ''; } $orders = []; + foreach ($columns as $name => $direction) { - $orders[] = $this->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); + $orders[] = $this->quoteColumnName($name) . match($direction) { + SORT_DESC => ' DESC', + SORT_ASC => ' ASC', + default => '' + }; } return 'SORT ' . implode(', ', $orders); @@ -819,27 +858,33 @@ class Query extends Component implements QueryInterface protected function genQuery($query = null, array $params = []) { // Инициализация - isset($query) ? $query : $query = $this; - $this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция')); - $this->for ?? $this->for = $this->in; - $this->collection ?? $this->collection = $this->in; + $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 = [ - static::genFor($query->for ?? $query->collection), - static::genIn($query->in ?? $query->collection, $query->traversals), - static::genLet($query->lets), - $this->genForeach($query->foreach), - $this->genWhere($query->where, $params), - isset($this->search) ? $this->genSearch($this->search, $this->searchType) : null, - $this->genOrderBy($query->orderBy, $params), - $this->genLimit($query->limit, $query->offset, $params), - $this->genSelect($query->select, $params), + $query::genFor($query->for), + $query::genIn($query->in, $query->traversals), + $query::genLet($query->lets), + $query->genForeach($query->foreach), + isset($query->search) ? $query->genSearch($query->search) : null, + isset($query->filter) ? $query->genFilter($query->filter) : null, + $query->genWhere($query->where, $params), + $query->genOrderBy($query->orderBy, $params), + $query->genLimit($query->limit, $query->offset, $params), + $query->genSelect($query->select, $params), ]; $aql = implode($query->separator, array_filter($clauses)); + if ($query->debug) { + var_dump($aql); + die; + } + return self::prepareBindVars($aql, $params); } @@ -874,9 +919,11 @@ class Query extends Component implements QueryInterface 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(); @@ -885,6 +932,7 @@ class Query extends Component implements QueryInterface Yii::endProfile($token, 'mirzaev\yii2\arangodb\Query::query'); throw new Exception($ex->getMessage(), (int) $ex->getCode(), $ex); } + return $this->prepareResult($cursor->getAll()); } @@ -894,10 +942,14 @@ class Query extends Component implements QueryInterface 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(); @@ -922,13 +974,13 @@ class Query extends Component implements QueryInterface public function insert($columns, $params = [], $db = null) { // Инициализация - $this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция')); + $this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция'); $this->collection ?? $this->collection = $this->in; $data = Serializer::encode($columns); $clauses = [ - "INSERT $data IN {$this->quoteCollectionName($this->in ?? $this->collection)}", + "INSERT $data IN {$this->quoteCollectionName($this->collection)}", $this->genOptions(), ]; @@ -968,15 +1020,15 @@ class Query extends Component implements QueryInterface public function update($columns, $params = [], $db = null) { // Инициализация - $this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция')); + $this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция'); $this->for ?? $this->for = $this->in; - $this->collection ?? $this->collection = $this->in; + $this->collection ?? $this->collection = self::checkArrayAndConvert($this->for); $clauses = [ - static::genFor($this->for ?? $this->collection), - static::genIn($this->in ?? $this->collection, $this->traversals), + static::genFor($this->for), + static::genIn($this->in, $this->traversals), $this->genWhere($this->where, $params), - $this->genUpdate($this->collection, $columns), + $this->genUpdate($this->in, $columns), $this->genOptions(), ]; @@ -1020,15 +1072,15 @@ class Query extends Component implements QueryInterface public function remove($params = [], $db = null) { // Инициализация - $this->in ?? (isset($this->collection) ? $this->in = $this->collection : throw new Exception('Не найдена коллекция')); + $this->in ?? $this->in = $this->collection ?? throw new Exception('Не найдена коллекция'); $this->for ?? $this->for = $this->in; - $this->collection ?? $this->collection = $this->in; + $this->collection ?? $this->collection = self::checkArrayAndConvert($this->for); $clauses = [ - static::genFor($this->for ?? $this->collection), - static::genIn($this->in ?? $this->collection, $this->traversals), + static::genFor($this->for), + static::genIn($this->in, $this->traversals), $this->genWhere($this->where, $params), - $this->genRemove($this->in ?? $this->collection), + $this->genRemove($this->in), $this->genOptions(), ]; @@ -1085,15 +1137,63 @@ class Query extends Component implements QueryInterface * @param $columns * @return string */ - protected function genSearch(array $expression, string $type = 'START'): string + protected function genSearch(array|string $expressions): string { + if (is_string($expressions)) return $expressions; + + // Инициализация строки запроса $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) - }; + foreach ($expressions as $expression) { + // Перебор выражений + + // Инициализация оператора + $operator = isset($expression['operator']) ? ' ' . $expression['operator'] . ' ' : ''; + + // Генерация строки запроса + $query .= match (strtoupper($expression['type'])) { + 'START', 'STARTS', 'STARTS_WITH' => $operator . $this->filterStartsWith($expression['condition']), + 'START_SENSETIVE' => $operator . $this->filterStartsWith($expression['condition'], sensetive: true), + 'CONTAINS', 'LIKE' => $operator . $this->filterContains($expression['condition']), + 'LEVENSHTEIN' => $operator . $this->filterLevenshtein($expression['condition'], ...$expression['parameters']), + 'PHRASE' => $operator . $this->filterPhrase($expression['condition'], ...$expression['parameters']), + default => $operator . Serializer::encode($expression['condition']) + }; + } + + return $query; + } + + /** + * @param $collection + * @param $columns + * @return string + */ + protected function genFilter(array|string $expressions): string + { + if (isString($expressions)) return $expressions; + + // Инициализация строки запроса + $query = 'FILTER '; + + foreach ($expressions as $expression) { + // Перебор выражений + + // Инициализация оператора + $operator = isset($expression['operator']) ? ' ' . $expression['operator'] . ' ' : ''; + + // Генерация строки запроса + $query .= match (strtoupper($expression['type'])) { + 'START', 'STARTS', 'STARTS_WITH' => $operator . $this->filterStartsWith($expression['condition']), + 'START_SENSETIVE' => $operator . $this->filterStartsWith($expression['condition'], sensetive: true), + 'CONTAINS', 'LIKE' => $operator . $this->filterContains($expression['condition']), + 'LEVENSHTEIN' => $operator . $this->filterLevenshtein($expression['condition'], ...$expression['parameters']), + 'PHRASE' => $operator . $this->filterPhrase($expression['condition'], ...$expression['parameters']), + default => $operator . Serializer::encode($expression['condition']) + }; + } + + return $query; } /** @@ -1133,8 +1233,6 @@ class Query extends Component implements QueryInterface * * Генерация AQL выражения * - * @see https://www.arangodb.com/docs/3.7/aql/operations-let.html - * * @param string $direction Направление * @param mixed $vertex Коллекция вершин из которой требуется обход */ @@ -1279,7 +1377,7 @@ class Query extends Component implements QueryInterface { $this->foreach = match (true) { empty($this->foreach) => [$expression], - default => $this->foreach []= [$expression] + default => $this->foreach[] = [$expression] }; return $this; @@ -1298,6 +1396,17 @@ class Query extends Component implements QueryInterface return $this; } + + /** + * @param array $expression + */ + public function debug(bool $status = true) + { + $this->debug = $status; + + return $this; + } + /** */ public function let(string $name, mixed $value): static @@ -1375,20 +1484,38 @@ class Query extends Component implements QueryInterface return $this; } - public function filterStartsWith(array $expression): string + public function filterStartsWith(array $expression, bool $sensetive = false): string { // Генерация foreach ($expression as $key => $value) { - if (isset($return)) { - $return .= ' OR STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\")"; + if ($sensetive) { + if (isset($return)) { + $return .= ' OR STARTS_WITH(' . $this->filterLower($this->quoteCollectionName($this->collection) . ".$key") . ", " . $this->filterLower("\"$value\"") . ")"; + } else { + $return = 'STARTS_WITH(' . $this->filterLower($this->quoteCollectionName($this->collection) . ".$key") . ", " . $this->filterLower("\"$value\"") . ")"; + } } else { - $return = 'STARTS_WITH(' . $this->quoteCollectionName($this->collection) . ".$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 filterUpper(string $target): string + { + return "UPPER($target)"; + } + + public function filterLower(string $target): string + { + return "LOWER($target)"; + } + public function filterContains(array $expression): string { // Инициализация @@ -1406,6 +1533,44 @@ class Query extends Component implements QueryInterface return $return; } + public function filterPhrase(array $expression, string $analyzer = 'text_en'): string + { + // Инициализация + $return = []; + + foreach ($expression as $key => $value) { + // Перебор выражений + + if ($return) { + $return .= ' OR PHRASE(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", \"$analyzer\")"; + } else { + $return = 'PHRASE(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", \"$analyzer\")"; + } + } + + return $return; + } + + public function filterLevenshtein(array $expression, string $analyzer = 'text_en', int $distance = 3, bool $transpositions = true): string + { + // Инициализация + $return = []; + + $transpositions = $transpositions ? 'true' : 'false'; + + foreach ($expression as $key => $value) { + // Перебор выражений + + if ($return) { + $return .= ' OR ANALYZER(LEVENSHTEIN_MATCH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", $distance, $transpositions), \"$analyzer\")"; + } else { + $return = 'ANALYZER(LEVENSHTEIN_MATCH(' . $this->quoteCollectionName($this->collection) . ".$key, \"$value\", $distance, $transpositions), \"$analyzer\")"; + } + } + + 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. @@ -1603,6 +1768,7 @@ class Query extends Component implements QueryInterface $result[$column] = SORT_ASC; } } + return $result; } }