diff --git a/packages/cubejs-testing/test/smoke-rbac.test.ts b/packages/cubejs-testing/test/smoke-rbac.test.ts index 22b584c2be4b7..4ea247d88b84a 100644 --- a/packages/cubejs-testing/test/smoke-rbac.test.ts +++ b/packages/cubejs-testing/test/smoke-rbac.test.ts @@ -838,6 +838,37 @@ describe('Cube RBAC Engine', () => { expect(result.rawData()).toMatchSnapshot('line_items_view_no_policy_rest'); }); + test('filter-only time dimension should not trigger hidden member error', async () => { + // Regression test: compareDateRange queries decompose into sub-queries + // where each time dimension has only dateRange (no granularity). + // These filter-only TDs don't appear in the annotation map. + // The Rust validate_query_members_in_annotation must skip them. + const result = await client.load({ + measures: ['orders_open.count'], + timeDimensions: [{ + dimension: 'orders_open.created_at', + dateRange: ['2099-01-01', '2099-12-31'], + }], + }); + // count returns a row with 0 even when no rows match the filter + expect(result.rawData().length).toBe(1); + expect(result.rawData()[0]['orders_open.count']).toBe('0'); + }); + + test('compareDateRange with filter-only time dimension should work', async () => { + const result = await client.load({ + measures: ['orders_open.count'], + timeDimensions: [{ + dimension: 'orders_open.created_at', + compareDateRange: [ + ['2020-01-01', '2020-06-30'], + ['2020-07-01', '2020-12-31'], + ], + }], + }, { queryType: 'multi' } as any); + expect(result).toBeDefined(); + }); + test('orders_view and cube with default policy', async () => { let error = ''; try { @@ -874,6 +905,35 @@ describe('Cube RBAC Engine', () => { // order_open should return all values since it has no access policy expect(result.rawData()).toMatchSnapshot('orders_open_rest'); }); + + test('filter-only time dimension on open cube should not fail for default user', async () => { + // Even with no roles, querying an open cube with a filter-only + // time dimension (no granularity) should work without hidden member errors. + const result = await defaultClient.load({ + measures: ['orders_open.count'], + timeDimensions: [{ + dimension: 'orders_open.created_at', + dateRange: ['2099-01-01', '2099-12-31'], + }], + }); + // count returns a row with 0 even when no rows match the filter + expect(result.rawData().length).toBe(1); + expect(result.rawData()[0]['orders_open.count']).toBe('0'); + }); + + test('compareDateRange on open cube should not fail for default user', async () => { + const result = await defaultClient.load({ + measures: ['orders_open.count'], + timeDimensions: [{ + dimension: 'orders_open.created_at', + compareDateRange: [ + ['2020-01-01', '2020-06-30'], + ['2020-07-01', '2020-12-31'], + ], + }], + }, { queryType: 'multi' } as any); + expect(result).toBeDefined(); + }); }); }); diff --git a/rust/cubeorchestrator/src/query_result_transform.rs b/rust/cubeorchestrator/src/query_result_transform.rs index 56ff3aed74cab..9966ef400fea0 100644 --- a/rust/cubeorchestrator/src/query_result_transform.rs +++ b/rust/cubeorchestrator/src/query_result_transform.rs @@ -2329,6 +2329,62 @@ mod tests { Ok(()) } + #[test] + fn test_get_members_compare_date_range_empty_dataset_filter_only_td() -> Result<()> { + let mut test_data = TEST_SUITE_DATA + .get(&"compare_date_range_count_by_order_date".to_string()) + .unwrap() + .clone(); + + // Simulate a compareDateRange sub-query where the time dimension has + // no granularity (filter-only). After compareDateRangeTransformer, + // each sub-query has dateRange but no granularity. + test_data.request.query.time_dimensions = Some(vec![QueryTimeDimension { + dimension: "ECommerceRecordsUs2021.orderDate".to_string(), + date_range: Some(vec![ + "2020-01-01T00:00:00.000".to_string(), + "2020-01-31T23:59:59.999".to_string(), + ]), + compare_date_range: None, + granularity: None, + }]); + + // Remove TD-related entries from annotation since filter-only TDs + // don't produce annotation entries (prepareAnnotation skips TDs + // without granularity). + test_data + .request + .annotation + .remove("ECommerceRecordsUs2021.orderDate.day"); + test_data + .request + .annotation + .remove("ECommerceRecordsUs2021.orderDate"); + + let alias_to_member_name_map = &test_data.request.alias_to_member_name_map; + let annotation = &test_data.request.annotation; + let query = &test_data.request.query; + let query_type = &test_data.request.query_type.clone().unwrap_or_default(); + + let result = get_members( + query_type, + query, + &QueryResult { + columns: vec![], + rows: vec![], + columns_pos: IndexMap::new(), + }, + alias_to_member_name_map, + annotation, + ); + assert!( + result.is_ok(), + "compareDateRange with filter-only TD should not trigger hidden member error, got: {:?}", + result.err() + ); + Ok(()) + } + #[test] fn test_get_members_filled_dataset() -> Result<()> { let test_data = TEST_SUITE_DATA