Browse Source

Merge pull request #1259 from klimov-paul/sphinx

Sphinx fulltext search engine integration
tags/2.0.0-beta
Qiang Xue 11 years ago
parent
commit
13f6a11277
  1. 2
      .travis.yml
  2. 204
      extensions/sphinx/ActiveQuery.php
  3. 1392
      extensions/sphinx/ActiveRecord.php
  4. 22
      extensions/sphinx/ActiveRelation.php
  5. 81
      extensions/sphinx/ColumnSchema.php
  6. 323
      extensions/sphinx/Command.php
  7. 129
      extensions/sphinx/Connection.php
  8. 63
      extensions/sphinx/IndexSchema.php
  9. 32
      extensions/sphinx/LICENSE.md
  10. 701
      extensions/sphinx/Query.php
  11. 904
      extensions/sphinx/QueryBuilder.php
  12. 118
      extensions/sphinx/README.md
  13. 489
      extensions/sphinx/Schema.php
  14. 29
      extensions/sphinx/composer.json
  15. 13
      tests/unit/data/config.php
  16. 16
      tests/unit/data/sphinx/ar/ActiveRecord.php
  17. 25
      tests/unit/data/sphinx/ar/ArticleDb.php
  18. 35
      tests/unit/data/sphinx/ar/ArticleIndex.php
  19. 13
      tests/unit/data/sphinx/ar/ItemDb.php
  20. 11
      tests/unit/data/sphinx/ar/ItemIndex.php
  21. 11
      tests/unit/data/sphinx/ar/RuntimeIndex.php
  22. 12
      tests/unit/data/sphinx/ar/TagDb.php
  23. 59
      tests/unit/data/sphinx/source.sql
  24. 125
      tests/unit/data/sphinx/sphinx.conf
  25. 66
      tests/unit/extensions/sphinx/ActiveDataProviderTest.php
  26. 238
      tests/unit/extensions/sphinx/ActiveRecordTest.php
  27. 44
      tests/unit/extensions/sphinx/ActiveRelationTest.php
  28. 55
      tests/unit/extensions/sphinx/ColumnSchemaTest.php
  29. 409
      tests/unit/extensions/sphinx/CommandTest.php
  30. 42
      tests/unit/extensions/sphinx/ConnectionTest.php
  31. 74
      tests/unit/extensions/sphinx/ExternalActiveRelationTest.php
  32. 187
      tests/unit/extensions/sphinx/QueryTest.php
  33. 84
      tests/unit/extensions/sphinx/SchemaTest.php
  34. 154
      tests/unit/extensions/sphinx/SphinxTestCase.php

2
.travis.yml

@ -18,7 +18,7 @@ before_script:
- tests/unit/data/travis/cubrid-setup.sh
script:
- phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor
- phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor,sphinx
after_script:
- php vendor/bin/coveralls

204
extensions/sphinx/ActiveQuery.php

@ -0,0 +1,204 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\InvalidCallException;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveQueryTrait;
/**
* ActiveQuery represents a Sphinx query associated with an Active Record class.
*
* ActiveQuery instances are usually created by [[ActiveRecord::find()]] and [[ActiveRecord::findBySql()]].
*
* Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
* [[orderBy()]] to customize the query options.
*
* ActiveQuery also provides the following additional query options:
*
* - [[with()]]: list of relations that this query should be performed with.
* - [[indexBy()]]: the name of the column by which the query result should be indexed.
* - [[asArray()]]: whether to return each record as an array.
*
* These options can be configured using methods of the same name. For example:
*
* ~~~
* $articles = Article::find()->with('source')->asArray()->all();
* ~~~
*
* ActiveQuery allows to build the snippets using sources provided by ActiveRecord.
* You can use [[snippetByModel()]] method to enable this.
* For example:
*
* ~~~
* class Article extends ActiveRecord
* {
* public function getSource()
* {
* return $this->hasOne('db', ArticleDb::className(), ['id' => 'id']);
* }
*
* public function getSnippetSource()
* {
* return $this->source->content;
* }
*
* ...
* }
*
* $articles = Article::find()->with('source')->snippetByModel()->all();
* ~~~
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ActiveQuery extends Query implements ActiveQueryInterface
{
use ActiveQueryTrait;
/**
* @var string the SQL statement to be executed for retrieving AR records.
* This is set by [[ActiveRecord::findBySql()]].
*/
public $sql;
/**
* Sets the [[snippetCallback]] to [[fetchSnippetSourceFromModels()]], which allows to
* fetch the snippet source strings from the Active Record models, using method
* [[ActiveRecord::getSnippetSource()]].
* For example:
*
* ~~~
* class Article extends ActiveRecord
* {
* public function getSnippetSource()
* {
* return file_get_contents('/path/to/source/files/' . $this->id . '.txt');;
* }
* }
*
* $articles = Article::find()->snippetByModel()->all();
* ~~~
*
* Warning: this option should NOT be used with [[asArray]] at the same time!
* @return static the query object itself
*/
public function snippetByModel()
{
$this->snippetCallback([$this, 'fetchSnippetSourceFromModels']);
return $this;
}
/**
* Executes query and returns all results as an array.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @return array the query results. If the query results in nothing, an empty array will be returned.
*/
public function all($db = null)
{
$command = $this->createCommand($db);
$rows = $command->queryAll();
if (!empty($rows)) {
$models = $this->createModels($rows);
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
$models = $this->fillUpSnippets($models);
return $models;
} else {
return [];
}
}
/**
* Executes query and returns a single row of result.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
* the query result may be either an array or an ActiveRecord object. Null will be returned
* if the query results in nothing.
*/
public function one($db = null)
{
$command = $this->createCommand($db);
$row = $command->queryOne();
if ($row !== false) {
if ($this->asArray) {
$model = $row;
} else {
/** @var $class ActiveRecord */
$class = $this->modelClass;
$model = $class::create($row);
}
if (!empty($this->with)) {
$models = [$model];
$this->findWith($this->with, $models);
$model = $models[0];
}
list ($model) = $this->fillUpSnippets([$model]);
return $model;
} else {
return null;
}
}
/**
* Creates a DB command that can be used to execute this query.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @return Command the created DB command instance.
*/
public function createCommand($db = null)
{
/** @var $modelClass ActiveRecord */
$modelClass = $this->modelClass;
$this->setConnection($db);
$db = $this->getConnection();
$params = $this->params;
if ($this->sql === null) {
if ($this->from === null) {
$tableName = $modelClass::indexName();
if ($this->select === null && !empty($this->join)) {
$this->select = ["$tableName.*"];
}
$this->from = [$tableName];
}
list ($this->sql, $params) = $db->getQueryBuilder()->build($this);
}
return $db->createCommand($this->sql, $params);
}
/**
* @inheritdoc
*/
protected function defaultConnection()
{
$modelClass = $this->modelClass;
return $modelClass::getDb();
}
/**
* Fetches the source for the snippets using [[ActiveRecord::getSnippetSource()]] method.
* @param ActiveRecord[] $models raw query result rows.
* @throws \yii\base\InvalidCallException if [[asArray]] enabled.
* @return array snippet source strings
*/
protected function fetchSnippetSourceFromModels($models)
{
if ($this->asArray) {
throw new InvalidCallException('"' . __METHOD__ . '" unable to determine snippet source from plain array. Either disable "asArray" option or use regular "snippetCallback"');
}
$result = [];
foreach ($models as $model) {
$result[] = $model->getSnippetSource();
}
return $result;
}
}

1392
extensions/sphinx/ActiveRecord.php

File diff suppressed because it is too large Load Diff

22
extensions/sphinx/ActiveRelation.php

@ -0,0 +1,22 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\db\ActiveRelationInterface;
use yii\db\ActiveRelationTrait;
/**
* ActiveRelation represents a relation to Sphinx Active Record class.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
{
use ActiveRelationTrait;
}

81
extensions/sphinx/ColumnSchema.php

@ -0,0 +1,81 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\Object;
use yii\db\Expression;
/**
* ColumnSchema class describes the metadata of a column in a Sphinx index.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class ColumnSchema extends Object
{
/**
* @var string name of this column (without quotes).
*/
public $name;
/**
* @var string abstract type of this column. Possible abstract types include:
* string, text, boolean, smallint, integer, bigint, float, decimal, datetime,
* timestamp, time, date, binary, and money.
*/
public $type;
/**
* @var string the PHP type of this column. Possible PHP types include:
* string, boolean, integer, double.
*/
public $phpType;
/**
* @var string the DB type of this column. Possible DB types vary according to the type of DBMS.
*/
public $dbType;
/**
* @var boolean whether this column is a primary key
*/
public $isPrimaryKey;
/**
* @var boolean whether this column is an attribute
*/
public $isAttribute;
/**
* @var boolean whether this column is a indexed field
*/
public $isField;
/**
* @var boolean whether this column is a multi value attribute (MVA)
*/
public $isMva;
/**
* Converts the input value according to [[phpType]].
* If the value is null or an [[Expression]], it will not be converted.
* @param mixed $value input value
* @return mixed converted value
*/
public function typecast($value)
{
if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) {
return $value;
}
if ($value === '' && $this->type !== Schema::TYPE_STRING) {
return null;
}
switch ($this->phpType) {
case 'string':
return (string)$value;
case 'integer':
return (integer)$value;
case 'boolean':
return (boolean)$value;
}
return $value;
}
}

323
extensions/sphinx/Command.php

@ -0,0 +1,323 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use Yii;
use yii\base\NotSupportedException;
/**
* Command represents a SQL statement to be executed against a Sphinx.
*
* A command object is usually created by calling [[Connection::createCommand()]].
* The SQL statement it represents can be set via the [[sql]] property.
*
* To execute a non-query SQL (such as INSERT, REPLACE, DELETE, UPDATE), call [[execute()]].
* To execute a SQL statement that returns result data set (such as SELECT, CALL SNIPPETS, CALL KEYWORDS),
* use [[queryAll()]], [[queryOne()]], [[queryColumn()]], [[queryScalar()]], or [[query()]].
* For example,
*
* ~~~
* $articles = $connection->createCommand("SELECT * FROM `idx_article` WHERE MATCH('programming')")->queryAll();
* ~~~
*
* Command supports SQL statement preparation and parameter binding just as [[\yii\db\Command]] does.
*
* Command also supports building SQL statements by providing methods such as [[insert()]],
* [[update()]], etc. For example,
*
* ~~~
* $connection->createCommand()->update('idx_article', [
* 'genre_id' => 15,
* 'author_id' => 157,
* ])->execute();
* ~~~
*
* To build SELECT SQL statements, please use [[Query]] and [[QueryBuilder]] instead.
*
* @property \yii\sphinx\Connection $db the Sphinx connection that this command is associated with.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Command extends \yii\db\Command
{
/**
* Creates a batch INSERT command.
* For example,
*
* ~~~
* $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [
* ['Tom', 30],
* ['Jane', 20],
* ['Linda', 25],
* ])->execute();
* ~~~
*
* Note that the values in each row must match the corresponding column names.
*
* @param string $index the index that new rows will be inserted into.
* @param array $columns the column names
* @param array $rows the rows to be batch inserted into the index
* @return static the command object itself
*/
public function batchInsert($index, $columns, $rows)
{
$params = [];
$sql = $this->db->getQueryBuilder()->batchInsert($index, $columns, $rows, $params);
return $this->setSql($sql)->bindValues($params);
}
/**
* Creates an REPLACE command.
* For example,
*
* ~~~
* $connection->createCommand()->insert('idx_user', [
* 'name' => 'Sam',
* 'age' => 30,
* ])->execute();
* ~~~
*
* The method will properly escape the column names, and bind the values to be replaced.
*
* Note that the created command is not executed until [[execute()]] is called.
*
* @param string $index the index that new rows will be replaced into.
* @param array $columns the column data (name => value) to be replaced into the index.
* @return static the command object itself
*/
public function replace($index, $columns)
{
$params = [];
$sql = $this->db->getQueryBuilder()->replace($index, $columns, $params);
return $this->setSql($sql)->bindValues($params);
}
/**
* Creates a batch REPLACE command.
* For example,
*
* ~~~
* $connection->createCommand()->batchInsert('idx_user', ['name', 'age'], [
* ['Tom', 30],
* ['Jane', 20],
* ['Linda', 25],
* ])->execute();
* ~~~
*
* Note that the values in each row must match the corresponding column names.
*
* @param string $index the index that new rows will be replaced.
* @param array $columns the column names
* @param array $rows the rows to be batch replaced in the index
* @return static the command object itself
*/
public function batchReplace($index, $columns, $rows)
{
$params = [];
$sql = $this->db->getQueryBuilder()->batchReplace($index, $columns, $rows, $params);
return $this->setSql($sql)->bindValues($params);
}
/**
* Creates an UPDATE command.
* For example,
*
* ~~~
* $connection->createCommand()->update('tbl_user', ['status' => 1], 'age > 30')->execute();
* ~~~
*
* The method will properly escape the column names and bind the values to be updated.
*
* Note that the created command is not executed until [[execute()]] is called.
*
* @param string $index the index to be updated.
* @param array $columns the column data (name => value) to be updated.
* @param string|array $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition.
* @param array $params the parameters to be bound to the command
* @param array $options list of options in format: optionName => optionValue
* @return static the command object itself
*/
public function update($index, $columns, $condition = '', $params = [], $options = [])
{
$sql = $this->db->getQueryBuilder()->update($index, $columns, $condition, $params, $options);
return $this->setSql($sql)->bindValues($params);
}
/**
* Creates a SQL command for truncating a runtime index.
* @param string $index the index to be truncated. The name will be properly quoted by the method.
* @return static the command object itself
*/
public function truncateIndex($index)
{
$sql = $this->db->getQueryBuilder()->truncateIndex($index);
return $this->setSql($sql);
}
/**
* Builds a snippet from provided data and query, using specified index settings.
* @param string $index name of the index, from which to take the text processing settings.
* @param string|array $source is the source data to extract a snippet from.
* It could be either a single string or array of strings.
* @param string $match the full-text query to build snippets for.
* @param array $options list of options in format: optionName => optionValue
* @return static the command object itself
*/
public function callSnippets($index, $source, $match, $options = [])
{
$params = [];
$sql = $this->db->getQueryBuilder()->callSnippets($index, $source, $match, $options, $params);
return $this->setSql($sql)->bindValues($params);
}
/**
* Returns tokenized and normalized forms of the keywords, and, optionally, keyword statistics.
* @param string $index the name of the index from which to take the text processing settings
* @param string $text the text to break down to keywords.
* @param boolean $fetchStatistic whether to return document and hit occurrence statistics
* @return string the SQL statement for call keywords.
*/
public function callKeywords($index, $text, $fetchStatistic = false)
{
$params = [];
$sql = $this->db->getQueryBuilder()->callKeywords($index, $text, $fetchStatistic, $params);
return $this->setSql($sql)->bindValues($params);
}
// Not Supported :
/**
* @inheritdoc
*/
public function createTable($table, $columns, $options = null)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function renameTable($table, $newName)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function dropTable($table)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function truncateTable($table)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function addColumn($table, $column, $type)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function dropColumn($table, $column)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function renameColumn($table, $oldName, $newName)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function alterColumn($table, $column, $type)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function addPrimaryKey($name, $table, $columns)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function dropPrimaryKey($name, $table)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function dropForeignKey($name, $table)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function createIndex($name, $table, $columns, $unique = false)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function dropIndex($name, $table)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function resetSequence($table, $value = null)
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
/**
* @inheritdoc
*/
public function checkIntegrity($check = true, $schema = '')
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
}

