diff --git a/docs/docs/developers/build/metrics-view/rollups.md b/docs/docs/developers/build/metrics-view/rollups.md index 82cafcf65e3..d8567b0a02a 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. 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: + - 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 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 + +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: events_daily_narrow # selected for queries that only need publisher + time_grain: day + dimensions: [publisher] + measures: [total_impressions] + - model: events_daily_wide # selected when publisher + domain are queried + time_grain: day + dimensions: [publisher, domain] + measures: [total_impressions] + - 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 — `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 - **`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. @@ -131,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/docs/docs/reference/project-files/metrics-views.md b/docs/docs/reference/project-files/metrics-views.md index d00e76919f3..30512cbf976 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) 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 @@ -312,6 +316,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) 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 bb34ab6c326..caeaa6fcda0 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,37,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 @@ -2142,6 +2146,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 @@ -7493,6 +7504,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 @@ -7567,6 +7582,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 @@ -7984,7 +8006,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, 0xba, 0x21, 0x0a, 0x0f, 0x4d, 0x65, 0x74, 0x72, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x8a, 0x22, 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, @@ -8013,204 +8035,209 @@ 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, + 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, 0x25, 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, 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, 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, + 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, 0x2f, 0x0a, 0x14, + 0x6d, 0x61, 0x78, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, + 0x61, 0x6e, 0x67, 0x65, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x51, + 0x75, 0x65, 0x72, 0x79, 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, 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, 0x12, 0x2f, 0x0a, 0x14, 0x6d, 0x61, 0x78, 0x5f, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x24, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x11, 0x6d, 0x61, 0x78, 0x51, 0x75, 0x65, 0x72, 0x79, 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, + 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, 0xcd, 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, 0xcd, 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, 0x12, 0x26, 0x0a, 0x0f, 0x6c, - 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x69, 0x73, 0x5f, 0x62, 0x65, 0x74, 0x74, 0x65, 0x72, 0x18, 0x11, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x73, 0x42, 0x65, 0x74, - 0x74, 0x65, 0x72, 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, + 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, 0x12, 0x26, 0x0a, 0x0f, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x69, 0x73, 0x5f, + 0x62, 0x65, 0x74, 0x74, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6c, 0x6f, + 0x77, 0x65, 0x72, 0x49, 0x73, 0x42, 0x65, 0x74, 0x74, 0x65, 0x72, 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, 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, diff --git a/proto/gen/rill/runtime/v1/resources.pb.validate.go b/proto/gen/rill/runtime/v1/resources.pb.validate.go index 0ca6fee8c2d..971c5eb0c54 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 @@ -13548,6 +13550,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 diff --git a/proto/gen/rill/runtime/v1/runtime.swagger.yaml b/proto/gen/rill/runtime/v1/runtime.swagger.yaml index 76eb573d644..bd81ba22f93 100644 --- a/proto/gen/rill/runtime/v1/runtime.swagger.yaml +++ b/proto/gen/rill/runtime/v1/runtime.swagger.yaml @@ -3852,6 +3852,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. @@ -6455,6 +6461,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: diff --git a/proto/rill/runtime/v1/resources.proto b/proto/rill/runtime/v1/resources.proto index 652341bc442..f2bdc360f39 100644 --- a/proto/rill/runtime/v1/resources.proto +++ b/proto/rill/runtime/v1/resources.proto @@ -308,6 +308,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 @@ -344,6 +348,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 = 37; // Dimensions in the metrics view repeated Dimension dimensions = 6; // Measures in the metrics view diff --git a/runtime/metricsview/executor/executor.go b/runtime/metricsview/executor/executor.go index 79916246082..75f28909ede 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,35 @@ 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. 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 +230,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 4bc2cdcd11e..a524f8fe5f9 100644 --- a/runtime/metricsview/executor/executor_rewrite_rollup.go +++ b/runtime/metricsview/executor/executor_rewrite_rollup.go @@ -62,22 +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 with the smallest data range (tighter coverage). +// 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 - dataRange time.Duration // max - min; 0 if no time dimension + 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. @@ -139,7 +146,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) } @@ -207,29 +214,17 @@ 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 (or full base table range when no time range is set), - // and additionally for the comparison time range when present. + // 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, 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, rollupEffEnd); reason != "" { @@ -242,8 +237,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. - // Applies to both the base and comparison time ranges. + // the last rollup bucket would include data beyond the requested range. rollupEligible before only checked start alignment. if hasTimeRange { if reason, attrs := checkEndAlignment(qry.TimeRange.End, baseMax, rollup.TimeGrain, rollupLoc, e.metricsView.FirstDayOfWeek); reason != "" { rejectCandidate(reason, attrs...) @@ -260,17 +254,41 @@ 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, + // 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); smallest data range (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.dataRange > 0 && (best.dataRange == 0 || c.dataRange < best.dataRange) { + 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 } } @@ -482,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 first -// clamped to the base table's actual data range so a rollup isn't rejected when the query extends -// beyond the base table. Returns the reject reason and trace attributes, or "" if covered. +// 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) { - 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) { - 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 dfc6817ffe4..cfa287a7a56 100644 --- a/runtime/metricsview/executor/executor_rollup_integration_test.go +++ b/runtime/metricsview/executor/executor_rollup_integration_test.go @@ -543,8 +543,193 @@ 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("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("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"], + "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("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{ "rill.yaml": "", "models/base_events.sql": rollupTestFiles()["models/base_events.sql"], @@ -605,12 +790,72 @@ 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) }) }) 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{ @@ -746,6 +991,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 93dc12fb0d6..e7dcb6581e3 100644 --- a/runtime/parser/parse_metrics_view.go +++ b/runtime/parser/parse_metrics_view.go @@ -93,7 +93,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 { @@ -311,6 +313,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 { @@ -791,6 +800,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 @@ -830,6 +844,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}) } @@ -882,6 +897,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 c3a458d697e..66913bf73dc 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 { @@ -891,3 +930,50 @@ measures: }) } } + +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 099bbc47f99..f9c57d64b7a 100644 --- a/runtime/parser/schema/project.schema.yaml +++ b/runtime/parser/schema/project.schema.yaml @@ -2073,6 +2073,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) 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' @@ -2303,6 +2306,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) 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 32e1bcf1edf..cb12ff2638b 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 = 37; + */ + dataTimeRange = ""; + /** * Dimensions in the metrics view * @@ -1577,6 +1586,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: 37, 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 }, @@ -2200,6 +2210,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. * @@ -2254,6 +2273,7 @@ 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 },