Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem
- When operating with complex select/update/delete query conditions, you need to write relatively complex PHP code:
$delete = $this->connection->delete($this->tableName); $or = $delete->orConditionGroup(); $or->condition('provider', $options['provider']); if ($names) { $or->condition('name', $names, 'IN'); } $delete->condition($or); $delete->execute();
When you don't, an often unhelpful MySQL exception is thrown, where you need to search for what went wrong
Goal
Three options
- Skip the condition, execute the query anyway
- Instead add an always FALSE (1 = 2) condition, execute the query anyway
- Throw a better exception
As discussed, 1. is not an option, as it could silently delete or update too many rows, or return something unexpected. 2. Might also be unexpected and could make finding out what happens hard as you'd need to debug the finally executed query to get the idea.
So, it was decided that 3 results in best DX and doesn't contain any risks.
Comment | File | Size | Author |
---|---|---|---|
#17 | interdiff.txt | 602 bytes | jibran |
#17 | database-empty-condition-2161943-17.patch | 2.84 KB | jibran |
#14 | database-empty-condition-2161943-14-interdiff.txt | 1011 bytes | Berdir |
#14 | database-empty-condition-2161943-14.patch | 2.84 KB | Berdir |
Comments
Comment #1
Damien Tournoud CreditAttribution: Damien Tournoud commentedThat doesn't make much sense for me. If you do an empty IN, it should be unconditionally false.
Comment #2
Crell CreditAttribution: Crell commentedWell as is I believe it's an SQL syntax error. I agree we should do better than that. The question is what we should do instead:
1) Skip it, as #0 does.
2) Force it to a WHERE 1=2 (or other such condition that is unconditionally false)
3) Throw an exception, since code upstream screwed up. (That's what we do for mixing values and default_values in insert queries, for instance.)
Comment #3
sun.3) Throwing an exception would have the same result as now, as the query will fail with an SQL syntax error:
WHERE name IN ()
— An exception would just be more explicit, but not really necessary.The original concern and idea here is that having to manually compose a conditional query like this in PHP reminded me a lot of this:
So I wondered whether it is really necessary with an abstracted query builder/condition engine, or whether that couldn't be a bit smarter.
I guess it boils down to the question of (1) how explicit query builder usage has to be and (2) what the potential consequences of smartly/silently ignoring a condition might be.
Perhaps, if a developer forgot to pollute the passed in $values, then the query would silently succeed whereas it should have not.
OTOH, that's what tests are for?
I'm not sure. Thoughts?
If we don't think that this is a good idea, then we can close this as won't fix.
Comment #4
Crell CreditAttribution: Crell commentedI agree with doing something about it, ie not won't fix. Which something we should do, though, I'm not sold either direction yet.
Thinking aloud, silently ignoring an empty IN clause, if you didn't realize the variable was empty, could result in data loss, no? That is, if you had the equivalent of this query:
DELETE FROM {mytable} WHERE a=:a and b IN (:b)
And :b is unexpectedly empty, then the query would delete likely more than intended. If :b had one value, it would delete a small number of items. If :b is empty, the IN clause is dropped, and it would delete a huge number of things.
That seems very bad. :-)
So I think that rules out "ignore empty". That leaves replacing IN () with an always-false statement, or raising a more useful error. (MySQL at least tends to not have the best error messages; providing better errors via sane and documented exceptions where feasible is, IMO, better than letting MySQL handle it.)
Comment #5
sunI agree, silently ignoring conditions could impose problems.
I guess we'd simply throw an InvalidArgumentException then?
Comment #6
Crell CreditAttribution: Crell commentedI believe this formats to "Query condition 'provider IN ()' cannot be empty." That seems fine.
Most of the DB layer, I believe, is subclassing base exceptions and throwing descriptive exceptions. So this is probably something like InvalidQueryException extends \InvalidArgumentException implements DatabaseException. That way, all database exceptions can still be caught by catching DatabaseException.
Comment #7
BerdirWas thinking the same as @Crell when I read the issue title and summary, so updated to what we are actually doing.
Added the exception class and tests.
Comment #8
Crell CreditAttribution: Crell commentedI like the new title, too. Thanks, Berdir.
Comment #9
alexpottComment #10
alexpottThis test should fail if the expected exception is not thrown.
Comment #11
BerdirThat already happens, as an exception is thrown if that query is executed.
We can add a $this->fail() below, but we'll never reach it, even if the expected exception is not thrown (aka in a test-only patch)
Comment #12
alexpott@Berdir I saw that... but say someone changes the code in Condition to not throw an exception and do option 2 from the issue summary - this test won't fail and will be probably hang about confusing people. This is why PHPUnit's expected exception annotation is so great :)
Comment #13
Crell CreditAttribution: Crell commentedIf we were using PHPUnit for these tests, then yes we should just do that. We're not, however, and that test refactor is out of scope here.
That said, I think Alex is correct that we need to test the negative case, too. Probably just set a variable inside the catch, and then pass/fail based on whether the variable is set to "yep, an exception happened".
And while we're at it, let's split that test method into 2. It shouldn't be one method. That's a Drupal-is-dumb pattern. :-)
Comment #14
BerdirWe can just call this->fail().
And I don't agree with multiple methods, I'm testing the exact same code base there, and even though it's a DUBT test now, test methods still have quite some overhead. PHPUnit @expectedException would require that, but as you said yourself, we're not going there :) I'm just making sure that the exception message is dynamic and contains the condition, having this in the same test is imho more self-explaining.
Comment #15
Crell CreditAttribution: Crell commentedEh, or that I guess. These mega tests just make me cry every time I see them.
Comment #16
catchVery minor micro-optimization but can we reverse the conditions here?
Comment #17
jibranFixed. Back to RTBC.
Comment #18
catchCommitted/pushed to 8.x, thanks!