Skip to content

Commit a8cbde3

Browse files
feat(bigframes): UDF transpiler handles some control flow (googleapis#17558)
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-cloud-python/issues) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #<issue_number_goes_here> 🦕
1 parent 0dfdf44 commit a8cbde3

13 files changed

Lines changed: 1005 additions & 178 deletions

File tree

packages/bigframes/bigframes/core/bytecode.py

Lines changed: 641 additions & 166 deletions
Large diffs are not rendered by default.

packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_registry.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,25 @@ def numeric_to_datetime(
884884
)
885885

886886

887+
@scalar_op_compiler.register_unary_op(ops.coerce_to_bool_op)
888+
def coerce_to_bool_op_impl(x: ibis_types.Value):
889+
x_type = x.type()
890+
if x_type.is_boolean():
891+
res = x
892+
elif x_type.is_numeric():
893+
res = x != 0 # type: ignore
894+
elif x_type.is_string():
895+
res = x.length() > 0 # type: ignore
896+
elif x_type.is_binary():
897+
res = x.length() > 0 # type: ignore
898+
elif isinstance(x_type, ibis_dtypes.Array):
899+
res = x.length() > 0 # type: ignore
900+
else:
901+
res = x.notnull()
902+
903+
return res.fill_null(False) # type: ignore
904+
905+
887906
@scalar_op_compiler.register_unary_op(ops.AsTypeOp, pass_op=True)
888907
def astype_op_impl(x: ibis_types.Value, op: ops.AsTypeOp):
889908
to_type = bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype(

packages/bigframes/bigframes/core/compile/polars/compiler.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,28 @@ def _(
372372
) -> pl.Expr:
373373
return pl.when(condition).then(original).otherwise(otherwise)
374374

375+
@compile_op.register(gen_ops.CoerceToBoolOp)
376+
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
377+
assert isinstance(op, gen_ops.CoerceToBoolOp)
378+
from_type = self._expr_types.get(id(input))
379+
if from_type is None:
380+
return input.cast(pl.Boolean).fill_null(False)
381+
382+
if from_type == bigframes.dtypes.BOOL_DTYPE:
383+
res = input
384+
elif bigframes.dtypes.is_numeric(from_type):
385+
res = input != 0
386+
elif from_type == bigframes.dtypes.BYTES_DTYPE:
387+
res = input.bin.size() > 0
388+
elif bigframes.dtypes.is_string_like(from_type):
389+
res = input.str.len_chars() > 0
390+
elif bigframes.dtypes.is_array_like(from_type):
391+
res = input.list.len() > 0
392+
else:
393+
res = input.is_not_null()
394+
395+
return res.fill_null(False)
396+
375397
@compile_op.register(gen_ops.AsTypeOp)
376398
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
377399
assert isinstance(op, gen_ops.AsTypeOp)

packages/bigframes/bigframes/core/compile/sqlglot/expressions/generic_ops.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,28 @@ def _(expr: TypedExpr) -> sge.Expression:
148148
)
149149

150150

151+
@register_unary_op(ops.coerce_to_bool_op)
152+
def _(expr: TypedExpr) -> sge.Expression:
153+
from_type = expr.dtype
154+
sg_expr = expr.expr
155+
156+
if from_type == dtypes.BOOL_DTYPE:
157+
res = sg_expr
158+
elif dtypes.is_numeric(from_type):
159+
res = sge.NEQ(this=sg_expr, expression=sge.convert(0))
160+
elif dtypes.is_string_like(from_type):
161+
res = sge.GT(this=sge.func("LENGTH", sg_expr), expression=sge.convert(0))
162+
elif dtypes.is_array_like(from_type):
163+
res = sge.GT(this=sge.func("ARRAY_LENGTH", sg_expr), expression=sge.convert(0))
164+
else:
165+
res = sge.Is(
166+
this=sge.paren(sg_expr, copy=False),
167+
expression=sg.not_(sge.Null(), copy=False),
168+
)
169+
170+
return sge.Coalesce(this=res, expressions=[sge.convert(False)])
171+
172+
151173
@register_ternary_op(ops.where_op)
152174
def _(
153175
original: TypedExpr, condition: TypedExpr, replacement: TypedExpr

packages/bigframes/bigframes/core/py_expressions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@
2929
const,
3030
deref,
3131
)
32-
from bigframes.operations import NUMPY_TO_BINOP, NUMPY_TO_OP, generic_ops, numeric_ops
32+
from bigframes.operations import (
33+
NUMPY_TO_BINOP,
34+
NUMPY_TO_OP,
35+
ScalarOp,
36+
generic_ops,
37+
numeric_ops,
38+
)
3339

3440
_CALLABLE_TO_OP = {
3541
**NUMPY_TO_OP,
@@ -365,6 +371,8 @@ def resolve_call(call: Call) -> Expression:
365371
op = _CALLABLE_TO_OP[fn]
366372
return OpExpression(op, call.inputs)
367373
elif isinstance(callable, PyObject):
374+
if isinstance(callable.value, ScalarOp):
375+
return OpExpression(callable.value, call.inputs)
368376
if callable.value in python_op_maps.PYTHON_TO_BIGFRAMES:
369377
op = python_op_maps.PYTHON_TO_BIGFRAMES[callable.value] # type: ignore
370378
return OpExpression(op, call.inputs)

packages/bigframes/bigframes/dtypes.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,12 @@ def is_clusterable(type_: ExpressionType) -> bool:
468468

469469
def is_bool_coercable(type_: ExpressionType) -> bool:
470470
# TODO: Implement more bool coercions
471-
return (type_ is None) or is_numeric(type_) or is_string_like(type_)
471+
return (
472+
(type_ is None)
473+
or is_numeric(type_)
474+
or is_string_like(type_)
475+
or is_array_like(type_)
476+
)
472477

473478

474479
BIGFRAMES_STRING_TO_BIGFRAMES: Dict[DtypeString, Dtype] = {

packages/bigframes/bigframes/operations/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,15 @@
9393
from bigframes.operations.generic_ops import (
9494
AsTypeOp,
9595
CaseWhenOp,
96+
CoerceToBoolOp,
9697
IsInOp,
9798
MapOp,
9899
RowKey,
99100
SqlScalarOp,
100101
case_when_op,
101102
clip_op,
102103
coalesce_op,
104+
coerce_to_bool_op,
103105
fillna_op,
104106
hash_op,
105107
invert_op,
@@ -255,6 +257,8 @@
255257
"maximum_op",
256258
"minimum_op",
257259
"notnull_op",
260+
"CoerceToBoolOp",
261+
"coerce_to_bool_op",
258262
"RowKey",
259263
"SqlScalarOp",
260264
"where_op",

packages/bigframes/bigframes/operations/generic_ops.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@
4545
)
4646
notnull_op = NotNullOp()
4747

48+
49+
# Semantics match Python's truth value testing (truthy and falsey objects).
50+
# See https://docs.python.org/3/library/stdtypes.html#truth-value-testing
51+
CoerceToBoolOp = base_ops.create_unary_op(
52+
name="coerce_to_bool",
53+
type_signature=op_typing.FixedOutputType(
54+
dtypes.is_bool_coercable, dtypes.BOOL_DTYPE, description="coercable to bool"
55+
),
56+
)
57+
CoerceToBoolOp.__doc__ = (
58+
"Coerce a value to a boolean, matching Python's truth value testing semantics "
59+
"(truthy/falsey). See https://docs.python.org/3/library/stdtypes.html#truth-value-testing"
60+
)
61+
coerce_to_bool_op = CoerceToBoolOp()
62+
4863
HashOp = base_ops.create_unary_op(
4964
name="hash",
5065
type_signature=op_typing.FixedOutputType(

packages/bigframes/bigframes/operations/python_op_maps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
array_ops,
2323
bool_ops,
2424
comparison_ops,
25+
generic_ops,
2526
numeric_ops,
2627
string_ops,
2728
)
@@ -47,6 +48,8 @@
4748
operator.and_: bool_ops.and_op,
4849
operator.or_: bool_ops.or_op,
4950
operator.xor: bool_ops.xor_op,
51+
operator.invert: generic_ops.invert_op,
52+
operator.not_: generic_ops.invert_op,
5053
## math
5154
math.log: numeric_ops.ln_op,
5255
math.log10: numeric_ops.log10_op,

packages/bigframes/tests/system/small/engines/test_generic_ops.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,39 @@ def test_engines_notnull_op(scalars_array_value: array_value.ArrayValue, engine)
424424
assert_equivalence_execution(arr.node, REFERENCE_ENGINE, engine)
425425

426426

427+
@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True)
428+
def test_engines_coerce_to_bool_op_scalars(
429+
scalars_array_value: array_value.ArrayValue, engine
430+
):
431+
arr, _ = scalars_array_value.compute_values(
432+
[
433+
ops.coerce_to_bool_op.as_expr(expression.deref("bool_col")),
434+
ops.coerce_to_bool_op.as_expr(expression.deref("int64_col")),
435+
ops.coerce_to_bool_op.as_expr(expression.deref("float64_col")),
436+
ops.coerce_to_bool_op.as_expr(expression.deref("string_col")),
437+
ops.coerce_to_bool_op.as_expr(expression.deref("bytes_col")),
438+
]
439+
)
440+
441+
assert_equivalence_execution(arr.node, REFERENCE_ENGINE, engine)
442+
443+
444+
@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True)
445+
def test_engines_coerce_to_bool_op_arrays(
446+
arrays_array_value: array_value.ArrayValue, engine
447+
):
448+
arr, _ = arrays_array_value.compute_values(
449+
[
450+
ops.coerce_to_bool_op.as_expr(expression.deref("int_list_col")),
451+
ops.coerce_to_bool_op.as_expr(expression.deref("bool_list_col")),
452+
ops.coerce_to_bool_op.as_expr(expression.deref("float_list_col")),
453+
ops.coerce_to_bool_op.as_expr(expression.deref("string_list_col")),
454+
]
455+
)
456+
457+
assert_equivalence_execution(arr.node, REFERENCE_ENGINE, engine)
458+
459+
427460
@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True)
428461
def test_engines_invert_op(scalars_array_value: array_value.ArrayValue, engine):
429462
arr, _ = scalars_array_value.compute_values(

0 commit comments

Comments
 (0)