Browse Source

Merge pull request #8526 from cakephp/expression-types

Support for expression converting types
José Lorenzo Rodríguez 10 years ago
parent
commit
5541862143

+ 0 - 53
src/Database/Dialect/SqliteDialectTrait.php

@@ -150,59 +150,6 @@ trait SqliteDialectTrait
     }
 
     /**
-     * Transforms an insert query that is meant to insert multiple rows at a time,
-     * otherwise it leaves the query untouched.
-     *
-     * The way SQLite works with multi insert is by having multiple select statements
-     * joined with UNION.
-     *
-     * @param \Cake\Database\Query $query The query to translate
-     * @return \Cake\Database\Query
-     */
-    protected function _insertQueryTranslator($query)
-    {
-        $v = $query->clause('values');
-        if (count($v->values()) === 1 || $v->query()) {
-            return $query;
-        }
-
-        $newQuery = $query->connection()->newQuery();
-        $cols = $v->columns();
-        $placeholder = 0;
-        $replaceQuery = false;
-
-        foreach ($v->values() as $k => $val) {
-            $fillLength = count($cols) - count($val);
-            if ($fillLength > 0) {
-                $val = array_merge($val, array_fill(0, $fillLength, null));
-            }
-
-            foreach ($val as $col => $attr) {
-                if (!($attr instanceof ExpressionInterface)) {
-                    $val[$col] = sprintf(':c%d', $placeholder);
-                    $placeholder++;
-                }
-            }
-
-            $select = array_combine($cols, $val);
-            if ($k === 0) {
-                $replaceQuery = true;
-                $newQuery->select($select);
-                continue;
-            }
-
-            $q = $newQuery->connection()->newQuery();
-            $newQuery->unionAll($q->select($select));
-        }
-
-        if ($replaceQuery) {
-            $v->query($newQuery);
-        }
-
-        return $query;
-    }
-
-    /**
      * Get the schema dialect.
      *
      * Used by Cake\Database\Schema package to reflect schema and

+ 12 - 0
src/Database/Dialect/SqlserverDialectTrait.php

@@ -270,6 +270,18 @@ trait SqlserverDialectTrait
                     ->tieWith(' ')
                     ->add(['weekday, ' => 'literal'], [], true);
                 break;
+            case 'SUBSTR':
+                $expression->name('SUBSTRING');
+                if (count($expression) < 4) {
+                    $params = [];
+                    $expression
+                        ->iterateParts(function ($p) use (&$params) {
+                            return $params[] = $p;
+                        })
+                        ->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
+                }
+
+                break;
         }
     }
 

+ 7 - 0
src/Database/Expression/BetweenExpression.php

@@ -15,6 +15,7 @@
 namespace Cake\Database\Expression;
 
 use Cake\Database\ExpressionInterface;
+use Cake\Database\Type\ExpressionTypeCasterTrait;
 use Cake\Database\ValueBinder;
 
 /**
@@ -25,6 +26,7 @@ use Cake\Database\ValueBinder;
 class BetweenExpression implements ExpressionInterface, FieldInterface
 {
 
+    use ExpressionTypeCasterTrait;
     use FieldTrait;
 
     /**
@@ -58,6 +60,11 @@ class BetweenExpression implements ExpressionInterface, FieldInterface
      */
     public function __construct($field, $from, $to, $type = null)
     {
+        if ($type !== null) {
+            $from = $this->_castToExpression($from, $type);
+            $to = $this->_castToExpression($to, $type);
+        }
+
         $this->_field = $field;
         $this->_from = $from;
         $this->_to = $to;

+ 21 - 3
src/Database/Expression/CaseExpression.php

@@ -15,6 +15,7 @@
 namespace Cake\Database\Expression;
 
 use Cake\Database\ExpressionInterface;
+use Cake\Database\Type\ExpressionTypeCasterTrait;
 use Cake\Database\ValueBinder;
 
 /**
@@ -25,6 +26,8 @@ use Cake\Database\ValueBinder;
 class CaseExpression implements ExpressionInterface
 {
 
+    use ExpressionTypeCasterTrait;
+
     /**
      * A list of strings or other expression objects that represent the conditions of
      * the case statement. For example one key of the array might look like "sum > :value"
@@ -113,17 +116,19 @@ class CaseExpression implements ExpressionInterface
     {
         $rawValues = array_values($values);
         $keyValues = array_keys($values);
+
         foreach ($conditions as $k => $c) {
             $numericKey = is_numeric($k);
 
             if ($numericKey && empty($c)) {
                 continue;
             }
+
             if (!$c instanceof ExpressionInterface) {
                 continue;
             }
-            array_push($this->_conditions, $c);
 
+            array_push($this->_conditions, $c);
             $value = isset($rawValues[$k]) ? $rawValues[$k] : 1;
 
             if ($value === 'literal') {
@@ -131,17 +136,24 @@ class CaseExpression implements ExpressionInterface
                 array_push($this->_values, $value);
                 continue;
             }
+
             if ($value === 'identifier') {
                 $value = new IdentifierExpression($keyValues[$k]);
                 array_push($this->_values, $value);
                 continue;
             }
+
+            $type = isset($types[$k]) ? $types[$k] : null;
+
+            if ($type !== null && !$value instanceof ExpressionInterface) {
+                $value = $this->_castToExpression($value, $type);
+            }
+
             if ($value instanceof ExpressionInterface) {
                 array_push($this->_values, $value);
                 continue;
             }
 
-            $type = isset($types[$k]) ? $types[$k] : null;
             array_push($this->_values, ['value' => $value, 'type' => $type]);
         }
     }
@@ -159,7 +171,13 @@ class CaseExpression implements ExpressionInterface
         if (is_array($value)) {
             end($value);
             $value = key($value);
-        } elseif ($value !== null && !$value instanceof ExpressionInterface) {
+        }
+
+        if ($value !== null && !$value instanceof ExpressionInterface) {
+            $value = $this->_castToExpression($value, $type);
+        }
+
+        if (!$value instanceof ExpressionInterface) {
             $value = ['value' => $value, 'type' => $type];
         }
 

+ 81 - 8
src/Database/Expression/Comparison.php

@@ -16,6 +16,7 @@ namespace Cake\Database\Expression;
 
 use Cake\Database\Exception as DatabaseException;
 use Cake\Database\ExpressionInterface;
+use Cake\Database\Type\ExpressionTypeCasterTrait;
 use Cake\Database\ValueBinder;
 
 /**
@@ -28,6 +29,7 @@ use Cake\Database\ValueBinder;
 class Comparison implements ExpressionInterface, FieldInterface
 {
 
+    use ExpressionTypeCasterTrait;
     use FieldTrait;
 
     /**
@@ -52,6 +54,21 @@ class Comparison implements ExpressionInterface, FieldInterface
     protected $_operator;
 
     /**
+     * Whether or not the value in this expression is a traversable
+     *
+     * @var bool
+     */
+    protected $_isMultiple = false;
+
+    /**
+     * A cached list of ExpressionInterface objects that were
+     * found in the value for this expression.
+     *
+     * @var array
+     */
+    protected $_valueExpressions = [];
+
+    /**
      * Constructor
      *
      * @param string $field the field name to compare to a value
@@ -61,13 +78,13 @@ class Comparison implements ExpressionInterface, FieldInterface
      */
     public function __construct($field, $value, $type, $operator)
     {
-        $this->setField($field);
-        $this->setValue($value);
-        $this->_operator = $operator;
-
         if (is_string($type)) {
             $this->_type = $type;
         }
+
+        $this->setField($field);
+        $this->setValue($value);
+        $this->_operator = $operator;
     }
 
     /**
@@ -78,6 +95,18 @@ class Comparison implements ExpressionInterface, FieldInterface
      */
     public function setValue($value)
     {
+        $hasType = isset($this->_type) && is_string($this->_type);
+        $isMultiple = $hasType && strpos($this->_type, '[]') !== false;
+
+        if ($hasType) {
+            $value = $this->_castToExpression($value, $this->_type);
+        }
+
+        if ($isMultiple) {
+            list($value, $this->_valueExpressions) = $this->_collectExpressions($value);
+        }
+
+        $this->_isMultiple = $isMultiple;
         $this->_value = $value;
     }
 
@@ -151,6 +180,11 @@ class Comparison implements ExpressionInterface, FieldInterface
             $callable($this->_value);
             $this->_value->traverse($callable);
         }