129
extensions/sphinx/Connection.php

@ -0,0 +1,129 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\NotSupportedException;
/**
* Connection represents the Sphinx connection via MySQL protocol.
* This class uses [PDO](http://www.php.net/manual/en/ref.pdo.php) to maintain such connection.
* Note: although PDO supports numerous database drivers, this class supports only MySQL.
*
* In order to setup Sphinx "searchd" to support MySQL protocol following configuration should be added:
* ~~~
* searchd
* {
* listen = localhost:9306:mysql41
* ...
* }
* ~~~
*
* The following example shows how to create a Connection instance and establish
* the Sphinx connection:
* ~~~
* $connection = new \yii\db\Connection([
* 'dsn' => 'mysql:host=127.0.0.1;port=9306;',
* 'username' => $username,
* 'password' => $password,
* ]);
* $connection->open();
* ~~~
*
* After the Sphinx connection is established, one can execute SQL statements like the following:
* ~~~
* $command = $connection->createCommand("SELECT * FROM idx_article WHERE MATCH('programming')");
* $articles = $command->queryAll();
* $command = $connection->createCommand('UPDATE idx_article SET status=2 WHERE id=1');
* $command->execute();
* ~~~
*
* For more information about how to perform various DB queries, please refer to [[Command]].
*
* This class supports transactions exactly as "yii\db\Connection".
*
* Note: while this class extends "yii\db\Connection" some of its methods are not supported.
*
* @property Schema $schema The schema information for this Sphinx connection. This property is read-only.
* @property \yii\sphinx\QueryBuilder $queryBuilder The query builder for this Sphinx connection. This property is
* read-only.
* @method \yii\sphinx\Schema getSchema() The schema information for this Sphinx connection
* @method \yii\sphinx\QueryBuilder getQueryBuilder() the query builder for this Sphinx connection
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Connection extends \yii\db\Connection
{
/**
* @inheritdoc
*/
public $schemaMap = [
'mysqli' => 'yii\sphinx\Schema', // MySQL
'mysql' => 'yii\sphinx\Schema', // MySQL
];
/**
* Obtains the schema information for the named index.
* @param string $name index name.
* @param boolean $refresh whether to reload the table schema even if it is found in the cache.
* @return IndexSchema index schema information. Null if the named index does not exist.
*/
public function getIndexSchema($name, $refresh = false)
{
return $this->getSchema()->getIndexSchema($name, $refresh);
}
/**
* Quotes a index name for use in a query.
* If the index name contains schema prefix, the prefix will also be properly quoted.
* If the index name is already quoted or contains special characters including '(', '[[' and '{{',
* then this method will do nothing.
* @param string $name index name
* @return string the properly quoted index name
*/
public function quoteIndexName($name)
{
return $this->getSchema()->quoteIndexName($name);
}
/**
* Alias of [[quoteIndexName()]].
* @param string $name table name
* @return string the properly quoted table name
*/
public function quoteTableName($name)
{
return $this->quoteIndexName($name);
}
/**
* Creates a command for execution.
* @param string $sql the SQL statement to be executed
* @param array $params the parameters to be bound to the SQL statement
* @return Command the Sphinx command
*/
public function createCommand($sql = null, $params = [])
{
$this->open();
$command = new Command([
'db' => $this,
'sql' => $sql,
]);
return $command->bindValues($params);
}
/**
* This method is not supported by Sphinx.
* @param string $sequenceName name of the sequence object
* @return string the row ID of the last row inserted, or the last value retrieved from the sequence object
* @throws \yii\base\NotSupportedException always.
*/
public function getLastInsertID($sequenceName = '')
{
throw new NotSupportedException('"' . __METHOD__ . '" is not supported.');
}
}

63
extensions/sphinx/IndexSchema.php

@ -0,0 +1,63 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008-2011 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\Object;
use yii\base\InvalidParamException;
/**
* IndexSchema represents the metadata of a Sphinx index.
*
* @property array $columnNames List of column names. This property is read-only.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class IndexSchema extends Object
{
/**
* @var string name of this index.
*/
public $name;
/**
* @var string type of the index.
*/
public $type;
/**
* @var boolean whether this index is a runtime index.
*/
public $isRuntime;
/**
* @var string primary key of this index.
*/
public $primaryKey;
/**
* @var ColumnSchema[] column metadata of this index. Each array element is a [[ColumnSchema]] object, indexed by column names.
*/
public $columns = [];
/**
* Gets the named column metadata.
* This is a convenient method for retrieving a named column even if it does not exist.
* @param string $name column name
* @return ColumnSchema metadata of the named column. Null if the named column does not exist.
*/
public function getColumn($name)
{
return isset($this->columns[$name]) ? $this->columns[$name] : null;
}
/**
* Returns the names of all columns in this table.
* @return array list of column names
*/
public function getColumnNames()
{
return array_keys($this->columns);
}
}

32
extensions/sphinx/LICENSE.md

@ -0,0 +1,32 @@
The Yii framework is free software. It is released under the terms of
the following BSD License.
Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

701
extensions/sphinx/Query.php

@ -0,0 +1,701 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use Yii;
use yii\base\Component;
use yii\base\InvalidCallException;
use yii\db\Expression;
use yii\db\QueryInterface;
use yii\db\QueryTrait;
/**
* Query represents a SELECT SQL statement.
*
* Query provides a set of methods to facilitate the specification of different clauses
* in a SELECT statement. These methods can be chained together.
*
* By calling [[createCommand()]], we can get a [[Command]] instance which can be further
* used to perform/execute the Sphinx query.
*
* For example,
*
* ~~~
* $query = new Query;
* $query->select('id, groupd_id')
* ->from('idx_item')
* ->limit(10);
* // build and execute the query
* $command = $query->createCommand();
* // $command->sql returns the actual SQL
* $rows = $command->queryAll();
* ~~~
*
* Since Sphinx does not store the original indexed text, the snippets for the rows in query result
* should be build separately via another query. You can simplify this workflow using [[snippetCallback]].
*
* Warning: even if you do not set any query limit, implicit LIMIT 0,20 is present by default!
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Query extends Component implements QueryInterface
{
use QueryTrait;
/**
* @var array the columns being selected. For example, `['id', 'group_id']`.
* 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 string additional option that should be appended to the 'SELECT' keyword.
*/
public $selectOption;
/**
* @var boolean whether to select distinct rows of data only. If this is set true,
* the SELECT clause would be changed to SELECT DISTINCT.
*/
public $distinct;
/**
* @var array the index(es) to be selected from. For example, `['idx_user', 'idx_user_delta']`.
* This is used to construct the FROM clause in a SQL statement.
* @see from()
*/
public $from;
/**
* @var string text, which should be searched in fulltext mode.
* This value will be composed into MATCH operator inside the WHERE clause.
*/
public $match;
/**
* @var array how to group the query results. For example, `['company', 'department']`.
* This is used to construct the GROUP BY clause in a SQL statement.
*/
public $groupBy;
/**
* @var string WITHIN GROUP ORDER BY clause. This is a Sphinx specific extension
* that lets you control how the best row within a group will to be selected.
* The possible value matches the [[orderBy]] one.
*/
public $within;
/**
* @var array per-query options in format: optionName => optionValue
* They will compose OPTION clause. This is a Sphinx specific extension
* that lets you control a number of per-query options.
*/
public $options;
/**
* @var array list of query parameter values indexed by parameter placeholders.
* For example, `[':name' => 'Dan', ':age' => 31]`.
*/
public $params;
/**
* @var callback PHP callback, which should be used to fetch source data for the snippets.
* Such callback will receive array of query result rows as an argument and must return the
* array of snippet source strings in the order, which match one of incoming rows.
* For example:
* ~~~
* $query = new Query;
* $query->from('idx_item')
* ->match('pencil')
* ->snippetCallback(function ($rows) {
* $result = [];
* foreach ($rows as $row) {
* $result[] = file_get_contents('/path/to/index/files/' . $row['id'] . '.txt');
* }
* return $result;
* })
* ->all();
* ~~~
*/
public $snippetCallback;
/**
* @var array query options for the call snippet.
*/
public $snippetOptions;
/**
* @var Connection the Sphinx connection used to generate the SQL statements.
*/
private $_connection;
/**
* @param Connection $connection Sphinx connection instance
* @return static the query object itself
*/
public function setConnection($connection)
{
$this->_connection = $connection;
return $this;
}
/**
* @return Connection Sphinx connection instance
*/
public function getConnection()
{
if ($this->_connection === null) {
$this->_connection = $this->defaultConnection();
}
return $this->_connection;
}
/**
* @return Connection default connection value.
*/
protected function defaultConnection()
{
return Yii::$app->getComponent('sphinx');
}
/**
* Creates a Sphinx command that can be used to execute this query.
* @param Connection $connection the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return Command the created Sphinx command instance.
*/
public function createCommand($connection = null)
{
$this->setConnection($connection);
$connection = $this->getConnection();
list ($sql, $params) = $connection->getQueryBuilder()->build($this);
return $connection->createCommand($sql, $params);
}
/**
* Executes the query and returns all results as an array.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return array the query results. If the query results in nothing, an empty array will be returned.
*/
public function all($db = null)
{
$rows = $this->createCommand($db)->queryAll();
$rows = $this->fillUpSnippets($rows);
if ($this->indexBy === null) {
return $rows;
}
$result = [];
foreach ($rows as $row) {
if (is_string($this->indexBy)) {
$key = $row[$this->indexBy];
} else {
$key = call_user_func($this->indexBy, $row);
}
$result[$key] = $row;
}
return $result;
}
/**
* Executes the query and returns a single row of result.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query
* results in nothing.
*/
public function one($db = null)
{
$row = $this->createCommand($db)->queryOne();
if ($row !== false) {
list ($row) = $this->fillUpSnippets([$row]);
}
return $row;
}
/**
* Returns the query result as a scalar value.
* The value returned will be the first column in the first row of the query results.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return string|boolean the value of the first column in the first row of the query result.
* False is returned if the query result is empty.
*/
public function scalar($db = null)
{
return $this->createCommand($db)->queryScalar();
}
/**
* Executes the query and returns the first column of the result.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return array the first column of the query result. An empty array is returned if the query results in nothing.
*/
public function column($db = null)
{
return $this->createCommand($db)->queryColumn();
}
/**
* Returns the number of records.
* @param string $q the COUNT expression. Defaults to '*'.
* Make sure you properly quote column names in the expression.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return integer number of records
*/
public function count($q = '*', $db = null)
{
$this->select = ["COUNT($q)"];
return $this->createCommand($db)->queryScalar();
}
/**
* Returns the sum of the specified column values.
* @param string $q the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return integer the sum of the specified column values
*/
public function sum($q, $db = null)
{
$this->select = ["SUM($q)"];
return $this->createCommand($db)->queryScalar();
}
/**
* Returns the average of the specified column values.
* @param string $q the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return integer the average of the specified column values.
*/
public function average($q, $db = null)
{
$this->select = ["AVG($q)"];
return $this->createCommand($db)->queryScalar();
}
/**
* Returns the minimum of the specified column values.
* @param string $q the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return integer the minimum of the specified column values.
*/
public function min($q, $db = null)
{
$this->select = ["MIN($q)"];
return $this->createCommand($db)->queryScalar();
}
/**
* Returns the maximum of the specified column values.
* @param string $q the column name or expression.
* Make sure you properly quote column names in the expression.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return integer the maximum of the specified column values.
*/
public function max($q, $db = null)
{
$this->select = ["MAX($q)"];
return $this->createCommand($db)->queryScalar();
}
/**
* Returns a value indicating whether the query result contains any row of data.
* @param Connection $db the Sphinx connection used to generate the SQL statement.
* If this parameter is not given, the `sphinx` application component will be used.
* @return boolean whether the query result contains any row of data.
*/
public function exists($db = null)
{
$this->select = [new Expression('1')];
return $this->scalar($db) !== false;
}
/**
* 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. ['id', 'name']).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a Sphinx expression).
* @param string $option additional option that should be appended to the 'SELECT' keyword.
* @return static the query object itself
*/
public function select($columns, $option = null)
{
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->select = $columns;
$this->selectOption = $option;
return $this;
}
/**
* Sets the value indicating whether to SELECT DISTINCT or not.
* @param bool $value whether to SELECT DISTINCT or not.
* @return static the query object itself
*/
public function distinct($value = true)
{
$this->distinct = $value;
return $this;
}
/**
* Sets the FROM part of the query.
* @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. `'idx_user'`)
* or an array (e.g. `['idx_user', 'idx_user_delta']`) specifying one or several index names.
* The method will automatically quote the table names unless it contains some parenthesis
* (which means the table is given as a sub-query or Sphinx expression).
* @return static the query object itself
*/
public function from($tables)
{
if (!is_array($tables)) {
$tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
}
$this->from = $tables;
return $this;
}
/**
* Sets the fulltext query text. This text will be composed into
* MATCH operator inside the WHERE clause.
* @param string $query fulltext query text.
* @return static the query object itself
*/
public function match($query)
{
$this->match = $query;
return $this;
}
/**
* Sets the WHERE part of the query.
*
* The method requires a $condition parameter, and optionally a $params parameter
* specifying the values to be bound to the query.
*
* The $condition parameter should be either a string (e.g. 'id=1') or an array.
* If the latter, it must be in one of the following two formats:
*
* - hash format: `['column1' => value1, 'column2' => value2, ...]`
* - operator format: `[operator, operand1, operand2, ...]`
*
* A condition in hash format represents the following SQL expression in general:
* `column1=value1 AND column2=value2 AND ...`. In case when a value is an array,
* an `IN` expression will be generated. And if a value is null, `IS NULL` will be used
* in the generated expression. Below are some examples:
*
* - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`.
* - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`.
* - `['status' => null] generates `status IS NULL`.
*
* A condition in operator format generates the SQL expression according to the specified operator, which
* can be one of the followings:
*
* - `and`: the operands should be concatenated together using `AND`. For example,
* `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array,
* it will be converted into a string using the rules described here. For example,
* `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`.
* The method will NOT do any quoting or escaping.
*
* - `or`: similar to the `and` operator except that the operands are concatenated using `OR`.
*
* - `between`: operand 1 should be the column name, and operand 2 and 3 should be the
* starting and ending values of the range that the column is in.
* For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`.
*
* - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN`
* in the generated condition.
*
* - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing
* the range of the values that the column or DB expression should be in. For example,
* `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`.
* The method will properly quote the column name and escape values in the range.
*
* - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition.
*
* - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing
* the values that the column or DB expression should be like.
* For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`.
* When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated
* using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate
* `name LIKE '%test%' AND name LIKE '%sample%'`.
* The method will properly quote the column name and escape values in the range.
*
* - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE`
* predicates when operand 2 is an array.
*
* - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE`
* in the generated condition.
*
* - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate
* the `NOT LIKE` predicates.
*
* @param string|array $condition the conditions that should be put in the WHERE part.
* @param array $params the parameters (name => value) to be bound to the query.
* @return static the query object itself
* @see andWhere()
* @see orWhere()
*/
public function where($condition, $params = [])
{
$this->where = $condition;
$this->addParams($params);
return $this;
}
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the 'AND' operator.
* @param string|array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return static the query object itself
* @see where()
* @see orWhere()
*/
public function andWhere($condition, $params = [])
{
if ($this->where === null) {
$this->where = $condition;
} else {
$this->where = ['and', $this->where, $condition];
}
$this->addParams($params);
return $this;
}
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the 'OR' operator.
* @param string|array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return static the query object itself
* @see where()
* @see andWhere()
*/
public function orWhere($condition, $params = [])
{
if ($this->where === null) {
$this->where = $condition;
} else {
$this->where = ['or', $this->where, $condition];
}
$this->addParams($params);
return $this;
}
/**
* Sets the GROUP BY part of the query.
* @param string|array $columns the columns to be grouped by.
* Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see addGroupBy()
*/
public function groupBy($columns)
{
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->groupBy = $columns;
return $this;
}
/**
* Adds additional group-by columns to the existing ones.
* @param string|array $columns additional columns to be grouped by.
* Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see groupBy()
*/
public function addGroupBy($columns)
{
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
if ($this->groupBy === null) {
$this->groupBy = $columns;
} else {
$this->groupBy = array_merge($this->groupBy, $columns);
}
return $this;
}
/**
* 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 addParams()
*/
public function params($params)
{
$this->params = $params;
return $this;
}
/**
* Adds additional 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 addParams($params)
{
if (!empty($params)) {
if ($this->params === null) {
$this->params = $params;
} else {
foreach ($params as $name => $value) {
if (is_integer($name)) {
$this->params[] = $value;
} else {
$this->params[$name] = $value;
}
}
}
}
return $this;
}
/**
* Sets the query options.
* @param array $options query options in format: optionName => optionValue
* @return static the query object itself
* @see addOptions()
*/
public function options($options)
{
$this->options = $options;
return $this;
}
/**
* Adds additional query options.
* @param array $options query options in format: optionName => optionValue
* @return static the query object itself
* @see options()
*/
public function addOptions($options)
{
if (is_array($this->options)) {
$this->options = array_merge($this->options, $options);
} else {
$this->options = $options;
}
return $this;
}
/**
* Sets the WITHIN GROUP ORDER BY part of the query.
* @param string|array $columns the columns (and the directions) to find best row within a group.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see addWithin()
*/
public function within($columns)
{
$this->within = $this->normalizeOrderBy($columns);
return $this;
}
/**
* Adds additional WITHIN GROUP ORDER BY columns to the query.
* @param string|array $columns the columns (and the directions) to find best row within a group.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see within()
*/
public function addWithin($columns)
{
$columns = $this->normalizeOrderBy($columns);
if ($this->within === null) {
$this->within = $columns;
} else {
$this->within = array_merge($this->within, $columns);
}
return $this;
}
/**
* Sets the PHP callback, which should be used to retrieve the source data
* for the snippets building.
* @param callback $callback PHP callback, which should be used to fetch source data for the snippets.
* @return static the query object itself
* @see snippetCallback
*/
public function snippetCallback($callback)
{
$this->snippetCallback = $callback;
return $this;
}
/**
* Sets the call snippets query options.
* @param array $options call snippet options in format: option_name => option_value
* @return static the query object itself
* @see snippetCallback
*/
public function snippetOptions($options)
{
$this->snippetOptions = $options;
return $this;
}
/**
* Fills the query result rows with the snippets built from source determined by
* [[snippetCallback]] result.
* @param array $rows raw query result rows.
* @return array query result rows with filled up snippets.
*/
protected function fillUpSnippets($rows)
{
if ($this->snippetCallback === null) {
return $rows;
}
$snippetSources = call_user_func($this->snippetCallback, $rows);
$snippets = $this->callSnippets($snippetSources);
$snippetKey = 0;
foreach ($rows as $key => $row) {
$rows[$key]['snippet'] = $snippets[$snippetKey];
$snippetKey++;
}
return $rows;
}
/**
* Builds a snippets from provided source data.
* @param array $source the source data to extract a snippet from.
* @throws InvalidCallException in case [[match]] is not specified.
* @return array snippets list.
*/
protected function callSnippets(array $source)
{
$connection = $this->getConnection();
$match = $this->match;
if ($match === null) {
throw new InvalidCallException('Unable to call snippets: "' . $this->className() . '::match" should be specified.');
}
return $connection->createCommand()
->callSnippets($this->from[0], $source, $match, $this->snippetOptions)
->queryColumn();
}
}

