When indexing datetime field, the code gets the value of field without timezone (which is storage timezone, but gets ignored) and then it converts it to timestamp, by using strtotime(), which then uses default timezone of system.
Most easiest way to reproduce this is:
- Outside UTC timezone (user with other timezone should be enough)
- Create entity with datetime field, enter datetime (which in form will be in users timezone)
- Index that entity
- Create view that lists entities of given type
- Add filter for the (indexed) field where time should be <= 'now' so the field represents publishing date
- Modify the field to check if the filter would work correctly, you should be having offset without the fix.
Now to go a bit further from that, here's the idea made with code:
$dst = defined('DATETIME_STORAGE_TIMEZONE') ? DATETIME_STORAGE_TIMEZONE : 'UTC';
$datetime = '2018-02-18T22:30:00';
$datetime2 = $datetime . '+0200';
$datetime3 = $datetime . '+0300';
echo "Default timezone: " . date_default_timezone_get() . PHP_EOL;
echo "With strtotime:" . PHP_EOL;
echo strtotime($datetime3) . PHP_EOL;
echo strtotime($datetime2) . PHP_EOL;
echo strtotime($datetime);
echo " <- previous and this is same." . PHP_EOL;
echo "With Datetime without timezone:" . PHP_EOL;
$d = new Datetime($datetime3);
echo $d->getTimestamp() . " " . $d->getTimezone()->getName() . PHP_EOL;
$d = new Datetime($datetime2);
echo $d->getTimestamp() . " " . $d->getTimezone()->getName() . PHP_EOL;
$d = new Datetime($datetime);
echo $d->getTimestamp() . " " . $d->getTimezone()->getName();
echo " <- Timestamp is correct only if timezone is noted." . PHP_EOL;
echo "With Datetime with timezone:" . PHP_EOL;
$timezone = new DateTimeZone($dst);
$d = new Datetime($datetime3, $timezone);
echo $d->getTimestamp() . " " . $d->getTimezone()->getName() . PHP_EOL;
$d = new Datetime($datetime2, $timezone);
echo $d->getTimestamp() . " " . $d->getTimezone()->getName() . PHP_EOL;
$d = new Datetime($datetime, $timezone);
echo $d->getTimestamp() . " " . $d->getTimezone()->getName();
echo " <- Timestamp is correct always." . PHP_EOL;
Example output would be:
Europe/Helsinki
With strtotime:
1518982200
1518985800
1518985800 <- previous and this is same.
With Datetime without timezone:
1518982200 +03:00
1518985800 +02:00
1518985800 Europe/Helsinki <- Timestamp is correct only if timezone is noted.
With Datetime with timezone:
1518982200 +03:00
1518985800 +02:00
1518993000 UTC <- Timestamp is correct always.
| Comment | File | Size | Author |
|---|---|---|---|
| #18 | 2945870-18-db_backend_date_indexing-interdiff-14.txt | 2.36 KB | dropa |
| #18 | 2945870-18-db_backend_date_indexing.patch | 4.32 KB | dropa |
Comments
Comment #2
dropa commentedAnd here would be proposed solution.
Comment #3
dropa commentedComment #4
drunken monkeyThanks a lot for reporting this problem and providing such a detailed analysis!
Let me first state that timezones are aweful. I think we can all agree on that.
Then, as it is often the case with timezones, I'm not sure I completely understand the problem.
First, it seems this would only concern cases where you want to index a date string as a date, right? I.e., the usual use case of indexing a timestamp (e.g., "Authored on") works correctly regardless?
Then it would seem your solution only works if a time zone is explicitly given in the string, right? If there is no timezone information in the date string, I'd expect using the system's default timezone would be the expected behavior, but (unless I'm mistaken) with your patch this would be parsed using UTC instead, arriving at the wrong timestamp after all. (A use case which currently works – again, unless I'm mistaken.)
Finally, the question would be whether the timestamp we filter by is actually always the correct one. However, I guess that is indeed the case, so not an issue here.
Also, regardless of all other concerns, it would seem to me like this conversion should actually happen in
\Drupal\search_api\Plugin\search_api\data_type\DateDataType::getValue()(to be added), not in the backend plugin. Not sure why we didn't change that when overhauling the data type system for D8, probably just an oversight.Comment #5
dropa commentedSo the problem is indeed with datetime typed fields.
Here I have this entity, which has Date field
publish_dateNow, it's settings will look like this:

Then I'll index the given field:

Then let's see the database.
Entity create & change times are in timestamp form,

But the Publish date actually is in string form

