From 00a12bc03ea924dec14f48f29b6b4f77bc735e67 Mon Sep 17 00:00:00 2001 From: Lordworms Date: Tue, 24 Sep 2024 21:25:01 -0700 Subject: [PATCH 1/5] adding struct_extract function --- datafusion/functions/src/core/mod.rs | 12 +- datafusion/functions/src/core/planner.rs | 4 + .../functions/src/core/struct_extract.rs | 177 ++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 datafusion/functions/src/core/struct_extract.rs diff --git a/datafusion/functions/src/core/mod.rs b/datafusion/functions/src/core/mod.rs index 1c69f9c9b2f37..d1573c2b1b414 100644 --- a/datafusion/functions/src/core/mod.rs +++ b/datafusion/functions/src/core/mod.rs @@ -31,6 +31,7 @@ pub mod nvl; pub mod nvl2; pub mod planner; pub mod r#struct; +pub mod struct_extract; pub mod version; // create UDFs @@ -44,7 +45,11 @@ make_udf_function!(named_struct::NamedStructFunc, NAMED_STRUCT, named_struct); make_udf_function!(getfield::GetFieldFunc, GET_FIELD, get_field); make_udf_function!(coalesce::CoalesceFunc, COALESCE, coalesce); make_udf_function!(version::VersionFunc, VERSION, version); - +make_udf_function!( + struct_extract::StructExtractFunc, + STRUCTEXTRACT, + struct_extract +); pub mod expr_fn { use datafusion_expr::{Expr, Literal}; @@ -80,6 +85,10 @@ pub mod expr_fn { coalesce, "Returns `coalesce(args...)`, which evaluates to the value of the first expr which is not NULL", args, + ),( + struct_extract, + "Return the member of a struct based on index or name", + arg1 arg2 )); #[doc = "Returns the value of the field with the given name from the struct"] @@ -107,5 +116,6 @@ pub fn functions() -> Vec> { get_field(), coalesce(), version(), + struct_extract(), ] } diff --git a/datafusion/functions/src/core/planner.rs b/datafusion/functions/src/core/planner.rs index 889f191d592f5..f0197ec52e1f3 100644 --- a/datafusion/functions/src/core/planner.rs +++ b/datafusion/functions/src/core/planner.rs @@ -70,6 +70,10 @@ impl ExprPlanner for CoreFunctionPlanner { qualifier: Option<&TableReference>, nested_names: &[String], ) -> Result>> { + println!( + "field's type is {:?} \n and qualifier is {:?}", + field, qualifier + ); // TODO: remove when can support multiple nested identifiers if nested_names.len() > 1 { return not_impl_err!( diff --git a/datafusion/functions/src/core/struct_extract.rs b/datafusion/functions/src/core/struct_extract.rs new file mode 100644 index 0000000000000..ccd4d14bb79f0 --- /dev/null +++ b/datafusion/functions/src/core/struct_extract.rs @@ -0,0 +1,177 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use arrow::datatypes::DataType; +use datafusion_common::DataFusionError; +use datafusion_common::{exec_err, ScalarValue}; +use datafusion_expr::{ScalarUDFImpl, Signature, TypeSignature, Volatility}; + +#[derive(Debug)] +pub struct StructExtractFunc { + signature: Signature, +} + +impl Default for StructExtractFunc { + fn default() -> Self { + Self::new() + } +} + +impl StructExtractFunc { + pub fn new() -> Self { + Self { + signature: Signature::one_of( + vec![TypeSignature::Any(2)], + Volatility::Immutable, + ), + } + } +} + +impl ScalarUDFImpl for StructExtractFunc { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn name(&self) -> &str { + "struct_extract" + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn return_type( + &self, + _arg_types: &[arrow::datatypes::DataType], + ) -> datafusion_common::Result { + exec_err!("should not call return type for struct extract function") + } + + fn return_type_from_exprs( + &self, + args: &[datafusion_expr::Expr], + _schema: &dyn datafusion_common::ExprSchema, + arg_types: &[DataType], + ) -> datafusion_common::Result { + if let DataType::Struct(fields) = &arg_types[0] { + match &arg_types[1] { + DataType::Int64 => { + if let datafusion_expr::Expr::Literal(ScalarValue::Int64(Some( + index, + ))) = &args[1] + { + if *index >= 0 && (*index as usize) < fields.len() { + return Ok(fields[*index as usize].data_type().clone()); + } else { + return exec_err!( + "Index {} is out of bounds for struct", + index + ); + } + } else { + return exec_err!("Expected an Int64 literal for field index"); + } + } + DataType::Utf8 => { + if let datafusion_expr::Expr::Literal(ScalarValue::Utf8(Some(name))) = + &args[1] + { + return fields + .iter() + .find(|field| field.name().eq(name)) + .map(|field| field.data_type().clone()) + .ok_or_else(|| DataFusionError::Execution(format!("Error finding name '{}' in the schema for struct extract function", name))); + } + } + _ => { + return exec_err!( + "not supported data type for struct extract function" + ); + } + } + } + exec_err!("not supported data type for struct extract function") + } + fn invoke( + &self, + args: &[datafusion_expr::ColumnarValue], + ) -> datafusion_common::Result { + match &args[0] { + // Handle the case where the first argument is a StructArray + datafusion_expr::ColumnarValue::Array(array) => { + let struct_array = array + .as_any() + .downcast_ref::() + .ok_or_else(|| { + DataFusionError::Execution(format!( + "Failed to downcast to StructArray, got: {:?}", + args[0].data_type() + )) + })?; + + extract_from_struct_array(struct_array, &args[1]) + } + + // Handle the case where the first argument is ScalarValue::Struct + datafusion_expr::ColumnarValue::Scalar(ScalarValue::Struct( + arc_struct_array, + )) => { + let struct_array = arc_struct_array.as_ref(); + extract_from_struct_array(struct_array, &args[1]) + } + + _ => Err(DataFusionError::Execution( + "First argument to struct_extract must be a struct".to_string(), + )), + } + } +} +// Extract data from a StructArray +fn extract_from_struct_array( + struct_array: &arrow::array::StructArray, + key: &datafusion_expr::ColumnarValue, +) -> datafusion_common::Result { + match key { + datafusion_expr::ColumnarValue::Scalar(ScalarValue::Int64(Some(index))) => { + if *index >= 0 && (*index as usize) < struct_array.num_columns() { + Ok(datafusion_expr::ColumnarValue::Array( + struct_array.column(*index as usize).clone(), + )) + } else { + exec_err!("Index {} is out of bounds for struct", index) + } + } + datafusion_expr::ColumnarValue::Scalar(ScalarValue::Utf8(Some(field_name))) => { + let field_index = struct_array + .fields() + .iter() + .position(|f| f.name() == field_name) + .ok_or_else(|| { + DataFusionError::Execution(format!( + "Field '{}' not found in struct", + field_name + )) + })?; + Ok(datafusion_expr::ColumnarValue::Array( + struct_array.column(field_index).clone(), + )) + } + _ => exec_err!( + "Second argument must be either an Int64 index or a Utf8 field name" + ), + } +} From 39fc4f07a7a50f8a5210a5a150c8818f134b2b63 Mon Sep 17 00:00:00 2001 From: Lordworms Date: Thu, 26 Sep 2024 00:37:24 -0700 Subject: [PATCH 2/5] changing logics --- datafusion/functions/src/core/planner.rs | 31 +++---- datafusion/sql/src/expr/identifier.rs | 23 ++--- datafusion/sqllogictest/test_files/struct.slt | 91 +++++++++++++++++++ 3 files changed, 113 insertions(+), 32 deletions(-) diff --git a/datafusion/functions/src/core/planner.rs b/datafusion/functions/src/core/planner.rs index f0197ec52e1f3..12fcb4a26365a 100644 --- a/datafusion/functions/src/core/planner.rs +++ b/datafusion/functions/src/core/planner.rs @@ -17,7 +17,7 @@ use arrow::datatypes::Field; use datafusion_common::Result; -use datafusion_common::{not_impl_err, Column, DFSchema, ScalarValue, TableReference}; +use datafusion_common::{Column, DFSchema, ScalarValue, TableReference}; use datafusion_expr::expr::ScalarFunction; use datafusion_expr::planner::{ExprPlanner, PlannerResult, RawDictionaryExpr}; use datafusion_expr::{lit, Expr}; @@ -70,23 +70,20 @@ impl ExprPlanner for CoreFunctionPlanner { qualifier: Option<&TableReference>, nested_names: &[String], ) -> Result>> { - println!( - "field's type is {:?} \n and qualifier is {:?}", - field, qualifier - ); - // TODO: remove when can support multiple nested identifiers - if nested_names.len() > 1 { - return not_impl_err!( - "Nested identifiers not yet supported for column {}", - Column::from((qualifier, field)).quoted_flat_name() - ); + let col = Expr::Column(Column::from((qualifier, field))); + + // Start with the base column expression + let mut expr = col; + + // Iterate over nested_names and create nested get_field expressions + for nested_name in nested_names { + let get_field_args = vec![expr, lit(ScalarValue::from(nested_name.clone()))]; + expr = Expr::ScalarFunction(ScalarFunction::new_udf( + crate::core::get_field(), + get_field_args, + )); } - let nested_name = nested_names[0].to_string(); - let col = Expr::Column(Column::from((qualifier, field))); - let get_field_args = vec![col, lit(ScalarValue::from(nested_name))]; - Ok(PlannerResult::Planned(Expr::ScalarFunction( - ScalarFunction::new_udf(crate::core::get_field(), get_field_args), - ))) + Ok(PlannerResult::Planned(expr)) } } diff --git a/datafusion/sql/src/expr/identifier.rs b/datafusion/sql/src/expr/identifier.rs index 36776c690235b..540dfdbc8383f 100644 --- a/datafusion/sql/src/expr/identifier.rs +++ b/datafusion/sql/src/expr/identifier.rs @@ -15,15 +15,17 @@ // specific language governing permissions and limitations // under the License. -use arrow_schema::Field; +use std::sync::Arc; + +use arrow_schema::{DataType, Field}; use sqlparser::ast::{Expr as SQLExpr, Ident}; use datafusion_common::{ - internal_err, not_impl_err, plan_datafusion_err, Column, DFSchema, DataFusionError, - Result, TableReference, + exec_err, internal_err, not_impl_err, plan_datafusion_err, Column, DFSchema, + DataFusionError, Result, ScalarValue, TableReference, }; use datafusion_expr::planner::PlannerResult; -use datafusion_expr::{Case, Expr}; +use datafusion_expr::{lit, Case, Expr}; use crate::planner::{ContextProvider, PlannerContext, SqlToRel}; use datafusion_expr::UNNAMED_TABLE; @@ -113,13 +115,6 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { .map(|id| self.ident_normalizer.normalize(id)) .collect::>(); - // Currently not supporting more than one nested level - // Though ideally once that support is in place, this code should work with it - // TODO: remove when can support multiple nested identifiers - if ids.len() > 5 { - return not_impl_err!("Compound identifier: {ids:?}"); - } - let search_result = search_dfschema(&ids, schema); match search_result { // found matching field with spare identifier(s) for nested field(s) in structure @@ -142,9 +137,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { } } } - not_impl_err!( - "Compound identifiers not supported by ExprPlanner: {ids:?}" - ) + exec_err!("could not parse compound identifier from {ids:?}") } // found matching field with no spare identifier(s) Some((field, qualifier, _nested_names)) => { @@ -170,7 +163,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { not_impl_err!( "Nested identifiers are not yet supported for OuterReferenceColumn {}", Column::from((qualifier, field)).quoted_flat_name() - ) + ) } // found matching field with no spare identifier(s) Some((field, qualifier, _nested_names)) => { diff --git a/datafusion/sqllogictest/test_files/struct.slt b/datafusion/sqllogictest/test_files/struct.slt index f3ac6549ad066..d2e7160d00106 100644 --- a/datafusion/sqllogictest/test_files/struct.slt +++ b/datafusion/sqllogictest/test_files/struct.slt @@ -282,3 +282,94 @@ drop table values; statement ok drop table struct_values; + +statement ok +CREATE OR REPLACE VIEW complex_view AS +SELECT { + 'user': { + 'info': { + 'personal': { + 'name': 'John Doe', + 'age': 30, + 'email': 'john.doe@example.com' + }, + 'address': { + 'street': '123 Main St', + 'city': 'Anytown', + 'country': 'Countryland', + 'coordinates': [40.7128, -74.0060] + } + }, + 'preferences': { + 'theme': 'dark', + 'notifications': true, + 'languages': ['en', 'es', 'fr'] + }, + 'stats': { + 'logins': 42, + 'last_active': '2023-09-15', + 'scores': [85, 92, 78, 95], + 'achievements': { + 'badges': ['early_bird', 'top_contributor'], + 'levels': { + 'beginner': true, + 'intermediate': true, + 'advanced': false + } + } + } + }, + 'metadata': { + 'version': '1.0', + 'created_at': '2023-09-01T12:00:00Z' + }, + 'deep_nested': { + 'level1': { + 'level2': { + 'level3': { + 'level4': { + 'level5': { + 'level6': { + 'level7': { + 'level8': { + 'level9': { + 'level10': 'You reached the bottom!' + } + } + } + } + } + } + } + } + } + } +} AS complex_data; + +query T +SELECT complex_data.user.info.personal.name FROM complex_view; +---- +John Doe + +query I +SELECT complex_data.user.info.personal.age FROM complex_view; +---- +30 + +query T +SELECT complex_data.user.info.address.city FROM complex_view; +---- +Anytown + +query T +SELECT complex_data.user.preferences.languages[2] FROM complex_view; +---- +es + +query T +SELECT complex_data.deep_nested.level1.level2.level3.level4.level5.level6.level7.level8.level9.level10 FROM complex_view; +---- +You reached the bottom! + +statement ok +drop view complex_view; From ea63773ebacdcfd3878b1f2cc6d482efe9ab7b74 Mon Sep 17 00:00:00 2001 From: Lordworms Date: Thu, 26 Sep 2024 00:37:58 -0700 Subject: [PATCH 3/5] Revert "adding struct_extract function" This reverts commit 00a12bc03ea924dec14f48f29b6b4f77bc735e67. --- datafusion/functions/src/core/mod.rs | 12 +- .../functions/src/core/struct_extract.rs | 177 ------------------ 2 files changed, 1 insertion(+), 188 deletions(-) delete mode 100644 datafusion/functions/src/core/struct_extract.rs diff --git a/datafusion/functions/src/core/mod.rs b/datafusion/functions/src/core/mod.rs index d1573c2b1b414..1c69f9c9b2f37 100644 --- a/datafusion/functions/src/core/mod.rs +++ b/datafusion/functions/src/core/mod.rs @@ -31,7 +31,6 @@ pub mod nvl; pub mod nvl2; pub mod planner; pub mod r#struct; -pub mod struct_extract; pub mod version; // create UDFs @@ -45,11 +44,7 @@ make_udf_function!(named_struct::NamedStructFunc, NAMED_STRUCT, named_struct); make_udf_function!(getfield::GetFieldFunc, GET_FIELD, get_field); make_udf_function!(coalesce::CoalesceFunc, COALESCE, coalesce); make_udf_function!(version::VersionFunc, VERSION, version); -make_udf_function!( - struct_extract::StructExtractFunc, - STRUCTEXTRACT, - struct_extract -); + pub mod expr_fn { use datafusion_expr::{Expr, Literal}; @@ -85,10 +80,6 @@ pub mod expr_fn { coalesce, "Returns `coalesce(args...)`, which evaluates to the value of the first expr which is not NULL", args, - ),( - struct_extract, - "Return the member of a struct based on index or name", - arg1 arg2 )); #[doc = "Returns the value of the field with the given name from the struct"] @@ -116,6 +107,5 @@ pub fn functions() -> Vec> { get_field(), coalesce(), version(), - struct_extract(), ] } diff --git a/datafusion/functions/src/core/struct_extract.rs b/datafusion/functions/src/core/struct_extract.rs deleted file mode 100644 index ccd4d14bb79f0..0000000000000 --- a/datafusion/functions/src/core/struct_extract.rs +++ /dev/null @@ -1,177 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -use arrow::datatypes::DataType; -use datafusion_common::DataFusionError; -use datafusion_common::{exec_err, ScalarValue}; -use datafusion_expr::{ScalarUDFImpl, Signature, TypeSignature, Volatility}; - -#[derive(Debug)] -pub struct StructExtractFunc { - signature: Signature, -} - -impl Default for StructExtractFunc { - fn default() -> Self { - Self::new() - } -} - -impl StructExtractFunc { - pub fn new() -> Self { - Self { - signature: Signature::one_of( - vec![TypeSignature::Any(2)], - Volatility::Immutable, - ), - } - } -} - -impl ScalarUDFImpl for StructExtractFunc { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn name(&self) -> &str { - "struct_extract" - } - - fn signature(&self) -> &Signature { - &self.signature - } - - fn return_type( - &self, - _arg_types: &[arrow::datatypes::DataType], - ) -> datafusion_common::Result { - exec_err!("should not call return type for struct extract function") - } - - fn return_type_from_exprs( - &self, - args: &[datafusion_expr::Expr], - _schema: &dyn datafusion_common::ExprSchema, - arg_types: &[DataType], - ) -> datafusion_common::Result { - if let DataType::Struct(fields) = &arg_types[0] { - match &arg_types[1] { - DataType::Int64 => { - if let datafusion_expr::Expr::Literal(ScalarValue::Int64(Some( - index, - ))) = &args[1] - { - if *index >= 0 && (*index as usize) < fields.len() { - return Ok(fields[*index as usize].data_type().clone()); - } else { - return exec_err!( - "Index {} is out of bounds for struct", - index - ); - } - } else { - return exec_err!("Expected an Int64 literal for field index"); - } - } - DataType::Utf8 => { - if let datafusion_expr::Expr::Literal(ScalarValue::Utf8(Some(name))) = - &args[1] - { - return fields - .iter() - .find(|field| field.name().eq(name)) - .map(|field| field.data_type().clone()) - .ok_or_else(|| DataFusionError::Execution(format!("Error finding name '{}' in the schema for struct extract function", name))); - } - } - _ => { - return exec_err!( - "not supported data type for struct extract function" - ); - } - } - } - exec_err!("not supported data type for struct extract function") - } - fn invoke( - &self, - args: &[datafusion_expr::ColumnarValue], - ) -> datafusion_common::Result { - match &args[0] { - // Handle the case where the first argument is a StructArray - datafusion_expr::ColumnarValue::Array(array) => { - let struct_array = array - .as_any() - .downcast_ref::() - .ok_or_else(|| { - DataFusionError::Execution(format!( - "Failed to downcast to StructArray, got: {:?}", - args[0].data_type() - )) - })?; - - extract_from_struct_array(struct_array, &args[1]) - } - - // Handle the case where the first argument is ScalarValue::Struct - datafusion_expr::ColumnarValue::Scalar(ScalarValue::Struct( - arc_struct_array, - )) => { - let struct_array = arc_struct_array.as_ref(); - extract_from_struct_array(struct_array, &args[1]) - } - - _ => Err(DataFusionError::Execution( - "First argument to struct_extract must be a struct".to_string(), - )), - } - } -} -// Extract data from a StructArray -fn extract_from_struct_array( - struct_array: &arrow::array::StructArray, - key: &datafusion_expr::ColumnarValue, -) -> datafusion_common::Result { - match key { - datafusion_expr::ColumnarValue::Scalar(ScalarValue::Int64(Some(index))) => { - if *index >= 0 && (*index as usize) < struct_array.num_columns() { - Ok(datafusion_expr::ColumnarValue::Array( - struct_array.column(*index as usize).clone(), - )) - } else { - exec_err!("Index {} is out of bounds for struct", index) - } - } - datafusion_expr::ColumnarValue::Scalar(ScalarValue::Utf8(Some(field_name))) => { - let field_index = struct_array - .fields() - .iter() - .position(|f| f.name() == field_name) - .ok_or_else(|| { - DataFusionError::Execution(format!( - "Field '{}' not found in struct", - field_name - )) - })?; - Ok(datafusion_expr::ColumnarValue::Array( - struct_array.column(field_index).clone(), - )) - } - _ => exec_err!( - "Second argument must be either an Int64 index or a Utf8 field name" - ), - } -} From 39f2f24b7995a6f1fc326119cca378e7633514ca Mon Sep 17 00:00:00 2001 From: Lordworms Date: Thu, 26 Sep 2024 00:42:52 -0700 Subject: [PATCH 4/5] fix clippy --- datafusion/sql/src/expr/identifier.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/datafusion/sql/src/expr/identifier.rs b/datafusion/sql/src/expr/identifier.rs index 540dfdbc8383f..a420aef7406be 100644 --- a/datafusion/sql/src/expr/identifier.rs +++ b/datafusion/sql/src/expr/identifier.rs @@ -15,17 +15,15 @@ // specific language governing permissions and limitations // under the License. -use std::sync::Arc; - -use arrow_schema::{DataType, Field}; +use arrow_schema::Field; use sqlparser::ast::{Expr as SQLExpr, Ident}; use datafusion_common::{ exec_err, internal_err, not_impl_err, plan_datafusion_err, Column, DFSchema, - DataFusionError, Result, ScalarValue, TableReference, + DataFusionError, Result, TableReference, }; use datafusion_expr::planner::PlannerResult; -use datafusion_expr::{lit, Case, Expr}; +use datafusion_expr::{Case, Expr}; use crate::planner::{ContextProvider, PlannerContext, SqlToRel}; use datafusion_expr::UNNAMED_TABLE; From bda158070c67af9a10a5c318b03fe0c144c8b695 Mon Sep 17 00:00:00 2001 From: Lordworms Date: Thu, 26 Sep 2024 14:56:00 -0700 Subject: [PATCH 5/5] optimize --- datafusion/sql/src/expr/identifier.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datafusion/sql/src/expr/identifier.rs b/datafusion/sql/src/expr/identifier.rs index a420aef7406be..b2c8312709ef0 100644 --- a/datafusion/sql/src/expr/identifier.rs +++ b/datafusion/sql/src/expr/identifier.rs @@ -19,7 +19,7 @@ use arrow_schema::Field; use sqlparser::ast::{Expr as SQLExpr, Ident}; use datafusion_common::{ - exec_err, internal_err, not_impl_err, plan_datafusion_err, Column, DFSchema, + internal_err, not_impl_err, plan_datafusion_err, plan_err, Column, DFSchema, DataFusionError, Result, TableReference, }; use datafusion_expr::planner::PlannerResult; @@ -135,7 +135,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { } } } - exec_err!("could not parse compound identifier from {ids:?}") + plan_err!("could not parse compound identifier from {ids:?}") } // found matching field with no spare identifier(s) Some((field, qualifier, _nested_names)) => { @@ -161,7 +161,7 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { not_impl_err!( "Nested identifiers are not yet supported for OuterReferenceColumn {}", Column::from((qualifier, field)).quoted_flat_name() - ) + ) } // found matching field with no spare identifier(s) Some((field, qualifier, _nested_names)) => {