Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { PostgresQuery } from '../../../src/adapter/PostgresQuery';
import { prepareJsCompiler } from '../../unit/PrepareCompiler';
import { dbRunner } from './PostgresDBRunner';

describe('Segments in View with SubQuery Dimensions', () => {
jest.setTimeout(200000);

const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler(`
cube(\`Accounts\`, {
sql: \`
SELECT 1 AS id, 'US' AS region UNION ALL
SELECT 2 AS id, 'US' AS region UNION ALL
SELECT 3 AS id, 'EU' AS region UNION ALL
SELECT 4 AS id, 'EU' AS region UNION ALL
SELECT 5 AS id, 'AP' AS region
\`,

joins: {
Tickets: {
relationship: \`one_to_many\`,
sql: \`\${CUBE}.id = \${Tickets}.account_id\`,
},
},

dimensions: {
id: {
sql: \`id\`,
type: \`number\`,
primaryKey: true,
public: true,
},

region: {
sql: \`\${CUBE}.region\`,
type: \`string\`,
},

ticketCount: {
sql: \`\${Tickets.count}\`,
type: \`number\`,
subQuery: true,
},
},

segments: {
hasNoTickets: {
sql: \`(\${ticketCount} = 0)\`,
},
},

measures: {
count: {
type: \`count\`,
},
},
});

cube(\`Tickets\`, {
sql: \`
SELECT 1 AS id, 1 AS account_id UNION ALL
SELECT 2 AS id, 1 AS account_id UNION ALL
SELECT 3 AS id, 3 AS account_id UNION ALL
SELECT 4 AS id, 5 AS account_id
\`,

dimensions: {
id: {
sql: \`id\`,
type: \`number\`,
primaryKey: true,
},

accountId: {
sql: \`\${CUBE}.account_id\`,
type: \`number\`,
},
},

measures: {
count: {
type: \`count\`,
},
},
});

view(\`accountOverview\`, {
cubes: [
{
join_path: Accounts,
includes: [
\`hasNoTickets\`,
\`count\`,
\`region\`,
],
},
],
});
`);

async function runQueryTest(q, expectedResult) {
await compiler.compile();
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, q);

console.log(query.buildSqlAndParams());

const res = await dbRunner.testQuery(query.buildSqlAndParams());
console.log(JSON.stringify(res));

expect(res).toEqual(
expectedResult
);
}

it('segment with subquery dimension in view', async () => runQueryTest({
measures: ['accountOverview.count'],
segments: ['accountOverview.hasNoTickets'],
}, [{
account_overview__count: '2',
}]));

it('segment with subquery dimension in view with dimension', async () => runQueryTest({
measures: ['accountOverview.count'],
segments: ['accountOverview.hasNoTickets'],
dimensions: ['accountOverview.region'],
order: [{ id: 'accountOverview.region' }],
}, [{
account_overview__region: 'EU',
account_overview__count: '1',
}, {
account_overview__region: 'US',
account_overview__count: '1',
}]));
});
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,24 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL
visitors__created_at_day: '2017-01-10T00:00:00.000Z',
}]));

it('rolling window with unbounded without time dimension', async () => runQueryTest({
measures: [
'visitors.countRollingUnbounded',
],
timeDimensions: [
{
dimension: 'visitors.created_at',
dateRange: ['2017-01-05', '2017-01-10']
}
],
order: [{
id: 'visitors.created_at'
}],
timezone: 'America/Los_Angeles'
}, [{
visitors__count_rolling_unbounded: '6'
}]));

