1use crate::column::Column;
7use crate::dataframe::DataFrame;
8use crate::expression::Expression;
9use crate::plan::LogicalPlan;
10use crate::session::SparkSession;
11use crate::types::DataType;
12use spark_connect_core::error::Result;
13
14pub struct TableValuedFunction {
18 session: SparkSession,
20}
21
22impl TableValuedFunction {
23 pub(crate) fn new(session: SparkSession) -> Self {
25 TableValuedFunction { session }
26 }
27
28 #[allow(clippy::too_many_arguments)]
33 pub fn udtf(
34 &self,
35 name: &str,
36 arguments: Vec<Column>,
37 return_type: Option<DataType>,
38 eval_type: i32,
39 command: Vec<u8>,
40 python_ver: String,
41 deterministic: bool,
42 ) -> DataFrame {
43 let args: Vec<Expression> = arguments.iter().map(|c| c.expression().clone()).collect();
44 let plan = LogicalPlan::CommonInlineUdtf {
45 function_name: name.to_string(),
46 deterministic,
47 arguments: args,
48 return_type,
49 eval_type,
50 command,
51 python_ver,
52 };
53 DataFrame::new(self.session.clone(), plan)
54 }
55
56 pub fn range(
74 &self,
75 start: i64,
76 end: Option<i64>,
77 step: i64,
78 num_partitions: Option<i32>,
79 ) -> Result<DataFrame> {
80 let (actual_start, actual_end) = if let Some(e) = end {
82 (start, e)
83 } else {
84 (0, start)
85 };
86
87 self.session
88 .range_full(actual_start, actual_end, step, num_partitions)
89 }
90
91 pub fn explode(&self, collection: &Column) -> Result<DataFrame> {
105 self._fn("explode", vec![collection.clone()])
106 }
107
108 pub fn explode_outer(&self, collection: &Column) -> Result<DataFrame> {
112 self._fn("explode_outer", vec![collection.clone()])
113 }
114
115 pub fn inline(&self, input: &Column) -> Result<DataFrame> {
129 self._fn("inline", vec![input.clone()])
130 }
131
132 pub fn inline_outer(&self, input: &Column) -> Result<DataFrame> {
136 self._fn("inline_outer", vec![input.clone()])
137 }
138
139 pub fn json_tuple<C: Into<Column>>(
154 &self,
155 input: &Column,
156 fields: impl IntoIterator<Item = C>,
157 ) -> Result<DataFrame> {
158 let fields: Vec<Column> = fields.into_iter().map(Into::into).collect();
159 if fields.is_empty() {
160 return Err(spark_connect_core::error::SparkError::value(
161 "CANNOT_BE_EMPTY",
162 &[("item", "field")],
163 ));
164 }
165 let mut args = vec![input.clone()];
166 args.extend(fields);
167 self._fn("json_tuple", args)
168 }
169
170 pub fn posexplode(&self, collection: &Column) -> Result<DataFrame> {
174 self._fn("posexplode", vec![collection.clone()])
175 }
176
177 pub fn posexplode_outer(&self, collection: &Column) -> Result<DataFrame> {
181 self._fn("posexplode_outer", vec![collection.clone()])
182 }
183
184 pub fn stack<C: Into<Column>>(
193 &self,
194 n: &Column,
195 fields: impl IntoIterator<Item = C>,
196 ) -> Result<DataFrame> {
197 let fields: Vec<Column> = fields.into_iter().map(Into::into).collect();
198 let mut args = vec![n.clone()];
199 args.extend(fields);
200 self._fn("stack", args)
201 }
202
203 pub fn collations(&self) -> Result<DataFrame> {
205 self._fn("collations", vec![])
206 }
207
208 pub fn sql_keywords(&self) -> Result<DataFrame> {
210 self._fn("sql_keywords", vec![])
211 }
212
213 pub fn variant_explode(&self, input: &Column) -> Result<DataFrame> {
217 self._fn("variant_explode", vec![input.clone()])
218 }
219
220 pub fn variant_explode_outer(&self, input: &Column) -> Result<DataFrame> {
222 self._fn("variant_explode_outer", vec![input.clone()])
223 }
224
225 pub fn python_worker_logs(&self) -> Result<DataFrame> {
227 self._fn("python_worker_logs", vec![])
228 }
229
230 fn _fn(&self, name: &str, args: Vec<Column>) -> Result<DataFrame> {
234 let expressions: Vec<Expression> = args.iter().map(|c| c.expression().clone()).collect();
235 let plan = LogicalPlan::UnresolvedTableValuedFunction {
236 name: name.to_string(),
237 arguments: expressions,
238 };
239 Ok(DataFrame::new(self.session.clone(), plan))
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::session::SparkSession;
247
248 fn session() -> SparkSession {
249 SparkSession::builder()
250 .remote("sc://localhost:15002")
251 .get_or_create()
252 .expect("failed to build session")
253 }
254
255 #[test]
256 fn tvf_range_plan() {
257 let spark = session();
258 let tvf = spark.tvf();
259 let df = tvf.range(0, Some(10), 1, None).unwrap();
260 match &df.plan {
261 LogicalPlan::Range { .. } => {
262 }
264 _ => panic!("expected Range plan"),
265 }
266 }
267
268 #[test]
269 fn tvf_range_single_arg() {
270 let spark = session();
271 let tvf = spark.tvf();
272 let df = tvf.range(5, None, 1, None).unwrap();
274 match &df.plan {
275 LogicalPlan::Range {
276 start, end, step, ..
277 } => {
278 assert_eq!(*start, 0);
279 assert_eq!(*end, 5);
280 assert_eq!(*step, 1);
281 }
282 _ => panic!("expected Range plan"),
283 }
284 }
285
286 #[test]
287 fn tvf_explode_plan() {
288 let spark = session();
289 let tvf = spark.tvf();
290 let col = crate::column::col("array_col");
291 let df = tvf.explode(&col).unwrap();
292 match &df.plan {
293 LogicalPlan::UnresolvedTableValuedFunction { name, arguments } => {
294 assert_eq!(name, "explode");
295 assert_eq!(arguments.len(), 1);
296 }
297 _ => panic!("expected UnresolvedTableValuedFunction plan"),
298 }
299 }
300
301 #[test]
302 fn tvf_explode_outer_plan() {
303 let spark = session();
304 let tvf = spark.tvf();
305 let col = crate::column::col("array_col");
306 let df = tvf.explode_outer(&col).unwrap();
307 match &df.plan {
308 LogicalPlan::UnresolvedTableValuedFunction { name, .. } => {
309 assert_eq!(name, "explode_outer");
310 }
311 _ => panic!("expected UnresolvedTableValuedFunction plan"),
312 }
313 }
314
315 #[test]
316 fn tvf_inline_plan() {
317 let spark = session();
318 let tvf = spark.tvf();
319 let col = crate::column::col("struct_array");
320 let df = tvf.inline(&col).unwrap();
321 match &df.plan {
322 LogicalPlan::UnresolvedTableValuedFunction { name, .. } => {
323 assert_eq!(name, "inline");
324 }
325 _ => panic!("expected UnresolvedTableValuedFunction plan"),
326 }
327 }
328
329 #[test]
330 fn tvf_json_tuple_plan() {
331 let spark = session();
332 let tvf = spark.tvf();
333 let input = crate::column::col("json_col");
334 let fields = vec![crate::column::col("field1"), crate::column::col("field2")];
335 let df = tvf.json_tuple(&input, fields).unwrap();
336 match &df.plan {
337 LogicalPlan::UnresolvedTableValuedFunction { name, arguments } => {
338 assert_eq!(name, "json_tuple");
339 assert_eq!(arguments.len(), 3); }
341 _ => panic!("expected UnresolvedTableValuedFunction plan"),
342 }
343 }
344
345 #[test]
346 fn tvf_json_tuple_empty_fields_error() {
347 let spark = session();
348 let tvf = spark.tvf();
349 let input = crate::column::col("json_col");
350 let result = tvf.json_tuple(&input, Vec::<Column>::new());
351 assert!(result.is_err());
352 }
353
354 #[test]
355 fn tvf_posexplode_plan() {
356 let spark = session();
357 let tvf = spark.tvf();
358 let col = crate::column::col("array_col");
359 let df = tvf.posexplode(&col).unwrap();
360 match &df.plan {
361 LogicalPlan::UnresolvedTableValuedFunction { name, .. } => {
362 assert_eq!(name, "posexplode");
363 }
364 _ => panic!("expected UnresolvedTableValuedFunction plan"),
365 }
366 }
367
368 #[test]
369 fn tvf_stack_plan() {
370 let spark = session();
371 let tvf = spark.tvf();
372 let n = crate::column::lit(3i64);
373 let fields = vec![
374 crate::column::col("col1"),
375 crate::column::col("col2"),
376 crate::column::col("col3"),
377 ];
378 let df = tvf.stack(&n, fields).unwrap();
379 match &df.plan {
380 LogicalPlan::UnresolvedTableValuedFunction { name, arguments } => {
381 assert_eq!(name, "stack");
382 assert_eq!(arguments.len(), 4); }
384 _ => panic!("expected UnresolvedTableValuedFunction plan"),
385 }
386 }
387
388 #[test]
389 fn tvf_collations_plan() {
390 let spark = session();
391 let tvf = spark.tvf();
392 let df = tvf.collations().unwrap();
393 match &df.plan {
394 LogicalPlan::UnresolvedTableValuedFunction { name, arguments } => {
395 assert_eq!(name, "collations");
396 assert!(arguments.is_empty());
397 }
398 _ => panic!("expected UnresolvedTableValuedFunction plan"),
399 }
400 }
401
402 #[test]
403 fn tvf_sql_keywords_plan() {
404 let spark = session();
405 let tvf = spark.tvf();
406 let df = tvf.sql_keywords().unwrap();
407 match &df.plan {
408 LogicalPlan::UnresolvedTableValuedFunction { name, arguments } => {
409 assert_eq!(name, "sql_keywords");
410 assert!(arguments.is_empty());
411 }
412 _ => panic!("expected UnresolvedTableValuedFunction plan"),
413 }
414 }
415}