Now lets see what index looks like
Changed date looks identical:

But now here's the trick, publish date has been converted to timestamp:

Now the thing here is that like in my example code, the Date field uses default storage timezone, which it tells to be UTC, but to actually use the same constant in SAPI would require dependency for the module that provides the field.
For example
\Drupal\Component\Datetime\DateTimePlus::createFromTimestampstates following:So that's why my patch forces UTC in the backend.
The problem that exists in current version is that if system timezone is different than UTC, following happens:
- user enters time (in his/her timezone)
- datetime gets stored in the backend in UTC timezone
- index fetches data from database
- passes data to strtotime()
- strtotime() thinks we got string in system timezone
- now we indexed value that's as much different from the original as the system timezone differs from UTC
Note that whenever datetime is shown to user, it's prepared first with the default timezone of Drupal or the timezone of user itself.
Also note that when I'm referring to system, I'm talking about the machine where Drupal runs.
Comment #6
dropa commentedYet to mention that current code is this:
And documentation of strtotime quite explains it shorter than I do:
Comment #7
drunken monkeyWow, thanks again for the detailed explanation! I think I now got it, and you're right, your solution makes perfect sense.
I guess this would fail if a date string in the DB were without timezone information but not in UTC – but that seems less likely than the problem we have currently. While further feedback from others would be great, I think we should go ahead with this.
However, as mentioned in #4, in my opinion all of this logic belongs into the data type plugin, not the backend plugin. So, moving it there, along with your improvements. I'm also adding test coverage, both Unit tests for the new method and a Kernel test covering the whole date indexing logic in the DB backend. Would be great if you could review those to see whether they make sense. (They do successfully pass and fail when they should, though, so doesn't seem too far off.)
Comment #10
dropa commentedIn my eyes, if any date is stored in database in other timezone than UTC, then it would be developer level mistake. Any user input should be converted anyway.
When date is stored in other timezone than UTC, it's not easy to create filter where date is limited by time (say expiration date or publishing date, so the view would be cutting results from either way from current time), and this is where I met this issue. When stored correctly in UTC, no matter where user, backend or database is located, filtering view by current time will give you correct results.
I tested your patch with Views, as well the Unit test, and can confirm them working. I was unable to run the Kernel test, but didn't find anything suspicious there (phpcs did with DrupalPractice standard though, guess it does not support list()).
All in all, seems good to me.
Comment #11
drunken monkeyHm, OK, that's unfortunate – I can't reproduce this test fail locally, but the test bot seems to insist.
Trying a debug patch to maybe get to the bottom of this.
Comment #12
drunken monkeyComment #14
drunken monkeyHuh.
I have to admit, I'm at a bit of a loss here. Can anyone reproduce the fail locally?
@ Dropa: Why were you "unable to run the Kernel test"? What did you try, what happened?
Comment #15
drunken monkeyComment #16
dropa commentedLooking at the error for example in here: https://www.drupal.org/pift-ci-job/909915 as it says
Table 'jenkins_drupal8_contrib_patches_24770.search_api_db_database_search_index_1' doesn't exist:and my own test says
In my eyes that actually means the simpletest is trying to locate the table from the actual (real and live) database.
That would sound a bit dangerous thought, but I'm looking whether that is the case.
I think you could check whether you accidentally have search index names "database_search_index_1" in your system that you're running tests on.
Comment #17
dropa commentedEdit: Wrong analysis removed :)
Comment #18
dropa commentedNow that I took a look on how other tests had made queries to database, they have surrounded table with {} marks, so apparently it is meant to be executed in real database.
Added curlys to table, removed debugging lines and changed
$exceptedto$expectedComment #19
dropa commentedComment #21
drunken monkeyAwesome, thanks for debugging that! You're right, silly mistake on my part – good that you found it.
You forgot to include
DateDataTypeTestagain in your patch, but other than that it looks perfect now.Committed.
Thanks a lot again for your contribution!
Comment #23
bburgAny thoughts on a new release? I believe I am running into this on 8.x-1.9.Disregard. This should be fixed in 1.9, just need to re-index.
Comment #24
bburgOk,
Follow up from above. I am still getting the wrong dates. It seems that Search API is saving the localized timestamps, and then later pulling them out of the database as UTC. Should the hard-coded 'UTC' in DateDataType
Instead be?
Even with this modification, I'm getting incorrect times. It seems stored in the index as the correct UTC value, but being pulled and rendered as the local time, but re-interpreted as UTC.
Edit: Disregard all this, I think my logged in user was on UTC, but the site used local time.