nexyron.temporal_bucket_aggregate

Generic Description

Aggregate one declared temporal property across all subjects, or across a selected subject list, into calendar or fixed-width time buckets without first materializing every temporal point as a Cypher row.

Simple example:

CALL nexyron.temporal_bucket_aggregate(
  'User',
  'payments',
  'month',
  'sum',
  'amount',
  null,
  null,
  null
)
YIELD bucket_start, bucket_end, value
RETURN bucket_start, bucket_end, value
ORDER BY bucket_start

Arguments:

Returned columns:

Consumer-Level Explanation

Use this procedure when the business question is naturally phrased as "sum/count/average this temporal metric per year, month, week, or day" and the data is stored as a declared temporal property.

The important difference from nexyron.temporal_history or nexyron.temporal_rollup is scope. temporal_history(node_id, ...) and temporal_rollup(node_id, ...) read one owner at a time. That is useful for one account, one device, or one user. It is not the clean shape for "all users by month" because the caller would have to discover owners, loop over them, and then aggregate outside the temporal store.

nexyron.temporal_bucket_aggregate keeps that work inside the temporal store. The caller names the label, property, bucket, aggregate, and optional value path. If owners is null, Nexyron aggregates all subjects that have that declared temporal property. If owners is supplied, Nexyron aggregates only those subjects.

For business users, this means:

Conceptual Explanation

This procedure fills the gap between one-subject temporal rollups and general Cypher aggregation.

Normal Cypher aggregation is excellent after a query has already produced rows. One-subject temporal rollups are excellent when a known owner already has materialized buckets. Cross-owner business reporting needs a different shape: the engine should read the temporal store directly, group by the requested time bucket, and only return the small bucket result.

The procedure keeps the row grain explicit:

one returned row = one populated time bucket for one declared temporal property

That grain is useful for reports, charts, feature generation, assistant-authored analytics, and downstream calculations such as dividing every bucket by a denominator from another query.

Calendar buckets are UTC calendar buckets. month means real calendar months such as January and February, not a fixed 30-day duration. week uses ISO-style Monday-start weeks.

More Detailed Explanation

The procedure scans the temporal-property store directly and maintains one compact aggregate state per output bucket. It does not build a list of all matching history rows, and it does not keep per-subject row buffers.

The memory shape is:

bucket_start -> aggregate_state

For a ten-year monthly report this means roughly 120 states, plus a distinct-subject set per populated bucket for subject_count. It does not mean one in-memory object per payment row in the final aggregation layer.

The execution path uses the existing declared temporal-property storage:

  1. Resolve label.property in the temporal property store.
  2. Iterate only retained subjects for that property, or only subjects in the optional owner filter.
  3. Use sorted per-subject history vectors to skip records outside from_ts and to_ts.
  4. Extract the scalar value directly from the temporal value or from value_path.
  5. Put the timestamp into a calendar bucket (year, month, week, day) or fixed millisecond bucket.
  6. Update the aggregate state in place.
  7. Return finalized bucket rows ordered by bucket_start.

Advanced Example

This example calculates three monthly payment totals and then divides each monthly total by four. The division happens after the bucket aggregate, so the procedure still returns a compact bucket series first.

WITH datetime('2026-01-01T00:00:00Z') AS from_ts,
     datetime('2026-04-01T00:00:00Z') AS to_ts
CALL nexyron.temporal_bucket_aggregate(
  'User',
  'payments',
  'month',
  'sum',
  'amount',
  from_ts,
  to_ts,
  null
)
YIELD bucket_start, bucket_end, value, sample_count, subject_count
RETURN bucket_start,
       bucket_end,
       value AS monthly_payment_sum,
       value / 4.0 AS monthly_payment_sum_divided_by_4,
       sample_count,
       subject_count
ORDER BY bucket_start

This example applies a denominator from another graph query. The denominator is computed once, then applied to every returned bucket.

MATCH (:Company {id: $company_id})<-[:BELONGS_TO]-(u:User)
WITH collect(id(u)) AS owners
CALL {
  MATCH (p:Plan {id: $plan_id})
  RETURN coalesce(p.normalization_divisor, 1.0) AS divisor
}
WITH owners, divisor
CALL nexyron.temporal_bucket_aggregate(
  'User',
  'payments',
  'month',
  'sum',
  'amount',
  null,
  null,
  owners
)
YIELD bucket_start, value, sample_count, subject_count
RETURN bucket_start,
       value AS selected_users_monthly_sum,
       value / divisor AS normalized_monthly_sum,
       sample_count,
       subject_count
ORDER BY bucket_start

This example uses scalar temporal values instead of object values. Because the temporal value itself is numeric, value_path is null.

CALL nexyron.temporal_bucket_aggregate(
  'Account',
  'balance_delta',
  'week',
  'avg',
  null,
  datetime('2026-01-01T00:00:00Z'),
  datetime('2026-03-01T00:00:00Z'),
  null
)
YIELD bucket_start, value, sample_count, subject_count
RETURN bucket_start,
       value AS avg_weekly_balance_delta,
       sample_count,
       subject_count
ORDER BY bucket_start

Real Use Cases

Real Limitations And Tradeoffs