Skip to content

feat: Implement date_part scalar function #27005

Open
devanbenz wants to merge 107 commits into
master-1.xfrom
db/76/date_part
Open

feat: Implement date_part scalar function #27005
devanbenz wants to merge 107 commits into
master-1.xfrom
db/76/date_part

Conversation

@devanbenz

@devanbenz devanbenz commented Dec 3, 2025

Copy link
Copy Markdown

Implements date_part(part, expression), which extracts a component from a timestamp.

Signature:

  • Exactly 2 args, in order: date_part('<part>', time)
    • arg 1: string literal naming the part (case-insensitive)
    • arg 2: the time VarRef, nothing else
  • Returns int64, evaluated in the query timezone (tz(...)), default UTC

Parts:

part value
year calendar year
quarter quarter of year, [1, 4]
month month of year, [1, 12]
week ISO-8601 week of year, [1, 53]
day day of month, [1, 31]
hour / minute / second [0,23] / [0,59] / [0,59]
millisecond / microsecond / nanosecond seconds-of-minute scaled to the unit plus the sub-second component, e.g. 45.123s returns 45123 for millisecond
dow day of week, Sunday = 0 to Saturday = 6
isodow ISO-8601 day of week, Monday = 1 to Sunday = 7
doy day of year, [1, 366]
epoch seconds since Unix epoch (whole seconds)

week is the ISO week and year is the calendar year, so the two can disagree at
year boundaries. For example 2023-01-01 returns week 52.

Examples:

-- weekdays only
SELECT * FROM some_measurement
WHERE time >= now() - 10d AND time <= now()
  AND date_part('dow', time) != 0 AND date_part('dow', time) != 6

SELECT value, date_part('hour', time) FROM some_measurement

SELECT rules

  • Must be paired with an anchor, meaning a stored field or a non-date_part
    aggregate or selector. date_part-only selects are rejected.
  • Multiple date_part fields and aliases are allowed, and may nest in expressions
    such as date_part('hour', time) + 1.

GROUP BY date_part rules

  • Allowed alongside time(). Other calls in GROUP BY are rejected.
  • Requires exactly one non-date_part aggregate or selector in the SELECT list.
    Raw selects and multiple aggregates are rejected.
  • Duplicate parts are deduplicated.
  • A SELECTed date_part('part', time) must match a grouped part. A non-grouped
    part is rejected because it is undefined for the bucket. A non-active grouped
    part yields null in that series.
  • Output column is named after the canonical part such as year. A field or alias
    colliding with it is rejected.
  • Resolved from the bucket value, not the row timestamp.
  • fill(none) is always supported. fill(null) (the default) is supported for a
    bare GROUP BY date_part but rejected when combined with a time() interval; use
    fill(none). fill(previous), fill(linear), and fill(<value>) are rejected.

Subqueries

  • date_part is supported in the WHERE clause and in GROUP BY of a query over a
    subquery source, computed from the timestamps the subquery emits. tz(...) is
    honored.
  • The anchor rule applies through subqueries: the innermost statement must select
    a stored field or a non-date_part aggregate or selector.

See #27001 for 1.x limitations.

@devanbenz devanbenz self-assigned this Dec 4, 2025
@devanbenz devanbenz linked an issue Dec 8, 2025 that may be closed by this pull request
@devanbenz devanbenz marked this pull request as ready for review December 9, 2025 21:50
Comment thread query/cursor.go Outdated
Comment thread query/date_part.go Outdated
Comment thread query/date_part.go Outdated

