Skip to content

Commit 0558fc9

Browse files
committed
fix(codegen): widen mixed-numeric list elements + narrow unknown-func defer to __tfm_ (TASK-29B-2 review)
- infer_type's ListExpr arm now unifies element types via _common_base (same helper COALESCE uses), so make_array(int, float) types as list[float] and pydantic widens the int element at model_validate (measured: no emission-side change needed). - _emit_expr's Func branch narrows the UnsupportedInCodegen defer to __tfm_ transformer placeholders only; every other unknown function name (e.g. foobar(x), invalid SQL) keeps the original hard ValueError instead of being silently skipped by the differential harness.
1 parent 62b890f commit 0558fc9

4 files changed

Lines changed: 57 additions & 11 deletions

File tree

sql_transform/_codegen/engine.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,13 @@ def _emit_expr(e: Any, env: dict) -> str:
9494
if isinstance(e, cp.Func):
9595
fn = _BUILTINS.get(e.name)
9696
if fn is None:
97-
# Anything without a codegen builtin -- notably a transformer call
98-
# (`__tfm_N__(...)`), which codegen has no support for at all -- is a
99-
# deferred surface, not a hard error.
100-
raise UnsupportedInCodegen(f"{e.name}() is not supported in codegen yet")
97+
if e.name.startswith("__tfm_"):
98+
# A transformer placeholder call: codegen has no transformer
99+
# support (native-only, TASK-34) -- deferred surface, not an error.
100+
raise UnsupportedInCodegen(
101+
f"{e.name}() is not supported in codegen yet"
102+
)
103+
raise ValueError(f"Unknown function: {e.name}")
101104
return f"{fn}({', '.join(_emit_expr(a, env) for a in e.args)})"
102105
if isinstance(e, cp.Case):
103106
out = _emit_expr(e.default, env)

sql_transform/_codegen/plan.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -820,13 +820,16 @@ def infer_type(e: Any, schemas: dict) -> FieldType:
820820
return FieldType(StructBase(fields), False)
821821
if isinstance(e, ListExpr):
822822
elem_types = [infer_type(x, schemas) for x in e.items]
823-
# Native unifies: identical element types collapse to that type; an empty
824-
# or mixed list is unresolvable.
825-
elem = (
826-
elem_types[0]
827-
if elem_types and all(t == elem_types[0] for t in elem_types)
828-
else FieldType(OTHER, True)
829-
)
823+
if not elem_types:
824+
elem = FieldType(OTHER, True)
825+
else:
826+
# Unify element bases the same way COALESCE does (numeric int/float
827+
# -> float), so e.g. make_array(int_col, float_col) types as
828+
# list<float> -- matching DataFusion, which widens the int element
829+
# at runtime (measured: make_array(1, 2.5) -> [1.0, 2.5]).
830+
elem = FieldType(
831+
_common_base(elem_types), any(t.nullable for t in elem_types)
832+
)
830833
return FieldType(ListBase(elem), False)
831834
if isinstance(e, Func):
832835
return _function_type(e.name, [infer_type(a, schemas) for a in e.args])

tests/test_diff_errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ def test_int_mod_by_zero_rejected_by_both():
2929
"SELECT a % b AS r FROM t",
3030
{"t": rows({"a": "int", "b": "int"}, [{"a": 5, "b": 0}])},
3131
)
32+
33+
34+
def test_unknown_function_rejected_by_both():
35+
# foobar() is invalid SQL -- DataFusion errors -- and is NOT a `__tfm_`
36+
# transformer placeholder, so codegen must raise a hard error too (not
37+
# UnsupportedInCodegen, which the harness would skip as a deferred surface).
38+
check_both_raise(
39+
"SELECT foobar(x) AS y FROM t",
40+
{"t": rows({"x": "int"}, [{"x": 1}])},
41+
)

tests/test_diff_types.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@ def test_list_construct():
2121
)
2222

2323

24+
def test_list_construct_mixed_numeric_widens(xfail_on_native):
25+
# DataFusion widens the int element to match the float element (list<double>).
26+
# Locks in infer_type's ListExpr arm unifying element bases via _common_base
27+
# (like COALESCE), so the output model types the list as list[float] and
28+
# pydantic coerces the runtime int element to float.
29+
#
30+
# Bracket literal `[x, y]` (not make_array(...)): native's function dispatch
31+
# doesn't recognize "make_array"/"array" as a builtin at all (a separate,
32+
# pre-existing gap -- expr_build.rs's convert_function has no such case,
33+
# unlike Python's ANONYMOUS-function branch), so it isn't a usable surface
34+
# to compare both engines against the oracle. The bracket form reaches the
35+
# same Expr::List / ListExpr construction path in both engines.
36+
#
37+
# xfail_on_native: measured -- native's unify_list_element_types
38+
# (src/types.rs) is exact-equality-only, the same bug class just fixed here
39+
# in codegen's infer_type. Native still emits [1, 2.5] (un-widened) instead
40+
# of DataFusion's [1.0, 2.5]. A native-side fix is out of scope here; flag
41+
# as a parity bug for its own ticket rather than fixing inline.
42+
xfail_on_native(
43+
"native does not widen mixed int/float list elements "
44+
"(types.rs unify_list_element_types is exact-equality-only) -- "
45+
"separate parity bug, own ticket"
46+
)
47+
check(
48+
"SELECT [x, y] AS l FROM t",
49+
{"t": rows({"x": "int", "y": "float"}, [{"x": 1, "y": 2.5}])},
50+
expect=[{"l": [1.0, 2.5]}],
51+
)
52+
53+
2454
def test_struct_input_roundtrip():
2555
check(
2656
"SELECT s FROM t",

0 commit comments

Comments
 (0)