904
extensions/sphinx/QueryBuilder.php

@ -0,0 +1,904 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\Object;
use yii\db\Exception;
use yii\db\Expression;
/**
* QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
*
* QueryBuilder can also be used to build SQL statements such as INSERT, REPLACE, UPDATE, DELETE,
* from a [[Query]] object.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class QueryBuilder extends Object
{
/**
* The prefix for automatically generated query binding parameters.
*/
const PARAM_PREFIX = ':qp';
/**
* @var Connection the Sphinx connection.
*/
public $db;
/**
* @var string the separator between different fragments of a SQL statement.
* Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
*/
public $separator = " ";
/**
* Constructor.
* @param Connection $connection the Sphinx connection.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($connection, $config = [])
{
$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)
{
$params = $query->params;
if ($query->match !== null) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = (string)$query->match;
$query->andWhere('MATCH(' . $phName . ')');
}
$clauses = [
$this->buildSelect($query->select, $query->distinct, $query->selectOption),
$this->buildFrom($query->from),
$this->buildWhere($query->from, $query->where, $params),
$this->buildGroupBy($query->groupBy),
$this->buildWithin($query->within),
$this->buildOrderBy($query->orderBy),
$this->buildLimit($query->limit, $query->offset),
$this->buildOption($query->options, $params),
];
return [implode($this->separator, array_filter($clauses)), $params];
}
/**
* Creates an INSERT SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->insert('idx_user', [
* 'name' => 'Sam',
* 'age' => 30,
* 'id' => 10,
* ], $params);
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index that new rows will be inserted into.
* @param array $columns the column data (name => value) to be inserted into the index.
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the INSERT SQL
*/
public function insert($index, $columns, &$params)
{
return $this->generateInsertReplace('INSERT', $index, $columns, $params);
}
/**
* Creates an REPLACE SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->replace('idx_user', [
* 'name' => 'Sam',
* 'age' => 30,
* 'id' => 10,
* ], $params);
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index that new rows will be replaced.
* @param array $columns the column data (name => value) to be replaced in the index.
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the INSERT SQL
*/
public function replace($index, $columns, &$params)
{
return $this->generateInsertReplace('REPLACE', $index, $columns, $params);
}
/**
* Generates INSERT/REPLACE SQL statement.
* @param string $statement statement ot be generated.
* @param string $index the affected index name.
* @param array $columns the column data (name => value).
* @param array $params the binding parameters that will be generated by this method.
* @return string generated SQL
*/
protected function generateInsertReplace($statement, $index, $columns, &$params)
{
if (($indexSchema = $this->db->getIndexSchema($index)) !== null) {
$indexSchemas = [$indexSchema];
} else {
$indexSchemas = [];
}
$names = [];
$placeholders = [];
foreach ($columns as $name => $value) {
$names[] = $this->db->quoteColumnName($name);
$placeholders[] = $this->composeColumnValue($indexSchemas, $name, $value, $params);
}
return $statement . ' INTO ' . $this->db->quoteIndexName($index)
. ' (' . implode(', ', $names) . ') VALUES ('
. implode(', ', $placeholders) . ')';
}
/**
* Generates a batch INSERT SQL statement.
* For example,
*
* ~~~
* $connection->createCommand()->batchInsert('idx_user', ['id', 'name', 'age'], [
* [1, 'Tom', 30],
* [2, 'Jane', 20],
* [3, 'Linda', 25],
* ])->execute();
* ~~~
*
* Note that the values in each row must match the corresponding column names.
*
* @param string $index the index that new rows will be inserted into.
* @param array $columns the column names
* @param array $rows the rows to be batch inserted into the index
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the batch INSERT SQL statement
*/
public function batchInsert($index, $columns, $rows, &$params)
{
return $this->generateBatchInsertReplace('INSERT', $index, $columns, $rows, $params);
}
/**
* Generates a batch REPLACE SQL statement.
* For example,
*
* ~~~
* $connection->createCommand()->batchReplace('idx_user', ['id', 'name', 'age'], [
* [1, 'Tom', 30],
* [2, 'Jane', 20],
* [3, 'Linda', 25],
* ])->execute();
* ~~~
*
* Note that the values in each row must match the corresponding column names.
*
* @param string $index the index that new rows will be replaced.
* @param array $columns the column names
* @param array $rows the rows to be batch replaced in the index
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the batch INSERT SQL statement
*/
public function batchReplace($index, $columns, $rows, &$params)
{
return $this->generateBatchInsertReplace('REPLACE', $index, $columns, $rows, $params);
}
/**
* Generates a batch INSERT/REPLACE SQL statement.
* @param string $statement statement ot be generated.
* @param string $index the affected index name.
* @param array $columns the column data (name => value).
* @param array $rows the rows to be batch inserted into the index
* @param array $params the binding parameters that will be generated by this method.
* @return string generated SQL
*/
protected function generateBatchInsertReplace($statement, $index, $columns, $rows, &$params)
{
if (($indexSchema = $this->db->getIndexSchema($index)) !== null) {
$indexSchemas = [$indexSchema];
} else {
$indexSchemas = [];
}
foreach ($columns as $i => $name) {
$columns[$i] = $this->db->quoteColumnName($name);
}
$values = [];
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
$vs[] = $this->composeColumnValue($indexSchemas, $columns[$i], $value, $params);
}
$values[] = '(' . implode(', ', $vs) . ')';
}
return $statement . ' INTO ' . $this->db->quoteIndexName($index)
. ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
}
/**
* Creates an UPDATE SQL statement.
* For example,
*
* ~~~
* $params = [];
* $sql = $queryBuilder->update('idx_user', ['status' => 1], 'age > 30', $params);
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index to be updated.
* @param array $columns the column data (name => value) to be updated.
* @param array|string $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition.
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @param array $options list of options in format: optionName => optionValue
* @return string the UPDATE SQL
*/
public function update($index, $columns, $condition, &$params, $options)
{
if (($indexSchema = $this->db->getIndexSchema($index)) !== null) {
$indexSchemas = [$indexSchema];
} else {
$indexSchemas = [];
}
$lines = [];
foreach ($columns as $name => $value) {
$lines[] = $this->db->quoteColumnName($name) . '=' . $this->composeColumnValue($indexSchemas, $name, $value, $params);
}
$sql = 'UPDATE ' . $this->db->quoteIndexName($index) . ' SET ' . implode(', ', $lines);
$where = $this->buildWhere([$index], $condition, $params);
if ($where !== '') {
$sql = $sql . ' ' . $where;
}
$option = $this->buildOption($options, $params);
if ($option !== '') {
$sql = $sql . ' ' . $option;
}
return $sql;
}
/**
* Creates a DELETE SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->delete('idx_user', 'status = 0');
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index where the data will be deleted from.
* @param array|string $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition.
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @return string the DELETE SQL
*/
public function delete($index, $condition, &$params)
{
$sql = 'DELETE FROM ' . $this->db->quoteIndexName($index);
$where = $this->buildWhere([$index], $condition, $params);
return $where === '' ? $sql : $sql . ' ' . $where;
}
/**
* Builds a SQL statement for truncating an index.
* @param string $index the index to be truncated. The name will be properly quoted by the method.
* @return string the SQL statement for truncating an index.
*/
public function truncateIndex($index)
{
return 'TRUNCATE RTINDEX ' . $this->db->quoteIndexName($index);
}
/**
* Builds a SQL statement for call snippet from provided data and query, using specified index settings.
* @param string $index name of the index, from which to take the text processing settings.
* @param string|array $source is the source data to extract a snippet from.
* It could be either a single string or array of strings.
* @param string $match the full-text query to build snippets for.
* @param array $options list of options in format: optionName => optionValue
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @return string the SQL statement for call snippets.
*/
public function callSnippets($index, $source, $match, $options, &$params)
{
if (is_array($source)) {
$dataSqlParts = [];
foreach ($source as $sourceRow) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $sourceRow;
$dataSqlParts[] = $phName;
}
$dataSql = '(' . implode(',', $dataSqlParts) . ')';
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $source;
$dataSql = $phName;
}
$indexParamName = self::PARAM_PREFIX . count($params);
$params[$indexParamName] = $index;
$matchParamName = self::PARAM_PREFIX . count($params);
$params[$matchParamName] = $match;
if (!empty($options)) {
$optionParts = [];
foreach ($options as $name => $value) {
if ($value instanceof Expression) {
$actualValue = $value->expression;
} else {
$actualValue = self::PARAM_PREFIX . count($params);
$params[$actualValue] = $value;
}
$optionParts[] = $actualValue . ' AS ' . $name;
}
$optionSql = ', ' . implode(', ', $optionParts);
} else {
$optionSql = '';
}
return 'CALL SNIPPETS(' . $dataSql. ', ' . $indexParamName . ', ' . $matchParamName . $optionSql. ')';
}
/**
* Builds a SQL statement for returning tokenized and normalized forms of the keywords, and,
* optionally, keyword statistics.
* @param string $index the name of the index from which to take the text processing settings
* @param string $text the text to break down to keywords.
* @param boolean $fetchStatistic whether to return document and hit occurrence statistics
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @return string the SQL statement for call keywords.
*/
public function callKeywords($index, $text, $fetchStatistic, &$params)
{
$indexParamName = self::PARAM_PREFIX . count($params);
$params[$indexParamName] = $index;
$textParamName = self::PARAM_PREFIX . count($params);
$params[$textParamName] = $text;
return 'CALL KEYWORDS(' . $textParamName . ', ' . $indexParamName . ($fetchStatistic ? ', 1' : '') . ')';
}
/**
* @param array $columns
* @param boolean $distinct
* @param string $selectOption
* @return string the SELECT clause built from [[query]].
*/
public function buildSelect($columns, $distinct = false, $selectOption = null)
{
$select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
if ($selectOption !== null) {
$select .= ' ' . $selectOption;
}
if (empty($columns)) {
return $select . ' *';
}
foreach ($columns as $i => $column) {
if (is_object($column)) {
$columns[$i] = (string)$column;
} elseif (strpos($column, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
$columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
} else {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
}
if (is_array($columns)) {
$columns = implode(', ', $columns);
}
return $select . ' ' . $columns;
}
/**
* @param array $indexes
* @return string the FROM clause built from [[query]].
*/
public function buildFrom($indexes)
{
if (empty($indexes)) {
return '';
}
foreach ($indexes as $i => $index) {
if (strpos($index, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $index, $matches)) { // with alias
$indexes[$i] = $this->db->quoteIndexName($matches[1]) . ' ' . $this->db->quoteIndexName($matches[2]);
} else {
$indexes[$i] = $this->db->quoteIndexName($index);
}
}
}
if (is_array($indexes)) {
$indexes = implode(', ', $indexes);
}
return 'FROM ' . $indexes;
}
/**
* @param string[] $indexes list of index names, which affected by query
* @param string|array $condition
* @param array $params the binding parameters to be populated
* @return string the WHERE clause built from [[query]].
*/
public function buildWhere($indexes, $condition, &$params)
{
if (empty($condition)) {
return '';
}
$indexSchemas = [];
if (!empty($indexes)) {
foreach ($indexes as $indexName) {
$index = $this->db->getIndexSchema($indexName);
if ($index !== null) {
$indexSchemas[] = $index;
}
}
}
$where = $this->buildCondition($indexSchemas, $condition, $params);
return $where === '' ? '' : 'WHERE ' . $where;
}
/**
* @param array $columns
* @return string the GROUP BY clause
*/
public function buildGroupBy($columns)
{
return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns);
}
/**
* @param array $columns
* @return string the ORDER BY clause built from [[query]].
*/
public function buildOrderBy($columns)
{
if (empty($columns)) {
return '';
}
$orders = [];
foreach ($columns as $name => $direction) {
if (is_object($direction)) {
$orders[] = (string)$direction;
} else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : 'ASC');
}
}
return 'ORDER BY ' . implode(', ', $orders);
}
/**
* @param integer $limit
* @param integer $offset
* @return string the LIMIT and OFFSET clauses built from [[query]].
*/
public function buildLimit($limit, $offset)
{
$sql = '';
if ($limit !== null && $limit >= 0) {
$sql = 'LIMIT ' . (int)$limit;
}
if ($offset > 0) {
$sql .= ' OFFSET ' . (int)$offset;
}
return ltrim($sql);
}
/**
* Processes columns and properly quote them if necessary.
* It will join all columns into a string with comma as separators.
* @param string|array $columns the columns to be processed
* @return string the processing result
*/
public function buildColumns($columns)
{
if (!is_array($columns)) {
if (strpos($columns, '(') !== false) {
return $columns;
} else {
$columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
}
}
foreach ($columns as $i => $column) {
if (is_object($column)) {
$columns[$i] = (string)$column;
} elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
return is_array($columns) ? implode(', ', $columns) : $columns;
}
/**
* Parses the condition specification and generates the corresponding SQL expression.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @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($indexes, $condition, &$params)
{
static $builders = [
'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 (!is_array($condition)) {
return (string)$condition;
} elseif (empty($condition)) {
return '';
}
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);
return $this->$method($indexes, $operator, $condition, $params);
} else {
throw new Exception('Found unknown operator in query: ' . $operator);
}
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($indexes, $condition, $params);
}
}
/**
* Creates a condition based on column-value pairs.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param array $condition the condition specification.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildHashCondition($indexes, $condition, &$params)
{
$parts = [];
foreach ($condition as $column => $value) {
if (is_array($value)) { // IN condition
$parts[] = $this->buildInCondition($indexes, 'IN', [$column, $value], $params);
} else {
if (strpos($column, '(') === false) {
$quotedColumn = $this->db->quoteColumnName($column);
} else {
$quotedColumn = $column;
}
if ($value === null) {
$parts[] = "$quotedColumn IS NULL";
} else {
$parts[] = $quotedColumn . '=' . $this->composeColumnValue($indexes, $column, $value, $params);
}
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
}
/**
* Connects two or more SQL expressions with the `AND` or `OR` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use for connecting the given operands
* @param array $operands the SQL expressions to connect.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildAndCondition($indexes, $operator, $operands, &$params)
{
$parts = [];
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->buildCondition($indexes, $operand, $params);
}
if ($operand !== '') {
$parts[] = $operand;
}
}
if (!empty($parts)) {
return '(' . implode(") $operator (", $parts) . ')';
} else {
return '';
}
}
/**
* Creates an SQL expressions with the `BETWEEN` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
* @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 SQL expression
* @throws Exception if wrong number of operands have been given.
*/
public function buildBetweenCondition($indexes, $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) {
$quotedColumn = $this->db->quoteColumnName($column);
} else {
$quotedColumn = $column;
}
$phName1 = $this->composeColumnValue($indexes, $column, $value1, $params);
$phName2 = $this->composeColumnValue($indexes, $column, $value2, $params);
return "$quotedColumn $operator $phName1 AND $phName2";
}
/**
* Creates an SQL expressions with the `IN` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
* @param array $operands the first operand is the column name. If it is an array
* a composite IN condition will be generated.
* The second operand is an array of values that column value should be among.
* If it is an empty array the generated expression will be a `false` value if
* operator is `IN` and empty if operator is `NOT IN`.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws Exception if wrong number of operands have been given.
*/
public function buildInCondition($indexes, $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 === []) {
return $operator === 'IN' ? '0=1' : '';
}
if (count($column) > 1) {
return $this->buildCompositeInCondition($indexes, $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;
}
$values[$i] = $this->composeColumnValue($indexes, $column, $value, $params);
}
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]}";
}
}
/**
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
* @param array $columns
* @param array $values
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
protected function buildCompositeInCondition($indexes, $operator, $columns, $values, &$params)
{
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $column) {
if (isset($value[$column])) {
$vs[] = $this->composeColumnValue($indexes, $column, $value[$column], $params);
} 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) . ')';
}
/**
* Creates an SQL expressions with the `LIKE` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
* @param array $operands the first operand is the column name.
* The second operand is a single value or an array of values that column value
* should be compared with.
* If it is an empty array the generated expression will be a `false` value if
* operator is `LIKE` or `OR LIKE` and empty if operator is `NOT LIKE` or `OR NOT LIKE`.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws Exception if wrong number of operands have been given.
*/
public function buildLikeCondition($indexes, $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 = [];
foreach ($values as $value) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
$parts[] = "$column $operator $phName";
}
return implode($andor, $parts);
}
/**
* @param array $columns
* @return string the ORDER BY clause built from [[query]].
*/
public function buildWithin($columns)
{
if (empty($columns)) {
return '';
}
$orders = [];
foreach ($columns as $name => $direction) {
if (is_object($direction)) {
$orders[] = (string)$direction;
} else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
}
}
return 'WITHIN GROUP ORDER BY ' . implode(', ', $orders);
}
/**
* @param array $options query options in format: optionName => optionValue
* @param array $params the binding parameters to be populated
* @return string the OPTION clause build from [[query]]
*/
public function buildOption($options, &$params)
{
if (empty($options)) {
return '';
}
$optionLines = [];
foreach ($options as $name => $value) {
if ($value instanceof Expression) {
$actualValue = $value->expression;
} else {
if (is_array($value)) {
$actualValueParts = [];
foreach ($value as $key => $valuePart) {
if (is_numeric($key)) {
$actualValuePart = '';
} else {
$actualValuePart = $key . ' = ';
}
if ($valuePart instanceof Expression) {
$actualValuePart .= $valuePart->expression;
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $valuePart;
$actualValuePart .= $phName;
}
$actualValueParts[] = $actualValuePart;
}
$actualValue = '(' . implode(', ', $actualValueParts) . ')';
} else {
$actualValue = self::PARAM_PREFIX . count($params);
$params[$actualValue] = $value;
}
}
$optionLines[] = $name . ' = ' . $actualValue;
}
return 'OPTION ' . implode(', ', $optionLines);
}
/**
* Composes column value for SQL, taking in account the column type.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $columnName name of the column
* @param mixed $value raw column value
* @param array $params the binding parameters to be populated
* @return string SQL expression, which represents column value
*/
protected function composeColumnValue($indexes, $columnName, $value, &$params) {
if ($value === null) {
return 'NULL';
} elseif ($value instanceof Expression) {
$params = array_merge($params, $value->params);
return $value->expression;
}
foreach ($indexes as $index) {
$columnSchema = $index->getColumn($columnName);
if ($columnSchema !== null) {
break;
}
}
if (is_array($value)) {
// MVA :
$lineParts = [];
foreach ($value as $subValue) {
if ($subValue instanceof Expression) {
$params = array_merge($params, $subValue->params);
$lineParts[] = $subValue->expression;
} else {
$phName = self::PARAM_PREFIX . count($params);
$lineParts[] = $phName;
$params[$phName] = (isset($columnSchema)) ? $columnSchema->typecast($subValue) : $subValue;
}
}
return '(' . implode(',', $lineParts) . ')';
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = (isset($columnSchema)) ? $columnSchema->typecast($value) : $value;
return $phName;
}
}
}