const (
DatePartString = "date_part"
DatePartTimeString = "date_part_time"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is date_part_time? I don't see any tests for it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used to create a reference to time since time is an auxiliary field https://github.com/influxdata/influxdb/pull/27005/files#diff-609a7e16be956ed6386e1a4a4efadf600b7d4de7dcfea27330dc692d1e901dc8R930-R944 I'm going to create some ValueMapper tests for this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gwossum I can add tests for this but it would likely require exporting

type valueMapper struct {
and testing it. We don't currently have any valueMapper specific tests. It's basically just a struct filled with maps so we would likely just be testing go's map functionality, which may not be worth the effort?

@davidby-influx davidby-influx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some changes from the first pass. Will review again after changes.

Comment thread query/compile_test.go
Comment thread query/date_part.go
Comment thread query/date_part.go Outdated
Comment thread query/date_part.go Outdated
Comment thread query/date_part.go Outdated
Comment thread tsdb/engine/tsm1/iterator.gen.go Outdated
Comment thread tsdb/engine/tsm1/iterator.gen.go Outdated
Comment thread tsdb/engine/tsm1/iterator.gen.go Outdated
Comment thread tsdb/engine/tsm1/iterator.gen.go Outdated
Comment thread tsdb/engine/tsm1/iterator.gen.go.tmpl Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • query/internal/internal.pb.go: Generated file
Comments suppressed due to low confidence (1)

query/iterator.gen.go.tmpl:568

  • In ScanAt, when an aux value falls into the default branch and defaultValue == SkipDefault (e.g. fill(none)), the map entry for that key is left untouched. With the new date_part GROUP BY behavior, non-active dimension aux slots are explicitly set to nil, so this path will leave stale values from a previous row in m[k.Val] and can incorrectly populate non-active date_part dimension columns. Clearing the key when no fill default is configured avoids that leakage.
		default:
			// Insert the fill value if one was specified.
			if s.defaultValue != SkipDefault {
				m[k.Val] = castToType(s.defaultValue, k.Type)
			}

@devanbenz devanbenz marked this pull request as ready for review June 24, 2026 20:46
Comment thread query/compile.go Outdated
@davidby-influx

davidby-influx commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Code Review — date_part builtin + GROUP BY date_part(...)

Branch: db/76/date_part vs origin/master-1.x
Diff scope: merge-base 66b0dd767660 … HEAD 474ae657fa (24 files, +5,579 / −607)
Review: xhigh, workflow-backed (54 agents; 41 candidates verified → 19 refuted, 15 reported)

Findings are ranked most-severe first. Verdicts are from independent verifier agents.


🔴 Correctness — silently wrong results / data loss (single-node OSS, all CONFIRMED)

1. SELECT … INTO with GROUP BY date_part silently drops all but the last group

coordinator/statement_executor.go:1415convertRowToPoints treats the injected
date_part column (e.g. year) as a regular field, and every group row shares the same
tag set and the same representative bucket timestamp (single window, Interval=0).
Nothing blocks the INTO path.

SELECT count(v) INTO target FROM cpu GROUP BY date_part('year',time) writes
{year:2020,count:N1}@t0 and {year:2021,count:N2}@t0 with identical
measurement/tags/timestamp → they collide, last-write-wins, silent data loss,
plus a stray int field named after the part.

This is the worst one — silent data loss.

2. Multi-call GROUP BY date_part merges aggregates across groups, mislabeled

query/cursor.go:405multiScannerCursor.scan aligns per-call scanners on
(ts,name,tags) only and writes into one shared map keyed by the single
DatePartDimensionsString; each scanner's date_part value overwrites the others.

SELECT count(v), count(w) FROM cpu GROUP BY date_part('year',time) pairs one field's
count from one year with the other's count from a different year, stamped with
whichever scanner ran last. No compile guard rejects 2+ calls; no test covers it.

3. Raw (non-aggregate) SELECT … GROUP BY date_part does no grouping at all

query/select.go:710 — accepted but takes the aux-cursor branch with no
reduce/DimensionGrouper, so ScanAt hits the plain-int64 arm,
DatePartDimensionsString is never set, and GroupingKeys stays nil.

SELECT value FROM cpu GROUP BY date_part('year',time) returns one flat ungrouped
series with an extra year column — silently diverging from GROUP BY <tag>
semantics, no error.

4. fill(null) fragments date_part series under GROUP BY time(), date_part(...)

query/select.go:571 — filled points use a fixed auxFields slice that never
carries a DecodedDatePartKey, so empty-window rows lose the grouping value.
validateDatePartSelectFields rejects fill(previous/linear/number) but not the
default fill(null).

Empty windows emit null rows with empty GroupingKeys; the emitter's
sameGroupingKeys check then splits them into spurious extra series and fragments the
real ones.

5. Subquery validation bypass

query/compile.go:1411validateDatePartAnchor and the wildcard-collision
re-check run only on the outer statement in Prepare, never recursing into subquery
sources.

SELECT max(yr) FROM (SELECT host, date_part('year',time) AS yr FROM cpu) compiles
cleanly even though the equivalent top-level query is rejected — inner query plans as a
tag-only iterator emitting no points, so max(yr) silently returns nothing instead of
erroring. Inner SELECT * colliding with a stored field named year also escapes the
re-check.


🟠 Semantics diverge from SQL — but locked in by the new tests, so possibly intentional (CONFIRMED)

# Location Divergence
6 query/date_part.go:152 millisecond/microsecond return only the sub-second part — Postgres returns seconds*1000 + frac (off by up to 59,000 ms).
7 query/date_part.go:162 epoch uses t.Unix(), truncating fractional seconds that SQL epoch keeps.
8 query/date_part.go:163 isodow returns 0–6 (Sun=6) instead of SQL 1–7 (Sun=7) — off by one. date_part_test.go asserts the 0–6 values.

Worth a deliberate decision: if the intent is SQL compatibility, fix the code and the
tests; if InfluxDB-specific semantics are intended, document it.


🟡 Clustered/distributed only — not reachable in single-node OSS (PLAUSIBLE)

  • 9. query/point.go:266encodeAux/decodeAux can't serialize the
    DecodedDatePartKey struct; over the data-node wire codec the grouped value comes back
    null and all buckets collapse into one group.
  • 10. query/iterator.gen.go.tmpl:1486 — the generic FilterIterator.Next uses
    EvalBool with a non-CallValuer map, so a date_part(...) WHERE predicate reaching
    it filters out every point (zero rows). No in-repo callers of NewFilterIterator;
    reachability uncertain.
  • 11. tsdb/engine/tsm1/iterator.gen.go.tmpl:289itr.m is allocated only when
    Condition != nil, but written whenever NeedTimeRef. Safe locally (encoder keeps the
    invariant), but a wire-decoded options struct with NeedTimeRef=true, Condition=nil
    panics on a nil-map write → crashes the query/node.

⚡ Efficiency (CONFIRMED)

12. ⭐ DatePartValuer wired into every tsm1 query, date_part or not

tsdb/engine/tsm1/iterator.gen.go.tmpl:235 — unconditionally adds an extra valuer
indirection (an interface call per VarRef/Call lookup, per scanned point) to all
WHERE-filtered queries
— the common path. The scanner cursor already gates the
identical wiring behind needDatePart; opt.NeedTimeRef could gate it here too.

This is a per-point CPU regression across the whole engine, not just date_part queries
— arguably higher priority than its category suggests.

13. ResolveKeys allocates per-point garbage discarded on bucket hit

query/date_part.go:381 — allocates a fresh entries slice + a 9-byte EncodedKey
string per point, but the reduce loop consumes EncodedKey only on bucket creation (map
miss) → K−1 of every K allocations are GC garbage. Return only DimKey, compute
EncodedKey lazily, reuse a scratch buffer.

14. Per-field redundant work in Scan

query/cursor.go:277 — the DatePartDimensionsString lookup, type assertion,
dpd.Expr.String(), and GroupingKeys insert are recomputed per field though invariant
across the field loop; hoist above it.


📋 Convention (CONFIRMED)

15. New date_part tests use raw t.Fatal/t.Error instead of testify

tests/server_test.go:8085 (also 8790, 8863, 9242, 9297) — violates the project's
testify rule. The same functions already use require.NoError(t, err, "init error") for
setup, so it's internally inconsistent too.


Refuted (19, not reported)

Mostly DRY/maintainability "keep-in-sync" observations (parallel DatePartExpr switches,
duplicated AST walkers, LocationOrUTC not reused) where verifiers found no current
observable defect, plus the millisecond/isodow duplicates and a
DatePartValuer{}-in-compileFields "dead code" claim that was refuted (it can fire
with a literal second arg).


Suggested fix order

  1. 1 (data loss) and 12 (engine-wide perf regression) — highest impact.
  2. Correctness cluster 2–5.
  3. Decision on SQL semantics 6–8 (fix code+tests, or document).
  4. Mechanical cleanups 13–15.

@davidby-influx

Copy link
Copy Markdown
Contributor

AI review not verified by a human. Take with a grain of salt.

@devanbenz

Copy link
Copy Markdown
Author

Code Review — date_part builtin + GROUP BY date_part(...)

Going through this and fixed some of the ones that were valid:

  ┌───────┬──────────────────────────────────────────────┬────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┐
  │   #   │                   Finding                    │            Original verdict            │                         Current status                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 1     │ SELECT…INTO data loss                        │ ✅ FIXED by 0c8ace0                    │ ✅ Fixed (pre-session)                                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 2     │ Multi-call GROUP BY date_part merge          │ ✅ FIXED by 797b894                    │ ✅ Fixed (pre-session)                                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 3     │ Raw SELECT…GROUP BY date_part no grouping    │ ✅ FIXED by 797b894                    │ ✅ Fixed (pre-session)                                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 4     │ fill(null) fragments series                  │ ✅ FIXED by 797b894                    │ ✅ Fixed (pre-session)                                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 5     │ Subquery validation bypass                   │ ✅ FIXED by 797b894                    │ ✅ Fixed (pre-session)                                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 12 ⭐ │ DatePartValuer in every tsm1 query           │ ✅ FIXED by 0c8ace0                    │ ✅ Fixed (pre-session)                                          │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 9     │ aux codec can't serialize DecodedDatePartKey │ ⚪ MOOT in OSS → 🔴 real for plutonium │ ✅ FIXED this session (point.go aux codec + test)               │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 11    │ nil-map panic on wire-decoded opts           │ ⚪ MOOT → 🟡 defensive                 │ ✅ FIXED this session (iterator.gen.go.tmpl alloc guard + test) │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 10    │ FilterIterator drops date_part rows          │ ⚪ MOOT — no callers (incl. plutonium) │ ⚪ Skip — confirmed unreachable                                 │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤

There are a few left that I'm investigating:

  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 13    │ ResolveKeys per-point garbage                │ 🟡 STANDS — minor, date_part-only      │ ⏭️ Not needed (skip)                                            │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 14    │ per-field redundant work in Scan             │ 🟡 STANDS — minor, date_part-only      │ ⏭️ Not needed (skip)                                            │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 15    │ tests use raw t.Fatal not testify            │ 🟡 STANDS — cosmetic                   │ ⏭️ Optional (your call)                                         │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 6     │ millisecond/microsecond sub-second only      │ 🟠 STANDS — PG divergence              │ ⛔ Open — needs product decision                                │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 7     │ epoch truncates fractional                   │ 🟠 STANDS — forced by int64 return     │ 📝 Open — document/leave                                        │
  ├───────┼──────────────────────────────────────────────┼────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
  │ 8     │ isodow off-by-one (0–6 vs 1–7)               │ 🟠 STANDS — clearest real bug          │ ⛔ Open — likely fix                                            │
  └───────┴──────────────────────────────────────────────┴────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘

@devanbenz

Copy link
Copy Markdown
Author

And after the changes, the only items left

  ├──────────────────────────────────────────────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ #13 ResolveKeys per-point allocs             │ Not feasible safely    │ Attempted this session; reverted — the DimKey doubles as the output-series sort key, so the compact encoding changes result ordering. │
  ├──────────────────────────────────────────────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ cursor.go allocates GroupingKeys map per row │ Not needed             │ Minor, date_part-only path; correctness fine.                                                                                         │
  ├──────────────────────────────────────────────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ #7 epoch fractional seconds                  │ Won't fix (documented) │ Requires changing the int64 return contract; your call was leave+document.                                                            │
  ├──────────────────────────────────────────────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ #10 NewFilterIterator dead-code removal      │ Not needed             │ Zero callers; the WHERE-on-subquery case it worried about is handled by the compile-time rejection.                                   │

@davidby-influx davidby-influx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review

Comment thread cmd/influx/cli/cli.go Outdated
Comment thread query/compile.go
Comment thread query/compile.go Outdated
Comment thread coordinator/statement_executor.go Outdated
Comment thread coordinator/statement_executor.go Outdated
Comment thread coordinator/statement_executor.go Outdated
Comment thread query/compile.go Outdated
Comment thread query/compile.go Outdated
Comment thread query/compile_test.go Outdated

@davidby-influx davidby-influx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments, but still a partial review

Comment thread query/cursor.go Outdated
if cur.needDatePart {
// Clear date_part state from previous scan so it doesn't leak across rows.
delete(cur.m, DatePartDimensionsString)
row.GroupingKeys = nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about suggestion to use clear?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on row.GroupingKeys?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoids an alloc, no locking like the memory allocator uses, etc.

Comment thread query/cursor.go Outdated
if cur.needDatePart {
// Clear date_part state from previous scan so it doesn't leak across rows.
delete(cur.m, DatePartDimensionsString)
row.GroupingKeys = nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could move the make of the map here

if row.GroupingKeys == nil {
    // Do we have a count of max possible keys from compilation
    // that we could pass to make?  Not critical if missing, because
    // clear() will let the map grow monotonically, unlike setting to nil
    row.GroupingKeys = make(map[string]struct{})
} else {
    clear(row.GroupingKeys)
}

cur.Next()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a date_part benchmark. What does a typical use cost?

@devanbenz devanbenz Jul 7, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few date_part benchmarks now:

  • BenchmarkIntegerIterator_Next_Condition (This is without date_part as a "base")
  • BenchmarkIntegerIterator_Next_DatePartCondition
  • BenchmarkIntegerIterator_Next_DatePartDimension
  • BenchmarkFilterCursor_DatePartCondition

Outputs:

BenchmarkIntegerIterator_Next_Condition-20    	36202430	        33.03 ns/op	       0 B/op	       0 allocs/op

BenchmarkIntegerIterator_Next_DatePartCondition-20    	 8578252	       139.8 ns/op	       0 B/op	       0 allocs/op

BenchmarkIntegerIterator_Next_DatePartDimension-20    	82078786	        14.12 ns/op	       0 B/op	       0 allocs/op

BenchmarkFilterCursor_DatePartCondition-20    	33907940	        35.11 ns/op	       0 B/op	       0 allocs/op

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • query/internal/internal.pb.go: Generated file

Comment thread query/date_part.go
Comment thread query/date_part.go
Comment thread query/iterator.gen.go.tmpl Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • query/internal/internal.pb.go: Generated file

Comment thread query/emitter.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • query/internal/internal.pb.go: Generated file

Comment thread query/iterator_mapper.go
Comment thread query/date_part.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • query/internal/internal.pb.go: Generated file

@davidby-influx davidby-influx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. query/subquery.go:133 — user-triggerable process panic / DoS · CONFIRMED

mapAuxField now returns a datePartMap for any GROUP BY date_part dimension name, including the subquery's aggregate driver. But NewIteratorMapper's driver switch only handles FieldMap/TagMap and panics on the default case.

  • Trigger: SELECT count(year) FROM (SELECT year FROM cpu) GROUP BY date_part('year', time) — a subquery whose aggregate argument is named after a date part.
  • Impact: panic: unable to create iterator mapper with driver expression type: query.datePartMap (iterator_mapper.go:84), fired inside an errgroup goroutine spawned by buildCursor — the handler's recover can't catch it, so influxd crashes. Any user who can run queries can trigger it. Reproduced live by the verifier.
  1. query/date_part.go:429 — negative epoch groups sort out of order · CONFIRMED

computeDimKey encodes the signed int64 as unsigned big-endian: binary.BigEndian.PutUint64(buf[:], uint64(val)). A negative int64 becomes a huge uint64, so its bytes sort after every non-negative value, and reduce() sorts these DimKey strings to order the emitted rows.

  • Trigger: date_part('epoch', time) over data spanning 1970 (InfluxDB ns timestamps reach back to 1677), e.g. SELECT count(value) FROM cpu GROUP BY date_part('epoch', time).
  • Impact: pre-1970 (negative) buckets are emitted at the wrong end instead of chronologically. Silent for all other parts (year/month/dow/hour are non-negative), which is why the positive-only tests pass.
  1. query/iterator.go:1051 — rolling-upgrade mis-grouping across the wire codec · PLAUSIBLE

encode/decodeIteratorOptions add proto fields DatePartDimensions (23) and NeedTimeRef (24). An older remote data node drops the unknown fields, so it never computes the date_part aux value.

  • Impact: during a mixed-version rolling upgrade (new coordinator, old data node), a distributed GROUP BY date_part(...) query returns points with no date_part aux; len(aux) < len(dims) collapses everything into one ungrouped series — silently wrong results, no error. Verifier confirmed the mechanism but couldn't fully exercise the cross-version path, hence PLAUSIBLE.
  1. query/compile.go:975 — observable error-text change · CONFIRMED (low impact)

The invalid-GROUP-BY-function error was reworded from "only time() calls allowed in dimensions" to "only time() and date_part() calls allowed in dimensions". Any client/driver/test that string-matches the old text (e.g. for GROUP BY now()) silently stops matching. Arguably a correct improvement — flagged only because it's an observable behavior change.

Cleanups

  1. query/iterator.go:722DatePartDimension.Name is redundant derivable state · CONFIRMED

Name is always Expr.String(), yet it's round-tripped over the wire and read at some sites (select.go:686/719, subquery.go:64) while cursor.go:279 ignores it and recomputes Expr.String(). It's self-documented as a footgun: a decoded/hand-built dimension whose Name diverges silently yields an unpopulated grouped column. Drop the field; use Expr.String() everywhere.

  1. query/select.go:710 — two if len(opt.DatePartDimensions) > 0 loops over the same slice · CONFIRMED

Lines 682–691 and 710–721 both guard on the same condition and iterate the same dimensions (one appends the Field, the other the Aux/auxKeys). Easy to drift — add a dim to one loop but not the other and you get a column with no backing aux slot. Merge into one pass.

  1. query/cursor.go:487newFilterCursor defeats the NeedTimeRef cache · CONFIRMED

opt.NeedTimeRef is computed once and documented as caching the AST walk "to avoid repeatedly walking the condition AST for every iterator creation" — but newFilterCursor calls conditionNeedsTimeRef(filter) again per subquery filter cursor, re-walking the whole tree. Pass opt.NeedTimeRef (or opt) in.

  1. query/cursor.go:165 — duplicated "expr contains a date_part Call" WalkFunc · PLAUSIBLE

The same WalkFunc{ if call.Name == DatePartString ... } idiom appears in scannerCursorNeedsDatePart (cursor.go:171), conditionNeedsTimeRef (iterator.go:755), and inline in compile.go (validateDatePartSelectFields, validateDatePartAnchor). Change how date_part is identified and you must update every copy; miss one → inconsistent detection. Hoist exprContainsDatePart(expr).

  1. models/rows.go:36 (and :19) — FNV-hashing an already-sorted slice to compare it · CONFIRMED

SameSeries computes a full FNV64a hash of both GroupingKeys slices on every per-row comparison (chunked responses in handler.go:874), even though the slices are already sorted (emitter.sortedKeys). cmd/influx/cli/cli.go:884 does the same comparison with slices.Equal. The hash is more work, inconsistent with the CLI, and two distinct key sets that collide under FNV64a would merge into one series. Use slices.Equal.

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

Labels

1.x area/influxql Issues related to InfluxQL query language kind/enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[1.x] Add date_part scalar function to influxdb

7 participants