Problem

Started as an issue to allow query like SELECT ... FROM ... WHERE ... AND (SELECT COUNT(*) ...) = value), it turns out that the new algorithm is very generic and allows for many more situations that are allowed by (standard) SQL:

  • Select query at left hand side:
      SELECT ...
      FROM ...
      WHERE (SELECT COUNT(*) FROM ... WHERE ...) > value
      
  • Select query at right hand side:
      SELECT ...
      FROM ...
      WHERE value < (SELECT AVG(...) FROM ...)
      
  • Multiple select queries at right hand side:
      SELECT ...
      FROM ...
      WHERE value BETWEEN (SELECT MIN(...) FROM ...) AND (SELECT MAX(...) FROM ...)
      
  • Select query at left and right hand side (new test):
      SELECT t.name
      FROM test t
      WHERE (SELECT AVG(tt.priority) FROM test_task tt WHERE tt.pid = t.id) > (SELECT AVG(tt2.priority) FROM test_task tt2)
      
  • Select query at left hand side and multiple select queries at right hand side:
      SELECT ...
      FROM ...
      WHERE (SELECT AVG(...) FROM ... WHERE ...) BETWEEN (SELECT MIN(...) FROM ...) AND (SELECT MAX(...) FROM ...)
      

Original post

[Note: issue #369314: Subselects don't work in DB conditions... handled only the IN case]

I was trying to construct a query with a subselect that looks like:

SELECT ...
FROM ...
WHERE ...
  AND (SELECT COUNT(*) ...) = value)

However, I did not entirely manage to get it to work.

Attempt 1:

$query->condition($count_subquery, $value, '=');

Fails because $count_subquery is a SelectQueryInterface which is a QueryConditionInterface. And if the $field argument is a QueryConditionInterface it gets compiled and placed in the main query as is, thereby ignoring the $value and $operator arguments.

Attempt 2:

$query->condition($value, $count_subquery, '=');

Fails because no brackets are placed around the subquery.

This leads to a workaround:

$query->condition($value, $count_subquery, 'IN');

This happens to work because the IN operator places brackets around the value(s). However, it will not always be possible to use IN. There are times you may want to use a comparison operator other than '=', e.g. <, <=, >, >=. Also, at times, it may be necessary to compare 2 subselects (select objects whose average score is higher than the overall average score).

So, the error can be split into 2 situations:
1) SelectQueryInterface objects as the left hand side ($field operator) are not handled correctly.
2) SelectQueryInterface objects as the right hand side ($value operator) are only handled correctly when the operator happens to be 'IN'.

Proposed resolution