it('two rolling windows with two time dimension granularities', async () => runQueryTest({
measures: [
'visitors.countRollingUnbounded',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,84 @@ impl MultiStageAppliedState {
false
}

/// Replace InDateRange filter with bounded version for rolling window without granularity.
/// Unlike `replace_regular_date_range_filter` which uses time_series CTE references,
/// this keeps parameter-based filters suitable for queries without a time_series CTE.
pub fn replace_date_range_for_rolling_window_without_granularity(
&mut self,
member_name: &String,
trailing: &Option<String>,
leading: &Option<String>,
) {
let trailing_unbounded = trailing.as_deref() == Some("unbounded");
let leading_unbounded = leading.as_deref() == Some("unbounded");

if !trailing_unbounded && !leading_unbounded {
return;
}

if trailing_unbounded && leading_unbounded {
// Both unbounded — remove the date range filter entirely
self.time_dimensions_filters.retain(|item| match item {
FilterItem::Item(itm) => {
!(&itm.member_name() == member_name
&& matches!(itm.filter_operator(), FilterOperator::InDateRange))
}
_ => true,
});
} else if trailing_unbounded {
// Remove lower bound: InDateRange(from, to) → BeforeOrOnDate(to)
self.time_dimensions_filters = self
.time_dimensions_filters
.iter()
.map(|item| 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()
};
FilterItem::Item(itm.change_operator(
FilterOperator::BeforeOrOnDate,
to_value,
itm.use_raw_values(),
))
}
other => other.clone(),
})
.collect();
} else {
// leading unbounded: remove upper bound: InDateRange(from, to) → AfterOrOnDate(from)
self.time_dimensions_filters = self
.time_dimensions_filters
.iter()
.map(|item| 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()
};
FilterItem::Item(itm.change_operator(
FilterOperator::AfterOrOnDate,
from_value,
itm.use_raw_values(),
))
}
other => other.clone(),
})
.collect();
}
}

pub fn replace_regular_date_range_filter(
&mut self,
member_name: &String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,11 @@ impl MultiStageQueryPlanner {
}

if time_dimensions.is_empty() {
let base_state =
self.replace_date_range_for_rolling_window(&rolling_window, state.clone());
let rolling_base = self.add_rolling_window_base(
member.clone(),
state.clone(),
base_state,
ungrouped,
descriptions,
)?;
Expand Down Expand Up @@ -698,6 +700,29 @@ 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.
fn replace_date_range_for_rolling_window(
&self,
rolling_window: &RollingWindow,
state: Rc<MultiStageAppliedState>,
) -> Rc<MultiStageAppliedState> {
let mut new_state = state.clone_state();
for filter_item in state.time_dimensions_filters() {
if let FilterItem::Item(filter) = filter_item {
if matches!(filter.filter_operator(), FilterOperator::InDateRange) {
new_state.replace_date_range_for_rolling_window_without_granularity(
&filter.member_name(),
&rolling_window.trailing,
&rolling_window.leading,
);
}
}
}
Rc::new(new_state)
}

fn make_rolling_base_state(
&self,
time_dimension: Rc<MemberSymbol>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ impl BaseTools for MockBaseTools {

fn generate_time_series(
&self,
_granularity: String,
_date_range: Vec<String>,
granularity: String,
date_range: Vec<String>,
) -> Result<Vec<Vec<String>>, CubeError> {
todo!("generate_time_series not implemented in mock")
super::time_series::generate_time_series(&granularity, &date_range)
}

fn generate_custom_time_series(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,11 +546,16 @@ impl MockViewBuilder {
);
}

let original_type = &measure.static_data().measure_type;
let view_type = match original_type.as_str() {
"number" | "string" | "time" | "boolean" => original_type.clone(),
_ => "number".to_string(),
};
all_measures.insert(
view_name,
Rc::new(
MockMeasureDefinition::builder()
.measure_type(measure.static_data().measure_type.clone())
.measure_type(view_type)
.sql(view_member_sql)
.build(),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod mock_sql_templates_render;
mod mock_sql_utils;
mod mock_struct_with_sql_member;
mod mock_timeshift_definition;
pub mod time_series;

pub use base_query_options::{members_from_strings, MockBaseQueryOptions};
pub use mock_base_tools::MockBaseTools;
Expand Down
Loading
Loading