diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index e8adfe07ace2a..7c9974163c192 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -692,9 +692,11 @@ export class BaseQuery { rowLimit: this.options.rowLimit ? this.options.rowLimit.toString() : null, offset: this.options.offset ? this.options.offset.toString() : null, baseTools: this, - ungrouped: this.options.ungrouped + ungrouped: this.options.ungrouped, + exportAnnotatedSql: exportAnnotatedSql === true }; + const buildResult = nativeBuildSqlAndParams(queryParams); if (buildResult.error) { diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts index 342fc82b73f15..89249f52aeb0f 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts @@ -5,7 +5,7 @@ import { PostgresQuery } from '../../../src/adapter/PostgresQuery'; import { prepareYamlCompiler } from '../../unit/PrepareCompiler'; import { dbRunner } from './PostgresDBRunner'; -describe('Member Expression', () => { +describe('Member Expression Multistage', () => { jest.setTimeout(200000); const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(` @@ -37,6 +37,170 @@ cubes: - name: count type: count + - name: orders + sql: > + select 10 AS ID, 'complited' AS STATUS, '2021-01-05 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 50.0 as revenue + UNION ALL + select 11 AS ID, 'complited' AS STATUS, '2021-05-01 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 150.0 as revenue + UNION ALL + select 12 AS ID, 'complited' AS STATUS, '2021-06-01 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 200.0 as revenue + UNION ALL + select 13 AS ID, 'complited' AS STATUS, '2022-01-04 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 10.0 as revenue + UNION ALL + select 14 AS ID, 'complited' AS STATUS, '2022-05-04 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 30.0 as revenue + public: false + + joins: + - name: line_items + sql: "{CUBE}.ID = {line_items}.order_id" + relationship: many_to_one + + - name: customers + sql: "{CUBE}.CUSTOMER_ID = {customers}.ID" + relationship: many_to_one + + dimensions: + - name: id + sql: ID + type: number + primary_key: true + + - name: status + sql: STATUS + type: string + + - name: date + sql: CREATED_AT + type: time + + - name: amount + sql: '{line_items.total_amount}' + type: number + sub_query: true + + measures: + - name: count + type: count + + - name: completed_count + type: count + filters: + - sql: "{CUBE}.STATUS = 'completed'" + + - name: returned_count + type: count + filters: + - sql: "{CUBE}.STATUS = 'returned'" + + - name: return_rate + type: number + sql: "({returned_count} / NULLIF({completed_count}, 0)) * 100.0" + description: "Percentage of returned orders out of completed, exclude just placed orders." + format: percent + + - name: total_amount + sql: '{CUBE.amount}' + type: sum + + - name: revenue + sql: "revenue" + type: sum + format: currency + + - name: average_order_value + sql: '{CUBE.amount}' + type: avg + + - name: revenue_1_y_ago + sql: "{revenue}" + multi_stage: true + type: number + format: currency + time_shift: + - time_dimension: date + interval: 1 year + type: prior + - time_dimension: orders_view.date + interval: 1 year + type: prior + + - name: cagr_1_y + sql: "(({revenue} / {revenue_1_y_ago}) - 1)" + type: number + format: percent + description: "Annual CAGR, year over year growth in revenue" + + - name: line_items + sql: > + SELECT 10 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 10 as order_id + UNION ALL + SELECT 11 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 11 as order_id + UNION ALL + SELECT 12 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 11 as order_id + UNION ALL + SELECT 13 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 12 as order_id + public: false + + joins: + - name: products + sql: "{CUBE}.PRODUCT_ID = {products}.ID" + relationship: many_to_one + + dimensions: + - name: id + sql: ID + type: number + primary_key: true + + - name: created_at + sql: CREATED_AT + type: time + + - name: price + sql: "{products.price}" + type: number + + measures: + - name: count + type: count + + - name: total_amount + sql: "{price}" + type: sum + - name: products + sql: > + SELECT 10 AS ID, 'Clothes' AS PRODUCT_CATEGORY, 'Shirt' AS NAME, 10 AS PRICE + UNION ALL + SELECT 11 AS ID, 'Clothes' AS PRODUCT_CATEGORY, 'Shirt' AS NAME, 20 AS PRICE + public: false + description: > + Products and categories in our e-commerce store. + + dimensions: + - name: id + sql: ID + type: number + primary_key: true + + - name: product_category + sql: PRODUCT_CATEGORY + type: string + + - name: name + sql: NAME + type: string + + - name: price + sql: PRICE + type: number + + measures: + - name: count + type: count + + + + views: - name: customers_view @@ -47,6 +211,29 @@ views: - city + - name: orders_view + cubes: + - join_path: orders + includes: + - count + - date + - revenue + - cagr_1_y + - return_rate + + - join_path: line_items.products + prefix: true + includes: + - product_category + + - join_path: orders.customers + prefix: true + includes: + - city + - count + - id + + `); async function runQueryTest(q, expectedResult) { @@ -124,4 +311,77 @@ views: }, [{ count: 1, city: 'New York', cubejoinfield: 'NULL' }, { count: 1, city: 'New York', cubejoinfield: 'NULL' }])); + if (getEnv('nativeSqlPlanner')) { + it('member expression multi stage', async () => runQueryTest({ + measures: [ + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'orders', + // eslint-disable-next-line no-template-curly-in-string + 'return `${orders.cagr_1_y}`' + ), + // eslint-disable-next-line no-template-curly-in-string + definition: '${orders.cagr_1_y}', + expressionName: 'orders__cagr_2023', + cubeName: 'orders', + }, + ], + timeDimensions: [ + { + dimension: 'orders.date', + dateRange: ['2022-01-01', '2022-10-31'], + }, + ], + timezone: 'America/Los_Angeles' + }, + + [{ orders__cagr_2023: '-0.90000000000000000000' }])); + } else { + it.skip('member expression multi stage', () => { + // Skipping because it works only in Tesseract + }); + } + + if (getEnv('nativeSqlPlanner')) { + it('member expression multi stage with time dimension segment', async () => runQueryTest({ + measures: [ + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'orders', + // eslint-disable-next-line no-template-curly-in-string + 'return `${orders.cagr_1_y}`' + ), + // eslint-disable-next-line no-template-curly-in-string + definition: '${orders.cagr_1_y}', + expressionName: 'orders__cagr_2023', + cubeName: 'orders', + }, + ], + segments: [ + { + cubeName: 'orders', + name: 'orders_date____c', + expressionName: 'orders_date____c', + // eslint-disable-next-line no-new-func + expression: new Function( + 'orders', + // eslint-disable-next-line no-template-curly-in-string + 'return `((${orders.date} >= CAST(\'2022-01-01\' AS TIMESTAMP)) AND (${orders.date} < CAST(\'2022-10-31\' AS TIMESTAMP)))`' + ), + // eslint-disable-next-line no-template-curly-in-string + definition: '{"cube_name":"orders","alias":"orders_date____c","cube_params":["orders"],"expr":"((${orders.date} >= CAST($0$ AS TIMESTAMP)) AND (${orders.date} < CAST($1$ AS TIMESTAMP)))","grouping_set":null}', + } + ], + + timezone: 'America/Los_Angeles' + }, + + [{ orders__cagr_2023: '-0.90000000000000000000' }])); + } else { + it.skip('member expression multi stage with time dimension segment', () => { + // Skipping because it works only in Tesseract + }); + } }); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs index 8a0f1907928f3..4d7e20ab83d8e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs @@ -59,6 +59,8 @@ pub struct BaseQueryOptionsStatic { pub row_limit: Option, pub offset: Option, pub ungrouped: Option, + #[serde(rename = "exportAnnotatedSql")] + pub export_annotated_sql: bool, } #[nativebridge::native_bridge(BaseQueryOptionsStatic)] diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_dimension.rs index f02185f732e1d..bbc1118beabd0 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_dimension.rs @@ -81,6 +81,23 @@ impl BaseDimension { default_alias, })) } + MemberSymbol::MemberExpression(expression_symbol) => { + let full_name = expression_symbol.full_name(); + let cube_name = expression_symbol.cube_name().clone(); + let name = expression_symbol.name().clone(); + let member_expression_definition = expression_symbol.definition().clone(); + let default_alias = PlanSqlTemplates::alias_name(&name); + Some(Rc::new(Self { + dimension: full_name, + query_tools: query_tools.clone(), + member_evaluator: evaluation_node, + definition: None, + cube_name, + name, + member_expression_definition, + default_alias, + })) + } _ => None, }; Ok(result) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs index 4a1941e806842..d903e5ab49e83 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs @@ -113,7 +113,7 @@ impl BaseMember for BaseMeasure { } fn full_name(&self) -> String { - format!("{}.{}", self.cube_name, self.name) + self.member_evaluator.full_name() } fn cube_name(&self) -> &String { @@ -151,6 +151,24 @@ impl BaseMeasure { default_alias, })) } + MemberSymbol::MemberExpression(expression_symbol) => { + let full_name = expression_symbol.full_name(); + let cube_name = expression_symbol.cube_name().clone(); + let name = expression_symbol.name().clone(); + let member_expression_definition = expression_symbol.definition().clone(); + let default_alias = PlanSqlTemplates::alias_name(&name); + Some(Rc::new(Self { + measure: full_name, + query_tools: query_tools.clone(), + member_evaluator: evaluation_node, + definition: None, + cube_name, + name, + member_expression_definition, + default_alias, + time_shifts: vec![], + })) + } _ => None, }; Ok(res) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_query.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_query.rs index 21de374816760..b2c016e225201 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_query.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_query.rs @@ -27,6 +27,7 @@ impl BaseQuery { options.base_tools()?, options.join_graph()?, options.static_data().timezone.clone(), + options.static_data().export_annotated_sql, )?; let request = QueryProperties::try_new(query_tools.clone(), options)?; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs index 9699cbb8bd797..b9fb6a2b6e5a3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/params_allocator.rs @@ -11,13 +11,15 @@ lazy_static! { pub struct ParamsAllocator { sql_templates: PlanSqlTemplates, params: Vec, + export_annotated_sql: bool, } impl ParamsAllocator { - pub fn new(sql_templates: PlanSqlTemplates) -> ParamsAllocator { + pub fn new(sql_templates: PlanSqlTemplates, export_annotated_sql: bool) -> ParamsAllocator { ParamsAllocator { sql_templates, params: Vec::new(), + export_annotated_sql, } } @@ -56,13 +58,17 @@ impl ParamsAllocator { param_index_map.insert(ind, index); index }; - match self.sql_templates.param(new_index) { - Ok(res) => res, - Err(e) => { - if error.is_none() { - error = Some(e); + if self.export_annotated_sql { + format!("${}$", new_index) + } else { + match self.sql_templates.param(new_index) { + Ok(res) => res, + Err(e) => { + if error.is_none() { + error = Some(e); + } + "$error$".to_string() } - "$error$".to_string() } } }) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs index a2bb002124886..2f3cacf9dcccf 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs @@ -97,6 +97,7 @@ impl DimensionSubqueryPlanner { None, self.query_tools.clone(), )?; + let measure_expression_full_name = measure.full_name(); let (dimensions_filters, time_dimensions_filters) = if subquery_dimension .propagate_filters_to_sub_query() @@ -144,7 +145,10 @@ impl DimensionSubqueryPlanner { }) .collect_vec(); - if let Some(dim_ref) = sub_query.schema().resolve_member_reference(&dim_full_name) { + if let Some(dim_ref) = sub_query + .schema() + .resolve_member_reference(&measure_expression_full_name) + { let qualified_column_name = QualifiedColumnName::new(Some(sub_query_alias.clone()), dim_ref); self.dimensions_refs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs index d4d8e832d4eab..e39347de43f89 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs @@ -16,6 +16,7 @@ pub struct MultiStageAppliedState { time_dimensions_filters: Vec, dimensions_filters: Vec, measures_filters: Vec, + segments: Vec, time_shifts: HashMap, } @@ -26,6 +27,7 @@ impl MultiStageAppliedState { time_dimensions_filters: Vec, dimensions_filters: Vec, measures_filters: Vec, + segments: Vec, ) -> Rc { Rc::new(Self { time_dimensions, @@ -33,6 +35,7 @@ impl MultiStageAppliedState { time_dimensions_filters, dimensions_filters, measures_filters, + segments, time_shifts: HashMap::new(), }) } @@ -44,6 +47,7 @@ impl MultiStageAppliedState { time_dimensions_filters: self.time_dimensions_filters.clone(), dimensions_filters: self.dimensions_filters.clone(), measures_filters: self.measures_filters.clone(), + segments: self.segments.clone(), time_shifts: self.time_shifts.clone(), } } @@ -77,6 +81,10 @@ impl MultiStageAppliedState { &self.dimensions_filters } + pub fn segments(&self) -> &Vec { + &self.segments + } + pub fn measures_filters(&self) -> &Vec { &self.measures_filters } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs index ed99f5a517da0..f21c360502a5b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs @@ -11,6 +11,7 @@ use crate::planner::planners::{ }; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::sql_nodes::SqlNodesFactory; +use crate::planner::sql_evaluator::MemberSymbol; use crate::planner::sql_evaluator::ReferencesBuilder; use crate::planner::sql_templates::PlanSqlTemplates; use crate::planner::QueryProperties; @@ -482,7 +483,7 @@ impl MultiStageMemberQueryPlanner { self.description.state().time_dimensions_filters().clone(), self.description.state().dimensions_filters().clone(), self.description.state().measures_filters().clone(), - vec![], //TODO May be we should push down segments on some cases + self.description.state().segments().clone(), vec![], None, None, @@ -585,21 +586,29 @@ impl MultiStageMemberQueryPlanner { } fn query_member_as_base_member(&self) -> Result, CubeError> { - if let Some(measure) = BaseMeasure::try_new( - self.description.member_node().clone(), - self.query_tools.clone(), - )? { - Ok(measure) - } else if let Some(dimension) = BaseDimension::try_new( - self.description.member_node().clone(), - self.query_tools.clone(), - )? { - Ok(dimension) - } else { - Err(CubeError::internal( - "Expected measure or dimension as multi stage member".to_string(), - )) - } + let res = match self.description.member_node().as_ref() { + MemberSymbol::Dimension(_) | MemberSymbol::TimeDimension(_) => { + BaseDimension::try_new_required( + self.description.member_node().clone(), + self.query_tools.clone(), + )? + .as_base_member() + } + MemberSymbol::Measure(_) | MemberSymbol::MemberExpression(_) => { + // We always treat the member expression as a measure here. + BaseMeasure::try_new_required( + self.description.member_node().clone(), + self.query_tools.clone(), + )? + .as_base_member() + } + MemberSymbol::CubeName(_) | MemberSymbol::CubeTable(_) => { + return Err(CubeError::internal( + "Expected measure or dimension as multi stage member".to_string(), + )); + } + }; + Ok(res) } fn member_partition_by( diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs index f3c2c2279a247..4e0f0fb48ed45 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs @@ -57,6 +57,7 @@ impl MultiStageQueryPlanner { self.query_properties.time_dimensions_filters().clone(), self.query_properties.dimensions_filters().clone(), self.query_properties.measures_filters().clone(), + self.query_properties.segments().clone(), ); let top_level_ctes = multi_stage_members diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs index 034b5363917e1..6fc1d1abe84f3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs @@ -534,6 +534,10 @@ impl QueryProperties { } } + pub fn segments(&self) -> &Vec { + &self.segments + } + pub fn all_dimensions_and_measures( &self, measures: &Vec>, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs index 2bd617344d02e..93916af991a2b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs @@ -132,6 +132,7 @@ impl QueryTools { base_tools: Rc, join_graph: Rc, timezone_name: Option, + export_annotated_sql: bool, ) -> Result, CubeError> { let templates_render = base_tools.sql_templates()?; let timezone = if let Some(timezone) = timezone_name { @@ -151,7 +152,10 @@ impl QueryTools { base_tools, join_graph, templates_render, - params_allocator: Rc::new(RefCell::new(ParamsAllocator::new(sql_templates))), + params_allocator: Rc::new(RefCell::new(ParamsAllocator::new( + sql_templates, + export_annotated_sql, + ))), evaluator_compiler, cached_data: RefCell::new(QueryToolsCachedData::new()), timezone, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/member_childs_collector.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/member_childs_collector.rs index 43f76b41336dc..2b8d955e0e1a1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/member_childs_collector.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/member_childs_collector.rs @@ -40,6 +40,8 @@ impl TraversalVisitor for MemberChildsCollector { match node.as_ref() { MemberSymbol::Measure(_) => Ok(Some(new_state)), MemberSymbol::Dimension(_) => Ok(Some(new_state)), + MemberSymbol::TimeDimension(_) => Ok(Some(new_state)), + MemberSymbol::MemberExpression(_) => Ok(Some(new_state)), _ => Ok(None), } } else { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/references_builder.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/references_builder.rs index 84c2e795199e4..b80c87bf5894f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/references_builder.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/references_builder.rs @@ -52,6 +52,9 @@ impl ReferencesBuilder { references: &mut HashMap, ) -> Result<(), CubeError> { let member_name = member.full_name(); + if references.contains_key(&member_name) { + return Ok(()); + } if let Some(reference) = self.find_reference_for_member(&member_name, strict_source) { references.insert(member_name.clone(), reference); return Ok(()); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_expression_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_expression_symbol.rs index d72603a5729b7..6dc1b14a4b8d8 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_expression_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_expression_symbol.rs @@ -68,4 +68,8 @@ impl MemberExpressionSymbol { pub fn name(&self) -> &String { &self.name } + + pub fn definition(&self) -> &Option { + &self.definition + } }