Parse unquoted JSON numbers for DateTime64 and DateTime as Unix timestamps#108091
Parse unquoted JSON numbers for DateTime64 and DateTime as Unix timestamps#108091alexey-milovidov wants to merge 47 commits into
Conversation
…tamps An unquoted JSON number such as `1703363853.035` was rejected when inserted into a `DateTime64` column via `JSONEachRow` (and the other JSON/Quoted text paths), because the unquoted-number branch used `readIntText`, which stops at the decimal point and leaves `.035` unparsed, raising `CANNOT_PARSE_INPUT_ASSERTION_FAILED`. The same number is accepted by the `Values` format. For `DateTime64`, `readIntText` also read a bare integer straight into the raw underlying (scaled) value, so `1703363853` was interpreted as ticks (`1970-01-20 ...`) rather than seconds, inconsistent with `Values`, `CAST` and `toDateTime64`. Now an unquoted number is parsed as a decimal value at the column scale, i.e. a Unix timestamp (seconds since the epoch) with optional sub-second precision, matching `Values`, `CAST`, `toDateTime64` and the `Decimal` type (`DateTime64` is a `Decimal` underneath). One more digit than `Decimal`'s precision is allowed so that a 10-digit second count is still valid at scale 9. `DateTime` truncates the fractional part to whole seconds, also matching `CAST`. Closes: #59443 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Workflow [PR], commit [2c90b96] AI ReviewSummaryThis PR changes unquoted numeric Findings❌ Blockers
Final VerdictStatus: ❌ Block |
… detect Int64 overflow The unquoted-number branch read the value straight into the `Int64` ticks of `DateTime64` with one extra digit of precision allowed. As the AI review noted, `readDecimalText` accumulates into the destination type, whose `Decimal` operators wrap, so a 19-digit input just above the `Int64` boundary (e.g. `9223372036854775808` for `DateTime64(0)`) could wrap during accumulation and then pass the final scale-multiplication check as a bogus, often negative, timestamp. Read into a `Decimal128` temporary instead, apply the remaining scale in `Int128`, then range-check the result against the `Int64` ticks of `DateTime64` and raise `DECIMAL_OVERFLOW` if it does not fit. A 128-bit accumulator holds up to `max_precision<Decimal128>` (38) digits, which spans the whole valid range at every scale, so no in-range value is rejected and no out-of-range value wraps. This also makes a too-large value report `DECIMAL_OVERFLOW` rather than `ARGUMENT_OUT_OF_BOUND`, matching the error the `Values` expression fallback produces (fixes the `03797_convert_field_to_type_decimal_overflow` regression). Adds boundary coverage to `04402_json_datetime64_number_input`: `Int64` max/min are stored exactly, and values one step outside the range are rejected instead of silently wrapped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mantics A bare unquoted integer for a `DateTime64` column in the `Values`/Quoted text path is now a Unix timestamp in seconds rather than the raw scaled value, so tests that fed such an integer to a `DateTime64` column need their inputs (or references) adjusted: - `01516_date_time_output_format`: `1602709200123` (raw ms ticks) becomes `1602709200.123`, which is the same instant under the new rule, keeping the reference unchanged. - `02519_monotonicity_fuzz`: `1000` (one second of ticks) becomes `1`, one second since the epoch, keeping the reference unchanged. - `01732_more_consistent_datetime64_parsing`: the integer row is now whole seconds (`1111111111`); the other forms are unchanged. - `00927_asof_join_other_types`: the `DateTime64(3)` rows now read `1`, `3`, `5` as seconds, which incidentally makes them consistent with the `DateTime` rows in the same test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The backward-incompatible parser change makes a bare unquoted integer for a `DateTime64` column a Unix timestamp in seconds (matching `DateTime`, the `Values` format, `CAST` and `toDateTime64`) instead of the raw scaled ticks. The user doc `docs/en/sql-reference/data-types/datetime64.md` and the matching source documentation in `src/DataTypes/registerDataTypeDateTime.cpp` still described the old contract (an integer interpreted as scaled ticks, e.g. `1546300800000` at precision 3), which would now parse to a different instant. Update both to describe a number as a Unix timestamp in seconds with optional sub-second precision, change the example insert from `1546300800123` to `1546300800`, and add a backward-incompatible-change note pointing out that a bare integer used to be interpreted as raw ticks before version 26.7. Quoted strings and ClickHouse's own (always quoted) output are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 04402 prefix now has many tests in master after the merge; move the new DateTime64 number-input test to the free 04489 slot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed the work that addresses the AI Review ❌ Block (the commits were prepared but not yet on the branch): Minimum required actions from the verdict, all done:
The existing tests broken by the behavior change were updated to keep their original intent: Also merged the latest |
The sentence "Contrary to inserting, the `toDateTime64` function will treat all values as the decimal variant" described the pre-26.7 behavior, where an inserted number was the raw scaled value while `toDateTime64` interpreted a number as seconds. Now that an inserted number is also a Unix timestamp in seconds, the contrast no longer holds and the sentence contradicted the new insertion contract. Reword it in both `docs/en/sql-reference/data-types/datetime64.md` and the mirrored source documentation in `src/DataTypes/registerDataTypeDateTime.cpp`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `YTsaurus` engine reads YT data via `JSONEachRow` and maps the YT `timestamp`/`timestamp64` types (microseconds since the epoch) to `DateTime64(6)`, relying on an unquoted JSON number being stored as the raw underlying value (ticks). This PR changed an unquoted number to mean a Unix timestamp in seconds, so a real YT timestamp such as `1703363853000000` would be interpreted as seconds and rejected with `DECIMAL_OVERFLOW`. Add a `read_datetime64_number_as_raw_value` JSON format setting that restores the raw-ticks interpretation of an unquoted number for `DateTime64` columns, and enable it in `YTsaurusSource`. The setting is internal (set only by the `YTsaurus` reader) and defaults to off, so the user-facing JSON/Quoted parsing behavior is unchanged. Fixes the integration test `test_ytsaurus/test_tables.py::test_ytsaurus_primitive_types[timestamp0]` and `[timestamp1]`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An unquoted number for a `DateTime64` column is now interpreted as a Unix timestamp in seconds. The `VALUES` insert in this test used raw millisecond ticks (`1639872000000`, `1639872891123`, `42`) which under the new semantics overflow or shift to a different instant. Use the equivalent seconds form (`1639872000`, `1639872891.123`, `0.042`) so the stored ticks, the `MsgPack` round-trip and the reference output are all unchanged. `readDecimalText` parses these exactly, so the stored values are bit-identical to before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed 1. Docs (the sole blocking action / unresolved thread, 2. 3. Remaining red — Verification: the three changed translation units pass |
… `DateTime64` The `DateTime` unquoted-number path used `readIntText` followed by a manual `.`-only fraction skip, so a valid JSON number in exponent form such as `1.703363853e9` or `1703363853.0e0` left the `e...` suffix unread and the row was rejected by `JSONEachRow` - even though the `DateTime64` path (which uses `readDecimalText`) already accepts it and the PR contract promises unquoted JSON numbers behave like Unix timestamps matching `CAST`. Parse the `DateTime` number with the same decimal-aware `readDecimalText` logic used by `DateTime64`, but at scale 0: `DateTime` has whole-second resolution, so any sub-second part (from a fraction or a net-negative exponent) is truncated to whole seconds, matching `CAST`, `toDateTime` and the `Values` format. The value is accumulated in a 128-bit temporary and clamped to the `DateTime` range, as before for out-of-range numbers. Addresses the AI Review finding on `SerializationDateTime.cpp`.
Add exponent-notation cases (`1.703363853e9`, `1703363853.0e0`) for an unquoted `DateTime` number to `04489_json_datetime64_number_input`, alongside the existing `DateTime64` exponent case, and assert they match `CAST(1.703363853e9 AS DateTime)`. Renumber the test from `04489` to `04492`: `04489` is now taken on `master` by `04489_max_threads_auto_parsing_compat`, and `04490`/`04491` are also taken, so `04492` is the next free prefix.
|
Pushed 1. 2. Test ( The unresolved review thread is resolved. The changed translation unit passes |
The unquoted-number paths of `SerializationDateTime64` and `SerializationDateTime`
parse a Unix timestamp with `readDecimalText` and `digits_only = false`, which stops
at the first non-numeric character so the `,`/`}` that follows the value in JSON is
left for the format layer. That same leniency, however, also accepted a token with no
digits at all: `.`, `-` and `e9` were each consumed as the value zero, so a malformed
row such as `{"t":.}` was silently loaded as the Unix epoch instead of being rejected.
The previous `readIntText`-based code left such suffixes unread, so those rows did not
become data.
`readDigits`/`readDecimalText` now optionally report whether at least one digit
character was read. A bare `0` reads as zero but still has a digit, so the meaningful
`digits` output (which is 0 for a leading zero) cannot be used for this. The
`DateTime64`/`DateTime` readers pass this flag and raise `CANNOT_PARSE_DATETIME` (or
fail the try-path) when no digit was present. The new out-parameter defaults to null,
so every other `readDecimalText` caller is unaffected.
Also drop the redundant `static` from `datetime_number_precision` in the anonymous
namespace, which broke the `Build (arm_tidy)` check with
`readability-static-definition-in-anonymous-namespace`.
Adds malformed-token regression coverage (`{"t":.}`, `{"t":-}`, `{"t":e9}`) for both
`DateTime64` and `DateTime` to `04492_json_datetime64_number_input`.
CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=108091&sha=e2bc6a5e9c78758d3641312329512492d405c0ed&name_0=PR
PR: #108091
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed 1. Malformed unquoted numeric tokens (the AI Major / unresolved thread). 2. 3. Tests. Added the requested malformed-token cases ( The documentation checkbox is now checked (the Verification: the two changed serialization translation units pass |
…tamp parsing change The `Fast test` failed on `03371_dynamic_values_parsing_templates` with `result differs with reference`. This fuzzer-generated test inserts a large heterogeneous set of values into a `Dynamic` column via the `VALUES` format, which uses the constant-expression template parser (hence the test name). For one garbage row whose field is the array literal `['你们', '认识你很高兴', x'E28228FF', '哪国人']`, the template deduced from an earlier row reads the field through the unquoted-number `DateTime` path. On `master` the old `readIntText`-based path read the leading `[` as zero digits and silently produced the epoch (`1970-01-01 00:00:00`). This PR's malformed-token rejection (`readDecimalText` now reports whether at least one digit was read, and the `DateTime`/`DateTime64` readers reject a token with none) correctly refuses the no-digit token, so the value falls back and is stored as `Float64` `0`. This is the intended, more-correct behavior (a token with no digits is not a valid number), and it is the sole difference in the output: the reference now drops the spurious epoch `DateTime` and gains a `0` in the `Float64` group, matching the exact diff produced by CI on the branch. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=108091&sha=4102d1178e6f101e37d6df23d9a13853cb5b1644&name_0=PR&name_1=Fast%20test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `DateTime` DOM path (`JSONExtract`, typed `JSON`) rejected a negative numeric
timestamp before it reached the shared numeric reader, while the row input
serializer `SerializationDateTime::readAsIntText` parses such a token and clamps
it into the `DateTime` range (the Unix epoch). So `{"t":-1}` and `{"t":-0.5}`
became the epoch in `JSONEachRow` / `Values` but raised `INCORRECT_DATA` in typed
`JSON`, and yielded the default value in `JSONExtract`.
Remove the negative rejection from both the integer and the fractional branch of
`DateTimeNode` and let the final `std::clamp` (integer path) and
`timestampNumberToSeconds` inside `tryReadDateTimeAsNumber` (fractional path) map
a negative value to the epoch, matching the row serializer and the adjacent
`DateTime64` DOM path.
Extend `04522_json_extract_datetime64_fractional_precision` with negative integer
and fractional cases across `JSONExtract`, typed `JSON` and the row input path.
Addresses the AI review finding on `src/Formats/JSONExtractTree.cpp`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AI review left one Major on `SerializationDateTime.cpp`: the PR now makes an unquoted number for a `DateTime` column in the `JSON`, `Values`/`Quoted` and typed-`JSON` paths accept fractional and exponent forms (parsed via `readDecimalText` and truncated to whole seconds), and `input_format_read_datetime_number_as_raw_value` restores the legacy integer-only handling — but only the `DateTime64` documentation was updated for this contract, not `DateTime`. Update both `DateTime` documentation surfaces to match the `DateTime64` ones: `docs/en/sql-reference/data-types/datetime.md` and the mirrored source documentation in `src/DataTypes/registerDataTypeDateTime.cpp` now describe fractional/exponent numeric input, the truncation to whole seconds, and the scope of `input_format_read_datetime_number_as_raw_value`. Documentation only, no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…meric input The `DateTime` documentation said the 26.7 change also affects `JSONExtract` and the `JSON` data type but omitted the `Float64`-precision caveat that the matching `DateTime64` note already documents. `JSONExtract` and the `JSON` data type parse a fractional number through `Float64`, so a value close to a whole-second boundary can round to the adjacent second (for example `1703363853.9999999` is read as `1703363854`), while the row input formats truncate the original text exactly. This is pinned by `04522_json_extract_datetime64_fractional_precision`. Mirror the caveat from `datetime64.md` into `datetime.md` and into the source-registered `DateTime` docs in `registerDataTypeDateTime.cpp`, and add it to the `DateTime64` registration doc as well so all four documentation surfaces agree. Documentation-only; no behavior change.
| else /// Just 1504193808 or 1703363853.035 (a Unix timestamp, possibly with sub-second precision) | ||
| { | ||
| readIntText(x, istr); | ||
| readDateTime64AsNumber(x, scale, istr); |
There was a problem hiding this comment.
This quoted-path change is wider than the contract and test coverage around it. deserializeTextQuoted is not just the Values parser: MySQLDumpRowInputFormat::readField (src/Processors/Formats/Impl/MySQLDumpRowInputFormat.cpp:496-503) and the configurable quoted-escaping readers (Template, CustomSeparated, Regexp via deserializeFieldByEscapingRule) all route here too. That means a bare numeric DateTime64 literal in those formats now flips from raw ticks to seconds as well, and the mirrored SerializationDateTime::deserializeTextQuoted change also starts accepting bare fractional/exponent DateTime numbers there.
The setting help/history and the new tests only describe JSON, Values/Quoted, and JSONExtract/typed JSON, so this is currently an undocumented and unproven extra rollout surface. If that broader rollout is intentional, it needs explicit coverage and contract text for those extra callers; if it is not, this cannot live in the shared quoted serializer and needs a format-side flag so non-Values quoted readers keep the pre-26.7 behavior.
There was a problem hiding this comment.
The broader rollout is intentional: the semantics of an unquoted number in a Quoted field should not depend on which format routes through the quoted serializer, so MySQLDump and the Template/CustomSeparated/Regexp formats configured with Quoted field escaping follow the same contract as Values. b52776f91dc makes that explicit: the setting description (FormatFactorySettings.h) and the docs (datetime.md, datetime64.md, the source docs in registerDataTypeDateTime.cpp) now name this surface, and the new regression test 04622_datetime_number_quoted_escaping_formats pins the MySQLDump and CustomSeparated (Quoted escaping) behavior for a bare integer and a fractional number under the default setting and under input_format_read_datetime_number_as_raw_value = 1 (which works there through the same FormatSettings member), plus the unchanged handling of quoted strings.
There was a problem hiding this comment.
This still is not uniform for Regexp. In src/Processors/Formats/Impl/RegexpRowInputFormat.cpp, the matched subpattern is wrapped in a standalone field_buf and passed to deserializeFieldByEscapingRule, but the caller never checks that the whole field was consumed.
That matters in compatibility mode because the raw-value readers used by src/DataTypes/Serializations/SerializationDateTime64.cpp and src/DataTypes/Serializations/SerializationDateTime.cpp stop at . / e. So a Regexp row such as 1703363853.035 with format_regexp_escaping_rule = 'Quoted' and input_format_read_datetime_number_as_raw_value = 1 is accepted after reading only the 1703363853 prefix, instead of following the Values / MySQLDump / CustomSeparated contract described here.
Please either make the Regexp path require full field consumption for Quoted datetime numbers, or narrow the docs/setting text to exclude Regexp. A focused Regexp regression next to 04622_datetime_number_quoted_escaping_formats would pin the intended behavior.
The unquoted-number change for `DateTime`/`DateTime64` applies to every input path that parses fields with the `Quoted` escaping rule, because it lives in the shared quoted serializer: besides the `Values` format, this includes `MySQLDump` and the `Template`/`CustomSeparated`/`Regexp` formats configured with `Quoted` field escaping, and the `input_format_read_datetime_number_as_raw_value` rollback works there through the same `FormatSettings` member. Name that surface explicitly in the setting description and the docs, and add a regression test (`04622_datetime_number_quoted_escaping_formats`) that pins the `MySQLDump` and `CustomSeparated` (`Quoted` escaping) behavior for a bare integer and a fractional number under the default setting and under the compatibility setting. #108091 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t slop The unquoted-number parsing for `DateTime`/`DateTime64` was implemented with local helpers inside `SerializationDateTime.cpp` and `SerializationDateTime64.cpp`, which is the wrong home for shared IO helpers, and each had a separate throwing and try-returning copy of the same body (plus a third duplicate of `tryReadDateTimeAsNumber`). Move the readers to `ReadHelpers` (declarations in `ReadHelpers.h`, definitions in `ReadHelpers.cpp` so no `readDecimalText.h` include is needed in the header) and collapse each throw/try pair into a single `ReturnType`-templated `readDateTimeAsNumber` / `readDateTime64AsNumber` (and the `AsRawValue` legacy variants), following the existing `readDateTime64Text` pattern. The serializations and `JSONExtractTree` now call these shared readers. Also trim the over-long explanatory comments to the non-obvious rationale. No behavior change: the throwing and try paths keep their distinct error codes (`ARGUMENT_OUT_OF_BOUND`, `DECIMAL_OVERFLOW`, `CANNOT_PARSE_DATETIME`, `CANNOT_PARSE_NUMBER`). All existing DateTime-number tests pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Avogar
left a comment
There was a problem hiding this comment.
I refactored the code a bit - reduced code duplication, moved helper functions to ReadHelpers, shortened unnecessary big comments in the code. Hope it's ok.
…e mirrored DateTime64 docs The last refactoring commit reworded the `input_format_read_datetime_number_as_raw_value` help text back to the broad "an unquoted number" claim, while the implementation restores the legacy raw-value path only for a bare unquoted integer. Narrow the wording again and state explicitly what happens to a number with a fractional or exponent part in compatibility mode: the row input paths reject it, and `JSONExtract`/typed `JSON` still reads it as seconds for `DateTime64` and rejects it for `DateTime` — all matching the pre-26.7 behavior the setting restores. Also re-sync the mirrored `DateTime64` source documentation in `registerDataTypeDateTime.cpp` with `docs/en/sql-reference/data-types/datetime64.md`: point readers at the rollback setting, and say the tab-separated, CSV and other escaped text formats are not governed by it and keep their existing interpretation of an unquoted number (a large value is read as ticks) instead of the broader "keep that interpretation". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed 1. 2. Mirrored Both changed translation units compile cleanly. Triage of the one completed CI red at |
|
Merged Triage of the three CI failures on
The merge triggers a fresh CI run. Review state is otherwise complete: Avogar approved, AI Review verdict is ✅ Approve, and there are no unresolved threads. |
|
Investigated. This is the chronic transient mode of Evidence:
I could not reproduce a deterministic failure (a prior investigation ran it 30/30 locally, same conclusion), and no separate fix PR is currently open for this test. I will follow up here shortly with either a narrow test-robustness change that does not weaken the 202 throttle checks, or, if it stays non-reproducible, the concrete findings. |
|
Fix for Root cause: the test's long-running holder query Fix pins |
…the JSON DOM path behind the compatibility setting With `input_format_read_datetime_number_as_raw_value = 1` (or `compatibility = '26.6'`), a negative integer for a `DateTime` column in `JSONExtract` and the typed `JSON` type is rejected again with `cannot convert negative integer value ... to DateTime`, as before 26.7, instead of being clamped to the epoch. The clamping relaxation now applies only on the new default path. The typed-JSON `-1` case is pinned in the compatibility test in both modes. #108091
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 254/294 (86.39%) · Uncovered code |
… a fractional number in compatibility mode With `input_format_read_datetime_number_as_raw_value = 1`, a fractional literal in the `Values` format is rejected by the streaming parser and falls back to SQL expression evaluation, which reads it as seconds. This is not a gap in the rollback contract: the same fallback produced the same result in versions up to 26.6 (verified on the official 26.7.1 build, where `(1703363853.035)` for `DateTime64(3)` yields `2023-12-23 20:37:33.035`), so `Values` behavior for a fractional number is identical in every configuration. Clarify this in the setting description, both data type docs and registration docs, and pin it with regression queries in `04508`.
A JSON number with a fractional part such as
1703363853.035was rejected when inserted into aDateTime64column viaJSONEachRow, raisingCANNOT_PARSE_INPUT_ASSERTION_FAILEDat the decimal point — even though the same number is accepted by theValuesformat, and the docs advertise that floating-point epochs cast toDateTime64.The unquoted-number branch of
SerializationDateTime64(andSerializationDateTime) usedreadIntText, which stops at the.and leaves the fractional part unparsed. ForDateTime64it also read a bare integer straight into the raw underlying (scaled) value, so1703363853was interpreted as ticks (1970-01-20 ...) instead of seconds — inconsistent withValues,CASTandtoDateTime64.Now an unquoted number is parsed as a decimal value at the column scale: a Unix timestamp (seconds since the epoch) with optional sub-second precision, matching
Values,CAST,toDateTime64and theDecimaltype (DateTime64is aDecimalunderneath). One more digit thanDecimal's precision is allowed so that a 10-digit second count is still valid at scale 9.DateTimetruncates the fractional part to whole seconds, also matchingCAST.Backward incompatible note: a bare unquoted integer fed to a
DateTime64column inJSONEachRow(and the other JSON/Quoted text paths) is now interpreted as seconds since the epoch instead of the raw scaled value. For example1703363853forDateTime64(3)now yields2023-12-23 20:37:33.000instead of1970-01-20 17:09:23.853. This only affects unquoted numbers; quoted strings and ClickHouse's own JSON output (which is always quoted) are unaffected.Closes: #59443
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
An unquoted JSON number for a
DateTime64/DateTimecolumn inJSONEachRowand similar formats is now interpreted as a Unix timestamp (seconds since the epoch) with optional sub-second precision, consistent with theValuesformat,CASTandtoDateTime64. Previously a number with a fractional part (e.g.1703363853.035) was rejected withCANNOT_PARSE_INPUT_ASSERTION_FAILED, and a bare integer (e.g.1703363853) was read into the raw scaled value of aDateTime64, producing a1970-...timestamp. This is a backward incompatible change for unquoted integers fed toDateTime64columns; quoted strings and ClickHouse's own (always quoted) JSON output are unaffected.Documentation entry for user-facing changes