To solve 1)
Method DatabaseCondition::compile(): Add an additional check to this if:

          if ($condition['field'] instanceof QueryConditionInterface) {

Something like:

          if ($condition['field'] instanceof QueryConditionInterface && !($condition['field'] instanceof SelectQueryInterface && isset($condition['value']))) {

Subsequently the else branch needs to handle this situation, by adding:
- Compile on the $condition['field'] if it is a SelectQueryInterface.
- Brackets around that result of compile.

To solve 2)
- This will need some refactoring of the above mentioned else branch.

Remaining tasks

Can some experts regarding this part of Drupal confirm this is indeed an error and is a use case that is to be supported. Is the proposed solution in the correct direction. If so, I will try to write a patch including tests.

CommentFileSizeAuthor
#98 interdiff-1267508-92-98.txt1.31 KBdaffie
#98 1267508-98.patch31.9 KBdaffie
#94 1267508-93-without-the-fix.patch31.86 KBdaffie
#92 1267508-92-without-the-fix.patch31.91 KBdaffie
#92 1267508-92.patch31.91 KBdaffie
#92 interdiff-1267508-65-92.txt2.43 KBdaffie
#67 interdiff.txt3.99 KBfietserwin
#67 subselects_don_t_work-1267508-65.patch30.77 KBfietserwin
#62 interdiff.txt4.18 KBfietserwin
#62 subselects_don_t_work-1267508-62.patch30.96 KBfietserwin
#59 subselects_don_t_work-1267508-59.patch29.24 KBfietserwin
#58 subselects_don_t_work-1267508-58.patch28.65 KBfietserwin
#53 interdiff.txt2.23 KBfietserwin
#53 subselects_don_t_work-1267508-53.patch29.26 KBfietserwin
#50 interdiff.txt2.35 KBfietserwin
#50 subselects_don_t_work-1267508-50.patch29.28 KBfietserwin
#47 interdiff.txt1.31 KBfietserwin
#47 subselects_don_t_work-1267508-47.patch27.42 KBfietserwin
#45 interdiff.txt3.07 KBfietserwin
#45 subselects_don_t_work-1267508-45.patch27.48 KBfietserwin
#43 interdiff.txt6.37 KBfietserwin
#43 subselects_don_t_work-1267508-43.patch27.12 KBfietserwin
#41 interdiff.txt3.72 KBfietserwin
#41 subselects_don_t_work-1267508-41.patch20.76 KBfietserwin
#39 interdiff.txt2.91 KBfietserwin
#39 subselects_don_t_work-1267508-39.patch20.63 KBfietserwin
#35 subselects_don_t_work-1267508-35.patch20.39 KBfietserwin
#32 1267508-32-tests-only.patch2.81 KBdaffie
#26 subselects_don_t_work-1267508.patch2.83 KBlisa.ugray
#26 subselects_don_t_work-1267508-FAIL.patch1.14 KBlisa.ugray
#25 subselects_don_t_work-1267508-25.patch19.12 KBfietserwin
#13 1267508-13.patch19.35 KBfietserwin
#12 1267508-subselects-12.patch15.75 KBfietserwin
#7 1267508-subselects.patch11.95 KBfietserwin
#5 1267508-subselects-test-only.patch2.77 KBfietserwin
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

fietserwin’s picture

Version: 7.x-dev » 8.x-dev
Issue tags: +Needs backport to D7

Error is also in D8, solve there first.

Agileware’s picture

I can confirm this bug.

Damien Tournoud’s picture

Category: bug » feature

Yep, it was never intended to support sub-queries used as scalars. Let's see how to add support for this in Drupal 8.

Agileware’s picture

I have updated the documentation at http://drupal.org/node/310086 to make it clear that for now this only works with IN.

fietserwin’s picture

Assigned: Unassigned » fietserwin
Status: Active » Needs review
FileSize
2.77 KB

This patch includes tests that should test the proper processing of subqueries as scalars. It is test only, thus supposed to fail. Complete patch follows soon, running all tests once again locally.

@#3: It's OK, if it was not supposed to support these, but the way that placeholders are generated makes it very difficult (read next to impossible) to hack your own queries in by e.g. first processing them. So IMO, it should indeed support as much as possible.

Status: Needs review » Needs work

The last submitted patch, 1267508-subselects-test-only.patch, failed testing.

fietserwin’s picture

Status: Needs work » Needs review
FileSize
11.95 KB

This patch includes a solution as well. It is a rewrite of the compile() method to make it accept more situations. It will accept subconditions or subqueries at almost any place where a value is accepted. Though there is not a test for it, it should e.g. also accept 2 subqueries using the BETWEEN operator.

To make it even more flexible, we could accept arrays as operator, though that would expose the data structure used to represent operators. I'm not sure if we want that. On the other hand, it would allow for conditions using the more exotic operators like ANY, SOME, and ALL.

dawehner’s picture

Status: Needs review » Needs work

This patch worked fine for me on manual testing on my use case.

+++ b/includes/database/query.incundefined
@@ -1633,7 +1633,7 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
+    if (!isset($operator) && !($field instanceof QueryConditionInterface)) {

Can you explain this change? Isn't it a valid use case to use condition($subquery, $values); ? Perhaps some kind of comment helps as well.

+++ b/includes/database/query.incundefined
@@ -1727,67 +1727,97 @@ class DatabaseCondition implements QueryConditionInterface, Countable {
+              $arguments[$placeholder] = $value;
+              $value_fragment[] = $placeholder;

Just for similarity on all other places first the value/field_fragment is created then the argument, this should probably be switch here.

In general the code is now even a bit easier to understand and described in detail.
0 days to next Drupal core point release.

chx’s picture

$query->condition($value, $count_subquery, '=');

a subquery returns a set, a set can't be equal to a scalar. What is this issue?

chx’s picture

OK I eat my words. Apparently scalar subqueries http://kb.askmonty.org/en/scalar-subqueries are a standard feature where SELECT returns 1 or 0 rows.

Hell knows whether this is ANSI. http://www.akadia.com/services/ora_scalar_subquery.html Oracle 9. http://www.databasejournal.com/features/mssql/article.php/3407911/Scalar... MS SQL back in 2004. And so forth.

chx’s picture

Damien tells me in IRC this is standard. SQL--

fietserwin’s picture

Status: Needs work » Needs review
FileSize
15.75 KB

I changed the patch as indicated in #8:

1)
The first remark is a good catch. My local code was already different... The reason to add the check has to do that compiling the conditions now can handle a condition like "subquery IS NULL". Formerly, $operator was always set to "IS NULL" and subsequently ignored when $field turned out to be a QueryConditionInterface. Now a SelectQueryInterface, which is also a QueryConditionInterface, can be compared to NULL.

The requested comments are added to the documentation of QueryConditionInterface::condition() as that did (already before this patch) not cover all accepted situations.

2)
The 2nd remark has been processed as indicated.

3)
White space removal is not removed from the patch this time as with the previous patch. So, it may be a bit harder to read but the result will be better :).

fietserwin’s picture

FileSize
19.35 KB

I updated the patch:

  • To apply to the current HEAD, including:
  • The move to /core.
  • Resolved conflicting merge with issue #813540: Comparisons involving NULL must never return true.
  • Further improved comments and documentation.
  • Updated to the new documentation standards (always specify argument and return types).