118
extensions/sphinx/README.md

@ -0,0 +1,118 @@
Yii 2.0 Public Preview - Sphinx Extension
=========================================
Thank you for choosing Yii - a high-performance component-based PHP framework.
If you are looking for a production-ready PHP framework, please use
[Yii v1.1](https://github.com/yiisoft/yii).
Yii 2.0 is still under heavy development. We may make significant changes
without prior notices. **Yii 2.0 is not ready for production use yet.**
[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2)
This is the yii2-sphinx extension.
Installation
------------
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
Either run
```
php composer.phar require yiisoft/yii2-sphinx "*"
```
or add
```
"yiisoft/yii2-sphinx": "*"
```
to the require section of your composer.json.
*Note: You might have to run `php composer.phar selfupdate`*
Usage & Documentation
---------------------
This extension adds [Sphinx](http://sphinxsearch.com/docs) full text search engine extension for the Yii framework.
This extension interact with Sphinx search daemon using MySQL protocol and [SphinxQL](http://sphinxsearch.com/docs/current.html#sphinxql) query language.
In order to setup Sphinx "searchd" to support MySQL protocol following configuration should be added:
```
searchd
{
listen = localhost:9306:mysql41
...
}
```
This extension supports all Sphinx features including [Runtime Indexes](http://sphinxsearch.com/docs/current.html#rt-indexes).
Since this extension uses MySQL protocol to access Sphinx, it shares base approach and much code from the
regular "yii\db" package.
To use this extension, simply add the following code in your application configuration:
```php
return [
//....
'components' => [
'sphinx' => [
'class' => 'yii\sphinx\Connection',
'dsn' => 'mysql:host=127.0.0.1;port=9306;',
'username' => '',
'password' => '',
],
],
];
```
This extension provides ActiveRecord solution similar ot the [[\yii\db\ActiveRecord]].
To declare an ActiveRecord class you need to extend [[\yii\sphinx\ActiveRecord]] and
implement the `indexName` method:
```php
use yii\sphinx\ActiveRecord;
class Article extends ActiveRecord
{
/**
* @return string the name of the index associated with this ActiveRecord class.
*/
public static function indexName()
{
return 'idx_article';
}
}
```
You can use [[\yii\data\ActiveDataProvider]] with the [[\yii\sphinx\Query]] and [[\yii\sphinx\ActiveQuery]]:
```php
use yii\data\ActiveDataProvider;
use yii\sphinx\Query;
$query = new Query;
$query->from('yii2_test_article_index')->match('development');
$provider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 10,
]
]);
$models = $provider->getModels();
```
```php
use yii\data\ActiveDataProvider;
use app\models\Article;
$provider = new ActiveDataProvider([
'query' => Article::find(),
'pagination' => [
'pageSize' => 10,
]
]);
$models = $provider->getModels();
```

489
extensions/sphinx/Schema.php

@ -0,0 +1,489 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\Object;
use yii\caching\Cache;
use Yii;
use yii\caching\GroupDependency;
/**
* Schema represents the Sphinx schema information.
*
* @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
* @property string[] $indexNames All index names in the Sphinx. This property is read-only.
* @property string[] $indexTypes ALL index types in the Sphinx (index name => index type).
* This property is read-only.
* @property IndexSchema[] $tableSchemas The metadata for all indexes in the Sphinx. Each array element is an
* instance of [[IndexSchema]] or its child class. This property is read-only.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Schema extends Object
{
/**
* The followings are the supported abstract column data types.
*/
const TYPE_PK = 'pk';
const TYPE_STRING = 'string';
const TYPE_INTEGER = 'integer';
const TYPE_BIGINT = 'bigint';
const TYPE_FLOAT = 'float';
const TYPE_TIMESTAMP = 'timestamp';
const TYPE_BOOLEAN = 'boolean';
/**
* @var Connection the Sphinx connection
*/
public $db;
/**
* @var array list of ALL index names in the Sphinx
*/
private $_indexNames;
/**
* @var array list of ALL index types in the Sphinx (index name => index type)
*/
private $_indexTypes;
/**
* @var array list of loaded index metadata (index name => IndexSchema)
*/
private $_indexes = [];
/**
* @var QueryBuilder the query builder for this Sphinx connection
*/
private $_builder;
/**
* @var array mapping from physical column types (keys) to abstract column types (values)
*/
public $typeMap = [
'field' => self::TYPE_STRING,
'string' => self::TYPE_STRING,
'ordinal' => self::TYPE_STRING,
'integer' => self::TYPE_INTEGER,
'int' => self::TYPE_INTEGER,
'uint' => self::TYPE_INTEGER,
'bigint' => self::TYPE_BIGINT,
'timestamp' => self::TYPE_TIMESTAMP,
'bool' => self::TYPE_BOOLEAN,
'float' => self::TYPE_FLOAT,
'mva' => self::TYPE_INTEGER,
];
/**
* Loads the metadata for the specified index.
* @param string $name index name
* @return IndexSchema driver dependent index metadata. Null if the index does not exist.
*/
protected function loadIndexSchema($name)
{
$index = new IndexSchema;
$this->resolveIndexNames($index, $name);
$this->resolveIndexType($index);
if ($this->findColumns($index)) {
return $index;
} else {
return null;
}
}
/**
* Resolves the index name.
* @param IndexSchema $index the index metadata object
* @param string $name the index name
*/
protected function resolveIndexNames($index, $name)
{
$index->name = str_replace('`', '', $name);
}
/**
* Resolves the index name.
* @param IndexSchema $index the index metadata object
*/
protected function resolveIndexType($index)
{
$indexTypes = $this->getIndexTypes();
$index->type = array_key_exists($index->name, $indexTypes) ? $indexTypes[$index->name] : 'unknown';
$index->isRuntime = ($index->type == 'rt');
}
/**
* Obtains the metadata for the named index.
* @param string $name index name. The index name may contain schema name if any. Do not quote the index name.
* @param boolean $refresh whether to reload the index schema even if it is found in the cache.
* @return IndexSchema index metadata. Null if the named index does not exist.
*/
public function getIndexSchema($name, $refresh = false)
{
if (isset($this->_indexes[$name]) && !$refresh) {
return $this->_indexes[$name];
}
$db = $this->db;
$realName = $this->getRawIndexName($name);
if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) {
/** @var $cache Cache */
$cache = is_string($db->schemaCache) ? Yii::$app->getComponent($db->schemaCache) : $db->schemaCache;
if ($cache instanceof Cache) {
$key = $this->getCacheKey($name);
if ($refresh || ($index = $cache->get($key)) === false) {
$index = $this->loadIndexSchema($realName);
if ($index !== null) {
$cache->set($key, $index, $db->schemaCacheDuration, new GroupDependency([
'group' => $this->getCacheGroup(),
]));
}
}
return $this->_indexes[$name] = $index;
}
}
return $this->_indexes[$name] = $index = $this->loadIndexSchema($realName);
}
/**
* Returns the cache key for the specified index name.
* @param string $name the index name
* @return mixed the cache key
*/
protected function getCacheKey($name)
{
return [
__CLASS__,
$this->db->dsn,
$this->db->username,
$name,
];
}
/**
* Returns the cache group name.
* This allows [[refresh()]] to invalidate all cached index schemas.
* @return string the cache group name
*/
protected function getCacheGroup()
{
return md5(serialize([
__CLASS__,
$this->db->dsn,
$this->db->username,
]));
}
/**
* Returns the metadata for all indexes in the database.
* @param boolean $refresh whether to fetch the latest available index schemas. If this is false,
* cached data may be returned if available.
* @return IndexSchema[] the metadata for all indexes in the Sphinx.
* Each array element is an instance of [[IndexSchema]] or its child class.
*/
public function getIndexSchemas($refresh = false)
{
$indexes = [];
foreach ($this->getIndexNames($refresh) as $name) {
if (($index = $this->getIndexSchema($name, $refresh)) !== null) {
$indexes[] = $index;
}
}
return $indexes;
}
/**
* Returns all index names in the Sphinx.
* @param boolean $refresh whether to fetch the latest available index names. If this is false,
* index names fetched previously (if available) will be returned.
* @return string[] all index names in the Sphinx.
*/
public function getIndexNames($refresh = false)
{
if (!isset($this->_indexNames) || $refresh) {
$this->initIndexesInfo();
}
return $this->_indexNames;
}
/**
* Returns all index types in the Sphinx.
* @param boolean $refresh whether to fetch the latest available index types. If this is false,
* index types fetched previously (if available) will be returned.
* @return array all index types in the Sphinx in format: index name => index type.
*/
public function getIndexTypes($refresh = false)
{
if (!isset($this->_indexTypes) || $refresh) {
$this->initIndexesInfo();
}
return $this->_indexTypes;
}
/**
* Initializes information about name and type of all index in the Sphinx.
*/
protected function initIndexesInfo()
{
$this->_indexNames = [];
$this->_indexTypes = [];
$indexes = $this->findIndexes();
foreach ($indexes as $index) {
$indexName = $index['Index'];
$this->_indexNames[] = $indexName;
$this->_indexTypes[$indexName] = $index['Type'];
}
}
/**
* Returns all index names in the Sphinx.
* @return array all index names in the Sphinx.
*/
protected function findIndexes()
{
$sql = 'SHOW TABLES';
return $this->db->createCommand($sql)->queryAll();
}
/**
* @return QueryBuilder the query builder for this connection.
*/
public function getQueryBuilder()
{
if ($this->_builder === null) {
$this->_builder = $this->createQueryBuilder();
}
return $this->_builder;
}
/**
* Determines the PDO type for the given PHP data value.
* @param mixed $data the data whose PDO type is to be determined
* @return integer the PDO type
* @see http://www.php.net/manual/en/pdo.constants.php
*/
public function getPdoType($data)
{
static $typeMap = [
// php type => PDO type
'boolean' => \PDO::PARAM_BOOL,
'integer' => \PDO::PARAM_INT,
'string' => \PDO::PARAM_STR,
'resource' => \PDO::PARAM_LOB,
'NULL' => \PDO::PARAM_NULL,
];
$type = gettype($data);
return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR;
}
/**
* Refreshes the schema.
* This method cleans up all cached index schemas so that they can be re-created later
* to reflect the Sphinx schema change.
*/
public function refresh()
{
/** @var $cache Cache */
$cache = is_string($this->db->schemaCache) ? Yii::$app->getComponent($this->db->schemaCache) : $this->db->schemaCache;
if ($this->db->enableSchemaCache && $cache instanceof Cache) {
GroupDependency::invalidate($cache, $this->getCacheGroup());
}
$this->_indexNames = [];
$this->_indexes = [];
}
/**
* Creates a query builder for the Sphinx.
* @return QueryBuilder query builder instance
*/
public function createQueryBuilder()
{
return new QueryBuilder($this->db);
}
/**
* Quotes a string value for use in a query.
* Note that if the parameter is not a string, it will be returned without change.
* @param string $str string to be quoted
* @return string the properly quoted string
* @see http://www.php.net/manual/en/function.PDO-quote.php
*/
public function quoteValue($str)
{
if (!is_string($str)) {
return $str;
}
$this->db->open();
return $this->db->pdo->quote($str);
}
/**
* Quotes a index name for use in a query.
* If the index name contains schema prefix, the prefix will also be properly quoted.
* If the index name is already quoted or contains '(' or '{{',
* then this method will do nothing.
* @param string $name index name
* @return string the properly quoted index name
* @see quoteSimpleTableName
*/
public function quoteIndexName($name)
{
if (strpos($name, '(') !== false || strpos($name, '{{') !== false) {
return $name;
}
return $this->quoteSimpleIndexName($name);
}
/**
* Quotes a column name for use in a query.
* If the column name contains prefix, the prefix will also be properly quoted.
* If the column name is already quoted or contains '(', '[[' or '{{',
* then this method will do nothing.
* @param string $name column name
* @return string the properly quoted column name
* @see quoteSimpleColumnName
*/
public function quoteColumnName($name)
{
if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
return $name;
}
if (($pos = strrpos($name, '.')) !== false) {
$prefix = $this->quoteIndexName(substr($name, 0, $pos)) . '.';
$name = substr($name, $pos + 1);
} else {
$prefix = '';
}
return $prefix . $this->quoteSimpleColumnName($name);
}
/**
* Quotes a index name for use in a query.
* A simple index name has no schema prefix.
* @param string $name index name
* @return string the properly quoted index name
*/
public function quoteSimpleIndexName($name)
{
return strpos($name, "`") !== false ? $name : "`" . $name . "`";
}
/**
* Quotes a column name for use in a query.
* A simple column name has no prefix.
* @param string $name column name
* @return string the properly quoted column name
*/
public function quoteSimpleColumnName($name)
{
return strpos($name, '`') !== false || $name === '*' ? $name : '`' . $name . '`';
}
/**
* Returns the actual name of a given index name.
* This method will strip off curly brackets from the given index name
* and replace the percentage character '%' with [[Connection::indexPrefix]].
* @param string $name the index name to be converted
* @return string the real name of the given index name
*/
public function getRawIndexName($name)
{
if (strpos($name, '{{') !== false) {
$name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
return str_replace('%', $this->db->tablePrefix, $name);
} else {
return $name;
}
}
/**
* Extracts the PHP type from abstract DB type.
* @param ColumnSchema $column the column schema information
* @return string PHP type name
*/
protected function getColumnPhpType($column)
{
static $typeMap = [ // abstract type => php type
'smallint' => 'integer',
'integer' => 'integer',
'bigint' => 'integer',
'boolean' => 'boolean',
'float' => 'double',
];
if (isset($typeMap[$column->type])) {
if ($column->type === 'bigint') {
return PHP_INT_SIZE == 8 ? 'integer' : 'string';
} elseif ($column->type === 'integer') {
return PHP_INT_SIZE == 4 ? 'string' : 'integer';
} else {
return $typeMap[$column->type];
}
} else {
return 'string';
}
}
/**
* Collects the metadata of index columns.
* @param IndexSchema $index the index metadata
* @return boolean whether the index exists in the database
* @throws \Exception if DB query fails
*/
protected function findColumns($index)
{
$sql = 'DESCRIBE ' . $this->quoteSimpleIndexName($index->name);
try {
$columns = $this->db->createCommand($sql)->queryAll();
} catch (\Exception $e) {
$previous = $e->getPrevious();
if ($previous instanceof \PDOException && $previous->getCode() == '42S02') {
// index does not exist
return false;
}
throw $e;
}
foreach ($columns as $info) {
$column = $this->loadColumnSchema($info);
$index->columns[$column->name] = $column;
if ($column->isPrimaryKey) {
$index->primaryKey = $column->name;
}
}
return true;
}
/**
* Loads the column information into a [[ColumnSchema]] object.
* @param array $info column information
* @return ColumnSchema the column schema object
*/
protected function loadColumnSchema($info)
{
$column = new ColumnSchema;
$column->name = $info['Field'];
$column->dbType = $info['Type'];
$column->isPrimaryKey = ($column->name == 'id');
$type = $info['Type'];
if (isset($this->typeMap[$type])) {
$column->type = $this->typeMap[$type];
} else {
$column->type = self::TYPE_STRING;
}
$column->isField = ($type == 'field');
$column->isAttribute = !$column->isField;
$column->isMva = ($type == 'mva');
$column->phpType = $this->getColumnPhpType($column);
return $column;
}
}

29
extensions/sphinx/composer.json

@ -0,0 +1,29 @@
{
"name": "yiisoft/yii2-sphinx",
"description": "Sphinx full text search engine extension for the Yii framework",
"keywords": ["yii", "sphinx", "search", "fulltext"],
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2/issues?state=open",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
"source": "https://github.com/yiisoft/yii2"
},
"authors": [
{
"name": "Paul Klimov",
"email": "klimov.paul@gmail.com"
}
],
"minimum-stability": "dev",
"require": {
"yiisoft/yii2": "*",
"ext-pdo": "*",
"ext-pdo_mysql": "*"
},
"autoload": {
"psr-0": { "yii\\sphinx\\": "" }
}
}

