EntityStorageBase::loadByProperties() is broken on PostgreSQL when using two or more case insensitive properties

Created on 31 July 2020, over 4 years ago
Updated 20 August 2023, over 1 year ago

I faced with problem when try to load entities using loadByProperties method. If I set two and more properties I don't get any results.
So, I create a new content type test_content_type with two fields: field_first - stirng, limited (1) and field_second - string, unlimited.

I create nodes of this content type

and I try to load that content with the next command

$res = Drupal::entityTypeManager()
  ->getStorage('node')
  ->loadByProperties([
    'field_first' => '12345',
    'field_second' => 'test_value_1'
  ]);

I have nothing in result. I investigate the problem and found next issue in the method translateCondition of the Drupal\Core\Entity\Query\Sql\pgsql\Condition class. In the project we use PostgradeSQL database.

In line #21 there is the base code, in line #22 my fix. As we can see in case when $condition['value'] is array placeholders starts from $n = 1. And this method builds placeholders :value1, :value2 and etc for each parameter in one property.
But if we set two properties, we will have next situation. The compile() method of the class Drupal\Core\Entity\Query\Sql\Condition runs translateCondition foreach element (searching property) in array that passed to loadByProperties. In my case for field_first and field_second.

So, as result we have placeholder :value1 as for field_first and as for field_second. I debug SQL query and there is next:

SELECT base_table.vid AS vid, base_table.nid AS nid 
FROM node base_table 
INNER JOIN node__field_first node__field_first ON node__field_first.entity_id = base_table.nid 
INNER JOIN node__field_second node__field_second ON node__field_second.entity_id = base_table.nid 
INNER JOIN node_field_data node_field_data ON node_field_data.nid = base_table.nid 
WHERE ((LOWER(node__field_first.field_first_value) IN (LOWER('12345')))) AND ((LOWER(node__field_second.field_second_value) IN (LOWER('12345')))) AND (node_field_data.default_langcode IN ('1'))

As we can see for both fields node__field_first.field_first_value and node__field_second.field_second_value query use one searching value 12345. It because in line #21 of translateCondition method each condition starts from number 1. It clearly seen at the kint debug of condition:

If I use next command:

$res = Drupal::entityTypeManager()
  ->getStorage('node')
  ->loadByProperties([
    'field_first' => ['12345', '45678']
    'field_second' => 'test_value_1'
  ]);

The SQL query will be:

SELECT base_table.vid AS vid, base_table.nid AS nid
FROM node base_table 
INNER JOIN node__field_first node__field_first ON node__field_first.entity_id = base_table.nid 
INNER JOIN node__field_second node__field_second ON node__field_second.entity_id = base_table.nid 
INNER JOIN node_field_data node_field_data ON node_field_data.nid = base_table.nid 
WHERE ((LOWER(node__field_first.field_first_value) IN (LOWER('12345'),LOWER('45678')))) AND ((LOWER(node__field_second.field_second_value) IN (LOWER('12345')))) AND (node_field_data.default_langcode IN ('1'))

And kint debug:

We see, that for the field_first query has two placeholders :value1 and :value2, but for field_second again used placeholder :value1 instead of :value3. And as result we search `12345` in field_second instead of `test_value_1`.

If I uncomented line #22 and hide code in line #21

      //$n = 1;
      $n = count($sql_query->getArguments()) + 1;

And call to loadByProperties method I get next results. I display case when first field has multiple values.
SQL query become correct:

SELECT base_table.vid AS vid, base_table.nid AS nid 
FROM node base_table 
INNER JOIN node__field_first node__field_first ON node__field_first.entity_id = base_table.nid 
INNER JOIN node__field_second node__field_second ON node__field_second.entity_id = base_table.nid 
INNER JOIN node_field_data node_field_data ON node_field_data.nid = base_table.nid WHERE ((LOWER(node__field_first.field_first_value) IN (LOWER('12345'),LOWER('45678')))) AND ((LOWER(node__field_second.field_second_value) IN (LOWER('test_value_1')))) AND (node_field_data.default_langcode IN ('1'))

The field node__field_second.field_second_value has correct searching value test_value_1

And in condition kint debug we see that all placeholders are correct:

Please review described issue and if there is relevant fix it. Because it's very uncomfortable lose functionality of loadByProperties method.

πŸ› Bug report
Status

Fixed

Version

8.9 ⚰️

Component
PostgreSQL driverΒ  β†’

Last updated 10 days ago

No maintainer
Created by

πŸ‡ΊπŸ‡¦Ukraine vadim.jin

Live updates comments and jobs are added and updated live.
  • PostgreSQL

    Particularly affects sites running on the PostgreSQL database.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡²πŸ‡½Mexico luisnicg

    The patch works perfectly with ::loadByProperties() as it expects and array as value ​​and will add IN operator, in my case I am facing the same problem using ::entityQuery(), being more specific, I'm working with the menu_export module (and PostgreSQL of course) and it has this query:

          $menuLinkEntity = \Drupal::entityQuery('menu_link_content')
            ->accessCheck(FALSE)
            ->condition('uuid', $menu['uuid'])
            ->execute();
    

    Where $menu['uuid'] is an array of values.

    The problem with this is that the condition operator is not defined, and when the translateCondition() is executed, it didn't add the operator, which should be "IN", causing this error:

    SQLSTATE[42601]: Syntax error: 7 ERROR:  syntax error at or near "("
    LINE 5: WHERE (LOWER("menu_link_content"."uuid")  (LOWER('XXXXX

    Not sure if it should be a core or module issue, I added a patch that worked for me.

    I didn't have enough time to add a test, it would be something like this:

        $node1 = $this->drupalCreateNode([
          'type' => 'page',
          'field_first' => '1234',
          'field_second' => 'test_value_1',
        ]);
        $nodes = $this->container->get('entity_type.manager')->getStorage('node')->getQuery()->condition('uuid',[$node1->uuid()])->execute();
        // @todo validate results
    
Production build 0.71.5 2024