diff --git a/docs-mintlify/docs/explore-analyze/charts/chart-types/table.mdx b/docs-mintlify/docs/explore-analyze/charts/chart-types/table.mdx index a169c6828c463..61a919e484aab 100644 --- a/docs-mintlify/docs/explore-analyze/charts/chart-types/table.mdx +++ b/docs-mintlify/docs/explore-analyze/charts/chart-types/table.mdx @@ -49,13 +49,37 @@ Click the dropdown arrow on any field in the **Fields** section to configure it: | **Word wrap** | Allow cell content to wrap to multiple lines | | **Hide** | Show or hide the column | -## Inline bars +## Display tab — showing columns as links, images, bars, or sparklines + +By default, columns display their raw value. The **Display** tab for each field lets you change this: + +### Links + +Display a field's value as a clickable hyperlink. To create dynamic per-row links: + +1. Add a [calculated field](/docs/explore-analyze/workbooks/calculated-fields) that produces a URL — for example: + ``` + CONCAT("https://example.com/orders/", order_items.order_id) + ``` +2. In the table visualization, hide the calculated field column. +3. In the **Display** tab for the field you want to link, set **Display as** to **Link** and select the hidden URL field as the source. + +{/* TODO screenshot: column display dropdown showing Link option selected (hidden — replace this comment with when image is ready) */} + +### Images + +Display a field as an image by setting **Display as** to **Image**. Configure height and width. To make the image a link, check **Link image** and set the **Link URL**. + +The URL must be publicly accessible without authentication. + +### Inline bars Display a numeric column as a proportional in-cell bar. In the column's per-column section on the **Style** tab, use the **Display as** control to add a bar. Each bar's length reflects the value's magnitude within the column's range. | Option | Description | |---|---| -| **Display as** | Choose **Value**, **Bar**, or both. Checking **Bar** reveals the bar options below; keeping **Value** checked shows the number alongside the bar (uncheck it for a bar-only cell). | +| **Display as** | Choose **Value**, **Inline bars**, or **Sparkline** — a single choice; the modes are mutually exclusive. Selecting **Inline bars** reveals the bar options below. | +| **Show value** | Show the formatted number alongside the bar. Turn it off for a bar-only cell. | | **Positive bar color** | Fill color for non-negative bars | | **Negative bar color** | Fill color for negative bars (used when the column contains both positive and negative values) | | **Bar scale** | **Auto** anchors each bar at zero and scales to the column's largest value, so even the smallest value still shows a bar; **Manual** scales against the bounds you set | @@ -65,6 +89,38 @@ When a column contains both positive and negative values, bars are drawn in both {/* TODO screenshot: table with inline bar column (hidden — replace this comment with when image is ready) */} +### Sparklines + +Display a numeric column as a **sparkline** — a mini trend chart in each cell that plots the measure across a time dimension. In the column's per-column section on the **Style** tab, set **Display as** to **Sparkline**. + +A sparkline needs a time dimension to use as its horizontal axis. When you switch a column to **Sparkline** and pick its horizontal axis time dimension, that dimension is **removed from the table query** (if it was there): the table shows one row per remaining dimension, correctly aggregated, while the sparkline plots the measure's value across the time dimension. Values are always correct for any measure type, including counts of distinct values, averages, and custom measures. + + + +Internally, sparklines are powered by additional queries grouped by time dimension and granularity: measures sharing the same dimension and granularity are fetched together, so a chart with several sparklines runs at most one extra query per distinct dimension and granularity combination. + + + +| Option | Description | +|---|---| +| **Display as** | Set to **Sparkline**. Available only for numeric columns, and only when the query has a time dimension. | +| **Horizontal axis** | The time dimension plotted along the sparkline. Auto-selected (the first time dimension in the query); change it here when the query has several. | +| **Granularity** | The time bucket for the horizontal axis. Defaults to the dimension's granularity in the query if present, otherwise month. | +| **Type** | **Area** (filled line, the default), **Line**, or **Bar** (mini columns). | +| **Line color** / **Area color** | Stroke color for line and bar; fill color for area. | +| **Line width** | Stroke width in pixels (Line and Area types). | +| **Show value** | Show the most recent value as a headline number next to the sparkline. Turn it off for a chart-only cell. | + +A row needs at least two data points to draw a sparkline; cells with fewer fall back to the formatted value. + + + +A granularity that is too fine for the data's time span (e.g. by the second over several years) makes each background query return many rows, which can make a table with sparklines slow to load for end users. Pick the coarsest granularity that still shows the trend you need. + + + +{/* TODO screenshot: table with a sparkline column (hidden — replace this comment with when image is ready) */} + ## Cell menu Left-clicking a table cell opens a context menu with any [`links` defined on the diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/rolling-window-offset-no-granularity.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/rolling-window-offset-no-granularity.test.ts new file mode 100644 index 0000000000000..f60f730a17892 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/integration/postgres/rolling-window-offset-no-granularity.test.ts @@ -0,0 +1,175 @@ +import { PostgresQuery } from '../../../src/adapter/PostgresQuery'; +import { prepareJsCompiler } from '../../unit/PrepareCompiler'; +import { dbRunner } from './PostgresDBRunner'; + +// Rolling window measures queried WITHOUT a time dimension granularity (only a +// dateRange). The window is anchored by `offset`: 'start' anchors at the period +// start, 'end' anchors at the period end. With no granularity the result is a +// single aggregate row. +// +// The seed visitors table has one row dated 2016-09-07 (before the queried +// ranges) which distinguishes offset:'start' (accumulate everything before the +// period start) from offset:'end' (accumulate everything up to the period end). +describe('Rolling window offset without granularity', () => { + jest.setTimeout(200000); + + const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler(` + cube(\`balances\`, { + sql: \`select * from visitors\`, + + measures: { + // trailing: 'unbounded' + begBalance: { + type: 'sum', + sql: 'amount', + rollingWindow: { trailing: 'unbounded', offset: 'start' } + }, + endBalance: { + type: 'sum', + sql: 'amount', + rollingWindow: { trailing: 'unbounded', offset: 'end' } + }, + + // leading: 'unbounded' + leadingStart: { + type: 'sum', + sql: 'amount', + rollingWindow: { leading: 'unbounded', offset: 'start' } + }, + leadingEnd: { + type: 'sum', + sql: 'amount', + rollingWindow: { leading: 'unbounded', offset: 'end' } + }, + + // finite trailing interval + trailing5Start: { + type: 'sum', + sql: 'amount', + rollingWindow: { trailing: '5 day', offset: 'start' } + }, + trailing5End: { + type: 'sum', + sql: 'amount', + rollingWindow: { trailing: '5 day', offset: 'end' } + }, + }, + + dimensions: { + id: { + type: 'number', + sql: 'id', + primaryKey: true + }, + createdAt: { + type: 'time', + sql: 'created_at' + }, + }, + }) + + cube(\`balances_fp\`, { + sql: \`select * from visitors WHERE \${FILTER_PARAMS.balances_fp.createdAt.filter('created_at')}\`, + + measures: { + begBalance: { + type: 'sum', + sql: 'amount', + rollingWindow: { trailing: 'unbounded', offset: 'start' } + }, + }, + + dimensions: { + id: { + type: 'number', + sql: 'id', + primaryKey: true + }, + createdAt: { + type: 'time', + sql: 'created_at' + }, + }, + }) + `); + + const runQuery = async (measures: string[], dateRange: [string, string]) => { + const query = new PostgresQuery( + { joinGraph, cubeEvaluator, compiler }, + { + measures, + timeDimensions: [ + { + dimension: 'balances.createdAt', + dateRange, + }, + ], + timezone: 'UTC', + } + ); + + const queryAndParams = query.buildSqlAndParams(); + return dbRunner.testQuery(queryAndParams); + }; + + it('trailing: unbounded — offset start vs end', () => compiler.compile().then(async () => { + // beg: amount where created_at < 2017-01-01 -> only the 2016-09-07 row (500) + // end: amount where created_at <= 2017-01-30 -> all rows (2000) + expect(await runQuery( + ['balances.begBalance', 'balances.endBalance'], + ['2017-01-01', '2017-01-30'] + )).toEqual([ + { balances__beg_balance: '500', balances__end_balance: '2000' }, + ]); + })); + + it('leading: unbounded — offset start vs end', () => compiler.compile().then(async () => { + // start: amount where created_at >= 2017-01-01 -> all 2017 rows in/after range (1500) + // end: amount where created_at > 2017-01-30 -> none (null) + expect(await runQuery( + ['balances.leadingStart', 'balances.leadingEnd'], + ['2017-01-01', '2017-01-30'] + )).toEqual([ + { balances__leading_start: '1500', balances__leading_end: null }, + ]); + })); + + it('finite trailing interval — offset start vs end', () => compiler.compile().then(async () => { + // range 2017-01-06 .. 2017-01-10 + // start: created_at in [from - 5d, from) = [2017-01-01, 2017-01-06) -> 100 + 200 = 300 + // end: created_at in (to - 5d, to] = (2017-01-05, 2017-01-10] -> 300 + 400 + 500 = 1200 + expect(await runQuery( + ['balances.trailing5Start', 'balances.trailing5End'], + ['2017-01-06', '2017-01-10'] + )).toEqual([ + { balances__trailing5_start: '300', balances__trailing5_end: '1200' }, + ]); + })); + + // FILTER_PARAMS on the time dimension must receive the date-range bounds, not + // the rolling window config — the window's trailing/leading/offset must never + // leak into the filter as query parameters. + it('FILTER_PARAMS does not leak rolling window config into params', () => compiler.compile().then(async () => { + const query = new PostgresQuery( + { joinGraph, cubeEvaluator, compiler }, + { + measures: ['balances_fp.begBalance'], + timeDimensions: [ + { + dimension: 'balances_fp.createdAt', + dateRange: ['2017-01-01', '2017-01-30'], + }, + ], + timezone: 'UTC', + } + ); + + const [, params] = query.buildSqlAndParams(); + expect(params).not.toContain('unbounded'); + expect(params).not.toContain('start'); + expect(params).not.toContain('end'); + + // Sanity check: the query still executes. + await dbRunner.testQuery(query.buildSqlAndParams()); + })); +}); diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts index 4d5d404c5b458..84bcbdca87323 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts @@ -1066,6 +1066,57 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL } ])); + it('member to alias for granularized time dimension', async () => runQueryTest({ + measures: [ + 'visitors.visitor_revenue', + 'visitors.visitor_count', + 'visitors.per_visitor_revenue' + ], + dimensions: [ + 'visitors.source' + ], + timeDimensions: [{ + dimension: 'visitors.created_at', + dateRange: ['2017-01-01', '2017-01-30'], + granularity: 'month' + }], + timezone: 'America/Los_Angeles', + // SQL API keys a granularized override `{member}.{granularity}` (dotted) and + // references the CubeScan column by it. The native planner must honor it + // instead of defaulting to `{base alias}_{granularity}`. + memberToAlias: { + 'visitors.visitor_revenue': 'custom_revenue', + 'visitors.visitor_count': 'custom_count', + 'visitors.source': 'custom_source', + 'visitors.created_at.month': 'custom_created_at_month', + }, + order: [] + }, + + [ + { + custom_source: 'google', + custom_created_at_month: '2017-01-01T00:00:00.000Z', + custom_revenue: null, + custom_count: '1', + visitors__per_visitor_revenue: null + }, + { + custom_source: 'some', + custom_created_at_month: '2017-01-01T00:00:00.000Z', + custom_revenue: '300', + custom_count: '2', + visitors__per_visitor_revenue: '150' + }, + { + custom_source: null, + custom_created_at_month: '2017-01-01T00:00:00.000Z', + custom_revenue: null, + custom_count: '2', + visitors__per_visitor_revenue: null + } + ])); + it('running total', async () => { await compiler.compile(); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/operators/rolling_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/operators/rolling_window.rs index ce85dd7470cea..c77063dfde259 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/operators/rolling_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/operators/rolling_window.rs @@ -1,5 +1,7 @@ use super::{FilterOperationSql, FilterSqlContext}; -use crate::planner::filter::operators::rolling_window::RegularRollingWindowOp; +use crate::planner::filter::operators::rolling_window::{ + RegularRollingWindowOp, RollingWindowOffsetOp, +}; use cubenativeutils::CubeError; impl FilterOperationSql for RegularRollingWindowOp { @@ -22,3 +24,51 @@ impl FilterOperationSql for RegularRollingWindowOp { } } } + +impl FilterOperationSql for RollingWindowOffsetOp { + fn to_sql(&self, ctx: &FilterSqlContext) -> Result { + let from_start = self.offset == "start"; + let member = ctx.member_sql.to_string(); + + // Anchor: range start (formatted to start-of-day) for offset 'start', + // range end (formatted to end-of-day) for 'end'. Both bounds share it. + let anchor = if from_start { + let from = self.from.as_deref().ok_or_else(|| { + CubeError::internal("Rolling window date range is missing its start".to_string()) + })?; + ctx.format_and_allocate_from_date(from)? + } else { + let to = self.to.as_deref().ok_or_else(|| { + CubeError::internal("Rolling window date range is missing its end".to_string()) + })?; + ctx.format_and_allocate_to_date(to)? + }; + + let mut conditions = Vec::new(); + + // trailing side -> lower bound (shifted back by the trailing interval; + // `unbounded` drops the bound, `None` keeps the anchor unshifted). + if let Some(bound) = ctx.extend_date_range_bound(anchor.clone(), &self.trailing, true)? { + conditions.push(if from_start { + ctx.plan_templates.gte(member.clone(), bound)? + } else { + ctx.plan_templates.gt(member.clone(), bound)? + }); + } + + // leading side -> upper bound (shifted forward by the leading interval). + if let Some(bound) = ctx.extend_date_range_bound(anchor.clone(), &self.leading, false)? { + conditions.push(if from_start { + ctx.plan_templates.lt(member.clone(), bound)? + } else { + ctx.plan_templates.lte(member.clone(), bound)? + }); + } + + if conditions.is_empty() { + ctx.plan_templates.always_true() + } else { + Ok(conditions.join(" AND ")) + } + } +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/typed_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/typed_filter.rs index 31a4ca2eaa83e..247fe84aa1058 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/typed_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/filter/typed_filter.rs @@ -67,7 +67,11 @@ impl TypedFilter { } FilterParamsColumn::Callback(callback) => { let args = match self.operation() { - FilterOp::DateRange(_) | FilterOp::DateSingle(_) => { + // RollingWindowOffset carries [from, to, trailing, leading, offset]; + // only the from/to dates are filter-param args for the callback. + FilterOp::DateRange(_) + | FilterOp::DateSingle(_) + | FilterOp::RollingWindowOffset(_) => { let ctx = FilterSqlContext { member_sql: "", query_tools, @@ -115,6 +119,7 @@ fn dispatch_to_sql(op: &FilterOp, ctx: &FilterSqlContext) -> Result op.to_sql(ctx), FilterOp::RegularRollingWindow(op) => op.to_sql(ctx), + FilterOp::RollingWindowOffset(op) => op.to_sql(ctx), FilterOp::ToDateRollingWindow(op) => op.to_sql(ctx), } } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs index ca956c68655e3..9b3ddd52a2ee9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs @@ -2,9 +2,10 @@ use cubenativeutils::CubeError; use std::str::FromStr; /// Filter operator declared in the data model (`equals`, `in`, -/// `gt`, `inDateRange`, ...). `RegularRollingWindowDateRange` and -/// `ToDateRollingWindowDateRange` are synthetic — manufactured by -/// the rolling-window planner, never coming from a query. +/// `gt`, `inDateRange`, ...). `RegularRollingWindowDateRange`, +/// `RollingWindowOffsetDateRange` and `ToDateRollingWindowDateRange` +/// are synthetic — manufactured by the rolling-window planner, never +/// coming from a query. #[derive(Clone, PartialEq, Debug)] pub enum FilterOperator { Equal, @@ -16,6 +17,7 @@ pub enum FilterOperator { AfterDate, AfterOrOnDate, RegularRollingWindowDateRange, + RollingWindowOffsetDateRange, ToDateRollingWindowDateRange, In, NotIn, @@ -81,6 +83,7 @@ impl ToString for FilterOperator { FilterOperator::AfterDate => "afterDate", FilterOperator::AfterOrOnDate => "afterOrOnDate", FilterOperator::RegularRollingWindowDateRange => "inDateRange", + FilterOperator::RollingWindowOffsetDateRange => "inDateRange", FilterOperator::ToDateRollingWindowDateRange => "inDateRange", FilterOperator::In => "in", FilterOperator::NotIn => "notIn", diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs index 8c57685d1ce01..af4d488d6928c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs @@ -12,3 +12,35 @@ impl RegularRollingWindowOp { Self { trailing, leading } } } + +/// `RollingWindowOffset` filter operation: a rolling window over a single date +/// range `[from, to]` (no time series / granularity). The window is anchored by +/// `offset` ('start' → `from`, 'end' → `to`); the trailing side is the lower +/// bound, the leading side the upper bound. An `unbounded` side drops its bound, +/// a finite interval shifts it (trailing subtracts, leading adds). +#[derive(Clone, Debug)] +pub struct RollingWindowOffsetOp { + pub(crate) from: Option, + pub(crate) to: Option, + pub(crate) trailing: Option, + pub(crate) leading: Option, + pub(crate) offset: String, +} + +impl RollingWindowOffsetOp { + pub fn new( + from: Option, + to: Option, + trailing: Option, + leading: Option, + offset: String, + ) -> Self { + Self { + from, + to, + trailing, + leading, + offset, + } + } +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs index 323188184238e..548e37d5250e7 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs @@ -14,7 +14,7 @@ use super::operators::in_list::InListOp; use super::operators::like::LikeOp; use super::operators::measure_filter::MeasureFilterOp; use super::operators::nullability::NullabilityOp; -use super::operators::rolling_window::RegularRollingWindowOp; +use super::operators::rolling_window::{RegularRollingWindowOp, RollingWindowOffsetOp}; use super::operators::to_date_rolling_window::ToDateRollingWindowOp; use super::FilterOperator; use crate::planner::GranularityHelper; @@ -43,6 +43,7 @@ pub enum FilterOp { MeasureFilter(MeasureFilterOp), Nullability(NullabilityOp), RegularRollingWindow(RegularRollingWindowOp), + RollingWindowOffset(RollingWindowOffsetOp), ToDateRollingWindow(ToDateRollingWindowOp), } @@ -275,6 +276,19 @@ impl TypedFilterBuilder { let leading = values.get(3).and_then(|v| v.to_param_string()); FilterOp::RegularRollingWindow(RegularRollingWindowOp::new(trailing, leading)) } + FilterOperator::RollingWindowOffsetDateRange => { + let from = values.first().and_then(|v| v.to_param_string()); + let to = values.get(1).and_then(|v| v.to_param_string()); + let trailing = values.get(2).and_then(|v| v.to_param_string()); + let leading = values.get(3).and_then(|v| v.to_param_string()); + let offset = values + .get(4) + .and_then(|v| v.to_param_string()) + .unwrap_or_else(|| "end".to_string()); + FilterOp::RollingWindowOffset(RollingWindowOffsetOp::new( + from, to, trailing, leading, offset, + )) + } FilterOperator::ToDateRollingWindowDateRange => { let granularity_name = values .get(2) diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs index 10f6792f3851b..f8a9fa7c4755e 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs @@ -956,8 +956,9 @@ impl MultiStageQueryPlanner { } /// Adjust date range filters for rolling window when there's no granularity. - /// Without granularity there's no time_series CTE, so we replace InDateRange - /// with BeforeOrOnDate/AfterOrOnDate that use parameters directly. + /// Without granularity there's no time_series CTE, so the InDateRange filter + /// is rewritten into the rolling-window bounds (anchored by the window offset) + /// applied directly to the base measure. fn replace_date_range_for_rolling_window( &self, rolling_window: &RollingWindow, @@ -971,6 +972,7 @@ impl MultiStageQueryPlanner { &filter.member_name(), &rolling_window.trailing, &rolling_window.leading, + rolling_window.offset.as_deref().unwrap_or("end"), )?; } } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs index c8c50894e4fb3..37a7d696aea1f 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs @@ -885,26 +885,25 @@ impl QueryProperties { false } - /// Rewrite an `InDateRange` filter on `member_name` according to the - /// trailing/leading bounds: both `unbounded` removes the filter entirely; - /// trailing-`unbounded` rewrites to `BeforeOrOnDate(to)`; leading- - /// `unbounded` rewrites to `AfterOrOnDate(from)`. Other inputs are - /// no-ops. + /// Rewrite an `InDateRange(from, to)` filter on `member_name` into a single + /// rolling-window-over-date-range filter anchored by `offset`. The window is + /// rendered by `RollingWindowOffsetOp`; here we only carry the inputs + /// (`from`, `to`, `trailing`, `leading`, `offset`). No rolling interval on + /// either side (e.g. running total, to_date) keeps the filter as-is; both + /// sides `unbounded` drops it entirely. pub fn replace_date_range_for_rolling_window_without_granularity( &mut self, member_name: &str, trailing: &Option, leading: &Option, + offset: &str, ) -> Result<(), CubeError> { - let trailing_unbounded = trailing.as_deref() == Some("unbounded"); - let leading_unbounded = leading.as_deref() == Some("unbounded"); - - if !trailing_unbounded && !leading_unbounded { + if trailing.is_none() && leading.is_none() { return Ok(()); } - if trailing_unbounded && leading_unbounded { - // Both unbounded — remove the date range filter entirely + // Both sides unbounded: the window spans everything, drop the date filter. + if trailing.as_deref() == Some("unbounded") && leading.as_deref() == Some("unbounded") { self.time_dimensions_filters.retain(|item| match item { FilterItem::Item(itm) => { !(itm.member_name() == member_name @@ -912,61 +911,25 @@ impl QueryProperties { } _ => true, }); - } else if trailing_unbounded { - // Remove lower bound: InDateRange(from, to) → BeforeOrOnDate(to) - let mut new_filters = Vec::new(); - for item in self.time_dimensions_filters.iter() { - match item { - FilterItem::Item(itm) - if itm.member_name() == member_name - && matches!(itm.filter_operator(), FilterOperator::InDateRange) => - { - let values = itm.values(); - let to_value = if values.len() >= 2 { - vec![values[1].clone()] - } else { - values.clone() - }; - new_filters.push(FilterItem::Item(itm.change_operator( - FilterOperator::BeforeOrOnDate, - to_value, - itm.use_raw_values(), - self.query_tools.query_tools().clone(), - None, - )?)); - } - other => new_filters.push(other.clone()), - } - } - self.time_dimensions_filters = new_filters; - } else { - // leading unbounded: remove upper bound: InDateRange(from, to) → AfterOrOnDate(from) - let mut new_filters = Vec::new(); - for item in self.time_dimensions_filters.iter() { - match item { - FilterItem::Item(itm) - if itm.member_name() == member_name - && matches!(itm.filter_operator(), FilterOperator::InDateRange) => - { - let values = itm.values(); - let from_value = if !values.is_empty() { - vec![values[0].clone()] - } else { - values.clone() - }; - new_filters.push(FilterItem::Item(itm.change_operator( - FilterOperator::AfterOrOnDate, - from_value, - itm.use_raw_values(), - self.query_tools.query_tools().clone(), - None, - )?)); - } - other => new_filters.push(other.clone()), - } - } - self.time_dimensions_filters = new_filters; + self.invalidate_join_groups_cache(); + return Ok(()); } + + // Keep the original [from, to] values and append the window inputs, so + // the filter carries [from, to, trailing, leading, offset]. + let additional_values = vec![ + FilterValue::from(trailing.clone()), + FilterValue::from(leading.clone()), + FilterValue::Str(offset.to_string()), + ]; + self.time_dimensions_filters = self.change_date_range_filter_impl( + member_name, + &self.time_dimensions_filters, + &FilterOperator::RollingWindowOffsetDateRange, + None, + &additional_values, + &None, + )?; self.invalidate_join_groups_cache(); Ok(()) } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs index 5979a16cd2352..3f6dc85a092d0 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs @@ -253,12 +253,26 @@ impl QueryPropertiesCompiler { } else { None }; - Ok(MemberSymbol::new_time_dimension(TimeDimensionSymbol::new( - base_symbol, - d.granularity.clone(), - granularity_obj, - date_range_tuple, - ))) + // Honor an explicit `memberToAlias` override for the granularized + // member. The SQL API (cubesql) keys it `{member}.{granularity}` + // (dotted) and references the CubeScan column by that alias; the + // default `{base alias}_{granularity}` would otherwise mismatch. + let alias_override = d.granularity.as_ref().and_then(|granularity| { + evaluator_compiler.alias_for_member(&format!( + "{}.{}", + base_symbol.full_name(), + granularity + )) + }); + Ok(MemberSymbol::new_time_dimension( + TimeDimensionSymbol::new_with_alias( + base_symbol, + d.granularity.clone(), + granularity_obj, + date_range_tuple, + alias_override, + ), + )) }) .collect() } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs index efdccc541eeee..79bb8b3e07988 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs @@ -22,6 +22,7 @@ pub struct TimeDimensionSymbol { granularity_obj: Option, date_range: Option<(String, String)>, alias_suffix: String, + alias_override: Option, } impl TimeDimensionSymbol { @@ -30,14 +31,35 @@ impl TimeDimensionSymbol { granularity: Option, granularity_obj: Option, date_range: Option<(String, String)>, + ) -> Rc { + Self::new_with_alias(base_symbol, granularity, granularity_obj, date_range, None) + } + + /// Like [`Self::new`] but with an explicit alias override (e.g. the SQL + /// API's `memberToAlias` entry for the granularized member). When `None`, + /// the alias falls back to `{base alias}_{granularity}`. + pub fn new_with_alias( + base_symbol: Rc, + granularity: Option, + granularity_obj: Option, + date_range: Option<(String, String)>, + alias_override: Option, ) -> Rc { let name_suffix = if let Some(granularity) = &granularity { granularity.clone() } else { "day".to_string() }; + + assert!(!alias_override + .as_ref() + .map(|a| a.is_empty()) + .unwrap_or(false)); + let alias = alias_override + .clone() + .unwrap_or_else(|| format!("{}_{}", base_symbol.alias(), name_suffix)); let full_name = format!("{}_{}", base_symbol.full_name(), name_suffix); - let alias = format!("{}_{}", base_symbol.alias(), name_suffix); + let compiled_path = CompiledMemberPath::new( base_symbol.compiled_path().cube().clone(), full_name, @@ -52,6 +74,7 @@ impl TimeDimensionSymbol { granularity_obj, date_range, alias_suffix: name_suffix, + alias_override, }) } @@ -155,11 +178,12 @@ impl TimeDimensionSymbol { .map(|s| match s.as_ref() { MemberSymbol::Dimension(dimension_symbol) => { if dimension_symbol.is_time() { - let result = Self::new( + let result = Self::new_with_alias( s.clone(), self.granularity.clone(), self.granularity_obj.clone(), self.date_range.clone(), + self.alias_override.clone(), ); MemberSymbol::new_time_dimension(result) } else { @@ -240,11 +264,12 @@ impl TimeDimensionSymbol { /// range. `None` if the base is not a reference. pub fn reference_member(&self) -> Option> { if let Some(base_symbol) = self.base_symbol.clone().reference_member() { - let new_time_dim = Self::new( + let new_time_dim = Self::new_with_alias( base_symbol, self.granularity.clone(), self.granularity_obj.clone(), self.date_range.clone(), + self.alias_override.clone(), ); Some(MemberSymbol::new_time_dimension(new_time_dim)) } else { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/integration_rolling_window.yaml b/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/integration_rolling_window.yaml index f00b81a9436ef..fdfc7eb035cbb 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/integration_rolling_window.yaml +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/integration_rolling_window.yaml @@ -177,6 +177,30 @@ cubes: trailing: 3 day leading: 1 day offset: end + - name: rolling_sum_trailing_offset_start + type: sum + sql: amount + rolling_window: + trailing: unbounded + offset: start + - name: rolling_sum_trailing_offset_end + type: sum + sql: amount + rolling_window: + trailing: unbounded + offset: end + - name: rolling_sum_leading_offset_start + type: sum + sql: amount + rolling_window: + leading: unbounded + offset: start + - name: rolling_sum_leading_offset_end + type: sum + sql: amount + rolling_window: + leading: unbounded + offset: end # Cat 15 — running total - name: running_total type: runningTotal diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs index faa1aa45c5a92..44a1f06c1d06a 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs @@ -1,5 +1,6 @@ use crate::test_fixtures::cube_bridge::{members_from_strings, MockBaseQueryOptions, MockSchema}; use crate::test_fixtures::test_utils::TestContext; +use cubenativeutils::CubeError; use indoc::indoc; use std::rc::Rc; @@ -40,6 +41,40 @@ fn test_member_to_alias() { ); } +#[test] +fn test_member_to_alias_time_dimension_granularity() -> Result<(), CubeError> { + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let test_context = TestContext::new(schema)?; + + // The SQL API references a granularized time-dimension column by an alias + // sent via `memberToAlias` keyed `{member}.{granularity}`. The planner must + // honor it instead of defaulting to `{base alias}_{granularity}`. + let query_yaml = indoc! {r#" + measures: + - visitors.count + time_dimensions: + - dimension: visitors.created_at + granularity: month + memberToAlias: + visitors.created_at.month: "td_month_alias" + "#}; + + let sql = test_context.build_sql(query_yaml)?; + + // The override must be used as the projected alias … + assert!( + sql.contains("\"td_month_alias\""), + "expected granularized memberToAlias override, got: {sql}" + ); + // … instead of the default `{base alias}_{granularity}`. + assert!( + !sql.contains("visitors__created_at_month"), + "should not fall back to default granularized alias, got: {sql}" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_simple_join_sql() { let schema = MockSchema::from_yaml_file("common/diamond_joins.yaml"); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs index 0c93acbdb4778..3cbf5d0e320db 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs @@ -63,13 +63,19 @@ async fn test_leading_unbounded_no_granularity() { .build_sql(query) .expect("Should generate SQL for leading unbounded"); + // Default offset is 'end', so the window anchors at the range end: everything + // strictly after `to`. assert!( - sql.contains(r#""orders".created_at >= $_0_$::timestamptz"#), - "Leading unbounded should have lower time bound (>=), got: {sql}" + sql.contains(r#""orders".created_at > $_0_$::timestamptz"#), + "Leading unbounded should have strict lower time bound (> to), got: {sql}" ); assert!( - !sql.contains(r#"created_at <= $_"#), - "Leading unbounded should not have an upper time bound (<=), got: {sql}" + !sql.contains("created_at >= "), + "Leading unbounded (offset end) should not use inclusive lower bound, got: {sql}" + ); + assert!( + !sql.contains("created_at <"), + "Leading unbounded should not have an upper time bound, got: {sql}" ); if let Some(result) = ctx.try_execute_pg(query, SEED).await { @@ -142,15 +148,6 @@ async fn test_trailing_unbounded_with_granularity() { } } -// FIXME: Bounded rolling window (trailing: 3 day, leading: 1 day) without granularity -// currently ignores the trailing/leading intervals entirely and uses the raw dateRange -// as a plain WHERE filter: created_at >= Jan 10 AND created_at <= Jan 20, producing 830. -// Expected behavior: the intervals should expand the date range, so the effective window -// becomes [Jan 10 - 3d .. Jan 20 + 1d] = [Jan 7 .. Jan 21], which would include -// order ID4 (200, Jan 8) and produce a larger result. -// The correct expected value depends on the chosen anchor semantics (start-of-range vs -// end-of-range), but the current behavior of silently discarding the intervals is wrong. -#[ignore] #[tokio::test(flavor = "multi_thread")] async fn test_bounded_no_granularity() { let ctx = create_context(); @@ -169,14 +166,28 @@ async fn test_bounded_no_granularity() { .build_sql(query) .expect("Should generate SQL for bounded rolling window"); + // Default offset 'end' anchors at the range end: the trailing interval shifts + // the (strict) lower bound back, the leading interval shifts the (inclusive) + // upper bound forward. assert!( !sql.contains("time_series"), "Without granularity should not reference time_series CTE, got: {sql}" ); assert!( - sql.contains(r#"created_at >= $_0_$::timestamptz"#) - && sql.contains(r#"created_at <= $_1_$::timestamptz"#), - "Should use parameterized date range on created_at, got: {sql}" + sql.contains("- interval '3 day'"), + "Should subtract trailing interval '3 day', got: {sql}" + ); + assert!( + sql.contains("+ interval '1 day'"), + "Should add leading interval '1 day', got: {sql}" + ); + assert!( + sql.contains("created_at > ") && !sql.contains("created_at >= "), + "Should have strict lower bound (> to - trailing), got: {sql}" + ); + assert!( + sql.contains("created_at <= "), + "Should have inclusive upper bound (<= to + leading), got: {sql}" ); if let Some(result) = ctx.try_execute_pg(query, SEED).await { @@ -221,15 +232,6 @@ async fn test_bounded_with_granularity() { } } -// FIXME: Trailing bounded rolling window (trailing: 7 day) without granularity currently -// ignores the trailing interval and uses the raw dateRange as a plain WHERE filter: -// created_at >= Jan 10 AND created_at <= Jan 20, producing 830 (sum of orders in -// [Jan 10..Jan 20]). -// Expected behavior: the trailing interval should expand the lower bound of the date -// range, so the effective window becomes [Jan 10 - 7d .. Jan 20] = [Jan 3 .. Jan 20], -// which would include orders ID2 (45, Jan 3), ID3 (75, Jan 4), ID4 (200, Jan 8) and -// produce 1150 (830 + 45 + 75 + 200). -#[ignore] #[tokio::test(flavor = "multi_thread")] async fn test_trailing_bounded_no_granularity() { let ctx = create_context(); @@ -248,18 +250,27 @@ async fn test_trailing_bounded_no_granularity() { .build_sql(query) .expect("Should generate SQL for trailing bounded rolling window"); + // Default offset 'end' anchors at the range end: trailing shifts the (strict) + // lower bound back; with no leading the upper bound is the inclusive range end. assert!( - sql.contains(r#"created_at >= $_0_$::timestamptz"#) - && sql.contains(r#"created_at <= $_1_$::timestamptz"#), - "Should use parameterized date range on created_at, got: {sql}" + sql.contains("- interval '7 day'"), + "Should subtract trailing interval '7 day', got: {sql}" ); assert!( - !sql.contains("time_series"), - "Without granularity should not reference time_series CTE, got: {sql}" + !sql.contains("+ interval"), + "Should not have a leading interval, got: {sql}" ); assert!( - !sql.contains("interval"), - "Without granularity should not have interval arithmetic, got: {sql}" + sql.contains("created_at > ") && !sql.contains("created_at >= "), + "Should have strict lower bound (> to - trailing), got: {sql}" + ); + assert!( + sql.contains("created_at <= "), + "Should have inclusive upper bound (<= to), got: {sql}" + ); + assert!( + !sql.contains("time_series"), + "Without granularity should not reference time_series CTE, got: {sql}" ); if let Some(result) = ctx.try_execute_pg(query, SEED).await { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/mod.rs index b9138cd09277d..ef75a0ec268ac 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/mod.rs @@ -9,6 +9,7 @@ mod filtered_rolling_measures; mod mixed_measures; mod multi_fact; mod multiple_rolling; +mod offset_no_granularity; mod offset_variations; mod order_and_limit; mod running_total; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs new file mode 100644 index 0000000000000..df7ea585b1098 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs @@ -0,0 +1,228 @@ +use crate::test_fixtures::cube_bridge::MockSchema; +use crate::test_fixtures::test_utils::TestContext; +use indoc::indoc; + +fn create_context() -> TestContext { + let schema = MockSchema::from_yaml_file("common/integration_rolling_window.yaml"); + TestContext::new(schema).unwrap() +} + +const SEED: &str = "integration_rolling_window_tables.sql"; + +// Rolling window measures queried without a granularity (dateRange only). The +// whole range is a single window anchored by `offset`: 'start' anchors at the +// range start (`from`), 'end' at the range end (`to`). The seed has orders before +// (January) and after (March) the queried February range, which is what makes the +// start/end anchors produce different aggregates. + +#[tokio::test(flavor = "multi_thread")] +async fn test_trailing_unbounded_offset_start_no_granularity() { + let ctx = create_context(); + + let query = indoc! {r#" + measures: + - orders.rolling_sum_trailing_offset_start + time_dimensions: + - dimension: orders.created_at + dateRange: + - "2024-02-01" + - "2024-02-29" + "#}; + + let sql = ctx.build_sql(query).expect("Should generate SQL"); + + // offset start + trailing unbounded => everything strictly before the range start. + assert!( + sql.contains(r#""orders".created_at < $_0_$::timestamptz"#), + "Should have strict upper bound at range start (< from), got: {sql}" + ); + assert!( + !sql.contains("created_at <= "), + "Should not use inclusive upper bound, got: {sql}" + ); + assert!( + !sql.contains("created_at >"), + "Should not have a lower bound, got: {sql}" + ); + assert!( + !sql.contains("time_series"), + "Without granularity should not reference time_series CTE, got: {sql}" + ); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_trailing_unbounded_offset_end_no_granularity() { + let ctx = create_context(); + + let query = indoc! {r#" + measures: + - orders.rolling_sum_trailing_offset_end + time_dimensions: + - dimension: orders.created_at + dateRange: + - "2024-02-01" + - "2024-02-29" + "#}; + + let sql = ctx.build_sql(query).expect("Should generate SQL"); + + // offset end + trailing unbounded => everything up to and including the range end. + assert!( + sql.contains(r#""orders".created_at <= $_0_$::timestamptz"#), + "Should have inclusive upper bound at range end (<= to), got: {sql}" + ); + assert!( + !sql.contains("created_at >"), + "Should not have a lower bound, got: {sql}" + ); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_leading_unbounded_offset_start_no_granularity() { + let ctx = create_context(); + + let query = indoc! {r#" + measures: + - orders.rolling_sum_leading_offset_start + time_dimensions: + - dimension: orders.created_at + dateRange: + - "2024-02-01" + - "2024-02-29" + "#}; + + let sql = ctx.build_sql(query).expect("Should generate SQL"); + + // offset start + leading unbounded => everything from the range start onward. + assert!( + sql.contains(r#""orders".created_at >= $_0_$::timestamptz"#), + "Should have inclusive lower bound at range start (>= from), got: {sql}" + ); + assert!( + !sql.contains("created_at <"), + "Should not have an upper bound, got: {sql}" + ); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_leading_unbounded_offset_end_no_granularity() { + let ctx = create_context(); + + let query = indoc! {r#" + measures: + - orders.rolling_sum_leading_offset_end + time_dimensions: + - dimension: orders.created_at + dateRange: + - "2024-02-01" + - "2024-02-29" + "#}; + + let sql = ctx.build_sql(query).expect("Should generate SQL"); + + // offset end + leading unbounded => everything strictly after the range end. + assert!( + sql.contains(r#""orders".created_at > $_0_$::timestamptz"#), + "Should have strict lower bound at range end (> to), got: {sql}" + ); + assert!( + !sql.contains("created_at >= "), + "Should not use inclusive lower bound, got: {sql}" + ); + assert!( + !sql.contains("created_at <"), + "Should not have an upper bound, got: {sql}" + ); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_trailing_finite_offset_start_no_granularity() { + let ctx = create_context(); + + let query = indoc! {r#" + measures: + - orders.rolling_sum_7d_offset_start + time_dimensions: + - dimension: orders.created_at + dateRange: + - "2024-01-10" + - "2024-01-16" + "#}; + + let sql = ctx.build_sql(query).expect("Should generate SQL"); + + // offset start + trailing 7 day => [from - 7 day, from): the trailing interval + // shifts the lower bound; the upper bound is the strict range start. + assert!( + sql.contains("7 day"), + "Should apply the trailing interval, got: {sql}" + ); + assert!( + sql.contains(r#""orders".created_at < $"#), + "Should have strict upper bound at range start (< from), got: {sql}" + ); + assert!( + !sql.contains("created_at <= "), + "Should not use inclusive upper bound, got: {sql}" + ); + assert!( + !sql.contains("time_series"), + "Without granularity should not reference time_series CTE, got: {sql}" + ); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_trailing_finite_offset_end_no_granularity() { + let ctx = create_context(); + + let query = indoc! {r#" + measures: + - orders.rolling_sum_7d_offset_end + time_dimensions: + - dimension: orders.created_at + dateRange: + - "2024-01-10" + - "2024-01-16" + "#}; + + let sql = ctx.build_sql(query).expect("Should generate SQL"); + + // offset end + trailing 7 day => (to - 7 day, to]: the trailing interval shifts + // the lower bound (strict); the upper bound is the inclusive range end. + assert!( + sql.contains("7 day"), + "Should apply the trailing interval, got: {sql}" + ); + assert!( + sql.contains(r#""orders".created_at <= $"#), + "Should have inclusive upper bound at range end (<= to), got: {sql}" + ); + assert!( + sql.contains("created_at > ") && !sql.contains("created_at >= "), + "Should have strict lower bound (> to - interval), got: {sql}" + ); + + if let Some(result) = ctx.try_execute_pg(query, SEED).await { + insta::assert_snapshot!(result); + } +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__bounded_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__bounded_no_granularity.snap new file mode 100644 index 0000000000000..4f0a5593a447c --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__bounded_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs +assertion_line: 194 +expression: result +--- +orders__rolling_sum_bounded +--------------------------- +180.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__leading_unbounded_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__leading_unbounded_no_granularity.snap index 7e194c25f6523..bfc323eeb4111 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__leading_unbounded_no_granularity.snap +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__leading_unbounded_no_granularity.snap @@ -1,7 +1,8 @@ --- -source: cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs +assertion_line: 82 expression: result --- orders__rolling_sum_leading --------------------------- -2285.00 +1455.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__trailing_bounded_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__trailing_bounded_no_granularity.snap new file mode 100644 index 0000000000000..dd0f93f434f11 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__basic_types__trailing_bounded_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/basic_types.rs +assertion_line: 277 +expression: result +--- +orders__rolling_sum_trailing_7d +------------------------------- +450.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__leading_unbounded_offset_end_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__leading_unbounded_offset_end_no_granularity.snap new file mode 100644 index 0000000000000..8e0a432c3ef57 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__leading_unbounded_offset_end_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs +assertion_line: 150 +expression: result +--- +orders__rolling_sum_leading_offset_end +-------------------------------------- +480.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__leading_unbounded_offset_start_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__leading_unbounded_offset_start_no_granularity.snap new file mode 100644 index 0000000000000..19f203d581450 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__leading_unbounded_offset_start_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs +assertion_line: 115 +expression: result +--- +orders__rolling_sum_leading_offset_start +---------------------------------------- +1205.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_finite_offset_end_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_finite_offset_end_no_granularity.snap new file mode 100644 index 0000000000000..cbb0300a9b0f9 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_finite_offset_end_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs +assertion_line: 226 +expression: result +--- +orders__rolling_sum_7d_offset_end +--------------------------------- +650.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_finite_offset_start_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_finite_offset_start_no_granularity.snap new file mode 100644 index 0000000000000..12e3af9ee6829 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_finite_offset_start_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs +assertion_line: 190 +expression: result +--- +orders__rolling_sum_7d_offset_start +----------------------------------- +320.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_unbounded_offset_end_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_unbounded_offset_end_no_granularity.snap new file mode 100644 index 0000000000000..d3811672d27be --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_unbounded_offset_end_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs +assertion_line: 84 +expression: result +--- +orders__rolling_sum_trailing_offset_end +--------------------------------------- +2275.00 diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_unbounded_offset_start_no_granularity.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_unbounded_offset_start_no_granularity.snap new file mode 100644 index 0000000000000..1577b8c53a6eb --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/snapshots/cubesqlplanner__tests__integration__rolling_window__offset_no_granularity__trailing_unbounded_offset_start_no_granularity.snap @@ -0,0 +1,8 @@ +--- +source: cubesqlplanner/cubesqlplanner/src/tests/integration/rolling_window/offset_no_granularity.rs +assertion_line: 53 +expression: result +--- +orders__rolling_sum_trailing_offset_start +----------------------------------------- +1550.00