From 955bf7daaf4900e2e1e624f88e055f44a335a7f2 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 30 Sep 2013 18:47:31 +0200 Subject: [PATCH] basic CRUD for elastic search WIP --- framework/yii/elasticsearch/ActiveQuery.php | 109 +++--- framework/yii/elasticsearch/ActiveRecord.php | 123 ++++--- framework/yii/elasticsearch/Connection.php | 27 +- framework/yii/elasticsearch/Query.php | 18 + framework/yii/elasticsearch/QueryBuilder.php | 379 +++++++++++++++++++++ .../framework/elasticsearch/ActiveRecordTest.php | 283 +++++++-------- 6 files changed, 662 insertions(+), 277 deletions(-) create mode 100644 framework/yii/elasticsearch/Query.php create mode 100644 framework/yii/elasticsearch/QueryBuilder.php diff --git a/framework/yii/elasticsearch/ActiveQuery.php b/framework/yii/elasticsearch/ActiveQuery.php index 58303c7..ee7f872 100644 --- a/framework/yii/elasticsearch/ActiveQuery.php +++ b/framework/yii/elasticsearch/ActiveQuery.php @@ -6,6 +6,8 @@ */ namespace yii\elasticsearch; +use Guzzle\Http\Client; +use Guzzle\Http\Exception\MultiTransferException; use yii\base\NotSupportedException; use yii\db\Exception; use yii\helpers\Json; @@ -78,14 +80,20 @@ class ActiveQuery extends \yii\base\Component */ public $asArray; /** + * @var array the columns being selected. For example, `array('id', 'name')`. + * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. + * @see select() + */ + public $select; + /** * @var array the query condition. * @see where() */ public $where; /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. TODO infinite possible in ES? */ - public $limit; + public $limit = 10; /** * @var integer zero-based offset from where the records are to be returned. * If not set, it means starting from the beginning. @@ -128,12 +136,10 @@ class ActiveQuery extends \yii\base\Component // TODO add support for orderBy $data = $this->executeScript('All'); $rows = array(); + print_r($data); foreach($data as $dataRow) { - $row = array(); - $c = count($dataRow); - for($i = 0; $i < $c; ) { - $row[$dataRow[$i++]] = $dataRow[$i++]; - } + $row = $dataRow['_source']; + $row['id'] = $dataRow['_id']; $rows[] = $row; } if (!empty($rows)) { @@ -157,14 +163,11 @@ class ActiveQuery extends \yii\base\Component { // TODO add support for orderBy $data = $this->executeScript('One'); - if ($data === array()) { + if (!isset($data['_source'])) { return null; } - $row = array(); - $c = count($data); - for($i = 0; $i < $c; ) { - $row[$data[$i++]] = $data[$i++]; - } + $row = $data['_source']; + $row['id'] = $data['_id']; if ($this->asArray) { $model = $row; } else { @@ -284,12 +287,13 @@ class ActiveQuery extends \yii\base\Component { if (($data = $this->findByPk($type)) === false) { $modelClass = $this->modelClass; - /** @var Connection $db */ - $db = $modelClass::getDb(); + $http = $modelClass::getDb()->http(); - $method = 'build' . $type; - $script = $db->getLuaScriptBuilder()->$method($this, $columnName); - return $db->executeCommand('EVAL', array($script, 0)); + $url = '/' . $modelClass::indexName() . '/' . $modelClass::indexType() . '/_search'; + $query = $modelClass::getDb()->getQueryBuilder()->build($this); + $response = $http->post($url, null, Json::encode($query))->send(); + $data = Json::decode($response->getBody(true)); + return $data['hits']['hits']; } return $data; } @@ -301,46 +305,47 @@ class ActiveQuery extends \yii\base\Component { $modelClass = $this->modelClass; if (is_array($this->where) && !isset($this->where[0]) && $modelClass::isPrimaryKey(array_keys($this->where))) { - /** @var Connection $db */ - $db = $modelClass::getDb(); + /** @var Client $http */ + $http = $modelClass::getDb()->http(); $pks = (array) reset($this->where); - $start = $this->offset === null ? 0 : $this->offset; - $i = 0; - $data = array(); - $url = '/' . $modelClass::indexName() . '/' . $modelClass::indexType() . '/'; + $query = array('docs' => array()); foreach($pks as $pk) { - if (++$i > $start && ($this->limit === null || $i <= $start + $this->limit)) { - $request = $db->http()->get($url . $pk); - $response = $request->send(); - if ($response->getStatusCode() == 404) { - // ignore? - } else { - $data[] = Json::decode($response->getBody(true)); - if ($type === 'One' && $this->orderBy === null) { - break; - } - } + $doc = array('_id' => $pk); + if (!empty($this->select)) { + $doc['fields'] = $this->select; } + $query['docs'][] = $doc; } + $url = '/' . $modelClass::indexName() . '/' . $modelClass::indexType() . '/_mget'; + $response = $http->post($url, null, Json::encode($query))->send(); + $data = Json::decode($response->getBody(true)); + + $start = $this->offset === null ? 0 : $this->offset; + $data = array_slice($data['docs'], $start, $this->limit); + // TODO support orderBy switch($type) { case 'All': return $data; case 'One': - return reset($data); + return empty($data) ? null : reset($data); case 'Column': - // TODO support indexBy $column = array(); - foreach($data as $dataRow) { - $row = array(); - $c = count($dataRow); - for($i = 0; $i < $c; ) { - $row[$dataRow[$i++]] = $dataRow[$i++]; + foreach($data as $row) { + $row['_source']['id'] = $row['_id']; + if ($this->indexBy === null) { + $column[] = $row['_source'][$columnName]; + } else { + if (is_string($this->indexBy)) { + $key = $row['_source'][$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row['_source']); + } + $models[$key] = $row; } - $column[] = $row[$columnName]; } return $column; case 'Count': @@ -414,6 +419,24 @@ class ActiveQuery extends \yii\base\Component } /** + * Sets the SELECT part of the query. + * @param string|array $columns the columns to be selected. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return Query the query object itself + */ + public function select($columns) + { + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->select = $columns; + return $this; + } + + /** * Sets the ORDER BY part of the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array diff --git a/framework/yii/elasticsearch/ActiveRecord.php b/framework/yii/elasticsearch/ActiveRecord.php index 723c162..470ba9b 100644 --- a/framework/yii/elasticsearch/ActiveRecord.php +++ b/framework/yii/elasticsearch/ActiveRecord.php @@ -64,38 +64,42 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord */ public static function updateAll($attributes, $condition = null, $params = array()) { + // TODO add support for further options as described in http://www.elasticsearch.org/guide/reference/api/bulk/ if (empty($attributes)) { return 0; } - $db = static::getDb(); - $n=0; - foreach(static::fetchPks($condition) as $pk) { - $newPk = $pk; - $pk = static::buildKey($pk); - $key = static::tableName() . ':a:' . $pk; - // save attributes - $args = array($key); - foreach($attributes as $attribute => $value) { - if (isset($newPk[$attribute])) { - $newPk[$attribute] = $value; - } - $args[] = $attribute; - $args[] = $value; + if (count($condition) != 1 || !isset($condition[reset(static::primaryKey())])) { + throw new NotSupportedException('UpdateAll is only supported by primary key in elasticsearch.'); + } + if (isset($attributes[reset(static::primaryKey())])) { + throw new NotSupportedException('Updating the primary key is currently not supported by elasticsearch.'); + } + $query = ''; + foreach((array) reset($condition) as $pk) { + if (is_array($pk)) { + $pk = reset($pk); } - $newPk = static::buildKey($newPk); - $newKey = static::tableName() . ':a:' . $newPk; - // rename index if pk changed - if ($newPk != $pk) { - $db->executeCommand('MULTI'); - $db->executeCommand('HMSET', $args); - $db->executeCommand('LINSERT', array(static::tableName(), 'AFTER', $pk, $newPk)); - $db->executeCommand('LREM', array(static::tableName(), 0, $pk)); - $db->executeCommand('RENAME', array($key, $newKey)); - $db->executeCommand('EXEC'); - } else { - $db->executeCommand('HMSET', $args); + $action = Json::encode(array( + "update" => array( + "_id" => $pk, + "_type" => static::indexType(), + "_index" => static::indexName(), + ), + )); + $data = Json::encode(array( + "doc" => $attributes + )); + $query .= $action . "\n" . $data . "\n"; + // TODO implement pk change + } + $url = '/' . static::indexName() . '/' . static::indexType() . '/_bulk'; + $response = static::getDb()->http()->post($url, array(), $query)->send(); + $body = Json::decode($response->getBody(true)); + $n=0; + foreach($body['items'] as $item) { + if ($item['update']['ok']) { + $n++; } - $n++; } return $n; } @@ -117,19 +121,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord */ public static function updateAllCounters($counters, $condition = null, $params = array()) { - if (empty($counters)) { - return 0; - } - $db = static::getDb(); - $n=0; - foreach(static::fetchPks($condition) as $pk) { - $key = static::tableName() . ':a:' . static::buildKey($pk); - foreach($counters as $attribute => $value) { - $db->executeCommand('HINCRBY', array($key, $attribute, $value)); - } - $n++; - } - return $n; + throw new NotSupportedException('Update Counters is not supported by elasticsearch.'); } /** @@ -149,23 +141,36 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord */ public static function deleteAll($condition = null, $params = array()) { - $db = static::getDb(); - $attributeKeys = array(); - $pks = static::fetchPks($condition); - $db->executeCommand('MULTI'); - foreach($pks as $pk) { - $pk = static::buildKey($pk); - $db->executeCommand('LREM', array(static::tableName(), 0, $pk)); - $attributeKeys[] = static::tableName() . ':a:' . $pk; + // TODO use delete By Query feature + // http://www.elasticsearch.org/guide/reference/api/delete-by-query/ + if (count($condition) != 1 || !isset($condition[reset(static::primaryKey())])) { + throw new NotSupportedException('DeleteAll is only supported by primary key in elasticsearch.'); } - if (empty($attributeKeys)) { - $db->executeCommand('EXEC'); - return 0; + $query = ''; + foreach((array) reset($condition) as $pk) { + if (is_array($pk)) { + $pk = reset($pk); + } + $query .= Json::encode(array( + "delete" => array( + "_id" => $pk, + "_type" => static::indexType(), + "_index" => static::indexName(), + ), + )) . "\n"; + } + $url = '/' . static::indexName() . '/' . static::indexType() . '/_bulk'; + $response = static::getDb()->http()->post($url, array(), $query)->send(); + $body = Json::decode($response->getBody(true)); + $n=0; + foreach($body['items'] as $item) { + if ($item['delete']['ok']) { + $n++; + } } - $db->executeCommand('DEL', $attributeKeys); - $result = $db->executeCommand('EXEC'); - return end($result); + return $n; } + /** * Creates an [[ActiveQuery]] instance. * This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query. @@ -189,16 +194,6 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord return static::getTableSchema()->name; } - /** - * This method is ment to be overridden in redis ActiveRecord subclasses to return a [[RecordSchema]] instance. - * @return RecordSchema - * @throws \yii\base\InvalidConfigException - */ - public static function getRecordSchema() - { - throw new InvalidConfigException(__CLASS__.'::getRecordSchema() needs to be overridden in subclasses and return a RecordSchema.'); - } - public static function primaryKey() { return array('id'); diff --git a/framework/yii/elasticsearch/Connection.php b/framework/yii/elasticsearch/Connection.php index 9f53062..f970eae 100644 --- a/framework/yii/elasticsearch/Connection.php +++ b/framework/yii/elasticsearch/Connection.php @@ -160,27 +160,18 @@ class Connection extends Component // TODO HTTP request to localhost:9200/ } - public function http() + public function getQueryBuilder() { - return new \Guzzle\Http\Client('http://localhost:9200/'); - } - - public function get($url) - { - $c = $this->initCurl($url); - - $result = curl_exec($c); - curl_close($c); + return new QueryBuilder($this); } - private function initCurl($url) + /** + * @return \Guzzle\Http\Client + */ + public function http() { - $c = curl_init('http://localhost:9200/' . $url); - $fp = fopen("example_homepage.txt", "w"); - - curl_setopt($c, CURLOPT_FOLLOWLOCATION, false); - - curl_setopt($c, CURLOPT_FILE, $fp); - curl_setopt($c, CURLOPT_HEADER, 0); + $guzzle = new \Guzzle\Http\Client('http://localhost:9200/'); + //$guzzle->setDefaultOption() + return $guzzle; } } \ No newline at end of file diff --git a/framework/yii/elasticsearch/Query.php b/framework/yii/elasticsearch/Query.php new file mode 100644 index 0000000..99f7c07 --- /dev/null +++ b/framework/yii/elasticsearch/Query.php @@ -0,0 +1,18 @@ + + * @since 2.0 + */ +class QueryBuilder extends \yii\base\Object +{ + /** + * @var Connection the database connection. + */ + public $db; + + /** + * Constructor. + * @param Connection $connection the database connection. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($connection, $config = array()) + { + $this->db = $connection; + parent::__construct($config); + } + + /** + * Generates a SELECT SQL statement from a [[Query]] object. + * @param Query $query the [[Query]] object from which the SQL statement will be generated + * @return array the generated SQL statement (the first array element) and the corresponding + * parameters to be bound to the SQL statement (the second array element). + */ + public function build($query) + { + $searchQuery = array(); + $this->buildSelect($searchQuery, $query->select); +// $this->buildFrom(&$searchQuery, $query->from); + $this->buildCondition($searchQuery, $query->where); + $this->buildOrderBy($searchQuery, $query->orderBy); + $this->buildLimit($searchQuery, $query->limit, $query->offset); + + return $searchQuery; + } + + /** + * Converts an abstract column type into a physical column type. + * The conversion is done using the type map specified in [[typeMap]]. + * The following abstract column types are supported (using MySQL as an example to explain the corresponding + * physical types): + * + * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY" + * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY" + * - `string`: string type, will be converted into "varchar(255)" + * - `text`: a long string type, will be converted into "text" + * - `smallint`: a small integer type, will be converted into "smallint(6)" + * - `integer`: integer type, will be converted into "int(11)" + * - `bigint`: a big integer type, will be converted into "bigint(20)" + * - `boolean`: boolean type, will be converted into "tinyint(1)" + * - `float``: float number type, will be converted into "float" + * - `decimal`: decimal number type, will be converted into "decimal" + * - `datetime`: datetime type, will be converted into "datetime" + * - `timestamp`: timestamp type, will be converted into "timestamp" + * - `time`: time type, will be converted into "time" + * - `date`: date type, will be converted into "date" + * - `money`: money type, will be converted into "decimal(19,4)" + * - `binary`: binary data type, will be converted into "blob" + * + * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only + * the first part will be converted, and the rest of the parts will be appended to the converted result. + * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. + * + * For some of the abstract types you can also specify a length or precision constraint + * by prepending it in round brackets directly to the type. + * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. + * If the underlying DBMS does not support these kind of constraints for a type it will + * be ignored. + * + * If a type cannot be found in [[typeMap]], it will be returned without any change. + * @param string $type abstract column type + * @return string physical column type. + */ + public function getColumnType($type) + { + if (isset($this->typeMap[$type])) { + return $this->typeMap[$type]; + } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3]; + } + } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); + } + } + return $type; + } + + /** + * @param array $columns + * @param boolean $distinct + * @param string $selectOption + * @return string the SELECT clause built from [[query]]. + */ + public function buildSelect(&$query, $columns) + { + if (empty($columns)) { + return; + } + foreach ($columns as $i => $column) { + if (is_object($column)) { + $columns[$i] = (string)$column; + } + } + $query['fields'] = $columns; + } + + /** + * @param array $columns + * @return string the ORDER BY clause built from [[query]]. + */ + public function buildOrderBy(&$query, $columns) + { + if (empty($columns)) { + return; + } + $orders = array(); + foreach ($columns as $name => $direction) { + // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ + if (is_array($direction)) { + $orders[] = array($name => $direction); + } elseif (is_string($direction)) { + $orders[] = $direction; + } else { + $orders[] = array($name => ($direction === Query::SORT_DESC ? 'desc' : 'asc')); + } + } + $query['sort'] = $orders; + } + + /** + * @param integer $limit + * @param integer $offset + * @return string the LIMIT and OFFSET clauses built from [[query]]. + */ + public function buildLimit(&$query, $limit, $offset) + { + if ($limit !== null && $limit >= 0) { + $query['size'] = $limit; + } + if ($offset > 0) { + $query['from'] = (int) $offset; + } + } + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition(&$query, $condition) + { + static $builders = array( + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', + ); + + if (empty($condition)) { + return; + } + if (!is_array($condition)) { + throw new NotSupportedException('String conditions are not supported by elasticsearch.'); + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + $this->$method($query, $operator, $condition); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + $this->buildHashCondition($query, $condition); + } + } + + private function buildHashCondition(&$query, $condition) + { + $query['query']['term'] = $condition; + return; // TODO more + $parts = array(); + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('IN', array($column, $value), $params); + } else { + if ($value === null) { + $parts[] = "$column IS NULL"; // TODO null + } elseif ($value instanceof Expression) { + $parts[] = "$column=" . $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$column=$phName"; + $params[$phName] = $value; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + private function buildAndCondition($operator, $operands, &$params) + { + $parts = array(); + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + private function buildBetweenCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $phName1 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName2] = $value2; + + return "$column $operator $phName1 AND $phName2"; + } + + private function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if (empty($values) || $column === array()) { + return $operator === 'IN' ? '0=1' : ''; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } elseif (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'; + } elseif ($value instanceof Expression) { + $values[$i] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = $phName; + } + } + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + return "$column$operator{$values[0]}"; + } + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + $vss = array(); + foreach ($values as $value) { + $vs = array(); + 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->db->quoteColumnName($column); + } + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if (empty($values)) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = array(); + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } +} diff --git a/tests/unit/framework/elasticsearch/ActiveRecordTest.php b/tests/unit/framework/elasticsearch/ActiveRecordTest.php index 19dec38..da65471 100644 --- a/tests/unit/framework/elasticsearch/ActiveRecordTest.php +++ b/tests/unit/framework/elasticsearch/ActiveRecordTest.php @@ -3,6 +3,7 @@ namespace yiiunit\framework\elasticsearch; use yii\db\Query; +use yii\elasticsearch\Connection; use yii\redis\ActiveQuery; use yiiunit\data\ar\elasticsearch\ActiveRecord; use yiiunit\data\ar\elasticsearch\Customer; @@ -15,7 +16,12 @@ class ActiveRecordTest extends ElasticSearchTestCase public function setUp() { parent::setUp(); - ActiveRecord::$db = $this->getConnection(); + + /** @var Connection $db */ + $db = ActiveRecord::$db = $this->getConnection(); + + // delete all indexes + $db->http()->delete('_all')->send(); $customer = new Customer(); $customer->setAttributes(array('id' => 1, 'email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1), false); @@ -236,122 +242,122 @@ class ActiveRecordTest extends ElasticSearchTestCase $this->assertFalse(Customer::find()->where(array('id' => 5))->exists()); } - public function testFindLazy() - { - /** @var $customer Customer */ - $customer = Customer::find(2); - $orders = $customer->orders; - $this->assertEquals(2, count($orders)); - - $orders = $customer->getOrders()->where(array('id' => 3))->all(); - $this->assertEquals(1, count($orders)); - $this->assertEquals(3, $orders[0]->id); - } - - public function testFindEager() - { - $customers = Customer::find()->with('orders')->all(); - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, count($customers[0]->orders)); - $this->assertEquals(2, count($customers[1]->orders)); - } - - public function testFindLazyVia() - { - /** @var $order Order */ - $order = Order::find(1); - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - - $order = Order::find(1); - $order->id = 100; - $this->assertEquals(array(), $order->items); - } - - public function testFindEagerViaRelation() - { - $orders = Order::find()->with('items')->all(); - $this->assertEquals(3, count($orders)); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testFindNestedRelation() - { - $customers = Customer::find()->with('orders', 'orders.items')->all(); - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, count($customers[0]->orders)); - $this->assertEquals(2, count($customers[1]->orders)); - $this->assertEquals(0, count($customers[2]->orders)); - $this->assertEquals(2, count($customers[0]->orders[0]->items)); - $this->assertEquals(3, count($customers[1]->orders[0]->items)); - $this->assertEquals(1, count($customers[1]->orders[1]->items)); - } - - public function testLink() - { - $customer = Customer::find(2); - $this->assertEquals(2, count($customer->orders)); - - // has many - $order = new Order; - $order->total = 100; - $this->assertTrue($order->isNewRecord); - $customer->link('orders', $order); - $this->assertEquals(3, count($customer->orders)); - $this->assertFalse($order->isNewRecord); - $this->assertEquals(3, count($customer->getOrders()->all())); - $this->assertEquals(2, $order->customer_id); - - // belongs to - $order = new Order; - $order->total = 100; - $this->assertTrue($order->isNewRecord); - $customer = Customer::find(1); - $this->assertNull($order->customer); - $order->link('customer', $customer); - $this->assertFalse($order->isNewRecord); - $this->assertEquals(1, $order->customer_id); - $this->assertEquals(1, $order->customer->id); - - // via model - $order = Order::find(1); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(2, count($order->orderItems)); - $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); - $this->assertNull($orderItem); - $item = Item::find(3); - $order->link('items', $item, array('quantity' => 10, 'subtotal' => 100)); - $this->assertEquals(3, count($order->items)); - $this->assertEquals(3, count($order->orderItems)); - $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); - $this->assertTrue($orderItem instanceof OrderItem); - $this->assertEquals(10, $orderItem->quantity); - $this->assertEquals(100, $orderItem->subtotal); - } - - public function testUnlink() - { - // has many - $customer = Customer::find(2); - $this->assertEquals(2, count($customer->orders)); - $customer->unlink('orders', $customer->orders[1], true); - $this->assertEquals(1, count($customer->orders)); - $this->assertNull(Order::find(3)); - - // via model - $order = Order::find(2); - $this->assertEquals(3, count($order->items)); - $this->assertEquals(3, count($order->orderItems)); - $order->unlink('items', $order->items[2], true); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(2, count($order->orderItems)); - } +// public function testFindLazy() +// { +// /** @var $customer Customer */ +// $customer = Customer::find(2); +// $orders = $customer->orders; +// $this->assertEquals(2, count($orders)); +// +// $orders = $customer->getOrders()->where(array('id' => 3))->all(); +// $this->assertEquals(1, count($orders)); +// $this->assertEquals(3, $orders[0]->id); +// } +// +// public function testFindEager() +// { +// $customers = Customer::find()->with('orders')->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertEquals(1, count($customers[0]->orders)); +// $this->assertEquals(2, count($customers[1]->orders)); +// } +// +// public function testFindLazyVia() +// { +// /** @var $order Order */ +// $order = Order::find(1); +// $this->assertEquals(1, $order->id); +// $this->assertEquals(2, count($order->items)); +// $this->assertEquals(1, $order->items[0]->id); +// $this->assertEquals(2, $order->items[1]->id); +// +// $order = Order::find(1); +// $order->id = 100; +// $this->assertEquals(array(), $order->items); +// } +// +// public function testFindEagerViaRelation() +// { +// $orders = Order::find()->with('items')->all(); +// $this->assertEquals(3, count($orders)); +// $order = $orders[0]; +// $this->assertEquals(1, $order->id); +// $this->assertEquals(2, count($order->items)); +// $this->assertEquals(1, $order->items[0]->id); +// $this->assertEquals(2, $order->items[1]->id); +// } +// +// public function testFindNestedRelation() +// { +// $customers = Customer::find()->with('orders', 'orders.items')->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertEquals(1, count($customers[0]->orders)); +// $this->assertEquals(2, count($customers[1]->orders)); +// $this->assertEquals(0, count($customers[2]->orders)); +// $this->assertEquals(2, count($customers[0]->orders[0]->items)); +// $this->assertEquals(3, count($customers[1]->orders[0]->items)); +// $this->assertEquals(1, count($customers[1]->orders[1]->items)); +// } +// +// public function testLink() +// { +// $customer = Customer::find(2); +// $this->assertEquals(2, count($customer->orders)); +// +// // has many +// $order = new Order; +// $order->total = 100; +// $this->assertTrue($order->isNewRecord); +// $customer->link('orders', $order); +// $this->assertEquals(3, count($customer->orders)); +// $this->assertFalse($order->isNewRecord); +// $this->assertEquals(3, count($customer->getOrders()->all())); +// $this->assertEquals(2, $order->customer_id); +// +// // belongs to +// $order = new Order; +// $order->total = 100; +// $this->assertTrue($order->isNewRecord); +// $customer = Customer::find(1); +// $this->assertNull($order->customer); +// $order->link('customer', $customer); +// $this->assertFalse($order->isNewRecord); +// $this->assertEquals(1, $order->customer_id); +// $this->assertEquals(1, $order->customer->id); +// +// // via model +// $order = Order::find(1); +// $this->assertEquals(2, count($order->items)); +// $this->assertEquals(2, count($order->orderItems)); +// $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); +// $this->assertNull($orderItem); +// $item = Item::find(3); +// $order->link('items', $item, array('quantity' => 10, 'subtotal' => 100)); +// $this->assertEquals(3, count($order->items)); +// $this->assertEquals(3, count($order->orderItems)); +// $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); +// $this->assertTrue($orderItem instanceof OrderItem); +// $this->assertEquals(10, $orderItem->quantity); +// $this->assertEquals(100, $orderItem->subtotal); +// } +// +// public function testUnlink() +// { +// // has many +// $customer = Customer::find(2); +// $this->assertEquals(2, count($customer->orders)); +// $customer->unlink('orders', $customer->orders[1], true); +// $this->assertEquals(1, count($customer->orders)); +// $this->assertNull(Order::find(3)); +// +// // via model +// $order = Order::find(2); +// $this->assertEquals(3, count($order->items)); +// $this->assertEquals(3, count($order->orderItems)); +// $order->unlink('items', $order->items[2], true); +// $this->assertEquals(2, count($order->items)); +// $this->assertEquals(2, count($order->orderItems)); +// } public function testInsertNoPk() { @@ -410,46 +416,19 @@ class ActiveRecordTest extends ElasticSearchTestCase $this->assertEquals('temp', $customer->name); } - public function testUpdateCounters() - { - // updateCounters - $pk = array('order_id' => 2, 'item_id' => 4); - $orderItem = OrderItem::find($pk); - $this->assertEquals(1, $orderItem->quantity); - $ret = $orderItem->updateCounters(array('quantity' => -1)); - $this->assertTrue($ret); - $this->assertEquals(0, $orderItem->quantity); - $orderItem = OrderItem::find($pk); - $this->assertEquals(0, $orderItem->quantity); - - // updateAllCounters - $pk = array('order_id' => 1, 'item_id' => 2); - $orderItem = OrderItem::find($pk); - $this->assertEquals(2, $orderItem->quantity); - $ret = OrderItem::updateAllCounters(array( - 'quantity' => 3, - 'subtotal' => -10, - ), $pk); - $this->assertEquals(1, $ret); - $orderItem = OrderItem::find($pk); - $this->assertEquals(5, $orderItem->quantity); - $this->assertEquals(30, $orderItem->subtotal); - } - public function testUpdatePk() { - // updateCounters - $pk = array('order_id' => 2, 'item_id' => 4); - $orderItem = OrderItem::find($pk); - $this->assertEquals(2, $orderItem->order_id); - $this->assertEquals(4, $orderItem->item_id); - - $orderItem->order_id = 2; - $orderItem->item_id = 10; + $this->setExpectedException('yii\base\NotSupportedException'); + + $pk = array('id' => 2); + $orderItem = Order::find($pk); + $this->assertEquals(2, $orderItem->id); + + $orderItem->id = 13; $orderItem->save(); $this->assertNull(OrderItem::find($pk)); - $this->assertNotNull(OrderItem::find(array('order_id' => 2, 'item_id' => 10))); + $this->assertNotNull(OrderItem::find(array('id' => 13))); } public function testDelete()