13
tests/unit/data/config.php

@ -34,4 +34,17 @@ return [
'password' => null,
],
],
'sphinx' => [
'sphinx' => [
'dsn' => 'mysql:host=127.0.0.1;port=9306;',
'username' => '',
'password' => '',
],
'db' => [
'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest',
'username' => 'travis',
'password' => '',
'fixture' => __DIR__ . '/sphinx/source.sql',
],
]
];

16
tests/unit/data/sphinx/ar/ActiveRecord.php

@ -0,0 +1,16 @@
<?php
namespace yiiunit\data\sphinx\ar;
/**
* Test Sphinx ActiveRecord class
*/
class ActiveRecord extends \yii\sphinx\ActiveRecord
{
public static $db;
public static function getDb()
{
return self::$db;
}
}

25
tests/unit/data/sphinx/ar/ArticleDb.php

@ -0,0 +1,25 @@
<?php
namespace yiiunit\data\sphinx\ar;
use yii\sphinx\ActiveRelation;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
class ArticleDb extends ActiveRecordDb
{
public static function tableName()
{
return 'yii2_test_article';
}
public function getIndex()
{
$config = [
'modelClass' => ArticleIndex::className(),
'primaryModel' => $this,
'link' => ['id' => 'id'],
'multiple' => false,
];
return new ActiveRelation($config);
}
}

