From f4a8be1f6814205975bcd515e3547640c278f890 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 12 Nov 2013 17:07:13 +0200 Subject: [PATCH] Sphinx MVA insert and update resolved. --- extensions/sphinx/ColumnSchema.php | 2 +- extensions/sphinx/Command.php | 6 +- extensions/sphinx/Connection.php | 16 ++ extensions/sphinx/DataReader.php | 265 +++++++++++++++++++++++++++ extensions/sphinx/QueryBuilder.php | 63 +++++-- extensions/sphinx/Schema.php | 2 +- tests/unit/extensions/sphinx/CommandTest.php | 44 ++++- 7 files changed, 378 insertions(+), 20 deletions(-) create mode 100644 extensions/sphinx/DataReader.php diff --git a/extensions/sphinx/ColumnSchema.php b/extensions/sphinx/ColumnSchema.php index 47eca5f..5edca85 100644 --- a/extensions/sphinx/ColumnSchema.php +++ b/extensions/sphinx/ColumnSchema.php @@ -65,7 +65,7 @@ class ColumnSchema extends Object if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { return $value; } - if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING) { + if ($value === '' && $this->type !== Schema::TYPE_STRING) { return null; } switch ($this->phpType) { diff --git a/extensions/sphinx/Command.php b/extensions/sphinx/Command.php index d22d055..2a0d1e3 100644 --- a/extensions/sphinx/Command.php +++ b/extensions/sphinx/Command.php @@ -10,7 +10,6 @@ namespace yii\sphinx; use Yii; use yii\base\Component; use yii\caching\Cache; -use yii\db\DataReader; use yii\db\Exception; /** @@ -432,8 +431,9 @@ class Command extends Component */ public function batchInsert($index, $columns, $rows) { - $sql = $this->db->getQueryBuilder()->batchInsert($index, $columns, $rows); - return $this->setSql($sql); + $params = []; + $sql = $this->db->getQueryBuilder()->batchInsert($index, $columns, $rows, $params); + return $this->setSql($sql)->bindValues($params); } /** diff --git a/extensions/sphinx/Connection.php b/extensions/sphinx/Connection.php index 59ce4b6..8308005 100644 --- a/extensions/sphinx/Connection.php +++ b/extensions/sphinx/Connection.php @@ -48,4 +48,20 @@ class Connection extends \yii\db\Connection { return $this->getSchema()->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); + } } \ No newline at end of file diff --git a/extensions/sphinx/DataReader.php b/extensions/sphinx/DataReader.php new file mode 100644 index 0000000..4b8ffe5 --- /dev/null +++ b/extensions/sphinx/DataReader.php @@ -0,0 +1,265 @@ +query('SELECT * FROM idx_post'); + * + * while ($row = $reader->read()) { + * $rows[] = $row; + * } + * + * // equivalent to: + * foreach ($reader as $row) { + * $rows[] = $row; + * } + * + * // equivalent to: + * $rows = $reader->readAll(); + * ~~~ + * + * Note that since DataReader is a forward-only stream, you can only traverse it once. + * Doing it the second time will throw an exception. + * + * It is possible to use a specific mode of data fetching by setting + * [[fetchMode]]. See the [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php) + * for more details about possible fetch mode. + * + * @property integer $columnCount The number of columns in the result set. This property is read-only. + * @property integer $fetchMode Fetch mode. This property is write-only. + * @property boolean $isClosed Whether the reader is closed or not. This property is read-only. + * @property integer $rowCount Number of rows contained in the result. This property is read-only. + * + * @author Qiang Xue + * @since 2.0 + */ +class DataReader extends Object implements \Iterator, \Countable +{ + /** + * @var \PDOStatement the PDOStatement associated with the command + */ + private $_statement; + private $_closed = false; + private $_row; + private $_index = -1; + + /** + * Constructor. + * @param Command $command the command generating the query result + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct(Command $command, $config = []) + { + $this->_statement = $command->pdoStatement; + $this->_statement->setFetchMode(\PDO::FETCH_ASSOC); + parent::__construct($config); + } + + /** + * Binds a column to a PHP variable. + * When rows of data are being fetched, the corresponding column value + * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND. + * @param integer|string $column Number of the column (1-indexed) or name of the column + * in the result set. If using the column name, be aware that the name + * should match the case of the column, as returned by the driver. + * @param mixed $value Name of the PHP variable to which the column will be bound. + * @param integer $dataType Data type of the parameter + * @see http://www.php.net/manual/en/function.PDOStatement-bindColumn.php + */ + public function bindColumn($column, &$value, $dataType = null) + { + if ($dataType === null) { + $this->_statement->bindColumn($column, $value); + } else { + $this->_statement->bindColumn($column, $value, $dataType); + } + } + + /** + * Set the default fetch mode for this statement + * @param integer $mode fetch mode + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + */ + public function setFetchMode($mode) + { + $params = func_get_args(); + call_user_func_array([$this->_statement, 'setFetchMode'], $params); + } + + /** + * Advances the reader to the next row in a result set. + * @return array the current row, false if no more row available + */ + public function read() + { + return $this->_statement->fetch(); + } + + /** + * Returns a single column from the next row of a result set. + * @param integer $columnIndex zero-based column index + * @return mixed the column of the current row, false if no more rows available + */ + public function readColumn($columnIndex) + { + return $this->_statement->fetchColumn($columnIndex); + } + + /** + * Returns an object populated with the next row of data. + * @param string $className class name of the object to be created and populated + * @param array $fields Elements of this array are passed to the constructor + * @return mixed the populated object, false if no more row of data available + */ + public function readObject($className, $fields) + { + return $this->_statement->fetchObject($className, $fields); + } + + /** + * Reads the whole result set into an array. + * @return array the result set (each array element represents a row of data). + * An empty array will be returned if the result contains no row. + */ + public function readAll() + { + return $this->_statement->fetchAll(); + } + + /** + * Advances the reader to the next result when reading the results of a batch of statements. + * This method is only useful when there are multiple result sets + * returned by the query. Not all DBMS support this feature. + * @return boolean Returns true on success or false on failure. + */ + public function nextResult() + { + if (($result = $this->_statement->nextRowset()) !== false) { + $this->_index = -1; + } + return $result; + } + + /** + * Closes the reader. + * This frees up the resources allocated for executing this SQL statement. + * Read attempts after this method call are unpredictable. + */ + public function close() + { + $this->_statement->closeCursor(); + $this->_closed = true; + } + + /** + * whether the reader is closed or not. + * @return boolean whether the reader is closed or not. + */ + public function getIsClosed() + { + return $this->_closed; + } + + /** + * Returns the number of rows in the result set. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function getRowCount() + { + return $this->_statement->rowCount(); + } + + /** + * Returns the number of rows in the result set. + * This method is required by the Countable interface. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function count() + { + return $this->getRowCount(); + } + + /** + * Returns the number of columns in the result set. + * Note, even there's no row in the reader, this still gives correct column number. + * @return integer the number of columns in the result set. + */ + public function getColumnCount() + { + return $this->_statement->columnCount(); + } + + /** + * Resets the iterator to the initial state. + * This method is required by the interface Iterator. + * @throws InvalidCallException if this method is invoked twice + */ + public function rewind() + { + if ($this->_index < 0) { + $this->_row = $this->_statement->fetch(); + $this->_index = 0; + } else { + throw new InvalidCallException('DataReader cannot rewind. It is a forward-only reader.'); + } + } + + /** + * Returns the index of the current row. + * This method is required by the interface Iterator. + * @return integer the index of the current row. + */ + public function key() + { + return $this->_index; + } + + /** + * Returns the current row. + * This method is required by the interface Iterator. + * @return mixed the current row. + */ + public function current() + { + return $this->_row; + } + + /** + * Moves the internal pointer to the next row. + * This method is required by the interface Iterator. + */ + public function next() + { + $this->_row = $this->_statement->fetch(); + $this->_index++; + } + + /** + * Returns whether there is a row of data at current position. + * This method is required by the interface Iterator. + * @return boolean whether there is a row of data at current position. + */ + public function valid() + { + return $this->_row !== false; + } +} \ No newline at end of file diff --git a/extensions/sphinx/QueryBuilder.php b/extensions/sphinx/QueryBuilder.php index 02f0145..a7bd6be 100644 --- a/extensions/sphinx/QueryBuilder.php +++ b/extensions/sphinx/QueryBuilder.php @@ -103,15 +103,26 @@ class QueryBuilder extends Object $params[$n] = $v; } } else { - $phName = self::PARAM_PREFIX . count($params); - $placeholders[] = $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; + if (is_array($value)) { + // MVA : + $placeholderParts = []; + foreach ($value as $subValue) { + $phName = self::PARAM_PREFIX . count($params); + $placeholderParts[] = $phName; + $params[$phName] = isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($subValue) : $subValue; + } + $placeholders[] = '(' . implode(',', $placeholderParts) . ')'; + } else { + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; + } } } return 'INSERT INTO ' . $this->db->quoteIndexName($index) - . ' (' . implode(', ', $names) . ') VALUES (' - . implode(', ', $placeholders) . ')'; + . ' (' . implode(', ', $names) . ') VALUES (' + . implode(', ', $placeholders) . ')'; } /** @@ -131,9 +142,11 @@ class QueryBuilder extends Object * @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 DB command later. * @return string the batch INSERT SQL statement */ - public function batchInsert($index, $columns, $rows) + public function batchInsert($index, $columns, $rows, &$params) { if (($indexSchema = $this->db->getIndexSchema($index)) !== null) { $columnSchemas = $indexSchema->columns; @@ -149,16 +162,29 @@ class QueryBuilder extends Object foreach ($rows as $row) { $vs = []; foreach ($row as $i => $value) { - if (!is_array($value) && isset($columnSchemas[$columns[$i]])) { - $value = $columnSchemas[$columns[$i]]->typecast($value); + if (is_array($value)) { + // MVA : + $vsParts = []; + foreach ($value as $subValue) { + $phName = self::PARAM_PREFIX . count($params); + $vsParts[] = $phName; + $params[$phName] = isset($columnSchemas[$columns[$i]]) ? $columnSchemas[$columns[$i]]->typecast($subValue) : $subValue; + } + $vs[] = '(' . implode(',', $vsParts) . ')'; + } else { + $phName = self::PARAM_PREFIX . count($params); + if (isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->typecast($value); + } + $params[$phName] = is_string($value) ? $this->db->quoteValue($value) : $value; + $vs[] = $phName; } - $vs[] = is_string($value) ? $this->db->quoteValue($value) : $value; } $values[] = '(' . implode(', ', $vs) . ')'; } return 'INSERT INTO ' . $this->db->quoteIndexName($index) - . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); } /** @@ -196,9 +222,20 @@ class QueryBuilder extends Object $params[$n] = $v; } } else { - $phName = self::PARAM_PREFIX . count($params); - $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; + if (is_array($value)) { + // MVA : + $lineParts = []; + foreach ($value as $subValue) { + $phName = self::PARAM_PREFIX . count($params); + $lineParts[] = $phName; + $params[$phName] = isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($subValue) : $subValue; + } + $lines[] = $this->db->quoteColumnName($name) . '=' . '(' . implode(',', $lineParts) . ')'; + } else { + $phName = self::PARAM_PREFIX . count($params); + $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->typecast($value) : $value; + } } } diff --git a/extensions/sphinx/Schema.php b/extensions/sphinx/Schema.php index 8313ae4..558f071 100644 --- a/extensions/sphinx/Schema.php +++ b/extensions/sphinx/Schema.php @@ -62,7 +62,7 @@ class Schema extends Object 'timestamp' => self::TYPE_TIMESTAMP, 'bool' => self::TYPE_BOOLEAN, 'float' => self::TYPE_FLOAT, - 'mva' => self::TYPE_STRING, + 'mva' => self::TYPE_INTEGER, ]; /** diff --git a/tests/unit/extensions/sphinx/CommandTest.php b/tests/unit/extensions/sphinx/CommandTest.php index 32a6ffa..40cf895 100644 --- a/tests/unit/extensions/sphinx/CommandTest.php +++ b/tests/unit/extensions/sphinx/CommandTest.php @@ -2,7 +2,7 @@ namespace yiiunit\extensions\sphinx; -use yii\db\DataReader; +use yii\sphinx\DataReader; use yii\db\Expression; /** @@ -103,7 +103,7 @@ class CommandTest extends SphinxTestCase 'title' => 'Test title', 'content' => 'Test content', 'type_id' => 2, - //'category' => [41, 42], + 'category' => [1, 2], 'id' => 1, ]); $this->assertEquals(1, $command->execute(), 'Unable to execute insert!'); @@ -115,6 +115,45 @@ class CommandTest extends SphinxTestCase /** * @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 testUpdate() { $db = $this->getConnection(); @@ -131,6 +170,7 @@ class CommandTest extends SphinxTestCase 'yii2_test_rt_index', [ 'type_id' => $newTypeId, + 'category' => [3, 4], ], 'id = 1' );