Fixes #7640: Implemented custom data types support. Added JSON support for MySQL and PostgreSQL, array support for PostgreSQL

This commit is contained in:
Dmitry Naumenko
2018-02-04 23:59:14 +02:00
committed by Alexander Makarov
parent 5afe0a0d36
commit d16586334d
71 changed files with 3329 additions and 686 deletions

View File

@@ -46,9 +46,9 @@ cache:
- $HOME/.composer/cache
- $HOME/.npm
# try running against postgres 9.3
# try running against postgres 9.6
addons:
postgresql: "9.3"
postgresql: "9.6"
code_climate:
repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b
@@ -89,7 +89,7 @@ matrix:
addons:
code_climate:
repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b
postgresql: "9.3"
postgresql: "9.6"
apt:
packages:
- mysql-server-5.6

View File

@@ -472,7 +472,7 @@ $customer->loadDefaultValues();
### Attributes Typecasting <span id="attributes-typecasting"></span>
Being populated by query results [[yii\db\ActiveRecord]] performs automatic typecast for its attribute values, using
Being populated by query results, [[yii\db\ActiveRecord]] performs automatic typecast for its attribute values, using
information from [database table schema](db-dao.md#database-schema). This allows data retrieved from table column
declared as integer to be populated in ActiveRecord instance with PHP integer, boolean with boolean and so on.
However, typecasting mechanism has several limitations:
@@ -490,7 +490,33 @@ converted during saving process.
> Tip: you may use [[yii\behaviors\AttributeTypecastBehavior]] to facilitate attribute values typecasting
on ActiveRecord validation or saving.
Since 2.0.14, Yii ActiveRecord supports complex data types, such as JSON or multidimensional arrays.
#### JSON in MySQL and PostgreSQL
After data population, the value from JSON column will be automatically decoded from JSON according to standard JSON
decoding rules.
To save attribute value to a JSON column, ActiveRecord will automatically create a [[yii\db\JsonExpression|JsonExpression]] object
that will be encoded to a JSON string on [QueryBuilder](db-query-builder.md) level.
#### Arrays in PostgreSQL
After data population, the value from Array column will be automatically decoded from PgSQL notation to an [[yii\db\ArrayExpression|ArrayExpression]]
object. It implements PHP `ArrayAccess` interface, so you can use it as an array, or call `->getValue()` to get the array itself.
To save attribute value to an array column, ActiveRecord will automatically create an [[yii\db\ArrayExpression|ArrayExpression]] object
that will be encoded by [QueryBuilder](db-query-builder.md) to an PgSQL string representation of array.
You can also use conditions for JSON columns:
```php
$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])
```
To learn more about expressions building system read the [Query Builder  Adding custom Conditions and Expressions](db-query-builder.md#adding-custom-conditions-and-expressions)
article.
### Updating Multiple Rows <span id="updating-multiple-rows"></span>

View File

@@ -160,12 +160,12 @@ are in the ["Quoting Tables" section of the "Database Access Objects" guide](gui
### [[yii\db\Query::where()|where()]] <span id="where"></span>
The [[yii\db\Query::where()|where()]] method specifies the `WHERE` fragment of a SQL query. You can use one of
the three formats to specify a `WHERE` condition:
the four formats to specify a `WHERE` condition:
- string format, e.g., `'status=1'`
- hash format, e.g. `['status' => 1, 'type' => 2]`
- operator format, e.g. `['like', 'name', 'test']`
- object format, e.g. `new LikeCondition('name', 'LIKE', 'test')`
#### String Format <span id="string-format"></span>
@@ -306,6 +306,41 @@ the operator can be one of the following:
Using the Operator Format, Yii internally uses parameter binding so in contrast to the [string format](#string-format), here
you do not have to add parameters manually.
#### Object Format <span id="object-format"></span>
Object Form is available since 2.0.14 and is both most powerful and most complex way to define conditions.
You need to follow it either if you want to build your own abstraction over query builder or if you want to implement
your own complex conditions.
Instances of condition classes are immutable. Their only purpose is to store condition data and provide getters
for condition builders. Condition builder is a class that holds the logic that transforms data
stored in condition into the SQL expression.
Internally the formats described above are implicitly converted to object format prior to building raw SQL,
so it is possible to combine formats in a single condition:
```php
$query->andWhere(new OrCondition([
new InCondition('type', 'in', $types),
['like', 'name', '%good%'],
'disabled=false'
]))
```
Conversion from operator format into object format is performed according to
[[yii\db\QueryBuilder::conditionClasses|QueryBuilder::conditionClasses]] property, that maps operators names
to representative class names:
- `AND`, `OR` -> `yii\db\conditions\ConjunctionCondition`
- `NOT` -> `yii\db\conditions\NotCondition`
- `IN`, `NOT IN` -> `yii\db\conditions\InCondition`
- `BETWEEN`, `NOT BETWEEN` -> `yii\db\conditions\BetweenCondition`
And so on.
Using the object format makes it possible to create your own conditions or to change the way default ones are built.
See [Creating Custom Conditions and Expressions](#creating-custom-conditions-and-expressions) chapter to learn more.
#### Appending Conditions <span id="appending-conditions"></span>
@@ -758,3 +793,170 @@ $unbufferedDb->close();
```
> Note: unbuffered query uses less memory on the PHP-side, but can increase the load on the MySQL server. It is recommended to design your own code with your production practice for extra massive data, [for example, divide the range for integer keys, loop them with Unbuffered Queries](https://github.com/yiisoft/yii2/issues/8420#issuecomment-296109257).
### Adding custom Conditions and Expressions <span id="adding-custom-conditions-and-expressions"></span>
As it was mentioned in [Conditions  Object Fromat](#object-format) chapter, is is possible to create custom condition
classes. For example, let's create a condition that will check that specific columns are less than some value.
Using the operator format, it would look like the following:
```php
[
'and',
'>', 'posts', $minLimit,
'>', 'comments', $minLimit,
'>', 'reactions', $minLimit,
'>', 'subscriptions', $minLimit
]
```
When such condition applied once, it is fine. In case it is used multiple times in a single query it can
be optimized a lot. Let's create a custom condition object to demonstrate it.
Yii has a [[yii\db\conditions\ConditionInterface|ConditionInterface]], that must be used to mark classes, that represent
a condition. It requires `fromArrayDefinition()` method implementation, in order to make possible to create condition
from array format. In case you don't need it, you can implement this method with exception throwing.
Since we create our custom condition class, we can build API that suits our task the most.
```php
namespace app\db\conditions;
class AllGreaterCondition implements \yii\db\conditions\ConditionInterface
{
private $columns;
private $value;
/**
* @param string[] $columns Array of columns that must be greater, than $value
* @param mixed $value the value to compare each $column against.
*/
public function __construct(array $columns, $value)
{
$this->columns = $columns;
$this->value = $value;
}
public static function fromArrayDefinition($operator, $operands)
{
throw new InvalidParamException('Not implemented yet, but we will do it later');
}
public function getColumns() { return $this->columns; }
public function getValue() { return $this->vaule; }
}
```
So we can create a condition object:
```php
$conditon = new AllGreaterCondition(['col1', 'col2'], 42);
```
But `QueryBuilder` still does not know, to to make an SQL condition out of this object.
Now we need to create a builder for this condition. It must implement [[yii\db\ExpressionBuilderInterface]]
that requires us to implement a `build()` method.
```php
namespace app\db\conditions;
class AllGreaterConditionBuilder implements \yii\db\ExpressionBuilderInterface
{
use \yii\db\Condition\ExpressionBuilderTrait; // Contains constructor and `queryBuilder` property.
/**
* @param AllGreaterCondition $condition the condition to be built
* @param array $params the binding parameters.
*/
public function build(ConditionInterface $condition, &$params)
{
$value = $condition->getValue();
$conditions = [];
foreach ($condition->getColumns() as $column) {
$conditions[] = new SimpleCondition($column, '>', $value);
}
return $this->queryBuider->buildCondition(new AndCondition($conditions), $params);
}
}
```
Then simple let [[yii\db\QueryBuilder|QueryBuilder]] know about our new condition  add a mapping for it to
the `expressionBuilders` array. It could be done right from the application configuration:
```php
'db' => [
'class' => 'yii\db\mysql\Connection',
// ...
'queryBuilder' => [
'expressionBuilders' => [
'app\db\conditions\AllGreaterCondition' => 'app\db\conditions\AllGreaterConditionBuilder',
],
],
],
```
Now we can use our condition in `where()`:
```php
$query->andWhere(new AllGreaterCondition(['posts', 'comments', 'reactions', 'subscriptions'], $minValue));
```
If we want to make it possible to create our custom condition using operator format, we should declare it in
[[yii\db\QueryBuilder::conditionClasses|QueryBuilder::conditionClasses]]:
```php
'db' => [
'class' => 'yii\db\mysql\Connection',
// ...
'queryBuilder' => [
'expressionBuilders' => [
'app\db\conditions\AllGreaterCondition' => 'app\db\conditions\AllGreaterConditionBuilder',
],
'conditionClasses' => [
'ALL>' => 'app\db\conditions\AllGreaterCondition',
],
],
],
```
And create a real implementation of `AllGreaterCondition::fromArrayDefinition()` method
in `app\db\conditions\AllGreaterCondition`:
```php
namespace app\db\conditions;
class AllGreaterCondition implements \yii\db\conditions\ConditionInterface
{
// ... see the implementation above
public static function fromArrayDefinition($operator, $operands)
{
return new static($operands[0], $operands[1]);
}
}
```
After that, we can create our custom condition using shorter operator format:
```php
$query->andWhere(['ALL>', ['posts', 'comments', 'reactions', 'subscriptions'], $minValue]);
```
You might notice, that there was two concepts used: Expressions and Conditions. There is a [[yii\db\ExpressionInterface]]
that should be used to mark objects, that require an Expression Builder class, that implements
[[yii\db\ExpressionBuilderInterface]] to be built. Also there is a [[yii\db\condition\ConditionInterface]], that extends
[[yii\db\ExpressionInterface|ExpressionInterface]] and should be used to objects, that can be created from array definition
as it was shown above, but require builder as well.
To summarise:
- Expression is a Data Transfer Object (DTO) for a dataset, that can be somehow compiled to some SQL
statement (an operator, string, array, JSON, etc).
- Condition is an Expression superset, that aggregates multiple Expressions (or scalar values) that can be compiled
to a single SQL condition.
You can create your own classes that implement [[yii\db\ExpressionInterface|ExpressionInterface]] to hide the complexity
of transforming data to SQL statements. You will learn more about other examples of Expressions in the
[next article](db-active-record.md);

View File

@@ -157,6 +157,7 @@ Yii Framework 2 Change Log
- Enh #4495: Added closure support in `yii\i18n\Formatter` (developeruz)
- Enh #5786: Allowed to use custom constructors in ActiveRecord-based classes (ElisDN, klimov-paul)
- Enh #6644: Added `yii\helpers\ArrayHelper::setValue()` (LAV45)
- Enh #7640: Implemented custom data types support. Added JSON support for MySQL and PostgreSQL, array support for PostgreSQL (silverfire, cebe)
- Enh #7823: Added `yii\filters\AjaxFilter` filter (dmirogin)
- Enh #9438: `yii\web\DbSession` now relies on error handler to display errors (samdark)
- Enh #9703, #9709: Added `yii\i18n\Formatter::asWeight()` and `::asLength()` formatters (nineinchnick, silverfire)

View File

@@ -58,6 +58,20 @@ Upgrade from Yii 2.0.13
* `yii\base\Security::compareString()` is now throwing `yii\base\InvalidParamException` in case non-strings are compared.
* `yii\db\ExpressionInterface` has been introduced to represent a wider range of SQL expressions. In case you check for
`instanceof yii\db\Expression` in your code, you might consider changing that to checking for the interface and use the newly
introduced methods to retrieve the expression content.
* `yii\db\PdoValue` class has been introduced to replace a special syntax that was used to declare PDO parameter type
when binding parameters to an SQL command, for example: `['value', \PDO::PARAM_STR]`.
You should use `new PdoValue('value', \PDO::PARAM_STR)` instead. Old syntax will be removed in Yii 2.1.
* `yii\db\QueryBuilder::conditionBuilders` property and method-based condition builders are no longer used.
Class-based conditions and builders are introduces instead to provide more flexibility, extensibility and
space to customization. In case you rely on that property or override any of default condition builders, follow the
special [guide article](http://www.yiiframework.com/doc-2.0/guide-db-query-builder.html#adding-custom-conditions-and-expressions)
to update your code.
* Log targets (like `yii\log\EmailTarget`) are now throwing `yii\log\LogRuntimeException` in case log can not be properly exported.
Upgrade from Yii 2.0.12

View File

@@ -10,6 +10,7 @@ namespace yii\caching;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\Connection;
use yii\db\PdoValue;
use yii\db\Query;
use yii\di\Instance;
@@ -195,7 +196,7 @@ class DbCache extends Cache
$command = $db->createCommand()
->update($this->cacheTable, [
'expire' => $duration > 0 ? $duration + time() : 0,
'data' => [$value, \PDO::PARAM_LOB],
'data' => new PdoValue($value, \PDO::PARAM_LOB),
], ['id' => $key]);
return $command->execute();
});
@@ -228,7 +229,7 @@ class DbCache extends Cache
->insert($this->cacheTable, [
'id' => $key,
'expire' => $duration > 0 ? $duration + time() : 0,
'data' => [$value, \PDO::PARAM_LOB],
'data' => new PdoValue($value, \PDO::PARAM_LOB),
])->execute();
});

View File

@@ -0,0 +1,164 @@
<?php
namespace yii\db;
/**
* Class ArrayExpression represents an array SQL expression.
*
* Expressions of this type can be used in conditions as well:
*
* ```php
* $query->andWhere(['@>', 'items', new ArrayExpression([1, 2, 3], 'integer')])
* ```
*
* which, depending on DBMS, will result in a well-prepared condition. For example, in
* PostgreSQL it will be compiled to `WHERE "items" @> ARRAY[1, 2, 3]::integer[]`.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
{
/**
* @var null|string the type of the array elements. Defaults to `null` which means the type is
* not explicitly specified.
*
* Note that in case when type is not specified explicitly and DBMS can not guess it from the context,
* SQL error will be raised.
*/
private $type;
/**
* @var array|QueryInterface|mixed the array content. Either represented as an array of values or a [[Query]] that
* returns these values. A single value will be considered as an array containing one element.
*/
private $value;
/**
* @var int the number of indices needed to select an element
*/
private $dimension;
/**
* ArrayExpression constructor.
*
* @param array|QueryInterface|mixed $value the array content. Either represented as an array of values or a Query that
* returns these values. A single value will be considered as an array containing one element.
* @param string|null $type the type of the array elements. Defaults to `null` which means the type is
* not explicitly specified. In case when type is not specified explicitly and DBMS can not guess it from the context,
* SQL error will be raised.
* @param int $dimension the number of indices needed to select an element
*/
public function __construct($value, $type = null, $dimension = 1)
{
$this->value = $value;
$this->type = $type;
$this->dimension = $dimension;
}
/**
* @return null|string
* @see type
*/
public function getType()
{
return $this->type;
}
/**
* @return array|mixed|QueryInterface
* @see value
*/
public function getValue()
{
return $this->value;
}
/**
* @return int the number of indices needed to select an element
* @see dimensions
*/
public function getDimension()
{
return $this->dimension;
}
/**
* Whether a offset exists
*
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset <p>
* An offset to check for.
* </p>
* @return boolean true on success or false on failure.
* </p>
* <p>
* The return value will be casted to boolean if non-boolean was returned.
* @since 5.0.0
*/
public function offsetExists($offset)
{
return isset($this->value[$offset]);
}
/**
* Offset to retrieve
*
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 5.0.0
*/
public function offsetGet($offset)
{
return $this->value[$offset];
}
/**
* Offset to set
*
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetSet($offset, $value)
{
$this->value[$offset] = $value;
}
/**
* Offset to unset
*
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset <p>
* The offset to unset.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetUnset($offset)
{
unset($this->value[$offset]);
}
/**
* Count elements of an object
*
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count()
{
return count($this->value);
}
}

View File

@@ -34,7 +34,7 @@ class ColumnSchema extends BaseObject
public $type;
/**
* @var string the PHP type of this column. Possible PHP types include:
* `string`, `boolean`, `integer`, `double`.
* `string`, `boolean`, `integer`, `double`, `array`.
*/
public $phpType;
/**
@@ -114,12 +114,16 @@ class ColumnSchema extends BaseObject
*/
protected function typecast($value)
{
if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING && $this->type !== Schema::TYPE_BINARY && $this->type !== Schema::TYPE_CHAR) {
if ($value === '' && !in_array($this->type, [Schema::TYPE_TEXT, Schema::TYPE_STRING, Schema::TYPE_BINARY, Schema::TYPE_CHAR], true)) {
return null;
}
if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression || $value instanceof Query) {
if ($value === null || gettype($value) === $this->phpType || $value instanceof ExpressionInterface || $value instanceof Query) {
return $value;
}
if (is_array($value) && count($value) === 2 && isset($value[1]) && in_array($value[1], $this->getPdoParamTypes(), true)) {
return new PdoValue($value[0], $value[1]);
}
switch ($this->phpType) {
case 'resource':
case 'string':
@@ -143,4 +147,12 @@ class ColumnSchema extends BaseObject
return $value;
}
/**
* @return int[] array of numbers that represent possible PDO parameter types
*/
private function getPdoParamTypes()
{
return [\PDO::PARAM_BOOL, \PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_LOB, \PDO::PARAM_NULL, \PDO::PARAM_STMT];
}
}

View File

@@ -347,8 +347,8 @@ class Command extends Component
* @param array $values the values to be bound. This must be given in terms of an associative
* array with array keys being the parameter names, and array values the corresponding parameter values,
* e.g. `[':name' => 'John', ':age' => 25]`. By default, the PDO type of each value is determined
* by its PHP type. You may explicitly specify the PDO type by using an array: `[value, type]`,
* e.g. `[':name' => 'John', ':profile' => [$profile, \PDO::PARAM_LOB]]`.
* by its PHP type. You may explicitly specify the PDO type by using a [[yii\db\PdoValue]] class: `new PdoValue(value, type)`,
* e.g. `[':name' => 'John', ':profile' => new PdoValue($profile, \PDO::PARAM_LOB)]`.
* @return $this the current command being executed
*/
public function bindValues($values)
@@ -359,9 +359,9 @@ class Command extends Component
$schema = $this->db->getSchema();
foreach ($values as $name => $value) {
if (is_array($value)) {
$this->_pendingParams[$name] = $value;
$this->params[$name] = $value[0];
if ($value instanceof PdoValue) {
$this->_pendingParams[$name] = [$value->getValue(), $value->getType()];
$this->params[$name] = $value->getValue();
} else {
$type = $schema->getPdoType($value);
$this->_pendingParams[$name] = [$value, $type];

View File

@@ -834,6 +834,17 @@ class Connection extends Component
return $this->getSchema()->getQueryBuilder();
}
/**
* Can be used to set [[QueryBuilder]] configuration via Connection configuration array.
*
* @param $value
* @since 2.0.14
*/
public function setQueryBuilder($value)
{
Yii::configure($this->getQueryBuilder(), $value);
}
/**
* Obtains the schema information for the named table.
* @param string $name table name.

View File

@@ -28,7 +28,7 @@ namespace yii\db;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Expression extends \yii\base\BaseObject
class Expression extends \yii\base\BaseObject implements ExpressionInterface
{
/**
* @var string the DB expression
@@ -41,7 +41,6 @@ class Expression extends \yii\base\BaseObject
*/
public $params = [];
/**
* Constructor.
* @param string $expression the DB expression
@@ -57,7 +56,7 @@ class Expression extends \yii\base\BaseObject
/**
* String magic method.
* @return string the DB expression
* @return string the DB expression.
*/
public function __toString()
{

View File

@@ -0,0 +1,24 @@
<?php
namespace yii\db;
/**
* Interface ExpressionBuilderInterface
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ExpressionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* {@inheritdoc}
* @param Expression|ExpressionInterface $expression the expression to be built
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$params = array_merge($params, $expression->params);
return $expression->__toString();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace yii\db;
/**
* Interface ExpressionBuilderInterface is designed to build raw SQL from specific expression
* objects that implement [[ExpressionInterface]].
*
* @author Dmitry Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
interface ExpressionBuilderInterface
{
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = []);
}

View File

@@ -0,0 +1,28 @@
<?php
namespace yii\db;
/**
* Trait ExpressionBuilderTrait provides common constructor for classes that
* should implement [[ExpressionBuilderInterface]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
trait ExpressionBuilderTrait
{
/**
* @var QueryBuilder
*/
protected $queryBuilder;
/**
* ExpressionBuilderTrait constructor.
*
* @param QueryBuilder $queryBuilder
*/
public function __construct(QueryBuilder $queryBuilder)
{
$this->queryBuilder = $queryBuilder;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace yii\db;
/**
* Interface ExpressionInterface should be used to mark classes, that should be built
* in a special way.
*
* The database abstraction layer of Yii framework supports objects that implement this
* interface and will use [[ExpressionBuilderInterface]] to build them.
*
* The default implementation is a class [[Expression]].
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
interface ExpressionInterface
{
}

View File

@@ -0,0 +1,68 @@
<?php
namespace yii\db;
/**
* Class JsonExpression represents data that should be encoded to JSON.
*
* For example:
*
* ```php
* new JsonExpression(['a' => 1, 'b' => 2]); // will be encoded to '{"a": 1, "b": 2}'
* ```
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class JsonExpression implements ExpressionInterface
{
const TYPE_JSON = 'json';
const TYPE_JSONB = 'jsonb';
/**
* @var mixed the value to be encoded to JSON.
* The value must be compatible with [\yii\helpers\Json::encode()|Json::encode()]] input requirements.
*/
protected $value;
/**
* @var string|null Type of JSON, expression should be casted to. Defaults to `null`, meaning
* no explicit casting will be performed.
* This property will be encountered only for DBMSs that support different types of JSON.
* For example, PostgreSQL has `json` and `jsonb` types.
*/
protected $type;
/**
* JsonExpression constructor.
*
* @param mixed $value the value to be encoded to JSON.
* The value must be compatible with [\yii\helpers\Json::encode()|Json::encode()]] requirements.
* @param string|null $type the type of the JSON. See [[JsonExpression::type]]
*
* @see type
*/
public function __construct($value, $type = null)
{
$this->value = $value;
$this->type = $type;
}
/**
* @return mixed
* @see value
*/
public function getValue()
{
return $this->value;
}
/**
* @return null|string the type of JSON
* @see type
*/
public function getType()
{
return $this->type;
}
}

59
framework/db/PdoValue.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace yii\db;
/**
* Class PdoValue represents a $value that should be bound to PDO with exact $type.
*
* For example, it will be useful when you need to bind binary data to BLOB column in DBMS:
*
* ```php
* [':name' => 'John', ':profile' => new PdoValue($profile, \PDO::PARAM_LOB)]`.
* ```
*
* To see possible types, check [PDO::PARAM_* constants](http://php.net/manual/en/pdo.constants.php).
*
* @see http://php.net/manual/en/pdostatement.bindparam.php
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
final class PdoValue implements ExpressionInterface
{
/**
* @var mixed
*/
private $value;
/**
* @var int One of PDO_PARAM_* constants
* @see http://php.net/manual/en/pdo.constants.php
*/
private $type;
/**
* PdoValue constructor.
*
* @param $value
* @param $type
*/
public function __construct($value, $type)
{
$this->value = $value;
$this->type = $type;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @return int
*/
public function getType()
{
return $this->type;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace yii\db;
/**
* Class PdoValue builder builds object of the [[PdoValue]] expression class.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class PdoValueBuilder implements ExpressionBuilderInterface
{
const PARAM_PREFIX = ':pv';
/**
* {@inheritdoc}
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$placeholder = static::PARAM_PREFIX . count($params);
$params[$placeholder] = $expression;
return $placeholder;
}
}

View File

@@ -97,7 +97,7 @@ class Query extends Component implements QueryInterface
*/
public $join;
/**
* @var string|array|Expression the condition to be applied in the GROUP BY clause.
* @var string|array|ExpressionInterface the condition to be applied in the GROUP BY clause.
* It can be either a string or an array. Please refer to [[where()]] on how to specify the condition.
*/
public $having;
@@ -414,7 +414,7 @@ class Query extends Component implements QueryInterface
/**
* Queries a scalar value by setting [[select]] first.
* Restores the value of select to make this query reusable.
* @param string|Expression $selectExpression
* @param string|ExpressionInterface $selectExpression
* @param Connection|null $db
* @return bool|string
*/
@@ -567,12 +567,12 @@ PATTERN;
/**
* Sets the SELECT part of the query.
* @param string|array|Expression $columns the columns to be selected.
* @param string|array|ExpressionInterface $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']).
* Columns can be prefixed with table names (e.g. "user.id") and/or contain column aliases (e.g. "user.id AS user_id").
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression). A DB expression may also be passed in form of
* an [[Expression]] object.
* an [[ExpressionInterface]] object.
*
* Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should
* use an array to specify the columns. Otherwise, the expression may be incorrectly split into several parts.
@@ -589,7 +589,7 @@ PATTERN;
*/
public function select($columns, $option = null)
{
if ($columns instanceof Expression) {
if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
@@ -609,14 +609,14 @@ PATTERN;
* $query->addSelect(["*", "CONCAT(first_name, ' ', last_name) AS full_name"])->one();
* ```
*
* @param string|array|Expression $columns the columns to add to the select. See [[select()]] for more
* @param string|array|ExpressionInterface $columns the columns to add to the select. See [[select()]] for more
* details about the format of this parameter.
* @return $this the query object itself
* @see select()
*/
public function addSelect($columns)
{
if ($columns instanceof Expression) {
if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
@@ -689,7 +689,7 @@ PATTERN;
/**
* Sets the FROM part of the query.
* @param string|array|Expression $tables the table(s) to be selected from. This can be either a string (e.g. `'user'`)
* @param string|array|ExpressionInterface $tables the table(s) to be selected from. This can be either a string (e.g. `'user'`)
* or an array (e.g. `['user', 'profile']`) specifying one or several table names.
* Table names can contain schema prefixes (e.g. `'public.user'`) and/or table aliases (e.g. `'user u'`).
* The method will automatically quote the table names unless it contains some parenthesis
@@ -701,7 +701,7 @@ PATTERN;
* Use a Query object to represent a sub-query. In this case, the corresponding array key will be used
* as the alias for the sub-query.
*
* To specify the `FROM` part in plain SQL, you may pass an instance of [[Expression]].
* To specify the `FROM` part in plain SQL, you may pass an instance of [[ExpressionInterface]].
*
* Here are some examples:
*
@@ -743,7 +743,7 @@ PATTERN;
*
* {@inheritdoc}
*
* @param string|array|Expression $condition the conditions that should be put in the WHERE part.
* @param string|array|ExpressionInterface $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 $this the query object itself
* @see andWhere()
@@ -760,7 +760,7 @@ PATTERN;
/**
* 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|Expression $condition the new WHERE condition. Please refer to [[where()]]
* @param string|array|ExpressionInterface $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 $this the query object itself
@@ -783,7 +783,7 @@ PATTERN;
/**
* 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|Expression $condition the new WHERE condition. Please refer to [[where()]]
* @param string|array|ExpressionInterface $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 $this the query object itself
@@ -949,7 +949,7 @@ PATTERN;
/**
* Sets the GROUP BY part of the query.
* @param string|array|Expression $columns the columns to be grouped by.
* @param string|array|ExpressionInterface $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).
@@ -958,13 +958,13 @@ PATTERN;
* to represent the group-by information. Otherwise, the method will not be able to correctly determine
* the group-by columns.
*
* Since version 2.0.7, an [[Expression]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
* Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
* @return $this the query object itself
* @see addGroupBy()
*/
public function groupBy($columns)
{
if ($columns instanceof Expression) {
if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
@@ -984,13 +984,13 @@ PATTERN;
* to represent the group-by information. Otherwise, the method will not be able to correctly determine
* the group-by columns.
*
* Since version 2.0.7, an [[Expression]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
* Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
* @return $this the query object itself
* @see groupBy()
*/
public function addGroupBy($columns)
{
if ($columns instanceof Expression) {
if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
@@ -1006,7 +1006,7 @@ PATTERN;
/**
* Sets the HAVING part of the query.
* @param string|array|Expression $condition the conditions to be put after HAVING.
* @param string|array|ExpressionInterface $condition the conditions to be put after HAVING.
* Please refer to [[where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
@@ -1023,7 +1023,7 @@ PATTERN;
/**
* Adds an additional HAVING condition to the existing one.
* The new condition and the existing one will be joined using the `AND` operator.
* @param string|array|Expression $condition the new HAVING condition. Please refer to [[where()]]
* @param string|array|ExpressionInterface $condition the new HAVING 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 $this the query object itself
@@ -1044,7 +1044,7 @@ PATTERN;
/**
* Adds an additional HAVING condition to the existing one.
* The new condition and the existing one will be joined using the `OR` operator.
* @param string|array|Expression $condition the new HAVING condition. Please refer to [[where()]]
* @param string|array|ExpressionInterface $condition the new HAVING 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 $this the query object itself

View File

@@ -9,7 +9,8 @@ namespace yii\db;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\helpers\ArrayHelper;
use yii\db\conditions\ConditionInterface;
use yii\db\conditions\HashCondition;
use yii\helpers\StringHelper;
/**
@@ -50,39 +51,55 @@ class QueryBuilder extends \yii\base\BaseObject
/**
* @var array map of query condition to builder methods.
* These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
* @deprecated since 2.0.14. Is not used, will be dropped in 2.1.0
*/
protected $conditionBuilders = [
'NOT' => 'buildNotCondition',
'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',
'EXISTS' => 'buildExistsCondition',
'NOT EXISTS' => 'buildExistsCondition',
];
/**
* @var array map of chars to their replacements in LIKE conditions.
* By default it's configured to escape `%`, `_` and `\` with `\`.
* @since 2.0.12.
*/
protected $likeEscapingReplacements = [
'%' => '\%',
'_' => '\_',
'\\' => '\\\\',
];
/**
* @var string|null character used to escape special characters in LIKE conditions.
* By default it's assumed to be `\`.
* @since 2.0.12
*/
protected $likeEscapeCharacter;
protected $conditionBuilders = [];
/**
* @var array map of condition aliases to condition classes. For example:
*
* ```php
* return [
* 'LIKE' => yii\db\condition\LikeCondition::class,
* ];
* ```
*
* This property is used by [[createConditionFromArray]] method.
* See default condition classes list in [[defaultConditionClasses()]] method.
*
* In case you want to add custom conditions support, use the [[setConditionClasses()]] method.
*
* @see setConditonClasses()
* @see defaultConditionClasses()
* @since 2.0.14
*/
protected $conditionClasses = [];
/**
* @var string[]|ExpressionBuilderInterface[] maps expression class to expression builder class.
* For example:
*
* ```php
* [
* yii\db\Expression::class => yii\db\ExpressionBuilder::class
* ]
* ```
* This property is mainly used by [[buildExpression()]] to build SQL expressions form expression objects.
* See default values in [[defaultExpressionBuilders()]] method.
*
*
* To override existing builders or add custom, use [[setExpressionBuilder()]] method. New items will be added
* to the end of this array.
*
* To find a builder, [[buildExpression()]] will check the expression class for its exact presence in this map.
* In case it is NOT present, the array will be iterated in reverse direction, checking whether the expression
* extends the class, defined in this map.
*
* @see setExpressionBuilders()
* @see defaultExpressionBuilders()
* @since 2.0.14
*/
protected $expressionBuilders = [];
/**
* Constructor.
@@ -95,8 +112,85 @@ class QueryBuilder extends \yii\base\BaseObject
parent::__construct($config);
}
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
$this->expressionBuilders = array_merge($this->defaultExpressionBuilders(), $this->expressionBuilders);
$this->conditionClasses = array_merge($this->defaultConditionClasses(), $this->conditionClasses);
}
/**
* Contains array of default condition classes. Extend this method, if you want to change
* default condition classes for the query builder. See [[conditionClasses]] docs for details.
*
* @return array
* @see conditionClasses
* @since 2.0.14
*/
protected function defaultConditionClasses()
{
return [
'NOT' => 'yii\db\conditions\NotCondition',
'AND' => 'yii\db\conditions\AndCondition',
'OR' => 'yii\db\conditions\OrCondition',
'BETWEEN' => 'yii\db\conditions\BetweenCondition',
'NOT BETWEEN' => 'yii\db\conditions\BetweenCondition',
'IN' => 'yii\db\conditions\InCondition',
'NOT IN' => 'yii\db\conditions\InCondition',
'LIKE' => 'yii\db\conditions\LikeCondition',
'NOT LIKE' => 'yii\db\conditions\LikeCondition',
'OR LIKE' => 'yii\db\conditions\LikeCondition',
'OR NOT LIKE' => 'yii\db\conditions\LikeCondition',
'EXISTS' => 'yii\db\conditions\ExistsCondition',
'NOT EXISTS' => 'yii\db\conditions\ExistsCondition',
];
}
/**
* Contains array of default expression builders. Extend this method and override it, if you want to change
* default expression builders for this query builder. See [[expressionBuilders]] docs for details.
*
* @return array
* @see $expressionBuilders
* @since 2.0.14
*/
protected function defaultExpressionBuilders()
{
return [
'yii\db\PdoValue' => 'yii\db\PdoValueBuilder',
'yii\db\Expression' => 'yii\db\ExpressionBuilder',
'yii\db\conditions\ConjunctionCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
'yii\db\conditions\NotCondition' => 'yii\db\conditions\NotConditionBuilder',
'yii\db\conditions\AndCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
'yii\db\conditions\OrCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
'yii\db\conditions\BetweenCondition' => 'yii\db\conditions\BetweenConditionBuilder',
'yii\db\conditions\InCondition' => 'yii\db\conditions\InConditionBuilder',
'yii\db\conditions\LikeCondition' => 'yii\db\conditions\LikeConditionBuilder',
'yii\db\conditions\ExistsCondition' => 'yii\db\conditions\ExistsConditionBuilder',
'yii\db\conditions\SimpleCondition' => 'yii\db\conditions\SimpleConditionBuilder',
'yii\db\conditions\HashCondition' => 'yii\db\conditions\HashConditionBuilder',
];
}
/**
* Setter for [[expressionBuilders]] property.
*
* @param string[] $builders array of builder that should be merged with [[expressionBuilders]]
* @since 2.0.14
* @see expressionBuilders
*/
public function setExpressionBuilders($builders)
{
$this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
}
/**
* Generates a SELECT SQL statement from a [[Query]] object.
*
* @param Query $query the [[Query]] object from which the SQL statement will be generated.
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the query building process.
@@ -124,15 +218,15 @@ class QueryBuilder extends \yii\base\BaseObject
if (!empty($query->orderBy)) {
foreach ($query->orderBy as $expression) {
if ($expression instanceof Expression) {
$params = array_merge($params, $expression->params);
if ($expression instanceof ExpressionInterface) {
$this->buildExpression($expression, $params);
}
}
}
if (!empty($query->groupBy)) {
foreach ($query->groupBy as $expression) {
if ($expression instanceof Expression) {
$params = array_merge($params, $expression->params);
if ($expression instanceof ExpressionInterface) {
$this->buildExpression($expression, $params);
}
}
}
@@ -145,6 +239,43 @@ class QueryBuilder extends \yii\base\BaseObject
return [$sql, $params];
}
/**
* Builds given $expression
*
* @param ExpressionInterface $expression the expression to be built
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the expression building process.
* @return string the SQL statement that will not be neither quoted nor encoded before passing to DBMS
* @see ExpressionInterface
* @see ExpressionBuilderInterface
* @see expressionBuilders
* @since 2.0.14
* @throws InvalidParamException when $expression building is not supported by this QueryBuilder.
*/
public function buildExpression(ExpressionInterface $expression, &$params = [])
{
$className = get_class($expression);
if (!isset($this->expressionBuilders[$className])) {
foreach (array_reverse($this->expressionBuilders) as $expressionClass => $builderClass) {
if (is_subclass_of($expression, $expressionClass)) {
$this->expressionBuilders[$className] = $builderClass;
break;
}
}
if (!isset($this->expressionBuilders[$className])) {
throw new InvalidParamException('Expression of class ' . $className . ' can not be built in ' . get_class($this));
}
}
if (!is_object($this->expressionBuilders[$className])) {
$this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
}
$builder = $this->expressionBuilders[$className];
return $builder->build($expression, $params);
}
/**
* Creates an INSERT SQL statement.
*
@@ -183,18 +314,15 @@ class QueryBuilder extends \yii\base\BaseObject
} else {
foreach ($columns as $name => $value) {
$names[] = $schema->quoteColumnName($name);
if ($value instanceof Expression) {
$placeholders[] = $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
$value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
if ($value instanceof ExpressionInterface) {
$placeholders[] = $this->buildExpression($value, $params);
} elseif ($value instanceof \yii\db\Query) {
list($sql, $params) = $this->build($value, $params);
$placeholders[] = "($sql)";
} else {
$phName = self::PARAM_PREFIX . count($params);
$placeholders[] = $phName;
$params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
$placeholders[] = $this->bindParam($value, $params);
}
}
}
@@ -276,7 +404,7 @@ class QueryBuilder extends \yii\base\BaseObject
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -335,15 +463,14 @@ class QueryBuilder extends \yii\base\BaseObject
$lines = [];
foreach ($columns as $name => $value) {
if ($value instanceof Expression) {
$lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
if ($value instanceof ExpressionInterface) {
$lines[] = $this->db->quoteColumnName($name) . '=' . $this->buildExpression($value, $params);
} else {
$phName = self::PARAM_PREFIX . count($params);
$phName = $this->bindParam(
isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value,
$params
);
$lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
$params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
}
}
@@ -919,13 +1046,12 @@ class QueryBuilder extends \yii\base\BaseObject
}
foreach ($columns as $i => $column) {
if ($column instanceof Expression) {
if ($column instanceof ExpressionInterface) {
if (is_int($i)) {
$columns[$i] = $column->expression;
$columns[$i] = $this->buildExpression($column, $params);
} else {
$columns[$i] = $column->expression . ' AS ' . $this->db->quoteColumnName($i);
$columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
}
$params = array_merge($params, $column->params);
} elseif ($column instanceof Query) {
list($sql, $params) = $this->build($column, $params);
$columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
@@ -1046,8 +1172,8 @@ class QueryBuilder extends \yii\base\BaseObject
return '';
}
foreach ($columns as $i => $column) {
if ($column instanceof Expression) {
$columns[$i] = $column->expression;
if ($column instanceof ExpressionInterface) {
$columns[$i] = $this->buildExpression($column);
} elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
@@ -1101,8 +1227,8 @@ class QueryBuilder extends \yii\base\BaseObject
}
$orders = [];
foreach ($columns as $name => $direction) {
if ($direction instanceof Expression) {
$orders[] = $direction->expression;
if ($direction instanceof ExpressionInterface) {
$orders[] = $this->buildExpression($direction);
} else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
}
@@ -1136,7 +1262,7 @@ class QueryBuilder extends \yii\base\BaseObject
*/
protected function hasLimit($limit)
{
return ($limit instanceof Expression) || ctype_digit((string) $limit);
return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
}
/**
@@ -1146,7 +1272,7 @@ class QueryBuilder extends \yii\base\BaseObject
*/
protected function hasOffset($offset)
{
return ($offset instanceof Expression) || ctype_digit((string) $offset) && (string) $offset !== '0';
return ($offset instanceof ExpressionInterface) || ctype_digit((string) $offset) && (string) $offset !== '0';
}
/**
@@ -1194,8 +1320,8 @@ class QueryBuilder extends \yii\base\BaseObject
}
}
foreach ($columns as $i => $column) {
if ($column instanceof Expression) {
$columns[$i] = $column->expression;
if ($column instanceof ExpressionInterface) {
$columns[$i] = $this->buildExpression($column);
} elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
@@ -1206,38 +1332,51 @@ class QueryBuilder extends \yii\base\BaseObject
/**
* Parses the condition specification and generates the corresponding SQL expression.
* @param string|array|Expression $condition the condition specification. Please refer to [[Query::where()]]
* @param string|array|ExpressionInterface $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
*/
public function buildCondition($condition, &$params)
{
if ($condition instanceof Expression) {
foreach ($condition->params as $n => $v) {
$params[$n] = $v;
}
return $condition->expression;
} elseif (!is_array($condition)) {
return (string) $condition;
} elseif (empty($condition)) {
return '';
if (is_array($condition)) {
$condition = $this->createConditionFromArray($condition);
}
if ($condition instanceof ExpressionInterface) {
return $this->buildExpression($condition, $params);
}
if (empty($condition)) {
return '';
}
return (string) $condition;
}
/**
* Transforms $condition defined in array format (as described in [[Query::where()]]
* to instance of [[yii\db\condition\ConditionInterface|ConditionInterface]] according to
* [[conditionClasses]] map.
*
* @param string|array $condition
* @see conditionClasses
* @return ConditionInterface
* @since 2.0.14
*/
public function createConditionFromArray($condition)
{
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
$operator = strtoupper($condition[0]);
if (isset($this->conditionBuilders[$operator])) {
$method = $this->conditionBuilders[$operator];
$operator = strtoupper(array_shift($condition));
if (isset($this->conditionClasses[$operator])) {
$className = $this->conditionClasses[$operator];
} else {
$method = 'buildSimpleCondition';
$className = 'yii\db\conditions\SimpleCondition';
}
array_shift($condition);
return $this->$method($operator, $condition, $params);
/** @var ConditionInterface $className */
return $className::fromArrayDefinition($operator, $condition);
}
// hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($condition, $params);
return new HashCondition($condition);
}
/**
@@ -1245,34 +1384,11 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $condition the condition specification.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildHashCondition($condition, &$params)
{
$parts = [];
foreach ($condition as $column => $value) {
if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
// IN condition
$parts[] = $this->buildInCondition('IN', [$column, $value], $params);
} else {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if ($value === null) {
$parts[] = "$column IS NULL";
} elseif ($value instanceof Expression) {
$parts[] = "$column=" . $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
} else {
$phName = self::PARAM_PREFIX . count($params);
$parts[] = "$column=$phName";
$params[$phName] = $value;
}
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
return $this->buildCondition(new HashCondition($condition), $params);
}
/**
@@ -1281,29 +1397,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $operands the SQL expressions to connect.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildAndCondition($operator, $operands, &$params)
{
$parts = [];
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->buildCondition($operand, $params);
}
if ($operand instanceof Expression) {
foreach ($operand->params as $n => $v) {
$params[$n] = $v;
}
$operand = $operand->expression;
}
if ($operand !== '') {
$parts[] = $operand;
}
}
if (!empty($parts)) {
return '(' . implode(") $operator (", $parts) . ')';
}
return '';
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1313,22 +1412,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildNotCondition($operator, $operands, &$params)
{
if (count($operands) !== 1) {
throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
}
$operand = reset($operands);
if (is_array($operand) || $operand instanceof Expression) {
$operand = $this->buildCondition($operand, $params);
}
if ($operand === '') {
return '';
}
return "$operator ($operand)";
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1339,38 +1428,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildBetweenCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new InvalidParamException("Operator '$operator' requires three operands.");
}
list($column, $value1, $value2) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if ($value1 instanceof Expression) {
foreach ($value1->params as $n => $v) {
$params[$n] = $v;
}
$phName1 = $value1->expression;
} else {
$phName1 = self::PARAM_PREFIX . count($params);
$params[$phName1] = $value1;
}
if ($value2 instanceof Expression) {
foreach ($value2->params as $n => $v) {
$params[$n] = $v;
}
$phName2 = $value2->expression;
} else {
$phName2 = self::PARAM_PREFIX . count($params);
$params[$phName2] = $value2;
}
return "$column $operator $phName1 AND $phName2";
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1384,134 +1447,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @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.
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildInCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
if ($column === []) {
// no columns to test against
return $operator === 'IN' ? '0=1' : '';
}
if ($values instanceof Query) {
return $this->buildSubqueryInCondition($operator, $column, $values, $params);
}
if (!is_array($values) && !$values instanceof \Traversable) {
// ensure values is an array
$values = (array) $values;
}
if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
return $this->buildCompositeInCondition($operator, $column, $values, $params);
} elseif (is_array($column)) {
$column = reset($column);
}
$sqlValues = [];
foreach ($values as $i => $value) {
if (is_array($value) || $value instanceof \ArrayAccess) {
$value = isset($value[$column]) ? $value[$column] : null;
}
if ($value === null) {
$sqlValues[$i] = 'NULL';
} elseif ($value instanceof Expression) {
$sqlValues[$i] = $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
$sqlValues[$i] = $phName;
}
}
if (empty($sqlValues)) {
return $operator === 'IN' ? '0=1' : '';
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if (count($sqlValues) > 1) {
return "$column $operator (" . implode(', ', $sqlValues) . ')';
}
$operator = $operator === 'IN' ? '=' : '<>';
return $column . $operator . reset($sqlValues);
}
/**
* Builds SQL for IN condition.
*
* @param string $operator
* @param array $columns
* @param Query $values
* @param array $params
* @return string SQL
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{
list($sql, $params) = $this->build($values, $params);
if (is_array($columns)) {
foreach ($columns as $i => $col) {
if (strpos($col, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($col);
}
}
return '(' . implode(', ', $columns) . ") $operator ($sql)";
}
if (strpos($columns, '(') === false) {
$columns = $this->db->quoteColumnName($columns);
}
return "$columns $operator ($sql)";
}
/**
* Builds SQL for IN condition.
*
* @param string $operator
* @param array|\Traversable $columns
* @param array $values
* @param array $params
* @return string SQL
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $column) {
if (isset($value[$column])) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value[$column];
$vs[] = $phName;
} else {
$vs[] = 'NULL';
}
}
$vss[] = '(' . implode(', ', $vs) . ')';
}
if (empty($vss)) {
return $operator === 'IN' ? '0=1' : '';
}
$sqlColumns = [];
foreach ($columns as $i => $column) {
$sqlColumns[] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
}
return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1533,56 +1474,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildLikeCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
$escape = isset($operands[2]) ? $operands[2] : $this->likeEscapingReplacements;
unset($operands[2]);
if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
throw new InvalidParamException("Invalid operator '$operator'.");
}
$andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
$not = !empty($matches[3]);
$operator = $matches[2];
list($column, $values) = $operands;
if (!is_array($values)) {
$values = [$values];
}
if (empty($values)) {
return $not ? '' : '0=1';
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$parts = [];
foreach ($values as $value) {
if ($value instanceof Expression) {
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
$phName = $value->expression;
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
}
$escapeSql = '';
if ($this->likeEscapeCharacter !== null) {
$escapeSql = " ESCAPE '{$this->likeEscapeCharacter}'";
}
$parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
}
return implode($andor, $parts);
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1592,15 +1489,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if the operand is not a [[Query]] object.
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildExistsCondition($operator, $operands, &$params)
{
if ($operands[0] instanceof Query) {
list($sql, $params) = $this->build($operands[0], $params);
return "$operator ($sql)";
}
throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1610,35 +1504,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
* @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildSimpleCondition($operator, $operands, &$params)
{
if (count($operands) !== 2) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $value) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if ($value === null) {
return "$column $operator NULL";
} elseif ($value instanceof Expression) {
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
return "$column $operator {$value->expression}";
} elseif ($value instanceof Query) {
list($sql, $params) = $this->build($value, $params);
return "$column $operator ($sql)";
}
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
return "$column $operator $phName";
array_unshift($operands, $operator);
return $this->buildCondition($operands, $params);
}
/**
@@ -1651,4 +1522,21 @@ class QueryBuilder extends \yii\base\BaseObject
{
return 'SELECT EXISTS(' . $rawSql . ')';
}
/**
* Helper method to add $value to $params array using [[PARAM_PREFIX]].
*
* @param string|null $value
* @param array $params passed by reference
* @return string the placeholder name in $params array
*
* @since 2.0.14
*/
public function bindParam($value, &$params)
{
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
return $phName;
}
}

View File

@@ -27,13 +27,13 @@ trait QueryTrait
*/
public $where;
/**
* @var int|Expression maximum number of records to be returned. May be an instance of [[Expression]].
* @var int|ExpressionInterface maximum number of records to be returned. May be an instance of [[ExpressionInterface]].
* If not set or less than 0, it means no limit.
*/
public $limit;
/**
* @var int|Expression zero-based offset from where the records are to be returned.
* May be an instance of [[Expression]]. If not set or less than 0, it means starting from the beginning.
* @var int|ExpressionInterface zero-based offset from where the records are to be returned.
* May be an instance of [[ExpressionInterface]]. If not set or less than 0, it means starting from the beginning.
*/
public $offset;
/**
@@ -41,7 +41,7 @@ trait QueryTrait
* The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which
* can be either [SORT_ASC](http://php.net/manual/en/array.constants.php#constant.sort-asc)
* or [SORT_DESC](http://php.net/manual/en/array.constants.php#constant.sort-desc).
* The array may also contain [[Expression]] objects. If that is the case, the expressions
* The array may also contain [[ExpressionInterface]] objects. If that is the case, the expressions
* will be converted into strings without any change.
*/
public $orderBy;
@@ -305,7 +305,7 @@ trait QueryTrait
/**
* Sets the ORDER BY part of the query.
* @param string|array|Expression $columns the columns (and the directions) to be ordered by.
* @param string|array|ExpressionInterface $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. `"id ASC, name DESC"`) or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
*
@@ -316,7 +316,7 @@ trait QueryTrait
* to represent the order-by information. Otherwise, the method will not be able to correctly determine
* the order-by columns.
*
* Since version 2.0.7, an [[Expression]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
* Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
* @return $this the query object itself
* @see addOrderBy()
*/
@@ -328,7 +328,7 @@ trait QueryTrait
/**
* Adds additional ORDER BY columns to the query.
* @param string|array|Expression $columns the columns (and the directions) to be ordered by.
* @param string|array|ExpressionInterface $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
*
@@ -339,7 +339,7 @@ trait QueryTrait
* to represent the order-by information. Otherwise, the method will not be able to correctly determine
* the order-by columns.
*
* Since version 2.0.7, an [[Expression]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
* Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
* @return $this the query object itself
* @see orderBy()
*/
@@ -358,12 +358,12 @@ trait QueryTrait
/**
* Normalizes format of ORDER BY data.
*
* @param array|string|Expression $columns the columns value to normalize. See [[orderBy]] and [[addOrderBy]].
* @param array|string|ExpressionInterface $columns the columns value to normalize. See [[orderBy]] and [[addOrderBy]].
* @return array
*/
protected function normalizeOrderBy($columns)
{
if ($columns instanceof Expression) {
if ($columns instanceof ExpressionInterface) {
return [$columns];
} elseif (is_array($columns)) {
return $columns;
@@ -384,7 +384,7 @@ trait QueryTrait
/**
* Sets the LIMIT part of the query.
* @param int|Expression|null $limit the limit. Use null or negative value to disable limit.
* @param int|ExpressionInterface|null $limit the limit. Use null or negative value to disable limit.
* @return $this the query object itself
*/
public function limit($limit)
@@ -395,7 +395,7 @@ trait QueryTrait
/**
* Sets the OFFSET part of the query.
* @param int|Expression|null $offset the offset. Use null or negative value to disable offset.
* @param int|ExpressionInterface|null $offset the offset. Use null or negative value to disable offset.
* @return $this the query object itself
*/
public function offset($offset)

View File

@@ -61,6 +61,7 @@ abstract class Schema extends BaseObject
const TYPE_BINARY = 'binary';
const TYPE_BOOLEAN = 'boolean';
const TYPE_MONEY = 'money';
const TYPE_JSON = 'json';
/**
* Schema cache version, to detect incompatibilities in cached values when the
* data format of the cache changes.
@@ -557,6 +558,7 @@ abstract class Schema extends BaseObject
'float' => 'double',
'double' => 'double',
'binary' => 'resource',
'json' => 'array',
];
if (isset($typeMap[$column->type])) {
if ($column->type === 'bigint') {

View File

@@ -0,0 +1,22 @@
<?php
namespace yii\db\conditions;
/**
* Condition that connects two or more SQL expressions with the `AND` operator.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class AndCondition extends ConjunctionCondition
{
/**
* Returns the operator that is represented by this condition class, e.g. `AND`, `OR`.
*
* @return string
*/
public function getOperator()
{
return 'AND';
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
/**
* Class BetweenCondition represents a `BETWEEN` condition.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class BetweenCondition implements ConditionInterface
{
/**
* @var string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
*/
protected $operator;
/**
* @var mixed the column name to the left of [[operator]]
*/
protected $column;
/**
* @var mixed beginning of the interval
*/
private $intervalStart;
/**
* @var mixed end of the interval
*/
private $intervalEnd;
/**
* Creates a condition with the `BETWEEN` operator.
*
* @param mixed $column the literal to the left of $operator
* @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
* @param mixed $intervalStart beginning of the interval
* @param mixed $intervalEnd end of the interval
*/
public function __construct($column, $operator, $intervalStart, $intervalEnd)
{
$this->column = $column;
$this->operator = $operator;
$this->intervalStart = $intervalStart;
$this->intervalEnd = $intervalEnd;
}
/**
* @return string
*/
public function getOperator()
{
return $this->operator;
}
/**
* @return mixed
*/
public function getColumn()
{
return $this->column;
}
/**
* @return mixed
*/
public function getIntervalStart()
{
return $this->intervalStart;
}
/**
* @return mixed
*/
public function getIntervalEnd()
{
return $this->intervalEnd;
}
/**
* {@inheritdoc}
* @throws InvalidParamException if wrong number of operands have been given.
*/
public static function fromArrayDefinition($operator, $operands)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new InvalidParamException("Operator '$operator' requires three operands.");
}
return new static($operands[0], $operator, $operands[1], $operands[2]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
/**
* Class BetweenConditionBuilder builds objects of [[BetweenCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class BetweenConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|BetweenCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$operator = $expression->getOperator();
$column = $expression->getColumn();
if (strpos($column, '(') === false) {
$column = $this->queryBuilder->db->quoteColumnName($column);
}
$phName1 = $this->createPlaceholder($expression->getIntervalStart(), $params);
$phName2 = $this->createPlaceholder($expression->getIntervalEnd(), $params);
return "$column $operator $phName1 AND $phName2";
}
/**
* Attaches $value to $params array and returns placeholder.
*
* @param mixed $value
* @param array $params passed by reference
* @return string
*/
protected function createPlaceholder($value, &$params)
{
if ($value instanceof ExpressionInterface) {
return $this->queryBuilder->buildExpression($value, $params);
}
return $this->queryBuilder->bindParam($value, $params);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
use yii\db\ExpressionInterface;
/**
* Interface ConditionInterface should be implemented by classes that represent a condition
* in DBAL of framework.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
interface ConditionInterface extends ExpressionInterface
{
/**
* Creates object by array-definition as described in
* [Query Builder  Operator format](guide:db-query-builder#operator-format) guide article.
*
* @param string $operator operator in uppercase.
* @param array $operands array of corresponding operands
*
* @return $this
* @throws InvalidParamException if input parameters are not suitable for this condition
*/
public static function fromArrayDefinition($operator, $operands);
}

View File

@@ -0,0 +1,47 @@
<?php
namespace yii\db\conditions;
/**
* Class ConjunctionCondition
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
abstract class ConjunctionCondition implements ConditionInterface
{
/**
* @var mixed[]
*/
protected $expressions;
/**
* @param mixed $expressions
*/
public function __construct($expressions) // TODO: use variadic params when PHP>5.6
{
$this->expressions = $expressions;
}
/**
* @return mixed[]
*/
public function getExpressions()
{
return $this->expressions;
}
/**
* Returns the operator that is represented by this condition class, e.g. `AND`, `OR`.
* @return string
*/
abstract public function getOperator();
/**
* {@inheritdoc}
*/
public static function fromArrayDefinition($operator, $operands)
{
return new static($operands);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
/**
* Class ConjunctionConditionBuilder builds objects of abstract class [[ConjunctionCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ConjunctionConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|ConjunctionCondition $condition the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $condition, array &$params = [])
{
$parts = $this->buildExpressionsFrom($condition, $params);
if (!empty($parts)) {
return '(' . implode(") {$condition->getOperator()} (", $parts) . ')';
}
return '';
}
/**
* Builds expressions, that are stored in $condition
*
* @param ExpressionInterface|ConjunctionCondition $condition the expression to be built.
* @param array $params the binding parameters.
* @return string[]
*/
private function buildExpressionsFrom(ExpressionInterface $condition, &$params = [])
{
$parts = [];
foreach ($condition->getExpressions() as $condition) {
if (is_array($condition)) {
$condition = $this->queryBuilder->buildCondition($condition, $params);
}
if ($condition instanceof ExpressionInterface) {
$condition = $this->queryBuilder->buildExpression($condition, $params);
}
if ($condition !== '') {
$parts[] = $condition;
}
}
return $parts;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
use yii\db\Query;
/**
* Condition that represents `EXISTS` operator.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ExistsCondition implements ConditionInterface
{
/**
* @var string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
*/
private $operator;
/**
* @var Query the [[Query]] object representing the sub-query.
*/
private $query;
/**
* ExistsCondition constructor.
*
* @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
* @param Query $query the [[Query]] object representing the sub-query.
*/
public function __construct($operator, $query)
{
$this->operator = $operator;
$this->query = $query;
}
/**
* {@inheritdoc}
*/
public static function fromArrayDefinition($operator, $operands)
{
if (!isset($operands[0]) || !$operands[0] instanceof Query) {
throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
}
return new static($operator, $operands[0]);
}
/**
* @return string
*/
public function getOperator()
{
return $this->operator;
}
/**
* @return Query
*/
public function getQuery()
{
return $this->query;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
/**
* Class ExistsConditionBuilder builds objects of [[ExistsCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ExistsConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|ExistsCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$operator = $expression->getOperator();
$query = $expression->getQuery();
list($sql, $params) = $this->queryBuilder->build($query, $params);
return "$operator ($sql)";
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace yii\db\conditions;
/**
* Condition based on column-value pairs.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class HashCondition implements ConditionInterface
{
/**
* @var array|null the condition specification.
*/
private $hash;
/**
* HashCondition constructor.
*
* @param array|null $hash
*/
public function __construct($hash)
{
$this->hash = $hash;
}
/**
* @return array|null
*/
public function getHash()
{
return $this->hash;
}
/**
* {@inheritdoc}
*/
public static function fromArrayDefinition($operator, $operands)
{
return new static($operands);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
use yii\db\Query;
use yii\helpers\ArrayHelper;
/**
* Class HashConditionBuilder builds objects of [[HashCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class HashConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|HashCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$hash = $expression->getHash();
$parts = [];
foreach ($hash as $column => $value) {
if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
// IN condition
$parts[] = $this->queryBuilder->buildCondition(new InCondition($column, 'IN', $value), $params);
} else {
if (strpos($column, '(') === false) {
$column = $this->queryBuilder->db->quoteColumnName($column);
}
if ($value === null) {
$parts[] = "$column IS NULL";
} elseif ($value instanceof ExpressionInterface) {
$parts[] = "$column=" . $this->queryBuilder->buildExpression($value, $params);
} else {
$phName = $this->queryBuilder->bindParam($value, $params);
$parts[] = "$column=$phName";
}
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
/**
* Class LikeCondition represents `IN` condition.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class InCondition implements ConditionInterface
{
/**
* @var string $operator the operator to use (e.g. `IN` or `NOT IN`)
*/
protected $operator;
/**
* @var string|string[] the column name. If it is an array, a composite `IN` condition
* will be generated.
*/
protected $column;
/**
* @var array 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`.
*/
protected $values;
/**
* SimpleCondition constructor
*
* @param string|string[] the column name. If it is an array, a composite `IN` condition
* will be generated.
* @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
* @param array 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`.
*/
public function __construct($column, $operator, $values)
{
$this->column = $column;
$this->operator = $operator;
$this->values = $values;
}
/**
* @return string
*/
public function getOperator()
{
return $this->operator;
}
/**
* @return mixed
*/
public function getColumn()
{
return $this->column;
}
/**
* @return mixed
*/
public function getValues()
{
return $this->values;
}
/**
* {@inheritdoc}
* @throws InvalidParamException if wrong number of operands have been given.
*/
public static function fromArrayDefinition($operator, $operands)
{
if (!isset($operands[0], $operands[1])) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
return new static($operands[0], $operator, $operands[1]);
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
use yii\db\Query;
/**
* Class InConditionBuilder builds objects of [[InCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class InConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|InCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$operator = $expression->getOperator();
$column = $expression->getColumn();
$values = $expression->getValues();
if ($column === []) {
// no columns to test against
return $operator === 'IN' ? '0=1' : '';
}
if ($values instanceof Query) {
return $this->buildSubqueryInCondition($operator, $column, $values, $params);
}
if (!is_array($values) && !$values instanceof \Traversable) {
// ensure values is an array
$values = (array) $values;
}
if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
return $this->buildCompositeInCondition($operator, $column, $values, $params);
}
if (is_array($column)) {
$column = reset($column);
}
$sqlValues = $this->buildValues($expression, $values, $params);
if (empty($sqlValues)) {
return $operator === 'IN' ? '0=1' : '';
}
if (strpos($column, '(') === false) {
$column = $this->queryBuilder->db->quoteColumnName($column);
}
if (count($sqlValues) > 1) {
return "$column $operator (" . implode(', ', $sqlValues) . ')';
}
$operator = $operator === 'IN' ? '=' : '<>';
return $column . $operator . reset($sqlValues);
}
/**
* Builds $values to be used in [[InCondition]]
*
* @param ConditionInterface|InCondition $condition
* @param array $values
* @param array $params the binding parameters
* @return array of prepared for SQL placeholders
*/
protected function buildValues(ConditionInterface $condition, $values, &$params)
{
$sqlValues = [];
$column = $condition->getColumn();
foreach ($values as $i => $value) {
if (is_array($value) || $value instanceof \ArrayAccess) {
$value = isset($value[$column]) ? $value[$column] : null;
}
if ($value === null) {
$sqlValues[$i] = 'NULL';
} elseif ($value instanceof ExpressionInterface) {
$sqlValues[$i] = $this->queryBuilder->buildExpression($value, $params);
} else {
$sqlValues[$i] = $this->queryBuilder->bindParam($value, $params);
}
}
return $sqlValues;
}
/**
* Builds SQL for IN condition.
*
* @param string $operator
* @param array|string $columns
* @param Query $values
* @param array $params
* @return string SQL
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{
list($sql, $params) = $this->queryBuilder->build($values, $params);
if (is_array($columns)) {
foreach ($columns as $i => $col) {
if (strpos($col, '(') === false) {
$columns[$i] = $this->queryBuilder->db->quoteColumnName($col);
}
}
return '(' . implode(', ', $columns) . ") $operator ($sql)";
}
if (strpos($columns, '(') === false) {
$columns = $this->queryBuilder->db->quoteColumnName($columns);
}
return "$columns $operator ($sql)";
}
/**
* Builds SQL for IN condition.
*
* @param string $operator
* @param array|\Traversable $columns
* @param array $values
* @param array $params
* @return string SQL
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $column) {
if (isset($value[$column])) {
$vs[] = $this->queryBuilder->bindParam($value[$column], $params);
} else {
$vs[] = 'NULL';
}
}
$vss[] = '(' . implode(', ', $vs) . ')';
}
if (empty($vss)) {
return $operator === 'IN' ? '0=1' : '';
}
$sqlColumns = [];
foreach ($columns as $i => $column) {
$sqlColumns[] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column;
}
return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
/**
* Class LikeCondition represents a `LIKE` condition.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class LikeCondition extends SimpleCondition
{
/**
* @var array map of chars to their replacements.
* By default it's set to `null` meaning responsibility is fully on condition builder.
*/
protected $escapingReplacements;
/**
* @param string $column the column name.
* @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
* @param string[]|string $value single value or an array of values that $column 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`.
*/
public function __construct($column, $operator, $value)
{
parent::__construct($column, $operator, $value);
}
/**
* This method allows to specify how to escape special characters in the value(s).
*
* @param array an array of mappings from the special characters to their escaped counterparts.
* You may use `false` or an empty array to indicate the values are already escaped and no escape
* should be applied. Note that when using an escape mapping (or the third operand is not provided),
* the values will be automatically enclosed within a pair of percentage characters.
*/
public function setEscapingReplacements($escapingReplacements)
{
$this->escapingReplacements = $escapingReplacements;
}
/**
* @return array
*/
public function getEscapingReplacements()
{
return $this->escapingReplacements;
}
/**
* {@inheritdoc}
* @throws InvalidParamException if wrong number of operands have been given.
*/
public static function fromArrayDefinition($operator, $operands)
{
if (!isset($operands[0], $operands[1])) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
$condition = new static($operands[0], $operator, $operands[1]);
if (isset($operands[2])) {
$condition->escapingReplacements = $operands[2];
}
return $condition;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
/**
* Class LikeConditionBuilder builds objects of [[LikeCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class LikeConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* @var array map of chars to their replacements in LIKE conditions.
* By default it's configured to escape `%`, `_` and `\` with `\`.
*/
protected $escapingReplacements = [
'%' => '\%',
'_' => '\_',
'\\' => '\\\\',
];
/**
* @var string|null character used to escape special characters in LIKE conditions.
* By default it's assumed to be `\`.
*/
protected $escapeCharacter;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|LikeCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$operator = $expression->getOperator();
$column = $expression->getColumn();
$values = $expression->getValue();
$escape = $expression->getEscapingReplacements() ?: $this->escapingReplacements;
list($andor, $not, $operator) = $this->parseOperator($operator);
if (!is_array($values)) {
$values = [$values];
}
if (empty($values)) {
return $not ? '' : '0=1';
}
if (strpos($column, '(') === false) {
$column = $this->queryBuilder->db->quoteColumnName($column);
}
$escapeSql = $this->getEscapeSql();
$parts = [];
foreach ($values as $value) {
if ($value instanceof ExpressionInterface) {
$phName = $this->queryBuilder->buildExpression($value, $params);
} else {
$phName = $this->queryBuilder->bindParam(empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'), $params);
}
$parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
}
return implode($andor, $parts);
}
/**
* @return string
*/
private function getEscapeSql()
{
if ($this->escapeCharacter !== null) {
return " ESCAPE '{$this->escapeCharacter}'";
}
return '';
}
/**
* @param string $operator
* @return array
*/
protected function parseOperator($operator)
{
if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
throw new InvalidParamException("Invalid operator '$operator'.");
}
$andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
$not = !empty($matches[3]);
$operator = $matches[2];
return [$andor, $not, $operator];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
/**
* Condition that inverts passed [[condition]].
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class NotCondition implements ConditionInterface
{
/**
* @var mixed the condition to be negated
*/
protected $condition;
/**
* NotCondition constructor.
*
* @param mixed $condition the condition to be negated
*/
public function __construct($condition)
{
$this->condition = $condition;
}
/**
* @return mixed
*/
public function getCondition()
{
return $this->condition;
}
/**
* {@inheritdoc}
* @throws InvalidParamException if wrong number of operands have been given.
*/
public static function fromArrayDefinition($operator, $operands)
{
if (count($operands) !== 1) {
throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
}
return new static(array_shift($operands));
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
/**
* Class NotConditionBuilder builds objects of [[NotCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class NotConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|NotCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$operand = $expression->getCondition();
if ($operand === '') {
return '';
}
$expession = $this->queryBuilder->buildCondition($operand, $params);
return "{$this->getNegationOperator()} ($expession)";
}
/**
* @return string
*/
protected function getNegationOperator()
{
return 'NOT';
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace yii\db\conditions;
/**
* Condition that connects two or more SQL expressions with the `AND` operator.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class OrCondition extends ConjunctionCondition
{
/**
* Returns the operator that is represented by this condition class, e.g. `AND`, `OR`.
*
* @return string
*/
public function getOperator()
{
return 'OR';
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace yii\db\conditions;
use yii\base\InvalidParamException;
/**
* Class SimpleCondition represents a simple condition like `"column" operator value`.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class SimpleCondition implements ConditionInterface
{
/**
* @var string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
*/
protected $operator;
/**
* @var mixed the column name to the left of [[operator]]
*/
protected $column;
/**
* @var mixed the value to the right of the [[operator]]
*/
protected $value;
/**
* SimpleCondition constructor
*
* @param mixed $column the literal to the left of $operator
* @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
* @param mixed $value the literal to the right of $operator
*/
public function __construct($column, $operator, $value)
{
$this->column = $column;
$this->operator = $operator;
$this->value = $value;
}
/**
* @return string
*/
public function getOperator()
{
return $this->operator;
}
/**
* @return mixed
*/
public function getColumn()
{
return $this->column;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* {@inheritdoc}
* @throws InvalidParamException if wrong number of operands have been given.
*/
public static function fromArrayDefinition($operator, $operands)
{
if (count($operands) !== 2) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
return new static($operands[0], $operator, $operands[1]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace yii\db\conditions;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
use yii\db\Query;
/**
* Class NotConditionBuilder builds objects of [[SimpleCondition]]
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class SimpleConditionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|SimpleCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$operator = $expression->getOperator();
$column = $expression->getColumn();
$value = $expression->getValue();
if (strpos($column, '(') === false) {
$column = $this->queryBuilder->db->quoteColumnName($column);
}
if ($value === null) {
return "$column $operator NULL";
}
if ($value instanceof ExpressionInterface) {
return "$column $operator {$this->queryBuilder->buildExpression($value, $params)}";
}
if ($value instanceof Query) {
list($sql, $params) = $this->queryBuilder->build($value, $params);
return "$column $operator ($sql)";
}
$phName = $this->queryBuilder->bindParam($value, $params);
return "$column $operator $phName";
}
}

View File

@@ -48,16 +48,12 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
*/
protected $likeEscapeCharacter = '!';
/**
* {@inheritdoc}
*/
protected $likeEscapingReplacements = [
'%' => '!%',
'_' => '!_',
'!' => '!!',
];
protected function defaultExpressionBuilders()
{
return array_merge(parent::defaultExpressionBuilders(), [
'yii\db\conditions\LikeCondition' => 'yii\db\cubrid\conditions\LikeConditionBuilder',
]);
}
/**
* Creates a SQL statement for resetting the sequence value of a table's primary key.

View File

@@ -0,0 +1,24 @@
<?php
namespace yii\db\cubrid\conditions;
/**
* {@inheritdoc}
*/
class LikeConditionBuilder extends \yii\db\conditions\LikeConditionBuilder
{
/**
* @inheritdoc
*/
protected $escapeCharacter = '!';
/**
* `\` is initialized in [[buildLikeCondition()]] method since
* we need to choose replacement value based on [[\yii\db\Schema::quoteValue()]].
* @inheritdoc
*/
protected $escapingReplacements = [
'%' => '!%',
'_' => '!_',
'!' => '!!',
];
}

View File

@@ -48,14 +48,13 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
*/
protected $likeEscapingReplacements = [
'%' => '[%]',
'_' => '[_]',
'[' => '[[]',
']' => '[]]',
'\\' => '[\\]',
];
protected function defaultExpressionBuilders()
{
return array_merge(parent::defaultExpressionBuilders(), [
'yii\db\conditions\InCondition' => 'yii\db\mssql\conditions\InConditionBuilder',
'yii\db\conditions\LikeCondition' => 'yii\db\mssql\conditions\LikeConditionBuilder',
]);
}
/**
* {@inheritdoc}
@@ -322,52 +321,6 @@ class QueryBuilder extends \yii\db\QueryBuilder
return $this->_oldMssql;
}
/**
* {@inheritdoc}
* @throws NotSupportedException if `$columns` is an array
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{
if (is_array($columns)) {
throw new NotSupportedException(__METHOD__ . ' is not supported by MSSQL.');
}
return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
}
/**
* Builds SQL for IN condition.
*
* @param string $operator
* @param array $columns
* @param array $values
* @param array $params
* @return string SQL
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
$quotedColumns = [];
foreach ($columns as $i => $column) {
$quotedColumns[$i] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
}
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $i => $column) {
if (isset($value[$column])) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value[$column];
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
} else {
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
}
}
$vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
}
return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
}
/**
* {@inheritdoc}
* @since 2.0.8
@@ -390,8 +343,9 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($columns as $name => $value) {
// @see https://github.com/yiisoft/yii2/issues/12599
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && is_string($value)) {
$phName = self::PARAM_PREFIX . count($params);
$columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", [$phName => $value]);
$exParams = [];
$phName = $this->bindParam($value, $exParams);
$columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace yii\db\mssql\conditions;
use yii\base\NotSupportedException;
/**
* {@inheritdoc}
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class InConditionBuilder extends \yii\db\conditions\InConditionBuilder
{
/**
* {@inheritdoc}
* @throws NotSupportedException if `$columns` is an array
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{
if (is_array($columns)) {
throw new NotSupportedException(__METHOD__ . ' is not supported by MSSQL.');
}
return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
}
/**
* {@inheritdoc}
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
$quotedColumns = [];
foreach ($columns as $i => $column) {
$quotedColumns[$i] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column;
}
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $i => $column) {
if (isset($value[$column])) {
$phName = $this->queryBuilder->bindParam($value[$column], $params);
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
} else {
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
}
}
$vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
}
return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace yii\db\mssql\conditions;
/**
* {@inheritdoc}
*/
class LikeConditionBuilder extends \yii\db\conditions\LikeConditionBuilder
{
/**
* @inheritdoc
*/
protected $escapingReplacements = [
'%' => '[%]',
'_' => '[_]',
'[' => '[[]',
']' => '[]]',
'\\' => '[\\]',
];
}

View File

@@ -0,0 +1,42 @@
<?php
namespace yii\db\mysql;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
use yii\db\Query;
use yii\helpers\Json;
/**
* Class JsonExpressionBuilder builds [[JsonExpression]] for MySQL DBMS.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class JsonExpressionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
const PARAM_PREFIX = ':qp';
/**
* {@inheritdoc}
* @param JsonExpression|ExpressionInterface $expression the expression to be built
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$value = $expression->getValue();
if ($value instanceof Query) {
list ($sql, $params) = $this->queryBuilder->build($value, $params);
return "($sql)";
}
$placeholder = static::PARAM_PREFIX . count($params);
$params[$placeholder] = Json::encode($value);
return $placeholder;
}
}

View File

@@ -10,7 +10,7 @@ namespace yii\db\mysql;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\db\Exception;
use yii\db\Expression;
use yii\db\ExpressionInterface;
/**
* QueryBuilder is the query builder for MySQL databases.
@@ -46,6 +46,15 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_MONEY => 'decimal(19,4)',
];
/**
* {@inheritdoc}
*/
protected function defaultExpressionBuilders()
{
return array_merge(parent::defaultExpressionBuilders(), [
'yii\db\JsonExpression' => 'yii\db\mysql\JsonExpressionBuilder',
]);
}
/**
* Builds a SQL statement for renaming a column.
@@ -247,18 +256,16 @@ class QueryBuilder extends \yii\db\QueryBuilder
} else {
foreach ($columns as $name => $value) {
$names[] = $schema->quoteColumnName($name);
if ($value instanceof Expression) {
$placeholders[] = $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
if ($value instanceof ExpressionInterface) {
$placeholders[] = $this->buildExpression($value, $params);
} elseif ($value instanceof \yii\db\Query) {
list($sql, $params) = $this->build($value, $params);
$placeholders[] = "($sql)";
} else {
$phName = self::PARAM_PREFIX . count($params);
$placeholders[] = $phName;
$params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
$placeholders[] = $this->bindParam(
isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value,
$params
);
}
}
if (empty($names) && $tableSchema !== null) {
@@ -350,4 +357,5 @@ class QueryBuilder extends \yii\db\QueryBuilder
return null;
}
}

View File

@@ -12,6 +12,7 @@ use yii\db\Connection;
use yii\db\Exception;
use yii\db\Expression;
use yii\helpers\StringHelper;
use yii\db\ExpressionInterface;
/**
* QueryBuilder is the query builder for Oracle databases.
@@ -50,18 +51,13 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
*/
protected $likeEscapeCharacter = '!';
/**
* `\` is initialized in [[buildLikeCondition()]] method since
* we need to choose replacement value based on [[\yii\db\Schema::quoteValue()]].
* {@inheritdoc}
*/
protected $likeEscapingReplacements = [
'%' => '!%',
'_' => '!_',
'!' => '!!',
];
protected function defaultExpressionBuilders()
{
return array_merge(parent::defaultExpressionBuilders(), [
'yii\db\conditions\InCondition' => 'yii\db\conditions\oci\InConditionBuilder',
'yii\db\conditions\LikeCondition' => 'yii\db\oci\conditions\LikeConditionBuilder',
]);
}
/**
* {@inheritdoc}
@@ -200,18 +196,16 @@ EOD;
} else {
foreach ($columns as $name => $value) {
$names[] = $schema->quoteColumnName($name);
if ($value instanceof Expression) {
$placeholders[] = $value->expression;
foreach ($value->params as $n => $v) {
$params[$n] = $v;
}
if ($value instanceof ExpressionInterface) {
$placeholders[] = $this->buildExpression($value, $params);
} elseif ($value instanceof \yii\db\Query) {
list($sql, $params) = $this->build($value, $params);
$placeholders[] = "($sql)";
} else {
$phName = self::PARAM_PREFIX . count($params);
$placeholders[] = $phName;
$params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
$placeholders[] = $this->bindParam(
isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value,
$params
);
}
}
if (empty($names) && $tableSchema !== null) {
@@ -265,7 +259,7 @@ EOD;
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -322,74 +316,4 @@ EOD;
{
return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . " IS ''";
}
/**
* @inheritDoc
*/
public function buildLikeCondition($operator, $operands, &$params)
{
if (!isset($this->likeEscapingReplacements['\\'])) {
/*
* Different pdo_oci8 versions may or may not implement PDO::quote(), so
* yii\db\Schema::quoteValue() may or may not quote \.
*/
$this->likeEscapingReplacements['\\'] = substr($this->db->quoteValue('\\'), 1, -1);
}
return parent::buildLikeCondition($operator, $operands, $params);
}
/**
* {@inheritdoc}
*/
public function buildInCondition($operator, $operands, &$params)
{
$splitCondition = $this->splitInCondition($operator, $operands, $params);
if ($splitCondition !== null) {
return $splitCondition;
}
return parent::buildInCondition($operator, $operands, $params);
}
/**
* Oracle DBMS does not support more than 1000 parameters in `IN` condition.
* This method splits long `IN` condition into series of smaller ones.
*
* @param string $operator
* @param array $operands
* @param array $params
* @return null|string null when split is not required. Otherwise - built SQL condition.
* @throws Exception
* @since 2.0.12
*/
protected function splitInCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
if ($values instanceof \Traversable) {
$values = iterator_to_array($values);
}
if (!is_array($values)) {
return null;
}
$maxParameters = 1000;
$count = count($values);
if ($count <= $maxParameters) {
return null;
}
$condition = [($operator === 'IN') ? 'OR' : 'AND'];
for ($i = 0; $i < $count; $i += $maxParameters) {
$condition[] = [$operator, $column, array_slice($values, $i, $maxParameters)];
}
return $this->buildCondition(['AND', $condition], $params);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace yii\db\oci\conditions;
use yii\db\conditions\InCondition;
use yii\db\ExpressionInterface;
/**
* {@inheritdoc}
*/
class InConditionBuilder extends \yii\db\conditions\InConditionBuilder
{
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|InCondition $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$splitCondition = $this->splitCondition($expression, $params);
if ($splitCondition !== null) {
return $splitCondition;
}
return parent::build($expression, $params);
}
/**
* Oracle DBMS does not support more than 1000 parameters in `IN` condition.
* This method splits long `IN` condition into series of smaller ones.
*
* @param ExpressionInterface|InCondition $condition the expression to be built.
* @param array $params the binding parameters.
* @return null|string null when split is not required. Otherwise - built SQL condition.
*/
protected function splitCondition(InCondition $condition, &$params)
{
$operator = $condition->getOperator();
$values = $condition->getValues();
$column = $condition->getColumn();
if ($values instanceof \Traversable) {
$values = iterator_to_array($values);
}
if (!is_array($values)) {
return null;
}
$maxParameters = 1000;
$count = count($values);
if ($count <= $maxParameters) {
return null;
}
$slices = [];
for ($i = 0; $i < $count; $i += $maxParameters) {
$slices[] = $this->queryBuilder->createConditionFromArray([$operator, $column, array_slice($values, $i, $maxParameters)]);
}
return $this->queryBuilder->buildCondition([($operator === 'IN') ? 'OR' : 'AND', $slices], $params);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace yii\db\oci\conditions;
use yii\db\ExpressionInterface;
/**
* {@inheritdoc}
*/
class LikeConditionBuilder extends \yii\db\conditions\LikeConditionBuilder
{
/**
* @inheritdoc
*/
protected $escapeCharacter = '!';
/**
* `\` is initialized in [[buildLikeCondition()]] method since
* we need to choose replacement value based on [[\yii\db\Schema::quoteValue()]].
* @inheritdoc
*/
protected $escapingReplacements = [
'%' => '!%',
'_' => '!_',
'!' => '!!',
];
/**
* {@inheritdoc}
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
if (!isset($this->escapingReplacements['\\'])) {
/*
* Different pdo_oci8 versions may or may not implement PDO::quote(), so
* yii\db\Schema::quoteValue() may or may not quote \.
*/
$this->escapingReplacements['\\'] = substr($this->queryBuilder->db->quoteValue('\\'), 1, -1);
}
return parent::build($expression, $params);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace yii\db\pgsql;
use yii\db\ArrayExpression;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
use yii\db\Query;
/**
* Class ArrayExpressionBuilder builds [[ArrayExpression]] for PostgreSQL DBMS.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ArrayExpressionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* {@inheritdoc}
* @param ArrayExpression|ExpressionInterface $expression the expression to be built
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$value = $expression->getValue();
if ($value instanceof Query) {
list ($sql, $params) = $this->queryBuilder->build($value, $params);
return $this->buildSubqueryArray($sql, $expression);
}
$placeholders = $this->buildPlaceholders($expression, $params);
if (empty($placeholders)) {
return "'{}'";
}
return 'ARRAY[' . implode(', ', $placeholders) . ']' . $this->getTypehint($expression);
}
/**
* Builds placeholders array out of $expression values
* @param ExpressionInterface|ArrayExpression $expression
* @param array $params the binding parameters.
* @return array
*/
protected function buildPlaceholders(ExpressionInterface $expression, &$params)
{
$value = $expression->getValue();
$placeholders = [];
if ($value === null || !is_array($value) && !$value instanceof \Traversable) {
return $placeholders;
}
if ($expression->getDimension() > 1) {
foreach ($value as $item) {
$placeholders[] = $this->build($this->unnestArrayExpression($expression, $item), $params);
}
return $placeholders;
}
foreach ($value as $item) {
if ($item instanceof Query) {
list ($sql, $params) = $this->queryBuilder->build($item, $params);
$placeholders[] = $this->buildSubqueryArray($sql, $expression);
continue;
}
$item = $this->typecastValue($expression, $item);
if ($item instanceof ExpressionInterface) {
$placeholders[] = $this->queryBuilder->buildExpression($item, $params);
continue;
}
$placeholders[] = $this->queryBuilder->bindParam($item, $params);
}
return $placeholders;
}
/**
* @param ArrayExpression $expression
* @param mixed $value
* @return ArrayExpression
*/
private function unnestArrayExpression(ArrayExpression $expression, $value)
{
$expressionClass = get_class($expression);
return new $expressionClass($value, $expression->getType(), $expression->getDimension()-1);
}
/**
* @param ArrayExpression $expression
* @return string the typecast expression based on [[type]].
*/
protected function getTypehint(ArrayExpression $expression)
{
if ($expression->getType() === null) {
return '';
}
$result = '::' . $expression->getType();
$result .= str_repeat('[]', $expression->getDimension());
return $result;
}
/**
* Build an array expression from a subquery SQL.
*
* @param string $sql the subquery SQL.
* @param ArrayExpression $expression
* @return string the subquery array expression.
*/
protected function buildSubqueryArray($sql, ArrayExpression $expression)
{
return 'ARRAY(' . $sql . ')' . $this->getTypehint($expression);
}
/**
* Casts $value to use in $expression
*
* @param ArrayExpression $expression
* @param mixed $value
* @return JsonExpression
*/
protected function typecastValue(ArrayExpression $expression, $value)
{
if ($value instanceof ExpressionInterface) {
return $value;
}
if (in_array($expression->getType(), [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) {
return new JsonExpression($value);
}
return $value;
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace yii\db\pgsql;
/**
* The class converts PostgreSQL array representation to PHP array
*
* @author Sergei Tigrov <rrr-r@ya.ru>
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class ArrayParser
{
/**
* @var string Character used in array
*/
private $delimiter = ',';
/**
* Convert array from PostgreSQL to PHP
*
* @param string $value string to be converted
* @return array|null
*/
public function parse($value)
{
if ($value === null) {
return null;
}
if ($value === '{}') {
return [];
}
return $this->parseArray($value);
}
/**
* Pares PgSQL array encoded in string
*
* @param string $value
* @param int $i parse starting position
* @return array
*/
private function parseArray($value, &$i = 0)
{
$result = [];
$len = strlen($value);
for (++$i; $i < $len; ++$i) {
switch ($value[$i]) {
case '{':
$result[] = $this->parseArray($value, $i);
break;
case '}':
break 2;
case $this->delimiter:
if (empty($result)) { // `{}` case
$result[] = null;
}
if (in_array($value[$i + 1], [$this->delimiter, '}'], true)) { // `{,}` case
$result[] = null;
}
break;
default:
$result[] = $this->parseString($value, $i);
}
}
return $result;
}
/**
* Parses PgSQL encoded string
*
* @param string $value
* @param int $i parse starting position
* @return null|string
*/
private function parseString($value, &$i)
{
$isQuoted = $value[$i] === '"';
$stringEndChars = $isQuoted ? ['"'] : [$this->delimiter, '}'];
$result = '';
$len = strlen($value);
for ($i += $isQuoted ? 1 : 0; $i < $len; ++$i) {
if (in_array($value[$i], ['\\', '"'], true) && in_array($value[$i + 1], [$value[$i], '"'], true)) {
++$i;
} elseif (in_array($value[$i], $stringEndChars, true)) {
break;
}
$result .= $value[$i];
}
$i -= $isQuoted ? 0 : 1;
if (!$isQuoted && $result === 'NULL') {
$result = null;
}
return $result;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace yii\db\pgsql;
use yii\db\ArrayExpression;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
/**
* Class ColumnSchema
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
*/
class ColumnSchema extends \yii\db\ColumnSchema
{
/**
* @var int the dimension of array. Defaults to 0, means this column is not an array.
*/
public $dimension = 0;
/**
* {@inheritdoc}
*/
public function dbTypecast($value)
{
if ($value instanceof ExpressionInterface) {
return $value;
}
if ($this->dimension > 0) {
return new ArrayExpression($value, $this->dbType, $this->dimension);
}
if (in_array($this->dbType, [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) {
return new JsonExpression($value, $this->type);
}
return $this->typecast($value);
}
/**
* @inheritdoc
*/
public function phpTypecast($value)
{
if ($this->dimension > 0) {
if (!is_array($value)) {
$value = $this->getArrayParser()->parse($value);
}
if (is_array($value)) {
array_walk_recursive($value, function (&$val, $key) {
$val = $this->phpTypecastValue($val);
});
}
return new ArrayExpression($value, $this->dbType, $this->dimension);
}
return $this->phpTypecastValue($value);
}
/**
* Casts $value after retrieving from the DBMS to PHP representation.
*
* @param string|null $value
* @return bool|mixed|null
*/
protected function phpTypecastValue($value)
{
if ($value === null) {
return null;
}
switch ($this->type) {
case Schema::TYPE_BOOLEAN:
switch (strtolower($value)) {
case 't':
case 'true':
return true;
case 'f':
case 'false':
return false;
}
return (bool) $value;
case Schema::TYPE_JSON:
return json_decode($value, true);
}
return parent::phpTypecast($value);
}
/**
* Creates instance of ArrayParser
*
* @return ArrayParser
*/
protected function getArrayParser()
{
static $parser = null;
if ($parser === null) {
$parser = new ArrayParser();
}
return $parser;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace yii\db\pgsql;
use yii\db\ExpressionBuilderInterface;
use yii\db\ExpressionBuilderTrait;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
use yii\db\Query;
use yii\helpers\Json;
/**
* Class JsonExpressionBuilder builds [[JsonExpression]] for PostgreSQL DBMS.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class JsonExpressionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* {@inheritdoc}
* @param JsonExpression|ExpressionInterface $expression the expression to be built
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$value = $expression->getValue();
if ($value instanceof Query) {
list ($sql, $params) = $this->queryBuilder->build($value, $params);
return "($sql)" . $this->getTypecast($expression);
}
$placeholder = $this->queryBuilder->bindParam(Json::encode($value), $params);
return $placeholder . $this->getTypecast($expression);
}
/**
* @param JsonExpression $expression
* @return string the typecast expression based on [[type]].
*/
protected function getTypecast(JsonExpression $expression)
{
if ($expression->getType() === null) {
return '';
}
return '::' . $expression->getType();
}
}

View File

@@ -8,6 +8,9 @@
namespace yii\db\pgsql;
use yii\base\InvalidParamException;
use yii\db\ExpressionInterface;
use yii\db\PdoValue;
use yii\db\Query;
use yii\helpers\StringHelper;
/**
@@ -68,32 +71,32 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_BINARY => 'bytea',
Schema::TYPE_BOOLEAN => 'boolean',
Schema::TYPE_MONEY => 'numeric(19,4)',
Schema::TYPE_JSON => 'jsonb'
];
/**
* @var array map of query condition to builder methods.
* These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
* {@inheritdoc}
*/
protected $conditionBuilders = [
'NOT' => 'buildNotCondition',
'AND' => 'buildAndCondition',
'OR' => 'buildAndCondition',
'BETWEEN' => 'buildBetweenCondition',
'NOT BETWEEN' => 'buildBetweenCondition',
'IN' => 'buildInCondition',
'NOT IN' => 'buildInCondition',
'LIKE' => 'buildLikeCondition',
'ILIKE' => 'buildLikeCondition',
'NOT LIKE' => 'buildLikeCondition',
'NOT ILIKE' => 'buildLikeCondition',
'OR LIKE' => 'buildLikeCondition',
'OR ILIKE' => 'buildLikeCondition',
'OR NOT LIKE' => 'buildLikeCondition',
'OR NOT ILIKE' => 'buildLikeCondition',
'EXISTS' => 'buildExistsCondition',
'NOT EXISTS' => 'buildExistsCondition',
];
protected function defaultConditionClasses()
{
return array_merge(parent::defaultConditionClasses(), [
'ILIKE' => 'yii\db\conditions\LikeCondition',
'NOT ILIKE' => 'yii\db\conditions\LikeCondition',
'OR ILIKE' => 'yii\db\conditions\LikeCondition',
'OR NOT ILIKE' => 'yii\db\conditions\LikeCondition',
]);
}
/**
* {@inheritdoc}
*/
protected function defaultExpressionBuilders()
{
return array_merge(parent::defaultExpressionBuilders(), [
'yii\db\ArrayExpression' => 'yii\db\pgsql\ArrayExpressionBuilder',
'yii\db\JsonExpression' => 'yii\db\pgsql\JsonExpressionBuilder',
]);
}
/**
* Builds a SQL statement for creating a new index.
@@ -274,7 +277,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
$columnSchemas = $tableSchema->columns;
foreach ($columns as $name => $value) {
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && is_string($value)) {
$columns[$name] = [$value, \PDO::PARAM_LOB]; // explicitly setup PDO param type for binary column
$columns[$name] = new PdoValue($value, \PDO::PARAM_LOB); // explicitly setup PDO param type for binary column
}
}
}
@@ -302,7 +305,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {

View File

@@ -30,10 +30,16 @@ class Schema extends \yii\db\Schema
use ViewFinderTrait;
use ConstraintFinderTrait;
const TYPE_JSONB = 'jsonb';
/**
* @var string the default schema used for the current session.
*/
public $defaultSchema = 'public';
/**
* {@inheritdoc}
*/
public $columnSchemaClass = 'yii\db\pgsql\ColumnSchema';
/**
* @var array mapping from physical column types (keys) to abstract
* column types (values)
@@ -113,8 +119,8 @@ class Schema extends \yii\db\Schema
'unknown' => self::TYPE_STRING,
'uuid' => self::TYPE_STRING,
'json' => self::TYPE_STRING,
'jsonb' => self::TYPE_STRING,
'json' => self::TYPE_JSON,
'jsonb' => self::TYPE_JSON,
'xml' => self::TYPE_STRING,
];
@@ -468,14 +474,18 @@ SELECT
d.nspname AS table_schema,
c.relname AS table_name,
a.attname AS column_name,
t.typname AS data_type,
COALESCE(td.typname, tb.typname, t.typname) AS data_type,
COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
a.attlen AS character_maximum_length,
pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
a.atttypmod AS modifier,
a.attnotnull = false AS is_nullable,
CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) AS is_autoinc,
array_to_string((select array_agg(enumlabel) from pg_enum where enumtypid=a.atttypid)::varchar[],',') as enum_values,
CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
ELSE NULL
END AS enum_values,
CASE atttypid
WHEN 21 /*int2*/ THEN 16
WHEN 23 /*int4*/ THEN 32
@@ -502,22 +512,24 @@ SELECT
information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
AS numeric
) AS size,
a.attnum = any (ct.conkey) as is_pkey
a.attnum = any (ct.conkey) as is_pkey,
COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
FROM
pg_class c
LEFT JOIN pg_attribute a ON a.attrelid = c.oid
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
LEFT JOIN pg_type t ON a.atttypid = t.oid
LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid OR t.typbasetype > 0 AND t.typbasetype = tb.oid
LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p'
LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
WHERE
a.attnum > 0 and t.typname != ''
and c.relname = {$tableName}
and d.nspname = {$schemaName}
a.attnum > 0 AND t.typname != ''
AND c.relname = {$tableName}
AND d.nspname = {$schemaName}
ORDER BY
a.attnum;
SQL;
$columns = $this->db->createCommand($sql)->queryAll();
if (empty($columns)) {
return false;
@@ -542,7 +554,7 @@ SQL;
} elseif (stripos($column->dbType, 'bit') === 0 || stripos($column->dbType, 'varbit') === 0) {
$column->defaultValue = bindec(trim($column->defaultValue, 'B\''));
} elseif (preg_match("/^'(.*?)'::/", $column->defaultValue, $matches)) {
$column->defaultValue = $matches[1];
$column->defaultValue = $column->phpTypecast($matches[1]);
} elseif (preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->defaultValue, $matches)) {
if ($matches[2] === 'NULL') {
$column->defaultValue = null;
@@ -565,6 +577,7 @@ SQL;
*/
protected function loadColumnSchema($info)
{
/** @var ColumnSchema $column */
$column = $this->createColumnSchema();
$column->allowNull = $info['is_nullable'];
$column->autoIncrement = $info['is_autoinc'];
@@ -578,6 +591,7 @@ SQL;
$column->precision = $info['numeric_precision'];
$column->scale = $info['numeric_scale'];
$column->size = $info['size'] === null ? null : (int) $info['size'];
$column->dimension = (int)$info['dimension'];
if (isset($this->typeMap[$column->dbType])) {
$column->type = $this->typeMap[$column->dbType];
} else {

View File

@@ -10,7 +10,7 @@ namespace yii\db\sqlite;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\db\Connection;
use yii\db\Expression;
use yii\db\ExpressionInterface;
use yii\db\Query;
use yii\helpers\StringHelper;
@@ -51,8 +51,13 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
*/
protected $likeEscapeCharacter = '\\';
protected function defaultExpressionBuilders()
{
return array_merge(parent::defaultExpressionBuilders(), [
'yii\db\conditions\LikeCondition' => 'yii\db\sqlite\conditions\LikeConditionBuilder',
'yii\db\conditions\InCondition' => 'yii\db\sqlite\conditions\InConditionBuilder',
]);
}
/**
* Generates a batch INSERT SQL statement.
@@ -98,7 +103,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
if (!is_array($value) && isset($columnSchemas[$columns[$i]])) {
if (isset($columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -418,52 +423,6 @@ class QueryBuilder extends \yii\db\QueryBuilder
return $sql;
}
/**
* {@inheritdoc}
* @throws NotSupportedException if `$columns` is an array
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{
if (is_array($columns)) {
throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
}
return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
}
/**
* Builds SQL for IN condition.
*
* @param string $operator
* @param array $columns
* @param array $values
* @param array $params
* @return string SQL
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
$quotedColumns = [];
foreach ($columns as $i => $column) {
$quotedColumns[$i] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
}
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $i => $column) {
if (isset($value[$column])) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value[$column];
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
} else {
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
}
}
$vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
}
return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
}
/**
* {@inheritdoc}
*/
@@ -487,15 +446,15 @@ class QueryBuilder extends \yii\db\QueryBuilder
if (!empty($query->orderBy)) {
foreach ($query->orderBy as $expression) {
if ($expression instanceof Expression) {
$params = array_merge($params, $expression->params);
if ($expression instanceof ExpressionInterface) {
$this->buildExpression($expression, $params);
}
}
}
if (!empty($query->groupBy)) {
foreach ($query->groupBy as $expression) {
if ($expression instanceof Expression) {
$params = array_merge($params, $expression->params);
if ($expression instanceof ExpressionInterface) {
$this->buildExpression($expression, $params);
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace yii\db\sqlite\conditions;
use yii\base\NotSupportedException;
/**
* {@inheritdoc}
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class InConditionBuilder extends \yii\db\conditions\InConditionBuilder
{
/**
* {@inheritdoc}
* @throws NotSupportedException if `$columns` is an array
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{
if (is_array($columns)) {
throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
}
return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
}
/**
* {@inheritdoc}
*/
protected function buildCompositeInCondition($operator, $columns, $values, &$params)
{
$quotedColumns = [];
foreach ($columns as $i => $column) {
$quotedColumns[$i] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column;
}
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $i => $column) {
if (isset($value[$column])) {
$phName = $this->queryBuilder->bindParam($value[$column], $params);
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
} else {
$vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
}
}
$vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
}
return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace yii\db\sqlite\conditions;
/**
* {@inheritdoc}
*/
class LikeConditionBuilder extends \yii\db\conditions\LikeConditionBuilder
{
/**
* @inheritdoc
*/
protected $escapeCharacter = '\\';
}

View File

@@ -10,6 +10,7 @@ namespace yii\web;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\Connection;
use yii\db\PdoValue;
use yii\db\Query;
use yii\di\Instance;
@@ -232,8 +233,8 @@ class DbSession extends MultiFieldSession
*/
protected function typecastFields($fields)
{
if (isset($fields['data']) && !is_array($fields['data'])) {
$fields['data'] = [$fields['data'], \PDO::PARAM_LOB];
if (isset($fields['data']) && is_array($fields['data'] && is_object($fields['data']))) {
$fields['data'] = new PdoValue($fields['data'], \PDO::PARAM_LOB);
}
return $fields;

View File

@@ -135,7 +135,12 @@ CREATE TABLE "type" (
bool_col2 boolean DEFAULT TRUE,
ts_default TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
bit_col BIT(8) NOT NULL DEFAULT B'10000010',
bigint_col BIGINT
bigint_col BIGINT,
intarray_col integer[],
textarray2_col text[][],
json_col json DEFAULT '{"a":1}',
jsonb_col jsonb,
jsonarray_col json[]
);
CREATE TABLE "bool_values" (
@@ -281,6 +286,15 @@ CREATE TABLE "bit_values" (
INSERT INTO "bit_values" (id, val) VALUES (1, '0'), (2, '1');
DROP TABLE IF EXISTS "array_and_json_types" CASCADE;
CREATE TABLE "array_and_json_types" (
intarray_col INT[],
textarray2_col TEXT[][],
json_col JSON,
jsonb_col JSONB,
jsonarray_col JSON[]
);
CREATE TABLE "T_constraints_1"
(
"C_id" INT NOT NULL PRIMARY KEY,

View File

@@ -12,6 +12,7 @@ use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Connection;
use yii\db\Expression;
use yii\db\ExpressionInterface;
use yiiunit\TestCase;
/**
@@ -155,9 +156,9 @@ class TimestampBehaviorTest extends TestCase
];
$model = new ActiveRecordTimestamp();
$model->save(false);
if ($expression instanceof Expression) {
$this->assertInstanceOf(Expression::className(), $model->created_at);
$this->assertInstanceOf(Expression::className(), $model->updated_at);
if ($expression instanceof ExpressionInterface) {
$this->assertInstanceOf('yii\db\ExpressionInterface', $model->created_at);
$this->assertInstanceOf('yii\db\ExpressionInterface', $model->updated_at);
$model->refresh();
}
$this->assertEquals($expected, $model->created_at);

View File

@@ -118,9 +118,11 @@ abstract class DatabaseTestCase extends TestCase
case 'sqlite':
return str_replace(['[[', ']]'], '`', $sql);
case 'cubrid':
case 'pgsql':
case 'oci':
return str_replace(['[[', ']]'], '"', $sql);
case 'pgsql':
// more complex replacement needed to not conflict with postgres array syntax
return str_replace(['\\[', '\\]'], ['[', ']'], preg_replace('/(\[\[)|((?<!(\[))\]\])/', '"', $sql));
case 'sqlsrv':
return str_replace(['[[', ']]'], ['[', ']'], $sql);
default:

View File

@@ -485,7 +485,10 @@ abstract class SchemaTest extends DatabaseTestCase
$this->assertInternalType('object', $column->defaultValue, "defaultValue of column $name is expected to be an object but it is not.");
$this->assertEquals((string) $expected['defaultValue'], (string) $column->defaultValue, "defaultValue of column $name does not match.");
} else {
$this->assertSame($expected['defaultValue'], $column->defaultValue, "defaultValue of column $name does not match.");
$this->assertEquals($expected['defaultValue'], $column->defaultValue, "defaultValue of column $name does not match.");
}
if (isset($expected['dimension'])) { // PgSQL only
$this->assertSame($expected['dimension'], $column->dimension, "dimension of column $name does not match");
}
}
}

View File

@@ -7,6 +7,9 @@
namespace yiiunit\framework\db\mysql;
use yii\base\DynamicModel;
use yii\db\JsonExpression;
use yii\db\Query;
use yii\db\Schema;
/**
@@ -117,4 +120,34 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
$sql = $qb->resetSequence('item', 4);
$this->assertEquals($expected, $sql);
}
public function conditionProvider()
{
return array_merge(parent::conditionProvider(), [
// json conditions
[['=', 'jsoncol', new JsonExpression(['lang' => 'uk', 'country' => 'UA'])], '[[jsoncol]] = :qp0', [':qp0' => '{"lang":"uk","country":"UA"}']],
[['=', 'jsoncol', new JsonExpression([false])], '[[jsoncol]] = :qp0', [':qp0' => '[false]']],
'object with type. Type is ignored for MySQL' => [
['=', 'prices', new JsonExpression(['seeds' => 15, 'apples' => 25], 'jsonb')],
'[[prices]] = :qp0', [':qp0' => '{"seeds":15,"apples":25}']
],
'nested json' => [
['=', 'data', new JsonExpression(['user' => ['login' => 'silverfire', 'password' => 'c4ny0ur34d17?'], 'props' => ['mood' => 'good']])],
'[[data]] = :qp0', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}']
],
'null value' => [['=', 'jsoncol', new JsonExpression(null)], '[[jsoncol]] = :qp0', [':qp0' => 'null']],
'null as array value' => [['=', 'jsoncol', new JsonExpression([null])], '[[jsoncol]] = :qp0', [':qp0' => '[null]']],
'null as object value' => [['=', 'jsoncol', new JsonExpression(['nil' => null])], '[[jsoncol[[ = :qp0', [':qp0' => '{"nil":null}']],
[['=', 'jsoncol', new JsonExpression(new DynamicModel(['a' => 1, 'b' => 2]))], '[[jsoncol]] = :qp0', [':qp0' => '{"a":1,"b":2}']],
'query' => [
['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]))],
'[[jsoncol]] = (SELECT [[params]] FROM [[user]] WHERE [[id]]=:qp0)', [':qp0' => 1]
],
'query with type, that is ignored in MySQL' => [
['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]), 'jsonb')],
'[[jsoncol]] = (SELECT [[params]] FROM [[user]] WHERE [[id]]=:qp0)', [':qp0' => 1]
],
]);
}
}

View File

@@ -8,7 +8,12 @@
namespace yiiunit\framework\db\pgsql;
use yii\behaviors\TimestampBehavior;
use yii\db\ArrayExpression;
use yii\db\Expression;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
use yii\db\pgsql\Schema;
use yii\helpers\Json;
use yiiunit\data\ar\ActiveRecord;
use yiiunit\data\ar\DefaultPk;
use yiiunit\framework\ar\ActiveRecordTestTrait;
@@ -172,6 +177,76 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest
$record->save(false);
$this->assertEquals(5, $record->primaryKey);
}
/**
* @dataProvider arrayValuesProvider $attributes
*/
public function testArrayValues($attributes)
{
$type = new ArrayAndJsonTypes();
foreach ($attributes as $attribute => $expected) {
$type->$attribute = $expected[0];
}
$type->save();
$type = ArrayAndJsonTypes::find()->one();
foreach ($attributes as $attribute => $expected) {
$expected = isset($expected[1]) ? $expected[1] : $expected[0];
$value = $type->$attribute;
$this->assertEquals($expected, $value, 'In column ' . $attribute);
if ($value instanceof ArrayExpression) {
$this->assertInstanceOf('\ArrayAccess', $value);
foreach ($type->$attribute as $key => $v) { // testing arrayaccess
$this->assertSame($expected[$key], $value[$key]);
}
}
}
}
public function arrayValuesProvider()
{
return [
'simple arrays values' => [[
'intarray_col' => [new ArrayExpression([1,-2,null,'42'], 'int4', 1)],
'textarray2_col' => [new ArrayExpression([['text'], [null], [1]], 'text', 2)],
'json_col' => [['a' => 1, 'b' => null, 'c' => [1,3,5]]],
'jsonb_col' => [[null, 'a', 'b', '\"', '{"af"}']],
'jsonarray_col' => [new ArrayExpression([[',', 'null', true, 'false', 'f']], 'json')],
]],
'arrays packed in classes' => [[
'intarray_col' => [
new ArrayExpression([1,-2,null,'42'], 'int', 1),
new ArrayExpression([1,-2,null,'42'], 'int4', 1),
],
'textarray2_col' => [
new ArrayExpression([['text'], [null], [1]], 'text', 2),
new ArrayExpression([['text'], [null], [1]], 'text', 2),
],
'json_col' => [
new JsonExpression(['a' => 1, 'b' => null, 'c' => [1,3,5]]),
['a' => 1, 'b' => null, 'c' => [1,3,5]]
],
'jsonb_col' => [
new JsonExpression([null, 'a', 'b', '\"', '{"af"}']),
[null, 'a', 'b', '\"', '{"af"}']
],
'jsonarray_col' => [
new Expression("array['[\",\",\"null\",true,\"false\",\"f\"]'::json]::json[]"),
new ArrayExpression([[',', 'null', true, 'false', 'f']], 'json'),
]
]],
'scalars' => [[
'json_col' => [
'5.8',
],
'jsonb_col' => [
pi()
],
]]
];
}
}
class BoolAR extends ActiveRecord
@@ -200,3 +275,15 @@ class UserAR extends ActiveRecord
];
}
}
/**
* {@inheritdoc}
* @property array intarray_col
* @property array textarray2_col
* @property array json_col
* @property array jsonb_col
* @property array jsonarray_col
*/
class ArrayAndJsonTypes extends ActiveRecord
{
}

View File

@@ -0,0 +1,45 @@
<?php
namespace yiiunit\framework\db\pgsql;
use yii\db\pgsql\ArrayParser;
use yiiunit\TestCase;
class ArrayParserTest extends TestCase
{
/**
* @var ArrayParser
*/
protected $arrayParser;
protected function setUp()
{
parent::setUp();
$this->arrayParser = new ArrayParser();
}
public function convertProvider()
{
return [
['{}', []],
['{,}', [null, null]],
['{,,}', [null, null, null]],
['{1,2,}', ['1','2',null]],
['{{},,1}', [[], null, '1']],
['{"{\"key\":\"value\"}",NULL,"NULL","{}"}', ['{"key":"value"}', null, "NULL", '{}']],
['{boo,",",,test', ['boo', ',', null, 'test']],
['{"string1","str\\\\in\\"g2","str,ing3"}', ['string1','str\\in"g2','str,ing3']],
['{{1,2,3},{4,5,6},{7,8,9}}', [['1','2','3'], ['4','5','6'], ['7','8','9']]],
['{utf8€,👍}', ['utf8€', '👍']],
];
}
/**
* @dataProvider convertProvider
*/
public function testConvert($string, $expected)
{
$this->assertSame($expected, $this->arrayParser->parse($string));
}
}

View File

@@ -100,5 +100,7 @@ class ConnectionTest extends \yiiunit\framework\db\ConnectionTest
$transaction = $connection->beginTransaction();
$transaction->setIsolationLevel(Transaction::SERIALIZABLE . ' READ ONLY DEFERRABLE');
$transaction->commit();
$this->assertTrue(true); // No error occurred  assert passed.
}
}

View File

@@ -7,7 +7,14 @@
namespace yiiunit\framework\db\pgsql;
use yii\base\DynamicModel;
use yii\db\Expression;
use yii\db\ArrayExpression;
use yii\db\JsonExpression;
use yii\db\Query;
use yii\db\Schema;
use yii\helpers\Json;
use yiiunit\data\base\TraversableObject;
/**
* @group db
@@ -76,6 +83,68 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
[['not ilike', 'name', ['heyho', 'abc']], '"name" NOT ILIKE :qp0 AND "name" NOT ILIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%']],
[['or ilike', 'name', ['heyho', 'abc']], '"name" ILIKE :qp0 OR "name" ILIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%']],
[['or not ilike', 'name', ['heyho', 'abc']], '"name" NOT ILIKE :qp0 OR "name" NOT ILIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%']],
// array condition corner cases
[['@>', 'id', new ArrayExpression([1])], '"id" @> ARRAY[:qp0]', [':qp0' => 1]],
'scalar can not be converted to array #1' => [['@>', 'id', new ArrayExpression(1)], '"id" @> \'{}\'', []],
['scalar can not be converted to array #2' => ['@>', 'id', new ArrayExpression(false)], '"id" @> \'{}\'', []],
[['&&', 'price', new ArrayExpression([12, 14], 'float')], '"price" && ARRAY[:qp0, :qp1]::float[]', [':qp0' => 12, ':qp1' => 14]],
[['@>', 'id', new ArrayExpression([2, 3])], '"id" @> ARRAY[:qp0, :qp1]', [':qp0' => 2, ':qp1' => 3]],
'array of arrays' => [['@>', 'id', new ArrayExpression([[1,2], [3,4]], 'float', 2)], '"id" @> ARRAY[ARRAY[:qp0, :qp1]::float[], ARRAY[:qp2, :qp3]::float[]\\]::float[][]', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3, ':qp3' => 4]],
[['@>', 'id', new ArrayExpression([])], '"id" @> \'{}\'', []],
'array can contain nulls' => [['@>', 'id', new ArrayExpression([null])], '"id" @> ARRAY[:qp0]', [':qp0' => null]],
'traversable objects are supported' => [['@>', 'id', new ArrayExpression(new TraversableObject([1, 2, 3]))], '[[id]] @> ARRAY[:qp0, :qp1, :qp2]', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]],
[['@>', 'time', new ArrayExpression([new Expression('now()')])], '[[time]] @> ARRAY[now()]', []],
[['@>', 'id', new ArrayExpression((new Query())->select('id')->from('users')->where(['active' => 1]))], '[[id]] @> ARRAY(SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]],
[['@>', 'id', new ArrayExpression([(new Query())->select('id')->from('users')->where(['active' => 1])], 'integer')], '[[id]] @> ARRAY[ARRAY(SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)::integer[]]::integer[]', [':qp0' => 1]],
// json conditions
[['=', 'jsoncol', new JsonExpression(['lang' => 'uk', 'country' => 'UA'])], '[[jsoncol]] = :qp0', [':qp0' => '{"lang":"uk","country":"UA"}']],
[['=', 'jsoncol', new JsonExpression([false])], '[[jsoncol]] = :qp0', [':qp0' => '[false]']],
[['=', 'prices', new JsonExpression(['seeds' => 15, 'apples' => 25], 'jsonb')], '[[prices]] = :qp0::jsonb', [':qp0' => '{"seeds":15,"apples":25}']],
'nested json' => [
['=', 'data', new JsonExpression(['user' => ['login' => 'silverfire', 'password' => 'c4ny0ur34d17?'], 'props' => ['mood' => 'good']])],
'"data" = :qp0', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}']
],
'null value' => [['=', 'jsoncol', new JsonExpression(null)], '"jsoncol" = :qp0', [':qp0' => 'null']],
'null as array value' => [['=', 'jsoncol', new JsonExpression([null])], '"jsoncol" = :qp0', [':qp0' => '[null]']],
'null as object value' => [['=', 'jsoncol', new JsonExpression(['nil' => null])], '"jsoncol" = :qp0', [':qp0' => '{"nil":null}']],
[['=', 'jsoncol', new JsonExpression(new DynamicModel(['a' => 1, 'b' => 2]))], '[[jsoncol]] = :qp0', [':qp0' => '{"a":1,"b":2}']],
'query' => [['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]))], '[[jsoncol]] = (SELECT [[params]] FROM [[user]] WHERE [[id]]=:qp0)', [':qp0' => 1]],
'query with type' => [['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]), 'jsonb')], '[[jsoncol]] = (SELECT [[params]] FROM [[user]] WHERE [[id]]=:qp0)::jsonb', [':qp0' => 1]],
'array of json expressions' => [
['=', 'colname', new ArrayExpression([new JsonExpression(['a' => null, 'b' => 123, 'c' => [4, 5]]), new JsonExpression([true])])],
'"colname" = ARRAY[:qp0, :qp1]',
[':qp0' => '{"a":null,"b":123,"c":[4,5]}', ':qp1' => '[true]']
],
'Items in ArrayExpression of type json should be casted to Json' => [
['=', 'colname', new ArrayExpression([['a' => null, 'b' => 123, 'c' => [4, 5]], [true]], 'json')],
'"colname" = ARRAY[:qp0, :qp1]::json[]',
[':qp0' => '{"a":null,"b":123,"c":[4,5]}', ':qp1' => '[true]']
],
'Two dimension array of text' => [
['=', 'colname', new ArrayExpression([['text1', 'text2'], ['text3', 'text4'], [null, 'text5']], 'text', 2)],
'"colname" = ARRAY[ARRAY[:qp0, :qp1]::text[], ARRAY[:qp2, :qp3]::text[], ARRAY[:qp4, :qp5]::text[]]::text[][]',
[':qp0' => 'text1', ':qp1' => 'text2', ':qp2' => 'text3', ':qp3' => 'text4', ':qp4' => null, ':qp5' => 'text5'],
],
'Three dimension array of booleans' => [
['=', 'colname', new ArrayExpression([[[true], [false, null]], [[false], [true], [false]], [['t', 'f']]], 'bool', 3)],
'"colname" = ARRAY[ARRAY[ARRAY[:qp0]::bool[], ARRAY[:qp1, :qp2]::bool[]]::bool[][], ARRAY[ARRAY[:qp3]::bool[], ARRAY[:qp4]::bool[], ARRAY[:qp5]::bool[]]::bool[][], ARRAY[ARRAY[:qp6, :qp7]::bool[]]::bool[][]]::bool[][][]',
[':qp0' => true, ':qp1' => false, ':qp2' => null, ':qp3' => false, ':qp4' => true, ':qp5' => false, ':qp6' => 't', ':qp7' => 'f'],
],
// Checks to verity that operators work correctly
[['@>', 'id', new ArrayExpression([1])], '"id" @> ARRAY[:qp0]', [':qp0' => 1]],
[['<@', 'id', new ArrayExpression([1])], '"id" <@ ARRAY[:qp0]', [':qp0' => 1]],
[['=', 'id', new ArrayExpression([1])], '"id" = ARRAY[:qp0]', [':qp0' => 1]],
[['<>', 'id', new ArrayExpression([1])], '"id" <> ARRAY[:qp0]', [':qp0' => 1]],
[['>', 'id', new ArrayExpression([1])], '"id" > ARRAY[:qp0]', [':qp0' => 1]],
[['<', 'id', new ArrayExpression([1])], '"id" < ARRAY[:qp0]', [':qp0' => 1]],
[['>=', 'id', new ArrayExpression([1])], '"id" >= ARRAY[:qp0]', [':qp0' => 1]],
[['<=', 'id', new ArrayExpression([1])], '"id" <= ARRAY[:qp0]', [':qp0' => 1]],
[['&&', 'id', new ArrayExpression([1])], '"id" && ARRAY[:qp0]', [':qp0' => 1]],
]);
}

View File

@@ -7,6 +7,7 @@
namespace yiiunit\framework\db\pgsql;
use yii\db\conditions\ExistsConditionBuilder;
use yii\db\Expression;
use yiiunit\data\ar\ActiveRecord;
use yiiunit\data\ar\Type;
@@ -85,6 +86,71 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest
'scale' => 0,
'defaultValue' => null,
];
$columns['intarray_col'] = [
'type' => 'integer',
'dbType' => 'int4',
'phpType' => 'integer',
'allowNull' => true,
'autoIncrement' => false,
'enumValues' => null,
'size' => null,
'precision' => null,
'scale' => null,
'defaultValue' => null,
'dimension' => 1
];
$columns['textarray2_col'] = [
'type' => 'text',
'dbType' => 'text',
'phpType' => 'string',
'allowNull' => true,
'autoIncrement' => false,
'enumValues' => null,
'size' => null,
'precision' => null,
'scale' => null,
'defaultValue' => null,
'dimension' => 2
];
$columns['json_col'] = [
'type' => 'json',
'dbType' => 'json',
'phpType' => 'array',
'allowNull' => true,
'autoIncrement' => false,
'enumValues' => null,
'size' => null,
'precision' => null,
'scale' => null,
'defaultValue' => ["a" => 1],
'dimension' => 0
];
$columns['jsonb_col'] = [
'type' => 'json',
'dbType' => 'jsonb',
'phpType' => 'array',
'allowNull' => true,
'autoIncrement' => false,
'enumValues' => null,
'size' => null,
'precision' => null,
'scale' => null,
'defaultValue' => null,
'dimension' => 0
];
$columns['jsonarray_col'] = [
'type' => 'json',
'dbType' => 'json',
'phpType' => 'array',
'allowNull' => true,
'autoIncrement' => false,
'enumValues' => null,
'size' => null,
'precision' => null,
'scale' => null,
'defaultValue' => null,
'dimension' => 1
];
return $columns;
}