35
tests/unit/data/sphinx/ar/ArticleIndex.php

@ -0,0 +1,35 @@
<?php
namespace yiiunit\data\sphinx\ar;
use yii\db\ActiveRelation;
class ArticleIndex extends ActiveRecord
{
public $custom_column;
public static function indexName()
{
return 'yii2_test_article_index';
}
public static function favoriteAuthor($query)
{
$query->andWhere('author_id=1');
}
public function getSource()
{
return $this->hasOne('db', ArticleDb::className(), ['id' => 'id']);
}
public function getTags()
{
return $this->hasMany('db', TagDb::className(), ['id' => 'tag']);
}
public function getSnippetSource()
{
return $this->source->content;
}
}

13
tests/unit/data/sphinx/ar/ItemDb.php

@ -0,0 +1,13 @@
<?php
namespace yiiunit\data\sphinx\ar;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
class ItemDb extends ActiveRecordDb
{
public static function tableName()
{
return 'yii2_test_item';
}
}

11
tests/unit/data/sphinx/ar/ItemIndex.php

@ -0,0 +1,11 @@
<?php
namespace yiiunit\data\sphinx\ar;
class ItemIndex extends ActiveRecord
{
public static function indexName()
{
return 'yii2_test_item_index';
}
}

11
tests/unit/data/sphinx/ar/RuntimeIndex.php

@ -0,0 +1,11 @@
<?php
namespace yiiunit\data\sphinx\ar;
class RuntimeIndex extends ActiveRecord
{
public static function indexName()
{
return 'yii2_test_rt_index';
}
}

12
tests/unit/data/sphinx/ar/TagDb.php

@ -0,0 +1,12 @@
<?php
namespace yiiunit\data\sphinx\ar;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
class TagDb extends ActiveRecordDb
{
public static function tableName()
{
return 'yii2_test_tag';
}
}

59
tests/unit/data/sphinx/source.sql

@ -0,0 +1,59 @@
/**
* This is the MySQL database schema for creation of the test Sphinx index sources.
*/
DROP TABLE IF EXISTS yii2_test_article;
DROP TABLE IF EXISTS yii2_test_item;
DROP TABLE IF EXISTS yii2_test_tag;
DROP TABLE IF EXISTS yii2_test_article_tag;
CREATE TABLE IF NOT EXISTS `yii2_test_article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`content` text NOT NULL,
`author_id` int(11) NOT NULL,
`create_date` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;
CREATE TABLE IF NOT EXISTS `yii2_test_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
`category_id` int(11) NOT NULL,
`price` float NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;
CREATE TABLE IF NOT EXISTS `yii2_test_tag` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=5;
CREATE TABLE IF NOT EXISTS `yii2_test_article_tag` (
`article_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`article_id`,`tag_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `yii2_test_article` (`id`, `title`, `content`, `author_id`, `create_date`) VALUES
(1, 'About cats', 'This article is about cats', 1, '2013-10-23 00:00:00'),
(2, 'About dogs', 'This article is about dogs', 2, '2013-11-15 00:00:00');
INSERT INTO `yii2_test_item` (`id`, `name`, `description`, `category_id`, `price`) VALUES
(1, 'pencil', 'Simple pencil', 1, 2.5),
(2, 'table', 'Wooden table', 2, 100);
INSERT INTO `yii2_test_tag` (`id`, `name`) VALUES
(1, 'tag1'),
(2, 'tag2'),
(3, 'tag3'),
(4, 'tag4');
INSERT INTO `yii2_test_article_tag` (`article_id`, `tag_id`) VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 3),
(2, 4);

125
tests/unit/data/sphinx/sphinx.conf

@ -0,0 +1,125 @@
# Sphinx configuration for the unit tests
#
# Setup test environment:
# - initialize test database source:
# mysql -D yii2test -u test < /path/to/yii/tests/unit/data/sphinx/source.sql
# - setup test Sphinx indexes:
# indexer --config /path/to/yii/tests/unit/data/sphinx/sphinx.conf --all [--rotate]
# - run the "searchd" daemon:
# searchd --config /path/to/yii/tests/unit/data/sphinx/sphinx.conf
source yii2_test_article_src
{
type = mysql
sql_host = localhost
sql_user =
sql_pass =
sql_db = yii2test
sql_port = 3306 # optional, default is 3306
sql_query = \
SELECT *, UNIX_TIMESTAMP(create_date) AS add_date \
FROM yii2_test_article
sql_attr_uint = id
sql_attr_uint = author_id
sql_attr_timestamp = add_date
sql_attr_multi = uint tag from query; SELECT article_id AS id, tag_id AS tag FROM yii2_test_article_tag
sql_query_info = SELECT * FROM yii2_test_article WHERE id=$id
}
source yii2_test_item_src
{
type = mysql
sql_host = localhost
sql_user =
sql_pass =
sql_db = yii2test
sql_port = 3306 # optional, default is 3306
sql_query = \
SELECT *, CURRENT_TIMESTAMP() AS add_date \
FROM yii2_test_item \
WHERE id <= 100
sql_attr_uint = id
sql_attr_uint = category_id
sql_attr_float = price
sql_attr_timestamp = add_date
sql_query_info = SELECT * FROM yii2_test_item WHERE id=$id
}
source yii2_test_item_delta_src : yii2_test_item_src
{
sql_query = \
SELECT *, CURRENT_TIMESTAMP() AS add_date \
FROM yii2_test_item \
WHERE id > 100
}
index yii2_test_article_index
{
source = yii2_test_article_src
path = /var/lib/sphinx/yii2_test_article
docinfo = extern
charset_type = sbcs
}
index yii2_test_item_index
{
source = yii2_test_item_src
path = /var/lib/sphinx/yii2_test_item
docinfo = extern
charset_type = sbcs
}
index yii2_test_item_delta_index : yii2_test_item_index
{
source = yii2_test_item_delta_src
path = /var/lib/sphinx/yii2_test_item_delta
}
index yii2_test_rt_index
{
type = rt
path = /var/lib/sphinx/yii2_test_rt
rt_field = title
rt_field = content
rt_attr_uint = type_id
rt_attr_multi = category
}
indexer
{
mem_limit = 32M
}
searchd
{
listen = 127.0.0.1:9312
listen = 9306:mysql41
log = /var/log/sphinx/searchd.log
query_log = /var/log/sphinx/query.log
read_timeout = 5
max_children = 30
pid_file = /var/run/sphinx/searchd.pid
max_matches = 1000
seamless_rotate = 1
preopen_indexes = 1
unlink_old = 1
workers = threads # for RT to work
binlog_path = /var/lib/sphinx
}

66
tests/unit/extensions/sphinx/ActiveDataProviderTest.php

@ -0,0 +1,66 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\data\ActiveDataProvider;
use yii\sphinx\Query;
use yiiunit\data\sphinx\ar\ActiveRecord;
use yiiunit\data\sphinx\ar\ArticleIndex;
/**
* @group sphinx
*/
class ActiveDataProviderTest extends SphinxTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
}
// Tests :
public function testQuery()
{
$query = new Query;
$query->from('yii2_test_article_index');
$provider = new ActiveDataProvider([
'query' => $query,
'db' => $this->getConnection(),
]);
$models = $provider->getModels();
$this->assertEquals(2, count($models));
$provider = new ActiveDataProvider([
'query' => $query,
'db' => $this->getConnection(),
'pagination' => [
'pageSize' => 1,
]
]);
$models = $provider->getModels();
$this->assertEquals(1, count($models));
}
public function testActiveQuery()
{
$provider = new ActiveDataProvider([
'query' => ArticleIndex::find()->orderBy('id ASC'),
]);
$models = $provider->getModels();
$this->assertEquals(2, count($models));
$this->assertTrue($models[0] instanceof ArticleIndex);
$this->assertTrue($models[1] instanceof ArticleIndex);
$this->assertEquals([1, 2], $provider->getKeys());
$provider = new ActiveDataProvider([
'query' => ArticleIndex::find(),
'pagination' => [
'pageSize' => 1,
]
]);
$models = $provider->getModels();
$this->assertEquals(1, count($models));
}
}

238
tests/unit/extensions/sphinx/ActiveRecordTest.php

@ -0,0 +1,238 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\sphinx\ActiveQuery;
use yiiunit\data\sphinx\ar\ActiveRecord;
use yiiunit\data\sphinx\ar\ArticleIndex;
use yiiunit\data\sphinx\ar\RuntimeIndex;
/**
* @group sphinx
*/
class ActiveRecordTest extends SphinxTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
}
protected function tearDown()
{
$this->truncateRuntimeIndex('yii2_test_rt_index');
parent::tearDown();
}
// Tests :
public function testFind()
{
// find one
$result = ArticleIndex::find();
$this->assertTrue($result instanceof ActiveQuery);
$article = $result->one();
$this->assertTrue($article instanceof ArticleIndex);
// find all
$articles = ArticleIndex::find()->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles[0] instanceof ArticleIndex);
$this->assertTrue($articles[1] instanceof ArticleIndex);
// find fulltext
$articles = ArticleIndex::find('cats');
$this->assertEquals(1, count($articles));
$this->assertTrue($articles[0] instanceof ArticleIndex);
$this->assertEquals(1, $articles[0]->id);
// find by column values
$article = ArticleIndex::find(['id' => 2, 'author_id' => 2]);
$this->assertTrue($article instanceof ArticleIndex);
$this->assertEquals(2, $article->id);
$this->assertEquals(2, $article->author_id);
$article = ArticleIndex::find(['id' => 2, 'author_id' => 1]);
$this->assertNull($article);
// find by attributes
$article = ArticleIndex::find()->where(['author_id' => 2])->one();
$this->assertTrue($article instanceof ArticleIndex);
$this->assertEquals(2, $article->id);
// find custom column
$article = ArticleIndex::find()->select(['*', '(5*2) AS custom_column'])
->where(['author_id' => 1])->one();
$this->assertEquals(1, $article->id);
$this->assertEquals(10, $article->custom_column);
// find count, sum, average, min, max, scalar
$this->assertEquals(2, ArticleIndex::find()->count());
$this->assertEquals(1, ArticleIndex::find()->where('id=1')->count());
$this->assertEquals(3, ArticleIndex::find()->sum('id'));
$this->assertEquals(1.5, ArticleIndex::find()->average('id'));
$this->assertEquals(1, ArticleIndex::find()->min('id'));
$this->assertEquals(2, ArticleIndex::find()->max('id'));
$this->assertEquals(2, ArticleIndex::find()->select('COUNT(*)')->scalar());
// scope
$this->assertEquals(1, ArticleIndex::find()->favoriteAuthor()->count());
// asArray
$article = ArticleIndex::find()->where('id=2')->asArray()->one();
$this->assertEquals([
'id' => '2',
'author_id' => '2',
'add_date' => '1384466400',
'tag' => '3,4',
], $article);
// indexBy
$articles = ArticleIndex::find()->indexBy('author_id')->orderBy('id DESC')->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles['1'] instanceof ArticleIndex);
$this->assertTrue($articles['2'] instanceof ArticleIndex);
// indexBy callable
$articles = ArticleIndex::find()->indexBy(function ($article) {
return $article->id . '-' . $article->author_id;
})->orderBy('id DESC')->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles['1-1'] instanceof ArticleIndex);
$this->assertTrue($articles['2-2'] instanceof ArticleIndex);
}
public function testFindBySql()
{
// find one
$article = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index ORDER BY id DESC')->one();
$this->assertTrue($article instanceof ArticleIndex);
$this->assertEquals(2, $article->author_id);
// find all
$articles = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index')->all();
$this->assertEquals(2, count($articles));
// find with parameter binding
$article = ArticleIndex::findBySql('SELECT * FROM yii2_test_article_index WHERE id=:id', [':id' => 2])->one();
$this->assertTrue($article instanceof ArticleIndex);
$this->assertEquals(2, $article->author_id);
}
public function testInsert()
{
$record = new RuntimeIndex;
$record->id = 15;
$record->title = 'test title';
$record->content = 'test content';
$record->type_id = 7;
$record->category = [1, 2];
$this->assertTrue($record->isNewRecord);
$record->save();
$this->assertEquals(15, $record->id);
$this->assertFalse($record->isNewRecord);
}
/**
* @depends testInsert
*/
public function testUpdate()
{
$record = new RuntimeIndex;
$record->id = 2;
$record->title = 'test title';
$record->content = 'test content';
$record->type_id = 7;
$record->category = [1, 2];
$record->save();
// save
$record = RuntimeIndex::find(['id' => 2]);
$this->assertTrue($record instanceof RuntimeIndex);
$this->assertEquals(7, $record->type_id);
$this->assertFalse($record->isNewRecord);
$record->type_id = 9;
$record->save();
$this->assertEquals(9, $record->type_id);
$this->assertFalse($record->isNewRecord);
$record2 = RuntimeIndex::find(['id' => 2]);
$this->assertEquals(9, $record2->type_id);
// replace
$query = 'replace';
$rows = RuntimeIndex::find($query);
$this->assertEmpty($rows);
$record = RuntimeIndex::find(['id' => 2]);
$record->content = 'Test content with ' . $query;
$record->save();
$rows = RuntimeIndex::find($query);
$this->assertNotEmpty($rows);
// updateAll
$pk = ['id' => 2];
$ret = RuntimeIndex::updateAll(['type_id' => 55], $pk);
$this->assertEquals(1, $ret);
$record = RuntimeIndex::find($pk);
$this->assertEquals(55, $record->type_id);
}
/**
* @depends testInsert
*/
public function testDelete()
{
// delete
$record = new RuntimeIndex;
$record->id = 2;
$record->title = 'test title';
$record->content = 'test content';
$record->type_id = 7;
$record->category = [1, 2];
$record->save();
$record = RuntimeIndex::find(['id' => 2]);
$record->delete();
$record = RuntimeIndex::find(['id' => 2]);
$this->assertNull($record);
// deleteAll
$record = new RuntimeIndex;
$record->id = 2;
$record->title = 'test title';
$record->content = 'test content';
$record->type_id = 7;
$record->category = [1, 2];
$record->save();
$ret = RuntimeIndex::deleteAll('id = 2');
$this->assertEquals(1, $ret);
$records = RuntimeIndex::find()->all();
$this->assertEquals(0, count($records));
}
public function testCallSnippets()
{
$query = 'pencil';
$source = 'Some data sentence about ' . $query;
$snippet = ArticleIndex::callSnippets($source, $query);
$this->assertNotEmpty($snippet, 'Unable to call snippets!');
$this->assertContains('<b>' . $query . '</b>', $snippet, 'Query not present in the snippet!');
$rows = ArticleIndex::callSnippets([$source], $query);
$this->assertNotEmpty($rows, 'Unable to call snippets!');
$this->assertContains('<b>' . $query . '</b>', $rows[0], 'Query not present in the snippet!');
}
public function testCallKeywords()
{
$text = 'table pencil';
$rows = ArticleIndex::callKeywords($text);
$this->assertNotEmpty($rows, 'Unable to call keywords!');
$this->assertArrayHasKey('tokenized', $rows[0], 'No tokenized keyword!');
$this->assertArrayHasKey('normalized', $rows[0], 'No normalized keyword!');
}
}

