Skip to main content

datafusion_functions/string/
lower.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
18use arrow::datatypes::DataType;
19
20use crate::string::common::to_lower;
21use datafusion_common::Result;
22use datafusion_common::types::logical_string;
23use datafusion_expr::{
24    Coercion, ColumnarValue, Documentation, EncodingPreservation, ScalarFunctionArgs,
25    ScalarUDFImpl, Signature, TypeSignatureClass, Volatility,
26};
27use datafusion_macros::user_doc;
28
29#[user_doc(
30    doc_section(label = "String Functions"),
31    description = "Converts a string to lower-case.",
32    syntax_example = "lower(str)",
33    sql_example = r#"```sql
34> select lower('Ångström');
35+-------------------------+
36| lower(Utf8("Ångström")) |
37+-------------------------+
38| ångström                |
39+-------------------------+
40```"#,
41    standard_argument(name = "str", prefix = "String"),
42    related_udf(name = "initcap"),
43    related_udf(name = "upper")
44)]
45#[derive(Debug, PartialEq, Eq, Hash)]
46pub struct LowerFunc {
47    signature: Signature,
48}
49
50impl Default for LowerFunc {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl LowerFunc {
57    pub fn new() -> Self {
58        Self {
59            signature: Signature::coercible(
60                vec![
61                    Coercion::new_exact(TypeSignatureClass::Native(logical_string()))
62                        .with_encoding_preservation(EncodingPreservation::dictionary()),
63                ],
64                Volatility::Immutable,
65            ),
66        }
67    }
68}
69
70impl ScalarUDFImpl for LowerFunc {
71    fn name(&self) -> &str {
72        "lower"
73    }
74
75    fn signature(&self) -> &Signature {
76        &self.signature
77    }
78
79    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
80        Ok(arg_types[0].clone())
81    }
82
83    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
84        to_lower(&args.args, "lower")
85    }
86
87    fn documentation(&self) -> Option<&Documentation> {
88        self.doc()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use arrow::array::{Array, ArrayRef, StringArray, StringViewArray};
96    use arrow::datatypes::Field;
97    use datafusion_common::config::ConfigOptions;
98    use std::sync::Arc;
99
100    fn invoke_lower(input: ArrayRef) -> Result<ArrayRef> {
101        let func = LowerFunc::new();
102        let data_type = input.data_type().clone();
103        let args = ScalarFunctionArgs {
104            number_rows: input.len(),
105            args: vec![ColumnarValue::Array(input)],
106            arg_fields: vec![Field::new("a", data_type.clone(), true).into()],
107            return_field: Field::new("f", data_type, true).into(),
108            config_options: Arc::new(ConfigOptions::default()),
109        };
110        match func.invoke_with_args(args)? {
111            ColumnarValue::Array(r) => Ok(r),
112            _ => unreachable!("lower"),
113        }
114    }
115
116    fn to_lower(input: ArrayRef, expected: ArrayRef) -> Result<()> {
117        let result = invoke_lower(input)?;
118        assert_eq!(&expected, &result);
119        Ok(())
120    }
121
122    #[test]
123    fn lower_maybe_optimization() -> Result<()> {
124        let input = Arc::new(StringArray::from(vec![
125            Some("农历新年"),
126            None,
127            Some("DATAFUSION"),
128            Some("0123456789"),
129            Some(""),
130        ])) as ArrayRef;
131
132        let expected = Arc::new(StringArray::from(vec![
133            Some("农历新年"),
134            None,
135            Some("datafusion"),
136            Some("0123456789"),
137            Some(""),
138        ])) as ArrayRef;
139
140        to_lower(input, expected)
141    }
142
143    #[test]
144    fn lower_full_optimization() -> Result<()> {
145        let input = Arc::new(StringArray::from(vec![
146            Some("ARROW"),
147            None,
148            Some("DATAFUSION"),
149            Some("0123456789"),
150            Some(""),
151        ])) as ArrayRef;
152
153        let expected = Arc::new(StringArray::from(vec![
154            Some("arrow"),
155            None,
156            Some("datafusion"),
157            Some("0123456789"),
158            Some(""),
159        ])) as ArrayRef;
160
161        to_lower(input, expected)
162    }
163
164    #[test]
165    fn lower_partial_optimization() -> Result<()> {
166        let input = Arc::new(StringArray::from(vec![
167            Some("ARROW"),
168            None,
169            Some("DATAFUSION"),
170            Some("@_"),
171            Some("0123456789"),
172            Some(""),
173            Some("\t\n"),
174            Some("ὈΔΥΣΣΕΎΣ"),
175            Some("TSCHÜSS"),
176            Some("Ⱦ"), // ⱦ: length change
177            Some("农历新年"),
178        ])) as ArrayRef;
179
180        let expected = Arc::new(StringArray::from(vec![
181            Some("arrow"),
182            None,
183            Some("datafusion"),
184            Some("@_"),
185            Some("0123456789"),
186            Some(""),
187            Some("\t\n"),
188            Some("ὀδυσσεύς"),
189            Some("tschüss"),
190            Some("ⱦ"),
191            Some("农历新年"),
192        ])) as ArrayRef;
193
194        to_lower(input, expected)
195    }
196
197    #[test]
198    fn lower_utf8view() -> Result<()> {
199        let input = Arc::new(StringViewArray::from(vec![
200            Some("ARROW"),
201            None,
202            Some("TSCHÜSS"),
203        ])) as ArrayRef;
204
205        let expected = Arc::new(StringViewArray::from(vec![
206            Some("arrow"),
207            None,
208            Some("tschüss"),
209        ])) as ArrayRef;
210
211        to_lower(input, expected)
212    }
213
214    #[test]
215    fn lower_ascii_utf8view() -> Result<()> {
216        // Mix of inlined (≤12 bytes) and referenced (>12 bytes) strings, plus
217        // a null and an empty, to exercise the all-ASCII Utf8View fast path.
218        let input = Arc::new(StringViewArray::from(vec![
219            Some("ARROW"), // inlined short
220            None,
221            Some("HELLO WORLD 123"), // referenced (15 bytes)
222            Some(""),
223            Some("0123456789"),         // inlined, no case change
224            Some("DATAFUSION IS COOL"), // referenced
225        ])) as ArrayRef;
226
227        let expected = Arc::new(StringViewArray::from(vec![
228            Some("arrow"),
229            None,
230            Some("hello world 123"),
231            Some(""),
232            Some("0123456789"),
233            Some("datafusion is cool"),
234        ])) as ArrayRef;
235
236        to_lower(input, expected)
237    }
238
239    #[test]
240    fn lower_sliced_ascii_utf8view() -> Result<()> {
241        // Slice of a parent that contains a non-ASCII string outside the
242        // slice. The slice is all-ASCII, so the fast path must run and produce
243        // correct output while the parent's unaddressed non-ASCII bytes are
244        // irrelevant to the result.
245        let parent = Arc::new(StringViewArray::from(vec![
246            Some("农历新年LONG ENOUGH FOR BUFFER"),
247            Some("HELLO WORLD 123"),
248            Some("DATAFUSION ROCKS!"),
249            Some("ZZZZZZZZZZZZZZZZ"),
250        ])) as ArrayRef;
251        let sliced = parent.slice(1, 2);
252        let result = invoke_lower(sliced)?;
253        let result_sv = result.as_any().downcast_ref::<StringViewArray>().unwrap();
254
255        let expected = StringViewArray::from(vec![
256            Some("hello world 123"),
257            Some("datafusion rocks!"),
258        ]);
259        assert_eq!(result_sv, &expected);
260        // The slice's two long views address 15 + 17 = 32 bytes; the ASCII
261        // fast path must produce a single packed buffer of exactly that
262        // size, not one scaled to the parent's data buffer.
263        assert_eq!(result_sv.data_buffers().len(), 1);
264        assert_eq!(result_sv.data_buffers()[0].len(), 32);
265        Ok(())
266    }
267
268    #[test]
269    fn lower_utf8view_inline_only_no_buffers() -> Result<()> {
270        // An array whose values are all ≤ 12 bytes is fully inline; the ASCII
271        // fast path should produce no data buffers at all.
272        let input = Arc::new(StringViewArray::from(vec![
273            Some("HELLO"),
274            None,
275            Some(""),
276            Some("0123456789ab"), // 12 bytes — inline boundary
277        ])) as ArrayRef;
278        let result = invoke_lower(input)?;
279        let result_sv = result.as_any().downcast_ref::<StringViewArray>().unwrap();
280
281        let expected = StringViewArray::from(vec![
282            Some("hello"),
283            None,
284            Some(""),
285            Some("0123456789ab"),
286        ]);
287        assert_eq!(result_sv, &expected);
288        assert_eq!(
289            result_sv.data_buffers().len(),
290            0,
291            "inline-only Utf8View should produce no data buffers"
292        );
293        Ok(())
294    }
295
296    #[test]
297    fn lower_utf8view_long_packs_tight() -> Result<()> {
298        // Mix of long and inline values; the long values should be packed into
299        // a single tight output buffer whose size is exactly the sum of their
300        // lengths (inline values do not contribute).
301        let input = Arc::new(StringViewArray::from(vec![
302            Some("HELLO WORLD 123"), // 15 bytes (long)
303            Some("ABC"),             // inline
304            None,
305            Some("DATAFUSION ROCKS!"),   // 17 bytes (long)
306            Some("ANOTHER LONG STRING"), // 19 bytes (long)
307        ])) as ArrayRef;
308        let result = invoke_lower(input)?;
309        let result_sv = result.as_any().downcast_ref::<StringViewArray>().unwrap();
310
311        let expected = StringViewArray::from(vec![
312            Some("hello world 123"),
313            Some("abc"),
314            None,
315            Some("datafusion rocks!"),
316            Some("another long string"),
317        ]);
318        assert_eq!(result_sv, &expected);
319        assert_eq!(result_sv.data_buffers().len(), 1);
320        assert_eq!(result_sv.data_buffers()[0].len(), 15 + 17 + 19);
321        Ok(())
322    }
323
324    #[test]
325    fn lower_utf8view_splits_into_multiple_buffers() -> Result<()> {
326        // Produce enough long-string output to overflow the first data block
327        // (≈16 KiB after the initial doubling) and confirm the fast path
328        // splits across buffers rather than packing everything into one and
329        // risking the i32::MAX offset limit.
330        const STR_LEN: usize = 500;
331        const N: usize = 40; // 40 × 500 B = 20 KiB total — crosses the first block.
332        let value = "X".repeat(STR_LEN);
333        let inputs: Vec<Option<String>> = (0..N).map(|_| Some(value.clone())).collect();
334        let input = Arc::new(StringViewArray::from(inputs.clone())) as ArrayRef;
335        let result = invoke_lower(input)?;
336        let result_sv = result.as_any().downcast_ref::<StringViewArray>().unwrap();
337
338        let expected_value = "x".repeat(STR_LEN);
339        let expected: Vec<Option<&str>> =
340            (0..N).map(|_| Some(expected_value.as_str())).collect();
341        assert_eq!(result_sv, &StringViewArray::from(expected));
342        assert!(
343            result_sv.data_buffers().len() >= 2,
344            "expected the output to span more than one data buffer, got {}",
345            result_sv.data_buffers().len()
346        );
347        // Total bytes across buffers must equal total long-value bytes
348        // (no row was inlined since each value is > 12 bytes).
349        let total: usize = result_sv.data_buffers().iter().map(|b| b.len()).sum();
350        assert_eq!(total, N * STR_LEN);
351        Ok(())
352    }
353
354    #[test]
355    fn lower_sliced_utf8() -> Result<()> {
356        let parent = Arc::new(StringArray::from(vec![
357            Some("AAAAAAAA"),
358            Some("HELLO"),
359            Some("WORLD"),
360            Some(""),
361            Some("ZZZZZZZZ"),
362        ])) as ArrayRef;
363        let sliced = parent.slice(1, 3);
364        let result = invoke_lower(sliced)?;
365        let result_sa = result.as_any().downcast_ref::<StringArray>().unwrap();
366
367        let expected = StringArray::from(vec![Some("hello"), Some("world"), Some("")]);
368        assert_eq!(result_sa, &expected);
369        // The slice's addressed bytes are "HELLO" + "WORLD" = 10; the ASCII
370        // fast path must produce a tight output buffer (not the parent's).
371        assert_eq!(result_sa.value_data().len(), 10);
372        Ok(())
373    }
374}