As the methods condition(), where(), exists(), notExists() and conditions() are tightly coupled around the possibilities provided by this query builder, I added/changed also documentation for those methods, even if they may bot be changed in itself. This to further clarify when to use which method and what the options/restrictions of it are.

fietserwin’s picture

Status: Needs review » Needs work

Can this be rewritten now that #1321540: Convert DBTNG to namespaces; separate Drupal bits has landed or are there more disturbing patches to be expected soon?

kiranjala’s picture

Status: Needs work » Needs review

#7: 1267508-subselects.patch queued for re-testing.

kiranjala’s picture

please ignore the above request.

DanZ’s picture

I want to do this:

SELECT sid
FROM `uc_shipments`
WHERE order_id NOT
IN (
  SELECT order_id
  FROM `uc_orders`
)

...or...maybe better:

SELECT s.sid
FROM `uc_shipments` s
WHERE NOT
EXISTS (
  SELECT NULL
  FROM `uc_orders` o
  WHERE s.order_id = o.order_id
)

All of this is a bit over my head. Does the fact that this issue isn't fixed mean that I can't do this with the regular db_select() calls? See http://drupal.stackexchange.com/questions/18497/how-do-i-use-not-in-in-a....

dawehner’s picture

You can certainly do this with notExists($sub_query) ... in general: try to avoid posting somehow support questions in non-support issues. This doesn't really help to move the issue forward, so better test the patch for example.

fietserwin’s picture

Assigned: fietserwin » Unassigned
Status: Needs review » Needs work

I would like to have another look at it and rewrite it to current head, but know that my time will be too limited to do it in the near future. I will reassign to me if I do find time to work on it though.

tcarmona’s picture

Hi, sorry to necro this one, as the last comment was about a year ago.

Is there still need for work on this post? What is the current status? I'm willing to work on this issue if it there isn't a solution.

fietserwin’s picture

The last patch was against the procedural code. So it needs to be rewritten against the current OO code. I have no idea how much of the internals really changed. If not that much, it might be more of logically "relocating" the changes. If yes, the current patch will only guide you by the logic of it.

Thanks for picking this up, I promise to review it quickly.

chx’s picture

Please look at #1837118: UPDATE foo SET bar=(SELECT...) is not supported for a similar problem + accepted solution.

fietserwin’s picture

They are indeed related as the crux of that patch indeed also appears in this patch. However, this patch is about improved handling of conditions in the where clause, where the subquery may be at the left side, right side or both sides and all kinds of operators may be used. The other patch is about handling the set clause where subqueries can only appear at the right hand side and the operator is always =.

Thus this issue is still valid on its own. So, @tcarmona, if you want to give it a try, go ahead. A large part of the query is documentation improvements, as a few years ago I found that it was lacking to describe the full capabilities of the query conditions. I don't know if that has improved since.

chx’s picture

Yes the issue is certainly valid I was just suggesting the other issue for the architectural solution, calling compile and such . If this is not enough, I can dig it up what we did over there, I certainly can't remember it's been *ages* since that one but I'd be glad to do dig into it and help this issue be fixed.

fietserwin’s picture

Status: Needs work » Needs review
FileSize
19.12 KB

Updated the patch to the current state of the database code. It was mainly new locations for the files and some class name changes (due to namespacing). Let's see if other tests now fail (I caught one that did a match on the generated query text, while the condition compiler now uses less spaces and brackets.

lisa.ugray’s picture

Here's a much simpler patch more suited to the current state of D8. I've marked this as a bug report and not a feature request since the documentation suggests this should work, and this is standard SQL syntax. The -FAIL.patch adds a test that demonstrates the problem, and the other patch has the test and the fix.

The last submitted patch, 26: subselects_don_t_work-1267508-FAIL.patch, failed testing.

fietserwin’s picture

Much simpler indeed, but it looks like it doesn't solve the first error in the issue summary: a sub select at the left hand side makes the code ignore the operator and value arguments (attempt 1).

So, to progress, I think it would be better to get back to my patch in #25 and see if that still works and review that one.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

The last submitted patch, 26: subselects_don_t_work-1267508-FAIL.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 26: subselects_don_t_work-1267508.patch, failed testing.

daffie’s picture

I have to agree with fietsenwin. The fix from lisa.ugray is a good find, but for me a quit and dirty fix. I do like the fix from fietsenwin.

I was not able to do a easy reroll for that fix from the fietsenwin patch. The tests for that patch where easily rerolled. The tests from the patch are the ones we need to test any solution.

I would like to get this issue fixed. And I will do so by reviewing.

Status: Needs review » Needs work

The last submitted patch, 32: 1267508-32-tests-only.patch, failed testing.

daffie’s picture

Issue summary: View changes
fietserwin’s picture

Version: 8.1.x-dev » 8.2.x-dev
Status: Needs work » Needs review
Issue tags: -Needs backport to D7
FileSize
20.39 KB

"Manual reroll" from patch #25 (and including the tests from #32).

Some remarks:

  • This missing functionality is now absent for so long without much uproar that it may be seen as a feature instead of a bug. So I don't think a backport to D7 is needed.
  • For the same reason, I set the version to 8.2.x-dev.
  • @daffie: If the tests pass you can review :)