44
tests/unit/extensions/sphinx/ActiveRelationTest.php

@ -0,0 +1,44 @@
<?php
namespace yiiunit\extensions\sphinx;
use yiiunit\data\sphinx\ar\ActiveRecord;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
use yiiunit\data\sphinx\ar\ArticleIndex;
use yiiunit\data\sphinx\ar\ArticleDb;
/**
* @group sphinx
*/
class ActiveRelationTest extends SphinxTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
ActiveRecordDb::$db = $this->getDbConnection();
}
// Tests :
public function testFindLazy()
{
/** @var ArticleDb $article */
$article = ArticleDb::find(['id' => 2]);
$this->assertFalse($article->isRelationPopulated('index'));
$index = $article->index;
$this->assertTrue($article->isRelationPopulated('index'));
$this->assertTrue($index instanceof ArticleIndex);
$this->assertEquals(1, count($article->populatedRelations));
}
public function testFindEager()
{
$articles = ArticleDb::find()->with('index')->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles[0]->isRelationPopulated('index'));
$this->assertTrue($articles[1]->isRelationPopulated('index'));
$this->assertTrue($articles[0]->index instanceof ArticleIndex);
$this->assertTrue($articles[1]->index instanceof ArticleIndex);
}
}

55
tests/unit/extensions/sphinx/ColumnSchemaTest.php

@ -0,0 +1,55 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\sphinx\ColumnSchema;
/**
* @group sphinx
*/
class ColumnSchemaTest extends SphinxTestCase
{
/**
* Data provider for [[testTypeCast]]
* @return array test data.
*/
public function dataProviderTypeCast()
{
return [
[
'integer',
'integer',
5,
5
],
[
'integer',
'integer',
'5',
5
],
[
'string',
'string',
5,
'5'
],
];
}
/**
* @dataProvider dataProviderTypeCast
*
* @param $type
* @param $phpType
* @param $value
* @param $expectedResult
*/
public function testTypeCast($type, $phpType, $value, $expectedResult)
{
$columnSchema = new ColumnSchema();
$columnSchema->type = $type;
$columnSchema->phpType = $phpType;
$this->assertEquals($expectedResult, $columnSchema->typecast($value));
}
}

409
tests/unit/extensions/sphinx/CommandTest.php

@ -0,0 +1,409 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\db\DataReader;
/**
* @group sphinx
*/
class CommandTest extends SphinxTestCase
{
protected function tearDown()
{
$this->truncateRuntimeIndex('yii2_test_rt_index');
parent::tearDown();
}
// Tests :
public function testConstruct()
{
$db = $this->getConnection(false);
// null
$command = $db->createCommand();
$this->assertEquals(null, $command->sql);
// string
$sql = 'SELECT * FROM yii2_test_item_index';
$params = [
'name' => 'value'
];
$command = $db->createCommand($sql, $params);
$this->assertEquals($sql, $command->sql);
$this->assertEquals($params, $command->params);
}
public function testGetSetSql()
{
$db = $this->getConnection(false);
$sql = 'SELECT * FROM yii2_test_item_index';
$command = $db->createCommand($sql);
$this->assertEquals($sql, $command->sql);
$sql2 = 'SELECT * FROM yii2_test_item_index';
$command->sql = $sql2;
$this->assertEquals($sql2, $command->sql);
}
public function testAutoQuoting()
{
$db = $this->getConnection(false);
$sql = 'SELECT [[id]], [[t.name]] FROM {{yii2_test_item_index}} t';
$command = $db->createCommand($sql);
$this->assertEquals("SELECT `id`, `t`.`name` FROM `yii2_test_item_index` t", $command->sql);
}
public function testPrepareCancel()
{
$db = $this->getConnection(false);
$command = $db->createCommand('SELECT * FROM yii2_test_item_index');
$this->assertEquals(null, $command->pdoStatement);
$command->prepare();
$this->assertNotEquals(null, $command->pdoStatement);
$command->cancel();
$this->assertEquals(null, $command->pdoStatement);
}
public function testExecute()
{
$db = $this->getConnection();
$sql = 'SELECT COUNT(*) FROM yii2_test_item_index WHERE MATCH(\'wooden\')';
$command = $db->createCommand($sql);
$this->assertEquals(1, $command->queryScalar());
$command = $db->createCommand('bad SQL');
$this->setExpectedException('\yii\db\Exception');
$command->execute();
}
public function testQuery()
{
$db = $this->getConnection();
// query
$sql = 'SELECT * FROM yii2_test_item_index';
$reader = $db->createCommand($sql)->query();
$this->assertTrue($reader instanceof DataReader);
// queryAll
$rows = $db->createCommand('SELECT * FROM yii2_test_item_index')->queryAll();
$this->assertEquals(2, count($rows));
$row = $rows[1];
$this->assertEquals(2, $row['id']);
$this->assertEquals(2, $row['category_id']);
$rows = $db->createCommand('SELECT * FROM yii2_test_item_index WHERE id=10')->queryAll();
$this->assertEquals([], $rows);
// queryOne
$sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC';
$row = $db->createCommand($sql)->queryOne();
$this->assertEquals(1, $row['id']);
$this->assertEquals(1, $row['category_id']);
$sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC';
$command = $db->createCommand($sql);
$command->prepare();
$row = $command->queryOne();
$this->assertEquals(1, $row['id']);
$this->assertEquals(1, $row['category_id']);
$sql = 'SELECT * FROM yii2_test_item_index WHERE id=10';
$command = $db->createCommand($sql);
$this->assertFalse($command->queryOne());
// queryColumn
$sql = 'SELECT * FROM yii2_test_item_index';
$column = $db->createCommand($sql)->queryColumn();
$this->assertEquals(range(1, 2), $column);
$command = $db->createCommand('SELECT id FROM yii2_test_item_index WHERE id=10');
$this->assertEquals([], $command->queryColumn());
// queryScalar
$sql = 'SELECT * FROM yii2_test_item_index ORDER BY id ASC';
$this->assertEquals($db->createCommand($sql)->queryScalar(), 1);
$sql = 'SELECT id FROM yii2_test_item_index ORDER BY id ASC';
$command = $db->createCommand($sql);
$command->prepare();
$this->assertEquals(1, $command->queryScalar());
$command = $db->createCommand('SELECT id FROM yii2_test_item_index WHERE id=10');
$this->assertFalse($command->queryScalar());
$command = $db->createCommand('bad SQL');
$this->setExpectedException('\yii\db\Exception');
$command->query();
}
/**
* @depends testQuery
*/
public function testInsert()
{
$db = $this->getConnection();
$command = $db->createCommand()->insert('yii2_test_rt_index', [
'title' => 'Test title',
'content' => 'Test content',
'type_id' => 2,
'category' => [1, 2],
'id' => 1,
]);
$this->assertEquals(1, $command->execute(), 'Unable to execute insert!');
$rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals(1, count($rows), 'No row inserted!');
}
/**
* @depends testInsert
*/
public function testBatchInsert()
{
$db = $this->getConnection();
$command = $db->createCommand()->batchInsert(
'yii2_test_rt_index',
[
'title',
'content',
'type_id',
'category',
'id',
],
[
[
'Test title 1',
'Test content 1',
1,
[1, 2],
1,
],
[
'Test title 2',
'Test content 2',
2,
[3, 4],
2,
],
]
);
$this->assertEquals(2, $command->execute(), 'Unable to execute batch insert!');
$rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals(2, count($rows), 'No rows inserted!');
}
/**
* @depends testInsert
*/
public function testReplace()
{
$db = $this->getConnection();
$command = $db->createCommand()->replace('yii2_test_rt_index', [
'title' => 'Test title',
'content' => 'Test content',
'type_id' => 2,
'category' => [1, 2],
'id' => 1,
]);
$this->assertEquals(1, $command->execute(), 'Unable to execute replace!');
$rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals(1, count($rows), 'No row inserted!');
$newTypeId = 5;
$command = $db->createCommand()->replace('yii2_test_rt_index',[
'type_id' => $newTypeId,
'category' => [3, 4],
'id' => 1,
]);
$this->assertEquals(1, $command->execute(), 'Unable to update via replace!');
list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!');
}
/**
* @depends testReplace
*/
public function testBatchReplace()
{
$db = $this->getConnection();
$command = $db->createCommand()->batchReplace(
'yii2_test_rt_index',
[
'title',
'content',
'type_id',
'category',
'id',
],
[
[
'Test title 1',
'Test content 1',
1,
[1, 2],
1,
],
[
'Test title 2',
'Test content 2',
2,
[3, 4],
2,
],
]
);
$this->assertEquals(2, $command->execute(), 'Unable to execute batch replace!');
$rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals(2, count($rows), 'No rows inserted!');
$newTypeId = 5;
$command = $db->createCommand()->replace('yii2_test_rt_index',[
'type_id' => $newTypeId,
'id' => 1,
]);
$this->assertEquals(1, $command->execute(), 'Unable to update via replace!');
list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!');
}
/**
* @depends testInsert
*/
public function testUpdate()
{
$db = $this->getConnection();
$db->createCommand()->insert('yii2_test_rt_index', [
'title' => 'Test title',
'content' => 'Test content',
'type_id' => 2,
'id' => 1,
])->execute();
$newTypeId = 5;
$command = $db->createCommand()->update(
'yii2_test_rt_index',
[
'type_id' => $newTypeId,
'category' => [3, 4],
],
'id = 1'
);
$this->assertEquals(1, $command->execute(), 'Unable to execute update!');
list($row) = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals($newTypeId, $row['type_id'], 'Unable to update attribute value!');
}
/**
* @depends testUpdate
*/
public function testUpdateWithOptions()
{
$db = $this->getConnection();
$db->createCommand()->insert('yii2_test_rt_index', [
'title' => 'Test title',
'content' => 'Test content',
'type_id' => 2,
'id' => 1,
])->execute();
$newTypeId = 5;
$command = $db->createCommand()->update(
'yii2_test_rt_index',
[
'type_id' => $newTypeId,
'non_existing_attribute' => 10,
],
'id = 1',
[],
[
'ignore_nonexistent_columns' => 1
]
);
$this->assertEquals(1, $command->execute(), 'Unable to execute update!');
}
/**
* @depends testInsert
*/
public function testDelete()
{
$db = $this->getConnection();
$db->createCommand()->insert('yii2_test_rt_index', [
'title' => 'Test title',
'content' => 'Test content',
'type_id' => 2,
'id' => 1,
])->execute();
$command = $db->createCommand()->delete('yii2_test_rt_index', 'id = 1');
$this->assertEquals(1, $command->execute(), 'Unable to execute delete!');
$rows = $db->createCommand('SELECT * FROM yii2_test_rt_index')->queryAll();
$this->assertEquals(0, count($rows), 'Unable to delete record!');
}
/**
* @depends testQuery
*/
public function testCallSnippets()
{
$db = $this->getConnection();
$query = 'pencil';
$source = 'Some data sentence about ' . $query;
$rows = $db->createCommand()->callSnippets('yii2_test_item_index', $source, $query)->queryColumn();
$this->assertNotEmpty($rows, 'Unable to call snippets!');
$this->assertContains('<b>' . $query . '</b>', $rows[0], 'Query not present in the snippet!');
$rows = $db->createCommand()->callSnippets('yii2_test_item_index', [$source], $query)->queryColumn();
$this->assertNotEmpty($rows, 'Unable to call snippets for array source!');
$options = [
'before_match' => '[',
'after_match' => ']',
'limit' => 20,
];
$snippet = $db->createCommand()->callSnippets('yii2_test_item_index', $source, $query, $options)->queryScalar();
$this->assertContains($options['before_match'] . $query . $options['after_match'], $snippet, 'Unable to apply options!');
}
/**
* @depends testQuery
*/
public function testCallKeywords()
{
$db = $this->getConnection();
$text = 'table pencil';
$rows = $db->createCommand()->callKeywords('yii2_test_item_index', $text)->queryAll();
$this->assertNotEmpty($rows, 'Unable to call keywords!');
$this->assertArrayHasKey('tokenized', $rows[0], 'No tokenized keyword!');
$this->assertArrayHasKey('normalized', $rows[0], 'No normalized keyword!');
$text = 'table pencil';
$rows = $db->createCommand()->callKeywords('yii2_test_item_index', $text, true)->queryAll();
$this->assertNotEmpty($rows, 'Unable to call keywords with statistic!');
$this->assertArrayHasKey('docs', $rows[0], 'No docs!');
$this->assertArrayHasKey('hits', $rows[0], 'No hits!');
}
}

