diff --git a/packages/cubejs-api-gateway/openspec.yml b/packages/cubejs-api-gateway/openspec.yml index 5add9ce3a6bcb..585ffa3e38cee 100644 --- a/packages/cubejs-api-gateway/openspec.yml +++ b/packages/cubejs-api-gateway/openspec.yml @@ -369,6 +369,10 @@ components: - "on" - joinType - alias + V1LoadRequestJoinHint: + type: "array" + items: + type: "string" V1LoadRequestQuery: type: "object" properties: @@ -412,6 +416,10 @@ components: type: "array" items: $ref: "#/components/schemas/V1LoadRequestQueryJoinSubquery" + joinHints: + type: "array" + items: + $ref: "#/components/schemas/V1LoadRequestJoinHint" V1LoadRequest: type: "object" properties: diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 147aee1b98020..0d2a6a25919cc 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -162,6 +162,8 @@ const subqueryJoin = Joi.object().keys({ alias: Joi.string(), }); +const joinHint = Joi.array().items(Joi.string()); + const querySchema = Joi.object().keys({ // TODO add member expression alternatives only for SQL API queries? measures: Joi.array().items(Joi.alternatives(id, memberExpression, parsedMemberExpression)), @@ -189,6 +191,7 @@ const querySchema = Joi.object().keys({ ungrouped: Joi.boolean(), responseFormat: Joi.valid('default', 'compact'), subqueryJoins: Joi.array().items(subqueryJoin), + joinHints: Joi.array().items(joinHint), }); const normalizeQueryOrder = order => { diff --git a/packages/cubejs-api-gateway/src/types/query.ts b/packages/cubejs-api-gateway/src/types/query.ts index b26b39c09a9db..7c470a6aaa27c 100644 --- a/packages/cubejs-api-gateway/src/types/query.ts +++ b/packages/cubejs-api-gateway/src/types/query.ts @@ -122,6 +122,8 @@ type SubqueryJoins = { alias: string, }; +type JoinHint = Array; + /** * Incoming network query data type. */ @@ -143,6 +145,8 @@ interface Query { // TODO incoming query, query with parsed exprs and query with evaluated exprs are all different types subqueryJoins?: Array, + + joinHints?: Array } /** diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 729ec09e685a3..b7fc25e842a63 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -279,6 +279,7 @@ export class BaseQuery { multiStageDimensions: this.options.multiStageDimensions, multiStageTimeDimensions: this.options.multiStageTimeDimensions, subqueryJoins: this.options.subqueryJoins, + joinHints: this.options.joinHints, }); this.from = this.options.from; this.multiStageQuery = this.options.multiStageQuery; @@ -329,6 +330,7 @@ export class BaseQuery { const hasMultiStageMeasures = this.fullKeyQueryAggregateMeasures({ hasMultipliedForPreAggregation: true }).multiStageMembers.length > 0; this.canUseNativeSqlPlannerPreAggregation = hasMultiStageMeasures; } + this.queryLevelJoinHints = this.options.joinHints ?? []; this.prebuildJoin(); this.cubeAliasPrefix = this.options.cubeAliasPrefix; @@ -410,7 +412,10 @@ export class BaseQuery { */ get allJoinHints() { if (!this.collectedJoinHints) { - this.collectedJoinHints = this.collectJoinHints(); + this.collectedJoinHints = [ + ...this.queryLevelJoinHints, + ...this.collectJoinHints(), + ]; } return this.collectedJoinHints; } diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/multiple-join-paths.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/multiple-join-paths.test.ts index 3f174a6c9f1e3..63543a47459d5 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/multiple-join-paths.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/multiple-join-paths.test.ts @@ -641,4 +641,32 @@ describe('Multiple join paths', () => { }); } }); + + describe('Query level join hints', () => { + it('should respect query level join hints', async () => { + const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, { + measures: [], + dimensions: [ + 'A.a_id', + 'X.x_name_ref', + ], + joinHints: [ + ['A', 'D'], + ['D', 'E'], + ['E', 'X'], + ], + }); + + const [sql, _params] = query.buildSqlAndParams(); + + expect(sql).toMatch(/ON 'A' = 'D'/); + expect(sql).toMatch(/ON 'D' = 'E'/); + expect(sql).toMatch(/ON 'E' = 'X'/); + expect(sql).not.toMatch(/ON 'A' = 'B'/); + expect(sql).not.toMatch(/ON 'B' = 'C'/); + expect(sql).not.toMatch(/ON 'C' = 'X'/); + expect(sql).not.toMatch(/ON 'A' = 'F'/); + expect(sql).not.toMatch(/ON 'F' = 'X'/); + }); + }); }); diff --git a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs index 2ce959d038699..b96cab68df20e 100644 --- a/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs +++ b/rust/cubesql/cubeclient/src/models/v1_load_request_query.rs @@ -30,6 +30,8 @@ pub struct V1LoadRequestQuery { pub ungrouped: Option, #[serde(rename = "subqueryJoins", skip_serializing_if = "Option::is_none")] pub subquery_joins: Option>, + #[serde(rename = "joinHints", skip_serializing_if = "Option::is_none")] + pub join_hints: Option>>, } impl V1LoadRequestQuery { @@ -45,6 +47,7 @@ impl V1LoadRequestQuery { filters: None, ungrouped: None, subquery_joins: None, + join_hints: None, } } } diff --git a/rust/cubesql/cubesql/src/compile/builder.rs b/rust/cubesql/cubesql/src/compile/builder.rs index f265191bc85c4..7fdc2e86bc908 100644 --- a/rust/cubesql/cubesql/src/compile/builder.rs +++ b/rust/cubesql/cubesql/src/compile/builder.rs @@ -152,6 +152,7 @@ impl QueryBuilder { }, ungrouped: None, subquery_joins: None, + join_hints: None, }, meta: self.meta, } diff --git a/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs b/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs index aea4f3545bef6..fd7eb01a0e428 100644 --- a/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs +++ b/rust/cubesql/cubesql/src/compile/engine/df/wrapper.rs @@ -3368,6 +3368,8 @@ impl WrappedSelectNode { time_dimensions: load_request.time_dimensions.clone(), subquery_joins: (!prepared_join_subqueries.is_empty()) .then_some(prepared_join_subqueries), + + join_hints: load_request.join_hints.clone(), }; // TODO time dimensions, filters, segments diff --git a/rust/cubesql/cubesql/src/compile/mod.rs b/rust/cubesql/cubesql/src/compile/mod.rs index 5a914c302aa0e..6b4e403ff56f1 100644 --- a/rust/cubesql/cubesql/src/compile/mod.rs +++ b/rust/cubesql/cubesql/src/compile/mod.rs @@ -4665,6 +4665,13 @@ ORDER BY "ca_4" ASC segments: Some(vec![]), dimensions: Some(vec!["Logs.read".to_string()]), order: Some(vec![]), + join_hints: Some(vec![ + vec!["KibanaSampleDataEcommerce".to_string(), "Logs".to_string(),], + vec![ + "KibanaSampleDataEcommerce".to_string(), + "NumberCube".to_string(), + ], + ]), ..Default::default() } ); @@ -4726,6 +4733,10 @@ ORDER BY date_range: None, }]), order: Some(vec![]), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ); @@ -8218,6 +8229,10 @@ ORDER BY "source"."str0" ASC segments: Some(vec![]), order: Some(vec![]), ungrouped: Some(true), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ) @@ -9794,6 +9809,10 @@ ORDER BY "source"."str0" ASC segments: Some(vec![]), order: Some(vec![]), ungrouped: Some(true), + join_hints: Some(vec![vec![ + "Logs".to_string(), + "KibanaSampleDataEcommerce".to_string(), + ],]), ..Default::default() }, ); @@ -11845,6 +11864,12 @@ ORDER BY "source"."str0" ASC }).to_string(), ]), order: Some(vec![]), + join_hints: Some(vec![ + vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ], + ]), ..Default::default() } ); @@ -12271,6 +12296,10 @@ ORDER BY "source"."str0" ASC ]), segments: Some(vec![]), order: Some(vec![]), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ) diff --git a/rust/cubesql/cubesql/src/compile/rewrite/converter.rs b/rust/cubesql/cubesql/src/compile/rewrite/converter.rs index f44adc1293649..c105cc595d01e 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/converter.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/converter.rs @@ -12,11 +12,11 @@ use crate::{ AggregateFunctionExprDistinct, AggregateFunctionExprFun, AggregateSplit, AggregateUDFExprFun, AliasExprAlias, AnyExprAll, AnyExprOp, BetweenExprNegated, BinaryExprOp, CastExprDataType, ChangeUserMemberValue, ColumnExprColumn, - CubeScanAliasToCube, CubeScanLimit, CubeScanOffset, CubeScanUngrouped, CubeScanWrapped, - DimensionName, EmptyRelationDerivedSourceTableName, EmptyRelationIsWrappable, - EmptyRelationProduceOneRow, FilterMemberMember, FilterMemberOp, FilterMemberValues, - FilterOpOp, GroupingSetExprType, GroupingSetType, InListExprNegated, - InSubqueryExprNegated, JoinJoinConstraint, JoinJoinType, JoinLeftOn, + CubeScanAliasToCube, CubeScanJoinHints, CubeScanLimit, CubeScanOffset, + CubeScanUngrouped, CubeScanWrapped, DimensionName, EmptyRelationDerivedSourceTableName, + EmptyRelationIsWrappable, EmptyRelationProduceOneRow, FilterMemberMember, + FilterMemberOp, FilterMemberValues, FilterOpOp, GroupingSetExprType, GroupingSetType, + InListExprNegated, InSubqueryExprNegated, JoinJoinConstraint, JoinJoinType, JoinLeftOn, JoinNullEqualsNull, JoinRightOn, LikeExprEscapeChar, LikeExprLikeType, LikeExprNegated, LikeType, LimitFetch, LimitSkip, LiteralExprValue, LiteralMemberRelation, LiteralMemberValue, LogicalPlanLanguage, MeasureName, MemberErrorError, OrderAsc, @@ -2002,6 +2002,12 @@ impl LanguageToLogicalPlanConverter { query.ungrouped = Some(true); } + let join_hints = + match_data_node!(node_by_id, cube_scan_params[10], CubeScanJoinHints); + if join_hints.len() > 0 { + query.join_hints = Some(join_hints); + } + query.order = if !query_order.is_empty() { Some(query_order) } else { diff --git a/rust/cubesql/cubesql/src/compile/rewrite/cost.rs b/rust/cubesql/cubesql/src/compile/rewrite/cost.rs index 20fb584c68a53..0c338ee1d3b5e 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/cost.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/cost.rs @@ -118,7 +118,6 @@ impl BestCubePlan { LogicalPlanLanguage::GroupAggregateSplitReplacer(_) => 1, LogicalPlanLanguage::MemberPushdownReplacer(_) => 1, LogicalPlanLanguage::EventNotification(_) => 1, - LogicalPlanLanguage::MergedMembersReplacer(_) => 1, LogicalPlanLanguage::CaseExprReplacer(_) => 1, LogicalPlanLanguage::WrapperPushdownReplacer(_) => 1, LogicalPlanLanguage::WrapperPullupReplacer(_) => 1, diff --git a/rust/cubesql/cubesql/src/compile/rewrite/mod.rs b/rust/cubesql/cubesql/src/compile/rewrite/mod.rs index 552b9fc3ad28f..1ce096c452e0b 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/mod.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/mod.rs @@ -303,6 +303,7 @@ crate::plan_to_language! { can_pushdown_join: bool, wrapped: bool, ungrouped: bool, + join_hints: Vec>, }, CubeScanWrapper { input: Arc, @@ -385,9 +386,6 @@ crate::plan_to_language! { old_members: Arc, alias_to_cube: Vec<((String, String), String)>, }, - MergedMembersReplacer { - members: Vec, - }, ListConcatPushdownReplacer { members: Arc, }, @@ -1901,10 +1899,6 @@ fn member_pushdown_replacer( ) } -fn merged_members_replacer(members: impl Display) -> String { - format!("(MergedMembersReplacer {})", members) -} - fn list_concat_pushdown_replacer(members: impl Display) -> String { format!("(ListConcatPushdownReplacer {})", members) } @@ -2184,19 +2178,22 @@ fn cube_scan( can_pushdown_join: impl Display, wrapped: impl Display, ungrouped: impl Display, + join_hints: impl Display, ) -> String { format!( - "(CubeScan {} {} {} {} {} {} {} {} {} {})", - alias_to_cube, - members, - filters, - orders, - limit, - offset, - split, - can_pushdown_join, - wrapped, - ungrouped + r#"(CubeScan + {alias_to_cube} + {members} + {filters} + {orders} + {limit} + {offset} + {split} + {can_pushdown_join} + {wrapped} + {ungrouped} + {join_hints} + )"# ) } diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs index 264624cae1045..2e13f12aced64 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs @@ -85,6 +85,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), cube_scan( @@ -98,6 +99,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), self.push_down_filter_simplify("?expr"), ), @@ -117,6 +119,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), limit( @@ -133,6 +136,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), self.push_down_limit_filter( @@ -177,6 +181,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), ), @@ -196,6 +201,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), ), @@ -234,6 +240,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), "?alias", @@ -255,6 +262,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), "?alias", "?projection_split", @@ -274,6 +282,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), cube_scan( "?alias_to_cube", @@ -294,6 +303,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), self.push_down_filter("?alias_to_cube", "?filter_alias_to_cube", "?filter_aliases"), ), @@ -2468,6 +2478,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), cube_scan( "?source_table_name", @@ -2484,6 +2495,7 @@ impl RewriteRules for FilterRules { "?can_pushdown_join", "?wrapped", "?ungrouped", + "?join_hints", ), ), transforming_rewrite( diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/flatten/top_level.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/flatten/top_level.rs index 2951663ace473..a284dc170b0de 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/flatten/top_level.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/flatten/top_level.rs @@ -44,6 +44,7 @@ impl FlattenRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), ), ], @@ -65,6 +66,7 @@ impl FlattenRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?new_projection_alias", "ProjectionSplit:false", @@ -113,6 +115,7 @@ impl FlattenRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), ), ], @@ -128,6 +131,7 @@ impl FlattenRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), flatten_pushdown_replacer( "?outer_group_expr", diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/members.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/members.rs index fd2536903da42..d899a8d3cd6e9 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/members.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/members.rs @@ -5,11 +5,12 @@ use crate::{ agg_fun_expr, aggregate, alias_expr, all_members, analysis::{ConstantFolding, LogicalPlanData, Member, MemberNamesToExpr, OriginalExpr}, binary_expr, cast_expr, change_user_expr, column_expr, cross_join, cube_scan, - cube_scan_filters_empty_tail, cube_scan_members, cube_scan_members_empty_tail, - cube_scan_order_empty_tail, dimension_expr, distinct, expr_column_name, fun_expr, join, - like_expr, limit, list_concat_pushdown_replacer, list_concat_pushup_replacer, - literal_expr, literal_member, measure_expr, member_pushdown_replacer, member_replacer, - merged_members_replacer, original_expr_name, projection, referenced_columns, rewrite, + cube_scan_filters, cube_scan_filters_empty_tail, cube_scan_members, + cube_scan_members_empty_tail, cube_scan_order_empty_tail, dimension_expr, distinct, + expr_column_name, fun_expr, join, like_expr, limit, list_concat_pushdown_replacer, + list_concat_pushup_replacer, literal_expr, literal_member, measure_expr, + member_pushdown_replacer, member_replacer, original_expr_name, projection, + referenced_columns, rewrite, rewriter::{CubeEGraph, CubeRewrite, RewriteRules}, rules::{ replacer_flat_push_down_node_substitute_rules, replacer_push_down_node, @@ -20,17 +21,18 @@ use crate::{ udaf_expr, udf_expr, virtual_field_expr, AggregateFunctionExprDistinct, AggregateFunctionExprFun, AliasExprAlias, AllMembersAlias, AllMembersCube, BinaryExprOp, CastExprDataType, ColumnExprColumn, CubeScanAliasToCube, - CubeScanCanPushdownJoin, CubeScanLimit, CubeScanOffset, CubeScanUngrouped, - DimensionName, JoinLeftOn, JoinRightOn, LikeExprEscapeChar, LikeExprLikeType, - LikeExprNegated, LikeType, LimitFetch, LimitSkip, ListType, LiteralExprValue, - LiteralMemberRelation, LiteralMemberValue, LogicalPlanLanguage, MeasureName, - MemberErrorAliasToCube, MemberErrorError, MemberErrorPriority, + CubeScanCanPushdownJoin, CubeScanJoinHints, CubeScanLimit, CubeScanOffset, + CubeScanUngrouped, DimensionName, JoinLeftOn, JoinRightOn, LikeExprEscapeChar, + LikeExprLikeType, LikeExprNegated, LikeType, LimitFetch, LimitSkip, ListType, + LiteralExprValue, LiteralMemberRelation, LiteralMemberValue, LogicalPlanLanguage, + MeasureName, MemberErrorAliasToCube, MemberErrorError, MemberErrorPriority, MemberPushdownReplacerAliasToCube, MemberReplacerAliasToCube, ProjectionAlias, TableScanFetch, TableScanProjection, TableScanSourceTableName, TableScanTableName, TimeDimensionDateRange, TimeDimensionGranularity, TimeDimensionName, }, }, config::ConfigObj, + singular_eclass, sql::ColumnType, transport::{MetaContext, V1CubeMetaDimensionExt, V1CubeMetaExt, V1CubeMetaMeasureExt}, var, var_iter, var_list_iter, CubeError, @@ -47,6 +49,7 @@ use itertools::{EitherOrBoth, Itertools}; use std::{ collections::{HashMap, HashSet}, fmt::Display, + iter, ops::{Index, IndexMut}, sync::{Arc, LazyLock}, }; @@ -81,6 +84,7 @@ impl RewriteRules for MemberRules { "CubeScanCanPushdownJoin:true", "CubeScanWrapped:false", format!("CubeScanUngrouped:{}", self.enable_ungrouped), + "?cube_scan_join_hints", ), self.transform_table_scan( "?source_table_name", @@ -90,6 +94,7 @@ impl RewriteRules for MemberRules { "?fetch", "?alias_to_cube", "?cube_scan_members", + "?cube_scan_join_hints", ), ), self.measure_rewrite( @@ -150,6 +155,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?group_expr", "?aggr_expr", @@ -177,6 +183,7 @@ impl RewriteRules for MemberRules { "?new_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:false", + "?join_hints", ), self.push_down_non_empty_aggregate( "?alias_to_cube", @@ -205,6 +212,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?alias", "?projection_split", @@ -224,6 +232,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), self.push_down_projection( "?expr", @@ -250,6 +259,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), ), cube_scan( @@ -263,6 +273,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), self.push_down_limit( "?skip", @@ -286,6 +297,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "?left_ungrouped", + "?join_hints", )), cube_scan( "?alias_to_cube", @@ -298,6 +310,7 @@ impl RewriteRules for MemberRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:false", + "?join_hints", ), self.select_distinct_dimensions( "?alias_to_cube", @@ -327,52 +340,6 @@ impl RewriteRules for MemberRules { self.transform_like_expr("?like_type", "?negated", "?escape_char", "?op"), ), // Join - transforming_rewrite( - "push-down-cross-join-to-empty-scan", - cross_join( - cube_scan( - "?left_alias_to_cube", - cube_scan_members_empty_tail(), - cube_scan_filters_empty_tail(), - cube_scan_order_empty_tail(), - "?limit", - "?offset", - "CubeScanSplit:false", - "CubeScanCanPushdownJoin:true", - "CubeScanWrapped:false", - "CubeScanUngrouped:false", - ), - cube_scan( - "?right_alias_to_cube", - cube_scan_members_empty_tail(), - cube_scan_filters_empty_tail(), - cube_scan_order_empty_tail(), - "?limit", - "?offset", - "CubeScanSplit:false", - "CubeScanCanPushdownJoin:true", - "CubeScanWrapped:false", - "CubeScanUngrouped:false", - ), - ), - cube_scan( - "?joined_alias_to_cube", - cube_scan_members_empty_tail(), - cube_scan_filters_empty_tail(), - cube_scan_order_empty_tail(), - "?limit", - "?offset", - "CubeScanSplit:false", - "CubeScanCanPushdownJoin:true", - "CubeScanWrapped:false", - "CubeScanUngrouped:false", - ), - self.push_down_cross_join_to_empty_scan( - "?left_alias_to_cube", - "?right_alias_to_cube", - "?joined_alias_to_cube", - ), - ), self.push_down_cross_join_to_cubescan_rewrite( "not-merged-cubescans", "?left_members".to_string(), @@ -380,53 +347,44 @@ impl RewriteRules for MemberRules { "?left_members", "?right_members", ), - self.push_down_cross_join_to_cubescan_rewrite( - "merged-cubescan-left", - merged_members_replacer("?left_members"), - "?right_members".to_string(), - "?left_members", - "?right_members", - ), - self.push_down_cross_join_to_cubescan_rewrite( - "merged-cubescan-right", - "?left_members".to_string(), - merged_members_replacer("?right_members"), - "?left_members", - "?right_members", - ), - self.push_down_cross_join_to_cubescan_rewrite( - "merged-cubescans-both-sides", - merged_members_replacer("?left_members"), - merged_members_replacer("?right_members"), - "?left_members", - "?right_members", - ), + // It is incorrect to merge two CubeScan's into one after grouping + // __cubeJoinField is a representation of join from data model, + // and it is valid only for ungrouped queries to data source + // So CubeScanCanPushdownJoin and CubeScanUngrouped are fixed to true + // Limit and offset are not allowed + // Consider plan like Join(CubeScan(limit = 1), CubeScan(limit = 1)) + // Join would check only one row from left scan and only one from right + // And if they mismatch it will produce empty table + // There's no way to represent this as a single CubeScan + // Join does not guarantee ordering, so there's no point in keeping orders in RHS transforming_rewrite( - "join-to-cross-join", + "push-down-cube-join", join( cube_scan( "?left_alias_to_cube", "?left_members", "?left_filters", "?left_orders", - "?left_limit", - "?left_offset", + "CubeScanLimit:None", + "CubeScanOffset:None", "?left_split", - "?left_can_pushdown_join", + "CubeScanCanPushdownJoin:true", "CubeScanWrapped:false", - "?left_ungrouped", + "CubeScanUngrouped:true", + "?left_join_hints", ), cube_scan( "?right_alias_to_cube", "?right_members", "?right_filters", "?right_orders", - "?right_limit", - "?right_offset", + "CubeScanLimit:None", + "CubeScanOffset:None", "?right_split", - "?right_can_pushdown_join", + "CubeScanCanPushdownJoin:true", "CubeScanWrapped:false", - "?right_ungrouped", + "CubeScanUngrouped:true", + "?right_join_hints", ), "?left_on", "?right_on", @@ -434,33 +392,32 @@ impl RewriteRules for MemberRules { "?join_constraint", "?null_equals_null", ), - cross_join( - cube_scan( - "?left_alias_to_cube", - "?left_members", - "?left_filters", - "?left_orders", - "?left_limit", - "?left_offset", - "?left_split", - "?left_can_pushdown_join", - "CubeScanWrapped:false", - "?left_ungrouped", - ), - cube_scan( - "?right_alias_to_cube", - "?right_members", - "?right_filters", - "?right_orders", - "?right_limit", - "?right_offset", - "?right_split", - "?right_can_pushdown_join", - "CubeScanWrapped:false", - "?right_ungrouped", - ), + cube_scan( + "?out_alias_to_cube", + cube_scan_members("?left_members", "?right_members"), + cube_scan_filters("?left_filters", "?right_filters"), + cube_scan_order_empty_tail(), + "CubeScanLimit:None", + "CubeScanOffset:None", + // New CubeScan is treated as "not yet split", to give split rules one more chance + "CubeScanSplit:false", + "CubeScanCanPushdownJoin:true", + "CubeScanWrapped:false", + "CubeScanUngrouped:true", + "?out_join_hints", + ), + self.push_down_cube_join( + "?left_alias_to_cube", + "?right_alias_to_cube", + "?out_alias_to_cube", + "?left_members", + "?right_members", + "?left_on", + "?right_on", + "?left_join_hints", + "?right_join_hints", + "?out_join_hints", ), - self.join_to_cross_join("?left_on", "?right_on", "?left_members", "?right_members"), ), ]; @@ -1219,6 +1176,7 @@ impl MemberRules { table_scan_fetch_var: &'static str, alias_to_cube_var: &'static str, cube_scan_members_var: &'static str, + cube_scan_join_hints_var: &'static str, ) -> impl Fn(&mut CubeEGraph, &mut Subst) -> bool { let source_table_name_var = var!(source_table_name_var); let table_name_var = var!(table_name_var); @@ -1227,6 +1185,7 @@ impl MemberRules { let table_scan_fetch_var = var!(table_scan_fetch_var); let alias_to_cube_var = var!(alias_to_cube_var); let cube_scan_members_var = var!(cube_scan_members_var); + let cube_scan_join_hints_var = var!(cube_scan_join_hints_var); let meta_context = self.meta_context.clone(); move |egraph, subst| { for table_projection in var_iter!( @@ -1295,6 +1254,13 @@ impl MemberRules { ])), ); + subst.insert( + cube_scan_join_hints_var, + egraph.add(LogicalPlanLanguage::CubeScanJoinHints(CubeScanJoinHints( + vec![], + ))), + ); + return true; } } @@ -2627,191 +2593,106 @@ impl MemberRules { "count".to_string() } - fn push_down_cross_join_to_empty_scan( - &self, - left_alias_to_cube_var: &'static str, - right_alias_to_cube_var: &'static str, - joined_alias_to_cube_var: &'static str, - ) -> impl Fn(&mut CubeEGraph, &mut Subst) -> bool { - let left_alias_to_cube_var = var!(left_alias_to_cube_var); - let right_alias_to_cube_var = var!(right_alias_to_cube_var); - let joined_alias_to_cube_var = var!(joined_alias_to_cube_var); - move |egraph, subst| { - for left_alias_to_cube in - var_iter!(egraph[subst[left_alias_to_cube_var]], CubeScanAliasToCube).cloned() - { - for right_alias_to_cube in - var_iter!(egraph[subst[right_alias_to_cube_var]], CubeScanAliasToCube).cloned() - { - let joined_alias_to_cube = egraph.add( - LogicalPlanLanguage::CubeScanAliasToCube(CubeScanAliasToCube( - left_alias_to_cube - .into_iter() - .chain(right_alias_to_cube.into_iter()) - .collect(), - )), - ); - subst.insert(joined_alias_to_cube_var, joined_alias_to_cube); - - return true; - } - } - - false - } - } - fn push_down_cross_join_to_cube_scan( &self, left_alias_to_cube_var: &'static str, right_alias_to_cube_var: &'static str, joined_alias_to_cube_var: &'static str, - left_members_var: &'static str, - right_members_var: &'static str, - joined_members_var: &'static str, - left_filters_var: &'static str, - right_filters_var: &'static str, - new_filters_var: &'static str, - left_order_var: &'static str, - right_order_var: &'static str, - new_order_var: &'static str, - left_limit_var: &'static str, - right_limit_var: &'static str, - new_limit_var: &'static str, left_ungrouped_var: &'static str, right_ungrouped_var: &'static str, new_ungrouped_var: &'static str, + left_join_hints_var: &'static str, + right_join_hints_var: &'static str, + out_join_hints_var: &'static str, ) -> impl Fn(&mut CubeEGraph, &mut Subst) -> bool { let left_alias_to_cube_var = var!(left_alias_to_cube_var); let right_alias_to_cube_var = var!(right_alias_to_cube_var); let joined_alias_to_cube_var = var!(joined_alias_to_cube_var); - let left_members_var = var!(left_members_var); - let right_members_var = var!(right_members_var); - let joined_members_var = var!(joined_members_var); - let left_filters_var = var!(left_filters_var); - let right_filters_var = var!(right_filters_var); - let new_filters_var = var!(new_filters_var); - let left_order_var = var!(left_order_var); - let right_order_var = var!(right_order_var); - let new_order_var = var!(new_order_var); - let left_limit_var = var!(left_limit_var); - let right_limit_var = var!(right_limit_var); - let new_limit_var = var!(new_limit_var); let left_ungrouped_var = var!(left_ungrouped_var); let right_ungrouped_var = var!(right_ungrouped_var); let new_ungrouped_var = var!(new_ungrouped_var); + let left_join_hints_var = var!(left_join_hints_var); + let right_join_hints_var = var!(right_join_hints_var); + let out_join_hints_var = var!(out_join_hints_var); move |egraph, subst| { + let Some(left_ungrouped) = + singular_eclass!(egraph[subst[left_ungrouped_var]], CubeScanUngrouped).copied() + else { + return false; + }; + let Some(right_ungrouped) = + singular_eclass!(egraph[subst[right_ungrouped_var]], CubeScanUngrouped).copied() + else { + return false; + }; + for left_alias_to_cube in var_iter!(egraph[subst[left_alias_to_cube_var]], CubeScanAliasToCube).cloned() { for right_alias_to_cube in var_iter!(egraph[subst[right_alias_to_cube_var]], CubeScanAliasToCube).cloned() { - for left_members in - var_list_iter!(egraph[subst[left_members_var]], CubeScanMembers).cloned() + for left_join_hints in + var_iter!(egraph[subst[left_join_hints_var]], CubeScanJoinHints) { - for right_members in - var_list_iter!(egraph[subst[right_members_var]], CubeScanMembers) - .cloned() + for right_join_hints in + var_iter!(egraph[subst[right_join_hints_var]], CubeScanJoinHints) { - // push_down_cross_join_to_empty_scan works in this case - if left_members.is_empty() && right_members.is_empty() { - continue; - } - - let left_limit = - match var_iter!(egraph[subst[left_limit_var]], CubeScanLimit) - .cloned() - .next() - { - Some(limit) => limit, - None => continue, - }; + // This is CrossJoin(CubeScan,CubeScan), so there's no way to determine proper join hint + // It means that when there are several cubes on each side, we have to choose one + // When there are several cubes on the left, we should just choose last + // For a chained join query (cube1 CROSS JOIN cube2 CROSS JOIN ...) right CubeScan would always have single cube + // So this would choose last cube from last join hint on the left, and first cube on the right - let right_limit = - match var_iter!(egraph[subst[right_limit_var]], CubeScanLimit) - .cloned() - .next() - { - Some(limit) => limit, - None => continue, - }; - - // TODO handle the case when limit set on non multiplied cube. It's possible to push down the limit in this case. - if left_limit.is_some() || right_limit.is_some() { + let Some(left_cube) = left_join_hints + .iter() + .filter(|hint| !hint.is_empty()) + .last() + .and_then(|hint| hint.last()) + .or_else(|| left_alias_to_cube.first().map(|(_, cube)| cube)) + .cloned() + else { continue; - } - - let is_left_order_empty = - Some(true) == egraph[subst[left_order_var]].data.is_empty_list; - - let is_right_order_empty = - Some(true) == egraph[subst[right_order_var]].data.is_empty_list; - - if !is_left_order_empty && !is_right_order_empty { + }; + let Some(right_cube) = + right_alias_to_cube.first().map(|(_, cube)| cube).cloned() + else { continue; - } + }; - for left_ungrouped in - var_iter!(egraph[subst[left_ungrouped_var]], CubeScanUngrouped) + let out_join_hints = CubeScanJoinHints( + left_join_hints + .iter() + .chain(right_join_hints.iter()) .cloned() - { - for right_ungrouped in - var_iter!(egraph[subst[right_ungrouped_var]], CubeScanUngrouped) - .cloned() - { - subst.insert( - joined_alias_to_cube_var, - egraph.add(LogicalPlanLanguage::CubeScanAliasToCube( - CubeScanAliasToCube( - left_alias_to_cube - .into_iter() - .chain(right_alias_to_cube.into_iter()) - .collect(), - ), - )), - ); - - let joined_members = - egraph.add(LogicalPlanLanguage::CubeScanMembers(vec![ - subst[left_members_var], - subst[right_members_var], - ])); - - subst.insert(joined_members_var, joined_members); - - subst.insert( - new_filters_var, - egraph.add(LogicalPlanLanguage::CubeScanFilters(vec![ - subst[left_filters_var], - subst[right_filters_var], - ])), - ); - - let orders = if is_left_order_empty { - subst[right_order_var] - } else { - subst[left_order_var] - }; + .chain(iter::once(vec![left_cube, right_cube])) + .collect(), + ); - subst.insert( - new_limit_var, - egraph.add(LogicalPlanLanguage::CubeScanLimit( - CubeScanLimit(None), - )), - ); + subst.insert( + joined_alias_to_cube_var, + egraph.add(LogicalPlanLanguage::CubeScanAliasToCube( + CubeScanAliasToCube( + left_alias_to_cube + .into_iter() + .chain(right_alias_to_cube.into_iter()) + .collect(), + ), + )), + ); - subst.insert(new_order_var, orders); + let joined_ungrouped = + egraph.add(LogicalPlanLanguage::CubeScanUngrouped( + CubeScanUngrouped(left_ungrouped && right_ungrouped), + )); + subst.insert(new_ungrouped_var, joined_ungrouped); - let joined_ungrouped = - egraph.add(LogicalPlanLanguage::CubeScanUngrouped( - CubeScanUngrouped(left_ungrouped && right_ungrouped), - )); - subst.insert(new_ungrouped_var, joined_ungrouped); + subst.insert( + out_join_hints_var, + egraph.add(LogicalPlanLanguage::CubeScanJoinHints(out_join_hints)), + ); - return true; - } - } + return true; } } } @@ -2821,26 +2702,89 @@ impl MemberRules { } } - fn join_to_cross_join( + fn push_down_cube_join( &self, + left_alias_to_cube_var: &'static str, + right_alias_to_cube_var: &'static str, + out_alias_to_cube_var: &'static str, + left_members_var: &'static str, + right_members_var: &'static str, left_on_var: &'static str, right_on_var: &'static str, - left_aliases_var: &'static str, - right_aliases_var: &'static str, + left_join_hints_var: &'static str, + right_join_hints_var: &'static str, + out_join_hints_var: &'static str, ) -> impl Fn(&mut CubeEGraph, &mut Subst) -> bool { + let left_alias_to_cube_var = var!(left_alias_to_cube_var); + let right_alias_to_cube_var = var!(right_alias_to_cube_var); + let out_alias_to_cube_var = var!(out_alias_to_cube_var); + let left_members_var = var!(left_members_var); + let right_members_var = var!(right_members_var); let left_on_var = var!(left_on_var); let right_on_var = var!(right_on_var); - let left_aliases_var = var!(left_aliases_var); - let right_aliases_var = var!(right_aliases_var); + let left_join_hints_var = var!(left_join_hints_var); + let right_join_hints_var = var!(right_join_hints_var); + let out_join_hints_var = var!(out_join_hints_var); move |egraph, subst| { - is_proper_cube_join_condition( + let Some((left_cube, right_cube)) = is_proper_cube_join_condition( egraph, subst, - left_aliases_var, + left_members_var, left_on_var, - right_aliases_var, + right_members_var, right_on_var, - ) + ) else { + return false; + }; + + for left_alias_to_cube in + var_iter!(egraph[subst[left_alias_to_cube_var]], CubeScanAliasToCube) + { + for right_alias_to_cube in + var_iter!(egraph[subst[right_alias_to_cube_var]], CubeScanAliasToCube) + { + for left_join_hints in + var_iter!(egraph[subst[left_join_hints_var]], CubeScanJoinHints) + { + for right_join_hints in + var_iter!(egraph[subst[right_join_hints_var]], CubeScanJoinHints) + { + let out_alias_to_cube = CubeScanAliasToCube( + left_alias_to_cube + .iter() + .chain(right_alias_to_cube.iter()) + .cloned() + .collect(), + ); + + let out_join_hints = CubeScanJoinHints( + left_join_hints + .iter() + .chain(right_join_hints.iter()) + .cloned() + .chain(iter::once(vec![left_cube, right_cube])) + .collect(), + ); + + subst.insert( + out_alias_to_cube_var, + egraph.add(LogicalPlanLanguage::CubeScanAliasToCube( + out_alias_to_cube, + )), + ); + + subst.insert( + out_join_hints_var, + egraph.add(LogicalPlanLanguage::CubeScanJoinHints(out_join_hints)), + ); + + return true; + } + } + } + } + + false } } @@ -2852,6 +2796,7 @@ impl MemberRules { left_members: &'static str, right_members: &'static str, ) -> CubeRewrite { + // TODO handle the case when limit set on non multiplied cube. It's possible to push down the limit in this case. transforming_rewrite( &format!("push-down-cross-join-to-cube-scan-{}", name), cross_join( @@ -2860,57 +2805,51 @@ impl MemberRules { left_members_expr, "?left_filters", "?left_order", - "?left_limit", + "CubeScanLimit:None", "CubeScanOffset:None", "CubeScanSplit:false", "CubeScanCanPushdownJoin:true", "CubeScanWrapped:false", "?left_ungrouped", + "?left_join_hints", ), cube_scan( "?right_alias_to_cube", right_members_expr, "?right_filters", "?right_order", - "?right_limit", + "CubeScanLimit:None", "CubeScanOffset:None", "CubeScanSplit:false", "CubeScanCanPushdownJoin:true", "CubeScanWrapped:false", "?right_ungrouped", + "?right_join_hints", ), ), cube_scan( "?joined_alias_to_cube", - "?joined_members", - "?joined_filters", - "?new_order", - "?new_limit", + cube_scan_members(left_members, right_members), + cube_scan_filters("?left_filters", "?right_filters"), + cube_scan_order_empty_tail(), + "CubeScanLimit:None", "CubeScanOffset:None", "CubeScanSplit:false", "CubeScanCanPushdownJoin:true", "CubeScanWrapped:false", "?new_ungrouped", + "?out_join_hints", ), self.push_down_cross_join_to_cube_scan( "?left_alias_to_cube", "?right_alias_to_cube", "?joined_alias_to_cube", - left_members, - right_members, - "?joined_members", - "?left_filters", - "?right_filters", - "?joined_filters", - "?left_order", - "?right_order", - "?new_order", - "?left_limit", - "?right_limit", - "?new_limit", "?left_ungrouped", "?right_ungrouped", "?new_ungrouped", + "?left_join_hints", + "?right_join_hints", + "?out_join_hints", ), ) } @@ -3004,20 +2943,32 @@ pub fn min_granularity(granularity_a: &String, granularity_b: &String) -> Option } } -fn find_column_by_alias<'mn>( - column_name: &str, - member_names_to_expr: &'mn mut MemberNamesToExpr, - cube_alias: &str, -) -> Option<&'mn str> { - if let Some((tuple, _)) = LogicalPlanData::do_find_member_by_alias( - member_names_to_expr, - &format!("{}.{}", cube_alias, column_name), - ) { - return tuple.0.as_deref(); +// Return None if `join_on` is not a __cubeJoinField +// Return Some(cube_name) if it is +fn is_join_on_cube_join_field( + egraph: &mut CubeEGraph, + subst: &Subst, + cube_members_var: Var, + join_on: &[Column], +) -> Option { + if join_on.len() != 1 { + return None; } - None + let join_on = &join_on[0]; + let ((_, join_member, _), _) = egraph[subst[cube_members_var]] + .data + .find_member_by_column(join_on)?; + let Member::VirtualField { name, cube, .. } = join_member else { + return None; + }; + if name != "__cubeJoinField" { + return None; + } + Some(cube.clone()) } +// Return None if condition is not a left.__cubeJoinField = right.__cubeJoinField +// Return Some((left_cube_name, right_cube_name)) if it is fn is_proper_cube_join_condition( egraph: &mut CubeEGraph, subst: &Subst, @@ -3025,22 +2976,16 @@ fn is_proper_cube_join_condition( left_on_var: Var, right_cube_members_var: Var, right_on_var: Var, -) -> bool { - if egraph[subst[left_cube_members_var]] +) -> Option<(String, String)> { + egraph[subst[left_cube_members_var]] .data .member_name_to_expr - .is_none() - { - return false; - } + .as_ref()?; - if egraph[subst[right_cube_members_var]] + egraph[subst[right_cube_members_var]] .data .member_name_to_expr - .is_none() - { - return false; - } + .as_ref()?; let left_join_ons = var_iter!(egraph[subst[left_on_var]], JoinLeftOn) .cloned() @@ -3052,63 +2997,21 @@ fn is_proper_cube_join_condition( // For now this allows only exact left.__cubeJoinField = right.__cubeJoinField // TODO implement more complex conditions - for left_join_on in &left_join_ons { - if left_join_on.len() != 1 { - continue; - } - - let left_join_on = &left_join_on[0]; - - let left_member_names_to_expr = &mut egraph[subst[left_cube_members_var]] - .data - .member_name_to_expr - .as_mut() - .unwrap(); - - let mut left_column_name = left_join_on.name.as_str(); - if let Some(name) = find_column_by_alias( - left_column_name, - left_member_names_to_expr, - left_join_on.relation.as_deref().unwrap_or_default(), - ) { - left_column_name = name.rsplit_once(".").unwrap().1; - } - - if left_column_name != "__cubeJoinField" { - continue; - } - - for right_join_on in &right_join_ons { - if right_join_on.len() != 1 { - continue; - } - - let right_join_on = &right_join_on[0]; - - let right_member_names_to_expr = &mut egraph[subst[right_cube_members_var]] - .data - .member_name_to_expr - .as_mut() - .unwrap(); - - let mut right_column_name = right_join_on.name.as_str(); - if let Some(name) = find_column_by_alias( - right_column_name, - right_member_names_to_expr, - right_join_on.relation.as_deref().unwrap_or_default(), - ) { - right_column_name = name.rsplit_once(".").unwrap().1; - } - - if right_column_name != "__cubeJoinField" { - continue; - } + let left_cube = left_join_ons + .iter() + .filter_map(|left_join_on| { + is_join_on_cube_join_field(egraph, subst, left_cube_members_var, left_join_on) + }) + .next()?; - return true; - } - } + let right_cube = right_join_ons + .iter() + .filter_map(|right_join_on| { + is_join_on_cube_join_field(egraph, subst, right_cube_members_var, right_join_on) + }) + .next()?; - false + Some((left_cube, right_cube)) } #[cfg(test)] diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/old_split.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/old_split.rs index e784fe33f91ae..c5ab6fb1ac110 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/old_split.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/old_split.rs @@ -55,6 +55,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?group_expr", "?aggr_expr", @@ -77,6 +78,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), inner_aggregate_split_replacer("?group_expr", "?inner_aggregate_cube"), inner_aggregate_split_replacer("?aggr_expr", "?inner_aggregate_cube"), @@ -110,6 +112,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:false", + "?join_hints", ), "?alias", "ProjectionSplit:false", @@ -129,6 +132,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:false", + "?join_hints", ), "?projection_alias", "ProjectionSplit:true", @@ -160,6 +164,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:true", + "?join_hints", ), "?alias", "ProjectionSplit:false", @@ -179,6 +184,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:true", + "?join_hints", ), "?projection_alias", "ProjectionSplit:true", @@ -210,6 +216,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:false", + "?join_hints", ), "?alias", "ProjectionSplit:false", @@ -230,6 +237,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:false", + "?join_hints", ), "?inner_projection_alias", "ProjectionSplit:true", @@ -265,6 +273,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?group_expr", "?aggr_expr", @@ -283,6 +292,7 @@ impl RewriteRules for OldSplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), inner_aggregate_split_replacer("?group_expr", "?inner_aggregate_cube"), inner_aggregate_split_replacer("?aggr_expr", "?inner_aggregate_cube"), diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/order.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/order.rs index bccc2cfbba892..9dfa0f5550f0f 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/order.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/order.rs @@ -32,6 +32,7 @@ impl RewriteRules for OrderRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), ), cube_scan( @@ -45,6 +46,7 @@ impl RewriteRules for OrderRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), self.push_down_sort("?expr", "?members", "?aliases"), ), diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/split/top_level.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/split/top_level.rs index a453baab31b82..6eebf9db7ef2f 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/split/top_level.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/split/top_level.rs @@ -29,6 +29,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?group_expr", "?aggr_expr", @@ -46,6 +47,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), projection_split_pushdown_replacer( "?group_expr", @@ -82,6 +84,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), projection_split_pullup_replacer( "?inner_group_expr", @@ -111,6 +114,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?inner_group_expr", "?inner_aggr_expr", @@ -141,6 +145,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?group_expr", "?aggr_expr", @@ -158,6 +163,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), aggregate_split_pushdown_replacer( "?group_expr", @@ -193,6 +199,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), aggregate_split_pullup_replacer( "?inner_group_expr", @@ -221,6 +228,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), "?inner_group_expr", "?inner_aggr_expr", @@ -247,6 +255,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:true", + "?join_hints", ), "?projection_alias", "ProjectionSplit:false", @@ -263,6 +272,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:true", + "?join_hints", ), aggregate_split_pushdown_replacer( "?projection_expr", @@ -295,6 +305,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:true", + "?join_hints", ), aggregate_split_pullup_replacer( "?inner_expr", @@ -320,6 +331,7 @@ impl SplitRules { "?can_pushdown_join", "CubeScanWrapped:false", "CubeScanUngrouped:true", + "?join_hints", ), "?projection_alias", "ProjectionSplit:true", diff --git a/rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/cube_scan_wrapper.rs b/rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/cube_scan_wrapper.rs index 85ba618faf873..9bcfe471e1a7f 100644 --- a/rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/cube_scan_wrapper.rs +++ b/rust/cubesql/cubesql/src/compile/rewrite/rules/wrapper/cube_scan_wrapper.rs @@ -31,6 +31,7 @@ impl WrapperRules { "?can_pushdown_join", "CubeScanWrapped:false", "?ungrouped", + "?join_hints", ), cube_scan_wrapper( wrapper_pullup_replacer( @@ -45,6 +46,7 @@ impl WrapperRules { "?can_pushdown_join", "CubeScanWrapped:true", "?ungrouped", + "?join_hints", ), wrapper_replacer_context( "?alias_to_cube_out", diff --git a/rust/cubesql/cubesql/src/compile/test/test_cube_join.rs b/rust/cubesql/cubesql/src/compile/test/test_cube_join.rs index 281283f515ba2..f0dc5d2f648ce 100644 --- a/rust/cubesql/cubesql/src/compile/test/test_cube_join.rs +++ b/rust/cubesql/cubesql/src/compile/test/test_cube_join.rs @@ -48,6 +48,10 @@ async fn powerbi_join() { dimensions: Some(vec!["KibanaSampleDataEcommerce.customer_gender".to_string()]), segments: Some(vec![]), order: Some(vec![]), + join_hints: Some(vec![vec![ + "Logs".to_string(), + "KibanaSampleDataEcommerce".to_string(), + ],]), ..Default::default() } ); @@ -118,6 +122,13 @@ async fn powerbi_transitive_join() { dimensions: Some(vec!["Logs.content".to_string()]), segments: Some(vec![]), order: Some(vec![]), + join_hints: Some(vec![ + vec!["NumberCube".to_string(), "Logs".to_string(),], + vec![ + "KibanaSampleDataEcommerce".to_string(), + "NumberCube".to_string(), + ], + ]), ..Default::default() } ); @@ -171,6 +182,10 @@ async fn test_join_three_cubes() { segments: Some(vec![]), order: Some(vec![]), ungrouped: Some(true), + join_hints: Some(vec![ + vec!["KibanaSampleDataEcommerce".to_string(), "Logs".to_string(),], + vec!["Logs".to_string(), "NumberCube".to_string(),], + ]), ..Default::default() } ) @@ -217,6 +232,10 @@ async fn test_join_three_cubes_split() { or: None, and: None }]), + join_hints: Some(vec![ + vec!["KibanaSampleDataEcommerce".to_string(), "Logs".to_string(),], + vec!["Logs".to_string(), "NumberCube".to_string(),], + ]), ..Default::default() } ) @@ -246,10 +265,7 @@ async fn test_join_two_subqueries_with_filter_order_limit() { measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string(),]), dimensions: Some(vec!["Logs.read".to_string(),]), segments: Some(vec![]), - order: Some(vec![vec![ - "KibanaSampleDataEcommerce.customer_gender".to_string(), - "asc".to_string(), - ]]), + order: Some(vec![]), filters: Some(vec![ V1LoadRequestQueryFilterItem { member: Some("KibanaSampleDataEcommerce.customer_gender".to_string()), @@ -266,6 +282,10 @@ async fn test_join_two_subqueries_with_filter_order_limit() { and: None } ]), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ) @@ -301,10 +321,7 @@ async fn test_join_three_subqueries_with_filter_order_limit_and_split() { granularity: Some("month".to_owned()), date_range: None }]), - order: Some(vec![vec![ - "KibanaSampleDataEcommerce.customer_gender".to_string(), - "asc".to_string(), - ]]), + order: Some(vec![]), filters: Some(vec![ V1LoadRequestQueryFilterItem { member: Some("KibanaSampleDataEcommerce.customer_gender".to_string()), @@ -321,6 +338,10 @@ async fn test_join_three_subqueries_with_filter_order_limit_and_split() { and: None } ]), + join_hints: Some(vec![ + vec!["KibanaSampleDataEcommerce".to_string(), "Logs".to_string(),], + vec!["Logs".to_string(), "NumberCube".to_string(),], + ]), ..Default::default() } ) @@ -350,10 +371,7 @@ async fn test_join_subquery_and_table_with_filter_order_limit() { measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string(),]), dimensions: Some(vec!["Logs.read".to_string(),]), segments: Some(vec![]), - order: Some(vec![vec![ - "KibanaSampleDataEcommerce.customer_gender".to_string(), - "asc".to_string(), - ]]), + order: Some(vec![]), filters: Some(vec![ V1LoadRequestQueryFilterItem { member: Some("KibanaSampleDataEcommerce.customer_gender".to_string()), @@ -370,6 +388,10 @@ async fn test_join_subquery_and_table_with_filter_order_limit() { and: None } ]), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ) @@ -405,10 +427,7 @@ async fn test_join_two_subqueries_and_table_with_filter_order_limit_and_split() granularity: Some("month".to_owned()), date_range: None }]), - order: Some(vec![vec![ - "KibanaSampleDataEcommerce.customer_gender".to_string(), - "asc".to_string(), - ]]), + order: Some(vec![]), filters: Some(vec![ V1LoadRequestQueryFilterItem { member: Some("KibanaSampleDataEcommerce.customer_gender".to_string()), @@ -425,6 +444,10 @@ async fn test_join_two_subqueries_and_table_with_filter_order_limit_and_split() and: None } ]), + join_hints: Some(vec![ + vec!["KibanaSampleDataEcommerce".to_string(), "Logs".to_string(),], + vec!["Logs".to_string(), "NumberCube".to_string(),], + ]), ..Default::default() } ) @@ -486,6 +509,10 @@ async fn test_join_two_subqueries_filter_push_down() { and: None } ]), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ) @@ -605,6 +632,10 @@ FROM ]), order: Some(vec![]), ungrouped: Some(true), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ]]), ..Default::default() }; diff --git a/rust/cubesql/cubesql/src/compile/test/test_user_change.rs b/rust/cubesql/cubesql/src/compile/test/test_user_change.rs index d5746f6a01336..e20c1f01d7bc3 100644 --- a/rust/cubesql/cubesql/src/compile/test/test_user_change.rs +++ b/rust/cubesql/cubesql/src/compile/test/test_user_change.rs @@ -193,6 +193,10 @@ async fn test_user_with_join() { segments: Some(vec![]), order: Some(vec![]), ungrouped: Some(true), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } ); diff --git a/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs b/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs index f7adf69f90d12..3a1a68edd0470 100644 --- a/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs +++ b/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs @@ -1147,6 +1147,10 @@ WHERE ]), segments: Some(vec![]), order: Some(vec![]), + join_hints: Some(vec![vec![ + "KibanaSampleDataEcommerce".to_string(), + "Logs".to_string(), + ],]), ..Default::default() } );