Skip to content

Commit

Permalink
QueryBuilder fixed to be compatible with PHP 7.2 and Traversable
Browse files Browse the repository at this point in the history
…support
  • Loading branch information
klimov-paul committed Nov 28, 2017
1 parent d2db0b2 commit b4b507e
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Yii Framework 2 sphinx extension Change Log
------------------------

- Bug #90: Fixed `yii\sphinx\Schema::findColumns()` unable to merge 'field' and 'attribute' columns with same name (maz0717, klimov-paul)
- Bug #92: Fixed `yii\sphinx\QueryBuilder::buildInCondition()` incompatibility with PHP 7.2 (klimov-paul)
- Enh: `yii\sphinx\QueryBuilder` now supports `Traversable` objects for use in `in` conditions (klimov-paul)


2.0.9 November 03, 2017
Expand Down
61 changes: 34 additions & 27 deletions QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ public function buildHashCondition($indexes, $condition, &$params)
{
$parts = [];
foreach ($condition as $column => $value) {
if (is_array($value) || $value instanceof Query) {
if (is_array($value) || $value instanceof \Traversable || $value instanceof Query) {
// IN condition
$parts[] = $this->buildInCondition($indexes, 'IN', [$column, $value], $params);
} else {
Expand Down Expand Up @@ -812,7 +812,7 @@ public function buildAndCondition($indexes, $operator, $operands, &$params)
{
$parts = [];
foreach ($operands as $operand) {
if (is_array($operand)) {
if (is_array($operand) || $operand instanceof Expression) {
$operand = $this->buildCondition($indexes, $operand, $params);
}
if ($operand !== '') {
Expand All @@ -821,9 +821,9 @@ public function buildAndCondition($indexes, $operator, $operands, &$params)
}
if (!empty($parts)) {
return '(' . implode(") $operator (", $parts) . ')';
} else {
return '';
}

return '';
}

/**
Expand All @@ -842,7 +842,7 @@ public function buildNotCondition($indexes, $operator, $operands, &$params)
}

$operand = reset($operands);
if (is_array($operand)) {
if (is_array($operand) || $operand instanceof Expression) {
$operand = $this->buildCondition($indexes, $operand, $params);
}
if ($operand === '') {
Expand Down Expand Up @@ -902,16 +902,6 @@ public function buildInCondition($indexes, $operator, $operands, &$params)

list($column, $values) = $operands;

if ($values === []) {
if ($operator === 'IN') {
if (empty($column)) {
throw new Exception("Operator '$operator' requires column being specified.");
}
$column = $this->db->quoteColumnName($column);
return "({$column} = 0 AND {$column} = 1)";
}
return '';
}
if ($column === []) {
return '';
}
Expand All @@ -935,31 +925,46 @@ public function buildInCondition($indexes, $operator, $operands, &$params)
}
}

$values = (array) $values;
if (!is_array($values) && !$values instanceof \Traversable) {
// ensure values is an array
$values = (array) $values;
}

if (count($column) > 1) {
if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
return $this->buildCompositeInCondition($indexes, $operator, $column, $values, $params);
}
if (is_array($column)) {
} elseif (is_array($column)) {
$column = reset($column);
}

$sqlValues = [];
foreach ($values as $i => $value) {
if (is_array($value)) {
$value = isset($value[$column]) ? $value[$column] : null;
}
$values[$i] = $this->composeColumnValue($indexes, $column, $value, $params);
$sqlValues[$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' ? '=' : '<>';
if ($sqlValues === []) {
if ($operator === 'IN') {
if (empty($column)) {
throw new Exception("Operator '$operator' requires column being specified.");
}
$column = $this->db->quoteColumnName($column);
return "({$column} = 0 AND {$column} = 1)";
}
return '';
}

return $column . $operator . reset($values);
if (count($sqlValues) > 1) {
return "$column $operator (" . implode(', ', $sqlValues) . ')';
}

$operator = $operator === 'IN' ? '=' : '<>';
return $column . $operator . reset($sqlValues);
}

/**
Expand All @@ -984,13 +989,15 @@ protected function buildCompositeInCondition($indexes, $operator, $columns, $val
}
$vss[] = '(' . implode(', ', $vs) . ')';
}

$sqlColumns = [];
foreach ($columns as $i => $column) {
if (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
$sqlColumns[$i] = $this->db->quoteColumnName($column);
}
}

return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')';
return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
}

/**
Expand Down
120 changes: 120 additions & 0 deletions tests/QueryBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace yiiunit\extensions\sphinx;

use yii\db\Expression;
use yii\sphinx\Query;
use yii\sphinx\QueryBuilder;

class QueryBuilderTest extends TestCase
{
/**
* @return QueryBuilder query builder instance.
*/
protected function createQueryBuilder()
{
return new QueryBuilder($this->getConnection());
}

/**
* @return array test data.
*/
public function dataProviderBuildCondition()
{
$conditions = [
// empty values
[['like', 'name', []], '0=1', []],
[['not like', 'name', []], '', []],
[['or like', 'name', []], '0=1', []],
[['or not like', 'name', []], '', []],

// not
[['not', 'name'], 'NOT (name)', []],

// and
[['and', 'id=1', 'id=2'], '(id=1) AND (id=2)', []],
[['and', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) AND ((id=1) OR (id=2))', []],
[['and', 'id=1', new Expression('id=:qp0', [':qp0' => 2])], '(id=1) AND (id=:qp0)', [':qp0' => 2]],

// or
[['or', 'id=1', 'id=2'], '(id=1) OR (id=2)', []],
[['or', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) OR ((id=1) OR (id=2))', []],
[['or', 'type=1', new Expression('id=:qp0', [':qp0' => 1])], '(type=1) OR (id=:qp0)', [':qp0' => 1]],

// between
[['between', 'id', 1, 10], '`id` BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10]],
[['not between', 'id', 1, 10], '`id` NOT BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10]],
[['between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), new Expression('NOW()')], '`date` BETWEEN (NOW() - INTERVAL 1 MONTH) AND NOW()', []],
[['between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), 123], '`date` BETWEEN (NOW() - INTERVAL 1 MONTH) AND :qp0', [':qp0' => 123]],
[['not between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), new Expression('NOW()')], '`date` NOT BETWEEN (NOW() - INTERVAL 1 MONTH) AND NOW()', []],
[['not between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), 123], '`date` NOT BETWEEN (NOW() - INTERVAL 1 MONTH) AND :qp0', [':qp0' => 123]],

// in
[['in', 'id', [1, 2, 3]], '`id` IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]],
[['not in', 'id', [1, 2, 3]], '`id` NOT IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]],

[['in', 'id', 1], '`id`=:qp0', [':qp0' => 1]],
[['in', 'id', [1]], '`id`=:qp0', [':qp0' => 1]],
[['in', 'id', new \ArrayIterator([1])], '`id`=:qp0', [':qp0' => 1]],
'composite in' => [
['in', ['id', 'name'], [['id' => 1, 'name' => 'oy']]],
'(`id`, `name`) IN ((:qp0, :qp1))',
[':qp0' => 1, ':qp1' => 'oy'],
],

// in using array objects.
[['id' => new \ArrayIterator([1, 2])], '`id` IN (:qp0, :qp1)', [':qp0' => 1, ':qp1' => 2]],

[['in', 'id', new \ArrayIterator([1, 2, 3])], '`id` IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]],

'composite in using array objects' => [
['in', new \ArrayIterator(['id', 'name']), new \ArrayIterator([
['id' => 1, 'name' => 'oy'],
['id' => 2, 'name' => 'yo'],
])],
'(`id`, `name`) IN ((:qp0, :qp1), (:qp2, :qp3))',
[':qp0' => 1, ':qp1' => 'oy', ':qp2' => 2, ':qp3' => 'yo'],
],

// simple conditions
[['=', 'a', 'b'], '`a` = :qp0', [':qp0' => 'b']],
[['>', 'a', 1], '`a` > :qp0', [':qp0' => 1]],
[['>=', 'a', 'b'], '`a` >= :qp0', [':qp0' => 'b']],
[['<', 'a', 2], '`a` < :qp0', [':qp0' => 2]],
[['<=', 'a', 'b'], '`a` <= :qp0', [':qp0' => 'b']],
[['<>', 'a', 3], '`a` <> :qp0', [':qp0' => 3]],
[['!=', 'a', 'b'], '`a` != :qp0', [':qp0' => 'b']],
[['>=', 'date', new Expression('DATE_SUB(NOW(), INTERVAL 1 MONTH)')], '`date` >= DATE_SUB(NOW(), INTERVAL 1 MONTH)', []],
[['>=', 'date', new Expression('DATE_SUB(NOW(), INTERVAL :month MONTH)', [':month' => 2])], '`date` >= DATE_SUB(NOW(), INTERVAL :month MONTH)', [':month' => 2]],

// hash condition
[['a' => 1, 'b' => 2], '(`a`=:qp0) AND (`b`=:qp1)', [':qp0' => 1, ':qp1' => 2]],
[['a' => new Expression('CONCAT(col1, col2)'), 'b' => 2], '(`a`=CONCAT(col1, col2)) AND (`b`=:qp0)', [':qp0' => 2]],

// direct conditions
['a = CONCAT(col1, col2)', 'a = CONCAT(col1, col2)', []],
[new Expression('a = CONCAT(col1, :param1)', ['param1' => 'value1']), 'a = CONCAT(col1, :param1)', ['param1' => 'value1']],

// Expression with params as operand of 'not'
[['not', new Expression('any_expression(:a)', [':a' => 1])], 'NOT (any_expression(:a))', [':a' => 1]],
[new Expression('NOT (any_expression(:a))', [':a' => 1]), 'NOT (any_expression(:a))', [':a' => 1]],
];

return $conditions;
}

/**
* @dataProvider dataProviderBuildCondition
*
* @param array $condition
* @param string $expected
* @param array $expectedParams
*/
public function testBuildCondition($condition, $expected, $expectedParams)
{
$query = (new Query())->where($condition);
list($sql, $params) = $this->createQueryBuilder()->build($query);
$this->assertEquals('SELECT *' . (empty($expected) ? '' : ' WHERE ' . $expected), $sql);
$this->assertEquals($expectedParams, $params);
}
}

0 comments on commit b4b507e

Please sign in to comment.