From 9ae63932a4f03479ccbec0d4ef4e701e16304cb2 Mon Sep 17 00:00:00 2001 From: Parag Jain Date: Wed, 20 May 2026 21:06:27 +0530 Subject: [PATCH 1/6] support static data range, rollup priorities --- .../developers/build/metrics-view/rollups.md | 59 +++- .../reference/project-files/metrics-views.md | 6 + proto/gen/rill/runtime/v1/resources.pb.go | 331 ++++++++++-------- .../rill/runtime/v1/resources.pb.validate.go | 4 + .../gen/rill/runtime/v1/runtime.swagger.yaml | 12 + proto/rill/runtime/v1/resources.proto | 8 + runtime/metricsview/executor/executor.go | 52 ++- .../executor/executor_rewrite_rollup.go | 14 +- .../executor_rollup_integration_test.go | 184 +++++++++- runtime/parser/parse_metrics_view.go | 16 + runtime/parser/parse_metrics_view_test.go | 86 +++++ runtime/parser/schema/project.schema.yaml | 6 + .../proto/gen/rill/runtime/v1/resources_pb.ts | 20 ++ 13 files changed, 628 insertions(+), 170 deletions(-) diff --git a/docs/docs/developers/build/metrics-view/rollups.md b/docs/docs/developers/build/metrics-view/rollups.md index 82cafcf65e33..5a5f4c3148c0 100644 --- a/docs/docs/developers/build/metrics-view/rollups.md +++ b/docs/docs/developers/build/metrics-view/rollups.md @@ -70,6 +70,57 @@ rollups: exclude: [total_clicks] # all measures except total_clicks ``` +### Declaring Coverage with `data_time_range` + +By default Rill discovers a rollup's time coverage at query time by running `SELECT min(time), max(time)` against the rollup table (cached). If you'd rather declare coverage statically — to skip the probe, or to scope a rollup to a specific window — set `data_time_range` on the rollup. The value is a [rilltime expression](/reference/time-syntax/time-syntax): + +```yaml +rollups: + - model: events_hourly + time_grain: hour + data_time_range: -1Y to now # rolling last 12 months + dimensions: [publisher, domain] + measures: [total_impressions] + - model: events_daily_archive + time_grain: day + data_time_range: -5Y to -1Y # 1 to 5 years ago + dimensions: [publisher, domain] + measures: [total_impressions] +``` + +A query for last week routes to `events_hourly`; a query 18 months back routes to `events_daily_archive`; a query reaching past 5 years falls back to the base. + +You can declare coverage on the metrics view itself the same way — this skips the probe on the base table: + +```yaml +data_time_range: -5Y to now +``` + +When `data_time_range` is set, the rilltime expression is resolved against synthetic anchors at query time: `now`/`latest`/`watermark` all resolve to the current wallclock, and `earliest` resolves to the zero epoch. So `inf` (a shorthand for `earliest to latest`) declares "all time from zero epoch to now." Mixing declared and undeclared rollups in the same metrics view is fine — each table independently decides whether to probe or to trust its declaration. + +### Selection Priority and Definition Order + +When two rollups have the same time grain and both could answer a query, the **first one declared in the YAML wins**. This is the lever for priority-style routing: list narrower / fewer-row rollups first. + +```yaml +rollups: + # Same grain, different dimension sets. Priority is declared by order. + - model: rollup_advertiser # selected for queries that only need publisher + time_grain: day + dimensions: [publisher] + measures: [total_impressions] + - model: rollup_campaign # selected when publisher + domain are queried + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_adgroup # selected when publisher + domain + country are queried + time_grain: day + dimensions: [publisher, domain, country] + measures: [total_impressions] +``` + +A query on `publisher` alone is eligible against all three — `rollup_advertiser` wins because it's listed first (and scans the fewest rows). A query on `publisher` + `domain` knocks `rollup_advertiser` out of eligibility, so `rollup_campaign` wins. + ### Configuration Reference - **`model`** (required) — The pre-aggregated table or model. @@ -78,8 +129,9 @@ rollups: - **`database`**, **`database_schema`** (optional) — Override the OLAP database and schema for the rollup table. - **`dimensions`** (optional) — Field selector for which base-view dimensions are present in the rollup. Defaults to all. - **`measures`** (optional) — Field selector for which base-view measures are present in the rollup. Defaults to all. +- **`data_time_range`** (optional) — Rilltime expression describing the rollup's time coverage. When set, Rill skips the OLAP `min/max` probe for this rollup and uses the declared bounds for coverage checks. -A metrics view must define a `timeseries` to use rollups. The full schema is documented in the [metrics view reference](/reference/project-files/metrics-views#rollups). +A metrics view must define a `timeseries` to use rollups. The metrics view itself also accepts a top-level `data_time_range` to declare the base table's coverage. The full schema is documented in the [metrics view reference](/reference/project-files/metrics-views#rollups). ## How Rollup Selection Works @@ -116,7 +168,7 @@ For each eligible rollup, Rill checks that the rollup actually contains data for Among rollups that pass eligibility and coverage: 1. Prefer the **coarsest grain** — fewer rows to scan. -2. On a tie, prefer the rollup with the **smallest data range** (tightest coverage). +2. On a tie, prefer the rollup **declared earlier** in the `rollups` list — this is the lever for explicit priority among same-grain rollups. The base table is used if no rollup is eligible. @@ -130,7 +182,8 @@ The base table is used if no rollup is eligible. - **Rollups require a `timeseries`.** Metrics views without a primary time dimension cannot define rollups. - **Filters on missing dimensions disqualify the rollup.** A WHERE clause on `country` will skip a rollup that doesn't include `country`, even if the query's group-by columns are all in the rollup. - **The rollup is responsible for being correct.** Rill does not validate that the rollup's measure values are consistent with the base — it trusts the model. If the rollup model uses the wrong aggregation (e.g. `AVG` where the base measure is `SUM`), queries routed to it will return wrong numbers. -- **Rollups are assumed to be roughly caught up with the base table.** Coverage is measured against the base table's latest timestamp. A rollup that lags behind the base will be silently skipped for any query that reaches the tail of the data — including common "last 24 hours" queries and queries without a time range — even if it has the right grain, dimensions, and measures. Refresh rollups in step with the base model so selection actually happens. +- **Rollups are assumed to be roughly caught up with the base table.** Coverage is measured against the base table's latest timestamp. A rollup that lags behind the base will be silently skipped for any query that reaches the tail of the data — including common "last 24 hours" queries and queries without a time range — even if it has the right grain, dimensions, and measures. Refresh rollups in step with the base model so selection actually happens. (If you can't, declare a `data_time_range` so the rollup advertises its own bounds instead of being measured against the base.) +- **Declared `data_time_range`s drift with `time.Now()`.** Rilltime expressions are anchored to wallclock at query time. If your data actually lags or jumps ahead of what you declared, declared and real bounds diverge silently — queries can route to a rollup whose data stops short. Treat declared coverage as a contract you maintain alongside the rollup model. :::info The full configuration schema is in the [metrics view reference](/reference/project-files/metrics-views#rollups). diff --git a/docs/docs/reference/project-files/metrics-views.md b/docs/docs/reference/project-files/metrics-views.md index b4c108421780..b3026a276bc1 100644 --- a/docs/docs/reference/project-files/metrics-views.md +++ b/docs/docs/reference/project-files/metrics-views.md @@ -60,6 +60,10 @@ _[string]_ - Refers to the timestamp column from your model that will underlie x _[string]_ - A SQL expression that tells us the max timestamp that the measures are considered valid for. Usually does not need to be overwritten +### `data_time_range` + +_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks. + ### `smallest_time_grain` _[string]_ - Refers to the smallest time granularity the user is allowed to view. The valid values are: millisecond, second, minute, hour, day, week, month, quarter, year @@ -306,6 +310,8 @@ _[array of object]_ - Pre-aggregated rollup tables that can be used to accelerat - **`exclude`** - _[object]_ - Select all fields except those listed here + - **`data_time_range`** - _[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the rollup's time coverage (e.g. `-1Y to now`, `-5Y to -1Y`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for this rollup and uses the declared bounds for coverage checks. + ### `security` _[object]_ - Defines [security rules and access control policies](/developers/build/metrics-view/security) for resources diff --git a/proto/gen/rill/runtime/v1/resources.pb.go b/proto/gen/rill/runtime/v1/resources.pb.go index 67e232b6ece9..338f0dd1bfde 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.go +++ b/proto/gen/rill/runtime/v1/resources.pb.go @@ -2020,6 +2020,10 @@ type MetricsViewSpec struct { // Keys and values are stored as templates and will be resolved at query time. QueryAttributes map[string]string `protobuf:"bytes,33,rep,name=query_attributes,json=queryAttributes,proto3" json:"query_attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Rollups []*MetricsViewSpec_Rollup `protobuf:"bytes,34,rep,name=rollups,proto3" json:"rollups,omitempty"` + // Optional rilltime expression describing the time range covered by the base table. + // When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + DataTimeRange string `protobuf:"bytes,36,opt,name=data_time_range,json=dataTimeRange,proto3" json:"data_time_range,omitempty"` } func (x *MetricsViewSpec) Reset() { @@ -2236,6 +2240,13 @@ func (x *MetricsViewSpec) GetRollups() []*MetricsViewSpec_Rollup { return nil } +func (x *MetricsViewSpec) GetDataTimeRange() string { + if x != nil { + return x.DataTimeRange + } + return "" +} + type SecurityRule struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -7485,6 +7496,10 @@ type MetricsViewSpec_Rollup struct { DimensionsSelector *FieldSelector `protobuf:"bytes,9,opt,name=dimensions_selector,json=dimensionsSelector,proto3" json:"dimensions_selector,omitempty"` // Dynamic selector for `measures`. Will be processed during validation, so it will always be empty in `state.valid_spec`. MeasuresSelector *FieldSelector `protobuf:"bytes,10,opt,name=measures_selector,json=measuresSelector,proto3" json:"measures_selector,omitempty"` + // Optional rilltime expression describing the time range covered by the rollup. + // When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + DataTimeRange string `protobuf:"bytes,11,opt,name=data_time_range,json=dataTimeRange,proto3" json:"data_time_range,omitempty"` } func (x *MetricsViewSpec_Rollup) Reset() { @@ -7589,6 +7604,13 @@ func (x *MetricsViewSpec_Rollup) GetMeasuresSelector() *FieldSelector { return nil } +func (x *MetricsViewSpec_Rollup) GetDataTimeRange() string { + if x != nil { + return x.DataTimeRange + } + return "" +} + var File_rill_runtime_v1_resources_proto protoreflect.FileDescriptor var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ @@ -7964,7 +7986,7 @@ var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xe1, 0x20, 0x0a, 0x0f, 0x4d, 0x65, 0x74, 0x72, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xb1, 0x21, 0x0a, 0x0f, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, @@ -8049,162 +8071,167 @@ var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x75, 0x70, 0x52, 0x07, 0x72, 0x6f, 0x6c, - 0x6c, 0x75, 0x70, 0x73, 0x1a, 0xd9, 0x04, 0x0a, 0x09, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x42, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, - 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, - 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, + 0x6c, 0x75, 0x70, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, + 0x61, 0x74, 0x61, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0xd9, 0x04, 0x0a, + 0x09, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x42, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x72, + 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, + 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, + 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x6e, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x6e, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, + 0x72, 0x69, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x21, 0x0a, + 0x0c, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x54, 0x61, 0x62, 0x6c, 0x65, + 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x6f, 0x6f, + 0x6b, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x2e, 0x0a, 0x13, + 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, + 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x3a, 0x0a, 0x19, + 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x65, + 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x17, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x45, 0x78, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4a, 0x0a, 0x13, 0x73, 0x6d, 0x61, 0x6c, + 0x6c, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, + 0x6e, 0x52, 0x11, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x47, + 0x72, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, + 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, + 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x1a, 0x76, 0x0a, 0x11, 0x44, 0x69, 0x6d, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, + 0x6e, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, + 0x1a, 0xa7, 0x01, 0x0a, 0x0d, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x57, 0x69, 0x6e, 0x64, + 0x6f, 0x77, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x4d, 0x0a, 0x08, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, + 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x12, + 0x29, 0x0a, 0x10, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x66, 0x72, 0x61, 0x6d, 0x65, + 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xa5, 0x06, 0x0a, 0x07, 0x4d, + 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, - 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x65, - 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x75, - 0x6e, 0x6e, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x6e, - 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x21, 0x0a, 0x0c, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, - 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6c, 0x6f, 0x6f, - 0x6b, 0x75, 0x70, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x6f, 0x6f, 0x6b, - 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x43, 0x6f, - 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x11, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x43, 0x6f, - 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x3a, 0x0a, 0x19, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x44, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x4a, 0x0a, 0x13, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x5f, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, - 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x52, 0x11, 0x73, 0x6d, 0x61, 0x6c, 0x6c, - 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x09, - 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, - 0x1a, 0x76, 0x0a, 0x11, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x74, 0x69, 0x6d, - 0x65, 0x5f, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, - 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x47, - 0x72, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0xa7, 0x01, 0x0a, 0x0d, 0x4d, 0x65, 0x61, - 0x73, 0x75, 0x72, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x61, - 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, - 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x08, 0x6f, 0x72, 0x64, 0x65, - 0x72, 0x5f, 0x62, 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x69, 0x6c, - 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, - 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x07, - 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x72, 0x61, 0x6d, 0x65, - 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x1a, 0xa5, 0x06, 0x0a, 0x07, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, - 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x65, - 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x72, 0x69, 0x6c, 0x6c, - 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4d, 0x65, 0x61, 0x73, - 0x75, 0x72, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x46, 0x0a, - 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, - 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, - 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x06, 0x77, - 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x59, 0x0a, 0x0e, 0x70, 0x65, 0x72, 0x5f, 0x64, 0x69, 0x6d, - 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, - 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, - 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x52, 0x0d, 0x70, 0x65, 0x72, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x63, 0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x69, 0x6d, - 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, - 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, - 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x52, 0x12, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x44, 0x69, 0x6d, 0x65, 0x6e, - 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x4d, 0x65, - 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x5f, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x64, 0x33, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x44, 0x33, 0x12, 0x41, 0x0a, 0x10, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x5f, 0x64, 0x33, 0x5f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x44, 0x33, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x65, 0x12, 0x33, 0x0a, 0x16, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x6f, 0x66, 0x5f, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x4f, 0x66, 0x54, 0x6f, 0x74, 0x61, 0x6c, - 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x72, 0x65, 0x61, 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x73, 0x5f, - 0x61, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x65, 0x61, 0x74, 0x4e, - 0x75, 0x6c, 0x6c, 0x73, 0x41, 0x73, 0x12, 0x32, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x69, 0x6c, 0x6c, - 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x79, 0x70, 0x65, - 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x1a, 0xdd, 0x02, 0x0a, 0x0a, 0x41, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x64, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x62, - 0x61, 0x73, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, - 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, - 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x11, 0x6d, 0x65, 0x61, 0x73, - 0x75, 0x72, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, - 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x52, 0x10, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x68, 0x61, 0x73, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x5f, 0x65, 0x6e, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x68, 0x61, 0x73, - 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x68, 0x61, 0x73, 0x5f, 0x64, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x68, - 0x61, 0x73, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0xab, 0x03, 0x0a, 0x06, 0x52, - 0x6f, 0x6c, 0x6c, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, - 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x73, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x61, 0x74, 0x61, - 0x62, 0x61, 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, - 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, - 0x72, 0x61, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, - 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, - 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x1e, - 0x0a, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, - 0x0a, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x13, 0x64, 0x69, - 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, - 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x12, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, - 0x6f, 0x6e, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x4b, 0x0a, 0x11, 0x6d, - 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, - 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x1a, 0x42, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x72, + 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x2c, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, + 0x70, 0x65, 0x63, 0x2e, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x46, 0x0a, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, + 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x57, + 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x59, 0x0a, + 0x0e, 0x70, 0x65, 0x72, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, + 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0d, 0x70, 0x65, 0x72, 0x44, 0x69, + 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x63, 0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, + 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x12, 0x72, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x64, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2f, 0x0a, + 0x13, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x61, 0x73, + 0x75, 0x72, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x23, + 0x0a, 0x0d, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x50, 0x72, 0x65, + 0x73, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x64, 0x33, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x44, 0x33, + 0x12, 0x41, 0x0a, 0x10, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x64, 0x33, 0x5f, 0x6c, 0x6f, + 0x63, 0x61, 0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x52, 0x0e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x44, 0x33, 0x4c, 0x6f, 0x63, + 0x61, 0x6c, 0x65, 0x12, 0x33, 0x0a, 0x16, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x70, 0x65, 0x72, + 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x6f, 0x66, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x13, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x4f, 0x66, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x72, 0x65, 0x61, + 0x74, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x73, 0x5f, 0x61, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x74, 0x72, 0x65, 0x61, 0x74, 0x4e, 0x75, 0x6c, 0x6c, 0x73, 0x41, 0x73, 0x12, 0x32, + 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, + 0x70, 0x65, 0x1a, 0xdd, 0x02, 0x0a, 0x0a, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, + 0x27, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, + 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, + 0x12, 0x4b, 0x0a, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, + 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, 0x6d, 0x65, 0x61, + 0x73, 0x75, 0x72, 0x65, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x20, 0x0a, + 0x0c, 0x68, 0x61, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, 0x6e, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0a, 0x68, 0x61, 0x73, 0x54, 0x69, 0x6d, 0x65, 0x45, 0x6e, 0x64, 0x12, + 0x21, 0x0a, 0x0c, 0x68, 0x61, 0x73, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x68, 0x61, 0x73, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0xd3, 0x03, 0x0a, 0x06, 0x52, 0x6f, 0x6c, 0x6c, 0x75, 0x70, 0x12, 0x1a, 0x0a, + 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x61, 0x74, + 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x39, + 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x52, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x5f, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, + 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x69, 0x6d, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, + 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x13, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, + 0x12, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x4b, 0x0a, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x5f, + 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, + 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, + 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x54, + 0x69, 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x42, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, diff --git a/proto/gen/rill/runtime/v1/resources.pb.validate.go b/proto/gen/rill/runtime/v1/resources.pb.validate.go index abf5274e6ee5..8d5f28450a18 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.validate.go +++ b/proto/gen/rill/runtime/v1/resources.pb.validate.go @@ -3439,6 +3439,8 @@ func (m *MetricsViewSpec) validate(all bool) error { } + // no validation rules for DataTimeRange + if m.CacheEnabled != nil { // no validation rules for CacheEnabled } @@ -13606,6 +13608,8 @@ func (m *MetricsViewSpec_Rollup) validate(all bool) error { } } + // no validation rules for DataTimeRange + if len(errors) > 0 { return MetricsViewSpec_RollupMultiError(errors) } diff --git a/proto/gen/rill/runtime/v1/runtime.swagger.yaml b/proto/gen/rill/runtime/v1/runtime.swagger.yaml index 83f81a5b39f3..fa91d08798ad 100644 --- a/proto/gen/rill/runtime/v1/runtime.swagger.yaml +++ b/proto/gen/rill/runtime/v1/runtime.swagger.yaml @@ -3871,6 +3871,12 @@ definitions: measuresSelector: $ref: '#/definitions/v1FieldSelector' description: Dynamic selector for `measures`. Will be processed during validation, so it will always be empty in `state.valid_spec`. + dataTimeRange: + type: string + description: |- + Optional rilltime expression describing the time range covered by the rollup. + When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. description: |- Pre-aggregated rollup that can be used to accelerate queries. The system automatically routes queries to a rollup when the query can be satisfied from the pre-aggregated data. @@ -6519,6 +6525,12 @@ definitions: items: type: object $ref: '#/definitions/MetricsViewSpecRollup' + dataTimeRange: + type: string + description: |- + Optional rilltime expression describing the time range covered by the base table. + When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. v1MetricsViewSpecAnnotation: type: object properties: diff --git a/proto/rill/runtime/v1/resources.proto b/proto/rill/runtime/v1/resources.proto index 7deaa1fea325..605aca36a1a7 100644 --- a/proto/rill/runtime/v1/resources.proto +++ b/proto/rill/runtime/v1/resources.proto @@ -318,6 +318,10 @@ message MetricsViewSpec { FieldSelector dimensions_selector = 9; // Dynamic selector for `measures`. Will be processed during validation, so it will always be empty in `state.valid_spec`. FieldSelector measures_selector = 10; + // Optional rilltime expression describing the time range covered by the rollup. + // When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + string data_time_range = 11; } // Connector containing the table string connector = 1; @@ -372,6 +376,10 @@ message MetricsViewSpec { // Keys and values are stored as templates and will be resolved at query time. map query_attributes = 33; repeated Rollup rollups = 34; + // Optional rilltime expression describing the time range covered by the base table. + // When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + string data_time_range = 36; } message SecurityRule { diff --git a/runtime/metricsview/executor/executor.go b/runtime/metricsview/executor/executor.go index 79916246082b..8157f87375b3 100644 --- a/runtime/metricsview/executor/executor.go +++ b/runtime/metricsview/executor/executor.go @@ -17,6 +17,7 @@ import ( "github.com/rilldata/rill/runtime/metricsview" "github.com/rilldata/rill/runtime/parser" "github.com/rilldata/rill/runtime/pkg/jsonval" + "github.com/rilldata/rill/runtime/pkg/rilltime" ) const ( @@ -187,17 +188,36 @@ func (e *Executor) Timestamps(ctx context.Context, timeDim string) (metricsview. } mv := e.metricsView - res, err := e.resolveTimestampsForTable(ctx, mv.Database, mv.DatabaseSchema, mv.Table, timeExpr, mv.WatermarkExpression) - if err != nil { - return metricsview.TimestampsResult{}, err + + var res metricsview.TimestampsResult + if timeDim == mv.TimeDimension && mv.DataTimeRange != "" { + // Use the declared base data_time_range and skip the OLAP probe. + res, err = e.resolveDeclaredTimestamps(mv.DataTimeRange) + if err != nil { + return metricsview.TimestampsResult{}, fmt.Errorf(`failed to resolve "data_time_range": %w`, err) + } + } else { + res, err = e.resolveTimestampsForTable(ctx, mv.Database, mv.DatabaseSchema, mv.Table, timeExpr, mv.WatermarkExpression) + if err != nil { + return metricsview.TimestampsResult{}, err + } } res.Now = time.Now() - // For the primary time dimension, also resolve rollup table timestamps + // For the primary time dimension, also resolve rollup table timestamps. + // Per-rollup data_time_range short-circuits the OLAP probe for that rollup. if timeDim == mv.TimeDimension && len(mv.Rollups) > 0 { res.Rollups = make(map[string]metricsview.TimestampsResult, len(mv.Rollups)) for _, rollup := range mv.Rollups { + if rollup.DataTimeRange != "" { + rts, err := e.resolveDeclaredTimestamps(rollup.DataTimeRange) + if err != nil { + return metricsview.TimestampsResult{}, fmt.Errorf(`failed to resolve "data_time_range" for rollup %q: %w`, rollup.Table, err) + } + res.Rollups[rollup.Table] = rts + continue + } rts, err := e.resolveTimestampsForTable(ctx, rollup.Database, rollup.DatabaseSchema, rollup.Table, timeExpr, "") if err != nil { return metricsview.TimestampsResult{}, fmt.Errorf("failed to resolve timestamps for rollup %q: %w", rollup.Table, err) @@ -211,6 +231,30 @@ func (e *Executor) Timestamps(ctx context.Context, timeDim string) (metricsview. return res, nil } +// resolveDeclaredTimestamps evaluates a rilltime expression against synthetic +// anchors (now=time.Now(), earliest=zero, latest/watermark=time.Now()) so that +// a declared data_time_range can supply table bounds without probing the OLAP. +func (e *Executor) resolveDeclaredTimestamps(expr string) (metricsview.TimestampsResult, error) { + rt, err := rilltime.Parse(expr, rilltime.ParseOptions{}) + if err != nil { + return metricsview.TimestampsResult{}, err + } + now := time.Now() + start, end, _ := rt.Eval(rilltime.EvalOptions{ + Now: now, + MinTime: time.Time{}, + MaxTime: now, + Watermark: now, + FirstDay: int(e.metricsView.FirstDayOfWeek), + FirstMonth: int(e.metricsView.FirstMonthOfYear), + }) + return metricsview.TimestampsResult{ + Min: start, + Max: end, + Watermark: end, + }, nil +} + // BindQuery allows to set min, max and watermark from a cache. func (e *Executor) BindQuery(qry *metricsview.Query, timestamps metricsview.TimestampsResult) error { err := qry.Validate() diff --git a/runtime/metricsview/executor/executor_rewrite_rollup.go b/runtime/metricsview/executor/executor_rewrite_rollup.go index 7f6993549c00..9805bdf2e8d7 100644 --- a/runtime/metricsview/executor/executor_rewrite_rollup.go +++ b/runtime/metricsview/executor/executor_rewrite_rollup.go @@ -67,7 +67,8 @@ const ( // to the rollup grain (to prevent the last bucket from pulling in extra data). // // 4. Selection: among eligible rollups, prefer the coarsest grain (fewer rows to scan). -// On ties, prefer the rollup with the smallest data range (tighter coverage). +// On ties, prefer the rollup defined earlier in the metrics view's rollups list — authors use this +// to express priority among same-grain rollups with overlapping dimensions. // // The selected rollup is returned as a synthetic MetricsViewSpec that points to the rollup table. // The caller uses this spec to build the query AST, so the rest of the query pipeline remains same. @@ -76,7 +77,7 @@ const ( type rollupCandidate struct { rollup *runtimev1.MetricsViewSpec_Rollup grainOrder int - dataRange time.Duration // max - min; 0 if no time dimension + index int // position in MetricsViewSpec.Rollups; used as the secondary tiebreaker (earlier wins) } // rewriteQueryForRollup checks if a rollup table can satisfy the query. @@ -148,7 +149,7 @@ func (e *Executor) rewriteQueryForRollup(ctx context.Context, qry *metricsview.Q tsFetched := false var best *rollupCandidate - for _, rollup := range e.metricsView.Rollups { + for i, rollup := range e.metricsView.Rollups { if rollup.Table == "" { return nil, fmt.Errorf("rollup for model %q has no resolved table", rollup.Model) } @@ -277,17 +278,16 @@ func (e *Executor) rewriteQueryForRollup(ctx context.Context, qry *metricsview.Q candidateSpan.SetAttributes(attribute.String("rollup.eligible", "true")) candidateSpan.End() - dataRange := rollupMax.Sub(rollupMin) c := &rollupCandidate{ rollup: rollup, grainOrder: grainOrder[rollup.TimeGrain], - dataRange: dataRange, + index: i, } - // Selection priority: coarsest grain (primary); smallest data range (secondary tiebreaker) + // Selection priority: coarsest grain (primary); earlier definition order (secondary tiebreaker) if best == nil || c.grainOrder > best.grainOrder { best = c - } else if c.grainOrder == best.grainOrder && c.dataRange > 0 && (best.dataRange == 0 || c.dataRange < best.dataRange) { + } else if c.grainOrder == best.grainOrder && c.index < best.index { best = c } } diff --git a/runtime/metricsview/executor/executor_rollup_integration_test.go b/runtime/metricsview/executor/executor_rollup_integration_test.go index 8333b7a37653..c6bdb3fdeeb3 100644 --- a/runtime/metricsview/executor/executor_rollup_integration_test.go +++ b/runtime/metricsview/executor/executor_rollup_integration_test.go @@ -543,8 +543,8 @@ explore: require.Equal(t, rollupTestDailyTable, table) }) - t.Run("prefer_smallest_data_range", func(t *testing.T) { - // Two monthly rollups: wide (Jan-Mar) and narrow (Feb-Mar); query Feb-Apr picks narrow + t.Run("prefer_definition_order", func(t *testing.T) { + // Two monthly rollups, both eligible for the query. The earlier one in the rollups list wins. files := map[string]string{ "rill.yaml": "", "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], @@ -605,8 +605,8 @@ explore: }, } table := queryAndGetTable(t, e, qry) - // Both monthly rollups cover the range; narrow has smaller data range - require.Equal(t, "rollup_month_narrow", table) + // Both monthly rollups cover the range; wide is defined first so it wins under the definition-order tiebreaker. + require.Equal(t, "rollup_month_wide", table) }) }) @@ -746,6 +746,182 @@ explore: }) }) + t.Run("data_time_range", func(t *testing.T) { + t.Run("hot_cold_split", func(t *testing.T) { + // Two day-grain rollups with disjoint declared ranges. Distinct physical tables so the + // selector picks the one whose declared range covers the query. + files := map[string]string{ + "rill.yaml": "", + "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], + "models/rollup_day_hot.sql": ` +SELECT date_trunc('day', timestamp) AS timestamp, publisher, domain, + SUM(impressions) AS impressions, SUM(clicks) AS clicks +FROM base_events GROUP BY 1, 2, 3`, + "models/rollup_day_cold.sql": ` +SELECT date_trunc('day', timestamp) AS timestamp, publisher, domain, + SUM(impressions) AS impressions, SUM(clicks) AS clicks +FROM base_events GROUP BY 1, 2, 3`, + "metrics_views/mv.yaml": ` +type: metrics_view +version: 1 +model: base_events +timeseries: timestamp +data_time_range: "2024-01-01 to 2024-04-01" +dimensions: + - name: publisher + column: publisher + - name: domain + column: domain +measures: + - name: total_impressions + expression: 'SUM("impressions")' +rollups: + - model: rollup_day_hot + time_grain: day + data_time_range: "2024-02-01 to 2024-04-01" + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_day_cold + time_grain: day + data_time_range: "2024-01-01 to 2024-02-01" + dimensions: [publisher, domain] + measures: [total_impressions] +explore: + skip: true`, + } + customRT, customID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{Files: files}) + testruntime.RequireReconcileState(t, customRT, customID, 5, 0, 0) + e := newRollupTestExecutor(t, customRT, customID) + defer e.Close() + + // Query inside the hot window (Feb-Mar) → hot's declared range covers it; cold's does not. + hotQry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_day_hot", queryAndGetTable(t, e, hotQry)) + + // Query inside the cold window (Jan) → only cold's declared range covers it. + coldQry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_day_cold", queryAndGetTable(t, e, coldQry)) + + // Query straddling both windows → neither rollup covers the full range; falls back to base. + straddleQry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, rollupTestBaseTable, queryAndGetTable(t, e, straddleQry)) + }) + + t.Run("paca_priority", func(t *testing.T) { + // Three day-grain rollups with overlapping dimensions. A publisher-only query is eligible + // against all three; definition order picks the first (least-granular by dim count). + // A publisher+domain query is only eligible against the latter two; definition order picks the second. + files := map[string]string{ + "rill.yaml": "", + "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], + "models/rollup_pub.sql": ` +SELECT date_trunc('day', timestamp) AS timestamp, publisher, + SUM(impressions) AS impressions, SUM(clicks) AS clicks +FROM base_events GROUP BY 1, 2`, + "models/rollup_pubdom.sql": ` +SELECT date_trunc('day', timestamp) AS timestamp, publisher, domain, + SUM(impressions) AS impressions, SUM(clicks) AS clicks +FROM base_events GROUP BY 1, 2, 3`, + "models/rollup_pubdomctry.sql": ` +SELECT date_trunc('day', timestamp) AS timestamp, publisher, domain, country, + SUM(impressions) AS impressions, SUM(clicks) AS clicks +FROM base_events GROUP BY 1, 2, 3, 4`, + "metrics_views/mv.yaml": ` +type: metrics_view +version: 1 +model: base_events +timeseries: timestamp +data_time_range: "2024-01-01 to 2024-04-01" +dimensions: + - name: publisher + column: publisher + - name: domain + column: domain + - name: country + column: country +measures: + - name: total_impressions + expression: 'SUM("impressions")' +rollups: + - model: rollup_pub + time_grain: day + data_time_range: "2024-01-01 to 2024-04-01" + dimensions: [publisher] + measures: [total_impressions] + - model: rollup_pubdom + time_grain: day + data_time_range: "2024-01-01 to 2024-04-01" + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_pubdomctry + time_grain: day + data_time_range: "2024-01-01 to 2024-04-01" + dimensions: [publisher, domain, country] + measures: [total_impressions] +explore: + skip: true`, + } + customRT, customID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{Files: files}) + testruntime.RequireReconcileState(t, customRT, customID, 6, 0, 0) + e := newRollupTestExecutor(t, customRT, customID) + defer e.Close() + + pubQry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "publisher"}, + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_pub", queryAndGetTable(t, e, pubQry)) + + pubDomQry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "publisher"}, + {Name: "domain"}, + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_pubdom", queryAndGetTable(t, e, pubDomQry)) + }) + }) + t.Run("correctness", func(t *testing.T) { t.Run("daily_agg_correctness", func(t *testing.T) { e := newRollupTestExecutor(t, rt, instanceID) diff --git a/runtime/parser/parse_metrics_view.go b/runtime/parser/parse_metrics_view.go index 46a788a95374..c7a5d2298201 100644 --- a/runtime/parser/parse_metrics_view.go +++ b/runtime/parser/parse_metrics_view.go @@ -90,7 +90,9 @@ type MetricsViewYAML struct { TimeZone string `yaml:"time_zone"` Dimensions *FieldSelectorYAML `yaml:"dimensions"` Measures *FieldSelectorYAML `yaml:"measures"` + DataTimeRange string `yaml:"data_time_range"` } `yaml:"rollups"` + DataTimeRange string `yaml:"data_time_range"` Security *SecurityPolicyYAML QueryAttributes map[string]string `yaml:"query_attributes"` Cache struct { @@ -308,6 +310,13 @@ func (p *Parser) parseMetricsView(node *Node) error { } } + if tmp.DataTimeRange != "" { + _, err := rilltime.Parse(tmp.DataTimeRange, rilltime.ParseOptions{}) + if err != nil { + return fmt.Errorf(`invalid "data_time_range": %w`, err) + } + } + for _, tz := range tmp.AvailableTimeZones { _, err := time.LoadLocation(tz) if err != nil { @@ -773,6 +782,11 @@ func (p *Parser) parseMetricsView(node *Node) error { return fmt.Errorf(`rollup[%d]: invalid "time_zone" %q: %w`, i, rollup.TimeZone, err) } } + if rollup.DataTimeRange != "" { + if _, err := rilltime.Parse(rollup.DataTimeRange, rilltime.ParseOptions{}); err != nil { + return fmt.Errorf(`rollup[%d]: invalid "data_time_range": %w`, i, err) + } + } // Validate and resolve dimensions var dims []string var dimsSelector *runtimev1.FieldSelector @@ -812,6 +826,7 @@ func (p *Parser) parseMetricsView(node *Node) error { DimensionsSelector: dimsSelector, Measures: measures, MeasuresSelector: measSelector, + DataTimeRange: rollup.DataTimeRange, }) node.Refs = append(node.Refs, ResourceName{Name: rollup.Model}) } @@ -864,6 +879,7 @@ func (p *Parser) parseMetricsView(node *Node) error { spec.AiInstructions = tmp.AIInstructions spec.TimeDimension = tmp.TimeDimension spec.WatermarkExpression = tmp.Watermark + spec.DataTimeRange = tmp.DataTimeRange spec.SmallestTimeGrain = smallestTimeGrain spec.FirstDayOfWeek = tmp.FirstDayOfWeek spec.FirstMonthOfYear = tmp.FirstMonthOfYear diff --git a/runtime/parser/parse_metrics_view_test.go b/runtime/parser/parse_metrics_view_test.go index 57afcabd5a6c..660ae2f7b61b 100644 --- a/runtime/parser/parse_metrics_view_test.go +++ b/runtime/parser/parse_metrics_view_test.go @@ -814,6 +814,45 @@ rollups: `, wantErr: `invalid "time_zone"`, }, + { + name: "invalid rollup data_time_range", + yaml: ` +type: metrics_view +version: 1 +model: m1 +timeseries: id +dimensions: +- name: publisher + column: publisher +measures: +- name: count + expression: "COUNT(*)" +rollups: + - model: r1 + time_grain: day + measures: + - count + data_time_range: "not a rilltime" +`, + wantErr: `invalid "data_time_range"`, + }, + { + name: "invalid metrics view data_time_range", + yaml: ` +type: metrics_view +version: 1 +model: m1 +timeseries: id +data_time_range: "garbage" +dimensions: +- name: publisher + column: publisher +measures: +- name: count + expression: "COUNT(*)" +`, + wantErr: `invalid "data_time_range"`, + }, } for _, tt := range tests { @@ -832,3 +871,50 @@ rollups: }) } } + +func TestMetricsViewDataTimeRange(t *testing.T) { + files := map[string]string{ + `rill.yaml`: ``, + `models/m1.sql`: `SELECT 1 AS id, 'a' AS publisher`, + `models/rollup_daily.sql`: `SELECT 1 AS id`, + `metrics_views/mv1.yaml`: ` +type: metrics_view +version: 1 +model: m1 +timeseries: id +data_time_range: -5Y to now +dimensions: +- name: publisher + column: publisher +measures: +- name: total_impressions + expression: "SUM(impressions)" +rollups: + - model: rollup_daily + time_grain: day + data_time_range: -1Y to now + dimensions: + - publisher + measures: + - total_impressions +`, + } + + ctx := context.Background() + repo := makeRepo(t, files) + p, err := Parse(ctx, repo, "", "", "duckdb", true) + require.NoError(t, err) + require.Empty(t, p.Errors) + + var mvSpec *runtimev1.MetricsViewSpec + for _, r := range p.Resources { + if r.Name.Kind == ResourceKindMetricsView && r.Name.Name == "mv1" { + mvSpec = r.MetricsViewSpec + break + } + } + require.NotNil(t, mvSpec) + require.Equal(t, "-5Y to now", mvSpec.DataTimeRange) + require.Len(t, mvSpec.Rollups, 1) + require.Equal(t, "-1Y to now", mvSpec.Rollups[0].DataTimeRange) +} diff --git a/runtime/parser/schema/project.schema.yaml b/runtime/parser/schema/project.schema.yaml index 3c9dc15b8d13..8536f676bea1 100644 --- a/runtime/parser/schema/project.schema.yaml +++ b/runtime/parser/schema/project.schema.yaml @@ -2066,6 +2066,9 @@ definitions: watermark: type: string description: A SQL expression that tells us the max timestamp that the measures are considered valid for. Usually does not need to be overwritten + data_time_range: + type: string + description: 'Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table''s time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks.' smallest_time_grain: type: string description: 'Refers to the smallest time granularity the user is allowed to view. The valid values are: millisecond, second, minute, hour, day, week, month, quarter, year' @@ -2290,6 +2293,9 @@ definitions: measures: $ref: '#/definitions/field_selector_properties' description: Optional field selectors for measures to include in the rollup from the base metrics view. If not specified, all measures are included. + data_time_range: + type: string + description: 'Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the rollup''s time coverage (e.g. `-1Y to now`, `-5Y to -1Y`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for this rollup and uses the declared bounds for coverage checks.' required: - model - time_grain diff --git a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts index 0ad5ffeb7633..e65ce7cd7122 100644 --- a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts +++ b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts @@ -1548,6 +1548,15 @@ export class MetricsViewSpec extends Message { */ rollups: MetricsViewSpec_Rollup[] = []; + /** + * Optional rilltime expression describing the time range covered by the base table. + * When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + * Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + * + * @generated from field: string data_time_range = 36; + */ + dataTimeRange = ""; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -1582,6 +1591,7 @@ export class MetricsViewSpec extends Message { { no: 35, name: "cache_timestamps_ttl_seconds", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, { no: 33, name: "query_attributes", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} }, { no: 34, name: "rollups", kind: "message", T: MetricsViewSpec_Rollup, repeated: true }, + { no: 36, name: "data_time_range", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): MetricsViewSpec { @@ -2224,6 +2234,15 @@ export class MetricsViewSpec_Rollup extends Message { */ measuresSelector?: FieldSelector; + /** + * Optional rilltime expression describing the time range covered by the rollup. + * When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + * Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + * + * @generated from field: string data_time_range = 11; + */ + dataTimeRange = ""; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -2242,6 +2261,7 @@ export class MetricsViewSpec_Rollup extends Message { { no: 8, name: "measures", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 9, name: "dimensions_selector", kind: "message", T: FieldSelector }, { no: 10, name: "measures_selector", kind: "message", T: FieldSelector }, + { no: 11, name: "data_time_range", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): MetricsViewSpec_Rollup { From 3a937dbde865387f7a8a67bc2c3b6b30ca0d0d3f Mon Sep 17 00:00:00 2001 From: Parag Jain Date: Fri, 22 May 2026 17:02:24 +0530 Subject: [PATCH 2/6] self review --- .../developers/build/metrics-view/rollups.md | 15 +- .../reference/project-files/metrics-views.md | 2 +- proto/gen/rill/runtime/v1/resources.pb.go | 206 +++++++++--------- .../rill/runtime/v1/resources.pb.validate.go | 8 +- .../gen/rill/runtime/v1/runtime.swagger.yaml | 24 +- proto/rill/runtime/v1/resources.proto | 16 +- runtime/metricsview/executor/executor.go | 1 - .../executor/executor_rewrite_rollup.go | 4 +- .../proto/gen/rill/runtime/v1/resources_pb.ts | 40 ++-- 9 files changed, 157 insertions(+), 159 deletions(-) diff --git a/docs/docs/developers/build/metrics-view/rollups.md b/docs/docs/developers/build/metrics-view/rollups.md index 5a5f4c3148c0..81adba1687fb 100644 --- a/docs/docs/developers/build/metrics-view/rollups.md +++ b/docs/docs/developers/build/metrics-view/rollups.md @@ -72,7 +72,7 @@ rollups: ### Declaring Coverage with `data_time_range` -By default Rill discovers a rollup's time coverage at query time by running `SELECT min(time), max(time)` against the rollup table (cached). If you'd rather declare coverage statically — to skip the probe, or to scope a rollup to a specific window — set `data_time_range` on the rollup. The value is a [rilltime expression](/reference/time-syntax/time-syntax): +By default Rill discovers a rollup's time coverage at query time by running `SELECT min(time), max(time)` against the rollup table. If you'd rather declare coverage statically — to skip the probe, or to scope a rollup to a specific window — set `data_time_range` on the rollup. The value is a [rilltime expression](/reference/time-syntax/time-syntax): ```yaml rollups: @@ -96,7 +96,7 @@ You can declare coverage on the metrics view itself the same way — this skips data_time_range: -5Y to now ``` -When `data_time_range` is set, the rilltime expression is resolved against synthetic anchors at query time: `now`/`latest`/`watermark` all resolve to the current wallclock, and `earliest` resolves to the zero epoch. So `inf` (a shorthand for `earliest to latest`) declares "all time from zero epoch to now." Mixing declared and undeclared rollups in the same metrics view is fine — each table independently decides whether to probe or to trust its declaration. +When `data_time_range` is set, the rilltime expression is resolved against fixed anchors at query time: `now`/`latest`/`watermark` all resolve to the current wallclock, and `earliest` resolves to the zero epoch. So `inf` (a shorthand for `earliest to latest`) declares "all time from zero epoch to now." Mixing declared and undeclared rollups in the same metrics view is fine — each table independently decides whether to probe or to use its declaration. ### Selection Priority and Definition Order @@ -105,21 +105,21 @@ When two rollups have the same time grain and both could answer a query, the **f ```yaml rollups: # Same grain, different dimension sets. Priority is declared by order. - - model: rollup_advertiser # selected for queries that only need publisher + - model: events_daily_narrow # selected for queries that only need publisher time_grain: day dimensions: [publisher] measures: [total_impressions] - - model: rollup_campaign # selected when publisher + domain are queried + - model: events_daily_wide # selected when publisher + domain are queried time_grain: day dimensions: [publisher, domain] measures: [total_impressions] - - model: rollup_adgroup # selected when publisher + domain + country are queried + - model: events_daily_wider # selected when publisher + domain + country are queried time_grain: day dimensions: [publisher, domain, country] measures: [total_impressions] ``` -A query on `publisher` alone is eligible against all three — `rollup_advertiser` wins because it's listed first (and scans the fewest rows). A query on `publisher` + `domain` knocks `rollup_advertiser` out of eligibility, so `rollup_campaign` wins. +A query on `publisher` alone is eligible against all three — `events_daily_narrow` wins because it's listed first. A query on `publisher` + `domain` knocks `events_daily_narrow` out of eligibility, so `events_daily_wide` wins. ### Configuration Reference @@ -182,8 +182,7 @@ The base table is used if no rollup is eligible. - **Rollups require a `timeseries`.** Metrics views without a primary time dimension cannot define rollups. - **Filters on missing dimensions disqualify the rollup.** A WHERE clause on `country` will skip a rollup that doesn't include `country`, even if the query's group-by columns are all in the rollup. - **The rollup is responsible for being correct.** Rill does not validate that the rollup's measure values are consistent with the base — it trusts the model. If the rollup model uses the wrong aggregation (e.g. `AVG` where the base measure is `SUM`), queries routed to it will return wrong numbers. -- **Rollups are assumed to be roughly caught up with the base table.** Coverage is measured against the base table's latest timestamp. A rollup that lags behind the base will be silently skipped for any query that reaches the tail of the data — including common "last 24 hours" queries and queries without a time range — even if it has the right grain, dimensions, and measures. Refresh rollups in step with the base model so selection actually happens. (If you can't, declare a `data_time_range` so the rollup advertises its own bounds instead of being measured against the base.) -- **Declared `data_time_range`s drift with `time.Now()`.** Rilltime expressions are anchored to wallclock at query time. If your data actually lags or jumps ahead of what you declared, declared and real bounds diverge silently — queries can route to a rollup whose data stops short. Treat declared coverage as a contract you maintain alongside the rollup model. +- **Rollups are assumed to be roughly caught up with the base table.** Coverage is measured against the base table's latest timestamp. A rollup that lags behind the base will be silently skipped for any query that reaches the tail of the data — including common "last 24 hours" queries and queries without a time range — even if it has the right grain, dimensions, and measures. Refresh rollups in step with the base model so selection actually happens. :::info The full configuration schema is in the [metrics view reference](/reference/project-files/metrics-views#rollups). diff --git a/docs/docs/reference/project-files/metrics-views.md b/docs/docs/reference/project-files/metrics-views.md index b3026a276bc1..aa37847f8c5d 100644 --- a/docs/docs/reference/project-files/metrics-views.md +++ b/docs/docs/reference/project-files/metrics-views.md @@ -62,7 +62,7 @@ _[string]_ - A SQL expression that tells us the max timestamp that the measures ### `data_time_range` -_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks. +_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the model/table and uses the declared bounds for coverage checks. ### `smallest_time_grain` diff --git a/proto/gen/rill/runtime/v1/resources.pb.go b/proto/gen/rill/runtime/v1/resources.pb.go index 338f0dd1bfde..541ec70dee5f 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.go +++ b/proto/gen/rill/runtime/v1/resources.pb.go @@ -1990,6 +1990,10 @@ type MetricsViewSpec struct { SmallestTimeGrain TimeGrain `protobuf:"varint,8,opt,name=smallest_time_grain,json=smallestTimeGrain,proto3,enum=rill.runtime.v1.TimeGrain" json:"smallest_time_grain,omitempty"` // Expression to evaluate a watermark for the metrics view. If not set, the watermark defaults to max(time_dimension). WatermarkExpression string `protobuf:"bytes,20,opt,name=watermark_expression,json=watermarkExpression,proto3" json:"watermark_expression,omitempty"` + // Optional rilltime expression describing the time range covered by the base table. + // When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + DataTimeRange string `protobuf:"bytes,36,opt,name=data_time_range,json=dataTimeRange,proto3" json:"data_time_range,omitempty"` // Dimensions in the metrics view Dimensions []*MetricsViewSpec_Dimension `protobuf:"bytes,6,rep,name=dimensions,proto3" json:"dimensions,omitempty"` // Measures in the metrics view @@ -2020,10 +2024,6 @@ type MetricsViewSpec struct { // Keys and values are stored as templates and will be resolved at query time. QueryAttributes map[string]string `protobuf:"bytes,33,rep,name=query_attributes,json=queryAttributes,proto3" json:"query_attributes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Rollups []*MetricsViewSpec_Rollup `protobuf:"bytes,34,rep,name=rollups,proto3" json:"rollups,omitempty"` - // Optional rilltime expression describing the time range covered by the base table. - // When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. - DataTimeRange string `protobuf:"bytes,36,opt,name=data_time_range,json=dataTimeRange,proto3" json:"data_time_range,omitempty"` } func (x *MetricsViewSpec) Reset() { @@ -2142,6 +2142,13 @@ func (x *MetricsViewSpec) GetWatermarkExpression() string { return "" } +func (x *MetricsViewSpec) GetDataTimeRange() string { + if x != nil { + return x.DataTimeRange + } + return "" +} + func (x *MetricsViewSpec) GetDimensions() []*MetricsViewSpec_Dimension { if x != nil { return x.Dimensions @@ -2240,13 +2247,6 @@ func (x *MetricsViewSpec) GetRollups() []*MetricsViewSpec_Rollup { return nil } -func (x *MetricsViewSpec) GetDataTimeRange() string { - if x != nil { - return x.DataTimeRange - } - return "" -} - type SecurityRule struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -7484,6 +7484,10 @@ type MetricsViewSpec_Rollup struct { DatabaseSchema string `protobuf:"bytes,2,opt,name=database_schema,json=databaseSchema,proto3" json:"database_schema,omitempty"` Table string `protobuf:"bytes,3,opt,name=table,proto3" json:"table,omitempty"` Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` + // Optional rilltime expression describing the time range covered by the rollup. + // When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + DataTimeRange string `protobuf:"bytes,11,opt,name=data_time_range,json=dataTimeRange,proto3" json:"data_time_range,omitempty"` // Time grain of the rollup. TimeGrain TimeGrain `protobuf:"varint,5,opt,name=time_grain,json=timeGrain,proto3,enum=rill.runtime.v1.TimeGrain" json:"time_grain,omitempty"` // IANA timezone the rollup was aggregated in; defaults to UTC @@ -7496,10 +7500,6 @@ type MetricsViewSpec_Rollup struct { DimensionsSelector *FieldSelector `protobuf:"bytes,9,opt,name=dimensions_selector,json=dimensionsSelector,proto3" json:"dimensions_selector,omitempty"` // Dynamic selector for `measures`. Will be processed during validation, so it will always be empty in `state.valid_spec`. MeasuresSelector *FieldSelector `protobuf:"bytes,10,opt,name=measures_selector,json=measuresSelector,proto3" json:"measures_selector,omitempty"` - // Optional rilltime expression describing the time range covered by the rollup. - // When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. - DataTimeRange string `protobuf:"bytes,11,opt,name=data_time_range,json=dataTimeRange,proto3" json:"data_time_range,omitempty"` } func (x *MetricsViewSpec_Rollup) Reset() { @@ -7562,6 +7562,13 @@ func (x *MetricsViewSpec_Rollup) GetModel() string { return "" } +func (x *MetricsViewSpec_Rollup) GetDataTimeRange() string { + if x != nil { + return x.DataTimeRange + } + return "" +} + func (x *MetricsViewSpec_Rollup) GetTimeGrain() TimeGrain { if x != nil { return x.TimeGrain @@ -7604,13 +7611,6 @@ func (x *MetricsViewSpec_Rollup) GetMeasuresSelector() *FieldSelector { return nil } -func (x *MetricsViewSpec_Rollup) GetDataTimeRange() string { - if x != nil { - return x.DataTimeRange - } - return "" -} - var File_rill_runtime_v1_resources_proto protoreflect.FileDescriptor var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ @@ -8015,65 +8015,65 @@ var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ 0x61, 0x69, 0x6e, 0x12, 0x31, 0x0a, 0x14, 0x77, 0x61, 0x74, 0x65, 0x72, 0x6d, 0x61, 0x72, 0x6b, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x61, 0x74, 0x65, 0x72, 0x6d, 0x61, 0x72, 0x6b, 0x45, 0x78, 0x70, 0x72, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4a, 0x0a, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x69, 0x6c, - 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, - 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x44, 0x0a, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, - 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, - 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x52, 0x08, - 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x65, - 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x1f, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, - 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x52, 0x10, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, - 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x47, 0x0a, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, - 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x20, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, - 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0e, - 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x4d, - 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x1d, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, - 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, - 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x44, 0x0a, - 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, - 0x17, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, - 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, - 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, - 0x6c, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x11, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x64, 0x61, 0x79, - 0x5f, 0x6f, 0x66, 0x5f, 0x77, 0x65, 0x65, 0x6b, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, - 0x66, 0x69, 0x72, 0x73, 0x74, 0x44, 0x61, 0x79, 0x4f, 0x66, 0x57, 0x65, 0x65, 0x6b, 0x12, 0x2d, - 0x0a, 0x13, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x5f, 0x6f, 0x66, - 0x5f, 0x79, 0x65, 0x61, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x66, 0x69, 0x72, - 0x73, 0x74, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x4f, 0x66, 0x59, 0x65, 0x61, 0x72, 0x12, 0x28, 0x0a, - 0x0d, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x19, - 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x61, 0x63, 0x68, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x71, 0x6c, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x63, 0x61, 0x63, 0x68, 0x65, 0x4b, 0x65, 0x79, 0x53, 0x71, 0x6c, 0x12, 0x31, 0x0a, 0x15, 0x63, - 0x61, 0x63, 0x68, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x74, 0x74, 0x6c, 0x5f, 0x73, 0x65, 0x63, - 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x63, 0x61, 0x63, 0x68, - 0x65, 0x4b, 0x65, 0x79, 0x54, 0x74, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x3f, - 0x0a, 0x1c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x73, 0x5f, 0x74, 0x74, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x23, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, 0x63, 0x61, 0x63, 0x68, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x73, 0x54, 0x74, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, - 0x60, 0x0a, 0x10, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x21, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x72, 0x69, 0x6c, 0x6c, - 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x0f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x73, 0x12, 0x41, 0x0a, 0x07, 0x72, 0x6f, 0x6c, 0x6c, 0x75, 0x70, 0x73, 0x18, 0x22, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x64, 0x61, 0x74, 0x61, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4a, + 0x0a, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, - 0x53, 0x70, 0x65, 0x63, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x75, 0x70, 0x52, 0x07, 0x72, 0x6f, 0x6c, - 0x6c, 0x75, 0x70, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, - 0x61, 0x74, 0x61, 0x54, 0x69, 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0xd9, 0x04, 0x0a, + 0x53, 0x70, 0x65, 0x63, 0x2e, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, + 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x44, 0x0a, 0x08, 0x6d, 0x65, + 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x72, + 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4d, + 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, + 0x12, 0x4b, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, + 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x47, 0x0a, + 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, + 0x18, 0x20, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, + 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0e, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4d, 0x65, + 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x1d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x69, + 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, + 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x41, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x44, 0x0a, 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, + 0x79, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x17, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x73, 0x65, + 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x11, 0x66, + 0x69, 0x72, 0x73, 0x74, 0x5f, 0x64, 0x61, 0x79, 0x5f, 0x6f, 0x66, 0x5f, 0x77, 0x65, 0x65, 0x6b, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x66, 0x69, 0x72, 0x73, 0x74, 0x44, 0x61, 0x79, + 0x4f, 0x66, 0x57, 0x65, 0x65, 0x6b, 0x12, 0x2d, 0x0a, 0x13, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, + 0x6d, 0x6f, 0x6e, 0x74, 0x68, 0x5f, 0x6f, 0x66, 0x5f, 0x79, 0x65, 0x61, 0x72, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x10, 0x66, 0x69, 0x72, 0x73, 0x74, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x4f, + 0x66, 0x59, 0x65, 0x61, 0x72, 0x12, 0x28, 0x0a, 0x0d, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x19, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0c, + 0x63, 0x61, 0x63, 0x68, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, + 0x22, 0x0a, 0x0d, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x71, 0x6c, + 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x61, 0x63, 0x68, 0x65, 0x4b, 0x65, 0x79, + 0x53, 0x71, 0x6c, 0x12, 0x31, 0x0a, 0x15, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x6b, 0x65, 0x79, + 0x5f, 0x74, 0x74, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x1b, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x12, 0x63, 0x61, 0x63, 0x68, 0x65, 0x4b, 0x65, 0x79, 0x54, 0x74, 0x6c, 0x53, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x3f, 0x0a, 0x1c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x5f, 0x74, 0x74, 0x6c, 0x5f, 0x73, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, 0x63, 0x61, + 0x63, 0x68, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x54, 0x74, 0x6c, + 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x60, 0x0a, 0x10, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x21, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x35, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, + 0x70, 0x65, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x41, 0x0a, 0x07, 0x72, 0x6f, 0x6c, + 0x6c, 0x75, 0x70, 0x73, 0x18, 0x22, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x69, 0x6c, + 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, + 0x72, 0x69, 0x63, 0x73, 0x56, 0x69, 0x65, 0x77, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x52, 0x6f, 0x6c, + 0x6c, 0x75, 0x70, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x6c, 0x75, 0x70, 0x73, 0x1a, 0xd9, 0x04, 0x0a, 0x09, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x42, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e, 0x72, @@ -8209,29 +8209,29 @@ var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, - 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x39, - 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x52, 0x09, - 0x74, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x5f, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, - 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x69, 0x6d, 0x65, - 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, - 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, - 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x13, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x12, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x12, 0x4b, 0x0a, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x5f, - 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, - 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, - 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, - 0x6e, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x54, - 0x69, 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x42, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x72, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x26, + 0x0a, 0x0f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, + 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x54, 0x69, 0x6d, + 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, + 0x72, 0x61, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, + 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x47, 0x72, 0x61, 0x69, 0x6e, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x47, 0x72, 0x61, 0x69, + 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x1e, + 0x0a, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0a, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, + 0x0a, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x13, 0x64, 0x69, + 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, + 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x12, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x4b, 0x0a, 0x11, 0x6d, + 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, + 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x10, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x73, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x1a, 0x42, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, diff --git a/proto/gen/rill/runtime/v1/resources.pb.validate.go b/proto/gen/rill/runtime/v1/resources.pb.validate.go index 8d5f28450a18..a5d078381772 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.validate.go +++ b/proto/gen/rill/runtime/v1/resources.pb.validate.go @@ -3199,6 +3199,8 @@ func (m *MetricsViewSpec) validate(all bool) error { // no validation rules for WatermarkExpression + // no validation rules for DataTimeRange + for idx, item := range m.GetDimensions() { _, _ = idx, item @@ -3439,8 +3441,6 @@ func (m *MetricsViewSpec) validate(all bool) error { } - // no validation rules for DataTimeRange - if m.CacheEnabled != nil { // no validation rules for CacheEnabled } @@ -13546,6 +13546,8 @@ func (m *MetricsViewSpec_Rollup) validate(all bool) error { // no validation rules for Model + // no validation rules for DataTimeRange + // no validation rules for TimeGrain // no validation rules for TimeZone @@ -13608,8 +13610,6 @@ func (m *MetricsViewSpec_Rollup) validate(all bool) error { } } - // no validation rules for DataTimeRange - if len(errors) > 0 { return MetricsViewSpec_RollupMultiError(errors) } diff --git a/proto/gen/rill/runtime/v1/runtime.swagger.yaml b/proto/gen/rill/runtime/v1/runtime.swagger.yaml index fa91d08798ad..62acac117483 100644 --- a/proto/gen/rill/runtime/v1/runtime.swagger.yaml +++ b/proto/gen/rill/runtime/v1/runtime.swagger.yaml @@ -3849,6 +3849,12 @@ definitions: type: string model: type: string + dataTimeRange: + type: string + description: |- + Optional rilltime expression describing the time range covered by the rollup. + When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. timeGrain: $ref: '#/definitions/v1TimeGrain' description: Time grain of the rollup. @@ -3871,12 +3877,6 @@ definitions: measuresSelector: $ref: '#/definitions/v1FieldSelector' description: Dynamic selector for `measures`. Will be processed during validation, so it will always be empty in `state.valid_spec`. - dataTimeRange: - type: string - description: |- - Optional rilltime expression describing the time range covered by the rollup. - When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. description: |- Pre-aggregated rollup that can be used to accelerate queries. The system automatically routes queries to a rollup when the query can be satisfied from the pre-aggregated data. @@ -6458,6 +6458,12 @@ definitions: watermarkExpression: type: string description: Expression to evaluate a watermark for the metrics view. If not set, the watermark defaults to max(time_dimension). + dataTimeRange: + type: string + description: |- + Optional rilltime expression describing the time range covered by the base table. + When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. dimensions: type: array items: @@ -6525,12 +6531,6 @@ definitions: items: type: object $ref: '#/definitions/MetricsViewSpecRollup' - dataTimeRange: - type: string - description: |- - Optional rilltime expression describing the time range covered by the base table. - When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. v1MetricsViewSpecAnnotation: type: object properties: diff --git a/proto/rill/runtime/v1/resources.proto b/proto/rill/runtime/v1/resources.proto index 605aca36a1a7..45a3322a99a0 100644 --- a/proto/rill/runtime/v1/resources.proto +++ b/proto/rill/runtime/v1/resources.proto @@ -306,6 +306,10 @@ message MetricsViewSpec { string database_schema = 2; string table = 3; string model = 4; + // Optional rilltime expression describing the time range covered by the rollup. + // When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + string data_time_range = 11; // Time grain of the rollup. TimeGrain time_grain = 5; // IANA timezone the rollup was aggregated in; defaults to UTC @@ -318,10 +322,6 @@ message MetricsViewSpec { FieldSelector dimensions_selector = 9; // Dynamic selector for `measures`. Will be processed during validation, so it will always be empty in `state.valid_spec`. FieldSelector measures_selector = 10; - // Optional rilltime expression describing the time range covered by the rollup. - // When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. - string data_time_range = 11; } // Connector containing the table string connector = 1; @@ -346,6 +346,10 @@ message MetricsViewSpec { TimeGrain smallest_time_grain = 8; // Expression to evaluate a watermark for the metrics view. If not set, the watermark defaults to max(time_dimension). string watermark_expression = 20; + // Optional rilltime expression describing the time range covered by the base table. + // When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + string data_time_range = 36; // Dimensions in the metrics view repeated Dimension dimensions = 6; // Measures in the metrics view @@ -376,10 +380,6 @@ message MetricsViewSpec { // Keys and values are stored as templates and will be resolved at query time. map query_attributes = 33; repeated Rollup rollups = 34; - // Optional rilltime expression describing the time range covered by the base table. - // When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - // Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. - string data_time_range = 36; } message SecurityRule { diff --git a/runtime/metricsview/executor/executor.go b/runtime/metricsview/executor/executor.go index 8157f87375b3..75f28909ede7 100644 --- a/runtime/metricsview/executor/executor.go +++ b/runtime/metricsview/executor/executor.go @@ -206,7 +206,6 @@ func (e *Executor) Timestamps(ctx context.Context, timeDim string) (metricsview. res.Now = time.Now() // For the primary time dimension, also resolve rollup table timestamps. - // Per-rollup data_time_range short-circuits the OLAP probe for that rollup. if timeDim == mv.TimeDimension && len(mv.Rollups) > 0 { res.Rollups = make(map[string]metricsview.TimestampsResult, len(mv.Rollups)) for _, rollup := range mv.Rollups { diff --git a/runtime/metricsview/executor/executor_rewrite_rollup.go b/runtime/metricsview/executor/executor_rewrite_rollup.go index 9805bdf2e8d7..25203bc78302 100644 --- a/runtime/metricsview/executor/executor_rewrite_rollup.go +++ b/runtime/metricsview/executor/executor_rewrite_rollup.go @@ -67,7 +67,7 @@ const ( // to the rollup grain (to prevent the last bucket from pulling in extra data). // // 4. Selection: among eligible rollups, prefer the coarsest grain (fewer rows to scan). -// On ties, prefer the rollup defined earlier in the metrics view's rollups list — authors use this +// On ties, prefer the rollup defined earlier in the metrics view's rollups list — use this // to express priority among same-grain rollups with overlapping dimensions. // // The selected rollup is returned as a synthetic MetricsViewSpec that points to the rollup table. @@ -264,7 +264,7 @@ func (e *Executor) rewriteQueryForRollup(ctx context.Context, qry *metricsview.Q // Check end alignment now: if data extends beyond the query end and the end is not aligned to the rollup grain, // the last rollup bucket would include data beyond the requested range. rollupEligible only checks start alignment. - // Essentially it just check if base has data >= query end time, then makes sure the query end time is rollup grain aligned + // Essentially, it just checks if the base has data >= query end time, then makes sure the query end time is rollup grain aligned if hasTimeRange && !qry.TimeRange.End.IsZero() && !baseMax.Before(qry.TimeRange.End) && !timeAligned(qry.TimeRange.End, rollup.TimeGrain, rollupLoc, e.metricsView.FirstDayOfWeek) { rejectCandidate(rejectEndNotAligned, diff --git a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts index e65ce7cd7122..319794056be3 100644 --- a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts +++ b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts @@ -1449,6 +1449,15 @@ export class MetricsViewSpec extends Message { */ watermarkExpression = ""; + /** + * Optional rilltime expression describing the time range covered by the base table. + * When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + * Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + * + * @generated from field: string data_time_range = 36; + */ + dataTimeRange = ""; + /** * Dimensions in the metrics view * @@ -1548,15 +1557,6 @@ export class MetricsViewSpec extends Message { */ rollups: MetricsViewSpec_Rollup[] = []; - /** - * Optional rilltime expression describing the time range covered by the base table. - * When set, the base table's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - * Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. - * - * @generated from field: string data_time_range = 36; - */ - dataTimeRange = ""; - constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -1577,6 +1577,7 @@ export class MetricsViewSpec extends Message { { no: 5, name: "time_dimension", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 8, name: "smallest_time_grain", kind: "enum", T: proto3.getEnumType(TimeGrain) }, { no: 20, name: "watermark_expression", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 36, name: "data_time_range", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 6, name: "dimensions", kind: "message", T: MetricsViewSpec_Dimension, repeated: true }, { no: 7, name: "measures", kind: "message", T: MetricsViewSpec_Measure, repeated: true }, { no: 31, name: "parent_dimensions", kind: "message", T: FieldSelector }, @@ -1591,7 +1592,6 @@ export class MetricsViewSpec extends Message { { no: 35, name: "cache_timestamps_ttl_seconds", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, { no: 33, name: "query_attributes", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "scalar", T: 9 /* ScalarType.STRING */} }, { no: 34, name: "rollups", kind: "message", T: MetricsViewSpec_Rollup, repeated: true }, - { no: 36, name: "data_time_range", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): MetricsViewSpec { @@ -2192,6 +2192,15 @@ export class MetricsViewSpec_Rollup extends Message { */ model = ""; + /** + * Optional rilltime expression describing the time range covered by the rollup. + * When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. + * Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. + * + * @generated from field: string data_time_range = 11; + */ + dataTimeRange = ""; + /** * Time grain of the rollup. * @@ -2234,15 +2243,6 @@ export class MetricsViewSpec_Rollup extends Message { */ measuresSelector?: FieldSelector; - /** - * Optional rilltime expression describing the time range covered by the rollup. - * When set, the rollup's coverage is resolved from this expression instead of probing the OLAP for min/max timestamps. - * Evaluated with `now` = current time, `earliest` = zero time, `latest`/`watermark` = current time. - * - * @generated from field: string data_time_range = 11; - */ - dataTimeRange = ""; - constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -2255,13 +2255,13 @@ export class MetricsViewSpec_Rollup extends Message { { no: 2, name: "database_schema", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 3, name: "table", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 4, name: "model", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 11, name: "data_time_range", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 5, name: "time_grain", kind: "enum", T: proto3.getEnumType(TimeGrain) }, { no: 6, name: "time_zone", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 7, name: "dimensions", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 8, name: "measures", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 9, name: "dimensions_selector", kind: "message", T: FieldSelector }, { no: 10, name: "measures_selector", kind: "message", T: FieldSelector }, - { no: 11, name: "data_time_range", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): MetricsViewSpec_Rollup { From 2b6dc7750527235ccb6aa3d609bcc8f373c3f5d6 Mon Sep 17 00:00:00 2001 From: Parag Jain Date: Tue, 26 May 2026 17:20:47 +0530 Subject: [PATCH 3/6] handle rollup wider than base --- .../executor/executor_rewrite_rollup.go | 11 +- .../executor_rollup_integration_test.go | 121 ++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/runtime/metricsview/executor/executor_rewrite_rollup.go b/runtime/metricsview/executor/executor_rewrite_rollup.go index 25203bc78302..4556bd9dcd14 100644 --- a/runtime/metricsview/executor/executor_rewrite_rollup.go +++ b/runtime/metricsview/executor/executor_rewrite_rollup.go @@ -218,14 +218,17 @@ func (e *Executor) rewriteQueryForRollup(ctx context.Context, qry *metricsview.Q rollupEffEnd := timeutil.OffsetTime(rollupMax, timeutil.TimeGrainFromAPI(rollup.TimeGrain), 1, rollupLoc) if hasTimeRange { - // Clamp query range to the base table's actual data range. - // This ensures a rollup isn't rejected when the query extends beyond both the base table and rollup. + // Clamp the query range to the base table's data range, but only on the side where the + // rollup is no wider than the base. A rollup that extends past the base on a given end + // (e.g. an archive with rollupMin < baseMin, or a stream rollup with rollupMax > baseMax) + // is held to its own coverage on that end — clamping would silently let it through with + // a gap relative to the query. effectiveStart := qry.TimeRange.Start - if !effectiveStart.IsZero() && baseMin.After(effectiveStart) { + if !effectiveStart.IsZero() && baseMin.After(effectiveStart) && !rollupMin.Before(baseMin) { effectiveStart = baseMin } effectiveEnd := qry.TimeRange.End - if !effectiveEnd.IsZero() && baseMax.Before(effectiveEnd) { + if !effectiveEnd.IsZero() && baseMax.Before(effectiveEnd) && !rollupMax.After(baseMax) { effectiveEnd = baseMax } diff --git a/runtime/metricsview/executor/executor_rollup_integration_test.go b/runtime/metricsview/executor/executor_rollup_integration_test.go index c6bdb3fdeeb3..32174661213e 100644 --- a/runtime/metricsview/executor/executor_rollup_integration_test.go +++ b/runtime/metricsview/executor/executor_rollup_integration_test.go @@ -543,6 +543,127 @@ explore: require.Equal(t, rollupTestDailyTable, table) }) + t.Run("partial_archive_rejected_definition_order_picks_wider", func(t *testing.T) { + // Two day-grain rollups that both extend before the base. B (idx 0) starts at 2023-11-01; + // A (idx 1) starts at 2023-01-01. A query touching October 2023 — before both base and B — + // must skip B (low-end gap) and pick A. Without the asymmetric clamp, the start-side clamp + // would mask B's gap and definition order would silently pick B, dropping October data. + files := map[string]string{ + "rill.yaml": "", + "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], + "models/rollup_day_b.sql": ` +SELECT date_trunc('day', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-11-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "models/rollup_day_a.sql": ` +SELECT date_trunc('day', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-01-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "metrics_views/mv.yaml": ` +type: metrics_view +version: 1 +model: base_events +timeseries: timestamp +dimensions: + - name: publisher + column: publisher + - name: domain + column: domain +measures: + - name: total_impressions + expression: 'SUM("impressions")' +rollups: + - model: rollup_day_b + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_day_a + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] +explore: + skip: true`, + } + customRT, customID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{Files: files}) + testruntime.RequireReconcileState(t, customRT, customID, 5, 0, 0) + e := newRollupTestExecutor(t, customRT, customID) + defer e.Close() + + qry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_day_a", queryAndGetTable(t, e, qry)) + }) + + t.Run("partial_archive_rejected_coarser_grain_loses", func(t *testing.T) { + // Day rollup A (full archive from 2023-01-01) at idx 0 and month rollup B (partial archive from + // 2023-11-01) at idx 1. A month-grain query touching October 2023 would normally pick B on the + // coarsest-grain rule, but B's low-end gap must reject it; A wins. + files := map[string]string{ + "rill.yaml": "", + "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], + "models/rollup_day_a.sql": ` +SELECT date_trunc('day', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-01-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "models/rollup_month_b.sql": ` +SELECT date_trunc('month', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-11-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "metrics_views/mv.yaml": ` +type: metrics_view +version: 1 +model: base_events +timeseries: timestamp +dimensions: + - name: publisher + column: publisher + - name: domain + column: domain +measures: + - name: total_impressions + expression: 'SUM("impressions")' +rollups: + - model: rollup_day_a + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_month_b + time_grain: month + dimensions: [publisher, domain] + measures: [total_impressions] +explore: + skip: true`, + } + customRT, customID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{Files: files}) + testruntime.RequireReconcileState(t, customRT, customID, 5, 0, 0) + e := newRollupTestExecutor(t, customRT, customID) + defer e.Close() + + qry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainMonth}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_day_a", queryAndGetTable(t, e, qry)) + }) + t.Run("prefer_definition_order", func(t *testing.T) { // Two monthly rollups, both eligible for the query. The earlier one in the rollups list wins. files := map[string]string{ From c42d4b5a7a0d870045270037374d0fc8a048acd7 Mon Sep 17 00:00:00 2001 From: Parag Jain Date: Wed, 27 May 2026 16:22:22 +0530 Subject: [PATCH 4/6] docs --- docs/docs/reference/project-files/metrics-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/reference/project-files/metrics-views.md b/docs/docs/reference/project-files/metrics-views.md index 56c9d5e55706..8e3d44f335e4 100644 --- a/docs/docs/reference/project-files/metrics-views.md +++ b/docs/docs/reference/project-files/metrics-views.md @@ -62,7 +62,7 @@ _[string]_ - A SQL expression that tells us the max timestamp that the measures ### `data_time_range` -_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the model/table and uses the declared bounds for coverage checks. +_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks. ### `smallest_time_grain` From 39981a98f9379d637d41475a6366e66b61cc9d27 Mon Sep 17 00:00:00 2001 From: Parag Jain Date: Wed, 27 May 2026 16:51:36 +0530 Subject: [PATCH 5/6] docs --- docs/docs/developers/build/metrics-view/rollups.md | 2 +- docs/docs/reference/project-files/metrics-views.md | 4 ++-- runtime/parser/schema/project.schema.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/developers/build/metrics-view/rollups.md b/docs/docs/developers/build/metrics-view/rollups.md index 81adba1687fb..fd05f09f1009 100644 --- a/docs/docs/developers/build/metrics-view/rollups.md +++ b/docs/docs/developers/build/metrics-view/rollups.md @@ -72,7 +72,7 @@ rollups: ### Declaring Coverage with `data_time_range` -By default Rill discovers a rollup's time coverage at query time by running `SELECT min(time), max(time)` against the rollup table. If you'd rather declare coverage statically — to skip the probe, or to scope a rollup to a specific window — set `data_time_range` on the rollup. The value is a [rilltime expression](/reference/time-syntax/time-syntax): +By default Rill discovers a rollup's time coverage at query time by running `SELECT min(time), max(time)` against the rollup table. If you'd rather declare coverage statically — to skip the probe, or to scope a rollup to a specific window — set `data_time_range` on the rollup. The value is a [rilltime expression](/reference/time-syntax): ```yaml rollups: diff --git a/docs/docs/reference/project-files/metrics-views.md b/docs/docs/reference/project-files/metrics-views.md index 8e3d44f335e4..30512cbf9762 100644 --- a/docs/docs/reference/project-files/metrics-views.md +++ b/docs/docs/reference/project-files/metrics-views.md @@ -62,7 +62,7 @@ _[string]_ - A SQL expression that tells us the max timestamp that the measures ### `data_time_range` -_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks. +_[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax) expression describing the base table's time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks. ### `smallest_time_grain` @@ -316,7 +316,7 @@ _[array of object]_ - Pre-aggregated rollup tables that can be used to accelerat - **`exclude`** - _[object]_ - Select all fields except those listed here - - **`data_time_range`** - _[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the rollup's time coverage (e.g. `-1Y to now`, `-5Y to -1Y`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for this rollup and uses the declared bounds for coverage checks. + - **`data_time_range`** - _[string]_ - Optional [rilltime](https://docs.rilldata.com/reference/time-syntax) expression describing the rollup's time coverage (e.g. `-1Y to now`, `-5Y to -1Y`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for this rollup and uses the declared bounds for coverage checks. ### `security` diff --git a/runtime/parser/schema/project.schema.yaml b/runtime/parser/schema/project.schema.yaml index 96b58695569d..f9c57d64b7ab 100644 --- a/runtime/parser/schema/project.schema.yaml +++ b/runtime/parser/schema/project.schema.yaml @@ -2075,7 +2075,7 @@ definitions: description: A SQL expression that tells us the max timestamp that the measures are considered valid for. Usually does not need to be overwritten data_time_range: type: string - description: 'Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the base table''s time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks.' + description: 'Optional [rilltime](https://docs.rilldata.com/reference/time-syntax) expression describing the base table''s time coverage (e.g. `-5Y to now`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for the base table and uses the declared bounds for coverage checks.' smallest_time_grain: type: string description: 'Refers to the smallest time granularity the user is allowed to view. The valid values are: millisecond, second, minute, hour, day, week, month, quarter, year' @@ -2308,7 +2308,7 @@ definitions: description: Optional field selectors for measures to include in the rollup from the base metrics view. If not specified, all measures are included. data_time_range: type: string - description: 'Optional [rilltime](https://docs.rilldata.com/reference/time-syntax/time-syntax) expression describing the rollup''s time coverage (e.g. `-1Y to now`, `-5Y to -1Y`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for this rollup and uses the declared bounds for coverage checks.' + description: 'Optional [rilltime](https://docs.rilldata.com/reference/time-syntax) expression describing the rollup''s time coverage (e.g. `-1Y to now`, `-5Y to -1Y`, `inf`). When set, Rill skips the `min`/`max` OLAP probe for this rollup and uses the declared bounds for coverage checks.' required: - model - time_grain From 8c020bab7609d4e559367d5eb0c936653e92dd1e Mon Sep 17 00:00:00 2001 From: Parag Jain Date: Wed, 27 May 2026 22:59:52 +0530 Subject: [PATCH 6/6] fix some edge cases, simplify things --- .../developers/build/metrics-view/rollups.md | 1 + .../executor/executor_rewrite_rollup.go | 121 ++++++++++------ .../executor_rollup_integration_test.go | 132 +++++++++++++++++- 3 files changed, 204 insertions(+), 50 deletions(-) diff --git a/docs/docs/developers/build/metrics-view/rollups.md b/docs/docs/developers/build/metrics-view/rollups.md index fd05f09f1009..d8567b0a02aa 100644 --- a/docs/docs/developers/build/metrics-view/rollups.md +++ b/docs/docs/developers/build/metrics-view/rollups.md @@ -183,6 +183,7 @@ The base table is used if no rollup is eligible. - **Filters on missing dimensions disqualify the rollup.** A WHERE clause on `country` will skip a rollup that doesn't include `country`, even if the query's group-by columns are all in the rollup. - **The rollup is responsible for being correct.** Rill does not validate that the rollup's measure values are consistent with the base — it trusts the model. If the rollup model uses the wrong aggregation (e.g. `AVG` where the base measure is `SUM`), queries routed to it will return wrong numbers. - **Rollups are assumed to be roughly caught up with the base table.** Coverage is measured against the base table's latest timestamp. A rollup that lags behind the base will be silently skipped for any query that reaches the tail of the data — including common "last 24 hours" queries and queries without a time range — even if it has the right grain, dimensions, and measures. Refresh rollups in step with the base model so selection actually happens. +- **Rollups must not extend beyond the base table.** Routing assumes the rollup's max timestamp is no later than the base's. A rollup that gets ahead of the base (e.g. ingested through a separate path) may return incorrect results at the tail of the data. :::info The full configuration schema is in the [metrics view reference](/reference/project-files/metrics-views#rollups). diff --git a/runtime/metricsview/executor/executor_rewrite_rollup.go b/runtime/metricsview/executor/executor_rewrite_rollup.go index 96b06bd6217f..a524f8fe5f94 100644 --- a/runtime/metricsview/executor/executor_rewrite_rollup.go +++ b/runtime/metricsview/executor/executor_rewrite_rollup.go @@ -62,23 +62,29 @@ const ( // g. All WHERE filter dimensions are present in the rollup. // // 3. Time coverage: an eligible rollup must cover both the base and (when present) comparison time ranges. -// For explicit time ranges, the query range is clamped to the base table's actual data range first. -// For no-time-range queries ("all data"), the rollup must cover the base table's full min/max range. -// Additionally, if the base table has data beyond a range's end, that end must be aligned -// to the rollup grain (to prevent the last bucket from pulling in extra data). +// The query range is clamped to the widest available source: the start to min(baseMin, rollupMin) and +// the end to max(baseMax, rollupEffEnd). So a rollup that extends further back (or forward) than the +// base is preserved here, not rejected. Additionally, if the base table has data beyond the query range's end, +// that end must be aligned to the rollup grain (to prevent the last bucket from pulling in extra data). // -// 4. Selection: among eligible rollups, prefer the coarsest grain (fewer rows to scan). -// On ties, prefer the rollup defined earlier in the metrics view's rollups list — use this -// to express priority among same-grain rollups with overlapping dimensions. +// 4. Selection: selection is based on coarsest grain, then earlier definition order. The +// exception is when the query time range goes beyond what the candidate rollups cover — i.e. +// rollups have mismatched data time range depths in that case the rollup with the wider effective return range wins. // // The selected rollup is returned as a synthetic MetricsViewSpec that points to the rollup table. // The caller uses this spec to build the query AST, so the rest of the query pipeline remains same. +// +// Assumption: a rollup's max timestamp is no later than the base table's max. Coverage and end-alignment checks use +// the base's max as the reference for "is there data at or beyond the query end" — a rollup that gets ahead of the base +// may return incorrect results at the tail of the data. // rollupCandidate tracks an eligible rollup along with selection metadata. type rollupCandidate struct { - rollup *runtimev1.MetricsViewSpec_Rollup - grainOrder int - index int // position in MetricsViewSpec.Rollups; used as the secondary tiebreaker (earlier wins) + rollup *runtimev1.MetricsViewSpec_Rollup + grainOrder int + index int // position in MetricsViewSpec.Rollups; used as the final tiebreaker (earlier wins) + effRetStart time.Time // earliest timestamp this rollup will actually return for this query + effRetEnd time.Time // latest timestamp this rollup will actually return for this query (exclusive) } // rewriteQueryForRollup checks if a rollup table can satisfy the query. @@ -209,30 +215,19 @@ func (e *Executor) rewriteQueryForRollup(ctx context.Context, qry *metricsview.Q rollupEffEnd := timeutil.OffsetTime(rollupMax, timeutil.TimeGrainFromAPI(rollup.TimeGrain), 1, rollupLoc) // Check coverage for the base time range and the comparison time range when present. + // When the query has no time range, use {baseMin, baseMax} for the coverage check so the rollup must still + // cover the base table's full data range; otherwise a partial rollup would silently truncate the result. + coverageTimeRange := qry.TimeRange + if !hasTimeRange { + coverageTimeRange = &metricsview.TimeRange{Start: baseMin, End: baseMax} + } rangeRejected := false - if hasTimeRange { - if reason, attrs := checkRangeCoverage(qry.TimeRange, baseMin, baseMax, rollupMin, rollupMax, rollupEffEnd); reason != "" { - rejectCandidate(reason, attrs...) - rangeRejected = true - } - } else { - // No time range: rollup must cover the base table's full range - if rollupMin.After(baseMin) { - rejectCandidate(rejectStartNotCovered, - attribute.String("rollup.base_min", baseMin.Format(time.RFC3339)), - attribute.String("rollup.rollup_min", rollupMin.Format(time.RFC3339)), - ) - rangeRejected = true - } else if rollupEffEnd.Before(baseMax) { - rejectCandidate(rejectEndNotCovered, - attribute.String("rollup.base_max", baseMax.Format(time.RFC3339)), - attribute.String("rollup.rollup_eff_end", rollupEffEnd.Format(time.RFC3339)), - ) - rangeRejected = true - } + if reason, attrs := checkRangeCoverage(coverageTimeRange, baseMin, baseMax, rollupMin, rollupEffEnd); reason != "" { + rejectCandidate(reason, attrs...) + rangeRejected = true } if !rangeRejected && hasComparisonTimeRange { - if reason, attrs := checkRangeCoverage(qry.ComparisonTimeRange, baseMin, baseMax, rollupMin, rollupMax, rollupEffEnd); reason != "" { + if reason, attrs := checkRangeCoverage(qry.ComparisonTimeRange, baseMin, baseMax, rollupMin, rollupEffEnd); reason != "" { rejectCandidate(reason, attrs...) rangeRejected = true } @@ -259,16 +254,41 @@ func (e *Executor) rewriteQueryForRollup(ctx context.Context, qry *metricsview.Q candidateSpan.SetAttributes(attribute.String("rollup.eligible", "true")) candidateSpan.End() - c := &rollupCandidate{ - rollup: rollup, - grainOrder: grainOrder[rollup.TimeGrain], - index: i, + // Compute the candidate's effective return range: the slice of time this rollup will actually emit for this query. + // The rollup is filtered to [max(query.start, rollupMin), min(query.end, rollupEffEnd)]. + // For two candidates with identical effective return ranges — selection falls to coarsest grain and then index order. + effRetStart := rollupMin + effRetEnd := rollupEffEnd + if hasTimeRange { + if !qry.TimeRange.Start.IsZero() && qry.TimeRange.Start.After(effRetStart) { + effRetStart = qry.TimeRange.Start + } + if !qry.TimeRange.End.IsZero() && qry.TimeRange.End.Before(effRetEnd) { + effRetEnd = qry.TimeRange.End + } } - // Selection priority: coarsest grain (primary); earlier definition order (secondary tiebreaker) - if best == nil || c.grainOrder > best.grainOrder { + c := &rollupCandidate{ + rollup: rollup, + grainOrder: grainOrder[rollup.TimeGrain], + index: i, + effRetStart: effRetStart, + effRetEnd: effRetEnd, + } + + // Selection: coarsest grain wins, then earlier definition order. The exception is when the query time range goes beyond what the candidate rollups cover; + // in that case the wider effective return range wins (earliest start, then latest end) as it will give the most complete data. + // When multiple rollups cover the query time range, they will all have the same effRetStart and effRetEnd so selection is base on grain and index order. + switch { + case best == nil: + best = c + case c.effRetStart.Before(best.effRetStart): best = c - } else if c.grainOrder == best.grainOrder && c.index < best.index { + case c.effRetStart.Equal(best.effRetStart) && c.effRetEnd.After(best.effRetEnd): + best = c + case c.effRetStart.Equal(best.effRetStart) && c.effRetEnd.Equal(best.effRetEnd) && c.grainOrder > best.grainOrder: + best = c + case c.effRetStart.Equal(best.effRetStart) && c.effRetEnd.Equal(best.effRetEnd) && c.grainOrder == best.grainOrder && c.index < best.index: best = c } } @@ -480,17 +500,26 @@ func collectWhereDimensionsRec(expr *metricsview.Expression, dims map[string]boo } } -// checkRangeCoverage verifies that the rollup covers the given time range. The query range is clamped to the -// base table's actual data range or the rollup if its not wider than the base, so a rollup isn't rejected when the query extends -// beyond the actual data. Returns the reject reason and trace attributes, or "" if covered. -func checkRangeCoverage(tr *metricsview.TimeRange, baseMin, baseMax, rollupMin, rollupMax, rollupEffEnd time.Time) (string, []attribute.KeyValue) { +// checkRangeCoverage verifies that the rollup covers the given time range. The query range is clamped to the widest +// available data range — i.e. min(baseMin, rollupMin) on the start and max(baseMax, rollupEffEnd) on the end +// so a rollup isn't rejected when the query asks for data outside what either source can serve. +// Returns the reject reason and trace attributes, or "" if covered. +func checkRangeCoverage(tr *metricsview.TimeRange, baseMin, baseMax, rollupMin, rollupEffEnd time.Time) (string, []attribute.KeyValue) { + deepestStart := baseMin + if rollupMin.Before(baseMin) { + deepestStart = rollupMin + } effectiveStart := tr.Start - if !effectiveStart.IsZero() && baseMin.After(effectiveStart) && !rollupMin.Before(baseMin) { - effectiveStart = baseMin + if !effectiveStart.IsZero() && deepestStart.After(effectiveStart) { + effectiveStart = deepestStart + } + furthestEnd := baseMax + if rollupEffEnd.After(baseMax) { + furthestEnd = rollupEffEnd } effectiveEnd := tr.End - if !effectiveEnd.IsZero() && baseMax.Before(effectiveEnd) && !rollupMax.After(baseMax) { - effectiveEnd = baseMax + if !effectiveEnd.IsZero() && effectiveEnd.After(furthestEnd) { + effectiveEnd = furthestEnd } if !effectiveStart.IsZero() && rollupMin.After(effectiveStart) { return rejectStartNotCovered, []attribute.KeyValue{ diff --git a/runtime/metricsview/executor/executor_rollup_integration_test.go b/runtime/metricsview/executor/executor_rollup_integration_test.go index 146bfcf0d5bc..cfa287a7a569 100644 --- a/runtime/metricsview/executor/executor_rollup_integration_test.go +++ b/runtime/metricsview/executor/executor_rollup_integration_test.go @@ -604,10 +604,12 @@ explore: require.Equal(t, "rollup_day_a", queryAndGetTable(t, e, qry)) }) - t.Run("partial_archive_rejected_coarser_grain_loses", func(t *testing.T) { - // Day rollup A (full archive from 2023-01-01) at idx 0 and month rollup B (partial archive from - // 2023-11-01) at idx 1. A month-grain query touching October 2023 would normally pick B on the - // coarsest-grain rule, but B's low-end gap must reject it; A wins. + t.Run("query_extends_below_base_widest_archive_wins", func(t *testing.T) { + // Day rollup A (archive from 2023-01-01) at idx 0 and month rollup B (archive from + // 2023-11-01) at idx 1. The query reaches back to October 2023, which is below the base + // table's start (2024-01-01). Both rollups pass coverage (clamped to the widest available + // source), so selection switches to the widest-range rule and A's deeper history wins + // over B's coarser grain. files := map[string]string{ "rill.yaml": "", "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], @@ -664,6 +666,68 @@ explore: require.Equal(t, "rollup_day_a", queryAndGetTable(t, e, qry)) }) + t.Run("query_extends_below_base_same_grain_widest_archive_wins", func(t *testing.T) { + // Two daily rollups: B (idx 0) reaches back to 2023-11-01; A (idx 1) reaches back to + // 2023-01-01. The query asks for 2022-10-01..2024-04-01, which extends below the base + // table's start (2024-01-01). Both pass coverage under the widest-source clamp; A wins + // selection because it has the deepest archive. Without the widest-range rule, the + // definition-order tiebreaker would have picked B. + files := map[string]string{ + "rill.yaml": "", + "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], + "models/rollup_day_b.sql": ` +SELECT date_trunc('day', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-11-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "models/rollup_day_a.sql": ` +SELECT date_trunc('day', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-01-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "metrics_views/mv.yaml": ` +type: metrics_view +version: 1 +model: base_events +timeseries: timestamp +dimensions: + - name: publisher + column: publisher + - name: domain + column: domain +measures: + - name: total_impressions + expression: 'SUM("impressions")' +rollups: + - model: rollup_day_b + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_day_a + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] +explore: + skip: true`, + } + customRT, customID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{Files: files}) + testruntime.RequireReconcileState(t, customRT, customID, 5, 0, 0) + e := newRollupTestExecutor(t, customRT, customID) + defer e.Close() + + qry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "timestamp", Compute: &metricsview.DimensionCompute{TimeFloor: &metricsview.DimensionComputeTimeFloor{Dimension: "timestamp", Grain: metricsview.TimeGrainDay}}}, + }, + Measures: []metricsview.Measure{{Name: "total_impressions"}}, + TimeRange: &metricsview.TimeRange{ + Start: time.Date(2022, 10, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC), + }, + } + require.Equal(t, "rollup_day_a", queryAndGetTable(t, e, qry)) + }) + t.Run("prefer_definition_order", func(t *testing.T) { // Two monthly rollups, both eligible for the query. The earlier one in the rollups list wins. files := map[string]string{ @@ -732,6 +796,66 @@ explore: }) t.Run("no_time_range_checks_coverage", func(t *testing.T) { + t.Run("prefers_widest_range_over_coarsest_grain", func(t *testing.T) { + // Base table spans 2024-01-01..2024-03-31. Monthly rollup A (idx 0) has extra + // history back to 2023-11-01; daily rollup B (idx 1) has even more history back + // to 2023-01-01. With no time range, the user's "all data" view should expose + // the deepest history available, so B (widest range) wins despite A's coarser grain. + files := map[string]string{ + "rill.yaml": "", + "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], + "models/rollup_month_a.sql": ` +SELECT date_trunc('month', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-11-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "models/rollup_day_b.sql": ` +SELECT date_trunc('day', ts) AS timestamp, 'Google' AS publisher, 'news.com' AS domain, + 100 AS impressions, 20 AS clicks +FROM generate_series(TIMESTAMP '2023-01-01 00:00:00', TIMESTAMP '2024-03-31 23:00:00', INTERVAL '1 HOUR') t(ts) +GROUP BY 1`, + "metrics_views/mv.yaml": ` +type: metrics_view +version: 1 +model: base_events +timeseries: timestamp +dimensions: + - name: publisher + column: publisher + - name: domain + column: domain +measures: + - name: total_impressions + expression: 'SUM("impressions")' +rollups: + - model: rollup_month_a + time_grain: month + dimensions: [publisher, domain] + measures: [total_impressions] + - model: rollup_day_b + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] +explore: + skip: true`, + } + customRT, customID := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{Files: files}) + testruntime.RequireReconcileState(t, customRT, customID, 5, 0, 0) + e := newRollupTestExecutor(t, customRT, customID) + defer e.Close() + + qry := &metricsview.Query{ + Dimensions: []metricsview.Dimension{ + {Name: "publisher"}, + }, + Measures: []metricsview.Measure{ + {Name: "total_impressions"}, + }, + // No TimeRange: means "all data" + } + require.Equal(t, "rollup_day_b", queryAndGetTable(t, e, qry)) + }) + t.Run("only_partial_rollup_returns_nil", func(t *testing.T) { // Only rollup is partial (Jan+Feb); no time range requires full coverage files := map[string]string{