Commit 88a46ae9 authored by alexpott's avatar alexpott

Issue #2068655 by Berdir, chx, martin107, longwave, webflo, damiankloip,...

Issue #2068655 by Berdir, chx, martin107, longwave, webflo, damiankloip, lokapujya, JacobSanford: Entity fields do not support case sensitive queries
parent 85764e4e
......@@ -449,6 +449,9 @@ field.storage_settings.string:
max_length:
type: integer
label: 'Maximum length'
case_sensitive:
type: boolean
label: 'Case sensitive'
field.field_settings.string:
type: mapping
......@@ -467,6 +470,10 @@ field.value.string:
field.storage_settings.string_long:
type: mapping
label: 'String (long) settings'
mapping:
case_sensitive:
type: boolean
label: 'Case sensitive'
field.field_settings.string_long:
type: mapping
......@@ -489,6 +496,9 @@ field.storage_settings.uri:
max_length:
type: integer
label: 'Maximum length'
case_sensitive:
type: boolean
label: 'Case sensitive'
field.field_settings.uri:
type: mapping
......
......@@ -271,3 +271,4 @@ field.formatter.settings.uri_link:
field.formatter.settings.timestamp_ago:
type: mapping
label: 'Timestamp ago display format settings'
......@@ -229,6 +229,7 @@ public function mapConditionOperator($operator) {
// In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
// statements, we need to use ILIKE instead.
'LIKE' => array('operator' => 'ILIKE'),
'LIKE BINARY' => array('operator' => 'LIKE'),
'NOT LIKE' => array('operator' => 'NOT ILIKE'),
'REGEXP' => array('operator' => '~*'),
);
......
......@@ -24,6 +24,19 @@
* Do not use this method to test for NULL values. Instead, use
* QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull().
*
* Drupal considers LIKE case insensitive and the following is often used
* to tell the database that case insensitive equivalence is desired:
* @code
* db_select('users')
* ->condition('name', db_like($name), 'LIKE')
* @endcode
* Use 'LIKE BINARY' instead of 'LIKE' for case sensitive queries.
*
* Note: When using MySQL, the exact behavior also depends on the used
* collation. if the field is set to binary, then a LIKE condition will also
* be case sensitive and when a case insensitive collation is used, the =
* operator will also be case insensitive.
*
* @param $field
* The name of the field to check. If you would like to add a more complex
* condition involving operators or functions, use where().
......@@ -33,8 +46,8 @@
* the array is dependent on the $operator.
* @param $operator
* The comparison operator, such as =, <, or >=. It also accepts more
* complex options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is
* an array, and = otherwise.
* complex options such as IN, LIKE, LIKE BINARY, or BETWEEN. Defaults to IN
* if $value is an array, and = otherwise.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
......
......@@ -498,6 +498,13 @@ public function &getUnion() {
return $this->union;
}
/**
* {@inheritdoc}
*/
public function escapeLike($string) {
return $this->connection->escapeLike($string);
}
/**
* {@inheritdoc}
*/
......@@ -984,4 +991,5 @@ public function __clone() {
$this->union[$key]['query'] = clone($aggregate['query']);
}
}
}
......@@ -171,6 +171,13 @@ public function &getUnion() {
return $this->query->getUnion();
}
/**
* {@inheritdoc}
*/
public function escapeLike($string) {
return $this->query->escapeLike($string);
}
public function getArguments(PlaceholderInterface $queryPlaceholder = NULL) {
return $this->query->getArguments($queryPlaceholder);
}
......
......@@ -125,6 +125,19 @@ public function &getTables();
*/
public function &getUnion();
/**
* Escapes characters that work as wildcard characters in a LIKE pattern.
*
* @param $string
* The string to escape.
*
* @return string
* The escaped string.
*
* @see \Drupal\Core\Database\Connection::escapeLike()
*/
public function escapeLike($string);
/**
* Compiles and returns an associative array of the arguments for this prepared statement.
*
......
......@@ -54,7 +54,7 @@ public function getEntityTypeId();
* same delta within that field.
* @param $value
* The value for $field. In most cases, this is a scalar and it's treated as
* case-insensitive. For more complex options, it is an array. The meaning
* case-insensitive. For more complex operators, it is an array. The meaning
* of each element in the array is dependent on $operator.
* @param $operator
* Possible values:
......
......@@ -33,20 +33,20 @@ public function compile($conditionContainer) {
// SQL query object is only necessary to pass to Query::addField() so it
// can join tables as necessary. On the other hand, conditions need to be
// added to the $conditionContainer object to keep grouping.
$sqlQuery = $conditionContainer instanceof SelectInterface ? $conditionContainer : $conditionContainer->sqlQuery;
$tables = $this->query->getTables($sqlQuery);
$sql_query = $conditionContainer instanceof SelectInterface ? $conditionContainer : $conditionContainer->sqlQuery;
$tables = $this->query->getTables($sql_query);
foreach ($this->conditions as $condition) {
if ($condition['field'] instanceOf ConditionInterface) {
$sqlCondition = new SqlCondition($condition['field']->getConjunction());
$sql_condition = new SqlCondition($condition['field']->getConjunction());
// Add the SQL query to the object before calling this method again.
$sqlCondition->sqlQuery = $sqlQuery;
$condition['field']->compile($sqlCondition);
$sqlQuery->condition($sqlCondition);
$sql_condition->sqlQuery = $sql_query;
$condition['field']->compile($sql_condition);
$sql_query->condition($sql_condition);
}
else {
$type = strtoupper($this->conjunction) == 'OR' || $condition['operator'] == 'IS NULL' ? 'LEFT' : 'INNER';
$this->translateCondition($condition);
$field = $tables->addField($condition['field'], $type, $condition['langcode']);
static::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
$conditionContainer->condition($field, $condition['value'], $condition['operator']);
}
}
......@@ -70,24 +70,70 @@ public function notExists($field, $langcode = NULL) {
* Translates the string operators to SQL equivalents.
*
* @param array $condition
* The condition array.
* @param \Drupal\Core\Database\Query\SelectInterface $sql_query
* Select query instance.
* @param bool|null $case_sensitive
* If the condition should be case sensitive or not, NULL if the field does
* not define it.
*
* @see \Drupal\Core\Database\Query\ConditionInterface::condition()
*/
protected function translateCondition(&$condition) {
public static function translateCondition(&$condition, SelectInterface $sql_query, $case_sensitive) {
// There is nothing we can do for IN ().
if (is_array($condition['value'])) {
return;
}
// Ensure that the default operator is set to simplify the cases below.
if (empty($condition['operator'])) {
$condition['operator'] = '=';
}
switch ($condition['operator']) {
case '=':
// If a field explicitly requests that queries should not be case
// sensitive, use the LIKE operator, otherwise keep =.
if ($case_sensitive === FALSE) {
$condition['value'] = $sql_query->escapeLike($condition['value']);
$condition['operator'] = 'LIKE';
}
break;
case '<>':
// If a field explicitly requests that queries should not be case
// sensitive, use the NOT LIKE operator, otherwise keep <>.
if ($case_sensitive === FALSE) {
$condition['value'] = $sql_query->escapeLike($condition['value']);
$condition['operator'] = 'NOT LIKE';
}
break;
case 'STARTS_WITH':
$condition['value'] .= '%';
$condition['operator'] = 'LIKE';
if ($case_sensitive) {
$condition['operator'] = 'LIKE BINARY';
}
else {
$condition['operator'] = 'LIKE';
}
$condition['value'] = $sql_query->escapeLike($condition['value']) . '%';
break;
case 'CONTAINS':
$condition['value'] = '%' . $condition['value'] . '%';
$condition['operator'] = 'LIKE';
if ($case_sensitive) {
$condition['operator'] = 'LIKE BINARY';
}
else {
$condition['operator'] = 'LIKE';
}
$condition['value'] = '%' . $sql_query->escapeLike($condition['value']) . '%';
break;
case 'ENDS_WITH':
$condition['value'] = '%' . $condition['value'];
$condition['operator'] = 'LIKE';
if ($case_sensitive) {
$condition['operator'] = 'LIKE BINARY';
}
else {
$condition['operator'] = 'LIKE';
}
$condition['value'] = '%' . $sql_query->escapeLike($condition['value']);
break;
}
}
......
......@@ -10,6 +10,7 @@
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Query\ConditionAggregateBase;
use Drupal\Core\Entity\Query\ConditionAggregateInterface;
use Drupal\Core\Database\Query\Condition as SqlCondition;
/**
* Defines the aggregate condition for sql based storage.
......@@ -29,7 +30,7 @@ public function compile($conditionContainer) {
$tables = new Tables($sql_query);
foreach ($this->conditions as $condition) {
if ($condition['field'] instanceOf ConditionAggregateInterface) {
$sql_condition = new Condition($condition['field']->getConjunction());
$sql_condition = new SqlCondition($condition['field']->getConjunction());
// Add the SQL query to the object before calling this method again.
$sql_condition->sqlQuery = $sql_query;
$condition['field']->compile($sql_condition);
......@@ -37,8 +38,8 @@ public function compile($conditionContainer) {
}
else {
$type = ((strtoupper($this->conjunction) == 'OR') || ($condition['operator'] == 'IS NULL')) ? 'LEFT' : 'INNER';
$this->translateCondition($condition);
$field = $tables->addField($condition['field'], $type, $condition['langcode']);
Condition::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
$function = $condition['function'];
$placeholder = ':db_placeholder_' . $conditionContainer->nextPlaceholder();
$conditionContainer->having("$function($field) {$condition['operator']} $placeholder", array($placeholder => $condition['value']));
......@@ -60,29 +61,4 @@ public function notExists($field, $function, $langcode = NULL) {
return $this->condition($field, $function, NULL, 'IS NULL', $langcode);
}
/**
* Translates the string operators to SQL equivalents.
*
* @param array $condition
* An associative array containing the following keys:
* - value: The value to filter by
* - operator: The operator to use for comparison, for example "=".
*/
protected function translateCondition(&$condition) {
switch ($condition['operator']) {
case 'STARTS_WITH':
$condition['value'] .= '%';
$condition['operator'] = 'LIKE';
break;
case 'CONTAINS':
$condition['value'] = '%' . $condition['value'] . '%';
$condition['operator'] = 'LIKE';
break;
case 'ENDS_WITH':
$condition['value'] = '%' . $condition['value'];
$condition['operator'] = 'LIKE';
break;
}
}
}
......@@ -48,6 +48,13 @@ class Tables implements TablesInterface {
*/
protected $entityManager;
/**
* List of case sensitive fields.
*
* @var array
*/
protected $caseSensitiveFields = array();
/**
* @param \Drupal\Core\Database\Query\SelectInterface $sql_query
*/
......@@ -139,6 +146,10 @@ public function addField($field, $type, $langcode) {
}
$table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
$sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
$property_definitions = $field_storage->getPropertyDefinitions();
if (isset($property_definitions[$column])) {
$this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
}
}
// The field is stored in a shared table.
else {
......@@ -155,6 +166,17 @@ public function addField($field, $type, $langcode) {
$entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
$sql_column = $specifier;
$table = $this->ensureEntityTable($index_prefix, $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
// If there is a field storage (some specifiers are not, like
// default_langcode), check for case sensitivity.
if ($field_storage) {
$column = $field_storage->getMainPropertyName();
$base_field_property_definitions = $field_storage->getPropertyDefinitions();
if (isset($base_field_property_definitions[$column])) {
$this->caseSensitiveFields[$field] = $base_field_property_definitions[$column]->getSetting('case_sensitive');
}
}
}
// If there are more specifiers to come, it's a relationship.
if ($field_storage && $key < $count) {
......@@ -186,6 +208,15 @@ public function addField($field, $type, $langcode) {
return "$table.$sql_column";
}
/**
* {@inheritdoc}
*/
public function isFieldCaseSensitive($field_name) {
if (isset($this->caseSensitiveFields[$field_name])) {
return $this->caseSensitiveFields[$field_name];
}
}
/**
* Join entity table if necessary and return the alias for it.
*
......
......@@ -20,8 +20,8 @@
* then entity property name.
* @param string $type
* Join type, can either be INNER or LEFT.
* @param $langcode
* The language code the field values are to be shown in.
* @param string $langcode
* The language code the field values are to be queried in.
*
* @throws \Drupal\Core\Entity\Query\QueryException
* If $field specifies an invalid relationship.
......@@ -33,4 +33,18 @@
*/
public function addField($field, $type, $langcode);
/**
* Returns whether the given field is case sensitive.
*
* This information can only be provided after it was added with addField().
*
* @param string $field_name
* The name of the field.
*
* @return bool|null
* TRUE if the field is case sensitive, FALSE if not. Returns NULL when the
* field did not define if it is case sensitive or not.
*/
public function isFieldCaseSensitive($field_name);
}
......@@ -44,6 +44,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
'type' => 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
'not null' => FALSE,
'binary' => $field_definition->getSetting('case_sensitive'),
),
),
);
......
......@@ -17,6 +17,15 @@
*/
abstract class StringItemBase extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return array(
'case_sensitive' => FALSE,
) + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
......@@ -24,7 +33,8 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
// This is called very early by the user entity roles field. Prevent
// early t() calls by using the TranslationWrapper.
$properties['value'] = DataDefinition::create('string')
->setLabel(new TranslationWrapper('Text value'));
->setLabel(new TranslationWrapper('Text value'))
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'));
return $properties;
}
......
......@@ -31,7 +31,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
return array(
'columns' => array(
'value' => array(
'type' => 'text',
'type' => $field_definition->getSetting('case_sensitive') ? 'blob' : 'text',
'size' => 'big',
),
),
......
......@@ -41,7 +41,8 @@ public static function defaultStorageSettings() {
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('uri')
->setLabel(t('URI value'));
->setLabel(t('URI value'))
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'));
return $properties;
}
......@@ -56,6 +57,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
'type' => 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
'not null' => TRUE,
'binary' => $field_definition->getSetting('case_sensitive'),
),
),
);
......
......@@ -260,7 +260,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['uri'] = BaseFieldDefinition::create('uri')
->setLabel(t('URI'))
->setDescription(t('The URI to access the file (either local or remote).'))
->setSetting('max_length', 255);
->setSetting('max_length', 255)
->setSetting('case_sensitive', TRUE);
$fields['filemime'] = BaseFieldDefinition::create('string')
->setLabel(t('File MIME type'))
......
......@@ -31,9 +31,6 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st
case 'uri':
$this->addSharedTableFieldUniqueKey($storage_definition, $schema, TRUE);
// @todo There should be a 'binary' field type or setting:
// https://www.drupal.org/node/2068655.
$schema['fields'][$field_name]['binary'] = TRUE;
break;
}
}
......
......@@ -7,7 +7,7 @@
namespace Drupal\file\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\file\Entity\File;
/**
* File saving tests.
......@@ -17,7 +17,7 @@
class SaveTest extends FileManagedUnitTestBase {
function testFileSave() {
// Create a new file entity.
$file = entity_create('file', array(
$file = File::create(array(
'uid' => 1,
'filename' => 'druplicon.txt',
'uri' => 'public://druplicon.txt',
......@@ -59,7 +59,7 @@ function testFileSave() {
// Try to insert a second file with the same name apart from case insensitivity
// to ensure the 'uri' index allows for filenames with different cases.
$file = entity_create('file', array(
$uppercase_file = File::create(array(
'uid' => 1,
'filename' => 'DRUPLICON.txt',
'uri' => 'public://DRUPLICON.txt',
......@@ -68,7 +68,16 @@ function testFileSave() {
'changed' => 1,
'status' => FILE_STATUS_PERMANENT,
));
file_put_contents($file->getFileUri(), 'hello world');
$file->save();
file_put_contents($uppercase_file->getFileUri(), 'hello world');
$uppercase_file->save();
// Ensure that file URI entity queries are case sensitive.
$fids = \Drupal::entityQuery('file')
->condition('uri', $uppercase_file->getFileUri())
->execute();
$this->assertEqual(1, count($fids));
$this->assertEqual(array($uppercase_file->id() => $uppercase_file->id()), $fids);
}
}
......@@ -45,7 +45,7 @@ class EntityQueryAggregateTest extends EntityUnitTestBase {
protected function setUp() {
parent::setUp();
$this->entityStorage = $this->container->get('entity.manager')->getStorage('entity_test');
$this->entityStorage = $this->entityManager->getStorage('entity_test');
$this->factory = $this->container->get('entity.query');
// Add some fieldapi fields to be used in the test.
......
......@@ -9,6 +9,9 @@
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Symfony\Component\HttpFoundation\Request;
......@@ -490,7 +493,7 @@ protected function assertBundleOrder($order) {
*
* The tags and metadata should propagate to the SQL query object.
*/
function testMetaData() {
public function testMetaData() {
$query = \Drupal::entityQuery('entity_test_mulrev');
$query
->addTag('efq_metadata_test')
......@@ -500,4 +503,167 @@ function testMetaData() {
global $efq_test_metadata;
$this->assertEqual($efq_test_metadata, 'bar', 'Tag and metadata propagated to the SQL query object.');
}
/**
* Test case sensitive and in-sensitive query conditions.
*/
public function testCaseSensitivity() {
$bundle = $this->randomMachineName();
$field_storage = FieldStorageConfig::create(array(
'field_name' => 'field_ci',
'entity_type' => 'entity_test_mulrev',
'type' => 'string',
'cardinality' => 1,
'translatable' => FALSE,
'settings' => array(
'case_sensitive' => FALSE,
)
));
$field_storage->save();
FieldConfig::create(array(
'field_storage' => $field_storage,
'bundle' => $bundle,
))->save();
$field_storage = FieldStorageConfig::create(array(
'field_name' => 'field_cs',
'entity_type' => 'entity_test_mulrev',
'type' => 'string',
'cardinality' => 1,
'translatable' => FALSE,
'settings' => array(
'case_sensitive' => TRUE,
),
));
$field_storage->save();
FieldConfig::create(array(
'field_storage' => $field_storage,
'bundle' => $bundle,
))->save();
$fixtures = array();
for ($i = 0; $i < 2; $i++) {
$string = $this->randomMachineName();
$fixtures[] = array(
'original' => $string,
'uppercase' => Unicode::strtoupper($string),
'lowercase' => Unicode::strtolower($string),
);
}
EntityTestMulRev::create(array(
'type' => $bundle,
'name' => $this->randomMachineName(),
'langcode' => 'en',
'field_ci' => $fixtures[0]['uppercase'] . $fixtures[1]['lowercase'],
'field_cs' => $fixtures[0]['uppercase'] . $fixtures[1]['lowercase']
))->save();
// Check the case insensitive field, = operator.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[0]['lowercase'] . $fixtures[1]['lowercase']
)->execute();
$this->assertIdentical(count($result), 1, 'Case insensitive, lowercase');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[0]['uppercase'] . $fixtures[1]['uppercase']
)->execute();
$this->assertIdentical(count($result), 1, 'Case insensitive, uppercase');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[0]['uppercase'] . $fixtures[1]['lowercase']
)->execute();
$this->assertIdentical(count($result), 1, 'Case insensitive, mixed.');
// Check the case sensitive field, = operator.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[0]['lowercase'] . $fixtures[1]['lowercase']
)->execute();
$this->assertIdentical(count($result), 0, 'Case sensitive, lowercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[0]['uppercase'] . $fixtures[1]['uppercase']
)->execute();
$this->assertIdentical(count($result), 0, 'Case sensitive, uppercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[0]['uppercase'] . $fixtures[1]['lowercase']
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, exact match.');
// Check the case insensitive field, STARTS_WITH operator.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[0]['lowercase'], 'STARTS_WITH'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, lowercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[0]['uppercase'], 'STARTS_WITH'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, exact match.');
// Check the case sensitive field, STARTS_WITH operator.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[0]['lowercase'], 'STARTS_WITH'
)->execute();
$this->assertIdentical(count($result), 0, 'Case sensitive, lowercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[0]['uppercase'], 'STARTS_WITH'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, exact match.');
// Check the case insensitive field, ENDS_WITH operator.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[1]['lowercase'], 'ENDS_WITH'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, lowercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', $fixtures[1]['uppercase'], 'ENDS_WITH'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, exact match.');
// Check the case sensitive field, ENDS_WITH operator.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[1]['lowercase'], 'ENDS_WITH'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, lowercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_cs', $fixtures[1]['uppercase'], 'ENDS_WITH'
)->execute();
$this->assertIdentical(count($result), 0, 'Case sensitive, exact match.');
// Check the case insensitive field, CONTAINS operator, use the inner 8
// characters of the uppercase and lowercase strings.
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', Unicode::substr($fixtures[0]['uppercase'] . $fixtures[1]['lowercase'], 4, 8), 'CONTAINS'
)->execute();
$this->assertIdentical(count($result), 1, 'Case sensitive, lowercase.');
$result = \Drupal::entityQuery('entity_test_mulrev')->condition(
'field_ci', Unicode::<