Skip to main content

robin_sparkless_polars/plan/
expr.rs

1//! Expression interpreter: turn serialized expression trees (JSON/serde) into Polars Expr.
2//! Used by the plan interpreter for filter, select, and withColumn payloads.
3
4use polars::prelude::{DataType, Expr, Series, col, lit};
5use serde_json::Value;
6use std::error::Error;
7use std::fmt;
8
9/// Error from parsing or interpreting a plan expression.
10#[derive(Debug)]
11pub struct PlanExprError(String);
12
13impl fmt::Display for PlanExprError {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        write!(f, "{}", self.0)
16    }
17}
18
19impl Error for PlanExprError {}
20
21/// Convert a serialized expression tree (JSON Value) into a Polars Expr.
22/// Supports: bare string (column reference), col, lit, comparison ops (eq, ne, gt, ge, lt, le),
23/// logical (and, or), not, and a subset of functions.
24/// (Fixes #644: accept bare string as column reference so embedders can pass column names.)
25/// Bare number, bool, null are treated as literals (coalesce/nvl args from Sparkless; fixes #828–#838 etc.).
26pub fn expr_from_value(v: &Value) -> Result<Expr, PlanExprError> {
27    // Bare string: treat as column reference (PySpark parity: select/filter with column name).
28    if let Some(name) = v.as_str() {
29        return Ok(col(name));
30    }
31
32    // Bare literals: number, bool, null (e.g. coalesce(col("a"), col("b"), 4.5) when args include 4.5).
33    if v.is_null() {
34        return Ok(lit(polars::prelude::NULL));
35    }
36    if let Some(n) = v.as_i64() {
37        return Ok(lit(n));
38    }
39    if let Some(n) = v.as_f64() {
40        return Ok(lit(n));
41    }
42    if let Some(b) = v.as_bool() {
43        return Ok(lit(b));
44    }
45
46    let obj = v.as_object().ok_or_else(|| {
47        PlanExprError(
48            "expression must be a JSON object, column name string, or literal (number/bool/null)"
49                .to_string(),
50        )
51    })?;
52
53    // Column reference: {"col": "name"} or {"column": "name"} (PySpark/Sparkless parity; #969).
54    if let Some(name) = obj
55        .get("col")
56        .or_else(|| obj.get("column"))
57        .and_then(Value::as_str)
58    {
59        return Ok(col(name));
60    }
61
62    // Literal: {"lit": <value>}
63    if let Some(lit_val) = obj.get("lit") {
64        return lit_from_value(lit_val);
65    }
66
67    // Binary op: {"op": "gt"|"eq"|..., "left": <expr>, "right": <expr>}
68    // When "op" is "coalesce"|"nvl" with "args", treat as function (handled below via fn_name).
69    if let Some(op) = obj.get("op").and_then(Value::as_str) {
70        let args_present = obj.get("args").and_then(Value::as_array).is_some();
71        let fn_style_with_args = args_present && matches!(op, "coalesce" | "nvl");
72        if !fn_style_with_args {
73            match op {
74                "eq" | "ne" | "gt" | "ge" | "lt" | "le" => {
75                    let left_v = obj
76                        .get("left")
77                        .ok_or_else(|| PlanExprError(format!("op '{op}' requires 'left'")))?;
78                    let right_v = obj
79                        .get("right")
80                        .ok_or_else(|| PlanExprError(format!("op '{op}' requires 'right'")))?;
81                    let l = expr_from_value(left_v)?;
82                    let r = expr_from_value(right_v)?;
83
84                    // Best-effort type hints: literals we can infer directly; columns are left
85                    // without types here and will be handled by the DataFrame-level rewriter.
86                    let infer_lit_type = |e: &Expr| -> Option<DataType> {
87                        if let Expr::Literal(lv) = e {
88                            let dt = lv.get_datatype();
89                            if matches!(dt, DataType::Unknown(_)) {
90                                None
91                            } else {
92                                Some(dt)
93                            }
94                        } else {
95                            None
96                        }
97                    };
98
99                    let l_ty = infer_lit_type(&l);
100                    let r_ty = infer_lit_type(&r);
101
102                    // #612: When one side is a literal and the other is a column, assume column is String
103                    // so string–numeric coercion is applied (PySpark parity: df.filter(df["s"] == 123)).
104                    use crate::type_coercion::{CompareOp, coerce_for_pyspark_comparison};
105                    let op_enum = match op {
106                        "eq" => CompareOp::Eq,
107                        "ne" => CompareOp::NotEq,
108                        "gt" => CompareOp::Gt,
109                        "ge" => CompareOp::GtEq,
110                        "lt" => CompareOp::Lt,
111                        "le" => CompareOp::LtEq,
112                        _ => unreachable!(),
113                    };
114                    // #1084: When one side is a column (no type at plan time) and the other is a
115                    // string literal, skip coercion here so the DataFrame-level coerce_string_numeric_comparisons
116                    // (in filter()) can cast the string literal to date/datetime using the actual schema.
117                    let (lt, rt) = match (&l_ty, &r_ty) {
118                        (Some(lt), Some(rt)) => (lt.clone(), rt.clone()),
119                        (Some(lt), None) => (lt.clone(), DataType::String),
120                        (None, Some(rt)) if *rt == DataType::String => {
121                            return Ok(match op {
122                                "eq" => l.eq(r),
123                                "ne" => l.neq(r),
124                                "gt" => l.gt(r),
125                                "ge" => l.gt_eq(r),
126                                "lt" => l.lt(r),
127                                "le" => l.lt_eq(r),
128                                _ => unreachable!(),
129                            });
130                        }
131                        (None, Some(rt)) => (DataType::String, rt.clone()),
132                        (None, None) => {
133                            // No type info; skip coercion (may error at runtime).
134                            return Ok(match op {
135                                "eq" => l.eq(r),
136                                "ne" => l.neq(r),
137                                "gt" => l.gt(r),
138                                "ge" => l.gt_eq(r),
139                                "lt" => l.lt(r),
140                                "le" => l.lt_eq(r),
141                                _ => unreachable!(),
142                            });
143                        }
144                    };
145                    let expr = match coerce_for_pyspark_comparison(
146                        l.clone(),
147                        r.clone(),
148                        &lt,
149                        &rt,
150                        &op_enum,
151                    ) {
152                        Ok((lc, rc)) => match op {
153                            "eq" => lc.eq(rc),
154                            "ne" => lc.neq(rc),
155                            "gt" => lc.gt(rc),
156                            "ge" => lc.gt_eq(rc),
157                            "lt" => lc.lt(rc),
158                            "le" => lc.lt_eq(rc),
159                            _ => unreachable!(),
160                        },
161                        Err(_) => match op {
162                            "eq" => l.eq(r),
163                            "ne" => l.neq(r),
164                            "gt" => l.gt(r),
165                            "ge" => l.gt_eq(r),
166                            "lt" => l.lt(r),
167                            "le" => l.lt_eq(r),
168                            _ => unreachable!(),
169                        },
170                    };
171
172                    return Ok(expr);
173                }
174                "eq_null_safe" => {
175                    let left = obj.get("left").ok_or_else(|| {
176                        PlanExprError("op 'eq_null_safe' requires 'left'".to_string())
177                    })?;
178                    let right = obj.get("right").ok_or_else(|| {
179                        PlanExprError("op 'eq_null_safe' requires 'right'".to_string())
180                    })?;
181                    let l = expr_from_value(left)?;
182                    let r = expr_from_value(right)?;
183                    return eq_null_safe_expr(l, r);
184                }
185                "and" => {
186                    let left = obj
187                        .get("left")
188                        .ok_or_else(|| PlanExprError("op 'and' requires 'left'".to_string()))?;
189                    let right = obj
190                        .get("right")
191                        .ok_or_else(|| PlanExprError("op 'and' requires 'right'".to_string()))?;
192                    return Ok(expr_from_value(left)?.and(expr_from_value(right)?));
193                }
194                "or" => {
195                    let left = obj
196                        .get("left")
197                        .ok_or_else(|| PlanExprError("op 'or' requires 'left'".to_string()))?;
198                    let right = obj
199                        .get("right")
200                        .ok_or_else(|| PlanExprError("op 'or' requires 'right'".to_string()))?;
201                    return Ok(expr_from_value(left)?.or(expr_from_value(right)?));
202                }
203                "not" => {
204                    // #991: Sparkless sends Python ~ (bitwise NOT) as "not". Use bitwise_not so Unknown(Any)
205                    // from when/otherwise is coerced to Int64 and ~x = -1 - x (PySpark parity).
206                    let arg = obj
207                        .get("arg")
208                        .ok_or_else(|| PlanExprError("op 'not' requires 'arg'".to_string()))?;
209                    let arg_expr = expr_from_value(arg)?;
210                    let arg_col = expr_to_column(arg_expr);
211                    return Ok(arg_col.bitwise_not().into_expr());
212                }
213                "bitwise_not" => {
214                    // Explicit bitwise NOT (e.g. if Sparkless sends "bitwise_not" for ~).
215                    let arg = obj.get("arg").ok_or_else(|| {
216                        PlanExprError("op 'bitwise_not' requires 'arg'".to_string())
217                    })?;
218                    let arg_expr = expr_from_value(arg)?;
219                    let arg_col = expr_to_column(arg_expr);
220                    return Ok(arg_col.bitwise_not().into_expr());
221                }
222                "between" => {
223                    let left_v = obj
224                        .get("left")
225                        .ok_or_else(|| PlanExprError("op 'between' requires 'left'".to_string()))?;
226                    let lower_v = obj.get("lower").ok_or_else(|| {
227                        PlanExprError("op 'between' requires 'lower'".to_string())
228                    })?;
229                    let upper_v = obj.get("upper").ok_or_else(|| {
230                        PlanExprError("op 'between' requires 'upper'".to_string())
231                    })?;
232                    let left = expr_from_value(left_v)?;
233                    let lower = expr_from_value(lower_v)?;
234                    let upper = expr_from_value(upper_v)?;
235                    // #628: Apply string–numeric coercion so col("val").between(1, 10) works when val is string.
236                    let infer_lit_type = |e: &Expr| -> Option<DataType> {
237                        if let Expr::Literal(lv) = e {
238                            let dt = lv.get_datatype();
239                            if matches!(dt, DataType::Unknown(_)) {
240                                None
241                            } else {
242                                Some(dt)
243                            }
244                        } else {
245                            None
246                        }
247                    };
248                    let lower_ty = infer_lit_type(&lower);
249                    let upper_ty = infer_lit_type(&upper);
250                    use crate::type_coercion::{CompareOp, coerce_for_pyspark_comparison};
251                    // Assume column is String when unknown so string–numeric coercion is applied (#628).
252                    let lt = DataType::String;
253                    let rt_lower = lower_ty.unwrap_or(DataType::String);
254                    let rt_upper = upper_ty.unwrap_or(DataType::String);
255                    let (left_c, lower_c) = match coerce_for_pyspark_comparison(
256                        left.clone(),
257                        lower.clone(),
258                        &lt,
259                        &rt_lower,
260                        &CompareOp::GtEq,
261                    ) {
262                        Ok((a, b)) => (a, b),
263                        Err(_) => (left.clone(), lower),
264                    };
265                    let upper_clone = upper.clone();
266                    let (left_cc, upper_c) = match coerce_for_pyspark_comparison(
267                        left_c.clone(),
268                        upper,
269                        &lt,
270                        &rt_upper,
271                        &CompareOp::LtEq,
272                    ) {
273                        Ok((a, b)) => (a, b),
274                        Err(_) => (left_c.clone(), upper_clone),
275                    };
276                    return Ok(left_cc.clone().gt_eq(lower_c).and(left_cc.lt_eq(upper_c)));
277                }
278                "**" | "pow" => {
279                    let left_v = obj
280                        .get("left")
281                        .ok_or_else(|| PlanExprError(format!("op '{op}' requires 'left'")))?;
282                    let right_v = obj
283                        .get("right")
284                        .ok_or_else(|| PlanExprError(format!("op '{op}' requires 'right'")))?;
285                    let l = expr_from_value(left_v)?;
286                    let r = expr_from_value(right_v)?;
287                    let left_col = expr_to_column(l);
288                    let right_col = expr_to_column(r);
289                    return Ok(left_col.pow_with(&right_col).into_expr());
290                }
291                "isin" => {
292                    // {"op": "isin", "left": <col_expr>, "right": <list_expr>} or
293                    // {"op": "isin", "left": <col_expr>, "values": [<lit>, ...]}
294                    // Empty list -> lit(false) for all rows (issue #518)
295                    let left_v = obj
296                        .get("left")
297                        .ok_or_else(|| PlanExprError("op 'isin' requires 'left'".to_string()))?;
298                    let left_expr = expr_from_value(left_v)?;
299                    let values_opt =
300                        if let Some(values_arr) = obj.get("values").and_then(Value::as_array) {
301                            try_values_for_isin(values_arr)?
302                        } else if let Some(right_v) = obj.get("right") {
303                            try_values_from_plan_value(right_v)?
304                        } else {
305                            return Err(PlanExprError(
306                                "op 'isin' requires 'right' or 'values'".to_string(),
307                            ));
308                        };
309                    return Ok(match values_opt {
310                        None => lit(false),
311                        Some(values_expr) => {
312                            // #742, #854: values are string series; cast left to string so string and numeric columns both work
313                            // Use implode() to avoid Polars deprecation: is_in with same-dtype collection (pola-rs/polars#22149)
314                            left_expr
315                                .cast(DataType::String)
316                                .is_in(values_expr.implode(), false)
317                        }
318                    });
319                }
320                "getItem" => {
321                    // {"op": "getItem", "left": <col_expr>, "right": <index_lit>} - PySpark 0-based
322                    let left_v = obj
323                        .get("left")
324                        .ok_or_else(|| PlanExprError("op 'getItem' requires 'left'".to_string()))?;
325                    let right_v = obj.get("right").ok_or_else(|| {
326                        PlanExprError("op 'getItem' requires 'right'".to_string())
327                    })?;
328                    let col_expr = expr_from_value(left_v)?;
329                    let idx = lit_as_i64(right_v)?;
330                    let col_c = expr_to_column(col_expr);
331                    return Ok(col_c.get_item(idx).into_expr());
332                }
333                "startswith" => {
334                    // {"op": "startswith", "left": col, "right": {"lit": "prefix"}}
335                    let left_v = obj.get("left").ok_or_else(|| {
336                        PlanExprError("op 'startswith' requires 'left'".to_string())
337                    })?;
338                    let right_v = obj.get("right").ok_or_else(|| {
339                        PlanExprError("op 'startswith' requires 'right'".to_string())
340                    })?;
341                    let col_expr = expr_from_value(left_v)?;
342                    let prefix = lit_as_string(right_v)?;
343                    let col_c = expr_to_column(col_expr);
344                    return Ok(crate::functions::startswith(&col_c, &prefix).into_expr());
345                }
346                "is_null" => {
347                    let arg = obj
348                        .get("arg")
349                        .ok_or_else(|| PlanExprError("op 'is_null' requires 'arg'".to_string()))?;
350                    return Ok(expr_from_value(arg)?.is_null());
351                }
352                "is_not_null" => {
353                    let arg = obj.get("arg").ok_or_else(|| {
354                        PlanExprError("op 'is_not_null' requires 'arg'".to_string())
355                    })?;
356                    return Ok(expr_from_value(arg)?.is_not_null());
357                }
358                "regexp_extract" => {
359                    // {"op": "regexp_extract", "left": col, "pattern": {"lit": "..."}, "group": {"lit": 0}}
360                    // or {"op": "regexp_extract", "args": [col, pattern, group]}
361                    if let Some(args) = obj.get("args").and_then(Value::as_array) {
362                        require_args_min("regexp_extract", args, 3)?;
363                        let col_expr = expr_from_value(&args[0])?;
364                        let pattern = lit_as_string(&args[1])?;
365                        let group_idx = lit_as_usize(&args[2])?;
366                        let col_c = expr_to_column(col_expr);
367                        return Ok(
368                            crate::functions::regexp_extract(&col_c, &pattern, group_idx)
369                                .into_expr(),
370                        );
371                    }
372                    let left_v = obj.get("left").ok_or_else(|| {
373                        PlanExprError("op 'regexp_extract' requires 'left'".to_string())
374                    })?;
375                    let col_expr = expr_from_value(left_v)?;
376                    let pattern_v =
377                        obj.get("pattern")
378                            .or_else(|| obj.get("right"))
379                            .ok_or_else(|| {
380                                PlanExprError(
381                                    "op 'regexp_extract' requires 'pattern' or 'right'".to_string(),
382                                )
383                            })?;
384                    let pattern = lit_as_string(pattern_v)?;
385                    let group_idx = obj
386                        .get("group")
387                        .and_then(|v| lit_as_usize(v).ok())
388                        .unwrap_or(0);
389                    let col_c = expr_to_column(col_expr);
390                    return Ok(
391                        crate::functions::regexp_extract(&col_c, &pattern, group_idx).into_expr(),
392                    );
393                }
394                "regexp_replace" => {
395                    // {"op": "regexp_replace", "left": col, "pattern": {"lit": "..."}, "replacement": {"lit": "..."}}
396                    // or {"op": "regexp_replace", "args": [col, pattern, replacement]} (issue #528)
397                    if let Some(args) = obj.get("args").and_then(Value::as_array) {
398                        require_args_min("regexp_replace", args, 3)?;
399                        let col_expr = expr_from_value(&args[0])?;
400                        let pattern = lit_as_string(&args[1])?;
401                        let replacement = lit_as_string(&args[2])?;
402                        let col_c = expr_to_column(col_expr);
403                        return Ok(crate::functions::regexp_replace(
404                            &col_c,
405                            &pattern,
406                            &replacement,
407                        )
408                        .into_expr());
409                    }
410                    let left_v = obj.get("left").ok_or_else(|| {
411                        PlanExprError("op 'regexp_replace' requires 'left'".to_string())
412                    })?;
413                    let col_expr = expr_from_value(left_v)?;
414                    let pattern = lit_as_string(
415                        obj.get("pattern")
416                            .or_else(|| obj.get("right"))
417                            .ok_or_else(|| {
418                                PlanExprError(
419                                    "op 'regexp_replace' requires 'pattern' or 'right'".to_string(),
420                                )
421                            })?,
422                    )?;
423                    let replacement = lit_as_string(obj.get("replacement").ok_or_else(|| {
424                        PlanExprError("op 'regexp_replace' requires 'replacement'".to_string())
425                    })?)?;
426                    let col_c = expr_to_column(col_expr);
427                    return Ok(
428                        crate::functions::regexp_replace(&col_c, &pattern, &replacement)
429                            .into_expr(),
430                    );
431                }
432                "create_map" | "createMap" => {
433                    // {"op": "create_map"|"createMap", "args": [key1, val1, key2, val2, ...]} (issue #542)
434                    let args_arr = obj.get("args").and_then(Value::as_array).ok_or_else(|| {
435                        PlanExprError(
436                            "op 'create_map'/'createMap' requires 'args' array".to_string(),
437                        )
438                    })?;
439                    let exprs: Result<Vec<Expr>, _> =
440                        args_arr.iter().map(expr_from_value).collect();
441                    let cols: Vec<crate::Column> = exprs?.into_iter().map(expr_to_column).collect();
442                    let refs: Vec<&crate::Column> = cols.iter().collect();
443                    return Ok(crate::functions::create_map(&refs)
444                        .map_err(|e| PlanExprError(e.to_string()))?
445                        .into_expr());
446                }
447                "add" | "+" => {
448                    // {"op": "add", "left": <expr>, "right": <expr>} - e.g. row_number().over(w) + 10
449                    let left_v = obj
450                        .get("left")
451                        .ok_or_else(|| PlanExprError("op 'add' requires 'left'".to_string()))?;
452                    let right_v = obj
453                        .get("right")
454                        .ok_or_else(|| PlanExprError("op 'add' requires 'right'".to_string()))?;
455                    let l = expr_from_value(left_v)?;
456                    let r = expr_from_value(right_v)?;
457                    let a = expr_to_column(l);
458                    let b = expr_to_column(r);
459                    return Ok(a.add_pyspark(&b).into_expr());
460                }
461                "sub" | "minus" | "-" => {
462                    // {"op": "sub", "left": <expr>, "right": <expr>} — e.g. (1 - col("x")) #556
463                    // #990: Use subtract_pyspark so string/string or string/numeric coerces (PySpark parity).
464                    let left_v = obj
465                        .get("left")
466                        .ok_or_else(|| PlanExprError("op 'sub' requires 'left'".to_string()))?;
467                    let right_v = obj
468                        .get("right")
469                        .ok_or_else(|| PlanExprError("op 'sub' requires 'right'".to_string()))?;
470                    let l = expr_from_value(left_v)?;
471                    let r = expr_from_value(right_v)?;
472                    let a = expr_to_column(l);
473                    let b = expr_to_column(r);
474                    return Ok(a.subtract_pyspark(&b).into_expr());
475                }
476                "mul" | "*" => {
477                    // {"op": "mul", "left": <expr>, "right": <expr>} — e.g. (100 * col("x")) #556
478                    // #990: Use multiply_pyspark so string/string or string/numeric coerces (PySpark parity).
479                    let left_v = obj
480                        .get("left")
481                        .ok_or_else(|| PlanExprError("op 'mul' requires 'left'".to_string()))?;
482                    let right_v = obj
483                        .get("right")
484                        .ok_or_else(|| PlanExprError("op 'mul' requires 'right'".to_string()))?;
485                    let l = expr_from_value(left_v)?;
486                    let r = expr_from_value(right_v)?;
487                    let a = expr_to_column(l);
488                    let b = expr_to_column(r);
489                    return Ok(a.multiply_pyspark(&b).into_expr());
490                }
491                "div" | "/" | "divide" => {
492                    // #683: PySpark-style division with string/numeric coercion (op form from Sparkless).
493                    let left_v = obj
494                        .get("left")
495                        .ok_or_else(|| PlanExprError("op 'div' requires 'left'".to_string()))?;
496                    let right_v = obj
497                        .get("right")
498                        .ok_or_else(|| PlanExprError("op 'div' requires 'right'".to_string()))?;
499                    let l = expr_from_value(left_v)?;
500                    let r = expr_from_value(right_v)?;
501                    let a = expr_to_column(l);
502                    let b = expr_to_column(r);
503                    return Ok(a.divide_pyspark(&b).into_expr());
504                }
505                "udf" => {
506                    // {"op": "udf", "udf"|"name": "udf_name", "args": [<expr>, ...]} (issue #545)
507                    let udf_name = obj
508                        .get("udf")
509                        .or_else(|| obj.get("name"))
510                        .and_then(Value::as_str)
511                        .ok_or_else(|| {
512                            PlanExprError("op 'udf' requires 'udf' or 'name'".to_string())
513                        })?;
514                    let args = obj.get("args").and_then(Value::as_array).ok_or_else(|| {
515                        PlanExprError("op 'udf' requires 'args' array".to_string())
516                    })?;
517                    let col = column_from_udf_call(udf_name, args)?;
518                    if col.udf_call.is_some() {
519                        return Err(PlanExprError(
520                        "Python/Vectorized UDFs are only supported in withColumn/select, not in filter/plan expressions"
521                            .into(),
522                    ));
523                    }
524                    return Ok(col.expr().clone());
525                }
526                // #547, #554, #583: Sparkless may send functions as op with "args" (same semantics as fn)
527                "translate" | "substring_index" | "substringIndex" | "levenshtein" | "soundex"
528                | "crc32" | "xxhash64" | "get_json_object" | "getJsonObject" | "json_tuple"
529                | "jsonTuple" | "regexp_extract_all" | "regexpExtractAll" | "date_trunc"
530                | "dateTrunc" | "to_date" | "toDate" | "format_string" | "formatString" | "log"
531                | "explode" | "explode_outer" | "explodeOuter" | "concat" | "contains" => {
532                    let args = obj
533                        .get("args")
534                        .and_then(Value::as_array)
535                        .ok_or_else(|| PlanExprError(format!("op '{op}' requires 'args' array")))?;
536                    let fn_name = match op {
537                        "substringIndex" => "substring_index",
538                        "getJsonObject" => "get_json_object",
539                        "jsonTuple" => "json_tuple",
540                        "regexpExtractAll" => "regexp_extract_all",
541                        "dateTrunc" => "date_trunc",
542                        "toDate" => "to_date",
543                        "formatString" => "format_string",
544                        "explodeOuter" => "explode_outer",
545                        other => other,
546                    };
547                    return expr_from_fn(fn_name, args);
548                }
549                _ => {
550                    return Err(PlanExprError(format!("unsupported expression op: {op}")));
551                }
552            }
553        }
554    }
555
556    // UDF call: {"udf": "name", "args": [<expr>, ...]} - returns Expr for Rust UDF only (Python UDF needs Column path)
557    if let Some(udf_name) = obj.get("udf").and_then(Value::as_str) {
558        let args = obj
559            .get("args")
560            .and_then(Value::as_array)
561            .ok_or_else(|| PlanExprError("udf requires 'args' array".to_string()))?;
562        let col = column_from_udf_call(udf_name, args)?;
563        if col.udf_call.is_some() {
564            return Err(PlanExprError(
565                "Python/Vectorized UDFs are only supported in withColumn/select, not in filter/plan expressions"
566                    .into(),
567            ));
568        }
569        return Ok(col.expr().clone());
570    }
571
572    // Function call: {"fn"|"function": "upper"|...|"row_number", "args": [<expr>, ...], optional "window": {...}}
573    // Sparkless may send "function" instead of "fn" (issue #517). Also accept {"op": "coalesce"|"nvl"|..., "args": [...]} (fixes #828–#838).
574    let fn_name = obj
575        .get("fn")
576        .or_else(|| obj.get("function"))
577        .or_else(|| {
578            // When "args" is present, "op" may denote a function (e.g. coalesce, nvl) rather than a binary op.
579            obj.get("args").and_then(|_| obj.get("op"))
580        })
581        .and_then(Value::as_str);
582    if let Some(fn_name) = fn_name {
583        if let Some(window_val) = obj.get("window") {
584            return expr_from_window_fn(
585                fn_name,
586                window_val,
587                obj.get("args").and_then(Value::as_array),
588            );
589        }
590        let args = obj
591            .get("args")
592            .and_then(Value::as_array)
593            .ok_or_else(|| PlanExprError(format!("fn '{fn_name}' requires 'args' array")))?;
594        return expr_from_fn(fn_name, args);
595    }
596
597    // type: "window" - Sparkless: {"type": "window", "fn"|"function": "row_number", "window": {...}}
598    if let Some(typ) = obj.get("type").and_then(Value::as_str) {
599        if typ == "window" {
600            let fn_name = obj
601                .get("fn")
602                .or_else(|| obj.get("function"))
603                .and_then(Value::as_str)
604                .ok_or_else(|| {
605                    PlanExprError("type window requires 'fn' or 'function'".to_string())
606                })?;
607            let window_val = obj
608                .get("window")
609                .ok_or_else(|| PlanExprError("type window requires 'window'".to_string()))?;
610            let args = obj.get("args").and_then(Value::as_array);
611            return expr_from_window_fn(fn_name, window_val, args);
612        }
613    }
614
615    Err(PlanExprError(
616        "expression must have 'col', 'lit', 'op', 'fn', or 'type'".to_string(),
617    ))
618}
619
620/// Extract column name from window spec item: "col" or {"col": "name"} or {"col": "name", "asc": true} (issue #517).
621fn window_col_from_value(x: &Value) -> Option<String> {
622    if let Some(s) = x.as_str() {
623        return Some(s.to_string());
624    }
625    if let Some(obj) = x.as_object() {
626        if let Some(name) = obj.get("col").and_then(Value::as_str) {
627            return Some(name.to_string());
628        }
629    }
630    None
631}
632
633/// Parse window spec object into (order_col_names, partition_by_col_names, order_by_explicitly_empty).
634/// order_by / partition_by can be arrays of "col" or {"col": "name", "asc": true}.
635/// Does not apply fallback; callers that need order column use order_cols.or(part_cols) (issue #517).
636/// order_by_explicitly_empty is true when "order_by" key is present and its value is an empty array (#985).
637fn parse_window_spec(v: &Value) -> Result<(Vec<String>, Vec<String>, bool), PlanExprError> {
638    let obj = v
639        .as_object()
640        .ok_or_else(|| PlanExprError("window spec must be object".to_string()))?;
641    let order_arr = obj.get("order_by").and_then(Value::as_array);
642    let part_arr = obj.get("partition_by").and_then(Value::as_array);
643    let order_by_explicitly_empty = order_arr.map(|a| a.is_empty()).unwrap_or(false);
644    let order_cols: Vec<String> = order_arr
645        .map(|a| a.iter().filter_map(window_col_from_value).collect())
646        .unwrap_or_default();
647    let part_cols: Vec<String> = part_arr
648        .map(|a| a.iter().filter_map(window_col_from_value).collect())
649        .unwrap_or_default();
650    Ok((order_cols, part_cols, order_by_explicitly_empty))
651}
652
653/// Order column names for window: prefer order_by, fall back to partition_by (issue #517).
654fn window_order_cols(order_cols: &[String], part_cols: &[String]) -> Vec<String> {
655    if order_cols.is_empty() {
656        part_cols.to_vec()
657    } else {
658        order_cols.to_vec()
659    }
660}
661
662/// Build window expression for row_number (issue #517, #520).
663/// #985: When order_by is explicitly empty (and no partition_by to fall back to), error so PySpark/Sparkless tests see "At least one column must be specified".
664fn expr_from_row_number_window(v: &Value) -> Result<Expr, PlanExprError> {
665    let (order_cols, part_cols, order_by_explicitly_empty) = parse_window_spec(v)?;
666    if order_by_explicitly_empty && order_cols.is_empty() && part_cols.is_empty() {
667        return Err(PlanExprError(
668            "At least one column must be specified for orderBy".to_string(),
669        ));
670    }
671    let part_refs: Vec<&str> = part_cols.iter().map(|s| s.as_str()).collect();
672    let effective_order = window_order_cols(&order_cols, &part_cols);
673    let order_col = if effective_order.is_empty() {
674        crate::Column::from_expr(lit(1i32), None)
675    } else {
676        crate::Column::new(effective_order[0].clone())
677    };
678    let rn = order_col.row_number(false).over(&part_refs);
679    Ok(rn.into_expr())
680}
681
682/// Dispatch window fn by name: row_number, rank, dense_rank, percent_rank, ntile, lag, lead, sum, avg (issues #517, #521).
683/// #985: When order_by is explicitly empty and no fallback, error with "At least one column must be specified for orderBy".
684fn expr_from_window_fn(
685    fn_name: &str,
686    window_val: &Value,
687    args: Option<&Vec<Value>>,
688) -> Result<Expr, PlanExprError> {
689    use crate::Column;
690    let (order_cols, part_cols, order_by_explicitly_empty) = parse_window_spec(window_val)?;
691    if order_by_explicitly_empty && order_cols.is_empty() && part_cols.is_empty() {
692        return Err(PlanExprError(
693            "At least one column must be specified for orderBy".to_string(),
694        ));
695    }
696    let part_refs: Vec<&str> = part_cols.iter().map(|s| s.as_str()).collect();
697    let effective_order = window_order_cols(&order_cols, &part_cols);
698    let empty: &[Value] = &[];
699    let args: &[Value] = args.map_or(empty, |v| v);
700    let order_col = if effective_order.is_empty() {
701        Column::from_expr(lit(1i32), None)
702    } else {
703        Column::new(effective_order[0].clone())
704    };
705
706    match fn_name {
707        "row_number" => expr_from_row_number_window(window_val),
708        "rank" => {
709            let c = order_col.rank(false).over(&part_refs);
710            Ok(c.into_expr())
711        }
712        "dense_rank" => {
713            let c = order_col.dense_rank(false).over(&part_refs);
714            Ok(c.into_expr())
715        }
716        "percent_rank" => {
717            let c = order_col.percent_rank(&part_refs, false);
718            Ok(c.into_expr())
719        }
720        "ntile" => {
721            let n = args
722                .first()
723                .and_then(|v| v.get("lit").and_then(Value::as_i64))
724                .or_else(|| args.first().and_then(Value::as_i64))
725                .ok_or_else(|| {
726                    PlanExprError("ntile window requires n (number of buckets)".to_string())
727                })? as u32;
728            let c = order_col.ntile(n.max(1), &part_refs, false);
729            Ok(c.into_expr())
730        }
731        "lag" => {
732            let n = args
733                .get(1)
734                .and_then(|v| v.get("lit").and_then(Value::as_i64))
735                .or_else(|| args.get(1).and_then(Value::as_i64))
736                .unwrap_or(1);
737            let col_expr =
738                expr_to_column(expr_from_value(args.first().ok_or_else(|| {
739                    PlanExprError("lag window requires column arg".to_string())
740                })?)?);
741            let c = col_expr.lag(n).over(&part_refs);
742            Ok(c.into_expr())
743        }
744        "lead" => {
745            let n = args
746                .get(1)
747                .and_then(|v| v.get("lit").and_then(Value::as_i64))
748                .or_else(|| args.get(1).and_then(Value::as_i64))
749                .unwrap_or(1);
750            let col_expr =
751                expr_to_column(expr_from_value(args.first().ok_or_else(|| {
752                    PlanExprError("lead window requires column arg".to_string())
753                })?)?);
754            let c = col_expr.lead(n).over(&part_refs);
755            Ok(c.into_expr())
756        }
757        "sum" => {
758            let col_expr =
759                expr_to_column(expr_from_value(args.first().ok_or_else(|| {
760                    PlanExprError("sum window requires column arg".to_string())
761                })?)?);
762            let sum_expr = col_expr.expr().clone().sum();
763            let partition_exprs: Vec<Expr> = part_refs.iter().map(|s| col(*s)).collect();
764            Ok(sum_expr.over(partition_exprs))
765        }
766        "avg" | "mean" => {
767            let col_expr =
768                expr_to_column(expr_from_value(args.first().ok_or_else(|| {
769                    PlanExprError("avg window requires column arg".to_string())
770                })?)?);
771            let mean_expr = col_expr.expr().clone().mean();
772            let partition_exprs: Vec<Expr> = part_refs.iter().map(|s| col(*s)).collect();
773            Ok(mean_expr.over(partition_exprs))
774        }
775        "approx_count_distinct" => {
776            let col_expr = expr_to_column(expr_from_value(args.first().ok_or_else(|| {
777                PlanExprError("approx_count_distinct window requires column arg".to_string())
778            })?)?);
779            let n_unique_expr = col_expr.expr().clone().n_unique().cast(DataType::Int64);
780            let partition_exprs: Vec<Expr> = part_refs.iter().map(|s| col(*s)).collect();
781            Ok(n_unique_expr.over(partition_exprs))
782        }
783        "cume_dist" => {
784            let c = order_col.cume_dist(&part_refs, false);
785            Ok(c.into_expr())
786        }
787        _ => Err(PlanExprError(format!(
788            "unsupported window fn '{fn_name}' (supported: row_number, rank, dense_rank, percent_rank, cume_dist, ntile, lag, lead, sum, avg, approx_count_distinct)"
789        ))),
790    }
791}
792
793fn lit_from_value(v: &Value) -> Result<Expr, PlanExprError> {
794    use polars::prelude::{NULL, lit};
795    if v.is_null() {
796        return Ok(lit(NULL));
797    }
798    if let Some(n) = v.as_i64() {
799        return Ok(lit(n));
800    }
801    if let Some(n) = v.as_f64() {
802        return Ok(lit(n));
803    }
804    if let Some(b) = v.as_bool() {
805        return Ok(lit(b));
806    }
807    if let Some(s) = v.as_str() {
808        return Ok(lit(s));
809    }
810    Err(PlanExprError("unsupported literal type".to_string()))
811}
812
813fn is_cast_to_string(v: &Value) -> bool {
814    let args = match v.get("args").and_then(|a| a.as_array()) {
815        Some(a) if a.len() >= 2 => a,
816        _ => return false,
817    };
818    if v.get("fn").and_then(|f| f.as_str()) != Some("cast") {
819        return false;
820    }
821    let type_val = &args[1];
822    type_val.as_str() == Some("string")
823        || type_val
824            .get("lit")
825            .and_then(|l| l.as_str())
826            .map(|s| s == "string")
827            .unwrap_or(false)
828}
829
830/// Extract source column when value is cast(regexp_replace(col, r"\.\d+", ""), string).
831/// Used for fused to_timestamp path (#168/#153); returns None if shape does not match.
832fn try_extract_regexp_replace_strip_fraction_source(
833    v: &Value,
834) -> Result<Option<crate::Column>, PlanExprError> {
835    fn value_as_str(val: &Value) -> Option<String> {
836        val.as_str()
837            .map(String::from)
838            .or_else(|| val.get("lit").and_then(|l| l.as_str()).map(String::from))
839    }
840    if !is_cast_to_string(v) {
841        return Ok(None);
842    }
843    let cast_args = match v.get("args").and_then(|a| a.as_array()) {
844        Some(a) if !a.is_empty() => a,
845        _ => return Ok(None),
846    };
847    let inner = match cast_args.first() {
848        Some(x) => x,
849        None => return Ok(None),
850    };
851    let obj = match inner.as_object() {
852        Some(o) => o,
853        None => return Ok(None),
854    };
855    let (source_val, pattern_opt, replacement_opt) =
856        if obj.get("fn").and_then(|f| f.as_str()) == Some("regexp_replace") {
857            let args = match obj.get("args").and_then(|a| a.as_array()) {
858                Some(a) if a.len() >= 3 => a,
859                _ => return Ok(None),
860            };
861            let pattern_opt = value_as_str(&args[1]);
862            let replacement_opt = value_as_str(&args[2]);
863            (args[0].clone(), pattern_opt, replacement_opt)
864        } else if obj.get("op").and_then(|o| o.as_str()) == Some("regexp_replace") {
865            let source_val = match obj.get("left") {
866                Some(s) => s.clone(),
867                None => return Ok(None),
868            };
869            let pattern_opt = obj
870                .get("pattern")
871                .or_else(|| obj.get("right"))
872                .and_then(value_as_str);
873            let replacement_opt = obj.get("replacement").and_then(value_as_str);
874            (source_val, pattern_opt, replacement_opt)
875        } else {
876            return Ok(None);
877        };
878    if pattern_opt.as_deref() != Some(r"\.\d+") || replacement_opt.as_deref() != Some("") {
879        return Ok(None);
880    }
881    let col = expr_to_column(expr_from_value(&source_val)?);
882    Ok(Some(col))
883}
884
885// --- Literal extraction from {"lit": value} (for function args) ---
886
887fn lit_as_string(v: &Value) -> Result<String, PlanExprError> {
888    let lit_val = v
889        .get("lit")
890        .ok_or_else(|| PlanExprError("expected literal".to_string()))?;
891    if lit_val.is_null() {
892        return Err(PlanExprError("literal string cannot be null".to_string()));
893    }
894    if let Some(s) = lit_val.as_str() {
895        return Ok(s.to_string());
896    }
897    if let Some(n) = lit_val.as_i64() {
898        return Ok(n.to_string());
899    }
900    if let Some(n) = lit_val.as_f64() {
901        return Ok(n.to_string());
902    }
903    if let Some(b) = lit_val.as_bool() {
904        return Ok(b.to_string());
905    }
906    Err(PlanExprError(
907        "literal must be string, number, or bool".to_string(),
908    ))
909}
910
911fn lit_as_i64(v: &Value) -> Result<i64, PlanExprError> {
912    let lit_val = v
913        .get("lit")
914        .ok_or_else(|| PlanExprError("expected literal".to_string()))?;
915    lit_val
916        .as_i64()
917        .ok_or_else(|| PlanExprError("literal must be integer".to_string()))
918}
919
920fn lit_as_i32(v: &Value) -> Result<i32, PlanExprError> {
921    let n = lit_as_i64(v)?;
922    n.try_into()
923        .map_err(|_| PlanExprError("literal out of i32 range".to_string()))
924}
925
926fn lit_as_u32(v: &Value) -> Result<u32, PlanExprError> {
927    let lit_val = v
928        .get("lit")
929        .ok_or_else(|| PlanExprError("expected literal".to_string()))?;
930    if let Some(n) = lit_val.as_u64() {
931        return n
932            .try_into()
933            .map_err(|_| PlanExprError("literal out of u32 range".to_string()));
934    }
935    if let Some(n) = lit_val.as_i64() {
936        return (n as u64)
937            .try_into()
938            .map_err(|_| PlanExprError("literal out of u32 range".to_string()));
939    }
940    Err(PlanExprError("literal must be number".to_string()))
941}
942
943fn lit_as_f64(v: &Value) -> Result<f64, PlanExprError> {
944    let lit_val = v
945        .get("lit")
946        .ok_or_else(|| PlanExprError("expected literal".to_string()))?;
947    if let Some(n) = lit_val.as_f64() {
948        return Ok(n);
949    }
950    if let Some(n) = lit_val.as_i64() {
951        return Ok(n as f64);
952    }
953    Err(PlanExprError("literal must be number".to_string()))
954}
955
956#[allow(dead_code)]
957fn lit_as_bool(v: &Value) -> Result<bool, PlanExprError> {
958    let lit_val = v
959        .get("lit")
960        .ok_or_else(|| PlanExprError("expected literal".to_string()))?;
961    lit_val
962        .as_bool()
963        .ok_or_else(|| PlanExprError("literal must be boolean".to_string()))
964}
965
966fn lit_as_usize(v: &Value) -> Result<usize, PlanExprError> {
967    let n = lit_as_i64(v)?;
968    if n < 0 {
969        return Err(PlanExprError(
970            "literal must be non-negative for usize".to_string(),
971        ));
972    }
973    n.try_into()
974        .map_err(|_| PlanExprError("literal out of usize range".to_string()))
975}
976
977/// Extract values for isin from JSON array. Each element: {"lit": v} or plain v. Returns Expr for is_in.
978/// When arr is empty or parses to no values (e.g. all nulls), returns Ok(None) — caller uses lit(false)
979/// so that col.isin([]) is false for all rows (PySpark parity; issue #518).
980/// Type of series (issue #581): when all values are integers use Int64 series so Int64 column
981/// isin(1, 3) works; when all are floats use Float64; otherwise use String series for mixed/string.
982fn try_values_for_isin(arr: &[Value]) -> Result<Option<Expr>, PlanExprError> {
983    if arr.is_empty() {
984        return Ok(None);
985    }
986    let mut str_vals: Vec<String> = Vec::new();
987    for v in arr {
988        let lit_val = if let Some(obj) = v.as_object() {
989            obj.get("lit").unwrap_or(v)
990        } else {
991            v
992        };
993        if lit_val.is_null() {
994            continue;
995        }
996        if let Some(s) = lit_val.as_str() {
997            str_vals.push(s.to_string());
998        } else if let Some(n) = lit_val.as_i64() {
999            str_vals.push(n.to_string());
1000        } else if let Some(n) = lit_val.as_f64() {
1001            str_vals.push(n.to_string());
1002        }
1003    }
1004    if str_vals.is_empty() {
1005        return Ok(None);
1006    }
1007    // #742, #854: Use string series for is_in so string column isin([1,2,3]) works (compare as strings).
1008    // Numeric columns still work via Polars coercion when comparing to string list.
1009    let s: Series = Series::from_iter(str_vals.iter().map(|x| x.as_str()));
1010    Ok(Some(lit(s)))
1011}
1012
1013/// Extract values for isin from plan value. Returns None when empty (caller uses lit(false)).
1014fn try_values_from_plan_value(v: &Value) -> Result<Option<Expr>, PlanExprError> {
1015    if let Some(lit_val) = v.get("lit") {
1016        if let Some(arr) = lit_val.as_array() {
1017            return try_values_for_isin(arr);
1018        }
1019        // Single literal - wrap in array
1020        #[allow(clippy::cloned_ref_to_slice_refs)]
1021        return try_values_for_isin(&[v.clone()]);
1022    }
1023    if let Some(arr) = v.as_array() {
1024        return try_values_for_isin(arr);
1025    }
1026    Err(PlanExprError(
1027        "isin right/values must be array or {lit: [...]}".to_string(),
1028    ))
1029}
1030
1031/// Optional string literal: if `args[i]` is missing or null, return None; else require {"lit": "..."}.
1032fn arg_lit_opt_str(args: &[Value], i: usize) -> Result<Option<String>, PlanExprError> {
1033    let v = match args.get(i) {
1034        Some(x) => x,
1035        None => return Ok(None),
1036    };
1037    if v.is_null() {
1038        return Ok(None);
1039    }
1040    if let Some(obj) = v.as_object() {
1041        if obj.get("lit").is_some() {
1042            return Ok(Some(lit_as_string(v)?));
1043        }
1044    }
1045    Ok(None)
1046}
1047
1048fn arg_expr(args: &[Value], i: usize) -> Result<Expr, PlanExprError> {
1049    let v = args
1050        .get(i)
1051        .ok_or_else(|| PlanExprError(format!("fn requires argument at index {i}")))?;
1052    expr_from_value(v)
1053}
1054
1055/// Accept string literal in either form: bare JSON string or {"lit": "..."} (issue #582).
1056fn arg_lit_str(args: &[Value], i: usize) -> Result<String, PlanExprError> {
1057    let v = args
1058        .get(i)
1059        .ok_or_else(|| PlanExprError(format!("fn requires string literal at index {i}")))?;
1060    if let Some(s) = v.as_str() {
1061        return Ok(s.to_string());
1062    }
1063    lit_as_string(v)
1064}
1065
1066fn arg_lit_i64(args: &[Value], i: usize) -> Result<i64, PlanExprError> {
1067    let v = args
1068        .get(i)
1069        .ok_or_else(|| PlanExprError(format!("fn requires integer literal at index {i}")))?;
1070    lit_as_i64(v)
1071}
1072
1073fn arg_lit_i32(args: &[Value], i: usize) -> Result<i32, PlanExprError> {
1074    let v = args
1075        .get(i)
1076        .ok_or_else(|| PlanExprError(format!("fn requires integer literal at index {i}")))?;
1077    lit_as_i32(v)
1078}
1079
1080fn arg_lit_u32(args: &[Value], i: usize) -> Result<u32, PlanExprError> {
1081    let v = args
1082        .get(i)
1083        .ok_or_else(|| PlanExprError(format!("fn requires non-negative integer at index {i}")))?;
1084    lit_as_u32(v)
1085}
1086
1087fn arg_lit_f64(args: &[Value], i: usize) -> Result<f64, PlanExprError> {
1088    let v = args
1089        .get(i)
1090        .ok_or_else(|| PlanExprError(format!("fn requires number literal at index {i}")))?;
1091    lit_as_f64(v)
1092}
1093
1094/// Accept non-negative integer in either form: bare JSON number or {"lit": n} (issue #582).
1095fn arg_lit_usize(args: &[Value], i: usize) -> Result<usize, PlanExprError> {
1096    let v = args
1097        .get(i)
1098        .ok_or_else(|| PlanExprError(format!("fn requires non-negative integer at index {i}")))?;
1099    if let Some(n) = v.as_i64() {
1100        if n < 0 {
1101            return Err(PlanExprError(
1102                "literal must be non-negative for usize".to_string(),
1103            ));
1104        }
1105        return n
1106            .try_into()
1107            .map_err(|_| PlanExprError("literal out of usize range".to_string()));
1108    }
1109    if let Some(n) = v.as_u64() {
1110        return n
1111            .try_into()
1112            .map_err(|_| PlanExprError("literal out of usize range".to_string()));
1113    }
1114    lit_as_usize(v)
1115}
1116
1117/// Get optional i64 from `args[i]` if present and a literal.
1118fn opt_lit_i64(args: &[Value], i: usize) -> Option<i64> {
1119    let v = args.get(i)?;
1120    v.get("lit").and_then(Value::as_i64)
1121}
1122
1123/// Get optional u64 from args (e.g. for rand(seed)).
1124#[allow(dead_code)]
1125fn opt_lit_u64(args: &[Value], i: usize) -> Option<u64> {
1126    let v = args.get(i)?;
1127    if let Some(n) = v.get("lit").and_then(Value::as_i64) {
1128        if n >= 0 {
1129            return Some(n as u64);
1130        }
1131        return Some((-n) as u64); // allow negative seed as unsigned
1132    }
1133    v.get("lit").and_then(Value::as_u64)
1134}
1135
1136fn expr_to_column(expr: Expr) -> crate::Column {
1137    crate::Column::from_expr(expr, None)
1138}
1139
1140/// Null-safe equality: (a <=> b) is true when both null, or both non-null and equal.
1141/// Applies PySpark-style type coercion (e.g. string vs int) so eq_null_safe matches PySpark (issue #266).
1142fn eq_null_safe_expr(left: Expr, right: Expr) -> Result<Expr, PlanExprError> {
1143    use polars::prelude::*;
1144    let (left_c, right_c) = crate::type_coercion::coerce_for_pyspark_eq_null_safe(left, right)
1145        .map_err(|e| PlanExprError(e.to_string()))?;
1146    let left_null = left_c.clone().is_null();
1147    let right_null = right_c.clone().is_null();
1148    let both_null = left_null.clone().and(right_null.clone());
1149    let both_non_null = left_null.not().and(right_null.not());
1150    let eq_result = left_c.eq(right_c);
1151    Ok(when(both_null)
1152        .then(lit(true))
1153        .when(both_non_null)
1154        .then(eq_result)
1155        .otherwise(lit(false)))
1156}
1157
1158/// Find index of the closing paren matching the open paren at start.
1159fn matching_paren(s: &str, start: usize) -> Option<usize> {
1160    if s.as_bytes().get(start) != Some(&b'(') {
1161        return None;
1162    }
1163    let mut depth = 1u32;
1164    for (i, b) in s.bytes().enumerate().skip(start + 1) {
1165        match b {
1166            b'(' => depth += 1,
1167            b')' => {
1168                depth -= 1;
1169                if depth == 0 {
1170                    return Some(i);
1171                }
1172            }
1173            _ => {}
1174        }
1175    }
1176    None
1177}
1178
1179/// Parse a single part (column name or literal) for concat/concat_ws.
1180fn concat_part_to_expr(part: &str) -> Expr {
1181    let part = part.trim();
1182    if part.is_empty() {
1183        return lit("");
1184    }
1185    if (part.starts_with('"') && part.ends_with('"'))
1186        || (part.starts_with('\'') && part.ends_with('\''))
1187    {
1188        let inner = part[1..part.len() - 1].trim_matches(['\'', '"']);
1189        return lit(inner);
1190    }
1191    col(part)
1192}
1193
1194/// Try to parse a select-item string as concat(...) or concat_ws(...) expression.
1195/// Used when Sparkless sends e.g. "concat(first_name, , last_name)" as a column name;
1196/// we treat it as an expression and evaluate it. Returns None if s doesn't look like concat/concat_ws.
1197pub fn try_parse_concat_expr_from_string(s: &str) -> Option<Expr> {
1198    use polars::prelude::concat_str;
1199    let s = s.trim();
1200    // concat(...)
1201    if s.starts_with("concat(") {
1202        let close = matching_paren(s, 6)?; // 6 = len("concat")
1203        if close != s.len() - 1 {
1204            return None;
1205        }
1206        let inner = s[7..close].trim();
1207        let parts: Vec<&str> = inner.split(',').map(str::trim).collect();
1208        if parts.is_empty() {
1209            return None;
1210        }
1211        let exprs: Vec<Expr> = parts.iter().map(|p| concat_part_to_expr(p)).collect();
1212        return Some(concat_str(&exprs, "", false));
1213    }
1214    // concat_ws(sep, ...)
1215    if s.starts_with("concat_ws(") {
1216        let close = matching_paren(s, 10)?; // 10 = len("concat_ws")
1217        if close != s.len() - 1 {
1218            return None;
1219        }
1220        let inner = s[10..close].trim();
1221        let parts: Vec<&str> = inner.split(',').map(str::trim).collect();
1222        if parts.len() < 2 {
1223            return None;
1224        }
1225        let sep = parts[0].trim_matches(['\'', '"']);
1226        let exprs: Vec<Expr> = parts
1227            .iter()
1228            .skip(1)
1229            .map(|p| concat_part_to_expr(p))
1230            .collect();
1231        if exprs.is_empty() {
1232            return None;
1233        }
1234        return Some(concat_str(&exprs, sep, false));
1235    }
1236    None
1237}
1238
1239/// Build a Column from a UDF call. Used by expr_from_value and apply_op (withColumn).
1240/// Returns Column; caller checks udf_call for Python UDF (needs with_column, not with_column_expr).
1241pub fn column_from_udf_call(
1242    udf_name: &str,
1243    args: &[Value],
1244) -> Result<crate::Column, PlanExprError> {
1245    use crate::Column;
1246    let cols: Vec<Column> = args
1247        .iter()
1248        .map(|v| expr_from_value(v).map(expr_to_column))
1249        .collect::<Result<Vec<_>, _>>()?;
1250    crate::functions::call_udf(udf_name, &cols).map_err(|e| PlanExprError(e.to_string()))
1251}
1252
1253/// Try to parse a UDF expression and build Column. Supports {"udf": "name", "args": [...]}
1254/// and {"fn": "call_udf", "args": [{"lit": "name"}, ...]}. Returns None if not a UDF expression.
1255pub fn try_column_from_udf_value(v: &Value) -> Option<Result<crate::Column, PlanExprError>> {
1256    let obj = v.as_object()?;
1257    let (udf_name, args) = if let Some(name) = obj.get("udf").and_then(Value::as_str) {
1258        let args = obj.get("args")?.as_array()?;
1259        (name.to_string(), args)
1260    } else if obj.get("fn").and_then(Value::as_str) == Some("call_udf") {
1261        let args = obj.get("args")?.as_array()?;
1262        if args.is_empty() {
1263            return Some(Err(PlanExprError(
1264                "call_udf requires at least name and one arg".into(),
1265            )));
1266        }
1267        let name = match lit_as_string(&args[0]) {
1268            Ok(n) => n,
1269            Err(e) => return Some(Err(e)),
1270        };
1271        let rest: &[Value] = &args[1..];
1272        return Some(column_from_udf_call(&name, rest));
1273    } else {
1274        return None;
1275    };
1276    Some(column_from_udf_call(&udf_name, args))
1277}
1278
1279fn expr_from_fn(name: &str, args: &[Value]) -> Result<Expr, PlanExprError> {
1280    use crate::Column;
1281    #[allow(unused_imports)]
1282    use crate::functions::{
1283        add_months, array_agg, array_append, array_compact, array_contains, array_distinct,
1284        array_except, array_insert, array_intersect, array_join, array_prepend, array_remove,
1285        array_slice, array_sort, array_sum, array_union, arrays_overlap, arrays_zip, ascii,
1286        assert_true, atan2, base64, bin, bit_and, bit_count, bit_get, bit_length, bit_or, bit_xor,
1287        bitwise_not, bround, btrim, cast, cbrt, ceiling, char as rs_char, chr, coalesce, concat,
1288        concat_ws, contains, conv, cos, cosh, cot, crc32, csc, curdate, current_catalog,
1289        current_database, current_date, current_schema, current_timestamp, current_timezone,
1290        current_user, date_add, date_diff, date_format, date_from_unix_date, date_part, date_sub,
1291        date_trunc, dateadd, datediff, datepart, day, dayname, dayofmonth, dayofweek, dayofyear,
1292        days, decode, degrees, e, element_at, elt, encode, endswith, equal_null, exp,
1293        explode_outer, extract, factorial, find_in_set, floor, format_number, format_string,
1294        from_unixtime, from_utc_timestamp, get, get_json_object, getbit, greatest, hash, hex, hour,
1295        hypot, ilike, initcap, input_file_name, instr, isnan, json_tuple, last_day, lcase, least,
1296        left, length, like, lit_str, ln, localtimestamp, locate, log, log1p, log2, log10, lower,
1297        lpad, make_date, make_interval, make_timestamp, make_timestamp_ntz, mask, md5, minute,
1298        monotonically_increasing_id, month, months_between, nanvl, negate, negative, next_day, now,
1299        nullif, nvl, nvl2, octet_length, overlay, parse_url, pi, pmod, positive, pow, power,
1300        quarter, radians, raise_error, rand, randn, regexp, regexp_count, regexp_extract,
1301        regexp_extract_all, regexp_instr, regexp_like, regexp_replace, regexp_substr, repeat,
1302        replace, reverse, right, rint, rlike, round, rpad, sec, second, sha1, sha2, shift_left,
1303        shift_right, signum, sin, sinh, size, soundex, spark_partition_id, split, split_part, sqrt,
1304        startswith, str_to_map, struct_, substr, substring, substring_index, tan, tanh,
1305        timestamp_micros, timestamp_millis, timestamp_seconds, timestampadd, timestampdiff,
1306        to_binary, to_char, to_date, to_degrees, to_radians, to_timestamp,
1307        to_timestamp_fused_strip_fraction, to_unix_timestamp, to_utc_timestamp, to_varchar,
1308        translate, trim, trunc, try_add, try_cast, try_divide, try_element_at, try_multiply,
1309        try_subtract, try_to_binary, try_to_number, try_to_timestamp, typeof_, ucase, unbase64,
1310        unhex, unix_date, unix_micros, unix_millis, unix_seconds, unix_timestamp,
1311        unix_timestamp_now, upper, url_decode, url_encode, user, version, weekday, weekofyear,
1312        when_then_otherwise_null, width_bucket, xxhash64, year,
1313    };
1314
1315    match name {
1316        "call_udf" => {
1317            if args.is_empty() {
1318                return Err(PlanExprError(
1319                    "call_udf requires at least name and one arg".into(),
1320                ));
1321            }
1322            let udf_name = lit_as_string(&args[0])?;
1323            let col = column_from_udf_call(&udf_name, &args[1..])?;
1324            if col.udf_call.is_some() {
1325                return Err(PlanExprError(
1326                    "Python/Vectorized UDFs are only supported in withColumn/select, not in filter/plan expressions"
1327                        .into(),
1328                ));
1329            }
1330            Ok(col.expr().clone())
1331        }
1332        "upper" => {
1333            require_args(name, args, 1)?;
1334            let c = expr_to_column(arg_expr(args, 0)?);
1335            Ok(upper(&c).into_expr())
1336        }
1337        "lower" => {
1338            require_args(name, args, 1)?;
1339            let c = expr_to_column(arg_expr(args, 0)?);
1340            Ok(lower(&c).into_expr())
1341        }
1342        "coalesce" => {
1343            if args.is_empty() {
1344                return Err(PlanExprError(format!(
1345                    "fn '{name}' requires at least one argument"
1346                )));
1347            }
1348            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
1349            let exprs = exprs?;
1350            Ok(polars::prelude::coalesce(&exprs))
1351        }
1352        "when" => {
1353            if args.len() != 2 {
1354                return Err(PlanExprError(format!(
1355                    "fn '{name}' two-arg form requires [condition, then_expr]"
1356                )));
1357            }
1358            // #680: only the condition must be Boolean; then/else can be any type (string->boolean via expr_coerce_to_boolean).
1359            let cond_expr = crate::functions::expr_coerce_to_boolean(arg_expr(args, 0)?);
1360            let cond = expr_to_column(cond_expr);
1361            let then_val = expr_to_column(arg_expr(args, 1)?);
1362            Ok(when_then_otherwise_null(&cond, &then_val).into_expr())
1363        }
1364        "assert_true" => {
1365            // #979: assert_true requires Boolean; coerce so string/numeric columns work (PySpark parity).
1366            require_args_min(name, args, 1)?;
1367            let cond_expr = crate::functions::expr_coerce_to_boolean(arg_expr(args, 0)?);
1368            let c = expr_to_column(cond_expr);
1369            let err_msg_opt: Option<String> = arg_lit_opt_str(args, 1)?;
1370            let err_msg = err_msg_opt.as_deref();
1371            Ok(assert_true(&c, err_msg).into_expr())
1372        }
1373        // --- String ---
1374        "length" | "char_length" | "character_length" => {
1375            require_args(name, args, 1)?;
1376            Ok(length(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1377        }
1378        "trim" => {
1379            require_args(name, args, 1)?;
1380            Ok(trim(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1381        }
1382        "ltrim" => {
1383            require_args(name, args, 1)?;
1384            Ok(crate::functions::ltrim(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1385        }
1386        "rtrim" => {
1387            require_args(name, args, 1)?;
1388            Ok(crate::functions::rtrim(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1389        }
1390        "btrim" => {
1391            require_args_min(name, args, 1)?;
1392            let c = expr_to_column(arg_expr(args, 0)?);
1393            let trim_str: Option<String> = arg_lit_opt_str(args, 1)?;
1394            Ok(btrim(&c, trim_str.as_deref()).into_expr())
1395        }
1396        "substring" | "substr" => {
1397            require_args_min(name, args, 2)?;
1398            let c = expr_to_column(arg_expr(args, 0)?);
1399            let start = arg_lit_i64(args, 1)?;
1400            let len = opt_lit_i64(args, 2);
1401            Ok(substring(&c, start, len).into_expr())
1402        }
1403        "concat" => {
1404            if args.is_empty() {
1405                return Err(PlanExprError(format!(
1406                    "fn '{name}' requires at least one argument"
1407                )));
1408            }
1409            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
1410            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
1411            let refs: Vec<&Column> = cols.iter().collect();
1412            Ok(concat(&refs).into_expr())
1413        }
1414        "concat_ws" => {
1415            require_args_min(name, args, 2)?;
1416            let sep = arg_lit_str(args, 0)?;
1417            let exprs: Result<Vec<Expr>, _> = args.iter().skip(1).map(expr_from_value).collect();
1418            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
1419            let refs: Vec<&Column> = cols.iter().collect();
1420            Ok(concat_ws(&sep, &refs).into_expr())
1421        }
1422        "initcap" => {
1423            require_args(name, args, 1)?;
1424            Ok(initcap(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1425        }
1426        "repeat" => {
1427            require_args(name, args, 2)?;
1428            let c = expr_to_column(arg_expr(args, 0)?);
1429            let n = arg_lit_i32(args, 1)?;
1430            Ok(repeat(&c, n).into_expr())
1431        }
1432        "reverse" => {
1433            require_args(name, args, 1)?;
1434            Ok(reverse(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1435        }
1436        "instr" => {
1437            require_args(name, args, 2)?;
1438            let c = expr_to_column(arg_expr(args, 0)?);
1439            let substr = arg_lit_str(args, 1)?;
1440            Ok(instr(&c, &substr).into_expr())
1441        }
1442        "position" => {
1443            require_args_min(name, args, 2)?;
1444            let substr = arg_lit_str(args, 0)?;
1445            let c = expr_to_column(arg_expr(args, 1)?);
1446            let pos = opt_lit_i64(args, 2).unwrap_or(1);
1447            Ok(locate(&substr, &c, pos).into_expr())
1448        }
1449        "locate" => {
1450            require_args_min(name, args, 2)?;
1451            let substr = arg_lit_str(args, 0)?;
1452            let c = expr_to_column(arg_expr(args, 1)?);
1453            let pos = opt_lit_i64(args, 2).unwrap_or(1);
1454            Ok(locate(&substr, &c, pos).into_expr())
1455        }
1456        "ascii" => {
1457            require_args(name, args, 1)?;
1458            Ok(ascii(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1459        }
1460        "format_number" => {
1461            require_args(name, args, 2)?;
1462            let c = expr_to_column(arg_expr(args, 0)?);
1463            let decimals = arg_lit_u32(args, 1)?;
1464            Ok(format_number(&c, decimals).into_expr())
1465        }
1466        "overlay" => {
1467            require_args_min(name, args, 4)?;
1468            let c = expr_to_column(arg_expr(args, 0)?);
1469            let replace_str = arg_lit_str(args, 1)?;
1470            let pos = arg_lit_i64(args, 2)?;
1471            let len = arg_lit_i64(args, 3)?;
1472            Ok(overlay(&c, &replace_str, pos, len).into_expr())
1473        }
1474        "char" => {
1475            require_args(name, args, 1)?;
1476            Ok(rs_char(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1477        }
1478        "chr" => {
1479            require_args(name, args, 1)?;
1480            Ok(chr(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1481        }
1482        "base64" => {
1483            require_args(name, args, 1)?;
1484            Ok(base64(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1485        }
1486        "unbase64" => {
1487            require_args(name, args, 1)?;
1488            Ok(unbase64(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1489        }
1490        "sha1" => {
1491            require_args(name, args, 1)?;
1492            Ok(sha1(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1493        }
1494        "sha2" => {
1495            require_args(name, args, 2)?;
1496            let c = expr_to_column(arg_expr(args, 0)?);
1497            let bits = arg_lit_i32(args, 1)?;
1498            Ok(sha2(&c, bits).into_expr())
1499        }
1500        "md5" => {
1501            require_args(name, args, 1)?;
1502            Ok(md5(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1503        }
1504        "lpad" => {
1505            require_args(name, args, 3)?;
1506            let c = expr_to_column(arg_expr(args, 0)?);
1507            let len = arg_lit_i32(args, 1)?;
1508            let pad = arg_lit_str(args, 2)?;
1509            Ok(lpad(&c, len, &pad).into_expr())
1510        }
1511        "rpad" => {
1512            require_args(name, args, 3)?;
1513            let c = expr_to_column(arg_expr(args, 0)?);
1514            let len = arg_lit_i32(args, 1)?;
1515            let pad = arg_lit_str(args, 2)?;
1516            Ok(rpad(&c, len, &pad).into_expr())
1517        }
1518        "translate" => {
1519            require_args(name, args, 3)?;
1520            let c = expr_to_column(arg_expr(args, 0)?);
1521            let from_str = arg_lit_str(args, 1)?;
1522            let to_str = arg_lit_str(args, 2)?;
1523            Ok(translate(&c, &from_str, &to_str).into_expr())
1524        }
1525        "substring_index" => {
1526            require_args(name, args, 3)?;
1527            let c = expr_to_column(arg_expr(args, 0)?);
1528            let delim = arg_lit_str(args, 1)?;
1529            let count = arg_lit_i64(args, 2)?;
1530            Ok(substring_index(&c, &delim, count).into_expr())
1531        }
1532        "left" => {
1533            require_args(name, args, 2)?;
1534            let c = expr_to_column(arg_expr(args, 0)?);
1535            let n = arg_lit_i64(args, 1)?;
1536            Ok(left(&c, n).into_expr())
1537        }
1538        "right" => {
1539            require_args(name, args, 2)?;
1540            let c = expr_to_column(arg_expr(args, 0)?);
1541            let n = arg_lit_i64(args, 1)?;
1542            Ok(right(&c, n).into_expr())
1543        }
1544        "replace" => {
1545            require_args(name, args, 3)?;
1546            let c = expr_to_column(arg_expr(args, 0)?);
1547            let search = arg_lit_str(args, 1)?;
1548            let replacement = arg_lit_str(args, 2)?;
1549            Ok(replace(&c, &search, &replacement).into_expr())
1550        }
1551        "startswith" => {
1552            require_args(name, args, 2)?;
1553            let c = expr_to_column(arg_expr(args, 0)?);
1554            let prefix = arg_lit_str(args, 1)?;
1555            Ok(startswith(&c, &prefix).into_expr())
1556        }
1557        "endswith" => {
1558            require_args(name, args, 2)?;
1559            let c = expr_to_column(arg_expr(args, 0)?);
1560            let suffix = arg_lit_str(args, 1)?;
1561            Ok(endswith(&c, &suffix).into_expr())
1562        }
1563        "contains" => {
1564            require_args(name, args, 2)?;
1565            let c = expr_to_column(arg_expr(args, 0)?);
1566            let substring = arg_lit_str(args, 1)?;
1567            Ok(contains(&c, &substring).into_expr())
1568        }
1569        "like" => {
1570            require_args_min(name, args, 2)?;
1571            let c = expr_to_column(arg_expr(args, 0)?);
1572            let pattern = arg_lit_str(args, 1)?;
1573            let escape = arg_lit_opt_str(args, 2)?.and_then(|s| s.chars().next());
1574            Ok(like(&c, &pattern, escape).into_expr())
1575        }
1576        "ilike" => {
1577            require_args_min(name, args, 2)?;
1578            let c = expr_to_column(arg_expr(args, 0)?);
1579            let pattern = arg_lit_str(args, 1)?;
1580            let escape = arg_lit_opt_str(args, 2)?.and_then(|s| s.chars().next());
1581            Ok(ilike(&c, &pattern, escape).into_expr())
1582        }
1583        "rlike" | "regexp" => {
1584            require_args(name, args, 2)?;
1585            let c = expr_to_column(arg_expr(args, 0)?);
1586            let pattern = arg_lit_str(args, 1)?;
1587            Ok(rlike(&c, &pattern).into_expr())
1588        }
1589        "soundex" => {
1590            require_args(name, args, 1)?;
1591            Ok(soundex(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1592        }
1593        "levenshtein" => {
1594            require_args(name, args, 2)?;
1595            let a = expr_to_column(arg_expr(args, 0)?);
1596            let b = expr_to_column(arg_expr(args, 1)?);
1597            Ok(crate::functions::levenshtein(&a, &b).into_expr())
1598        }
1599        "crc32" => {
1600            require_args(name, args, 1)?;
1601            Ok(crc32(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1602        }
1603        "xxhash64" => {
1604            require_args(name, args, 1)?;
1605            Ok(xxhash64(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1606        }
1607        "regexp_extract" => {
1608            // Plan execution requires literal pattern and group index (issue #523).
1609            require_args(name, args, 3)?;
1610            let c = expr_to_column(arg_expr(args, 0)?);
1611            let pattern = arg_lit_str(args, 1).map_err(|_| {
1612                PlanExprError(
1613                    "regexp_extract in plan requires literal pattern at arg 2 (column refs not supported)".into(),
1614                )
1615            })?;
1616            let group_index = arg_lit_usize(args, 2).map_err(|_| {
1617                PlanExprError(
1618                    "regexp_extract in plan requires literal group index at arg 3 (column refs not supported)".into(),
1619                )
1620            })?;
1621            Ok(regexp_extract(&c, &pattern, group_index).into_expr())
1622        }
1623        "regexp_replace" => {
1624            require_args(name, args, 3)?;
1625            let c = expr_to_column(arg_expr(args, 0)?);
1626            let pattern = arg_lit_str(args, 1)?;
1627            let replacement = arg_lit_str(args, 2)?;
1628            Ok(regexp_replace(&c, &pattern, &replacement).into_expr())
1629        }
1630        "regexp_extract_all" => {
1631            require_args(name, args, 2)?;
1632            let c = expr_to_column(arg_expr(args, 0)?);
1633            let pattern = arg_lit_str(args, 1)?;
1634            Ok(regexp_extract_all(&c, &pattern).into_expr())
1635        }
1636        "regexp_like" => {
1637            require_args(name, args, 2)?;
1638            let c = expr_to_column(arg_expr(args, 0)?);
1639            let pattern = arg_lit_str(args, 1)?;
1640            Ok(regexp_like(&c, &pattern).into_expr())
1641        }
1642        "regexp_count" => {
1643            require_args(name, args, 2)?;
1644            let c = expr_to_column(arg_expr(args, 0)?);
1645            let pattern = arg_lit_str(args, 1)?;
1646            Ok(regexp_count(&c, &pattern).into_expr())
1647        }
1648        "regexp_substr" => {
1649            require_args(name, args, 2)?;
1650            let c = expr_to_column(arg_expr(args, 0)?);
1651            let pattern = arg_lit_str(args, 1)?;
1652            Ok(regexp_substr(&c, &pattern).into_expr())
1653        }
1654        "regexp_instr" => {
1655            require_args_min(name, args, 2)?;
1656            let c = expr_to_column(arg_expr(args, 0)?);
1657            let pattern = arg_lit_str(args, 1)?;
1658            let group_idx = args.get(2).and_then(|v| lit_as_usize(v).ok());
1659            Ok(regexp_instr(&c, &pattern, group_idx).into_expr())
1660        }
1661        "split" => {
1662            require_args_min(name, args, 2)?;
1663            if args.len() > 3 {
1664                return Err(PlanExprError("split takes at most 3 arguments".to_string()));
1665            }
1666            let c = expr_to_column(arg_expr(args, 0)?);
1667            let delimiter = arg_lit_str(args, 1)?;
1668            let limit = args
1669                .get(2)
1670                .and_then(|v| lit_as_i64(v).ok())
1671                .map(|n| n as i32);
1672            Ok(split(&c, &delimiter, limit).into_expr())
1673        }
1674        "split_part" => {
1675            require_args(name, args, 3)?;
1676            let c = expr_to_column(arg_expr(args, 0)?);
1677            let delimiter = arg_lit_str(args, 1)?;
1678            let part_num = arg_lit_i64(args, 2)?;
1679            Ok(split_part(&c, &delimiter, part_num).into_expr())
1680        }
1681        "find_in_set" => {
1682            require_args(name, args, 2)?;
1683            let str_col = expr_to_column(arg_expr(args, 0)?);
1684            let set_col = expr_to_column(arg_expr(args, 1)?);
1685            Ok(find_in_set(&str_col, &set_col).into_expr())
1686        }
1687        "format_string" | "printf" => {
1688            require_args_min(name, args, 2)?;
1689            let format_str = arg_lit_str(args, 0)?;
1690            let exprs: Result<Vec<Expr>, _> = args.iter().skip(1).map(expr_from_value).collect();
1691            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
1692            let refs: Vec<&Column> = cols.iter().collect();
1693            Ok(format_string(&format_str, &refs).into_expr())
1694        }
1695        "lcase" => {
1696            require_args(name, args, 1)?;
1697            Ok(lcase(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1698        }
1699        "ucase" => {
1700            require_args(name, args, 1)?;
1701            Ok(ucase(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1702        }
1703        "mask" => {
1704            require_args_min(name, args, 1)?;
1705            let c = expr_to_column(arg_expr(args, 0)?);
1706            let u = args
1707                .get(1)
1708                .and_then(|v| lit_as_string(v).ok())
1709                .and_then(|s| s.chars().next());
1710            let l = args
1711                .get(2)
1712                .and_then(|v| lit_as_string(v).ok())
1713                .and_then(|s| s.chars().next());
1714            let d = args
1715                .get(3)
1716                .and_then(|v| lit_as_string(v).ok())
1717                .and_then(|s| s.chars().next());
1718            let o = args
1719                .get(4)
1720                .and_then(|v| lit_as_string(v).ok())
1721                .and_then(|s| s.chars().next());
1722            Ok(mask(&c, u, l, d, o).into_expr())
1723        }
1724        "str_to_map" => {
1725            require_args_min(name, args, 1)?;
1726            let c = expr_to_column(arg_expr(args, 0)?);
1727            let pair_delim: Option<String> = arg_lit_opt_str(args, 1)?;
1728            let key_value_delim: Option<String> = arg_lit_opt_str(args, 2)?;
1729            Ok(str_to_map(&c, pair_delim.as_deref(), key_value_delim.as_deref()).into_expr())
1730        }
1731        "get_json_object" => {
1732            require_args(name, args, 2)?;
1733            let c = expr_to_column(arg_expr(args, 0)?);
1734            let path = arg_lit_str(args, 1)?;
1735            Ok(get_json_object(&c, &path).into_expr())
1736        }
1737        "json_tuple" => {
1738            require_args_min(name, args, 2)?;
1739            let c = expr_to_column(arg_expr(args, 0)?);
1740            let keys: Vec<String> = args[1..]
1741                .iter()
1742                .map(lit_as_string)
1743                .collect::<Result<Vec<_>, _>>()?;
1744            let key_refs: Vec<&str> = keys.iter().map(String::as_str).collect();
1745            Ok(json_tuple(&c, &key_refs).into_expr())
1746        }
1747        "isin" => {
1748            // {"fn": "isin", "args": [col_expr, lit1, lit2, ...]} - col.isin(1, 3)
1749            // Empty list or [col, {"lit": null}] -> lit(false) (issue #518)
1750            // #742, #854: values as string series; cast col to string so string and numeric columns work
1751            require_args_min(name, args, 1)?;
1752            let col_expr = arg_expr(args, 0)?;
1753            let values_opt = try_values_for_isin(&args[1..])?;
1754            Ok(match values_opt {
1755                None => lit(false),
1756                Some(values_expr) => {
1757                    // Use implode() to avoid Polars deprecation: is_in with same-dtype collection (pola-rs/polars#22149)
1758                    col_expr
1759                        .cast(DataType::String)
1760                        .is_in(values_expr.implode(), false)
1761                }
1762            })
1763        }
1764        _ => expr_from_fn_rest(name, args),
1765    }
1766}
1767
1768fn expr_from_fn_rest(name: &str, args: &[Value]) -> Result<Expr, PlanExprError> {
1769    use crate::Column;
1770    #[allow(unused_imports)]
1771    use crate::functions::{
1772        abs, acos, add_months, array, array_agg, array_append, array_compact, array_contains,
1773        array_distinct, array_except, array_insert, array_intersect, array_join, array_max,
1774        array_min, array_prepend, array_remove, array_size, array_slice, array_sort, array_sum,
1775        array_union, arrays_overlap, arrays_zip, asin, atan, atan2, bround, cast, cbrt, ceiling,
1776        cos, cosh, cot, create_map, csc, curdate, current_catalog, current_database, current_date,
1777        current_schema, current_timestamp, current_timezone, current_user, date_add, date_diff,
1778        date_format, date_from_unix_date, date_part, date_sub, date_trunc, dateadd, datediff,
1779        datepart, day, dayname, dayofmonth, dayofweek, dayofyear, days, decode, degrees, e,
1780        element_at, encode, equal_null, exp, explode, explode_outer, expm1, extract, factorial,
1781        floor, from_unixtime, from_utc_timestamp, get, get_json_object, greatest, grouping,
1782        grouping_id, hash, hour, hours, hypot, input_file_name, last_day, least, localtimestamp,
1783        log, log1p, log2, log10, make_date, make_interval, make_timestamp, make_timestamp_ntz,
1784        map_keys, map_values, minute, minutes, monotonically_increasing_id, month, months,
1785        months_between, negate, next_day, now, nullif, nvl, nvl2, parse_url, pi, pmod, positive,
1786        pow, quarter, radians, rint, round, sec, second, shift_left, shift_right, signum, sin,
1787        sinh, size, spark_partition_id, sqrt, tan, tanh, timestamp_micros, timestamp_millis,
1788        timestamp_seconds, timestampadd, timestampdiff, to_binary, to_char, to_date, to_degrees,
1789        to_number, to_radians, to_timestamp, to_timestamp_fused_strip_fraction, to_unix_timestamp,
1790        to_utc_timestamp, to_varchar, trunc, try_add, try_cast, try_divide, try_element_at,
1791        try_multiply, try_subtract, try_to_number, try_to_timestamp, typeof_, unix_date,
1792        unix_micros, unix_millis, unix_seconds, unix_timestamp, unix_timestamp_now, user, weekday,
1793        weekofyear, width_bucket, year, years,
1794    };
1795
1796    // --- Math / numeric ---
1797    match name {
1798        "abs" => {
1799            require_args(name, args, 1)?;
1800            Ok(abs(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1801        }
1802        "ceil" | "ceiling" => {
1803            require_args(name, args, 1)?;
1804            Ok(ceiling(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1805        }
1806        "floor" => {
1807            require_args(name, args, 1)?;
1808            Ok(floor(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1809        }
1810        "round" => {
1811            require_args_min(name, args, 1)?;
1812            let c = expr_to_column(arg_expr(args, 0)?);
1813            let scale = opt_lit_i64(args, 1).unwrap_or(0) as i32;
1814            Ok(round(&c, scale).into_expr())
1815        }
1816        "bround" => {
1817            require_args_min(name, args, 1)?;
1818            let c = expr_to_column(arg_expr(args, 0)?);
1819            let scale = opt_lit_i64(args, 1).unwrap_or(0) as i32;
1820            Ok(bround(&c, scale).into_expr())
1821        }
1822        "negate" | "negative" => {
1823            require_args(name, args, 1)?;
1824            Ok(negate(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1825        }
1826        "positive" => {
1827            require_args(name, args, 1)?;
1828            Ok(positive(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1829        }
1830        "sqrt" => {
1831            require_args(name, args, 1)?;
1832            Ok(sqrt(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1833        }
1834        "pow" | "power" => {
1835            require_args(name, args, 2)?;
1836            let c = expr_to_column(arg_expr(args, 0)?);
1837            let exp_val = arg_lit_i64(args, 1)?;
1838            Ok(pow(&c, exp_val).into_expr())
1839        }
1840        "exp" => {
1841            require_args(name, args, 1)?;
1842            Ok(exp(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1843        }
1844        "log" | "ln" => {
1845            if args.len() == 1 {
1846                Ok(log(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1847            } else if args.len() == 2 {
1848                let col_expr = expr_to_column(arg_expr(args, 0)?);
1849                let base = match &args[1] {
1850                    Value::Number(n) => n
1851                        .as_f64()
1852                        .ok_or_else(|| PlanExprError("log base must be a number".to_string()))?,
1853                    _ => return Err(PlanExprError("log base must be a number".to_string())),
1854                };
1855                Ok(crate::functions::log_with_base(&col_expr, base).into_expr())
1856            } else {
1857                Err(PlanExprError(format!(
1858                    "fn '{name}' requires 1 or 2 arguments"
1859                )))
1860            }
1861        }
1862        "sin" => {
1863            require_args(name, args, 1)?;
1864            Ok(sin(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1865        }
1866        "cos" => {
1867            require_args(name, args, 1)?;
1868            Ok(cos(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1869        }
1870        "tan" => {
1871            require_args(name, args, 1)?;
1872            Ok(tan(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1873        }
1874        "asin" => {
1875            require_args(name, args, 1)?;
1876            Ok(crate::functions::asin(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1877        }
1878        "acos" => {
1879            require_args(name, args, 1)?;
1880            Ok(crate::functions::acos(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1881        }
1882        "atan" => {
1883            require_args(name, args, 1)?;
1884            Ok(crate::functions::atan(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1885        }
1886        "atan2" => {
1887            require_args(name, args, 2)?;
1888            let y = expr_to_column(arg_expr(args, 0)?);
1889            let x = expr_to_column(arg_expr(args, 1)?);
1890            Ok(atan2(&y, &x).into_expr())
1891        }
1892        "degrees" => {
1893            require_args(name, args, 1)?;
1894            Ok(degrees(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1895        }
1896        "radians" => {
1897            require_args(name, args, 1)?;
1898            Ok(radians(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1899        }
1900        "signum" | "sign" => {
1901            require_args(name, args, 1)?;
1902            Ok(signum(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1903        }
1904        "cot" => {
1905            require_args(name, args, 1)?;
1906            Ok(cot(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1907        }
1908        "csc" => {
1909            require_args(name, args, 1)?;
1910            Ok(csc(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1911        }
1912        "sec" => {
1913            require_args(name, args, 1)?;
1914            Ok(sec(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1915        }
1916        "e" => {
1917            if !args.is_empty() {
1918                return Err(PlanExprError("fn 'e' takes no arguments".to_string()));
1919            }
1920            Ok(e().into_expr())
1921        }
1922        "pi" => {
1923            if !args.is_empty() {
1924                return Err(PlanExprError("fn 'pi' takes no arguments".to_string()));
1925            }
1926            Ok(pi().into_expr())
1927        }
1928        "pmod" => {
1929            require_args(name, args, 2)?;
1930            let a = expr_to_column(arg_expr(args, 0)?);
1931            let b = expr_to_column(arg_expr(args, 1)?);
1932            Ok(pmod(&a, &b).into_expr())
1933        }
1934        "factorial" => {
1935            require_args(name, args, 1)?;
1936            Ok(factorial(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1937        }
1938        "hypot" => {
1939            require_args(name, args, 2)?;
1940            let x = expr_to_column(arg_expr(args, 0)?);
1941            let y = expr_to_column(arg_expr(args, 1)?);
1942            Ok(hypot(&x, &y).into_expr())
1943        }
1944        "cosh" => {
1945            require_args(name, args, 1)?;
1946            Ok(cosh(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1947        }
1948        "sinh" => {
1949            require_args(name, args, 1)?;
1950            Ok(sinh(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1951        }
1952        "tanh" => {
1953            require_args(name, args, 1)?;
1954            Ok(tanh(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1955        }
1956        "cbrt" => {
1957            require_args(name, args, 1)?;
1958            Ok(cbrt(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1959        }
1960        "expm1" => {
1961            require_args(name, args, 1)?;
1962            Ok(crate::functions::expm1(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1963        }
1964        "log1p" => {
1965            require_args(name, args, 1)?;
1966            Ok(log1p(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1967        }
1968        "log10" => {
1969            require_args(name, args, 1)?;
1970            Ok(log10(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1971        }
1972        "log2" => {
1973            require_args(name, args, 1)?;
1974            Ok(log2(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1975        }
1976        "rint" => {
1977            require_args(name, args, 1)?;
1978            Ok(crate::functions::rint(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1979        }
1980        "to_degrees" => {
1981            require_args(name, args, 1)?;
1982            Ok(to_degrees(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1983        }
1984        "to_radians" => {
1985            require_args(name, args, 1)?;
1986            Ok(to_radians(&expr_to_column(arg_expr(args, 0)?)).into_expr())
1987        }
1988        // --- Type / conditional ---
1989        "cast" => {
1990            require_args(name, args, 2)?;
1991            let c = expr_to_column(arg_expr(args, 0)?);
1992            let type_name = arg_lit_str(args, 1)?;
1993            Ok(cast(&c, &type_name).map_err(PlanExprError)?.into_expr())
1994        }
1995        "try_cast" => {
1996            require_args(name, args, 2)?;
1997            let c = expr_to_column(arg_expr(args, 0)?);
1998            let type_name = arg_lit_str(args, 1)?;
1999            Ok(try_cast(&c, &type_name).map_err(PlanExprError)?.into_expr())
2000        }
2001        // #753: astype with null-on-invalid (PySpark astype returns null for invalid conversions).
2002        "astype" => {
2003            require_args(name, args, 2)?;
2004            let c = expr_to_column(arg_expr(args, 0)?);
2005            let type_name = arg_lit_str(args, 1)?;
2006            Ok(try_cast(&c, &type_name).map_err(PlanExprError)?.into_expr())
2007        }
2008        "nvl" | "ifnull" => {
2009            require_args(name, args, 2)?;
2010            let a = expr_to_column(arg_expr(args, 0)?);
2011            let b = expr_to_column(arg_expr(args, 1)?);
2012            Ok(nvl(&a, &b).into_expr())
2013        }
2014        "nullif" => {
2015            require_args(name, args, 2)?;
2016            let a = expr_to_column(arg_expr(args, 0)?);
2017            let b = expr_to_column(arg_expr(args, 1)?);
2018            Ok(nullif(&a, &b).into_expr())
2019        }
2020        "greatest" => {
2021            require_args_min(name, args, 1)?;
2022            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
2023            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
2024            let refs: Vec<&Column> = cols.iter().collect();
2025            Ok(greatest(&refs).map_err(PlanExprError)?.into_expr())
2026        }
2027        "least" => {
2028            require_args_min(name, args, 1)?;
2029            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
2030            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
2031            let refs: Vec<&Column> = cols.iter().collect();
2032            Ok(least(&refs).map_err(PlanExprError)?.into_expr())
2033        }
2034        "typeof" => {
2035            require_args(name, args, 1)?;
2036            Ok(typeof_(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2037        }
2038        "try_divide" => {
2039            require_args(name, args, 2)?;
2040            let a = expr_to_column(arg_expr(args, 0)?);
2041            let b = expr_to_column(arg_expr(args, 1)?);
2042            Ok(try_divide(&a, &b).into_expr())
2043        }
2044        // PySpark-style arithmetic with string/numeric coercion (issue #201)
2045        "add" | "+" => {
2046            require_args(name, args, 2)?;
2047            let a = expr_to_column(arg_expr(args, 0)?);
2048            let b = expr_to_column(arg_expr(args, 1)?);
2049            Ok(a.add_pyspark(&b).into_expr())
2050        }
2051        "subtract" | "-" => {
2052            require_args(name, args, 2)?;
2053            let a = expr_to_column(arg_expr(args, 0)?);
2054            let b = expr_to_column(arg_expr(args, 1)?);
2055            Ok(a.subtract_pyspark(&b).into_expr())
2056        }
2057        "multiply" | "*" => {
2058            require_args(name, args, 2)?;
2059            let a = expr_to_column(arg_expr(args, 0)?);
2060            let b = expr_to_column(arg_expr(args, 1)?);
2061            Ok(a.multiply_pyspark(&b).into_expr())
2062        }
2063        "divide" | "/" => {
2064            require_args(name, args, 2)?;
2065            let a = expr_to_column(arg_expr(args, 0)?);
2066            let b = expr_to_column(arg_expr(args, 1)?);
2067            Ok(a.divide_pyspark(&b).into_expr())
2068        }
2069        "mod" | "remainder" | "%" => {
2070            require_args(name, args, 2)?;
2071            let a = expr_to_column(arg_expr(args, 0)?);
2072            let b = expr_to_column(arg_expr(args, 1)?);
2073            Ok(a.mod_pyspark(&b).into_expr())
2074        }
2075        "try_add" => {
2076            require_args(name, args, 2)?;
2077            let a = expr_to_column(arg_expr(args, 0)?);
2078            let b = expr_to_column(arg_expr(args, 1)?);
2079            Ok(try_add(&a, &b).into_expr())
2080        }
2081        "try_subtract" => {
2082            require_args(name, args, 2)?;
2083            let a = expr_to_column(arg_expr(args, 0)?);
2084            let b = expr_to_column(arg_expr(args, 1)?);
2085            Ok(try_subtract(&a, &b).into_expr())
2086        }
2087        "try_multiply" => {
2088            require_args(name, args, 2)?;
2089            let a = expr_to_column(arg_expr(args, 0)?);
2090            let b = expr_to_column(arg_expr(args, 1)?);
2091            Ok(try_multiply(&a, &b).into_expr())
2092        }
2093        "width_bucket" => {
2094            require_args(name, args, 4)?;
2095            let val = expr_to_column(arg_expr(args, 0)?);
2096            let min_val = arg_lit_f64(args, 1)?;
2097            let max_val = arg_lit_f64(args, 2)?;
2098            let num_bucket = arg_lit_i64(args, 3)?;
2099            if num_bucket <= 0 {
2100                return Err(PlanExprError(
2101                    "width_bucket: num_bucket must be positive".into(),
2102                ));
2103            }
2104            Ok(width_bucket(&val, min_val, max_val, num_bucket).into_expr())
2105        }
2106        "equal_null" => {
2107            require_args(name, args, 2)?;
2108            let a = expr_to_column(arg_expr(args, 0)?);
2109            let b = expr_to_column(arg_expr(args, 1)?);
2110            Ok(equal_null(&a, &b).into_expr())
2111        }
2112        // --- Datetime ---
2113        "year" => {
2114            require_args(name, args, 1)?;
2115            Ok(year(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2116        }
2117        "month" => {
2118            require_args(name, args, 1)?;
2119            Ok(month(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2120        }
2121        "day" | "dayofmonth" => {
2122            require_args(name, args, 1)?;
2123            Ok(day(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2124        }
2125        "hour" => {
2126            require_args(name, args, 1)?;
2127            Ok(hour(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2128        }
2129        "minute" => {
2130            require_args(name, args, 1)?;
2131            Ok(minute(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2132        }
2133        "second" => {
2134            require_args(name, args, 1)?;
2135            Ok(second(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2136        }
2137        "quarter" => {
2138            require_args(name, args, 1)?;
2139            Ok(quarter(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2140        }
2141        "weekofyear" => {
2142            require_args(name, args, 1)?;
2143            Ok(weekofyear(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2144        }
2145        "dayofweek" => {
2146            require_args(name, args, 1)?;
2147            Ok(dayofweek(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2148        }
2149        "dayofyear" => {
2150            require_args(name, args, 1)?;
2151            Ok(dayofyear(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2152        }
2153        "to_date" => {
2154            require_args_min(name, args, 1)?;
2155            if args.len() > 2 {
2156                return Err(PlanExprError(format!(
2157                    "fn '{name}' takes at most 2 argument(s)"
2158                )));
2159            }
2160            let col = expr_to_column(arg_expr(args, 0)?);
2161            let format_str = if args.len() == 2 {
2162                Some(arg_lit_str(args, 1)?)
2163            } else {
2164                None
2165            };
2166            to_date(&col, format_str.as_deref())
2167                .map_err(PlanExprError)
2168                .map(|c| c.into_expr())
2169        }
2170        "date_format" => {
2171            require_args(name, args, 2)?;
2172            let c = expr_to_column(arg_expr(args, 0)?);
2173            let format = arg_lit_str(args, 1)?;
2174            Ok(date_format(&c, &format).into_expr())
2175        }
2176        "date_add" => {
2177            require_args(name, args, 2)?;
2178            let c = expr_to_column(arg_expr(args, 0)?);
2179            let n = arg_lit_i32(args, 1)?;
2180            Ok(date_add(&c, n).into_expr())
2181        }
2182        "date_sub" => {
2183            require_args(name, args, 2)?;
2184            let c = expr_to_column(arg_expr(args, 0)?);
2185            let n = arg_lit_i32(args, 1)?;
2186            Ok(date_sub(&c, n).into_expr())
2187        }
2188        "datediff" | "date_diff" => {
2189            require_args(name, args, 2)?;
2190            let end = expr_to_column(arg_expr(args, 0)?);
2191            let start = expr_to_column(arg_expr(args, 1)?);
2192            Ok(datediff(&end, &start).into_expr())
2193        }
2194        "last_day" => {
2195            require_args(name, args, 1)?;
2196            Ok(last_day(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2197        }
2198        "trunc" => {
2199            require_args(name, args, 2)?;
2200            let c = expr_to_column(arg_expr(args, 0)?);
2201            let format = arg_lit_str(args, 1)?;
2202            Ok(trunc(&c, &format).into_expr())
2203        }
2204        "date_trunc" => {
2205            require_args(name, args, 2)?;
2206            let format = arg_lit_str(args, 0)?;
2207            let c = expr_to_column(arg_expr(args, 1)?);
2208            Ok(date_trunc(&format, &c).into_expr())
2209        }
2210        "add_months" => {
2211            require_args(name, args, 2)?;
2212            let c = expr_to_column(arg_expr(args, 0)?);
2213            let n = arg_lit_i32(args, 1)?;
2214            Ok(add_months(&c, n).into_expr())
2215        }
2216        "months_between" => {
2217            require_args_min(name, args, 2)?;
2218            let end = expr_to_column(arg_expr(args, 0)?);
2219            let start = expr_to_column(arg_expr(args, 1)?);
2220            let round_off = args
2221                .get(2)
2222                .and_then(|v| v.get("lit").and_then(Value::as_bool))
2223                .unwrap_or(true);
2224            Ok(months_between(&end, &start, round_off).into_expr())
2225        }
2226        "next_day" => {
2227            require_args(name, args, 2)?;
2228            let c = expr_to_column(arg_expr(args, 0)?);
2229            let day_of_week = arg_lit_str(args, 1)?;
2230            Ok(next_day(&c, &day_of_week).into_expr())
2231        }
2232        "unix_timestamp" => {
2233            if args.is_empty() {
2234                Ok(unix_timestamp_now().into_expr())
2235            } else {
2236                require_args_min(name, args, 1)?;
2237                let c = expr_to_column(arg_expr(args, 0)?);
2238                let format: Option<String> = arg_lit_opt_str(args, 1)?;
2239                Ok(unix_timestamp(&c, format.as_deref()).into_expr())
2240            }
2241        }
2242        "from_unixtime" => {
2243            require_args_min(name, args, 1)?;
2244            let c = expr_to_column(arg_expr(args, 0)?);
2245            let format: Option<String> = arg_lit_opt_str(args, 1)?;
2246            Ok(from_unixtime(&c, format.as_deref()).into_expr())
2247        }
2248        "to_unix_timestamp" => {
2249            require_args_min(name, args, 1)?;
2250            let c = expr_to_column(arg_expr(args, 0)?);
2251            let format: Option<String> = arg_lit_opt_str(args, 1)?;
2252            Ok(to_unix_timestamp(&c, format.as_deref()).into_expr())
2253        }
2254        "make_date" => {
2255            require_args(name, args, 3)?;
2256            let y = expr_to_column(arg_expr(args, 0)?);
2257            let m = expr_to_column(arg_expr(args, 1)?);
2258            let d = expr_to_column(arg_expr(args, 2)?);
2259            Ok(make_date(&y, &m, &d).into_expr())
2260        }
2261        "make_timestamp" => {
2262            require_args_min(name, args, 6)?;
2263            let y = expr_to_column(arg_expr(args, 0)?);
2264            let mo = expr_to_column(arg_expr(args, 1)?);
2265            let d = expr_to_column(arg_expr(args, 2)?);
2266            let h = expr_to_column(arg_expr(args, 3)?);
2267            let mi = expr_to_column(arg_expr(args, 4)?);
2268            let s = expr_to_column(arg_expr(args, 5)?);
2269            let tz: Option<String> = arg_lit_opt_str(args, 6)?;
2270            Ok(make_timestamp(&y, &mo, &d, &h, &mi, &s, tz.as_deref()).into_expr())
2271        }
2272        "make_timestamp_ntz" => {
2273            require_args(name, args, 6)?;
2274            let y = expr_to_column(arg_expr(args, 0)?);
2275            let mo = expr_to_column(arg_expr(args, 1)?);
2276            let d = expr_to_column(arg_expr(args, 2)?);
2277            let h = expr_to_column(arg_expr(args, 3)?);
2278            let mi = expr_to_column(arg_expr(args, 4)?);
2279            let s = expr_to_column(arg_expr(args, 5)?);
2280            Ok(make_timestamp_ntz(&y, &mo, &d, &h, &mi, &s).into_expr())
2281        }
2282        "timestampadd" => {
2283            require_args(name, args, 3)?;
2284            let unit = arg_lit_str(args, 0)?;
2285            let amount = expr_to_column(arg_expr(args, 1)?);
2286            let ts = expr_to_column(arg_expr(args, 2)?);
2287            Ok(timestampadd(&unit, &amount, &ts).into_expr())
2288        }
2289        "timestampdiff" => {
2290            require_args(name, args, 3)?;
2291            let unit = arg_lit_str(args, 0)?;
2292            let start = expr_to_column(arg_expr(args, 1)?);
2293            let end = expr_to_column(arg_expr(args, 2)?);
2294            Ok(timestampdiff(&unit, &start, &end).into_expr())
2295        }
2296        "days" => {
2297            require_args(name, args, 1)?;
2298            let n = arg_lit_i64(args, 0)?;
2299            Ok(days(n).into_expr())
2300        }
2301        "hours" => {
2302            require_args(name, args, 1)?;
2303            let n = arg_lit_i64(args, 0)?;
2304            Ok(hours(n).into_expr())
2305        }
2306        "minutes" => {
2307            require_args(name, args, 1)?;
2308            let n = arg_lit_i64(args, 0)?;
2309            Ok(minutes(n).into_expr())
2310        }
2311        "months" => {
2312            require_args(name, args, 1)?;
2313            let n = arg_lit_i64(args, 0)?;
2314            Ok(months(n).into_expr())
2315        }
2316        "years" => {
2317            require_args(name, args, 1)?;
2318            let n = arg_lit_i64(args, 0)?;
2319            Ok(years(n).into_expr())
2320        }
2321        "from_utc_timestamp" => {
2322            require_args(name, args, 2)?;
2323            let c = expr_to_column(arg_expr(args, 0)?);
2324            let tz = arg_lit_str(args, 1)?;
2325            Ok(from_utc_timestamp(&c, &tz).into_expr())
2326        }
2327        "to_utc_timestamp" => {
2328            require_args(name, args, 2)?;
2329            let c = expr_to_column(arg_expr(args, 0)?);
2330            let tz = arg_lit_str(args, 1)?;
2331            Ok(to_utc_timestamp(&c, &tz).into_expr())
2332        }
2333        "convert_timezone" => {
2334            require_args(name, args, 3)?;
2335            let source_tz = arg_lit_str(args, 0)?;
2336            let target_tz = arg_lit_str(args, 1)?;
2337            let c = expr_to_column(arg_expr(args, 2)?);
2338            Ok(crate::functions::convert_timezone(&source_tz, &target_tz, &c).into_expr())
2339        }
2340        "current_date" | "curdate" => {
2341            if !args.is_empty() {
2342                return Err(PlanExprError(format!("fn '{name}' takes no arguments")));
2343            }
2344            Ok(current_date().into_expr())
2345        }
2346        "current_timestamp" | "now" => {
2347            if !args.is_empty() {
2348                return Err(PlanExprError(format!("fn '{name}' takes no arguments")));
2349            }
2350            Ok(current_timestamp().into_expr())
2351        }
2352        "localtimestamp" => {
2353            if !args.is_empty() {
2354                return Err(PlanExprError(
2355                    "fn 'localtimestamp' takes no arguments".to_string(),
2356                ));
2357            }
2358            Ok(localtimestamp().into_expr())
2359        }
2360        "extract" | "date_part" | "datepart" => {
2361            require_args(name, args, 2)?;
2362            let c = expr_to_column(arg_expr(args, 0)?);
2363            let field = arg_lit_str(args, 1)?;
2364            Ok(extract(&c, &field).into_expr())
2365        }
2366        "dateadd" => {
2367            require_args(name, args, 2)?;
2368            let c = expr_to_column(arg_expr(args, 0)?);
2369            let n = arg_lit_i32(args, 1)?;
2370            Ok(dateadd(&c, n).into_expr())
2371        }
2372        "unix_micros" | "unix_millis" | "unix_seconds" => {
2373            require_args(name, args, 1)?;
2374            let c = expr_to_column(arg_expr(args, 0)?);
2375            let out = match name {
2376                "unix_micros" => unix_micros(&c),
2377                "unix_millis" => unix_millis(&c),
2378                _ => unix_seconds(&c),
2379            };
2380            Ok(out.into_expr())
2381        }
2382        "dayname" => {
2383            require_args(name, args, 1)?;
2384            Ok(dayname(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2385        }
2386        "weekday" => {
2387            require_args(name, args, 1)?;
2388            Ok(weekday(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2389        }
2390        "timestamp_seconds" | "timestamp_millis" | "timestamp_micros" => {
2391            require_args(name, args, 1)?;
2392            let c = expr_to_column(arg_expr(args, 0)?);
2393            let out = match name {
2394                "timestamp_seconds" => timestamp_seconds(&c),
2395                "timestamp_millis" => timestamp_millis(&c),
2396                _ => timestamp_micros(&c),
2397            };
2398            Ok(out.into_expr())
2399        }
2400        "unix_date" => {
2401            require_args(name, args, 1)?;
2402            Ok(unix_date(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2403        }
2404        "date_from_unix_date" => {
2405            require_args(name, args, 1)?;
2406            Ok(date_from_unix_date(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2407        }
2408        "to_char" | "to_varchar" => {
2409            require_args_min(name, args, 1)?;
2410            let c = expr_to_column(arg_expr(args, 0)?);
2411            let format: Option<String> = arg_lit_opt_str(args, 1)?;
2412            Ok(to_char(&c, format.as_deref())
2413                .map_err(PlanExprError)?
2414                .into_expr())
2415        }
2416        "to_timestamp" => {
2417            require_args_min(name, args, 1)?;
2418            let format: Option<String> = arg_lit_opt_str(args, 1)?;
2419            // PySpark parity #168/#153: fused path when to_timestamp(cast(regexp_replace(col, r"\.\d+", ""), string), "yyyy-MM-dd'T'HH:mm:ss").
2420            // Map source column with UDF that strips fraction, parses, and returns null for "recent" timestamps (data-driven, no allowlist).
2421            let col_expr = if format.as_deref() == Some("yyyy-MM-dd'T'HH:mm:ss") {
2422                match try_extract_regexp_replace_strip_fraction_source(&args[0])? {
2423                    Some(source_col) => {
2424                        return Ok(to_timestamp_fused_strip_fraction(
2425                            &source_col,
2426                            "yyyy-MM-dd'T'HH:mm:ss",
2427                        )
2428                        .map_err(PlanExprError)?
2429                        .into_expr());
2430                    }
2431                    None => expr_to_column(arg_expr(args, 0)?),
2432                }
2433            } else {
2434                expr_to_column(arg_expr(args, 0)?)
2435            };
2436            Ok(to_timestamp(&col_expr, format.as_deref())
2437                .map_err(PlanExprError)?
2438                .into_expr())
2439        }
2440        "try_to_timestamp" => {
2441            require_args_min(name, args, 1)?;
2442            let c = expr_to_column(arg_expr(args, 0)?);
2443            let format: Option<String> = arg_lit_opt_str(args, 1)?;
2444            Ok(try_to_timestamp(&c, format.as_deref())
2445                .map_err(PlanExprError)?
2446                .into_expr())
2447        }
2448        "to_number" | "try_to_number" => {
2449            require_args_min(name, args, 1)?;
2450            let c = expr_to_column(arg_expr(args, 0)?);
2451            let format: Option<String> = arg_lit_opt_str(args, 1)?;
2452            let out = if name == "to_number" {
2453                to_number(&c, format.as_deref()).map_err(PlanExprError)?
2454            } else {
2455                try_to_number(&c, format.as_deref()).map_err(PlanExprError)?
2456            };
2457            Ok(out.into_expr())
2458        }
2459        "current_timezone" => {
2460            if !args.is_empty() {
2461                return Err(PlanExprError(
2462                    "fn 'current_timezone' takes no arguments".to_string(),
2463                ));
2464            }
2465            Ok(current_timezone().into_expr())
2466        }
2467        // --- Zero-arg JVM/runtime stubs ---
2468        "spark_partition_id"
2469        | "input_file_name"
2470        | "monotonically_increasing_id"
2471        | "current_catalog"
2472        | "current_database"
2473        | "current_schema"
2474        | "current_user"
2475        | "user" => {
2476            if !args.is_empty() {
2477                return Err(PlanExprError(format!("fn '{name}' takes no arguments")));
2478            }
2479            let out = match name {
2480                "spark_partition_id" => spark_partition_id(),
2481                "input_file_name" => input_file_name(),
2482                "monotonically_increasing_id" => monotonically_increasing_id(),
2483                "current_catalog" => current_catalog(),
2484                "current_database" => current_database(),
2485                "current_schema" => current_schema(),
2486                "current_user" => current_user(),
2487                "user" => user(),
2488                _ => current_catalog(), // unreachable
2489            };
2490            Ok(out.into_expr())
2491        }
2492        "hash" => {
2493            require_args_min(name, args, 1)?;
2494            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
2495            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
2496            let refs: Vec<&Column> = cols.iter().collect();
2497            Ok(crate::functions::hash(&refs).into_expr())
2498        }
2499        "shift_left" => {
2500            require_args(name, args, 2)?;
2501            let c = expr_to_column(arg_expr(args, 0)?);
2502            let n = arg_lit_i32(args, 1)?;
2503            Ok(shift_left(&c, n).into_expr())
2504        }
2505        "shift_right" => {
2506            require_args(name, args, 2)?;
2507            let c = expr_to_column(arg_expr(args, 0)?);
2508            let n = arg_lit_i32(args, 1)?;
2509            Ok(shift_right(&c, n).into_expr())
2510        }
2511        "version" => {
2512            if !args.is_empty() {
2513                return Err(PlanExprError("fn 'version' takes no arguments".to_string()));
2514            }
2515            Ok(crate::functions::version().into_expr())
2516        }
2517        // --- Array / list ---
2518        "array" => {
2519            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
2520            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
2521            let refs: Vec<&Column> = cols.iter().collect();
2522            Ok(array(&refs)
2523                .map_err(|e| PlanExprError(e.to_string()))?
2524                .into_expr())
2525        }
2526        "array_max" => {
2527            require_args(name, args, 1)?;
2528            Ok(crate::functions::array_max(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2529        }
2530        "array_min" => {
2531            require_args(name, args, 1)?;
2532            Ok(crate::functions::array_min(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2533        }
2534        "array_size" | "size" | "cardinality" => {
2535            require_args(name, args, 1)?;
2536            let c = expr_to_column(arg_expr(args, 0)?);
2537            Ok(array_size(&c).into_expr())
2538        }
2539        "element_at" => {
2540            require_args(name, args, 2)?;
2541            let c = expr_to_column(arg_expr(args, 0)?);
2542            let idx = arg_lit_i64(args, 1)?;
2543            Ok(element_at(&c, idx).into_expr())
2544        }
2545        "try_element_at" => {
2546            require_args(name, args, 2)?;
2547            let c = expr_to_column(arg_expr(args, 0)?);
2548            let idx = arg_lit_i64(args, 1)?;
2549            Ok(try_element_at(&c, idx).into_expr())
2550        }
2551        "array_contains" => {
2552            require_args(name, args, 2)?;
2553            let arr = expr_to_column(arg_expr(args, 0)?);
2554            let val = expr_to_column(arg_expr(args, 1)?);
2555            Ok(array_contains(&arr, &val).into_expr())
2556        }
2557        "array_join" => {
2558            require_args(name, args, 2)?;
2559            let c = expr_to_column(arg_expr(args, 0)?);
2560            let sep = arg_lit_str(args, 1)?;
2561            Ok(array_join(&c, &sep).into_expr())
2562        }
2563        "array_sort" => {
2564            require_args(name, args, 1)?;
2565            Ok(array_sort(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2566        }
2567        "array_distinct" => {
2568            require_args(name, args, 1)?;
2569            Ok(array_distinct(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2570        }
2571        "array_slice" => {
2572            require_args_min(name, args, 2)?;
2573            let c = expr_to_column(arg_expr(args, 0)?);
2574            let start = arg_lit_i64(args, 1)?;
2575            let length = opt_lit_i64(args, 2);
2576            Ok(array_slice(&c, start, length).into_expr())
2577        }
2578        "array_compact" => {
2579            require_args(name, args, 1)?;
2580            Ok(array_compact(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2581        }
2582        "array_remove" => {
2583            require_args(name, args, 2)?;
2584            let arr = expr_to_column(arg_expr(args, 0)?);
2585            let val = expr_to_column(arg_expr(args, 1)?);
2586            Ok(array_remove(&arr, &val).into_expr())
2587        }
2588        "explode" => {
2589            require_args(name, args, 1)?;
2590            Ok(explode(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2591        }
2592        "explode_outer" => {
2593            require_args(name, args, 1)?;
2594            Ok(explode_outer(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2595        }
2596        "inline" => {
2597            require_args(name, args, 1)?;
2598            Ok(crate::functions::inline(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2599        }
2600        "inline_outer" => {
2601            require_args(name, args, 1)?;
2602            Ok(crate::functions::inline_outer(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2603        }
2604        "sequence" => {
2605            require_args_min(name, args, 2)?;
2606            let start = expr_to_column(arg_expr(args, 0)?);
2607            let stop = expr_to_column(arg_expr(args, 1)?);
2608            let step = if args.len() > 2 {
2609                Some(expr_to_column(arg_expr(args, 2)?))
2610            } else {
2611                None
2612            };
2613            Ok(crate::functions::sequence(&start, &stop, step.as_ref()).into_expr())
2614        }
2615        "shuffle" => {
2616            require_args(name, args, 1)?;
2617            Ok(crate::functions::shuffle(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2618        }
2619        "array_position" => {
2620            require_args(name, args, 2)?;
2621            let arr = expr_to_column(arg_expr(args, 0)?);
2622            let val = expr_to_column(arg_expr(args, 1)?);
2623            Ok(crate::functions::array_position(&arr, &val).into_expr())
2624        }
2625        "array_append" => {
2626            require_args(name, args, 2)?;
2627            let arr = expr_to_column(arg_expr(args, 0)?);
2628            let elem = expr_to_column(arg_expr(args, 1)?);
2629            Ok(array_append(&arr, &elem).into_expr())
2630        }
2631        "array_prepend" => {
2632            require_args(name, args, 2)?;
2633            let arr = expr_to_column(arg_expr(args, 0)?);
2634            let elem = expr_to_column(arg_expr(args, 1)?);
2635            Ok(array_prepend(&arr, &elem).into_expr())
2636        }
2637        "array_insert" => {
2638            require_args(name, args, 3)?;
2639            let arr = expr_to_column(arg_expr(args, 0)?);
2640            let pos = expr_to_column(arg_expr(args, 1)?);
2641            let elem = expr_to_column(arg_expr(args, 2)?);
2642            Ok(array_insert(&arr, &pos, &elem).into_expr())
2643        }
2644        "array_except" => {
2645            require_args(name, args, 2)?;
2646            let a = expr_to_column(arg_expr(args, 0)?);
2647            let b = expr_to_column(arg_expr(args, 1)?);
2648            Ok(array_except(&a, &b).into_expr())
2649        }
2650        "array_intersect" => {
2651            require_args(name, args, 2)?;
2652            let a = expr_to_column(arg_expr(args, 0)?);
2653            let b = expr_to_column(arg_expr(args, 1)?);
2654            Ok(array_intersect(&a, &b).into_expr())
2655        }
2656        "array_union" => {
2657            require_args(name, args, 2)?;
2658            let a = expr_to_column(arg_expr(args, 0)?);
2659            let b = expr_to_column(arg_expr(args, 1)?);
2660            Ok(array_union(&a, &b).into_expr())
2661        }
2662        "arrays_overlap" => {
2663            require_args(name, args, 2)?;
2664            let a = expr_to_column(arg_expr(args, 0)?);
2665            let b = expr_to_column(arg_expr(args, 1)?);
2666            Ok(arrays_overlap(&a, &b).into_expr())
2667        }
2668        "arrays_zip" => {
2669            require_args(name, args, 2)?;
2670            let a = expr_to_column(arg_expr(args, 0)?);
2671            let b = expr_to_column(arg_expr(args, 1)?);
2672            Ok(arrays_zip(&a, &b).into_expr())
2673        }
2674        "array_agg" => {
2675            require_args(name, args, 1)?;
2676            Ok(array_agg(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2677        }
2678        "array_sum" => {
2679            require_args(name, args, 1)?;
2680            Ok(array_sum(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2681        }
2682        // --- Map / struct ---
2683        "create_map" | "createMap" => {
2684            // PySpark F.create_map(key1, val1, ...) or empty map {} per row (#512, #542).
2685            let exprs: Result<Vec<Expr>, _> = args.iter().map(expr_from_value).collect();
2686            let cols: Vec<Column> = exprs?.into_iter().map(expr_to_column).collect();
2687            let refs: Vec<&Column> = cols.iter().collect();
2688            Ok(create_map(&refs)
2689                .map_err(|e| PlanExprError(e.to_string()))?
2690                .into_expr())
2691        }
2692        "map_keys" | "keys" => {
2693            require_args(name, args, 1)?;
2694            Ok(map_keys(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2695        }
2696        "map_values" => {
2697            require_args(name, args, 1)?;
2698            Ok(map_values(&expr_to_column(arg_expr(args, 0)?)).into_expr())
2699        }
2700        "get" => {
2701            require_args(name, args, 2)?;
2702            let map_col = expr_to_column(arg_expr(args, 0)?);
2703            let key = expr_to_column(arg_expr(args, 1)?);
2704            Ok(get(&map_col, &key).into_expr())
2705        }
2706        "get_field" | "getField" => {
2707            // Struct field by name (PySpark Column.getField). Used e.g. to assert withField result (issue #541).
2708            require_args(name, args, 2)?;
2709            let struct_col = expr_to_column(arg_expr(args, 0)?);
2710            let field_name = lit_as_string(&args[1])?;
2711            Ok(struct_col.get_field(&field_name).into_expr())
2712        }
2713        "get_item" => {
2714            // Array: get_item(col, 0); map: get_item(col, "key") (issue #522)
2715            require_args(name, args, 2)?;
2716            let col_c = expr_to_column(arg_expr(args, 0)?);
2717            let second = &args[1];
2718            if let Some(idx) = second.get("lit").and_then(|v| v.as_i64()) {
2719                Ok(col_c.get_item(idx).into_expr())
2720            } else {
2721                let key = expr_to_column(arg_expr(args, 1)?);
2722                Ok(get(&col_c, &key).into_expr())
2723            }
2724        }
2725        "struct" => {
2726            // struct(col1, col2, ...) (issue #527)
2727            require_args_min(name, args, 1)?;
2728            let cols: Vec<crate::Column> = (0..args.len())
2729                .map(|i| arg_expr(args, i).map(expr_to_column))
2730                .collect::<Result<Vec<_>, _>>()?;
2731            let refs: Vec<&crate::Column> = cols.iter().collect();
2732            Ok(crate::functions::struct_(&refs).into_expr())
2733        }
2734        "named_struct" => {
2735            // named_struct("name1", col1, "name2", col2, ...) (issue #527)
2736            require_args_min(name, args, 2)?;
2737            if !args.len().is_multiple_of(2) {
2738                return Err(PlanExprError(
2739                    "named_struct requires even number of args (name, value pairs)".into(),
2740                ));
2741            }
2742            let mut names: Vec<String> = Vec::new();
2743            let mut cols: Vec<crate::Column> = Vec::new();
2744            for i in (0..args.len()).step_by(2) {
2745                names.push(lit_as_string(&args[i])?);
2746                cols.push(expr_to_column(arg_expr(args, i + 1)?));
2747            }
2748            let refs: Vec<(&str, &crate::Column)> =
2749                names.iter().map(|s| s.as_str()).zip(cols.iter()).collect();
2750            Ok(crate::functions::named_struct(&refs).into_expr())
2751        }
2752        "with_field" | "withField" => {
2753            // withField(struct_col, field_name, value) - PySpark struct field add/replace (issue #541)
2754            require_args(name, args, 3)?;
2755            let struct_col = expr_to_column(arg_expr(args, 0)?);
2756            let field_name = lit_as_string(&args[1])?;
2757            let value_col = expr_to_column(arg_expr(args, 2)?);
2758            let out = struct_col
2759                .try_with_field(&field_name, &value_col)
2760                .map_err(|e| PlanExprError(format!("with_field: {e}")))?;
2761            Ok(out.into_expr())
2762        }
2763        "nvl2" => {
2764            require_args(name, args, 3)?;
2765            let col1 = expr_to_column(arg_expr(args, 0)?);
2766            let col2 = expr_to_column(arg_expr(args, 1)?);
2767            let col3 = expr_to_column(arg_expr(args, 2)?);
2768            Ok(nvl2(&col1, &col2, &col3).into_expr())
2769        }
2770        _ => Err(PlanExprError(format!("unsupported function: {name}"))),
2771    }
2772}
2773
2774fn require_args(name: &str, args: &[Value], n: usize) -> Result<(), PlanExprError> {
2775    if args.len() != n {
2776        return Err(PlanExprError(format!(
2777            "fn '{name}' requires exactly {n} argument(s)"
2778        )));
2779    }
2780    Ok(())
2781}
2782
2783fn require_args_min(name: &str, args: &[Value], n: usize) -> Result<(), PlanExprError> {
2784    if args.len() < n {
2785        return Err(PlanExprError(format!(
2786            "fn '{name}' requires at least {n} argument(s)"
2787        )));
2788    }
2789    Ok(())
2790}
2791
2792#[cfg(test)]
2793mod tests {
2794    use super::*;
2795    use serde_json::json;
2796
2797    #[test]
2798    fn test_bare_string_column_ref() {
2799        // Fixes #644: bare string as column reference
2800        let v = json!("age");
2801        let e = expr_from_value(&v).unwrap();
2802        assert!(matches!(e, polars::prelude::Expr::Column(_)));
2803    }
2804
2805    #[test]
2806    fn test_col() {
2807        let v = json!({"col": "age"});
2808        let _e = expr_from_value(&v).unwrap();
2809    }
2810
2811    /// #969: Sparkless may send {"column": "name"} (PySpark Column); accept as column ref.
2812    #[test]
2813    fn test_column_key_as_col_ref() {
2814        let v = json!({"column": "age"});
2815        let e = expr_from_value(&v).unwrap();
2816        assert!(matches!(e, polars::prelude::Expr::Column(_)));
2817    }
2818
2819    #[test]
2820    fn test_lit_i64() {
2821        let v = json!({"lit": 30});
2822        let _ = expr_from_value(&v).unwrap();
2823    }
2824
2825    #[test]
2826    fn test_gt() {
2827        let v = json!({
2828            "op": "gt",
2829            "left": {"col": "age"},
2830            "right": {"lit": 30}
2831        });
2832        let _ = expr_from_value(&v).unwrap();
2833    }
2834
2835    #[test]
2836    fn test_and() {
2837        let v = json!({
2838            "op": "and",
2839            "left": {"op": "gt", "left": {"col": "a"}, "right": {"lit": 1}},
2840            "right": {"op": "lt", "left": {"col": "b"}, "right": {"lit": 10}}
2841        });
2842        let _ = expr_from_value(&v).unwrap();
2843    }
2844
2845    #[test]
2846    fn test_upper() {
2847        let v = json!({"fn": "upper", "args": [{"col": "name"}]});
2848        let _ = expr_from_value(&v).unwrap();
2849    }
2850
2851    #[test]
2852    fn test_length() {
2853        let v = json!({"fn": "length", "args": [{"col": "name"}]});
2854        let _ = expr_from_value(&v).unwrap();
2855    }
2856
2857    #[test]
2858    fn test_substring() {
2859        let v = json!({
2860            "fn": "substring",
2861            "args": [{"col": "s"}, {"lit": 1}, {"lit": 3}]
2862        });
2863        let _ = expr_from_value(&v).unwrap();
2864    }
2865
2866    #[test]
2867    fn test_year() {
2868        let v = json!({"fn": "year", "args": [{"col": "ts"}]});
2869        let _ = expr_from_value(&v).unwrap();
2870    }
2871
2872    #[test]
2873    fn test_cast() {
2874        let v = json!({
2875            "fn": "cast",
2876            "args": [{"col": "x"}, {"lit": "string"}]
2877        });
2878        let _ = expr_from_value(&v).unwrap();
2879    }
2880
2881    #[test]
2882    fn test_isin_op() {
2883        let v = json!({
2884            "op": "isin",
2885            "left": {"col": "id"},
2886            "right": {"lit": [1, 3]}
2887        });
2888        let _ = expr_from_value(&v).unwrap();
2889    }
2890
2891    #[test]
2892    fn test_isin_fn() {
2893        let v = json!({
2894            "fn": "isin",
2895            "args": [{"col": "id"}, {"lit": 1}, {"lit": 3}]
2896        });
2897        let _ = expr_from_value(&v).unwrap();
2898    }
2899
2900    /// col.isin([]) returns false for all rows (issue #518).
2901    #[test]
2902    fn test_isin_op_empty() {
2903        let v = json!({
2904            "op": "isin",
2905            "left": {"col": "id"},
2906            "right": {"lit": []}
2907        });
2908        let expr = expr_from_value(&v).unwrap();
2909        // Should be lit(false), not is_in with empty series
2910        assert!(matches!(expr, Expr::Literal(_)));
2911    }
2912
2913    /// col.isin() with no values or col.isin(null) -> false (issue #518).
2914    #[test]
2915    fn test_isin_fn_empty() {
2916        let v = json!({
2917            "fn": "isin",
2918            "args": [{"col": "id"}]
2919        });
2920        let expr = expr_from_value(&v).unwrap();
2921        assert!(matches!(expr, Expr::Literal(_)));
2922        let v2 = json!({
2923            "fn": "isin",
2924            "args": [{"col": "id"}, {"lit": null}]
2925        });
2926        let expr2 = expr_from_value(&v2).unwrap();
2927        assert!(matches!(expr2, Expr::Literal(_)));
2928    }
2929
2930    #[test]
2931    fn test_struct_named_struct_fn() {
2932        let v = json!({"fn": "struct", "args": [{"col": "a"}, {"col": "b"}]});
2933        let _ = expr_from_value(&v).unwrap();
2934        let v2 = json!({
2935            "fn": "named_struct",
2936            "args": [{"lit": "x"}, {"col": "a"}, {"lit": "y"}, {"col": "b"}]
2937        });
2938        let _ = expr_from_value(&v2).unwrap();
2939    }
2940
2941    #[test]
2942    fn test_get_item_fn() {
2943        let v = json!({"fn": "get_item", "args": [{"col": "arr"}, {"lit": 0}]});
2944        let _ = expr_from_value(&v).unwrap();
2945        let v2 = json!({"fn": "get_item", "args": [{"col": "m"}, {"lit": "key"}]});
2946        let _ = expr_from_value(&v2).unwrap();
2947    }
2948
2949    #[test]
2950    fn test_get_item_op() {
2951        let v = json!({"op": "getItem", "left": {"col": "arr"}, "right": {"lit": 1}});
2952        let _ = expr_from_value(&v).unwrap();
2953    }
2954
2955    #[test]
2956    fn test_startswith_op() {
2957        let v = json!({
2958            "op": "startswith",
2959            "left": {"col": "name"},
2960            "right": {"lit": "A"}
2961        });
2962        let _ = expr_from_value(&v).unwrap();
2963    }
2964
2965    #[test]
2966    fn test_is_null_op() {
2967        let v = json!({"op": "is_null", "arg": {"col": "x"}});
2968        let _ = expr_from_value(&v).unwrap();
2969    }
2970
2971    #[test]
2972    fn test_is_not_null_op() {
2973        let v = json!({"op": "is_not_null", "arg": {"col": "x"}});
2974        let _ = expr_from_value(&v).unwrap();
2975    }
2976
2977    #[test]
2978    fn test_regexp_extract_op() {
2979        let v = json!({
2980            "op": "regexp_extract",
2981            "left": {"col": "s"},
2982            "pattern": {"lit": r"(\w+)"},
2983            "group": {"lit": 1}
2984        });
2985        let _ = expr_from_value(&v).unwrap();
2986    }
2987
2988    /// Issue #582: fn form with bare string and number (Sparkless may send literals without {"lit": ...}).
2989    #[test]
2990    fn test_regexp_extract_fn_bare_literals() {
2991        let v = json!({
2992            "fn": "regexp_extract",
2993            "args": [{"col": "s"}, r"(\w+)", 1]
2994        });
2995        let _ = expr_from_value(&v).unwrap();
2996    }
2997
2998    #[test]
2999    fn test_regexp_replace_op() {
3000        let v = json!({
3001            "op": "regexp_replace",
3002            "left": {"col": "str"},
3003            "pattern": {"lit": r"\d"},
3004            "replacement": {"lit": "X"}
3005        });
3006        let _ = expr_from_value(&v).unwrap();
3007        let v2 = json!({
3008            "op": "regexp_replace",
3009            "args": [{"col": "str"}, {"lit": r"\d"}, {"lit": "X"}]
3010        });
3011        let _ = expr_from_value(&v2).unwrap();
3012    }
3013
3014    #[test]
3015    fn test_create_map_op() {
3016        let v = json!({
3017            "op": "create_map",
3018            "args": [{"lit": "k"}, {"col": "a"}]
3019        });
3020        let _ = expr_from_value(&v).unwrap();
3021    }
3022
3023    #[test]
3024    fn test_create_map_fn_empty() {
3025        // PySpark F.create_map() with no args: empty map {} per row (#512).
3026        let v = json!({"fn": "create_map", "args": []});
3027        let _ = expr_from_value(&v).unwrap();
3028    }
3029
3030    #[test]
3031    fn test_type_window_row_number_order_by() {
3032        let v = json!({
3033            "type": "window",
3034            "fn": "row_number",
3035            "window": {"order_by": ["val"]}
3036        });
3037        let _ = expr_from_value(&v).unwrap();
3038    }
3039
3040    /// Sparkless format: fn + args + window; order_by as [{"col": "salary", "asc": true}] (issue #517).
3041    #[test]
3042    fn test_window_row_number_sparkless_format() {
3043        let v = json!({
3044            "fn": "row_number",
3045            "args": [],
3046            "window": {
3047                "partition_by": ["dept"],
3048                "order_by": [{"col": "salary", "asc": true}]
3049            }
3050        });
3051        let _ = expr_from_value(&v).unwrap();
3052    }
3053
3054    /// row_number() with empty partition_by and order_by: window {} has no key so no "explicitly empty" (#520).
3055    #[test]
3056    fn test_row_number_window_empty() {
3057        let v = json!({
3058            "type": "window",
3059            "fn": "row_number",
3060            "window": {}
3061        });
3062        let _ = expr_from_value(&v).unwrap();
3063    }
3064
3065    /// #985: order_by explicitly [] with no partition_by must error with "At least one column must be specified for orderBy".
3066    #[test]
3067    fn test_window_order_by_empty_list_error() {
3068        let v = json!({
3069            "fn": "row_number",
3070            "args": [],
3071            "window": {"partition_by": [], "order_by": []}
3072        });
3073        let res = expr_from_value(&v);
3074        let err = res.unwrap_err();
3075        let msg = err.to_string();
3076        assert!(
3077            msg.contains("At least one column must be specified")
3078                || msg.contains("must be specified"),
3079            "expected error message to contain 'At least one column must be specified' or 'must be specified', got: {}",
3080            msg
3081        );
3082    }
3083
3084    /// Sparkless may send "function" instead of "fn" (issue #517).
3085    #[test]
3086    fn test_type_window_function_key() {
3087        let v = json!({
3088            "type": "window",
3089            "function": "row_number",
3090            "window": {"partition_by": ["dept"]}
3091        });
3092        let _ = expr_from_value(&v).unwrap();
3093    }
3094
3095    /// rank, dense_rank in plan execution (issue #521).
3096    #[test]
3097    fn test_window_rank_dense_rank() {
3098        let v = json!({
3099            "fn": "rank",
3100            "args": [],
3101            "window": {"partition_by": ["dept"], "order_by": ["salary"]}
3102        });
3103        let _ = expr_from_value(&v).unwrap();
3104        let v2 = json!({
3105            "type": "window",
3106            "fn": "dense_rank",
3107            "window": {"order_by": ["val"]}
3108        });
3109        let _ = expr_from_value(&v2).unwrap();
3110    }
3111
3112    #[test]
3113    fn test_window_plus_literal_op() {
3114        // (row_number().over(w) + 10) - add op with window as left
3115        let v = json!({
3116            "op": "add",
3117            "left": {"type": "window", "fn": "row_number", "window": {"order_by": ["val"]}},
3118            "right": {"lit": 10}
3119        });
3120        let _ = expr_from_value(&v).unwrap();
3121    }
3122
3123    #[test]
3124    fn test_when_two_arg() {
3125        let v = json!({
3126            "fn": "when",
3127            "args": [
3128                {"op": "gt", "left": {"col": "a"}, "right": {"lit": 0}},
3129                {"lit": "positive"}
3130            ]
3131        });
3132        let _ = expr_from_value(&v).unwrap();
3133    }
3134
3135    #[test]
3136    fn test_concat() {
3137        let v = json!({
3138            "fn": "concat",
3139            "args": [{"col": "first"}, {"lit": " "}, {"col": "last"}]
3140        });
3141        let _ = expr_from_value(&v).unwrap();
3142    }
3143
3144    /// Issue #583: op form of concat (F.concat(a, b)).
3145    #[test]
3146    fn test_concat_op() {
3147        let v = json!({
3148            "op": "concat",
3149            "args": [{"col": "a"}, {"col": "b"}]
3150        });
3151        let _ = expr_from_value(&v).unwrap();
3152    }
3153
3154    /// Issue #583: op form of contains (F.col("name").contains("lic")).
3155    #[test]
3156    fn test_contains_op() {
3157        let v = json!({
3158            "op": "contains",
3159            "args": [{"col": "name"}, {"lit": "lic"}]
3160        });
3161        let _ = expr_from_value(&v).unwrap();
3162    }
3163
3164    #[test]
3165    fn test_greatest() {
3166        let v = json!({
3167            "fn": "greatest",
3168            "args": [{"col": "a"}, {"col": "b"}, {"lit": 0}]
3169        });
3170        let _ = expr_from_value(&v).unwrap();
3171    }
3172
3173    #[test]
3174    fn test_array_size() {
3175        let v = json!({"fn": "array_size", "args": [{"col": "arr"}]});
3176        let _ = expr_from_value(&v).unwrap();
3177    }
3178
3179    #[test]
3180    fn test_element_at() {
3181        let v = json!({"fn": "element_at", "args": [{"col": "arr"}, {"lit": 1}]});
3182        let _ = expr_from_value(&v).unwrap();
3183    }
3184
3185    #[test]
3186    fn test_coalesce() {
3187        let v = json!({
3188            "fn": "coalesce",
3189            "args": [{"col": "a"}, {"col": "b"}, {"lit": 0}]
3190        });
3191        let _ = expr_from_value(&v).unwrap();
3192    }
3193
3194    /// Bare literals in coalesce args (fixes #828–#838): Sparkless may send 4.5 or 0 instead of {"lit": 4.5}.
3195    #[test]
3196    fn test_coalesce_bare_literal_arg() {
3197        let v = json!({
3198            "fn": "coalesce",
3199            "args": [{"col": "a"}, {"col": "b"}, 4.5]
3200        });
3201        let _ = expr_from_value(&v).unwrap();
3202    }
3203
3204    /// op+args form for coalesce (fixes #828–#838): Sparkless may send {"op": "coalesce", "args": [...]}.
3205    #[test]
3206    fn test_coalesce_op_args_form() {
3207        let v = json!({
3208            "op": "coalesce",
3209            "args": [{"col": "x"}, {"lit": "default"}]
3210        });
3211        let _ = expr_from_value(&v).unwrap();
3212    }
3213
3214    /// #683: op "div" parses and uses divide_pyspark so string/string division works.
3215    #[test]
3216    fn test_op_div_parses() {
3217        let v = json!({
3218            "op": "div",
3219            "left": {"col": "a"},
3220            "right": {"col": "b"}
3221        });
3222        let expr = expr_from_value(&v).unwrap();
3223        let _ = expr;
3224    }
3225
3226    /// #991: op "not" uses bitwise_not so Unknown(Any) from when/otherwise works (PySpark ~ parity).
3227    #[test]
3228    fn test_op_not_parses() {
3229        let v = json!({"op": "not", "arg": {"col": "flag"}});
3230        let expr = expr_from_value(&v).unwrap();
3231        let _ = expr;
3232    }
3233
3234    /// #628: between with string column and numeric bounds parses and uses coercion.
3235    #[test]
3236    fn test_between_string_column_numeric_bounds() {
3237        let v = json!({
3238            "op": "between",
3239            "left": {"col": "val"},
3240            "lower": {"lit": 1},
3241            "upper": {"lit": 10}
3242        });
3243        let expr = expr_from_value(&v).unwrap();
3244        // Expression should be (val >= 1) and (val <= 10) with coercion applied
3245        let _ = expr;
3246    }
3247}