8787import org .apache .calcite .sql .parser .SqlParser ;
8888import org .apache .calcite .tools .Frameworks ;
8989import org .apache .calcite .tools .RelBuilder ;
90+ import org .slf4j .Logger ;
91+ import org .slf4j .LoggerFactory ;
9092
9193/**
9294 * RelVisitor to convert Substrait Rel plan to Calcite RelNode plan. Unsupported Rel node will call
9597public class SubstraitRelNodeConverter
9698 extends AbstractRelVisitor <RelNode , SubstraitRelNodeConverter .Context , RuntimeException > {
9799
100+ private static final Logger LOGGER = LoggerFactory .getLogger (SubstraitRelNodeConverter .class );
101+
98102 protected final RelDataTypeFactory typeFactory ;
99103
100104 protected final ScalarFunctionConverter scalarFunctionConverter ;
@@ -120,9 +124,9 @@ public SubstraitRelNodeConverter(
120124 this (
121125 typeFactory ,
122126 relBuilder ,
123- createScalarFunctionConverter (extensions , typeFactory , featureBoard . allowDynamicUdfs () ),
124- new AggregateFunctionConverter (extensions . aggregateFunctions () , typeFactory ),
125- new WindowFunctionConverter (extensions . windowFunctions () , typeFactory ),
127+ createScalarFunctionConverter (extensions , typeFactory , featureBoard ),
128+ createAggregateFunctionConverter (extensions , typeFactory , featureBoard ),
129+ createWindowFunctionConverter (extensions , typeFactory , featureBoard ),
126130 TypeConverter .DEFAULT );
127131 }
128132
@@ -165,11 +169,11 @@ public SubstraitRelNodeConverter(
165169 private static ScalarFunctionConverter createScalarFunctionConverter (
166170 SimpleExtension .ExtensionCollection extensions ,
167171 RelDataTypeFactory typeFactory ,
168- boolean allowDynamicUdfs ) {
172+ FeatureBoard featureBoard ) {
169173
170- List <FunctionMappings .Sig > additionalSignatures ;
174+ List <FunctionMappings .Sig > additionalSignatures = new ArrayList <>() ;
171175
172- if (allowDynamicUdfs ) {
176+ if (featureBoard . allowDynamicUdfs () ) {
173177 java .util .Set <String > knownFunctionNames =
174178 FunctionMappings .SCALAR_SIGS .stream ()
175179 .map (FunctionMappings .Sig ::name )
@@ -180,28 +184,124 @@ private static ScalarFunctionConverter createScalarFunctionConverter(
180184 .filter (f -> !knownFunctionNames .contains (f .name ().toLowerCase ()))
181185 .collect (Collectors .toList ());
182186
183- if (dynamicFunctions .isEmpty ()) {
184- additionalSignatures = Collections .emptyList ();
185- } else {
187+ if (!dynamicFunctions .isEmpty ()) {
186188 SimpleExtension .ExtensionCollection dynamicExtensionCollection =
187189 SimpleExtension .ExtensionCollection .builder ().scalarFunctions (dynamicFunctions ).build ();
188190
189191 List <SqlOperator > dynamicOperators =
190192 SimpleExtensionToSqlOperator .from (dynamicExtensionCollection , typeFactory );
191193
192- additionalSignatures =
194+ additionalSignatures . addAll (
193195 dynamicOperators .stream ()
194196 .map (op -> FunctionMappings .s (op , op .getName ()))
195- .collect (Collectors .toList ());
197+ .collect (Collectors .toList ()));
198+ }
199+ }
200+
201+ if (featureBoard .autoFallbackToDynamicFunctionMapping ()) {
202+ List <SimpleExtension .ScalarFunctionVariant > unmappedFunctions =
203+ io .substrait .isthmus .expression .FunctionConverter .getUnmappedFunctions (
204+ extensions .scalarFunctions (), FunctionMappings .SCALAR_SIGS );
205+
206+ if (!unmappedFunctions .isEmpty ()) {
207+ LOGGER .info (
208+ "Dynamically mapping {} unmapped scalar functions: {}" ,
209+ unmappedFunctions .size (),
210+ unmappedFunctions .stream ().map (f -> f .name ()).collect (Collectors .toList ()));
211+
212+ List <SqlOperator > dynamicOperators =
213+ SimpleExtensionToSqlOperator .from (unmappedFunctions , typeFactory );
214+
215+ // Note: We use last-wins deduplication here because:
216+ // 1. Multiple variants of the same function create separate SqlOperator instances
217+ // 2. Calcite's SqlOperator equality is based on name and kind, not identity
218+ // 3. RexCalls may use any one of these equivalent operators
219+ // 4. We only need ONE SqlOperator registered per function name as a key in signatures map
220+ // 5. The FunctionFinder will match all variants based on type signatures
221+ java .util .Map <String , SqlOperator > operatorsByName = new java .util .LinkedHashMap <>();
222+ for (SqlOperator op : dynamicOperators ) {
223+ operatorsByName .put (op .getName ().toLowerCase (), op );
224+ }
225+
226+ additionalSignatures .addAll (
227+ operatorsByName .values ().stream ()
228+ .map (op -> FunctionMappings .s (op , op .getName ().toLowerCase ()))
229+ .collect (Collectors .toList ()));
196230 }
197- } else {
198- additionalSignatures = Collections .emptyList ();
199231 }
200232
201233 return new ScalarFunctionConverter (
202234 extensions .scalarFunctions (), additionalSignatures , typeFactory , TypeConverter .DEFAULT );
203235 }
204236
237+ private static AggregateFunctionConverter createAggregateFunctionConverter (
238+ SimpleExtension .ExtensionCollection extensions ,
239+ RelDataTypeFactory typeFactory ,
240+ FeatureBoard featureBoard ) {
241+
242+ List <FunctionMappings .Sig > additionalSignatures = new ArrayList <>();
243+
244+ if (featureBoard .autoFallbackToDynamicFunctionMapping ()) {
245+ List <SimpleExtension .AggregateFunctionVariant > unmappedFunctions =
246+ io .substrait .isthmus .expression .FunctionConverter .getUnmappedFunctions (
247+ extensions .aggregateFunctions (), FunctionMappings .AGGREGATE_SIGS );
248+
249+ if (!unmappedFunctions .isEmpty ()) {
250+ List <SqlOperator > dynamicOperators =
251+ SimpleExtensionToSqlOperator .from (unmappedFunctions , typeFactory );
252+
253+ // Deduplicate operators by name (last-wins precedence) since multiple variants
254+ // of the same function create multiple SqlOperator objects
255+ java .util .Map <String , SqlOperator > operatorsByName = new java .util .LinkedHashMap <>();
256+ for (SqlOperator op : dynamicOperators ) {
257+ operatorsByName .put (op .getName ().toLowerCase (), op );
258+ }
259+
260+ additionalSignatures .addAll (
261+ operatorsByName .values ().stream ()
262+ .map (op -> FunctionMappings .s (op , op .getName ().toLowerCase ()))
263+ .collect (Collectors .toList ()));
264+ }
265+ }
266+
267+ return new AggregateFunctionConverter (
268+ extensions .aggregateFunctions (), additionalSignatures , typeFactory , TypeConverter .DEFAULT );
269+ }
270+
271+ private static WindowFunctionConverter createWindowFunctionConverter (
272+ SimpleExtension .ExtensionCollection extensions ,
273+ RelDataTypeFactory typeFactory ,
274+ FeatureBoard featureBoard ) {
275+
276+ List <FunctionMappings .Sig > additionalSignatures = new ArrayList <>();
277+
278+ if (featureBoard .autoFallbackToDynamicFunctionMapping ()) {
279+ List <SimpleExtension .WindowFunctionVariant > unmappedFunctions =
280+ io .substrait .isthmus .expression .FunctionConverter .getUnmappedFunctions (
281+ extensions .windowFunctions (), FunctionMappings .WINDOW_SIGS );
282+
283+ if (!unmappedFunctions .isEmpty ()) {
284+ List <SqlOperator > dynamicOperators =
285+ SimpleExtensionToSqlOperator .from (unmappedFunctions , typeFactory );
286+
287+ // Deduplicate operators by name (last-wins precedence) since multiple variants
288+ // of the same function create multiple SqlOperator objects
289+ java .util .Map <String , SqlOperator > operatorsByName = new java .util .LinkedHashMap <>();
290+ for (SqlOperator op : dynamicOperators ) {
291+ operatorsByName .put (op .getName ().toLowerCase (), op );
292+ }
293+
294+ additionalSignatures .addAll (
295+ operatorsByName .values ().stream ()
296+ .map (op -> FunctionMappings .s (op , op .getName ().toLowerCase ()))
297+ .collect (Collectors .toList ()));
298+ }
299+ }
300+
301+ return new WindowFunctionConverter (
302+ extensions .windowFunctions (), additionalSignatures , typeFactory , TypeConverter .DEFAULT );
303+ }
304+
205305 public static RelNode convert (
206306 Rel relRoot ,
207307 RelOptCluster relOptCluster ,
0 commit comments