Skip to content

Normalize absolute date-time values in filters#166

Draft
9larsons wants to merge 2 commits into
mainfrom
slars/normalize-absolute-dates
Draft

Normalize absolute date-time values in filters#166
9larsons wants to merge 2 commits into
mainfrom
slars/normalize-absolute-dates

Conversation

@9larsons

@9larsons 9larsons commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Filtering by an absolute date returns the wrong rows on SQLite. Using filter="published_at:>'...'" in a theme's {{#get}}, or the same filter via the Content/Admin API, includes posts from the wrong side of the boundary. Fixes TryGhost/Ghost#23441.

Root cause

Dates are stored as YYYY-MM-DD HH:MM:SS (UTC). Relative dates (now-30d) are already normalized to that format at parse time (scope.js: relDateToAbsoluteformatDateForSQL), but absolute ISO values from a filter (e.g. published_at:>'2025-02-27T19:03:00.000-05:00') were passed through untouched:

  • On SQLite, datetimes are stored as text and compared lexically, so the T sorts after the stored space separator (T > ) and the comparison returns the wrong rows.
  • On MySQL, the timezone offset was silently dropped instead of applied.

Fix

Normalize absolute date-time values to the stored YYYY-MM-DD HH:mm:ss UTC format in the VALUE grammar rule — right next to the existing relative-date handling — so the value is canonicalized once at parse time and SQLite and MySQL receive identical input. No per-dialect logic.

Only full ISO-8601 date-times (a date with a time component, optionally with fractional seconds and a Z/±HH:MM zone) are rewritten. Bare dates (2025-02-27), plain strings, and non-date values are left untouched, because nql-lang has no column-type information and must not corrupt a legitimate non-date value (e.g. a date-like slug). A zone-less date-time is interpreted as UTC (dates are stored in UTC); anything that fails to parse is returned unchanged.

Notes for reviewers

  • This supersedes TryGhost/Ghost#28952, which fixed the same bug in a Ghost-core Bookshelf plugin applied to every model on both databases. Fixing it here keeps date normalization in one place (alongside relative dates), makes SQLite/MySQL behave identically without dialect branching, and benefits every NQL consumer rather than just Bookshelf models.
  • Bare dates are intentionally left alone. MySQL coerces '2025-02-27' to 2025-02-27 00:00:00 while SQLite compares it as text, so there is a pre-existing behavior difference for date-only boundaries. Normalizing bare dates universally would risk rewriting legitimate non-date string filters, so it's deliberately out of scope here — happy to discuss if we'd rather align that too.
  • The generated parser (dist/parser.js) is checked in and rebuilt via yarn build.

Tests

Added unit tests for normalizeAbsoluteDate (offset/Zulu/no-seconds/fractional/zone-less/idempotent/bare-date/non-date/unparseable/non-string) and parser-level tests covering >/</equality/$in/logical-group normalization plus the bare-date and non-date pass-through. Full suite passes and lint is clean.

Relative dates (e.g. `now-30d`) are already normalized to the stored
"YYYY-MM-DD HH:mm:ss" UTC format at parse time, but absolute ISO values
from a filter (e.g. `published_at:>'2025-02-27T19:03:00.000-05:00'`) were
passed through untouched. On SQLite datetimes are stored as text and
compared lexically, so the "T" sorts after the stored space separator and
the comparison returns the wrong rows; on MySQL the timezone offset was
dropped rather than applied.

Normalize absolute date-time values to the same stored format in the
`VALUE` grammar rule, next to the existing relative-date handling, so date
comparisons behave identically across SQLite and MySQL. Only full ISO-8601
date-times (a date with a time component) are rewritten — bare dates and
other plain strings are left untouched, since nql-lang has no column-type
information and must not corrupt a legitimate non-date value.

Fixes TryGhost/Ghost#23441
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 01966acc-90fc-4867-b331-7b0e4e10ef13

📥 Commits

Reviewing files that changed from the base of the PR and between 5e992f4 and 8ae6de1.

📒 Files selected for processing (3)
  • packages/nql-lang/lib/scope.js
  • packages/nql-lang/test/parser.test.js
  • packages/nql-lang/test/scope.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/nql-lang/test/parser.test.js
  • packages/nql-lang/test/scope.test.js
  • packages/nql-lang/lib/scope.js

Walkthrough

This PR adds absolute date-time normalization to nql-lang. A new normalizeAbsoluteDate helper in scope.js detects ISO-8601 date-time strings with a T separator, normalizes valid values to UTC SQL format, and leaves non-matching or invalid inputs unchanged. The nql.y grammar now applies this normalization to LITERAL and STRING values. Tests were added and updated in scope.test.js and parser.test.js.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant nql_y as nql.y VALUE rule
  participant scope_js as scope.normalizeAbsoluteDate
  nql_y->>scope_js: LITERAL/STRING value
  scope_js-->>nql_y: normalized UTC date or original value
Loading

Related issues: #23441

Suggested labels: bug, nql-lang

Suggested reviewers: kevinansfield, daniellockyer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: normalizing absolute date-time filter values.
Description check ✅ Passed The description is directly about fixing absolute date filtering and matches the code changes.
Linked Issues check ✅ Passed The grammar and normalization changes address the SQLite/MySQL date-boundary bug in [#23441].
Out of Scope Changes check ✅ Passed The changed files stay within date normalization, parser updates, and test coverage for the stated fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slars/normalize-absolute-dates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.18182% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.53%. Comparing base (d9285af) to head (8ae6de1).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
packages/nql-lang/lib/scope.js 98.11% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #166      +/-   ##
==========================================
+ Coverage   84.18%   84.53%   +0.34%     
==========================================
  Files           9        9              
  Lines        2074     2127      +53     
  Branches      428      437       +9     
==========================================
+ Hits         1746     1798      +52     
  Misses        322      322              
- Partials        6        7       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Reject calendar-invalid dates (e.g. Feb 30) instead of letting new Date
  roll them over to a different day
- Enforce hour/minute/second ranges in the ISO regex so out-of-range times
  (T24:00, 19:60) pass through instead of rolling over
- Require the T separator: space-separated values are either already in
  the stored format or arbitrary text, and only the ISO T form exhibits
  the comparison bug being fixed
- Skip normalization when preserveRelativeDates is set, so the lossless
  parse mode also preserves absolute date-times
@9larsons 9larsons marked this pull request as draft July 3, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot get posts from the same date (SQLite3-specific?)

2 participants