+
+        foreach ($this->_valueExpressions as $v) {
+            $callable($v);
+            $v->traverse($callable);
+        }
     }
 
     /**
@@ -184,7 +218,7 @@ class Comparison implements ExpressionInterface, FieldInterface
             $template = '(%s) ';
         }
 
-        if (strpos($this->_type, '[]') !== false) {
+        if ($this->_isMultiple) {
             $template .= '%s (%s)';
             $type = str_replace('[]', '', $this->_type);
             $value = $this->_flattenValue($this->_value, $generator, $type);
@@ -229,13 +263,52 @@ class Comparison implements ExpressionInterface, FieldInterface
      * @param string|array|null $type the type to cast values to
      * @return string
      */
-    protected function _flattenValue($value, $generator, $type = null)
+    protected function _flattenValue($value, $generator, $type = 'string')
     {
         $parts = [];
-        foreach ($value as $k => $v) {
-            $parts[] = $this->_bindValue($v, $generator, $type);
+        foreach ($this->_valueExpressions as $k => $v) {
+            $parts[$k] = $v->sql($generator);
+            unset($value[$k]);
+        }
+
+        if (!empty($value)) {
+            $parts += $generator->generateManyNamed($value, $type);
         }
 
         return implode(',', $parts);
     }
+
+    /**
+     * Returns an array with the original $values in the first position
+     * and all ExpressionInterface objects that could be found in the second
+     * position.
+     *
+     * @param array|Traversable $values The rows to insert
+     * @return array
+     */
+    protected function _collectExpressions($values)
+    {
+        if ($values instanceof ExpressionInterface) {
+            return [$values, []];
+        }
+
+        $expressions = $result = [];
+        $isArray = is_array($values);
+
+        if ($isArray) {
+            $result = $values;
+        }
+
+        foreach ($values as $k => $v) {
+            if ($v instanceof ExpressionInterface) {
+                $expressions[$k] = $v;
+            }
+
+            if ($isArray) {
+                $result[$k] = $v;
+            }
+        }
+
+        return [$result, $expressions];
+    }
 }

+ 10 - 1
src/Database/Expression/FunctionExpression.php

@@ -17,6 +17,7 @@ namespace Cake\Database\Expression;
 use Cake\Database\ExpressionInterface;
 use Cake\Database\TypedResultInterface;
 use Cake\Database\TypedResultTrait;
+use Cake\Database\Type\ExpressionTypeCasterTrait;
 use Cake\Database\ValueBinder;
 
 /**
@@ -30,6 +31,7 @@ use Cake\Database\ValueBinder;
 class FunctionExpression extends QueryExpression implements TypedResultInterface
 {
 
+    use ExpressionTypeCasterTrait;
     use TypedResultTrait;
 
     /**
@@ -113,11 +115,18 @@ class FunctionExpression extends QueryExpression implements TypedResultInterface
                 continue;
             }
 
+            $type = $typeMap->type($k);
+
+            if ($type !== null && !$p instanceof ExpressionInterface) {
+                $p = $this->_castToExpression($p, $type);
+            }
+
             if ($p instanceof ExpressionInterface) {
                 $put($this->_conditions, $p);
                 continue;
             }
-            $put($this->_conditions, ['value' => $p, 'type' => $typeMap->type($k)]);
+
+            $put($this->_conditions, ['value' => $p, 'type' => $type]);
         }
 
         return $this;

+ 70 - 4
src/Database/Expression/ValuesExpression.php

@@ -18,6 +18,7 @@ use Cake\Database\Exception;
 use Cake\Database\ExpressionInterface;
 use Cake\Database\Query;
 use Cake\Database\TypeMapTrait;
+use Cake\Database\Type\ExpressionTypeCasterTrait;
 use Cake\Database\ValueBinder;
 
 /**
@@ -31,6 +32,7 @@ use Cake\Database\ValueBinder;
 class ValuesExpression implements ExpressionInterface
 {
 
+    use ExpressionTypeCasterTrait;
     use TypeMapTrait;
 
     /**
@@ -55,6 +57,14 @@ class ValuesExpression implements ExpressionInterface
     protected $_query = false;
 
     /**
+     * Whether or not values have been casted to expressions
+     * already.
+     *
+     * @var string
+     */
+    protected $_castedExpressions = false;
+
+    /**
      * Constructor
      *
      * @param array $columns The list of columns that are going to be part of the values.
@@ -88,6 +98,7 @@ class ValuesExpression implements ExpressionInterface
             return;
         }
         $this->_values[] = $data;
+        $this->_castedExpressions = false;
     }
 
     /**
@@ -103,6 +114,7 @@ class ValuesExpression implements ExpressionInterface
             return $this->_columns;
         }
         $this->_columns = $cols;
+        $this->_castedExpressions = false;
         return $this;
     }
 
@@ -116,9 +128,13 @@ class ValuesExpression implements ExpressionInterface
     public function values($values = null)
     {
         if ($values === null) {
+            if (!$this->_castedExpressions) {
+                $this->_processExpressions();
+            }
             return $this->_values;
         }
         $this->_values = $values;
+        $this->_castedExpressions = false;
         return $this;
     }
 
@@ -150,23 +166,37 @@ class ValuesExpression implements ExpressionInterface
             return '';
         }
 
+        if (!$this->_castedExpressions) {
+            $this->_processExpressions();
+        }
+
         $i = 0;
         $defaults = array_fill_keys($this->_columns, null);
         $placeholders = [];
 
+        $types = [];
+        $typeMap = $this->typeMap();
+        foreach ($defaults as $col => $v) {
+            $types[$col] = $typeMap->type($col);
+        }
+
         foreach ($this->_values as $row) {
-            $row = array_merge($defaults, $row);
+            $row += $defaults;
             $rowPlaceholders = [];
-            foreach ($row as $column => $value) {
+
+            foreach ($this->_columns as $column) {
+                $value = $row[$column];
+
                 if ($value instanceof ExpressionInterface) {
                     $rowPlaceholders[] = '(' . $value->sql($generator) . ')';
                     continue;
                 }
-                $type = $this->typeMap()->type($column);
+
                 $placeholder = $generator->placeholder($i);
                 $rowPlaceholders[] = $placeholder;
-                $generator->bind($placeholder, $value, $type);
+                $generator->bind($placeholder, $value, $types[$column]);
             }
+
             $placeholders[] = implode(', ', $rowPlaceholders);
         }
 
@@ -192,6 +222,10 @@ class ValuesExpression implements ExpressionInterface
             return;
         }
 
+        if (!$this->_castedExpressions) {
+            $this->_processExpressions();
+        }
+
         foreach ($this->_values as $v) {
             if ($v instanceof ExpressionInterface) {
                 $v->traverse($visitor);
@@ -201,9 +235,41 @@ class ValuesExpression implements ExpressionInterface
             }
             foreach ($v as $column => $field) {
                 if ($field instanceof ExpressionInterface) {
+                    $visitor($field);
                     $field->traverse($visitor);
                 }
             }
         }
     }
+
+    /**
+     * Converts values that need to be casted to expressions
+     *
+     * @return void
+     */
+    protected function _processExpressions()
+    {
+        $types = [];
+        $typeMap = $this->typeMap();
+
+        foreach ($this->_columns as $c) {
+            if (!is_scalar($c)) {
+                continue;
+            }
+            $types[$c] = $typeMap->type($c);
+        }
+
+        $types = $this->_requiresToExpressionCasting($types);
+
+        if (empty($types)) {
+            return;
+        }
+
+        foreach ($this->_values as $row => $values) {
+            foreach ($types as $col => $type) {
+                $this->_values[$row][$col] = $type->toExpression($values[$col]);
+            }
+        }
+        $this->_castedExpressions = true;
+    }
 }

