Skip to content

Fix JSON-null datapoint queries and honor MODE gap filling - #3447

Open
ebariaux wants to merge 11 commits into
mainfrom
bugfix/queries-json-null
Open

ebariaux wants to merge 11 commits into
mainfrom
bugfix/queries-json-null

Conversation

@ebariaux

Copy link
Copy Markdown
Contributor

Stored JSON null values caused numeric and boolean datapoint queries to fail during SQL
casting. Normalize JSON null to SQL NULL before conversion, and preserve unavailable
values during boolean conversion instead of interpreting them as false.

This applies to historical and predicted datapoints:

  • All retains unavailable observations and their timestamps.
  • Numeric Nearest returns the nearest observation even if its value is unavailable.
  • MIN, MAX, AVG, SUM, MEDIAN and MODE ignore unavailable input values; buckets with no
    available values return null. COUNT still counts stored rows, including null-valued rows.
  • Boolean MODE and MEDIAN return numeric 1/0 values compatible with the query service.
  • DIFFERENCE preserves unavailable endpoints. Explicit nulls interrupt carry-forward;
    empty buckets still carry the previous bucket value when gap filling is enabled.
  • LTTB omits unavailable observations before downsampling. It does not retain null gap
    markers; an all-null range returns no points.

MODE now uses a grouped native aggregate, enabling Timescale gap filling. Numeric ties
still select the smaller value and boolean ties select true. Boolean MEDIAN retains its
threshold of at least 0.5 for a true/1 result.

Compatibility / migration

  1. MODE with gapFill=true now returns empty buckets across the inclusive requested range,
    including leading/trailing gaps and entirely empty ranges. A five-minute query from
    08:30 through 09:00 with observations only through 08:55 now returns an additional
    (09:00, unavailable) bucket. Callers must handle added rows and unavailable values.
    Use gapFill=false if only populated buckets are wanted. Fine-grained sparse ranges
    can now exceed the configured result limit (default 100000 points).
  2. DIFFERENCE after a leading empty bucket now returns an unavailable delta for the first
    available reading, rather than its absolute value. Callers must not assume a zero
    baseline when the previous bucket is unavailable.
  3. Boolean MODE/MEDIAN previously lost available results during conversion to the query
    service's numeric format. They now return 1 or 0, so existing callers may receive real
    values where they previously saw unavailable results.

Wire format is unchanged: datapoints use x for timestamp and y for value. The configured
JSON mapper omits null properties. A retained unavailable datapoint therefore appears as
{"x":1767225600000}, not necessarily with an explicit "y":null. Clients must handle an
absent y. Both forms deserialize to a null value. LTTB omits the observation entirely.
No database migration or rewrite of stored datapoints is required.

Validation

The core query suites passed 136 tests (132 new null-query cases plus four existing query
features), with none skipped and UTC explicitly configured on the test JVM. Coverage spans
historical/predicted data, NUMBER/BIG_NUMBER/BOOLEAN, both gap-fill settings, null-only and
empty ranges, ties, recovery after unavailable differences, query bounds and real LTTB
point selection. Each implementation step was checked against failing regressions.
The serialization probe also verified omitted y, explicit-null deserialization, and
preservation of zero and false. All 18 adjacent storage/export/deserialization tests
also passed, with none skipped.

MODE performance

Compared the previous null-safe custom MODE query against the grouped native query using
288 captured EXPLAIN ANALYZE plans on PostgreSQL 17.9 / TimescaleDB 2.26.4, with matching
indexes and seven-day chunks. Three measured warm-cache runs followed one warmup, with
old/new execution order alternated. All tested equal-output data queries improved.
For 20000 numeric observations across 4000 buckets, median execution fell from 3123 ms to
9.2 ms: the old plan scanned the 20000-row CTE 4000 times. Large numeric and boolean
buckets also improved despite sort spills. These are synthetic database execution times,
not end-to-end production guarantees.
Sparse minute-level gap filling across a year now generates 525600 rows instead of 365;
the count query increased from 0.55 ms to 56.8 ms and the default limit rejects that range.
All benchmark data and objects were rolled back.

Replace correlated frequency queries with grouped MODE aggregates.
Preserve null handling and tie-breaking, and cover empty ranges and leading/trailing gaps with regression tests.

BREAKING CHANGE: MODE with gapFill=true now includes empty buckets as null-valued results across the inclusive query range.
Results may contain more rows and exceed the configured datapoint query limit.
Use gapFill=false to retain populated-bucket-only results.
Normalize JSON nulls before casting and remove the fallback that replaced unavailable differences with absolute readings.
Document carry-forward semantics and add regression coverage.

BREAKING CHANGE: With gap filling enabled, the first reading after a leading empty bucket now yields null instead of its absolute value as the difference.
@ebariaux ebariaux added Bug Error or issue in the system Breaking Change Indicates that a PR includes a breaking change labels Oct 19, 2026
@ebariaux
ebariaux requested a review from a team October 19, 2026 09:26
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

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

Labels

Breaking Change Indicates that a PR includes a breaking change Bug Error or issue in the system

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Datapoint queries fail on stored JSON null values for numeric and boolean attributes

1 participant