Skip to content

Return one NULL row for last_by over a never-written device (#16985)#18267

Open
PDGGK wants to merge 1 commit into
apache:masterfrom
PDGGK:fix/last-by-never-had-data
Open

Return one NULL row for last_by over a never-written device (#16985)#18267
PDGGK wants to merge 1 commit into
apache:masterfrom
PDGGK:fix/last-by-never-had-data

Conversation

@PDGGK

@PDGGK PDGGK commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

Per @Wei-hao-Li's clarification in #16985: an aggregate function should always return NULL when there is no input data (except count), consistent with SQL-standard engines such as Trino. So select last_by(s0, time) from t where deviceId = 'never' — where the device was never written — must return exactly one row whose value is NULL, not zero rows.

Root cause: for the last-cache-optimized last / last_by path, a device set that resolves to zero devices leaves LastQueryAggTableScanOperator with allDeviceCount == 0, so the device loop never runs and the operator returns no rows.

How

LastQueryAggTableScanOperator now emits one all-NULL row when there are zero devices, no GROUP BY, and the aggregation is at its final/complete stage (each aggregator's no-input evaluate() writes NULL for last / last_by). Guards:

  • GROUP BY over an empty group still returns no rows (the one-NULL-row rule is for no-GROUP-BY global aggregation only).
  • A partial stage emits nothing (only the final stage produces the row).
  • The deleted-device case is untouched (the device still exists, so the operator already iterates and emits the NULL row).

Tests

IoTDBDeletionTableIT: a never-written device queried with 2-arg and 3-arg last_by returns one NULL row; multiple last_by aggregates return one all-NULL row; GROUP BY over a never-written device returns no rows; and a regression check that a deleted device still returns one NULL row (both the last-value-cache and recomputed paths).

Scope

Scoped to last / last_by. The general aggregation operator (max / min / sum / avg) likely returns empty for a never-written device as well; that is left as a follow-up rather than expanding this PR.

Relates to #16985.

…6985)

A global aggregation without GROUP BY always produces exactly one row; over
empty input every aggregate except count() evaluates to NULL. For the
last-cache-optimized last/last_by path, a device set that resolves to zero
devices (e.g. a filter on a device that was never written) left the operator
with nothing to iterate, so it returned zero rows instead of the single NULL
row required by SQL semantics (consistent with engines such as Trino).

LastQueryAggTableScanOperator now emits one all-NULL row when there are zero
devices, no GROUP BY, and the aggregation is at its final/complete stage. GROUP
BY over an empty group still returns no rows, and a partial stage still emits
nothing. The deleted-device case (the device still exists, so the operator
iterates and already emits the NULL row) is unchanged.

Scoped to last/last_by; the general aggregation operator (max/min/sum/avg)
likely returns empty for a never-written device too and is left as a follow-up.

Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
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.

1 participant