Skip to content

whereDate() / whereTime() never match on the Stache when app.timezone is not UTC #15336

Description

@blerimbunjaku

Bug description

On the Stache/flat-file query builder, whereDate() and whereTime() never match anything when app.timezone is set to something other than UTC.

The comparison value is normalised into the app timezone in src/Query/Builder.php:

// whereDate()
$value = Carbon::instance($value)->setTimezone(config('app.timezone'))->startOfDay();

// whereTime()
$value = Carbon::instance($value)->setTimezone(config('app.timezone'))->format('H:i:s');

The indexed values, however, are augmented dates and therefore in UTC. Stache\Query\Builder::filterWhereDate() then does:

return $value->copy()->startOfDay()->$method($where['value']);

startOfDay() here operates in the timezone of the indexed value (UTC), while $where['value'] sits at midnight of the app timezone. Those are two different instants, and eq() compares absolute instants — so with any non-zero UTC offset the two sides can never line up. Since the indexed side always lands on 00:00 UTC and the comparison side never does, no date matches at all.

filterWhereTime() has the same shape: it takes the H:i:s string built in app timezone and applies it to the UTC value via setTimeFromTimeString(), comparing local wall-clock digits against a UTC instant.

How to reproduce

App timezone Europe/Zurich, a dated collection, one entry at 2026-03-12 00:00 local time (file 2026-03-12-0000.foo.md, augmented to 2026-03-11 23:00:00 UTC).

Entry::query()->whereDate('date', '2026-03-10')->count(); // 0
Entry::query()->whereDate('date', '2026-03-11')->count(); // 0
Entry::query()->whereDate('date', '2026-03-12')->count(); // 0  <- expected 1
Entry::query()->whereDate('date', '2026-03-13')->count(); // 0

Entry::query()->whereYear('date', 2026)->count(); // 108 — whereYear uses format(), unaffected

What the two sides actually hold:

whereDate value:   2026-03-12 00:00:00 CET  = 2026-03-11 23:00:00 UTC
index startOfDay:  2026-03-11 00:00:00 UTC
eq() -> false

Setting app.timezone back to UTC makes both sides agree and the query works again.

Knock-on effect: scheduled entries are never published

HandleEntrySchedule runs MinuteEntries, which is built on whereDate() + whereTime():

->whereDate('date', $this->minute->format('Y-m-d'))
->whereTime('date', '>=', $this->minute->format('H:i').':00')
->whereTime('date', '<=', $this->minute->format('H:i').':59')

With a non-UTC app timezone that query returns an empty collection every minute, so EntryScheduleReached never fires. On a site with static caching that means a scheduled entry stays invisible indefinitely — the listing keeps serving the cached version until someone runs static:clear by hand. The scheduler itself is running fine; it just never finds anything.

Logs

Environment

Environment
Laravel Version: 12.67.0
PHP Version: 8.4.17
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: Europe/Zurich
Locale: de

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: NOT CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: smtp
Queue: sync
Session: file

Statamic
Addons: 1
Sites: 2
Stache Watcher: Enabled (auto)
Static Caching: half
Version: 6.28.0 PRO

Installation

Existing site, upgraded over time

Additional details

  • Only affects the Stache/flat-file builder. The Eloquent path delegates to the database and is unaffected.
  • whereYear(), whereMonth() and whereDay() compare via format() and keep working, which makes this easy to miss.
  • Related: whereDate() / whereTime() ignore timezone of Carbon/DateTimeInterface values, shifting comparison instant #14939. That report was about passing a Carbon in a foreign timezone, and the normalisation onto config('app.timezone') that came out of it is exactly the line that now collides with the UTC-based index values. The environment there was Timezone: UTC, so this case wouldn't have shown up.
  • Passing a plain string works if it reaches eq() as a string (Carbon then parses it in the receiver's timezone), which is why the mismatch is invisible when testing the comparison in isolation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions