Skip to content

Commit 69f5685

Browse files
committed
feat(codegen): || operator (NULL-propagating concat) — TASK-29 Phase A
1 parent 141b939 commit 69f5685

4 files changed

Lines changed: 16 additions & 5 deletions

File tree

sql_transform/_codegen/engine.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"gte": "rt.gte",
4141
"and": "rt.and_",
4242
"or": "rt.or_",
43+
"dpipe": "rt.dpipe",
4344
}
4445

4546
_BUILTINS = {

sql_transform/_codegen/plan.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ def _convert_expr(e: exp.Expression) -> Any:
323323
# NULL propagation. infer_type/emit already handle "sub".
324324
return BinaryOp("sub", Literal(0), inner)
325325
if isinstance(e, exp.DPipe):
326-
# The `||` string-concat operator: supported by DataFusion and native,
327-
# deferred by codegen (its NULL-propagating semantics differ from CONCAT).
328-
raise UnsupportedInCodegen("the || operator is not supported in codegen yet")
326+
# The `||` operator: NULL-propagating string concat (mirrors native
327+
# concat_op). Binary -- a||b||c nests as DPipe(DPipe(a,b), c).
328+
return BinaryOp("dpipe", _convert_expr(e.this), _convert_expr(e.expression))
329329
if isinstance(e, exp.Not):
330330
return Not(_convert_expr(e.this))
331331
if isinstance(e, exp.Case):
@@ -740,6 +740,8 @@ def infer_type(e: Any, schemas: dict) -> FieldType:
740740
if e.op in ("add", "sub", "mul", "div", "mod"):
741741
base = INT if left.base == INT and right.base == INT else FLOAT
742742
return FieldType(base, nullable)
743+
if e.op == "dpipe":
744+
return FieldType(STR, nullable)
743745
if is_container(left.base) or is_container(right.base):
744746
# Comparing/combining structs or lists needs deep equality, which
745747
# the runtime doesn't implement -- without this it silently reaches

sql_transform/_codegen/runtime.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,15 @@ def concat(*a: Any) -> str:
393393
return "".join(display(x) for x in a if x is not None)
394394

395395

396+
def dpipe(l: Any, r: Any) -> Any: # noqa: E741
397+
"""The `||` operator: NULL-propagating string concat -- any NULL operand
398+
yields NULL (unlike concat(), which skips NULLs). Mirrors expr.rs concat_op:
399+
non-NULL operands are rendered via display() and joined."""
400+
if l is None or r is None:
401+
return None
402+
return display(l) + display(r)
403+
404+
396405
def abs_(*a: Any) -> Any:
397406
v = a[0]
398407
if v is None:

tests/test_codegen_coverage.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
("SELECT a AS x FROM t", {"t": rows({"a": "int"}, [{"a": 1}])}),
1818
("SELECT a + 1 AS x FROM t", {"t": rows({"a": "int"}, [{"a": 1}])}),
1919
("SELECT -a AS x FROM t", {"t": rows({"a": "int"}, [{"a": 5}])}),
20-
# NB: the `||` operator is deliberately absent here until Phase A Task 2 lands
21-
# it -- see the codegen deferred-surface spec.
20+
("SELECT a || '!' AS x FROM t", {"t": rows({"a": "str"}, [{"a": "hi"}])}),
2221
(
2322
"SELECT a / b AS x FROM t",
2423
{"t": rows({"a": "int", "b": "int"}, [{"a": 7, "b": 2}])},

0 commit comments

Comments
 (0)