datafusion_expr/
arguments.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Argument resolution logic for named function parameters
19
20use crate::Expr;
21use datafusion_common::{Result, plan_err};
22
23/// Represents a named function argument with its original case and quote information.
24///
25/// This struct preserves whether an identifier was quoted in the SQL, which determines
26/// whether case-sensitive or case-insensitive matching should be used per SQL standards.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ArgumentName {
29    /// The argument name in its original case as it appeared in the SQL
30    pub value: String,
31    /// Whether the identifier was quoted (e.g., "STR" vs STR)
32    /// - true: quoted identifier, requires case-sensitive matching
33    /// - false: unquoted identifier, uses case-insensitive matching
34    pub is_quoted: bool,
35}
36
37/// Resolves function arguments, handling named and positional notation.
38///
39/// This function validates and reorders arguments to match the function's parameter names
40/// when named arguments are used.
41///
42/// # Rules
43/// - All positional arguments must come before named arguments
44/// - Named arguments can be in any order after positional arguments
45/// - Parameter names follow SQL identifier rules: unquoted names are case-insensitive
46///   (normalized to lowercase), quoted names are case-sensitive
47/// - No duplicate parameter names allowed
48///
49/// # Arguments
50/// * `param_names` - The function's parameter names in order
51/// * `args` - The argument expressions
52/// * `arg_names` - Optional parameter name for each argument
53///
54/// # Returns
55/// A vector of expressions in the correct order matching the parameter names
56///
57/// # Examples
58/// ```text
59/// Given parameters ["a", "b", "c"]
60/// And call: func(10, c => 30, b => 20)
61/// Returns: [Expr(10), Expr(20), Expr(30)]
62/// ```
63pub fn resolve_function_arguments(
64    param_names: &[String],
65    args: Vec<Expr>,
66    arg_names: Vec<Option<ArgumentName>>,
67) -> Result<Vec<Expr>> {
68    if args.len() != arg_names.len() {
69        return plan_err!(
70            "Internal error: args length ({}) != arg_names length ({})",
71            args.len(),
72            arg_names.len()
73        );
74    }
75
76    // Check if all arguments are positional (fast path)
77    if arg_names.iter().all(|name| name.is_none()) {
78        return Ok(args);
79    }
80
81    validate_argument_order(&arg_names)?;
82
83    reorder_named_arguments(param_names, args, arg_names)
84}
85
86/// Validates that positional arguments come before named arguments
87fn validate_argument_order(arg_names: &[Option<ArgumentName>]) -> Result<()> {
88    let mut seen_named = false;
89    for (i, arg_name) in arg_names.iter().enumerate() {
90        match arg_name {
91            Some(_) => seen_named = true,
92            None if seen_named => {
93                return plan_err!(
94                    "Positional argument at position {} follows named argument. \
95                     All positional arguments must come before named arguments.",
96                    i
97                );
98            }
99            None => {}
100        }
101    }
102    Ok(())
103}
104
105/// Reorders arguments based on named parameters to match signature order
106fn reorder_named_arguments(
107    param_names: &[String],
108    args: Vec<Expr>,
109    arg_names: Vec<Option<ArgumentName>>,
110) -> Result<Vec<Expr>> {
111    let positional_count = arg_names.iter().filter(|n| n.is_none()).count();
112
113    // Capture args length before consuming the vector
114    let args_len = args.len();
115
116    let expected_arg_count = param_names.len();
117
118    if positional_count > expected_arg_count {
119        return plan_err!(
120            "Too many positional arguments: expected at most {}, got {}",
121            expected_arg_count,
122            positional_count
123        );
124    }
125
126    let mut result: Vec<Option<Expr>> = vec![None; expected_arg_count];
127
128    for (i, (arg, arg_name)) in args.into_iter().zip(arg_names).enumerate() {
129        if let Some(arg_name) = arg_name {
130            // Named argument - find parameter index using linear search
131            // Match based on SQL identifier rules:
132            // - Quoted identifiers: case-sensitive (exact match)
133            // - Unquoted identifiers: case-insensitive match
134            let param_index = param_names
135                .iter()
136                .position(|p| {
137                    if arg_name.is_quoted {
138                        // Quoted: exact case match
139                        p == &arg_name.value
140                    } else {
141                        // Unquoted: case-insensitive match
142                        p.eq_ignore_ascii_case(&arg_name.value)
143                    }
144                })
145                .ok_or_else(|| {
146                    datafusion_common::plan_datafusion_err!(
147                        "Unknown parameter name '{}'. Valid parameters are: [{}]",
148                        arg_name.value,
149                        param_names.join(", ")
150                    )
151                })?;
152
153            if result[param_index].is_some() {
154                return plan_err!(
155                    "Parameter '{}' specified multiple times",
156                    arg_name.value
157                );
158            }
159
160            result[param_index] = Some(arg);
161        } else {
162            result[i] = Some(arg);
163        }
164    }
165
166    // Only require parameters up to the number of arguments provided (supports optional parameters)
167    let required_count = args_len;
168    for i in 0..required_count {
169        if result[i].is_none() {
170            return plan_err!("Missing required parameter '{}'", param_names[i]);
171        }
172    }
173
174    // Return only the assigned parameters (handles optional trailing parameters)
175    Ok(result.into_iter().take(required_count).flatten().collect())
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::lit;
182
183    #[test]
184    fn test_all_positional() {
185        let param_names = vec!["a".to_string(), "b".to_string()];
186
187        let args = vec![lit(1), lit("hello")];
188        let arg_names = vec![None, None];
189
190        let result =
191            resolve_function_arguments(&param_names, args.clone(), arg_names).unwrap();
192        assert_eq!(result.len(), 2);
193    }
194
195    #[test]
196    fn test_all_named() {
197        let param_names = vec!["a".to_string(), "b".to_string()];
198
199        let args = vec![lit(1), lit("hello")];
200        let arg_names = vec![
201            Some(ArgumentName {
202                value: "a".to_string(),
203                is_quoted: false,
204            }),
205            Some(ArgumentName {
206                value: "b".to_string(),
207                is_quoted: false,
208            }),
209        ];
210
211        let result = resolve_function_arguments(&param_names, args, arg_names).unwrap();
212        assert_eq!(result.len(), 2);
213    }
214
215    #[test]
216    fn test_case_insensitive_parameter_matching() {
217        // Parameter names in function signature (lowercase)
218        let param_names = vec!["startpos".to_string(), "length".to_string()];
219
220        // Unquoted arguments with different casing should match case-insensitively
221        let args = vec![lit(1), lit(10)];
222        let arg_names = vec![
223            Some(ArgumentName {
224                value: "STARTPOS".to_string(),
225                is_quoted: false,
226            }),
227            Some(ArgumentName {
228                value: "LENGTH".to_string(),
229                is_quoted: false,
230            }),
231        ];
232
233        let result = resolve_function_arguments(&param_names, args, arg_names).unwrap();
234        assert_eq!(result.len(), 2);
235        assert_eq!(result[0], lit(1));
236        assert_eq!(result[1], lit(10));
237
238        // Test with reordering and different cases
239        let args2 = vec![lit(20), lit(5)];
240        let arg_names2 = vec![
241            Some(ArgumentName {
242                value: "Length".to_string(),
243                is_quoted: false,
244            }),
245            Some(ArgumentName {
246                value: "StartPos".to_string(),
247                is_quoted: false,
248            }),
249        ];
250
251        let result2 =
252            resolve_function_arguments(&param_names, args2, arg_names2).unwrap();
253        assert_eq!(result2.len(), 2);
254        assert_eq!(result2[0], lit(5)); // startpos
255        assert_eq!(result2[1], lit(20)); // length
256    }
257
258    #[test]
259    fn test_quoted_parameter_case_sensitive() {
260        // Parameter names in function signature (lowercase)
261        let param_names = vec!["str".to_string(), "start_pos".to_string()];
262
263        // Quoted identifiers with wrong case should fail
264        let args = vec![lit("hello"), lit(1)];
265        let arg_names = vec![
266            Some(ArgumentName {
267                value: "STR".to_string(),
268                is_quoted: true,
269            }),
270            Some(ArgumentName {
271                value: "start_pos".to_string(),
272                is_quoted: true,
273            }),
274        ];
275
276        let result = resolve_function_arguments(&param_names, args, arg_names);
277        assert!(result.is_err());
278        assert!(
279            result
280                .unwrap_err()
281                .to_string()
282                .contains("Unknown parameter")
283        );
284
285        // Quoted identifiers with correct case should succeed
286        let args2 = vec![lit("hello"), lit(1)];
287        let arg_names2 = vec![
288            Some(ArgumentName {
289                value: "str".to_string(),
290                is_quoted: true,
291            }),
292            Some(ArgumentName {
293                value: "start_pos".to_string(),
294                is_quoted: true,
295            }),
296        ];
297
298        let result2 =
299            resolve_function_arguments(&param_names, args2, arg_names2).unwrap();
300        assert_eq!(result2.len(), 2);
301        assert_eq!(result2[0], lit("hello"));
302        assert_eq!(result2[1], lit(1));
303    }
304
305    #[test]
306    fn test_named_reordering() {
307        let param_names = vec!["a".to_string(), "b".to_string(), "c".to_string()];
308
309        // Call with: func(c => 3.0, a => 1, b => "hello")
310        let args = vec![lit(3.0), lit(1), lit("hello")];
311        let arg_names = vec![
312            Some(ArgumentName {
313                value: "c".to_string(),
314                is_quoted: false,
315            }),
316            Some(ArgumentName {
317                value: "a".to_string(),
318                is_quoted: false,
319            }),
320            Some(ArgumentName {
321                value: "b".to_string(),
322                is_quoted: false,
323            }),
324        ];
325
326        let result = resolve_function_arguments(&param_names, args, arg_names).unwrap();
327
328        // Should be reordered to [a, b, c] = [1, "hello", 3.0]
329        assert_eq!(result.len(), 3);
330        assert_eq!(result[0], lit(1));
331        assert_eq!(result[1], lit("hello"));
332        assert_eq!(result[2], lit(3.0));
333    }
334
335    #[test]
336    fn test_mixed_positional_and_named() {
337        let param_names = vec!["a".to_string(), "b".to_string(), "c".to_string()];
338
339        // Call with: func(1, c => 3.0, b => "hello")
340        let args = vec![lit(1), lit(3.0), lit("hello")];
341        let arg_names = vec![
342            None,
343            Some(ArgumentName {
344                value: "c".to_string(),
345                is_quoted: false,
346            }),
347            Some(ArgumentName {
348                value: "b".to_string(),
349                is_quoted: false,
350            }),
351        ];
352
353        let result = resolve_function_arguments(&param_names, args, arg_names).unwrap();
354
355        // Should be reordered to [a, b, c] = [1, "hello", 3.0]
356        assert_eq!(result.len(), 3);
357        assert_eq!(result[0], lit(1));
358        assert_eq!(result[1], lit("hello"));
359        assert_eq!(result[2], lit(3.0));
360    }
361
362    #[test]
363    fn test_positional_after_named_error() {
364        let param_names = vec!["a".to_string(), "b".to_string()];
365
366        // Call with: func(a => 1, "hello") - ERROR
367        let args = vec![lit(1), lit("hello")];
368        let arg_names = vec![
369            Some(ArgumentName {
370                value: "a".to_string(),
371                is_quoted: false,
372            }),
373            None,
374        ];
375
376        let result = resolve_function_arguments(&param_names, args, arg_names);
377        assert!(result.is_err());
378        assert!(
379            result
380                .unwrap_err()
381                .to_string()
382                .contains("Positional argument")
383        );
384    }
385
386    #[test]
387    fn test_unknown_parameter_name() {
388        let param_names = vec!["a".to_string(), "b".to_string()];
389
390        // Call with: func(x => 1, b => "hello") - ERROR
391        let args = vec![lit(1), lit("hello")];
392        let arg_names = vec![
393            Some(ArgumentName {
394                value: "x".to_string(),
395                is_quoted: false,
396            }),
397            Some(ArgumentName {
398                value: "b".to_string(),
399                is_quoted: false,
400            }),
401        ];
402
403        let result = resolve_function_arguments(&param_names, args, arg_names);
404        assert!(result.is_err());
405        assert!(
406            result
407                .unwrap_err()
408                .to_string()
409                .contains("Unknown parameter")
410        );
411    }
412
413    #[test]
414    fn test_duplicate_parameter_name() {
415        let param_names = vec!["a".to_string(), "b".to_string()];
416
417        // Call with: func(a => 1, a => 2) - ERROR
418        let args = vec![lit(1), lit(2)];
419        let arg_names = vec![
420            Some(ArgumentName {
421                value: "a".to_string(),
422                is_quoted: false,
423            }),
424            Some(ArgumentName {
425                value: "a".to_string(),
426                is_quoted: false,
427            }),
428        ];
429
430        let result = resolve_function_arguments(&param_names, args, arg_names);
431        assert!(result.is_err());
432        assert!(
433            result
434                .unwrap_err()
435                .to_string()
436                .contains("specified multiple times")
437        );
438    }
439
440    #[test]
441    fn test_missing_required_parameter() {
442        let param_names = vec!["a".to_string(), "b".to_string(), "c".to_string()];
443
444        // Call with: func(a => 1, c => 3.0) - missing 'b'
445        let args = vec![lit(1), lit(3.0)];
446        let arg_names = vec![
447            Some(ArgumentName {
448                value: "a".to_string(),
449                is_quoted: false,
450            }),
451            Some(ArgumentName {
452                value: "c".to_string(),
453                is_quoted: false,
454            }),
455        ];
456
457        let result = resolve_function_arguments(&param_names, args, arg_names);
458        assert!(result.is_err());
459        assert!(
460            result
461                .unwrap_err()
462                .to_string()
463                .contains("Missing required parameter")
464        );
465    }
466
467    #[test]
468    fn test_mixed_case_signature_unquoted_matching() {
469        // Test with mixed-case signature parameters (lowercase, camelCase, UPPERCASE)
470        // This proves case-insensitive matching works for unquoted identifiers
471        let param_names = vec![
472            "prefix".to_string(),   // lowercase
473            "startPos".to_string(), // camelCase
474            "LENGTH".to_string(),   // UPPERCASE
475        ];
476
477        // Test 1: All lowercase unquoted arguments should match
478        let args1 = vec![lit("a"), lit(1), lit(5)];
479        let arg_names1 = vec![
480            Some(ArgumentName {
481                value: "prefix".to_string(),
482                is_quoted: false,
483            }),
484            Some(ArgumentName {
485                value: "startpos".to_string(), // lowercase version of startPos
486                is_quoted: false,
487            }),
488            Some(ArgumentName {
489                value: "length".to_string(), // lowercase version of LENGTH
490                is_quoted: false,
491            }),
492        ];
493
494        let result1 =
495            resolve_function_arguments(&param_names, args1, arg_names1).unwrap();
496        assert_eq!(result1.len(), 3);
497        assert_eq!(result1[0], lit("a"));
498        assert_eq!(result1[1], lit(1));
499        assert_eq!(result1[2], lit(5));
500
501        // Test 2: All uppercase unquoted arguments should match
502        let args2 = vec![lit("b"), lit(2), lit(10)];
503        let arg_names2 = vec![
504            Some(ArgumentName {
505                value: "PREFIX".to_string(), // uppercase version of prefix
506                is_quoted: false,
507            }),
508            Some(ArgumentName {
509                value: "STARTPOS".to_string(), // uppercase version of startPos
510                is_quoted: false,
511            }),
512            Some(ArgumentName {
513                value: "LENGTH".to_string(), // matches UPPERCASE
514                is_quoted: false,
515            }),
516        ];
517
518        let result2 =
519            resolve_function_arguments(&param_names, args2, arg_names2).unwrap();
520        assert_eq!(result2.len(), 3);
521        assert_eq!(result2[0], lit("b"));
522        assert_eq!(result2[1], lit(2));
523        assert_eq!(result2[2], lit(10));
524
525        // Test 3: Mixed case unquoted arguments should match
526        let args3 = vec![lit("c"), lit(3), lit(15)];
527        let arg_names3 = vec![
528            Some(ArgumentName {
529                value: "Prefix".to_string(), // Title case
530                is_quoted: false,
531            }),
532            Some(ArgumentName {
533                value: "StartPos".to_string(), // matches camelCase
534                is_quoted: false,
535            }),
536            Some(ArgumentName {
537                value: "Length".to_string(), // Title case
538                is_quoted: false,
539            }),
540        ];
541
542        let result3 =
543            resolve_function_arguments(&param_names, args3, arg_names3).unwrap();
544        assert_eq!(result3.len(), 3);
545        assert_eq!(result3[0], lit("c"));
546        assert_eq!(result3[1], lit(3));
547        assert_eq!(result3[2], lit(15));
548    }
549
550    #[test]
551    fn test_mixed_case_signature_quoted_matching() {
552        // Test that quoted identifiers require exact case match with signature
553        let param_names = vec![
554            "prefix".to_string(),   // lowercase
555            "startPos".to_string(), // camelCase
556            "LENGTH".to_string(),   // UPPERCASE
557        ];
558
559        // Test 1: Quoted with wrong case should fail for "prefix"
560        let args_wrong_prefix = vec![lit("a"), lit(1), lit(5)];
561        let arg_names_wrong_prefix = vec![
562            Some(ArgumentName {
563                value: "PREFIX".to_string(), // Wrong case
564                is_quoted: true,
565            }),
566            Some(ArgumentName {
567                value: "startPos".to_string(),
568                is_quoted: true,
569            }),
570            Some(ArgumentName {
571                value: "LENGTH".to_string(),
572                is_quoted: true,
573            }),
574        ];
575
576        let result = resolve_function_arguments(
577            &param_names,
578            args_wrong_prefix,
579            arg_names_wrong_prefix,
580        );
581        assert!(result.is_err());
582        assert!(
583            result
584                .unwrap_err()
585                .to_string()
586                .contains("Unknown parameter")
587        );
588
589        // Test 2: Quoted with wrong case should fail for "startPos"
590        let args_wrong_startpos = vec![lit("a"), lit(1), lit(5)];
591        let arg_names_wrong_startpos = vec![
592            Some(ArgumentName {
593                value: "prefix".to_string(),
594                is_quoted: true,
595            }),
596            Some(ArgumentName {
597                value: "STARTPOS".to_string(), // Wrong case
598                is_quoted: true,
599            }),
600            Some(ArgumentName {
601                value: "LENGTH".to_string(),
602                is_quoted: true,
603            }),
604        ];
605
606        let result2 = resolve_function_arguments(
607            &param_names,
608            args_wrong_startpos,
609            arg_names_wrong_startpos,
610        );
611        assert!(result2.is_err());
612        assert!(
613            result2
614                .unwrap_err()
615                .to_string()
616                .contains("Unknown parameter")
617        );
618
619        // Test 3: Quoted with wrong case should fail for "LENGTH"
620        let args_wrong_length = vec![lit("a"), lit(1), lit(5)];
621        let arg_names_wrong_length = vec![
622            Some(ArgumentName {
623                value: "prefix".to_string(),
624                is_quoted: true,
625            }),
626            Some(ArgumentName {
627                value: "startPos".to_string(),
628                is_quoted: true,
629            }),
630            Some(ArgumentName {
631                value: "length".to_string(), // Wrong case
632                is_quoted: true,
633            }),
634        ];
635
636        let result3 = resolve_function_arguments(
637            &param_names,
638            args_wrong_length,
639            arg_names_wrong_length,
640        );
641        assert!(result3.is_err());
642        assert!(
643            result3
644                .unwrap_err()
645                .to_string()
646                .contains("Unknown parameter")
647        );
648
649        // Test 4: Quoted with exact case should succeed
650        let args_correct = vec![lit("a"), lit(1), lit(5)];
651        let arg_names_correct = vec![
652            Some(ArgumentName {
653                value: "prefix".to_string(), // Exact match
654                is_quoted: true,
655            }),
656            Some(ArgumentName {
657                value: "startPos".to_string(), // Exact match
658                is_quoted: true,
659            }),
660            Some(ArgumentName {
661                value: "LENGTH".to_string(), // Exact match
662                is_quoted: true,
663            }),
664        ];
665
666        let result4 =
667            resolve_function_arguments(&param_names, args_correct, arg_names_correct)
668                .unwrap();
669        assert_eq!(result4.len(), 3);
670        assert_eq!(result4[0], lit("a"));
671        assert_eq!(result4[1], lit(1));
672        assert_eq!(result4[2], lit(5));
673    }
674}