datafusion_spark/function/url/
url_encode.rs1use std::sync::Arc;
19
20use arrow::array::{
21 Array, ArrayRef, LargeStringBuilder, StringBuilder, StringViewBuilder,
22};
23use arrow::datatypes::DataType;
24use datafusion_common::cast::{
25 as_large_string_array, as_string_array, as_string_view_array,
26};
27use datafusion_common::{Result, exec_err, plan_err};
28use datafusion_expr::{
29 ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
30};
31use datafusion_functions::utils::make_scalar_function;
32use url::form_urlencoded::byte_serialize;
33
34#[derive(Debug, PartialEq, Eq, Hash)]
35pub struct UrlEncode {
36 signature: Signature,
37}
38
39impl Default for UrlEncode {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl UrlEncode {
46 pub fn new() -> Self {
47 Self {
48 signature: Signature::string(1, Volatility::Immutable),
49 }
50 }
51}
52
53impl ScalarUDFImpl for UrlEncode {
54 fn name(&self) -> &str {
55 "url_encode"
56 }
57
58 fn signature(&self) -> &Signature {
59 &self.signature
60 }
61
62 fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
63 if arg_types.len() != 1 {
64 return plan_err!(
65 "{} expects 1 argument, but got {}",
66 self.name(),
67 arg_types.len()
68 );
69 }
70 Ok(arg_types[0].clone())
72 }
73
74 fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
75 let ScalarFunctionArgs { args, .. } = args;
76 make_scalar_function(spark_url_encode, vec![])(&args)
77 }
78}
79
80fn spark_url_encode(args: &[ArrayRef]) -> Result<ArrayRef> {
92 if args.len() != 1 {
93 return exec_err!("`url_encode` expects 1 argument");
94 }
95
96 macro_rules! encode_all {
99 ($array:expr, $builder:expr) => {{
100 let array = $array;
101 let mut builder = $builder;
102 let mut encoded = String::new();
103 for value in array.iter() {
104 match value {
105 Some(value) => {
106 encoded.clear();
107 encoded.extend(byte_serialize(value.as_bytes()));
108 builder.append_value(&encoded);
109 }
110 None => builder.append_null(),
111 }
112 }
113 Ok(Arc::new(builder.finish()) as ArrayRef)
114 }};
115 }
116
117 match &args[0].data_type() {
118 DataType::Utf8 => {
119 let array = as_string_array(&args[0])?;
120 let builder =
121 StringBuilder::with_capacity(array.len(), array.value_data().len());
122 encode_all!(array, builder)
123 }
124 DataType::LargeUtf8 => {
125 let array = as_large_string_array(&args[0])?;
126 let builder =
127 LargeStringBuilder::with_capacity(array.len(), array.value_data().len());
128 encode_all!(array, builder)
129 }
130 DataType::Utf8View => {
131 let array = as_string_view_array(&args[0])?;
132 let builder = StringViewBuilder::with_capacity(array.len());
133 encode_all!(array, builder)
134 }
135 other => exec_err!("`url_encode`: Expr must be STRING, got {other:?}"),
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use arrow::array::{LargeStringArray, StringArray, StringViewArray};
143
144 const INPUT: [Option<&str>; 5] = [
145 Some("https://spark.apache.org"),
146 Some("inva lid://user:pass@host/file\\;param?query\\;p2"),
147 Some("你好"),
148 Some(""),
149 None,
150 ];
151
152 const EXPECTED: [Option<&str>; 5] = [
153 Some("https%3A%2F%2Fspark.apache.org"),
154 Some("inva+lid%3A%2F%2Fuser%3Apass%40host%2Ffile%5C%3Bparam%3Fquery%5C%3Bp2"),
155 Some("%E4%BD%A0%E5%A5%BD"),
156 Some(""),
157 None,
158 ];
159
160 #[test]
161 fn test_encode_utf8() -> Result<()> {
162 let input = Arc::new(StringArray::from(INPUT.to_vec())) as ArrayRef;
163 let result = spark_url_encode(&[input])?;
164 let result = as_string_array(&result)?;
165 assert_eq!(&StringArray::from(EXPECTED.to_vec()), result);
166 Ok(())
167 }
168
169 #[test]
170 fn test_encode_large_utf8() -> Result<()> {
171 let input = Arc::new(LargeStringArray::from(INPUT.to_vec())) as ArrayRef;
172 let result = spark_url_encode(&[input])?;
173 let result = as_large_string_array(&result)?;
174 assert_eq!(&LargeStringArray::from(EXPECTED.to_vec()), result);
175 Ok(())
176 }
177
178 #[test]
179 fn test_encode_utf8_view() -> Result<()> {
180 let input = Arc::new(StringViewArray::from(INPUT.to_vec())) as ArrayRef;
181 let result = spark_url_encode(&[input])?;
182 let result = as_string_view_array(&result)?;
183 assert_eq!(&StringViewArray::from(EXPECTED.to_vec()), result);
184 Ok(())
185 }
186}