+ 79 - 0
src/Database/Type/ExpressionTypeCasterTrait.php

@@ -0,0 +1,79 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Type;
+
+use Cake\Database\Type;
+use Cake\Database\Type\ExpressionTypeInterface;
+
+/**
+ * Offers a method to convert values to ExpressionInterface objects
+ * if the type they should be converted to implements ExpressionTypeInterface
+ *
+ */
+trait ExpressionTypeCasterTrait
+{
+
+    /**
+     * Conditionally converts the passed value to an ExpressionInterface object
+     * if the type class implements the ExpressionTypeInterface. Otherwise,
+     * returns the value unmodified.
+     *
+     * @param mixed $value The value to converto to ExpressionInterface
+     * @param string $type The type name
+     * @return mixed
+     */
+    protected function _castToExpression($value, $type)
+    {
+        if (empty($type)) {
+            return $value;
+        }
+
+        $baseType = str_replace('[]', '', $type);
+        $converter = Type::build($baseType);
+
+        if (!$converter instanceof ExpressionTypeInterface) {
+            return $value;
+        }
+
+        $multi = $type !== $baseType;
+
+        if ($multi) {
+            return array_map([$converter, 'toExpression'], $value);
+        }
+
+        return $converter->toExpression($value);
+    }
+
+    /**
+     * Returns an array with the types that require values to
+     * be casted to expressions, out of the list of type names
+     * passed as parameter.
+     *
+     * @param array $types List of type names
+     * @return array
+     */
+    protected function _requiresToExpressionCasting($types)
+    {
+        $result = [];
+        $types = array_filter($types);
+        foreach ($types as $k => $type) {
+            $object = Type::build($type);
+            if ($object instanceof ExpressionTypeInterface) {
+                $result[$k] = $object;
+            }
+        }
+        return $result;
+    }
+}

