Skip to content

fix(datagrid): read every database date spelling with one grammar - #2249

Merged
datlechin merged 2 commits into
mainfrom
fix/2241-one-date-grammar
Aug 19, 2026
Merged

fix(datagrid): read every database date spelling with one grammar#2249
datlechin merged 2 commits into
mainfrom
fix/2241-one-date-grammar

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2241.

TablePro had two grammars for the same job, recovering a value from the date text a driver puts on the wire. DatabaseDateParser held nine ordered DateFormatter patterns for the grid and the chart's time axis; DateEditingService held a regex for the cell editor. Nothing forced them to agree, and they had already drifted.

This gives the grammar to one type. DatabaseDateParser now owns the regex, TemporalLayout and ParsedTemporalValue, and answers both questions: date(from:) for display and the chart, parse(_:) for the editor. DateEditingService keeps only the write side.

Two live bugs the split was hiding

An empty date cell opened the picker at UTC. With no text to read a zone from, the picker fell back to .gmt and defaultString wrote in .gmt, so the value written was the user's clock shifted by their offset. Measured at UTC+7: picking "now" at 06:00 local opened the picker at 23:00 and wrote 2024-03-14 into a DATE column, the previous day. The offset is wrong at every hour; the day is wrong for the hours after local midnight. Both now resolve in the user's own zone.

A time carrying an offset rendered as raw text. 09:30:00+07, which PostgreSQL time with time zone produces, matched the editor's regex but none of the nine formatter patterns. So the cell was editable as a date but never formatted as one. One grammar closes that by construction.

The naive-value zone is settled deliberately, per the issue. Both readers now use TimeZone.current: display already formats in it (DateFormattingService), the chart axis renders in it, and the picker is handed the zone explicitly, so a naive value shows and writes as written.

Two regressions caught while building this, worth reading

I would have shipped both without measuring, so they are recorded here rather than buried.

DateFormatter accepts an unpadded 2024-9-1; my first regex required two digits and silently stopped such a value being a date at all. Month, day and hour now take one or two digits.

Calendar rolls an out-of-range component over instead of refusing it, where DateFormatter refused. MySQL's 0000-00-00 00:00:00 became a plausible 0002-11-30, and 2024-13-45 became 2025-02-14. Both used to render as text and do again: isInRange rejects the impossible ones and keepsItsDay catches 2024-02-30, which only Calendar can detect. The day check deliberately ignores the clock, so a naive time inside a spring-forward gap is still the day it says it is.

Fractional seconds

They now reach the Date, at full precision rather than the millisecond truncation SSSSSS gave. ResultChartProjector uses the Date as its grouping key, so two rows at .100 and .200 were sharing one x position. The text is kept verbatim in the layout and re-emitted on write, so DATETIME(6) still round-trips character for character. oneGrammarServesDisplayAndEditing asserts that for all 15 spellings.

Verification

  • Debug build PASS. DatabaseDateParserTests, DateEditingServiceTests, ResultChartProjectorTests: 52 executed, 52 passed. Log grepped for Failing tests:, TEST FAILED and Crash:, 0 hits.
  • swiftlint --strict clean on all 8 changed files.
  • Two swiftc probes drove the design: one compared both grammars across 15 wire spellings and found the timetz drift, the other measured the empty-cell zone bug at UTC+7. Neither is committed; they answered a question rather than pinning a constant.

A test that crashed instead of failing

While the padding regression was live, ResultChartProjectorTests.dateAxisIsTemporal subscripted dates[2] after an #expect(dates.count == 3). #expect does not stop execution, so the array read went out of bounds and killed the test process, marking 12 unrelated tests as failed with no message and a different set each run. That cost two cycles to diagnose. It is now try #require, so a failure stays one test.

Noted, not fixed

An offset-bearing value disagrees with its own cell: 2024-03-15T09:30:00Z draws as 16:30 in the grid (converted to your zone) while the picker opens at 09:30 (as written). Both are defensible and changing either is a product call, so this PR leaves it alone.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit 3b81554 into main Aug 19, 2026
3 checks passed
@datlechin
datlechin deleted the fix/2241-one-date-grammar branch August 19, 2026 14:52
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.

Two date grammars parse the same wire formats and have already drifted

1 participant