Problem/Motivation
To implement a complex join using the IN operator in a hook__views_query_alter implementation, I created a join for the view using code similar to this:
//Set up the join
$configuration = [
'table' => 'node__field_expert_subjects',
'field' => 'entity_id',
'left_field' => 'nid',
'operator' => '=',
'extra' => [
0 => [
'field' => 'field_expert_subjects_target_id',
'value' => $tids,
]
],
];
$join = Views::pluginManager('join')->createInstance('standard', $configuration);
$query->addRelationship('subject_expert_link', $join, 'node_field_data');
$query->addWhereExpression(0, 'subject_expert_link.entity_id IS NOT NULL');
where $tids is an array of taxonomy TIDs.
The view, when executed, generates invalid SQL, and I get a fatal error in core/lib/Drupal/Core/Database/Connection.php, in expandArguments().
It appears that in core/modules/views/src/Plugin/views/join/JoinPluginBase.php, the code to render out the SQL in this case was never changed to conform with the fix for SA-CORE-2014-005 (#2358079: Require a specific placeholder format in db_query() in order to trigger argument expansion, and require explicit 'IN' parameter for conditions), and the test coverage did not cover this particular corner case.
Proposed resolution
- Patch JoinPluginBase.php to use the new style of parameters for arrays.
- Add or modify the Views tests to hit this use of 'extra'
Remaining tasks
User interface changes
None.
API changes
None; this just makes the current API work.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #39 | interdiff-38-37.txt | 2.09 KB | kristiaanvandeneynde |
| #39 | issue-2658438-bad-join-sql-38.patch | 6.03 KB | kristiaanvandeneynde |
Comments
Comment #2
Torenware commentedInitial patch so I can check this against the test bot. No test yet.
Comment #3
Torenware commentedComment #5
Torenware commentedLooking at the test failure, it turns out that everything passes except
\Drupal\views\Tests\Plugin\JoinTest::testBasePlugin(). These are:My patch does two things. First, it adds parenthesis around argument values, to make sure that a JOIN made with the IN operator will be valid SQL. The change is
@@ -333,7 +330,7 @@ public function buildJoin($select_query, $table, $view_query) { $join_table_field = "$left[alias].$info[left_field]"; $arguments[$placeholder] = $info['value']; } - $extras[] = "$join_table_field $operator $placeholder"; + $extras[] = "$join_table_field $operator ($placeholder)"; }This is harmless if the operator is not expecting a list (at least, AFAIK), but essential if it is, as it is for IN and NOT IN.
All of the seven FAILS only test the format of the generated SQL (the tests don't attempt to execute it). All of my FAILS except #4 are because the string match objects to unneeded (if harmless) parenthesis.
#4, however, generates SQL that violates the assumptions of the SA-CORE-2014-005 fix. #4 will cause a fatal exception if our PDO wrappers ever try to execute it. The second part of the patch changes the parameters used for array valued "right sides" to a format with the added
[]at the end, in place of multiple parameters, which will now crash the Database API.I can make the patch a bit more specific if needed, if I need to pass the 6 extra fails w/o changing the test. But line 180 must change, since the SQL we're testing for will not execute:
since the 3 parameters need to change to something like
:views_join_condition_3[].Comment #6
Torenware commentedOnly add parenthesis for the array-valued case, and change the one assert that must change for validity.
No interdiff, since this is pretty small.
Comment #7
Torenware commentedTagging the issue for 2016 sprint weekend, since this should be low hanging fruit for somebody.
Comment #8
lokapujyaextra whitespace.
Also, can you please post a "test only" patch so that we can see the failure (with the new assert) for review purposes?
Comment #9
Torenware commented@lokapujya -- hm, thought I got that bit of extra white space. I'll re-roll with it gone.
As for a test-only patch: what do you need? Currently, the test in
core/modules/views/src/Tests/Plugin/JoinTest.phponly tests if the generated SQL fragments are as the test expects. If I remove my modification of the test, Fail #4 from the table in Comment #5 will fail, but everything else will pass.Right now, there are no tests related the Join plugins that actually test if the SQL will execute. If you want to actually see the crash, either I'd need to add code to one of the test modules that generates a query of the right type (using code like the snippet from Problem/Motivation section) and then do a SimpleTest, or I'd need to figure out how to do a unit test that is set up to execute a view query in isolation. is this what you're asking for?
Comment #10
Torenware commentedFrom IRC today:
The new patch should resolve the whitespace issue. So this should be ready-to-go per lokapujya.
Comment #12
kristiaanvandeneyndeReviewed and tested this as part of the patch in #2706495-16: add Views reverse relationships from entities to Group Content.
Looks good, does what it promises and has green tests. RTBC.
Comment #13
alexpotterror: core/modules/views/src/Tests/Plugin/JoinTest.php: does not exist in index
Looks like the test has been moved.
Comment #14
kristiaanvandeneyndeStraight reroll, so if tests go green this goes back to RTBC.
The test moved into a Kernel subfolder is all.
Comment #15
kristiaanvandeneyndeThe tests were moved as part of #2553655: Convert ViewKernelTestBase to use KernelTestBaseTNG
Comment #16
kristiaanvandeneyndeGreen so back to RTBC.
Comment #17
dawehnerThis looks great for me!
Comment #18
alexpottI think we can do away with the
$local_argumentscompletely. Ie I think we can just do$arguments[$placeholder] = $info['value'];.Space at the end of the line.
Comment #19
dawehnerI think so,
$placeholderis unique.Comment #20
kristiaanvandeneyndeBoth good points. Looking at the code it is indeed now possible to set the value directly.
Comment #21
dawehnerNice!
Comment #22
alexpottIs there any reason we don't do as before and add the parenthesis into the placeholder? I can't see one and this would mean there is less logic in the method. As the whole
if ($use_parenthesis) {change would be unnecessary.Sorry I should have asked this in earlier reviews.
Comment #23
kristiaanvandeneyndeNah, good point. I left the comment in there, as it doesn't hurt to have some in such a bloated function :)
Comment #24
outermedia-dku commentedIf you prefer the old IN(...) SQL syntax, there is probably a very short fix for the problem.
Comment #25
outermedia-dku commentedComment #26
outermedia-dku commentedIf you prefer the IN(...) SQL syntax, there is probably a very short fix for the problem.
Comment #27
kristiaanvandeneyndeI'm not sure I know what you're getting at dku. The issue is about the joins not working at all because the plugin never incorporated the logic from the security advisory and the latest patch fixes that.
Comment #28
dawehner@dku
This really sounds like a different problem, maybe reporting this in its own issue would be great!
Nice find @alexpott, this simplifies the code a bit!
Comment #29
kristiaanvandeneyndeActually, the parenthesis change lead to this patch breaking when used because the arguments array now has a key containing parentheses whereas Drupal looks for the placeholder without parentheses.
Reverting to the old patch from #20 and leaving as RTBC again because that one was properly reviewed and worked in #2706495: add Views reverse relationships from entities to Group Content, whereas the latest one doesn't.
Comment #30
kristiaanvandeneyndeActually, back to needs work :(
It throws notices in this part of
views_query_views_alter():$value is an array so invalid for use as an array key in the isset() call. Any suggestions on how to address that one? It's the final piece of this puzzle for it to be completed :)
Comment #31
Torenware commentedviews_query_views_alter()is making some assumptions as to what can legally be in $substitutions. Looking through sources a bit, $substitutions gets its value fromwhich is in
Drupal\views\Plugin\views\query\Sql, near the very end. So we have two possibilities here:views_query_views_alter()is DOING IT WRONG, orhook_views_query_substitutions()is DOING IT WRONG.So, do you know what the stack trace is from where you're getting the notice? This may tell us that we need to sanity check the input in the former, or that we need to fix the hook implementation in the latter.
Comment #32
Torenware commentedSomebody is violating the "contract" for our hook. Look at the definition:
So it's the responsibility of the caller to return a string for each of the key values.
I"m going to see if I can track down who's violating the contract. But we probably need to do two things here: check our input in views_query_views_alter() (either throwing something or writing to the log if we see something that will not cast to string), and find the caller that's doing this here.
Comment #33
Torenware commentedHere's where we need to look, I'd guess:
./node/node.views_execution.inc:13: * Implements hook_views_query_substitutions(). ./node/node.views_execution.inc:15:function node_views_query_substitutions(ViewExecutable $view) { ./user/user.views_execution.inc:11: * Implements hook_views_query_substitutions(). ./user/user.views_execution.inc:15:function user_views_query_substitutions(ViewExecutable $view) { ./views/tests/modules/views_test_data/views_test_data.views_execution.inc:13: * Implements hook_views_query_substitutions(). ./views/tests/modules/views_test_data/views_test_data.views_execution.inc:15:function views_test_data_views_query_substitutions(ViewExecutable $view) { ./views/tests/modules/views_test_data/views_test_data.views_execution.inc:16: \Drupal::state()->set('views_hook_test_views_query_substitutions', TRUE); ./views/views.module:653: * add in substitutions from hook_views_query_substitutions(). ./views/views.views_execution.inc:12: * Implements hook_views_query_substitutions(). ./views/views.views_execution.inc:20:function views_views_query_substitutions(ViewExecutable $view) {Comment #34
kristiaanvandeneyndeHow about a much simpler approach?
We refactor
views_query_views_alter()to account for $value potentially being an array and then loop over that. That way, a join condition ofWHERE foo IN ( :bar[] )with:barexpanding to***CURRENT_TIME***and***CURRENT_VERSION***would also work as expected.Something like this:
The funny part is: This is broken for all of Views, not just the functionality in this patch. Looking at the original code, there is no way an IN-query of any kind would have ever properly replaced its substitution keys with the actual values. So perhaps we should ignore it here and file a separate issue for it?
Comment #35
kristiaanvandeneyndeSo, as per #29 we can go back to RTBC for the patch in #20. As per #34, a follow-up should be created for
views_query_views_alter()because it (should throw/throws) notices for more than just this issue.Comment #36
alexpottThis could still be written as
Which would rid us of the conditional and
$use_parenthesis.Comment #37
dawehnerThat sounds sensible
Comment #38
kristiaanvandeneyndeSo the
$placeholdervariable was both used for the array key in$argumentsand the SQL string all the way at the end. Because there was some extra code later on, we can't simply encapsulate$placeholderin parentheses because we run the risk of the arguments array having the wrong key again.The attached patch solves this by introducing a second
$placeholder_sqlvariable which is only used in the generated SQL, which means we can safely keep using$placeholderwithout parentheses. I also added a test which checks for the arguments key to be a correct placeholder.Edit: will post a patch saying $placeholder_sql instead of $place_holder_sql after this one goes green.
Comment #39
kristiaanvandeneyndeTests are currently failing on 8.1.x due to a B/C break in #2735045: Convert StandardTest to BrowserTestBase by introducing a AssertLegacyTrait and new assertions on the WebAssert so flagging this issue as 8.1.2 and uploading the patch from #38 without the typo and with a slightly cleaner variable assignment.
Also created a spin-off for the warnings in the logs at #2744069: views_query_views_alter() does not handle IN queries
Comment #41
kristiaanvandeneyndeJoinTest actually passed in that test, so the tests we want to go green went green.
Comment #42
dawehnerThis looks perfect for me now. The changes are fairly minimal, we have some test.
Comment #43
alexpottCommitted 4bab1f7 and pushed to 8.1.x and 8.2.x. Thanks!