+ 35 - 0
src/Database/Type/ExpressionTypeInterface.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Type;
+
+use Cake\Database\Driver;
+
+/**
+ * An interface used by Type objects to signal whether the value should
+ * be converted to an ExpressionInterface instead of a string when sent
+ * to the database.
+ */
+interface ExpressionTypeInterface
+{
+
+    /**
+     * Returns an ExpressionInterface object for the given value that can
+     * be used in queries.
+     *
+     * @param mixed $value The value to be converted to an expression
+     * @return \Cake\Database\ExpressionInterface
+     */
+    public function toExpression($value);
+}

+ 26 - 3
src/Database/ValueBinder.php

@@ -74,6 +74,31 @@ class ValueBinder
     }
 
     /**
+     * Creates unique named placeholders for each of the passed values
+     * and binds them with the specified type.
+     *
+     * @param array|Traversable $values The list of values to be bound
+     * @param string $type The type with which all values will be bound
+     * @return array with the placeholders to insert in the query
+     */
+    public function generateManyNamed($values, $type = 'string')
+    {
+        $placeholders = [];
+        foreach ($values as $k => $value) {
+            $param = ":c" . $this->_bindingsCount;
+            $this->_bindings[$param] = [
+                'value' => $value,
+                'type' => $type,
+                'placeholder' => $param
+            ];
+            $placeholders[$k] = $param;
+            $this->_bindingsCount++;
+        }
+
+        return $placeholders;
+    }
+
+    /**
      * Returns all values bound to this expression object at this nesting level.
      * Subexpression bound values will not be returned with this function.
      *
@@ -119,9 +144,7 @@ class ValueBinder
         }
         $params = $types = [];
         foreach ($bindings as $b) {
-            $params[$b['placeholder']] = $b['value'];
-            $types[$b['placeholder']] = $b['type'];
+            $statement->bindValue($b['placeholder'], $b['value'], $b['type']);
         }
-        $statement->bind($params, $types);
     }
 }

+ 45 - 0
tests/Fixture/OrderedUuidItemsFixture.php

@@ -0,0 +1,45 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+/**
+ * Class OrderedUuiditemFixture
+ *
+ */
+class OrderedUuidItemsFixture extends TestFixture
+{
+
+    /**
+     * fields property
+     *
+     * @var array
+     */
+    public $fields = [
+        'id' => ['type' => 'string', 'length' => 32],
+        'published' => ['type' => 'boolean', 'null' => false],
+        'name' => ['type' => 'string', 'null' => false],
+        '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
+    ];
+
+    /**
+     * records property
+     *
+     * @var array
+     */
+    public $records = [
+    ];
+}

+ 238 - 0
tests/TestCase/Database/ExpressionTypeCastingIntegrationTest.php

