## Coverage Report

#### Created: 2025-11-22 06:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /Users/andrewlamb/Software/datafusion/datafusion/functions/src/unicode/strpos.rs

    Line
    Count
    Source
    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
    use std::any::Any;
    19
    use std::sync::Arc;
    20
    21
    use crate::utils::{make_scalar_function, utf8_to_int_type};
    22
    use arrow::array::{
    23
        ArrayRef, ArrowPrimitiveType, AsArray, PrimitiveArray, StringArrayType,
    24
    };
    25
    use arrow::datatypes::{
    26
        ArrowNativeType, DataType, Field, FieldRef, Int32Type, Int64Type,
    27
    };
    28
    use datafusion_common::types::logical_string;
    29
    use datafusion_common::{exec_err, internal_err, Result};
    30
    use datafusion_expr::{
    31
        Coercion, ColumnarValue, Documentation, ScalarUDFImpl, Signature, TypeSignatureClass,
    32
        Volatility,
    33
    };
    34
    use datafusion_macros::user_doc;
    35
    36
    #[user_doc(
    37
        doc_section(label = "String Functions"),
    38
        description = "Returns the starting position of a specified substring in a string. Positions begin at 1. If the substring does not exist in the string, the function returns 0.",
    39
        syntax_example = "strpos(str, substr)",
    40
        alternative_syntax = "position(substr in origstr)",
    41
        sql_example = r#"```sql
    42
    > select strpos('datafusion', 'fus');
    43
    +----------------------------------------+
    44
    | strpos(Utf8("datafusion"),Utf8("fus")) |
    45
    +----------------------------------------+
    46
    | 5                                      |
    47
    +----------------------------------------+
    48
    ```"#,
    49
        standard_argument(name = "str", prefix = "String"),
    50
        argument(name = "substr", description = "Substring expression to search for.")
    51
    )]
    52
    #[derive(Debug, PartialEq, Eq, Hash)]
    53
    pub struct StrposFunc {
    54
        signature: Signature,
    55
        aliases: Vec<String>,
    56
    }
    57
    58
    impl Default for StrposFunc {
    59
    0
        fn default() -> Self {
    60
    0
            Self::new()
    61
    0
        }
    62
    }
    63
    64
    impl StrposFunc {
    65
    0
        pub fn new() -> Self {
    66
    0
            Self {
    67
    0
                signature: Signature::coercible(
    68
    0
                    vec![
    69
    0
                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    70
    0
                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
    71
    0
                    ],
    72
    0
                    Volatility::Immutable,
    73
    0
                ),
    74
    0
                aliases: vec![String::from("instr"), String::from("position")],
    75
    0
            }
    76
    0
        }
    77
    }
    78
    79
    impl ScalarUDFImpl for StrposFunc {
    80
    0
        fn as_any(&self) -> &dyn Any {
    81
    0
            self
    82
    0
        }
    83
    84
    0
        fn name(&self) -> &str {
    85
    0
            "strpos"
    86
    0
        }
    87
    88
    0
        fn signature(&self) -> &Signature {
    89
    0
            &self.signature
    90
    0
        }
    91
    92
    0
        fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
    93
    0
            internal_err!("return_field_from_args should be used instead")
    94
    0
        }
    95
    96
    0
        fn return_field_from_args(
    97
    0
            &self,
    98
    0
            args: datafusion_expr::ReturnFieldArgs,
    99
    0
        ) -> Result<FieldRef> {
    100
    0
            utf8_to_int_type(args.arg_fields[0].data_type(), "strpos/instr/position").map(
    101
    0
                |data_type| {
    102
    0
                    Field::new(
    103
    0
                        self.name(),
    104
    0
                        data_type,
    105
    0
                        args.arg_fields.iter().any(|x| x.is_nullable()),
    106
                    )
    107
    0
                    .into()
    108
    0
                },
    109
            )
    110
    0
        }
    111
    112
    0
        fn invoke_with_args(
    113
    0
            &self,
    114
    0
            args: datafusion_expr::ScalarFunctionArgs,
    115
    0
        ) -> Result<ColumnarValue> {
    116
    0
            make_scalar_function(strpos, vec![])(&args.args)
    117
    0
        }
    118
    119
    0
        fn aliases(&self) -> &[String] {
    120
    0
            &self.aliases
    121
    0
        }
    122
    123
    0
        fn documentation(&self) -> Option<&Documentation> {
    124
    0
            self.doc()
    125
    0
        }
    126
    }
    127
    128
    0
    fn strpos(args: &[ArrayRef]) -> Result<ArrayRef> {
    129
    0
        match (args[0].data_type(), args[1].data_type()) {
    130
            (DataType::Utf8, DataType::Utf8) => {
    131
    0
                let string_array = args[0].as_string::<i32>();
    132
    0
                let substring_array = args[1].as_string::<i32>();
    133
    0
                calculate_strpos::<_, _, Int32Type>(string_array, substring_array)
    134
            }
    135
            (DataType::Utf8, DataType::Utf8View) => {
    136
    0
                let string_array = args[0].as_string::<i32>();
    137
    0
                let substring_array = args[1].as_string_view();
    138
    0
                calculate_strpos::<_, _, Int32Type>(string_array, substring_array)
    139
            }
    140
            (DataType::Utf8, DataType::LargeUtf8) => {
    141
    0
                let string_array = args[0].as_string::<i32>();
    142
    0
                let substring_array = args[1].as_string::<i64>();
    143
    0
                calculate_strpos::<_, _, Int32Type>(string_array, substring_array)
    144
            }
    145
            (DataType::LargeUtf8, DataType::Utf8) => {
    146
    0
                let string_array = args[0].as_string::<i64>();
    147
    0
                let substring_array = args[1].as_string::<i32>();
    148
    0
                calculate_strpos::<_, _, Int64Type>(string_array, substring_array)
    149
            }
    150
            (DataType::LargeUtf8, DataType::Utf8View) => {
    151
    0
                let string_array = args[0].as_string::<i64>();
    152
    0
                let substring_array = args[1].as_string_view();
    153
    0
                calculate_strpos::<_, _, Int64Type>(string_array, substring_array)
    154
            }
    155
            (DataType::LargeUtf8, DataType::LargeUtf8) => {
    156
    0
                let string_array = args[0].as_string::<i64>();
    157
    0
                let substring_array = args[1].as_string::<i64>();
    158
    0
                calculate_strpos::<_, _, Int64Type>(string_array, substring_array)
    159
            }
    160
            (DataType::Utf8View, DataType::Utf8View) => {
    161
    0
                let string_array = args[0].as_string_view();
    162
    0
                let substring_array = args[1].as_string_view();
    163
    0
                calculate_strpos::<_, _, Int32Type>(string_array, substring_array)
    164
            }
    165
            (DataType::Utf8View, DataType::Utf8) => {
    166
    0
                let string_array = args[0].as_string_view();
    167
    0
                let substring_array = args[1].as_string::<i32>();
    168
    0
                calculate_strpos::<_, _, Int32Type>(string_array, substring_array)
    169
            }
    170
            (DataType::Utf8View, DataType::LargeUtf8) => {
    171
    0
                let string_array = args[0].as_string_view();
    172
    0
                let substring_array = args[1].as_string::<i64>();
    173
    0
                calculate_strpos::<_, _, Int32Type>(string_array, substring_array)
    174
            }
    175
    176
    0
            other => {
    177
    0
                exec_err!("Unsupported data type combination {other:?} for function strpos")
    178
            }
    179
        }
    180
    0
    }
    181
    182
    /// Returns starting index of specified substring within string, or zero if it's not present. (Same as position(substring in string), but note the reversed argument order.)
    183
    /// strpos('high', 'ig') = 2
    184
    /// The implementation uses UTF-8 code points as characters
    185
    0
    fn calculate_strpos<'a, V1, V2, T: ArrowPrimitiveType>(
    186
    0
        string_array: V1,
    187
    0
        substring_array: V2,
    188
    0
    ) -> Result<ArrayRef>
    189
    0
    where
    190
    0
        V1: StringArrayType<'a, Item = &'a str>,
    191
    0
        V2: StringArrayType<'a, Item = &'a str>,
    192
    {
    193
    0
        let ascii_only = substring_array.is_ascii() && string_array.is_ascii();
    194
    0
        let string_iter = string_array.iter();
    195
    0
        let substring_iter = substring_array.iter();
    196
    197
    0
        let result = string_iter
    198
    0
            .zip(substring_iter)
    199
    0
            .map(|(string, substring)| match (string, substring) {
    200
    0
                (Some(string), Some(substring)) => {
    201
                    // If only ASCII characters are present, we can use the slide window method to find
    202
                    // the sub vector in the main vector. This is faster than string.find() method.
    203
    0
                    if ascii_only {
    204
                        // If the substring is empty, the result is 1.
    205
    0
                        if substring.is_empty() {
    206
    0
                            T::Native::from_usize(1)
    207
                        } else {
    208
    0
                            T::Native::from_usize(
    209
    0
                                string
    210
    0
                                    .as_bytes()
    211
    0
                                    .windows(substring.len())
    212
    0
                                    .position(|w| w == substring.as_bytes())
    213
    0
                                    .map(|x| x + 1)
    214
    0
                                    .unwrap_or(0),
    215
                            )
    216
                        }
    217
                    } else {
    218
                        // The `find` method returns the byte index of the substring.
    219
                        // We count the number of chars up to that byte index.
    220
    0
                        T::Native::from_usize(
    221
    0
                            string
    222
    0
                                .find(substring)
    223
    0
                                .map(|x| string[..x].chars().count() + 1)
    224
    0
                                .unwrap_or(0),
    225
                        )
    226
                    }
    227
                }
    228
    0
                _ => None,
    229
    0
            })
    230
    0
            .collect::<PrimitiveArray<T>>();
    231
    232
    0
        Ok(Arc::new(result) as ArrayRef)
    233
    0
    }
    234
    235
    #[cfg(test)]
    236
    mod tests {
    237
        use arrow::array::{Array, Int32Array, Int64Array};
    238
        use arrow::datatypes::DataType::{Int32, Int64};
    239
    240
        use arrow::datatypes::{DataType, Field};
    241
        use datafusion_common::{Result, ScalarValue};
    242
        use datafusion_expr::{ColumnarValue, ScalarUDFImpl};
    243
    244
        use crate::unicode::strpos::StrposFunc;
    245
        use crate::utils::test::test_function;
    246
    247
        macro_rules! test_strpos {
    248
            ($lhs:literal, $rhs:literal -> $result:literal; $t1:ident $t2:ident $t3:ident $t4:ident $t5:ident) => {
    249
                test_function!(
    250
                    StrposFunc::new(),
    251
                    vec![
    252
                        ColumnarValue::Scalar(ScalarValue::$t1(Some($lhs.to_owned()))),
    253
                        ColumnarValue::Scalar(ScalarValue::$t2(Some($rhs.to_owned()))),
    254
                    ],
    255
                    Ok(Some($result)),
    256
                    $t3,
    257
                    $t4,
    258
                    $t5
    259
                )
    260
            };
    261
        }
    262
    263
        #[test]
    264
        fn test_strpos_functions() {
    265
            // Utf8 and Utf8 combinations
    266
            test_strpos!("alphabet", "ph" -> 3; Utf8 Utf8 i32 Int32 Int32Array);
    267
            test_strpos!("alphabet", "a" -> 1; Utf8 Utf8 i32 Int32 Int32Array);
    268
            test_strpos!("alphabet", "z" -> 0; Utf8 Utf8 i32 Int32 Int32Array);
    269
            test_strpos!("alphabet", "" -> 1; Utf8 Utf8 i32 Int32 Int32Array);
    270
            test_strpos!("", "a" -> 0; Utf8 Utf8 i32 Int32 Int32Array);
    271
            test_strpos!("", "" -> 1; Utf8 Utf8 i32 Int32 Int32Array);
    272
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; Utf8 Utf8 i32 Int32 Int32Array);
    273
    274
            // LargeUtf8 and LargeUtf8 combinations
    275
            test_strpos!("alphabet", "ph" -> 3; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    276
            test_strpos!("alphabet", "a" -> 1; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    277
            test_strpos!("alphabet", "z" -> 0; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    278
            test_strpos!("alphabet", "" -> 1; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    279
            test_strpos!("", "a" -> 0; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    280
            test_strpos!("", "" -> 1; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    281
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; LargeUtf8 LargeUtf8 i64 Int64 Int64Array);
    282
    283
            // Utf8 and LargeUtf8 combinations
    284
            test_strpos!("alphabet", "ph" -> 3; Utf8 LargeUtf8 i32 Int32 Int32Array);
    285
            test_strpos!("alphabet", "a" -> 1; Utf8 LargeUtf8 i32 Int32 Int32Array);
    286
            test_strpos!("alphabet", "z" -> 0; Utf8 LargeUtf8 i32 Int32 Int32Array);
    287
            test_strpos!("alphabet", "" -> 1; Utf8 LargeUtf8 i32 Int32 Int32Array);
    288
            test_strpos!("", "a" -> 0; Utf8 LargeUtf8 i32 Int32 Int32Array);
    289
            test_strpos!("", "" -> 1; Utf8 LargeUtf8 i32 Int32 Int32Array);
    290
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; Utf8 LargeUtf8 i32 Int32 Int32Array);
    291
    292
            // LargeUtf8 and Utf8 combinations
    293
            test_strpos!("alphabet", "ph" -> 3; LargeUtf8 Utf8 i64 Int64 Int64Array);
    294
            test_strpos!("alphabet", "a" -> 1; LargeUtf8 Utf8 i64 Int64 Int64Array);
    295
            test_strpos!("alphabet", "z" -> 0; LargeUtf8 Utf8 i64 Int64 Int64Array);
    296
            test_strpos!("alphabet", "" -> 1; LargeUtf8 Utf8 i64 Int64 Int64Array);
    297
            test_strpos!("", "a" -> 0; LargeUtf8 Utf8 i64 Int64 Int64Array);
    298
            test_strpos!("", "" -> 1; LargeUtf8 Utf8 i64 Int64 Int64Array);
    299
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; LargeUtf8 Utf8 i64 Int64 Int64Array);
    300
    301
            // Utf8View and Utf8View combinations
    302
            test_strpos!("alphabet", "ph" -> 3; Utf8View Utf8View i32 Int32 Int32Array);
    303
            test_strpos!("alphabet", "a" -> 1; Utf8View Utf8View i32 Int32 Int32Array);
    304
            test_strpos!("alphabet", "z" -> 0; Utf8View Utf8View i32 Int32 Int32Array);
    305
            test_strpos!("alphabet", "" -> 1; Utf8View Utf8View i32 Int32 Int32Array);
    306
            test_strpos!("", "a" -> 0; Utf8View Utf8View i32 Int32 Int32Array);
    307
            test_strpos!("", "" -> 1; Utf8View Utf8View i32 Int32 Int32Array);
    308
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; Utf8View Utf8View i32 Int32 Int32Array);
    309
    310
            // Utf8View and Utf8 combinations
    311
            test_strpos!("alphabet", "ph" -> 3; Utf8View Utf8 i32 Int32 Int32Array);
    312
            test_strpos!("alphabet", "a" -> 1; Utf8View Utf8 i32 Int32 Int32Array);
    313
            test_strpos!("alphabet", "z" -> 0; Utf8View Utf8 i32 Int32 Int32Array);
    314
            test_strpos!("alphabet", "" -> 1; Utf8View Utf8 i32 Int32 Int32Array);
    315
            test_strpos!("", "a" -> 0; Utf8View Utf8 i32 Int32 Int32Array);
    316
            test_strpos!("", "" -> 1; Utf8View Utf8 i32 Int32 Int32Array);
    317
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; Utf8View Utf8 i32 Int32 Int32Array);
    318
    319
            // Utf8View and LargeUtf8 combinations
    320
            test_strpos!("alphabet", "ph" -> 3; Utf8View LargeUtf8 i32 Int32 Int32Array);
    321
            test_strpos!("alphabet", "a" -> 1; Utf8View LargeUtf8 i32 Int32 Int32Array);
    322
            test_strpos!("alphabet", "z" -> 0; Utf8View LargeUtf8 i32 Int32 Int32Array);
    323
            test_strpos!("alphabet", "" -> 1; Utf8View LargeUtf8 i32 Int32 Int32Array);
    324
            test_strpos!("", "a" -> 0; Utf8View LargeUtf8 i32 Int32 Int32Array);
    325
            test_strpos!("", "" -> 1; Utf8View LargeUtf8 i32 Int32 Int32Array);
    326
            test_strpos!("ДатаФусион数据融合📊🔥", "📊" -> 15; Utf8View LargeUtf8 i32 Int32 Int32Array);
    327
        }
    328
    329
        #[test]
    330
        fn nullable_return_type() {
    331
            fn get_nullable(string_array_nullable: bool, substring_nullable: bool) -> bool {
    332
                let strpos = StrposFunc::new();
    333
                let args = datafusion_expr::ReturnFieldArgs {
    334
                    arg_fields: &[
    335
                        Field::new("f1", DataType::Utf8, string_array_nullable).into(),
    336
                        Field::new("f2", DataType::Utf8, substring_nullable).into(),
    337
                    ],
    338
                    scalar_arguments: &[None::<&ScalarValue>, None::<&ScalarValue>],
    339
                };
    340
    341
                strpos.return_field_from_args(args).unwrap().is_nullable()
    342
            }
    343
    344
            assert!(!get_nullable(false, false));
    345
    346
            // If any of the arguments is nullable, the result is nullable
    347
            assert!(get_nullable(true, false));
    348
            assert!(get_nullable(false, true));
    349
            assert!(get_nullable(true, true));
    350
        }
    351
    }