diff --git a/Cargo.lock b/Cargo.lock index 11fccb96d43a..60828b081b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3017,6 +3017,7 @@ dependencies = [ "log", "opendal", "parquet", + "rand 0.8.5", "serde", "storages-common-index", "storages-common-pruner", diff --git a/src/query/functions/src/srfs/mod.rs b/src/query/functions/src/srfs/mod.rs index 26c58664eef1..41e14ad55907 100644 --- a/src/query/functions/src/srfs/mod.rs +++ b/src/query/functions/src/srfs/mod.rs @@ -12,13 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common_expression::FunctionRegistry; - mod array; mod variant; -pub use variant::FlattenGenerator; -pub use variant::FlattenMode; +use common_expression::FunctionRegistry; pub fn register(registry: &mut FunctionRegistry) { array::register(registry); diff --git a/src/query/functions/src/srfs/variant.rs b/src/query/functions/src/srfs/variant.rs index eee807616b92..3720f70eebf2 100644 --- a/src/query/functions/src/srfs/variant.rs +++ b/src/query/functions/src/srfs/variant.rs @@ -14,12 +14,15 @@ use std::sync::Arc; +use common_expression::types::nullable::NullableColumnBuilder; use common_expression::types::string::StringColumnBuilder; use common_expression::types::AnyType; use common_expression::types::DataType; +use common_expression::types::NullableType; use common_expression::types::NumberDataType; use common_expression::types::StringType; use common_expression::types::UInt64Type; +use common_expression::types::ValueType; use common_expression::types::VariantType; use common_expression::Column; use common_expression::FromData; @@ -206,18 +209,20 @@ pub fn register(registry: &mut FunctionRegistry) { signature: FunctionSignature { name: "json_each".to_string(), args_type: args_type.to_vec(), - return_type: DataType::Tuple(vec![DataType::Nullable(Box::new(DataType::Tuple( - vec![DataType::String, DataType::Variant], - )))]), + return_type: DataType::Tuple(vec![ + DataType::Nullable(Box::new(DataType::String)), + DataType::Nullable(Box::new(DataType::Variant)), + ]), }, eval: FunctionEval::SRF { eval: Box::new(|args, ctx, max_nums_per_row| { let arg = args[0].clone().to_owned(); (0..ctx.num_rows) .map(|row| match arg.index(row).unwrap() { - ScalarRef::Null => { - (Value::Scalar(Scalar::Tuple(vec![Scalar::Null])), 0) - } + ScalarRef::Null => ( + Value::Scalar(Scalar::Tuple(vec![Scalar::Null, Scalar::Null])), + 0, + ), ScalarRef::Variant(val) => { unnest_variant_obj(val, row, max_nums_per_row) } @@ -233,7 +238,7 @@ pub fn register(registry: &mut FunctionRegistry) { "flatten".to_string(), FunctionProperty::default().kind(FunctionKind::SRF), ); - registry.register_function_factory("flatten", |_, args_type| { + registry.register_function_factory("flatten", |params, args_type| { if args_type.is_empty() || args_type.len() > 5 { return None; } @@ -264,24 +269,23 @@ pub fn register(registry: &mut FunctionRegistry) { { return None; } + let params = params.to_vec(); Some(Arc::new(Function { signature: FunctionSignature { name: "flatten".to_string(), args_type: args_type.to_vec(), - return_type: DataType::Tuple(vec![DataType::Nullable(Box::new(DataType::Tuple( - vec![ - DataType::Number(NumberDataType::UInt64), - DataType::Nullable(Box::new(DataType::String)), - DataType::Nullable(Box::new(DataType::String)), - DataType::Nullable(Box::new(DataType::Number(NumberDataType::UInt64))), - DataType::Nullable(Box::new(DataType::Variant)), - DataType::Nullable(Box::new(DataType::Variant)), - ], - )))]), + return_type: DataType::Tuple(vec![ + DataType::Nullable(Box::new(DataType::Number(NumberDataType::UInt64))), + DataType::Nullable(Box::new(DataType::String)), + DataType::Nullable(Box::new(DataType::String)), + DataType::Nullable(Box::new(DataType::Number(NumberDataType::UInt64))), + DataType::Nullable(Box::new(DataType::Variant)), + DataType::Nullable(Box::new(DataType::Variant)), + ]), }, eval: FunctionEval::SRF { - eval: Box::new(|args, ctx, max_nums_per_row| { + eval: Box::new(move |args, ctx, max_nums_per_row| { let arg = args[0].clone().to_owned(); let mut json_path = None; @@ -401,7 +405,17 @@ pub fn register(registry: &mut FunctionRegistry) { { match arg.index(row).unwrap() { ScalarRef::Null => { - results.push((Value::Scalar(Scalar::Tuple(vec![Scalar::Null])), 0)); + results.push(( + Value::Scalar(Scalar::Tuple(vec![ + Scalar::Null, + Scalar::Null, + Scalar::Null, + Scalar::Null, + Scalar::Null, + Scalar::Null, + ])), + 0, + )); } ScalarRef::Variant(val) => { let columns = match json_path { @@ -414,15 +428,19 @@ pub fn register(registry: &mut FunctionRegistry) { &mut builder.offsets, ); let inner_val = builder.pop().unwrap_or_default(); - generator.generate((row + 1) as u64, &inner_val, path) + generator.generate( + (row + 1) as u64, + &inner_val, + path, + ¶ms, + ) } - None => generator.generate((row + 1) as u64, val, ""), + None => generator.generate((row + 1) as u64, val, "", ¶ms), }; let len = columns[0].len(); *max_nums_per_row = std::cmp::max(*max_nums_per_row, len); - let inner_col = Column::Tuple(columns).wrap_nullable(None); - results.push((Value::Column(Column::Tuple(vec![inner_col])), len)); + results.push((Value::Column(Column::Tuple(columns)), len)); } _ => unreachable!(), } @@ -478,32 +496,34 @@ fn unnest_variant_obj( val_builder.commit_row(); } - let key_col = Column::String(key_builder.build()); - let val_col = Column::Variant(val_builder.build()); - let tuple_col = Column::Tuple(vec![key_col, val_col]).wrap_nullable(None); + let key_col = Column::String(key_builder.build()).wrap_nullable(None); + let val_col = Column::Variant(val_builder.build()).wrap_nullable(None); - (Value::Column(Column::Tuple(vec![tuple_col])), len) + (Value::Column(Column::Tuple(vec![key_col, val_col])), len) } - _ => (Value::Scalar(Scalar::Tuple(vec![Scalar::Null])), 0), + _ => ( + Value::Scalar(Scalar::Tuple(vec![Scalar::Null, Scalar::Null])), + 0, + ), } } #[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum FlattenMode { +enum FlattenMode { Both, Object, Array, } #[derive(Copy, Clone)] -pub struct FlattenGenerator { +struct FlattenGenerator { outer: bool, recursive: bool, mode: FlattenMode, } impl FlattenGenerator { - pub fn create(outer: bool, recursive: bool, mode: FlattenMode) -> FlattenGenerator { + fn create(outer: bool, recursive: bool, mode: FlattenMode) -> FlattenGenerator { Self { outer, recursive, @@ -516,22 +536,59 @@ impl FlattenGenerator { &mut self, input: &[u8], path: &str, - keys: &mut Vec>>, - paths: &mut Vec>>, - indices: &mut Vec>, - values: &mut Vec>>, - thises: &mut Vec>>, + key_builder: &mut Option>, + path_builder: &mut Option, + index_builder: &mut Option>, + value_builder: &mut Option, + this_builder: &mut Option, + rows: &mut usize, ) { match self.mode { FlattenMode::Object => { - self.flatten_object(input, path, keys, paths, indices, values, thises); + self.flatten_object( + input, + path, + key_builder, + path_builder, + index_builder, + value_builder, + this_builder, + rows, + ); } FlattenMode::Array => { - self.flatten_array(input, path, keys, paths, indices, values, thises); + self.flatten_array( + input, + path, + key_builder, + path_builder, + index_builder, + value_builder, + this_builder, + rows, + ); } FlattenMode::Both => { - self.flatten_array(input, path, keys, paths, indices, values, thises); - self.flatten_object(input, path, keys, paths, indices, values, thises); + self.flatten_array( + input, + path, + key_builder, + path_builder, + index_builder, + value_builder, + this_builder, + rows, + ); + self.flatten_object( + input, + path, + key_builder, + path_builder, + index_builder, + value_builder, + this_builder, + rows, + ); } } } @@ -541,24 +598,49 @@ impl FlattenGenerator { &mut self, input: &[u8], path: &str, - keys: &mut Vec>>, - paths: &mut Vec>>, - indices: &mut Vec>, - values: &mut Vec>>, - thises: &mut Vec>>, + key_builder: &mut Option>, + path_builder: &mut Option, + index_builder: &mut Option>, + value_builder: &mut Option, + this_builder: &mut Option, + rows: &mut usize, ) { if let Some(len) = array_length(input) { for i in 0..len { - let val = get_by_index(input, i).unwrap(); - keys.push(None); let inner_path = format!("{}[{}]", path, i); - paths.push(Some(inner_path.as_bytes().to_vec())); - indices.push(Some(i.try_into().unwrap())); - values.push(Some(val.clone())); - thises.push(Some(input.to_vec().clone())); + let val = get_by_index(input, i).unwrap(); + + if let Some(key_builder) = key_builder { + key_builder.push_null(); + } + if let Some(path_builder) = path_builder { + path_builder.put_slice(inner_path.as_bytes()); + path_builder.commit_row(); + } + if let Some(index_builder) = index_builder { + index_builder.push(i.try_into().unwrap()); + } + if let Some(value_builder) = value_builder { + value_builder.put_slice(&val); + value_builder.commit_row(); + } + if let Some(this_builder) = this_builder { + this_builder.put_slice(input); + this_builder.commit_row(); + } + *rows += 1; if self.recursive { - self.flatten(&val, &inner_path, keys, paths, indices, values, thises); + self.flatten( + &val, + &inner_path, + key_builder, + path_builder, + index_builder, + value_builder, + this_builder, + rows, + ); } } } @@ -569,11 +651,12 @@ impl FlattenGenerator { &mut self, input: &[u8], path: &str, - keys: &mut Vec>>, - paths: &mut Vec>>, - indices: &mut Vec>, - values: &mut Vec>>, - thises: &mut Vec>>, + key_builder: &mut Option>, + path_builder: &mut Option, + index_builder: &mut Option>, + value_builder: &mut Option, + this_builder: &mut Option, + rows: &mut usize, ) { if let Some(obj_keys) = object_keys(input) { if let Some(len) = array_length(&obj_keys) { @@ -581,63 +664,143 @@ impl FlattenGenerator { let key = get_by_index(&obj_keys, i).unwrap(); let name = as_str(&key).unwrap(); let val = get_by_name(input, &name, false).unwrap(); - - keys.push(Some(name.as_bytes().to_vec())); let inner_path = if !path.is_empty() { format!("{}.{}", path, name) } else { name.to_string() }; - paths.push(Some(inner_path.as_bytes().to_vec())); - indices.push(None); - values.push(Some(val.clone())); - thises.push(Some(input.to_vec().clone())); + + if let Some(key_builder) = key_builder { + key_builder.push(name.as_bytes()); + } + if let Some(path_builder) = path_builder { + path_builder.put_slice(inner_path.as_bytes()); + path_builder.commit_row(); + } + if let Some(index_builder) = index_builder { + index_builder.push_null(); + } + if let Some(value_builder) = value_builder { + value_builder.put_slice(&val); + value_builder.commit_row(); + } + if let Some(this_builder) = this_builder { + this_builder.put_slice(input); + this_builder.commit_row(); + } + *rows += 1; if self.recursive { - self.flatten(&val, &inner_path, keys, paths, indices, values, thises); + self.flatten( + &val, + &inner_path, + key_builder, + path_builder, + index_builder, + value_builder, + this_builder, + rows, + ); } } } } } - pub fn generate(&mut self, seq: u64, input: &[u8], path: &str) -> Vec { - let mut keys: Vec>> = vec![]; - let mut paths: Vec>> = vec![]; - let mut indices: Vec> = vec![]; - let mut values: Vec>> = vec![]; - let mut thises: Vec>> = vec![]; + fn generate(&mut self, seq: u64, input: &[u8], path: &str, params: &[usize]) -> Vec { + // Only columns required by parent plan need a builder. + let mut key_builder = if params.is_empty() || params.contains(&2) { + Some(NullableColumnBuilder::::with_capacity(0, &[])) + } else { + None + }; + let mut path_builder = if params.is_empty() || params.contains(&3) { + Some(StringColumnBuilder::with_capacity(0, 0)) + } else { + None + }; + let mut index_builder = if params.is_empty() || params.contains(&4) { + Some(NullableColumnBuilder::::with_capacity(0, &[])) + } else { + None + }; + let mut value_builder = if params.is_empty() || params.contains(&5) { + Some(StringColumnBuilder::with_capacity(0, 0)) + } else { + None + }; + let mut this_builder = if params.is_empty() || params.contains(&6) { + Some(StringColumnBuilder::with_capacity(0, 0)) + } else { + None + }; + let mut rows = 0; if !input.is_empty() { self.flatten( input, path, - &mut keys, - &mut paths, - &mut indices, - &mut values, - &mut thises, + &mut key_builder, + &mut path_builder, + &mut index_builder, + &mut value_builder, + &mut this_builder, + &mut rows, ); } - if self.outer && values.is_empty() { - // add an empty row - keys.push(None); - paths.push(None); - indices.push(None); - values.push(None); - thises.push(None); + if self.outer && rows == 0 { + // add an empty row. + let columns = vec![ + UInt64Type::from_opt_data(vec![Some(seq)]), + StringType::from_opt_data(vec![None::<&str>]), + StringType::from_opt_data(vec![None::<&str>]), + UInt64Type::from_opt_data(vec![None]), + VariantType::from_opt_data(vec![None]), + VariantType::from_opt_data(vec![None]), + ]; + return columns; } - let seqs: Vec = [seq].repeat(values.len()); + // Generate an empty dummy column for columns that are not needed. + let seq_column = UInt64Type::upcast_column(vec![seq; rows].into()).wrap_nullable(None); + let key_column = if let Some(key_builder) = key_builder { + NullableType::::upcast_column(key_builder.build()) + } else { + StringType::upcast_column(StringColumnBuilder::repeat(&[], rows).build()) + .wrap_nullable(None) + }; + let path_column = if let Some(path_builder) = path_builder { + StringType::upcast_column(path_builder.build()).wrap_nullable(None) + } else { + StringType::upcast_column(StringColumnBuilder::repeat(&[], rows).build()) + .wrap_nullable(None) + }; + let index_column = if let Some(index_builder) = index_builder { + NullableType::::upcast_column(index_builder.build()) + } else { + UInt64Type::upcast_column(vec![0u64; rows].into()).wrap_nullable(None) + }; + let value_column = if let Some(value_builder) = value_builder { + VariantType::upcast_column(value_builder.build()).wrap_nullable(None) + } else { + VariantType::upcast_column(StringColumnBuilder::repeat(&[], rows).build()) + .wrap_nullable(None) + }; + let this_column = if let Some(this_builder) = this_builder { + VariantType::upcast_column(this_builder.build()).wrap_nullable(None) + } else { + VariantType::upcast_column(StringColumnBuilder::repeat(&[], rows).build()) + .wrap_nullable(None) + }; let columns = vec![ - UInt64Type::from_data(seqs), - StringType::from_opt_data(keys), - StringType::from_opt_data(paths), - UInt64Type::from_opt_data(indices), - VariantType::from_opt_data(values), - VariantType::from_opt_data(thises), + seq_column, + key_column, + path_column, + index_column, + value_column, + this_column, ]; columns } diff --git a/src/query/service/src/pipelines/processors/transforms/transform_srf.rs b/src/query/service/src/pipelines/processors/transforms/transform_srf.rs index e7c9bf001d2b..9ffb84f306f7 100644 --- a/src/query/service/src/pipelines/processors/transforms/transform_srf.rs +++ b/src/query/service/src/pipelines/processors/transforms/transform_srf.rs @@ -233,11 +233,14 @@ impl BlockingTransform for TransformSRF { } } } else { + let data_type = srf_expr.data_type(); + let inner_tys = data_type.as_tuple().unwrap(); + let inner_vals = vec![ScalarRef::Null; inner_tys.len()]; row_result = Value::Column( ColumnBuilder::repeat( - &ScalarRef::Tuple(vec![ScalarRef::Null]), + &ScalarRef::Tuple(inner_vals), self.num_rows[i], - srf_expr.data_type(), + data_type, ) .build(), ); diff --git a/src/query/sql/src/executor/physical_plans/physical_eval_scalar.rs b/src/query/sql/src/executor/physical_plans/physical_eval_scalar.rs index d57887867e45..5a6293e0b5f4 100644 --- a/src/query/sql/src/executor/physical_plans/physical_eval_scalar.rs +++ b/src/query/sql/src/executor/physical_plans/physical_eval_scalar.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + use common_exception::Result; use common_expression::ConstantFolder; use common_expression::DataField; @@ -25,6 +27,10 @@ use crate::executor::physical_plan::PhysicalPlan; use crate::executor::physical_plan_builder::PhysicalPlanBuilder; use crate::optimizer::ColumnSet; use crate::optimizer::SExpr; +use crate::plans::FunctionCall; +use crate::plans::ProjectSet; +use crate::plans::RelOperator; +use crate::plans::ScalarExpr; use crate::IndexType; use crate::TypeCheck; @@ -91,7 +97,17 @@ impl PhysicalPlanBuilder { if used.is_empty() { self.build(s_expr.child(0)?, required).await } else { - let input = self.build(s_expr.child(0)?, required).await?; + let child = s_expr.child(0)?; + let input = if let RelOperator::ProjectSet(project_set) = child.plan() { + let new_project_set = + self.prune_flatten_columns(eval_scalar, project_set, &required); + let mut new_child = child.clone(); + new_child.plan = Arc::new(new_project_set.into()); + self.build(&new_child, required).await? + } else { + self.build(child, required).await? + }; + let eval_scalar = crate::plans::EvalScalar { items: used }; self.create_eval_scalar(&eval_scalar, column_projections, input, stat_info) } @@ -149,4 +165,46 @@ impl PhysicalPlanBuilder { stat_info: Some(stat_info), })) } + + // The flatten function returns a tuple, which contains 6 columns. + // Only keep columns required by parent plan, other columns can be pruned + // to reduce the memory usage. + fn prune_flatten_columns( + &mut self, + eval_scalar: &crate::plans::EvalScalar, + project_set: &ProjectSet, + required: &ColumnSet, + ) -> ProjectSet { + let mut project_set = project_set.clone(); + for srf_item in &mut project_set.srfs { + if let ScalarExpr::FunctionCall(srf_func) = &srf_item.scalar { + if srf_func.func_name == "flatten" { + // Store the columns required by the parent plan in params. + let mut params = Vec::new(); + for item in &eval_scalar.items { + if !required.contains(&item.index) { + continue; + } + if let ScalarExpr::FunctionCall(func) = &item.scalar { + if func.func_name == "get" && !func.arguments.is_empty() { + if let ScalarExpr::BoundColumnRef(column_ref) = &func.arguments[0] { + if column_ref.column.index == srf_item.index { + params.push(func.params[0]); + } + } + } + } + } + + srf_item.scalar = ScalarExpr::FunctionCall(FunctionCall { + span: srf_func.span, + func_name: srf_func.func_name.clone(), + params, + arguments: srf_func.arguments.clone(), + }); + } + } + } + project_set + } } diff --git a/src/query/sql/src/planner/binder/project_set.rs b/src/query/sql/src/planner/binder/project_set.rs index 310a6ee480d3..0d4d57c0f79e 100644 --- a/src/query/sql/src/planner/binder/project_set.rs +++ b/src/query/sql/src/planner/binder/project_set.rs @@ -20,7 +20,6 @@ use common_ast::ast::Lambda; use common_ast::ast::Literal; use common_ast::ast::Window; use common_ast::Visitor; -use common_exception::ErrorCode; use common_exception::Result; use common_exception::Span; use common_expression::FunctionKind; @@ -148,12 +147,6 @@ impl Binder { let srf_expr = srf_scalar.as_expr()?; let return_types = srf_expr.data_type().as_tuple().unwrap(); - if return_types.len() > 1 { - return Err(ErrorCode::Unimplemented( - "set-returning functions with more than one return type are not supported yet", - )); - } - // Add result column to metadata let column_index = self .metadata @@ -173,20 +166,27 @@ impl Binder { }; items.push(item); - // Flatten the tuple fields of the srfs to the top level columns - // TODO(andylokandy/leisky): support multiple return types - let flatten_result = ScalarExpr::FunctionCall(FunctionCall { - span: srf.span(), - func_name: "get".to_string(), - params: vec![1], - arguments: vec![ScalarExpr::BoundColumnRef(BoundColumnRef { + // If tuple has more than one field, return the tuple column, + // otherwise, extract the tuple field to top level column. + let result_column = if return_types.len() > 1 { + ScalarExpr::BoundColumnRef(BoundColumnRef { span: srf.span(), column, - })], - }); + }) + } else { + ScalarExpr::FunctionCall(FunctionCall { + span: srf.span(), + func_name: "get".to_string(), + params: vec![1], + arguments: vec![ScalarExpr::BoundColumnRef(BoundColumnRef { + span: srf.span(), + column, + })], + }) + }; // Add the srf to bind context, so we can replace the srfs later. - bind_context.srfs.insert(srf.to_string(), flatten_result); + bind_context.srfs.insert(srf.to_string(), result_column); } let project_set = ProjectSet { srfs: items }; diff --git a/src/query/sql/src/planner/binder/table.rs b/src/query/sql/src/planner/binder/table.rs index 799b301fb393..c8c79bc37cf3 100644 --- a/src/query/sql/src/planner/binder/table.rs +++ b/src/query/sql/src/planner/binder/table.rs @@ -367,7 +367,7 @@ impl Binder { plan.items.len() ))); } - // Delete result tuple column + // Delete srf result tuple column, extract tuple inner columns instead let _ = bind_context.columns.pop(); let scalar = &plan.items[0].scalar; @@ -462,28 +462,34 @@ impl Binder { .bind_project_set(&mut bind_context, &srfs, child) .await?; - if let Some((_, flatten_scalar)) = bind_context.srfs.remove(&srf.to_string()) { - // Add result column to metadata - let data_type = flatten_scalar.data_type()?; - let index = self - .metadata - .write() - .add_derived_column(srf.to_string(), data_type.clone()); - let column_binding = ColumnBindingBuilder::new( - srf.to_string(), - index, - Box::new(data_type), - Visibility::Visible, - ) - .build(); - bind_context.add_column_binding(column_binding); + if let Some((_, srf_result)) = bind_context.srfs.remove(&srf.to_string()) { + let column_binding = + if let ScalarExpr::BoundColumnRef(column_ref) = &srf_result { + column_ref.column.clone() + } else { + // Add result column to metadata + let data_type = srf_result.data_type()?; + let index = self + .metadata + .write() + .add_derived_column(srf.to_string(), data_type.clone()); + ColumnBindingBuilder::new( + srf.to_string(), + index, + Box::new(data_type), + Visibility::Visible, + ) + .build() + }; let eval_scalar = EvalScalar { items: vec![ScalarItem { - scalar: flatten_scalar, - index, + scalar: srf_result, + index: column_binding.index, }], }; + // Add srf result column + bind_context.add_column_binding(column_binding); let flatten_expr = SExpr::create_unary(Arc::new(eval_scalar.into()), Arc::new(srf_expr)); @@ -505,9 +511,8 @@ impl Binder { return Ok((new_expr, bind_context)); } else { - return Err( - ErrorCode::Internal("srf flatten result is missing").set_span(*span) - ); + return Err(ErrorCode::Internal("lateral join bind project_set failed") + .set_span(*span)); } } else { return Err(ErrorCode::InvalidArgument(format!( diff --git a/tests/sqllogictests/suites/mode/standalone/explain/project_set.test b/tests/sqllogictests/suites/mode/standalone/explain/project_set.test index 223b01ed0e07..5d0e97ba2fa3 100644 --- a/tests/sqllogictests/suites/mode/standalone/explain/project_set.test +++ b/tests/sqllogictests/suites/mode/standalone/explain/project_set.test @@ -84,5 +84,53 @@ ProjectSet ├── push downs: [filters: [], limit: NONE] └── estimated rows: 10.00 +statement ok +drop table if exists t; + +statement ok +create table t(a int, b variant); + +query T +EXPLAIN SELECT t.a, f.seq, f.value FROM t, LATERAL FLATTEN(input => t.b) f +---- +EvalScalar +├── output columns: [t.a (#0), seq (#3), value (#7)] +├── expressions: [get(1)(flatten (#2)), get(5)(flatten (#2))] +├── estimated rows: 0.00 +└── ProjectSet + ├── output columns: [t.a (#0), flatten (#2)] + ├── estimated rows: 0.00 + ├── set returning functions: flatten(1, 5)(t.b (#1)) + └── TableScan + ├── table: default.project_set.t + ├── output columns: [a (#0), b (#1)] + ├── read rows: 0 + ├── read bytes: 0 + ├── partitions total: 0 + ├── partitions scanned: 0 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 0.00 + +query T +EXPLAIN SELECT json_each(t.b), unnest(t.b) FROM t +---- +EvalScalar +├── output columns: [json_each (#2), unnest(t.b) (#4)] +├── expressions: [get(1)(unnest (#3))] +├── estimated rows: 0.00 +└── ProjectSet + ├── output columns: [json_each (#2), unnest (#3)] + ├── estimated rows: 0.00 + ├── set returning functions: json_each(t.b (#1)), unnest(t.b (#1)) + └── TableScan + ├── table: default.project_set.t + ├── output columns: [b (#1)] + ├── read rows: 0 + ├── read bytes: 0 + ├── partitions total: 0 + ├── partitions scanned: 0 + ├── push downs: [filters: [], limit: NONE] + └── estimated rows: 0.00 + statement ok drop database project_set diff --git a/tests/sqllogictests/suites/query/lateral.test b/tests/sqllogictests/suites/query/lateral.test index 08dae8ac20b7..52c51341e180 100644 --- a/tests/sqllogictests/suites/query/lateral.test +++ b/tests/sqllogictests/suites/query/lateral.test @@ -156,5 +156,19 @@ GROUP BY p.id ORDER BY p.id 12712555 2 98127771 2 +query IT +SELECT u.user_id, f.value from + user_activities u, + LATERAL unnest(u.activities) f +---- +1 "reading" +1 "swimming" +1 "cycling" +2 "painting" +2 "running" +3 "cooking" +3 "climbing" +3 "writing" + statement ok drop database test_lateral