@@ -0,0 +1,238 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The Open Group Test Suite License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Database;
+
+use Cake\Database\Driver;
+use Cake\Database\Driver\Sqlserver;
+use Cake\Database\Expression\FunctionExpression;
+use Cake\Database\Type;
+use Cake\Database\Type\BinaryType;
+use Cake\Database\Type\ExpressionTypeInterface;
+use Cake\Datasource\ConnectionManager;
+use Cake\TestSuite\TestCase;
+
+/**
+ * Value object for testing mappings.
+ */
+class UuidValue
+{
+    public $value;
+
+    public function __construct($value)
+    {
+        $this->value = $value;
+    }
+}
+
+/**
+ * Custom type class that maps between value objects, and SQL expressions.
+ */
+class OrderedUuidType extends Type implements ExpressionTypeInterface
+{
+
+    public function toPHP($value, Driver $d)
+    {
+        return new UuidValue($value);
+    }
+
+    public function toExpression($value)
+    {
+        if ($value instanceof UuidValue) {
+            $value = $value->value;
+        }
+        $substr = function ($start, $length = null) use ($value) {
+            return new FunctionExpression(
+                'SUBSTR',
+                $length === null ? [$value, $start] : [$value, $start, $length],
+                ['string', 'integer', 'integer']
+            );
+        };
+        return new FunctionExpression(
+            'CONCAT',
+            [$substr(15, 4), $substr(10, 4), $substr(1, 8), $substr(20, 4), $substr(25)]
+        );
+    }
+}
+
+/**
+ * Tests for Expression objects casting values to other expressions
+ * using the type classes
+ *
+ */
+class ExpressionTypeCastingIntegrationTest extends TestCase
+{
+
+    public $fixtures = ['core.ordered_uuid_items'];
+
+    public function setUp()
+    {
+        parent::setUp();
+        $this->connection = ConnectionManager::get('test');
+        $this->skipIf($this->connection->driver() instanceof Sqlserver, 'This tests uses functions specific to other drivers');
+        Type::map('ordered_uuid', OrderedUuidType::class);
+    }
+
+    protected function _insert()
+    {
+        $query = $this->connection->newQuery();
+        $values = $query
+            ->insert(['id', 'published', 'name'], ['id' => 'ordered_uuid'])
+            ->into('ordered_uuid_items')
+            ->clause('values');
+        $values
+            ->values([
+                ['id' => '481fc6d0-b920-43e0-a40d-6d1740cf8569', 'published' => 0, 'name' => 'Item 1'],
+                ['id' => '48298a29-81c0-4c26-a7fb-413140cf8569', 'published' => 0, 'name' => 'Item 2'],
+                ['id' => '482b7756-8da0-419a-b21f-27da40cf8569', 'published' => 0, 'name' => 'Item 3'],
+            ]);
+
+        $query->execute();
+    }
+
+    /**
+     * Tests inserting a value that is to be converted to an expression
+     * automatically
+     *
+     * @return void
+     */
+    public function testInsert()
+    {
+        $this->_insert();
+        $query = $this->connection->newQuery()
+            ->select('id')
+            ->from('ordered_uuid_items')
+            ->order('id')
+            ->defaultTypes(['id' => 'ordered_uuid']);
+
+        $query->selectTypeMap($query->typeMap());
+        $results = $query->execute()->fetchAll('assoc');
+
+        $this->assertEquals(new UuidValue('419a8da0482b7756b21f27da40cf8569'), $results[0]['id']);
+        $this->assertEquals(new UuidValue('43e0b920481fc6d0a40d6d1740cf8569'), $results[1]['id']);
+        $this->assertEquals(new UuidValue('4c2681c048298a29a7fb413140cf8569'), $results[2]['id']);
+    }
+
+    /**
+     * Test selecting with a custom expression type using conditions
+     *
+     * @return void
+     */
+    public function testSelectWithConditions()
+    {
+        $this->_insert();
+        $result = $this->connection->newQuery()
+            ->select('id')
+            ->from('ordered_uuid_items')
+            ->where(['id' => '48298a29-81c0-4c26-a7fb-413140cf8569'], ['id' => 'ordered_uuid'])
+            ->execute()
+            ->fetchAll('assoc');
+
+        $this->assertCount(1, $result);
+        $this->assertEquals('4c2681c048298a29a7fb413140cf8569', $result[0]['id']);
+    }
+
+
+    /**
+     * Tests Select using value object in conditions
+     *
+     * @return void
+     */
+    public function testSelectWithConditionsValueObject()
+    {
+        $this->_insert();
+        $result = $this->connection->newQuery()
+            ->select('id')
+            ->from('ordered_uuid_items')
+            ->where(['id' => new UuidValue('48298a29-81c0-4c26-a7fb-413140cf8569')], ['id' => 'ordered_uuid'])
+            ->execute()
+            ->fetchAll('assoc');
+
+        $this->assertCount(1, $result);
+        $this->assertEquals('4c2681c048298a29a7fb413140cf8569', $result[0]['id']);
+    }
+
+    /**
+     * Tests using the expression type in with an IN condition
+     *
+     * @var string
+     */
+    public function testSelectWithInCondition()
+    {
+        $this->_insert();
+        $result = $this->connection->newQuery()
+            ->select('id')
+            ->from('ordered_uuid_items')
+            ->where(
+                ['id' => ['48298a29-81c0-4c26-a7fb-413140cf8569', '482b7756-8da0-419a-b21f-27da40cf8569']],
+                ['id' => 'ordered_uuid[]']
+            )
+            ->order('id')
+            ->execute()
+            ->fetchAll('assoc');
+
+        $this->assertCount(2, $result);
+        $this->assertEquals('419a8da0482b7756b21f27da40cf8569', $result[0]['id']);
+        $this->assertEquals('419a8da0482b7756b21f27da40cf8569', $result[0]['id']);
+    }
+
+    /**
+     * Tests using an expression type in a between condition
+     *
+     * @return void
+     */
+    public function testSelectWithBetween()
+    {
+        $this->_insert();
+        $result = $this->connection->newQuery()
+            ->select('id')
+            ->from('ordered_uuid_items')
+            ->where(function ($exp) {
+                return $exp->between(
+                    'id',
+                    '482b7756-8da0-419a-b21f-27da40cf8569',
+                    '48298a29-81c0-4c26-a7fb-413140cf8569',
+                    'ordered_uuid'
+                );
+            })
+            ->execute()
+            ->fetchAll('assoc');
+
+        $this->assertCount(3, $result);
+    }
+
+    /**
+     * Tests using an expression type inside a function expression
+     *
+     * @return void
+     */
+    public function testSelectWithFunction()
+    {
+        $this->_insert();
+        $result = $this->connection->newQuery()
+            ->select('id')
+            ->from('ordered_uuid_items')
+            ->where(function ($exp, $q) {
+                return $exp->eq(
+                    'id',
+                    $q->func()->concat(['48298a29-81c0-4c26-a7fb', '-413140cf8569'], []),
+                    'ordered_uuid'
+                );
+            })
+            ->execute()
+            ->fetchAll('assoc');
+
+        $this->assertCount(1, $result);
+        $this->assertEquals('4c2681c048298a29a7fb413140cf8569', $result[0]['id']);
+    }
+}

