Skip to content

add time_bucket feature - #794

Merged
adsharma merged 1 commit into
LadybugDB:mainfrom
ericyuanhui:tumble_feature
Aug 12, 2026
Merged

add time_bucket feature#794
adsharma merged 1 commit into
LadybugDB:mainfrom
ericyuanhui:tumble_feature

Conversation

@ericyuanhui

@ericyuanhui ericyuanhui commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Required Syntax

time_bucket(interval('<period>'), <date/time-expression>)

The first argument uses Ladybug's existing interval('...') expression and
must describe a non-null positive interval. It must support the units accepted
by Ladybug's current interval parser: years, quarters, months, weeks, days,
hours, minutes, seconds, and smaller units. The function is semantically
similar to DuckDB; it does not need to reproduce DuckDB's literal spelling.

Equivalent bucket-start usage for the existing daily IPv4 feature is:

MATCH (s:CampusUser)-[:CampusUserHasOnlineSession]->(e:EventNetworkOnline)
WITH s.id AS entity_id,
     time_bucket(interval('1 day'), e.login_time) AS window_start,
     e.user_ipv4 AS user_ipv4
WITH entity_id, window_start,
     window_start + interval('1 day') AS window_end,
     count(DISTINCT user_ipv4) AS daily_ipv4_count
RETURN entity_id, window_start, window_end, daily_ipv4_count;

The first WITH is only needed when the caller wants to reuse the bucket
start. window_end is not generated by time_bucket; callers compute it as
an ordinary expression when required.

Complete example:

CREATE NODE TABLE EventNetworkOnline(
  id INT64,
  login_time TIMESTAMP,
  user_ipv4 STRING,
  PRIMARY KEY(id)
);

CREATE
  (:EventNetworkOnline {id: 101, login_time: timestamp('2026-08-01 00:00:00'), user_ipv4: '10.0.0.1'}),
  (:EventNetworkOnline {id: 102, login_time: timestamp('2026-08-01 10:23:45'), user_ipv4: '10.0.0.2'}),
  (:EventNetworkOnline {id: 103, login_time: timestamp('2026-08-01 23:59:59'), user_ipv4: '10.0.0.3'}),
  (:EventNetworkOnline {id: 104, login_time: timestamp('2026-08-02 00:00:00'), user_ipv4: '10.0.0.4'}),
  (:EventNetworkOnline {id: 105, user_ipv4: '10.0.0.5'});

MATCH (e:EventNetworkOnline)
WITH e, time_bucket(interval('1 day'), e.login_time) AS window_start
RETURN e.id, window_start,
       window_start + interval('1 day') AS window_end
ORDER BY e.id;

Expected result:

101|2026-08-01 00:00:00|2026-08-02 00:00:00
102|2026-08-01 00:00:00|2026-08-02 00:00:00
103|2026-08-01 00:00:00|2026-08-02 00:00:00
104|2026-08-02 00:00:00|2026-08-03 00:00:00
105||

Edge-table / relationship-property example:

CREATE NODE TABLE CampusUser(id INT64, PRIMARY KEY(id));
CREATE NODE TABLE EventNetworkOnline(id INT64, PRIMARY KEY(id));
CREATE REL TABLE CampusUserHasOnlineSession(
  FROM CampusUser TO EventNetworkOnline,
  login_time TIMESTAMP,
  user_ipv4 STRING
);

CREATE (:CampusUser {id: 1}), (:CampusUser {id: 2}),
       (:EventNetworkOnline {id: 101}),
       (:EventNetworkOnline {id: 102}),
       (:EventNetworkOnline {id: 103});

MATCH (s:CampusUser), (e:EventNetworkOnline)
WHERE s.id = 1 AND e.id = 101
CREATE (s)-[:CampusUserHasOnlineSession {
  login_time: timestamp('2026-08-01 10:23:45'), user_ipv4: '10.0.0.1'
}]->(e);

MATCH (s:CampusUser), (e:EventNetworkOnline)
WHERE s.id = 1 AND e.id = 102
CREATE (s)-[:CampusUserHasOnlineSession {
  login_time: timestamp('2026-08-02 00:00:00'), user_ipv4: '10.0.0.2'
}]->(e);

MATCH (s:CampusUser), (e:EventNetworkOnline)
WHERE s.id = 2 AND e.id = 103
CREATE (s)-[:CampusUserHasOnlineSession {user_ipv4: '10.0.0.3'}]->(e);

MATCH (s:CampusUser)-[r:CampusUserHasOnlineSession]->(e:EventNetworkOnline)
WITH s.id AS entity_id, e.id AS event_id,
     time_bucket(interval('1 day'), r.login_time) AS window_start
RETURN entity_id, event_id, window_start,
       window_start + interval('1 day') AS window_end
ORDER BY entity_id, event_id;

Expected result:

1|101|2026-08-01 00:00:00|2026-08-02 00:00:00
1|102|2026-08-02 00:00:00|2026-08-03 00:00:00
2|103||

Interval Reuse Requirements

time_bucket must directly reuse Ladybug's interval('...') syntax, existing
interval parser, and its constant-folded result. It must not add a second unit
parser or reparse the input string. An INTERVAL is represented by three
independent components, months, days, and micros; the implementation must
preserve all three.

  • Supported units follow the current parser, including years, quarters, months,
    weeks, days, hours, minutes, seconds, milliseconds, microseconds, and their
    currently supported aliases.
  • When months == 0, days and micros can be treated as a fixed bucket
    width.
  • When months != 0, bucketing must use calendar year/month arithmetic. It
    must not call a helper that approximates months as 30 days or years as 365
    days.
  • interval('5 months'), interval('2 years'), interval('3 weeks'), and
    composite intervals are valid time_bucket inputs. The function binder may
    validate constantness, non-nullness, positivity, and representability in the
    output type, but must not narrow Ladybug's supported interval units.