42
tests/unit/extensions/sphinx/ConnectionTest.php

@ -0,0 +1,42 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\sphinx\Connection;
/**
* @group sphinx
*/
class ConnectionTest extends SphinxTestCase
{
public function testConstruct()
{
$connection = $this->getConnection(false);
$params = $this->sphinxConfig;
$this->assertEquals($params['dsn'], $connection->dsn);
$this->assertEquals($params['username'], $connection->username);
$this->assertEquals($params['password'], $connection->password);
}
public function testOpenClose()
{
$connection = $this->getConnection(false, false);
$this->assertFalse($connection->isActive);
$this->assertEquals(null, $connection->pdo);
$connection->open();
$this->assertTrue($connection->isActive);
$this->assertTrue($connection->pdo instanceof \PDO);
$connection->close();
$this->assertFalse($connection->isActive);
$this->assertEquals(null, $connection->pdo);
$connection = new Connection;
$connection->dsn = 'unknown::memory:';
$this->setExpectedException('yii\db\Exception');
$connection->open();
}
}

74
tests/unit/extensions/sphinx/ExternalActiveRelationTest.php

@ -0,0 +1,74 @@
<?php
namespace yiiunit\extensions\sphinx;
use yiiunit\data\sphinx\ar\ActiveRecord;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
use yiiunit\data\sphinx\ar\ArticleIndex;
use yiiunit\data\sphinx\ar\ArticleDb;
use yiiunit\data\sphinx\ar\TagDb;
/**
* @group sphinx
*/
class ExternalActiveRelationTest extends SphinxTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
ActiveRecordDb::$db = $this->getDbConnection();
}
// Tests :
public function testFindLazy()
{
/** @var ArticleIndex $article */
$article = ArticleIndex::find(['id' => 2]);
// has one :
$this->assertFalse($article->isRelationPopulated('source'));
$source = $article->source;
$this->assertTrue($article->isRelationPopulated('source'));
$this->assertTrue($source instanceof ArticleDb);
$this->assertEquals(1, count($article->populatedRelations));
// has many :
/*$this->assertFalse($article->isRelationPopulated('tags'));
$tags = $article->tags;
$this->assertTrue($article->isRelationPopulated('tags'));
$this->assertEquals(3, count($tags));
$this->assertTrue($tags[0] instanceof TagDb);*/
}
public function testFindEager()
{
// has one :
$articles = ArticleIndex::find()->with('source')->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles[0]->isRelationPopulated('source'));
$this->assertTrue($articles[1]->isRelationPopulated('source'));
$this->assertTrue($articles[0]->source instanceof ArticleDb);
$this->assertTrue($articles[1]->source instanceof ArticleDb);
// has many :
/*$articles = ArticleIndex::find()->with('tags')->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles[0]->isRelationPopulated('tags'));
$this->assertTrue($articles[1]->isRelationPopulated('tags'));*/
}
/**
* @depends testFindEager
*/
public function testFindWithSnippets()
{
$articles = ArticleIndex::find()
->match('about')
->with('source')
->snippetByModel()
->all();
$this->assertEquals(2, count($articles));
}
}

187
tests/unit/extensions/sphinx/QueryTest.php

@ -0,0 +1,187 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\sphinx\Query;
/**
* @group sphinx
*/
class QueryTest extends SphinxTestCase
{
public function testSelect()
{
// default
$query = new Query;
$query->select('*');
$this->assertEquals(['*'], $query->select);
$this->assertNull($query->distinct);
$this->assertEquals(null, $query->selectOption);
$query = new Query;
$query->select('id, name', 'something')->distinct(true);
$this->assertEquals(['id', 'name'], $query->select);
$this->assertTrue($query->distinct);
$this->assertEquals('something', $query->selectOption);
}
public function testFrom()
{
$query = new Query;
$query->from('tbl_user');
$this->assertEquals(['tbl_user'], $query->from);
}
public function testMatch()
{
$query = new Query;
$match = 'test match';
$query->match($match);
$this->assertEquals($match, $query->match);
$command = $query->createCommand($this->getConnection(false));
$this->assertContains('MATCH(', $command->getSql(), 'No MATCH operator present!');
$this->assertContains($match, $command->params, 'No match query among params!');
}
public function testWhere()
{
$query = new Query;
$query->where('id = :id', [':id' => 1]);
$this->assertEquals('id = :id', $query->where);
$this->assertEquals([':id' => 1], $query->params);
$query->andWhere('name = :name', [':name' => 'something']);
$this->assertEquals(['and', 'id = :id', 'name = :name'], $query->where);
$this->assertEquals([':id' => 1, ':name' => 'something'], $query->params);
$query->orWhere('age = :age', [':age' => '30']);
$this->assertEquals(['or', ['and', 'id = :id', 'name = :name'], 'age = :age'], $query->where);
$this->assertEquals([':id' => 1, ':name' => 'something', ':age' => '30'], $query->params);
}
public function testGroup()
{
$query = new Query;
$query->groupBy('team');
$this->assertEquals(['team'], $query->groupBy);
$query->addGroupBy('company');
$this->assertEquals(['team', 'company'], $query->groupBy);
$query->addGroupBy('age');
$this->assertEquals(['team', 'company', 'age'], $query->groupBy);
}
public function testOrder()
{
$query = new Query;
$query->orderBy('team');
$this->assertEquals(['team' => SORT_ASC], $query->orderBy);
$query->addOrderBy('company');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy);
$query->addOrderBy('age');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy);
$query->addOrderBy(['age' => SORT_DESC]);
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy);
$query->addOrderBy('age ASC, company DESC');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy);
}
public function testLimitOffset()
{
$query = new Query;
$query->limit(10)->offset(5);
$this->assertEquals(10, $query->limit);
$this->assertEquals(5, $query->offset);
}
public function testWithin()
{
$query = new Query;
$query->within('team');
$this->assertEquals(['team' => SORT_ASC], $query->within);
$query->addWithin('company');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->within);
$query->addWithin('age');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->within);
$query->addWithin(['age' => SORT_DESC]);
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->within);
$query->addWithin('age ASC, company DESC');
$this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->within);
}
public function testOptions()
{
$query = new Query;
$options = [
'cutoff' => 50,
'max_matches' => 50,
];
$query->options($options);
$this->assertEquals($options, $query->options);
$newMaxMatches = $options['max_matches'] + 10;
$query->addOptions(['max_matches' => $newMaxMatches]);
$this->assertEquals($newMaxMatches, $query->options['max_matches']);
}
public function testRun()
{
$connection = $this->getConnection();
$query = new Query;
$rows = $query->from('yii2_test_article_index')
->match('about')
->options([
'cutoff' => 50,
'field_weights' => [
'title' => 10,
'content' => 3,
],
])
->all($connection);
$this->assertNotEmpty($rows);
}
/**
* @depends testRun
*/
public function testSnippet()
{
$connection = $this->getConnection();
$match = 'about';
$snippetPrefix = 'snippet#';
$snippetCallback = function() use ($match, $snippetPrefix) {
return [
$snippetPrefix . '1: ' . $match,
$snippetPrefix . '2: ' . $match,
];
};
$snippetOptions = [
'before_match' => '[',
'after_match' => ']',
];
$query = new Query;
$rows = $query->from('yii2_test_article_index')
->match($match)
->snippetCallback($snippetCallback)
->snippetOptions($snippetOptions)
->all($connection);
$this->assertNotEmpty($rows);
foreach ($rows as $row) {
$this->assertContains($snippetPrefix, $row['snippet'], 'Snippet source not present!');
$this->assertContains($snippetOptions['before_match'] . $match, $row['snippet'] . $snippetOptions['after_match'], 'Options not applied!');
}
}
}

84
tests/unit/extensions/sphinx/SchemaTest.php

@ -0,0 +1,84 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\caching\FileCache;
use yii\sphinx\Schema;
/**
* @group sphinx
*/
class SchemaTest extends SphinxTestCase
{
public function testFindIndexNames()
{
$schema = $this->getConnection()->schema;
$indexes = $schema->getIndexNames();
$this->assertContains('yii2_test_article_index', $indexes);
$this->assertContains('yii2_test_item_index', $indexes);
$this->assertContains('yii2_test_rt_index', $indexes);
}
public function testGetIndexSchemas()
{
$schema = $this->getConnection()->schema;
$indexes = $schema->getIndexSchemas();
$this->assertEquals(count($schema->getIndexNames()), count($indexes));
foreach($indexes as $index) {
$this->assertInstanceOf('yii\sphinx\IndexSchema', $index);
}
}
public function testGetNonExistingIndexSchema()
{
$this->assertNull($this->getConnection()->schema->getIndexSchema('non_existing_index'));
}
public function testSchemaRefresh()
{
$schema = $this->getConnection()->schema;
$schema->db->enableSchemaCache = true;
$schema->db->schemaCache = new FileCache();
$noCacheIndex = $schema->getIndexSchema('yii2_test_rt_index', true);
$cachedIndex = $schema->getIndexSchema('yii2_test_rt_index', true);
$this->assertEquals($noCacheIndex, $cachedIndex);
}
public function testGetPDOType()
{
$values = [
[null, \PDO::PARAM_NULL],
['', \PDO::PARAM_STR],
['hello', \PDO::PARAM_STR],
[0, \PDO::PARAM_INT],
[1, \PDO::PARAM_INT],
[1337, \PDO::PARAM_INT],
[true, \PDO::PARAM_BOOL],
[false, \PDO::PARAM_BOOL],
[$fp=fopen(__FILE__, 'rb'), \PDO::PARAM_LOB],
];
$schema = $this->getConnection()->schema;
foreach($values as $value) {
$this->assertEquals($value[1], $schema->getPdoType($value[0]));
}
fclose($fp);
}
public function testIndexType()
{
$schema = $this->getConnection()->schema;
$index = $schema->getIndexSchema('yii2_test_article_index');
$this->assertEquals('local', $index->type);
$this->assertFalse($index->isRuntime);
$index = $schema->getIndexSchema('yii2_test_rt_index');
$this->assertEquals('rt', $index->type);
$this->assertTrue($index->isRuntime);
}
}

154
tests/unit/extensions/sphinx/SphinxTestCase.php

@ -0,0 +1,154 @@
<?php
namespace yiiunit\extensions\sphinx;
use yii\helpers\FileHelper;
use yii\sphinx\Connection;
use Yii;
use yiiunit\TestCase as TestCase;
/**
* Base class for the Sphinx test cases.
*/
class SphinxTestCase extends TestCase
{
/**
* @var array Sphinx connection configuration.
*/
protected $sphinxConfig = [
'dsn' => 'mysql:host=127.0.0.1;port=9306;',
'username' => '',
'password' => '',
];
/**
* @var Connection Sphinx connection instance.
*/
protected $sphinx;
/**
* @var array Database connection configuration.
*/
protected $dbConfig = [
'dsn' => 'mysql:host=127.0.0.1;',
'username' => '',
'password' => '',
];
/**
* @var \yii\db\Connection database connection instance.
*/
protected $db;
public static function setUpBeforeClass()
{
static::loadClassMap();
}
protected function setUp()
{
parent::setUp();
if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) {
$this->markTestSkipped('pdo and pdo_mysql extension are required.');
}
$config = $this->getParam('sphinx');
if (!empty($config)) {
$this->sphinxConfig = $config['sphinx'];
$this->dbConfig = $config['db'];
}
$this->mockApplication();
static::loadClassMap();
}
protected function tearDown()
{
if ($this->sphinx) {
$this->sphinx->close();
}
$this->destroyApplication();
}
/**
* Adds sphinx extension files to [[Yii::$classPath]],
* avoiding the necessity of usage Composer autoloader.
*/
protected static function loadClassMap()
{
$baseNameSpace = 'yii/sphinx';
$basePath = realpath(__DIR__. '/../../../../extensions/sphinx');
$files = FileHelper::findFiles($basePath);
foreach ($files as $file) {
$classRelativePath = str_replace($basePath, '', $file);
$classFullName = str_replace(['/', '.php'], ['\\', ''], $baseNameSpace . $classRelativePath);
Yii::$classMap[$classFullName] = $file;
}
}
/**
* @param bool $reset whether to clean up the test database
* @param bool $open whether to open test database
* @return \yii\sphinx\Connection
*/
public function getConnection($reset = false, $open = true)
{
if (!$reset && $this->sphinx) {
return $this->sphinx;
}
$db = new Connection;
$db->dsn = $this->sphinxConfig['dsn'];
if (isset($this->sphinxConfig['username'])) {
$db->username = $this->sphinxConfig['username'];
$db->password = $this->sphinxConfig['password'];
}
if (isset($this->sphinxConfig['attributes'])) {
$db->attributes = $this->sphinxConfig['attributes'];
}
if ($open) {
$db->open();
}
$this->sphinx = $db;
return $db;
}
/**
* Truncates the runtime index.
* @param string $indexName index name.
*/
protected function truncateRuntimeIndex($indexName)
{
if ($this->sphinx) {
$this->sphinx->createCommand('TRUNCATE RTINDEX ' . $indexName)->execute();
}
}
/**
* @param bool $reset whether to clean up the test database
* @param bool $open whether to open and populate test database
* @return \yii\db\Connection
*/
public function getDbConnection($reset = true, $open = true)
{
if (!$reset && $this->db) {
return $this->db;
}
$db = new \yii\db\Connection;
$db->dsn = $this->dbConfig['dsn'];
if (isset($this->dbConfig['username'])) {
$db->username = $this->dbConfig['username'];
$db->password = $this->dbConfig['password'];
}
if (isset($this->dbConfig['attributes'])) {
$db->attributes = $this->dbConfig['attributes'];
}
if ($open) {
$db->open();
if (!empty($this->dbConfig['fixture'])) {
$lines = explode(';', file_get_contents($this->dbConfig['fixture']));
foreach ($lines as $line) {
if (trim($line) !== '') {
$db->pdo->exec($line);
}
}
}
}
$this->db = $db;
return $db;
}
}
Loading…
Cancel
Save