+ 201 - 0
tests/TestCase/Database/ExpressionTypeCastingTest.php

@@ -0,0 +1,201 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The Open Group Test Suite License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Database;
+
+use Cake\Database\Expression\BetweenExpression;
+use Cake\Database\Expression\CaseExpression;
+use Cake\Database\Expression\Comparison;
+use Cake\Database\Expression\FunctionExpression;
+use Cake\Database\Expression\ValuesExpression;
+use Cake\Database\Type;
+use Cake\Database\Type\ExpressionTypeInterface;
+use Cake\Database\Type\StringType;
+use Cake\Database\ValueBinder;
+use Cake\TestSuite\TestCase;
+
+class TestType extends StringType implements ExpressionTypeInterface
+{
+
+    public function toExpression($value)
+    {
+        return new FunctionExpression('CONCAT', [$value, ' - foo']);
+    }
+}
+
+/**
+ * Tests for Expression objects casting values to other expressions
+ * using the type classes
+ *
+ */
+class ExpressionTypeCastingTest extends TestCase
+{
+
+    /**
+     * Setups a mock for FunctionsBuilder
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        Type::map('test', new TestType);
+    }
+
+    /**
+     * Tests that the Comparison expression can handle values convertible to
+     * expressions
+     *
+     * @return void
+     */
+    public function testComparisonSimple()
+    {
+        $comparison = new Comparison('field', 'the thing', 'test', '=');
+        $binder = new ValueBinder;
+        $sql = $comparison->sql($binder);
+        $this->assertEquals('field = (CONCAT(:c0, :c1))', $sql);
+        $this->assertEquals('the thing', $binder->bindings()[':c0']['value']);
+
+        $found = false;
+        $comparison->traverse(function ($exp) use (&$found) {
+            $found = $exp instanceof FunctionExpression;
+        });
+        $this->assertTrue($found, 'The expression is not included in the tree');
+    }
+
+    /**
+     * Tests that the Comparison expression can handle values convertible to
+     * expressions
+     *
+     * @return void
+     */
+    public function testComparisonMultiple()
+    {
+        $comparison = new Comparison('field', ['2', '3'], 'test[]', 'IN');
+        $binder = new ValueBinder;
+        $sql = $comparison->sql($binder);
+        $this->assertEquals('field IN (CONCAT(:c0, :c1),CONCAT(:c2, :c3))', $sql);
+        $this->assertEquals('2', $binder->bindings()[':c0']['value']);
+        $this->assertEquals('3', $binder->bindings()[':c2']['value']);
+
+        $found = false;
+        $comparison->traverse(function ($exp) use (&$found) {
+            $found = $exp instanceof FunctionExpression;
+        });
+        $this->assertTrue($found, 'The expression is not included in the tree');
+    }
+
+    /**
+     * Tests that the Between expression casts values to expresisons correctly
+     *
+     * @return void
+     */
+    public function testBetween()
+    {
+        $between = new BetweenExpression('field', 'from', 'to', 'test');
+        $binder = new ValueBinder;
+        $sql = $between->sql($binder);
+        $this->assertEquals('field BETWEEN CONCAT(:c0, :c1) AND CONCAT(:c2, :c3)', $sql);
+        $this->assertEquals('from', $binder->bindings()[':c0']['value']);
+        $this->assertEquals('to', $binder->bindings()[':c2']['value']);
+
+        $expressions = [];
+        $between->traverse(function ($exp) use (&$expressions) {
+            $expressions[] = $exp instanceof FunctionExpression ? 1 : 0;
+        });
+
+        $result = array_sum($expressions);
+        $this->assertEquals(2, $result, 'Missing expressions in the tree');
+    }
+
+    /**
+     * Tests that the Case expressions converts values to expressions correctly
+     *
+     * @return void
+     */
+    public function testCaseExpression()
+    {
+        $case = new CaseExpression(
+            [new Comparison('foo', '1', 'string', '=')],
+            ['value1', 'value2'],
+            ['test', 'test']
+        );
+
+        $binder = new ValueBinder;
+        $sql = $case->sql($binder);
+        $this->assertEquals('CASE WHEN foo = :c0 THEN CONCAT(:c1, :c2) ELSE CONCAT(:c3, :c4) END', $sql);
+
+        $this->assertEquals('1', $binder->bindings()[':c0']['value']);
+        $this->assertEquals('value1', $binder->bindings()[':c1']['value']);
+        $this->assertEquals('value2', $binder->bindings()[':c3']['value']);
+
+        $expressions = [];
+        $case->traverse(function ($exp) use (&$expressions) {
+            $expressions[] = $exp instanceof FunctionExpression ? 1 : 0;
+        });
+
+        $result = array_sum($expressions);
+        $this->assertEquals(2, $result, 'Missing expressions in the tree');
+    }
+
+    /**
+     * Tests that values in FunctionExpressions are converted to expressions correctly
+     *
+     * @return void
+     */
+    public function testFunctionExpression()
+    {
+        $function = new FunctionExpression('DATE', ['2016-01'], ['test']);
+        $binder = new ValueBinder;
+        $sql = $function->sql($binder);
+        $this->assertEquals('DATE((CONCAT(:c0, :c1)))', $sql);
+        $this->assertEquals('2016-01', $binder->bindings()[':c0']['value']);
+
+        $expressions = [];
+        $function->traverse(function ($exp) use (&$expressions) {
+            $expressions[] = $exp instanceof FunctionExpression ? 1 : 0;
+        });
+
+        $result = array_sum($expressions);
+        $this->assertEquals(1, $result, 'Missing expressions in the tree');
+    }
+
+    /**
+     * Tests that values in ValuesExpression are converted to expressions correctly
+     *
+     * @return void
+     */
+    public function testValuesExpression()
+    {
+        $values = new ValuesExpression(['title'], ['title' => 'test']);
+        $values->add(['title' => 'foo']);
+        $values->add(['title' => 'bar']);
+
+        $binder = new ValueBinder;
+        $sql = $values->sql($binder);
+        $this->assertEquals(
+            ' VALUES ((CONCAT(:c0, :c1))), ((CONCAT(:c2, :c3)))',
+            $sql
+        );
+        $this->assertEquals('foo', $binder->bindings()[':c0']['value']);
+        $this->assertEquals('bar', $binder->bindings()[':c2']['value']);
+
+        $expressions = [];
+        $values->traverse(function ($exp) use (&$expressions) {
+            $expressions[] = $exp instanceof FunctionExpression ? 1 : 0;
+        });
+
+        $result = array_sum($expressions);
+        $this->assertEquals(2, $result, 'Missing expressions in the tree');
+    }
+}