Status: Needs review » Needs work

The last submitted patch, 35: subselects_don_t_work-1267508-35.patch, failed testing.

fietserwin’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 35: subselects_don_t_work-1267508-35.patch, failed testing.

fietserwin’s picture

Status: Needs work » Needs review
FileSize
20.63 KB
2.91 KB

I forgot to "reroll" the detection of the 1 argument call to condition() as with the new parameter defaults that had to be done differently.

Status: Needs review » Needs work

The last submitted patch, 39: subselects_don_t_work-1267508-39.patch, failed testing.

fietserwin’s picture

Status: Needs work » Needs review
FileSize
20.76 KB
3.72 KB

And the detection of conditions created via the where method was not done correctly...

Status: Needs review » Needs work

The last submitted patch, 41: subselects_don_t_work-1267508-41.patch, failed testing.

fietserwin’s picture

And the tests (a lot more then before) that failed because the new compiler outputs less superfluous spaces and brackets. They pass locally, so this one should turn green.

[EDIT: after the 1st green, I added 2 other databases to test]

daffie’s picture

Status: Needs review » Needs work

I will do a full review later. But for now the testbot for SQLite is returning the syntax error: Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[HY000]: General error: 1 near "(": syntax error. You made a lot of changes where you removed the space before the round bracket. I think that is the source of the SQLite bugs.

fietserwin’s picture

Yeah, yesterday, I didn't have time to look at it anymore. I cannot really find any syntax error in the query though, unless sqlite chokes on to many (!) brackets in this part "AND (EXISTS ((...)))".

I had a good look at when and where brackets are expected and decided to make some small changes:
- Each subquery (field or value) gets always brackets around it.
- Thus EXISTS and NOT EXISTS (that only accept a subquery) do not need brackets as pre and postfix anymore: this prevents the double brackets around an EXISTS subquery.
- 'BETWEEN' as an operator does not have its NOT BETWEEN counterpart in the list of supported operators (mapConditionOperator). I added that, though strictly speaking this may be out of scope.

I will start with testing mysql first and add other database tests later.

daffie’s picture

Status: Needs review » Needs work

Some remarks after a short review:

  1. +++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
    @@ -12,13 +12,21 @@
    +   * If called with 1 parameter, it should be a ConditionInterface that in
    +   * itself forms a valid where clause. Use e.g. to build clauses with nested
    +   * AND's and OR's.
    

    Not sure if we have testing for this. I cannot find it.

  2. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -158,86 +163,127 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
    +        else if (!isset($condition['operator'])) {
    

    Nitpick: Must be elseif.

  3. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -158,86 +163,127 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
    +        if ($condition['field'] instanceof ConditionInterface) {
    ...
    +          if ($condition['field'] instanceof SelectInterface) {
    

    Can you explain this one to me. Can $condition['field'] be an instance of ConditionInterface and SelectInterface?

  4. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -300,15 +346,18 @@ protected function mapConditionOperator($operator) {
    +      'NOT BETWEEN' => array('delimiter' => ' AND '),
    

    We have #2686483: Add support for database condition operator "NOT BETWEEN" for adding the "NOT BETWEEN" operator. If this is needed we can postpone this issue on that.

  5. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -158,86 +163,127 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
    +            if ($value instanceof SelectInterface) {
    

    Are we allowing that there be an array of select statement in the $condition['value']? This is something that I do not like. Maybe a test to make sure that it is not allowed.

fietserwin’s picture

#46: Thanks for the review.

  • RE #46.1: This case already exists and is used and tested on multiple places, e.g. to create a where clause like "(A OR B) AND (C OR D)".
  • RE #46.2: Corrected.
  • RE #46.3: SelectInterface extends ConditionInterface, so it is possible and can be used as is tested in testConditionSubquerySelect3():
      SELECT t.name
      FROM test t
      WHERE (SELECT AVG(tt.priority) FROM test_task tt WHERE tt.pid = t.id) > (SELECT AVG(tt2.priority) FROM test_task tt2)
    
  • RE #46.4: I didn't know that issue, so I removed it from this patch.
  • RE #46.5: First, if there's only one value we make an array of it, so that case is also handled by this statement. Second, SQL allows the values in a set in a IN statement, or the values for the BETWEEN operator, to be scalar select queries (query returning a scalar value), this algorithm allows for that. So we do handle queries like (1st one from testConditionSubquerySelect()):
      SELECT tt2.name
      FROM test tt2
      WHERE tt2.pid IN (SELECT tt.pid AS pid FROM test_task tt WHERE tt.priority=1)
    

    and

      SELECT *
      FROM ...
      WHERE field BETWEEN (SELECT MIN(...) FROM ...) AND (SELECT MAX(...) FROM ...)
    

I will update the IS to reflect what becomes possible and then we can see if we want or need tests for all those situations.

fietserwin’s picture

Issue summary: View changes
daffie’s picture

Status: Needs review » Needs work

@fietserwin: Thank you for your explanations. Your two changes for the patch from comment #47 look good.

I you want to support multiple select queries at right hand side, then extra tests for that are needed.

fietserwin’s picture

Status: Needs review » Needs work

The last submitted patch, 50: subselects_don_t_work-1267508-50.patch, failed testing.

oriol_e9g’s picture

Minor: Asserts messsages doesn't need to use t().

fietserwin’s picture

daffie’s picture

Status: Needs work » Needs review

For the testbot.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

The problem as stated in the issue summary is fixed with this patch.
There are tests added to check the fix.
The patch looks good to me.
I am giving this a RTBC.

Good work fietserwin!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 53: subselects_don_t_work-1267508-53.patch, failed testing.

daffie’s picture

fietserwin’s picture

fietserwin’s picture

daffie’s picture

Status: Needs review » Reviewed & tested by the community

The test failures for PostgreSQL are not related to this patch.
The reroll looks good.
Back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record
  1. This looks like a nice improvement to the database API - I think we need a change record to tell people about it.
  2. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -301,15 +347,17 @@ protected function mapConditionOperator($operator) {
    -      'EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
    -      'NOT EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
    ...
    +      // Exists expects an already bracketed subquery as right hand part. Do
    +      // not define additional brackets.
    +      'EXISTS' => array(),
    +      'NOT EXISTS' => array(),
    

    I wonder what the impact for other db drivers this will have.

  3. +++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
    @@ -131,6 +149,9 @@ public function notExists(SelectInterface $select);
    +   * @return array
    +   *   The, possibly nested, list of all conditions (by reference).
    

    This looks like a pretty tricky API change.

  4. +++ b/core/tests/Drupal/KernelTests/Core/Database/SelectSubqueryTest.php
    @@ -93,6 +93,95 @@ function testConditionSubquerySelect() {
    +    $this->assertEqual(count($people), 2, 'Returned the correct number of rows.');
    

    There's assertCount()

  5. +++ b/core/tests/Drupal/KernelTests/Core/Database/SelectSubqueryTest.php
    @@ -93,6 +93,95 @@ function testConditionSubquerySelect() {
    +    $this->assertTrue(in_array('Paul', $people) && in_array('John', $people), 'Returned Paul and John.');
    

    There are special phpunit asserts for this ie. assertArraySubset()

  6. +++ b/core/tests/Drupal/KernelTests/Core/Database/SelectSubqueryTest.php
    @@ -93,6 +93,95 @@ function testConditionSubquerySelect() {
    +    $this->assertEqual(count($people), 1, 'Returned the correct number of rows.');
    +    $this->assertTrue(in_array('John', $people), 'Returned John.');
    ...
    +    $this->assertEqual(count($people), 2, 'Returned the correct number of rows.');
    +    $this->assertTrue(in_array('George', $people), 'Returned George.');
    +    $this->assertTrue(in_array('Paul', $people), 'Returned Paul.');
    

    As above

fietserwin’s picture

#61: Thanks for reviewing.

RE #61.1: Will try to set up something later today, but if someone else, a native English writer, want to give it a go, please do so, but add a comment that you are going to do so.

Re #61.2: With the patch of #43 we discovered that SQLite choked on to much brackets, therefore I had to make this change. We have positive test results for MySql, Postgres and SQLite. I guess that contrib provided drivers (Oracle and MSSQL server?) will not experience problems, resulting syntax is standard SQL. In fact, the method most touched by this patch (compile())is called hundreds or thousands of times per request, so that will be clear quite fast.

RE #61.3: No, this is only documenting what already existed. When I started working on this (back in 2011) I had a hard time figuring out what many of the methods returned, so I documented all those methods involved

RE #61.4/5/6: Changed. The tests were written in 2011 and only rerolled since. I also updated other usages of the deprecated assertEqual(). BTW: Is there an assert to check that arrays contain the same values (which is notably hard in PHP)?

Status: Needs review » Needs work

The last submitted patch, 62: subselects_don_t_work-1267508-62.patch, failed testing.

daffie’s picture

@fietserwin: One test fail to fix: testConditionSubquerySelect2 with "fail: [Other] Line 98 of core/tests/Drupal/KernelTests/Core/Database/SelectSubqueryTest.php:
0 => 'Paul'".

daffie’s picture

I shall write the change record.

daffie’s picture

Issue tags: -Needs change record

Change record added.

fietserwin’s picture

assertArraySubset() takes order (indices) into account. [My BTW turned out to be prophetic :).] However, it turns out that assertEquals has a $canonicalize parameter that sorts array before comparing them. This parameter turns this assert into a check on whether both arrays have the same values (with the same cardinality).

Interdiff is against #59

[@daffie: Thanks for writing the change record]

Status: Needs review » Needs work

The last submitted patch, 67: subselects_don_t_work-1267508-65.patch, failed testing.

daffie’s picture

Status: Needs work » Reviewed & tested by the community

The test failure for SQLite has noting to do with the patch for this issue.
All remarks of alexpott are addressed.
All changes look good.
Back to RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 67: subselects_don_t_work-1267508-65.patch, failed testing.

daffie’s picture

Status: Needs work » Reviewed & tested by the community

Back to RTBC.

alexpott’s picture

Issue tags: +beta deadline

Discussed @catch, @effulgentsia and @cottser we agree this needs to be in before the beta is cut on Tuesday. If this is not in before then it'll need to be for 8.3.x because whilst this probably won't impact other drivers - we can not be sure.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs subsystem maintainer review

It'd be great if a sub system maintainer could have a look at this. Afacis the solution is solid and works great and has test coverage.

Crell’s picture

First off, I *adore* how detailed the comments are in this patch. Well done!

The code overall looks fine as is, and if committed as is I wouldn't block it. However, I do have a few small nits:

  1. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -158,86 +163,127 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
    +            if ($condition['value'] instanceof SelectInterface && ($operator['operator'] === 'IN' || $operator['operator'] === 'NOT IN')) {
    

    Total not-blocking nitpick: I generally prefer to convert double-conditionals like this to an in_array(), as it's shorter and easier to read. (Vis, in_array($operator['operator'], ['IN', 'NOT IN']) )

  2. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -158,86 +163,127 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
    +          $value_fragment = array();
    

    May as well use short-array syntax while we're here.

  3. +++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
    @@ -32,33 +40,43 @@
    +   *   in itself. Use where(), if you would like to add a more complex condition
    

    No comma after where().

One larger, note, though, is that the compile() method is now quite long (even longer than it was before), and very deeply nested. That results in a very high cyclomatic complexity. Is there any way we can break it up into a series of sub-methods? At minimum, it looks like the main foreach()'s body could get split off to a sub-method. There may be others, or it may just to too complex to do, but it would be great if we could at least try.

If doing so is the difference between this patch being committed to 8.2 or not, though, I'm OK with committing it and tidying that up later.

The SQLite and Postgres fails are concerning, though. #69 indicates they may be spurious, so I defer to the committers on that front.

daffie’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs subsystem maintainer review

@Crell said:

If doing so is the difference between this patch being committed to 8.2 or not, though, I'm OK with committing it and tidying that up later.

Removing the tag "Needs subsystem maintainer review".

Created a followup #2775819: The method Drupal\Core\Database\Query\Condition::compile is too long and complex for:

One larger, note, though, is that the compile() method is now quite long (even longer than it was before), and very deeply nested. That results in a very high cyclomatic complexity. Is there any way we can break it up into a series of sub-methods? At minimum, it looks like the main foreach()'s body could get split off to a sub-method. There may be others, or it may just to too complex to do, but it would be great if we could at least try.

I think that the 3 minor points of comment #74 can be fixed on commit.

fietserwin’s picture

@crell: thanks for reviewing so quickly.

This patch basically stems from 2011 and has since "only been rerolled" to move with all the D8 changes. So, e.g. [] did not yet exists back then, also making in_array(...,array()) a little bit more awkward then nowadays. Comments (and doc added on a number of places) are for "compensating" the complexity and - for myself - to get a grip on what was going on and what could be expected (setting the expectations by writing correct preconditions).

1 to 3 can indeed be done on commit.

The fails do seem random (memory limit?) and with none of them I am able to track it back to a DB failure, while e.g.back in #43, the exception message and/or stack trace were clearly indicating that it was a failure due to the patch. This can be lack of logging the correct exception message at the origin of the failure or it has indeed not to do with this patch. Moreover,at this moment, the current dev branch without this patch also fails on d6\MigrateBookTest.

alexpott’s picture

Category: Bug report » Task
Status: Reviewed & tested by the community » Fixed

Thanks @Crell for the sub system maintainer review..

There's been some discussion of whether this is a bug or feature - let's split the difference and call it a task.

Committed 22a241c and pushed to 8.2.x. Thanks!

diff --git a/core/lib/Drupal/Core/Database/Query/Condition.php b/core/lib/Drupal/Core/Database/Query/Condition.php
index a6bb762..24a3582 100644
--- a/core/lib/Drupal/Core/Database/Query/Condition.php
+++ b/core/lib/Drupal/Core/Database/Query/Condition.php
@@ -255,7 +255,7 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
             $condition['value'] = array($condition['value']);
           }
           // Process all individual values.
-          $value_fragment = array();
+          $value_fragment = [];
           foreach ($condition['value'] as $value) {
             if ($value instanceof SelectInterface) {
               // Right hand part is a subquery. Compile, put brackets around it
diff --git a/core/lib/Drupal/Core/Database/Query/ConditionInterface.php b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
index c9b6333..2310e03 100644
--- a/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
+++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
@@ -42,7 +42,7 @@
    *
    * @param string|\Drupal\Core\Database\Query\ConditionInterface $field
    *   The name of the field to check. This can also be QueryConditionInterface
-   *   in itself. Use where(), if you would like to add a more complex condition
+   *   in itself. Use where() if you would like to add a more complex condition
    *   involving operators or functions, or an already compiled condition.
    * @param string|array|\Drupal\Core\Database\Query\SelectInterface|null $value
    *   The value to test the field against. In most cases, and depending on the

Fixed on commit. I can't do #74.1 on commit - it's a code change plus in_array() is a tricky beast - you (nearly) always want to make the 3rd argument TRUE so it doesn't produce unexpected results.

  • alexpott committed 22a241c on 8.2.x
    Issue #1267508 by fietserwin, lisa.ugray, daffie, chx, Crell: Subselects...
andypost’s picture

Looks this needs follow-ups - port to d7 and simplify code

One larger, note, though, is that the compile() method is now quite long (even longer than it was before), and very deeply nested. That results in a very high cyclomatic complexity. Is there any way we can break it up into a series of sub-methods? At minimum, it looks like the main foreach()'s body could get split off to a sub-method. There may be others, or it may just to too complex to do, but it would be great if we could at least try.

daffie’s picture

  • alexpott committed 22a241c on 8.3.x
    Issue #1267508 by fietserwin, lisa.ugray, daffie, chx, Crell: Subselects...

  • alexpott committed 22a241c on 8.3.x
    Issue #1267508 by fietserwin, lisa.ugray, daffie, chx, Crell: Subselects...
Berdir’s picture

It seems like this caused a test fail in TMGMT.

\Drupal\tmgmt_locale\Tests\LocaleSourceUiTest is failing.

We have a condition like this:

$select->isNull("lt_$missing_target_language.language");

$missing_target_language in this case is 'gsw-berne'. That used to be converted to gswberne, but no longer.

Maybe that's on purpose and can't be avoided, but I'm not sure?

We already have other places where we explicitly escape..

The code is in \Drupal\tmgmt_locale\LocaleSourcePluginUi

fietserwin’s picture

#83:
For now I can't see any link. This patch should not change existing already allowed queries, except that far less spaces and brackets are generated. So if someone is testing on literal query strings that will fail, but testing on query results should still work as before.

- Exactly which test is failing?
- How is it failing? Fatal error, uncaught (db) exception?
- Can you isolate the query that fails and more specifically the string version of it (e.g. dump all queries as string in execute()).
- Does it fail on a specific db or all dbs?

If this adapted compile() can generate where clauses with syntax errors I will immediately solve it.

Berdir’s picture

The failing test is in my comment, you can also see it here: https://www.drupal.org/pift-ci-job/413927, including the incorrectly generated query. I'm 100% sure that this issue changed that behavior, the only question is if that is a bug or by design. The test passes on the commit before this and fails with this.

Note that it only fails on the error_log parsing, on the terminal, the test is reported as passing.

fietserwin’s picture

@berdir: thanks for reporting. The committed patch indeed contains a small but highly critical omission. Please all review the follow-up issue #2783165: Follow up to "Subselects don't work in DBTNG conditions, except when used as value for IN".

Aside: @berdir: theoretically speaking, your code should use db_escape_table() instead of db_escape_field(). But as the standard implementation is the same it does not really matter, although on postgress they do differ.

Aside 2: The standard escapeField() implementation does not allow for alias.field to be passed as field part as it will be escaped to aliasfield. This seems an error to me, but one that should have been discovered ages ago, so I am not sure about this.

fietserwin’s picture

FYI: The query that fails and lead to @berdir's report gets (currently) compiled as follows:

SELECT COUNT(*) AS expression
FROM 
(SELECT 1 AS expression
FROM 
{locales_source} ls
LEFT OUTER JOIN {locales_target} lt_en ON ls.lid = lt_en.lid AND lt_en.language = 'en'
LEFT OUTER JOIN {locales_target} lt_de ON ls.lid = lt_de.lid AND lt_de.language = 'de'
LEFT OUTER JOIN {locales_target} lt_gswberne ON ls.lid = lt_gswberne.lid AND lt_gswberne.language = 'gsw-berne'
WHERE lt_gsw-berne.language IS NULL)

Also see:
- http://cgit.drupalcode.org/tmgmt/tree/sources/locale/src/Tests/LocaleSou...
- http://cgit.drupalcode.org/tmgmt/tree/sources/locale/src/LocaleSourcePlu...

Berdir’s picture

Thanks for the feedback. We already opened #2783123: Fix escape condition in \Drupal\tmgmt_locale\LocaleSourcePluginUi, we'll try to implement your feedback there. Reviews would be great :)

  • catch committed 1a29629 on 8.2.x
    Revert "Issue #1267508 by fietserwin, lisa.ugray, daffie, chx, Crell:...

  • catch committed a0096f1 on 8.3.x
    Revert "Issue #1267508 by fietserwin, lisa.ugray, daffie, chx, Crell:...
catch’s picture

Version: 8.2.x-dev » 8.3.x-dev
Status: Fixed » Needs work

I've reverted this due to #2783165: Follow up to "Subselects don't work in DBTNG conditions, except when used as value for IN" - that bug is worse than the limitation this fixes, and no need to have time pressure on fixing the security issue. Also bumping to 8.3.x

daffie’s picture

From #2783165: Follow up to "Subselects don't work in DBTNG conditions, except when used as value for IN":

The rewrite passed all existing and new tests on all supported databases and was thoroughly reviewed by the author and others. However 1 small change was not discovered: escaping plain fields, which was done in the old code but no longer in the new code.

The patch has added fix for the problem and also has added testing for it. The second patch has only the added testing.

fietserwin’s picture

- #3.1 from the other issue is not addressed. I thought that "provider function" from your comment had to do with addressing that comment, i.e. introducing some kind of "function provider", instead of using a data provider function.
- #74 (this issue) has not been addressed by this patch while #74.2. and #74.3 had been addressed on commit (I think).

Status: Needs review » Needs work

The last submitted patch, 94: 1267508-93-without-the-fix.patch, failed testing.

daffie’s picture

Status: Needs work » Needs review

The patch with the fix and the added test passes and the patch with only the added test fails.

Ready for a good review.

fietserwin’s picture

Status: Needs review » Needs work
Issue tags: -beta deadline
  1. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -40,6 +40,11 @@ class Condition implements ConditionInterface, \Countable {
    +   * @var string Contains the string version of the Condition.
    +   */
    

    Not according to the Drupal standard (In Drupal description comes before @var)

  2. +++ b/core/lib/Drupal/Core/Database/Query/Condition.php
    @@ -158,86 +163,127 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace
    +          if (stripos($condition['operator'], 'UNION') !== FALSE || strpbrk($condition['operator'], '[-\'"();') !== FALSE) {
    

    I never touched this statement and it is also probably out of scope for this issue but I think that comment starting characters should also be part of the list of dangerous characters: for Mysql:#-/ (for #, -- resp. /*). Minus is an operator in itself but is not a comparison or logical operator.

  3. +++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php
    @@ -32,33 +40,43 @@
    +   *   Other operators (e.g. LIke BINARY) may or may not work. Defaults to =.
    

    LIKE (instead of LIke)

Furthermore the release version in the change record should be updated.

I found some other minor points bu they are not part of this patch:
- 80 character limit violations on some comments.
- 1 untyped property ($queryPlaceholder)
- missing operators in the map: !=, <> (will work but according to the comment a small "performance hit").

daffie’s picture

@fietserwin: Thank you for the review!

For 97.1 and 97.3 Thanks and fixed!
For 97.2 That line is not being changed, but it is being moved. This is also happening in your own and previous commit patch from comment #67.

For:

I never touched this statement and it is also probably out of scope for this issue but I think that comment starting characters should also be part of the list of dangerous characters: for Mysql:#-/ (for #, -- resp. /*). Minus is an operator in itself but is not a comparison or logical operator.

and

I found some other minor points but they are not part of this patch:
- 80 character limit violations on some comments.
- 1 untyped property ($queryPlaceholder)
- missing operators in the map: !=, <> (will work but according to the comment a small "performance hit").

Please create follow-ups for those. I am happy to do the reviewing for them.

Status: Needs review » Needs work

The last submitted patch, 98: 1267508-98.patch, failed testing.

daffie’s picture

Status: Needs work » Needs review

Testbot is happy.

fietserwin’s picture

Status: Needs review » Reviewed & tested by the community

OK, good to go again. Can you change the version number in the change record?

daffie’s picture

@fietsenwin: Thanks and I updated the change record.

alexpott’s picture

I would nice to confirm that the updated fix does not cause the same issue for @Berdir's contrib module.

daffie’s picture

@alexpott: That is what I did in the comments #92 and #94.

fietserwin’s picture

  1. Checked out 8.3.x
  2. Installed and enabled module tmgmt
  3. Ran test Drupal\tmgmt_locale\Tests\LocaleSourceUiTest: "50 gelukt, 0 niet gelukt, 0 uitzonderingen, 14 debug-berichten". Ie: no failures.
  4. Applied patch
  5. Ran test Drupal\tmgmt_locale\Tests\LocaleSourceUiTest again: "50 gelukt, 0 niet gelukt, 0 uitzonderingen, 14 debug-berichten". Ie: no failures.

So the failing test in contrib has now explicitly been tested with this patch and doesn't fail anymore.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 98: 1267508-98.patch, failed testing.

daffie’s picture

Status: Needs work » Reviewed & tested by the community

Back to RTBC.

chx’s picture

19 fails in SQLite is OK?

daffie’s picture

@chx: The latest automatic testing without a patch has 16 fails. See https://www.drupal.org/pift-ci-job/435525.

daffie’s picture

I created an issue for the problems with the testbot for PHP 5.5 & SQLite. See #2791163: Random automatic testing failures on SQLite with PHP 5.5.

chx’s picture

Look, it's fine by me, as far as I am concerned @daffie is already the SQLite maintainer, I already encouraged them to make it official.

  • catch committed 9c63871 on 8.3.x
    Issue #1267508 by fietserwin, daffie, lisa.ugray, chx, alexpott, Berdir...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.3.x, thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.