Required Semantics

  • The first argument must be a non-null positive constant INTERVAL value
    produced by interval('...'). It must not reject a value merely because it
    has a month or year component: interval('5 months'),
    interval('2 years'), interval('3 weeks'), interval('13 days'), and
    current parser-supported hour/minute/second units are in scope.
  • The second argument must be any current Ladybug date/time logical type:
    DATE, TIMESTAMP, TIMESTAMP_SEC, TIMESTAMP_MS, TIMESTAMP_NS, or
    TIMESTAMP_TZ. The implementation must not arbitrarily omit a type that
    the current type system supports.
  • The return type is the same date/time logical type as the second argument.
  • Bucket anchors follow Ladybug's own date/time representation: DATE counts
    days from 1970-01-01, and TIMESTAMP counts microseconds from
    1970-01-01 00:00:00. Fixed widths floor directly from that epoch; calendar
    widths with a month component advance from 1970-01-01 using Ladybug's
    interval_t calendar addition. TIMESTAMP_TZ uses its represented instant;
    interval('1 day') means exactly 24 hours, not a session-local civil day.
  • Bucket boundaries are integer multiples of the width from the Ladybug epoch;
    they do not restart for the input year. For example,
    time_bucket(interval('5 months'), date('2024-02-29')) returns 2023-10-01:
    the interval from 2023-10-01 through 2024-03-01 contains that date. This
    matches Ladybug's single calendar addition of interval_t * n to a timestamp.
  • Each type's precision must be respected: DATE requires a whole-day width,
    TIMESTAMP_SEC a whole-second width, and TIMESTAMP_MS a whole-millisecond
    width. The other current types accept fixed widths representable by their
    storage precision.
  • A fixed width without a month component uses floor(t / width) * width.
    A width containing a month component uses calendar year/month arithmetic
    from 1970-01-01 and must not turn months or years into a fixed number of
    days or microseconds. Results are bucket starts with half-open membership
    [start, next_start).
  • A null date/time input returns null. A null is not filtered and no additional
    columns are produced.
  • Pre-epoch arithmetic uses floor division. In particular, a negative
    TIMESTAMP_NS value with a sub-microsecond remainder is normalized toward
    negative infinity before bucket arithmetic.
  • The result is deterministic for a fixed database snapshot and query.

@ericyuanhui

Copy link
Copy Markdown
Contributor Author

This is a proposal to implement a window function similar to time_bucket. From my preliminary understanding, only TigerGraph supports this feature at present. Discussion for development is welcome. Thank you.

@adsharma

Copy link
Copy Markdown
Contributor

TUMBLE is not a part of open cypher. When we add non-cypher extensions, we generally follow duckdb.

FROM TUMBLE(table, ts, INTERVAL '10' MINUTE). // streaming engines
GROUP BY time_bucket(INTERVAL '10 minutes', ts) // duckdb

Will the duckdb variant work for you?

@ericyuanhui

Copy link
Copy Markdown
Contributor Author

TUMBLE is not a part of open cypher. When we add non-cypher extensions, we generally follow duckdb.

FROM TUMBLE(table, ts, INTERVAL '10' MINUTE). // streaming engines
GROUP BY time_bucket(INTERVAL '10 minutes', ts) // duckdb

Will the duckdb variant work for you?

Either the streaming engine style or the DuckDB style works for me. What matters is that Ladybug supports this capability. So I need to implement this feature following either the DuckDB syntax or the streaming style, right?

@adsharma

Copy link
Copy Markdown
Contributor

Yes, the feature looks valuable. Let's use the duckdb style for consistency with the rest of UDFs.

@ericyuanhui

Copy link
Copy Markdown
Contributor Author

Yes, the feature looks valuable. Let's use the duckdb style for consistency with the rest of UDFs.

Are there any existing syntax implementations aligned with DuckDB or streaming engines in the current project? I'd like to refer to them. Could you provide an example?

@adsharma

Copy link
Copy Markdown
Contributor

#694 (comment)

is an example where we implemented functionality to be compatible with duckdb. It was more semantics than syntax.

Signed-off-by: ericyuanhui <285521263@qq.com>
@ericyuanhui

Copy link
Copy Markdown
Contributor Author

#694 (comment)

is an example where we implemented functionality to be compatible with duckdb. It was more semantics than syntax.

follow duckdb time_bucket add ladybug feature. thank you

@ericyuanhui ericyuanhui changed the title add tumble feature add time_bucket feature Aug 12, 2026
@adsharma
adsharma merged commit ff82257 into LadybugDB:main Aug 12, 2026
4 checks passed
@adsharma

Copy link
Copy Markdown
Contributor

@ericyuanhui the PR description still says TUMBLE. Please update. Thanks for the contribution!

@ericyuanhui

Copy link
Copy Markdown
Contributor Author

@ericyuanhui the PR description still says TUMBLE. Please update. Thanks for the contribution!

update

@adsharma

Copy link
Copy Markdown
Contributor

@ericyuanhui - broke the build on windows. Fixing now.

@ericyuanhui

Copy link
Copy Markdown
Contributor Author

@ericyuanhui - broke the build on windows. Fixing now.

I use this project under Linux and have never built it on Windows. Am I right that you’re fixing the Windows build issue now?
I don’t have a Windows environment available at the moment. I also have a small suggestion: could we add Windows build checks to the GitHub CI workflow? That way we can catch such problems at the earliest stage. Thanks

@adsharma

Copy link
Copy Markdown
Contributor

@ericyuanhui don't worry about it. Taken care of. This was for future reference.

Linux only CI is a conscious decision for dev velocity

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.

2 participants