Skip to content

Commit 86a2756

Browse files
feat: Support some python standard lib callables in apply/combine (#2187)
1 parent 6eab121 commit 86a2756

File tree

9 files changed

+412
-77
lines changed

9 files changed

+412
-77
lines changed

bigframes/core/compile/polars/compiler.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,26 @@ def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
328328
assert isinstance(op, string_ops.StrContainsRegexOp)
329329
return input.str.contains(pattern=op.pat, literal=False)
330330

331+
@compile_op.register(string_ops.UpperOp)
332+
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
333+
assert isinstance(op, string_ops.UpperOp)
334+
return input.str.to_uppercase()
335+
336+
@compile_op.register(string_ops.LowerOp)
337+
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
338+
assert isinstance(op, string_ops.LowerOp)
339+
return input.str.to_lowercase()
340+
341+
@compile_op.register(string_ops.ArrayLenOp)
342+
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
343+
assert isinstance(op, string_ops.ArrayLenOp)
344+
return input.list.len()
345+
346+
@compile_op.register(string_ops.StrLenOp)
347+
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
348+
assert isinstance(op, string_ops.StrLenOp)
349+
return input.str.len_chars()
350+
331351
@compile_op.register(string_ops.StartsWithOp)
332352
def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr:
333353
assert isinstance(op, string_ops.StartsWithOp)

bigframes/core/compile/polars/lowering.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
generic_ops,
2828
json_ops,
2929
numeric_ops,
30+
string_ops,
3031
)
3132
import bigframes.operations as ops
3233

@@ -347,11 +348,28 @@ def lower(self, expr: expression.OpExpression) -> expression.Expression:
347348
return ops.coalesce_op.as_expr(new_isin, expression.const(False))
348349

349350

351+
class LowerLenOp(op_lowering.OpLoweringRule):
352+
@property
353+
def op(self) -> type[ops.ScalarOp]:
354+
return string_ops.LenOp
355+
356+
def lower(self, expr: expression.OpExpression) -> expression.Expression:
357+
assert isinstance(expr.op, string_ops.LenOp)
358+
arg = expr.children[0]
359+
360+
if dtypes.is_string_like(arg.output_type):
361+
return string_ops.StrLenOp().as_expr(arg)
362+
elif dtypes.is_array_like(arg.output_type):
363+
return string_ops.ArrayLenOp().as_expr(arg)
364+
else:
365+
raise ValueError(f"Unexpected type: {arg.output_type}")
366+
367+
350368
def _coerce_comparables(
351369
expr1: expression.Expression,
352370
expr2: expression.Expression,
353371
*,
354-
bools_only: bool = False
372+
bools_only: bool = False,
355373
):
356374
if bools_only:
357375
if (
@@ -446,6 +464,7 @@ def _lower_cast(cast_op: ops.AsTypeOp, arg: expression.Expression):
446464
LowerAsTypeRule(),
447465
LowerInvertOp(),
448466
LowerIsinOp(),
467+
LowerLenOp(),
449468
)
450469

451470

bigframes/core/compile/polars/operations/numeric_ops.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,6 @@
2929
import polars as pl
3030

3131

32-
@polars_compiler.register_op(numeric_ops.CosOp)
33-
def cos_op_impl(
34-
compiler: polars_compiler.PolarsExpressionCompiler,
35-
op: numeric_ops.CosOp, # type: ignore
36-
input: pl.Expr,
37-
) -> pl.Expr:
38-
return input.cos()
39-
40-
4132
@polars_compiler.register_op(numeric_ops.LnOp)
4233
def ln_op_impl(
4334
compiler: polars_compiler.PolarsExpressionCompiler,
@@ -80,6 +71,78 @@ def sin_op_impl(
8071
return input.sin()
8172

8273

74+
@polars_compiler.register_op(numeric_ops.CosOp)
75+
def cos_op_impl(
76+
compiler: polars_compiler.PolarsExpressionCompiler,
77+
op: numeric_ops.CosOp, # type: ignore
78+
input: pl.Expr,
79+
) -> pl.Expr:
80+
return input.cos()
81+
82+
83+
@polars_compiler.register_op(numeric_ops.TanOp)
84+
def tan_op_impl(
85+
compiler: polars_compiler.PolarsExpressionCompiler,
86+
op: numeric_ops.SinOp, # type: ignore
87+
input: pl.Expr,
88+
) -> pl.Expr:
89+
return input.tan()
90+
91+
92+
@polars_compiler.register_op(numeric_ops.SinhOp)
93+
def sinh_op_impl(
94+
compiler: polars_compiler.PolarsExpressionCompiler,
95+
op: numeric_ops.SinOp, # type: ignore
96+
input: pl.Expr,
97+
) -> pl.Expr:
98+
return input.sinh()
99+
100+
101+
@polars_compiler.register_op(numeric_ops.CoshOp)
102+
def cosh_op_impl(
103+
compiler: polars_compiler.PolarsExpressionCompiler,
104+
op: numeric_ops.CosOp, # type: ignore
105+
input: pl.Expr,
106+
) -> pl.Expr:
107+
return input.cosh()
108+
109+
110+
@polars_compiler.register_op(numeric_ops.TanhOp)
111+
def tanh_op_impl(
112+
compiler: polars_compiler.PolarsExpressionCompiler,
113+
op: numeric_ops.SinOp, # type: ignore
114+
input: pl.Expr,
115+
) -> pl.Expr:
116+
return input.tanh()
117+
118+
119+
@polars_compiler.register_op(numeric_ops.ArcsinOp)
120+
def asin_op_impl(
121+
compiler: polars_compiler.PolarsExpressionCompiler,
122+
op: numeric_ops.ArcsinOp, # type: ignore
123+
input: pl.Expr,
124+
) -> pl.Expr:
125+
return input.arcsin()
126+
127+
128+
@polars_compiler.register_op(numeric_ops.ArccosOp)
129+
def acos_op_impl(
130+
compiler: polars_compiler.PolarsExpressionCompiler,
131+
op: numeric_ops.ArccosOp, # type: ignore
132+
input: pl.Expr,
133+
) -> pl.Expr:
134+
return input.arccos()
135+
136+
137+
@polars_compiler.register_op(numeric_ops.ArctanOp)
138+
def atan_op_impl(
139+
compiler: polars_compiler.PolarsExpressionCompiler,
140+
op: numeric_ops.ArctanOp, # type: ignore
141+
input: pl.Expr,
142+
) -> pl.Expr:
143+
return input.arctan()
144+
145+
83146
@polars_compiler.register_op(numeric_ops.SqrtOp)
84147
def sqrt_op_impl(
85148
compiler: polars_compiler.PolarsExpressionCompiler,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import math
16+
import operator
17+
from typing import Optional
18+
19+
import bigframes.operations
20+
from bigframes.operations import (
21+
aggregations,
22+
array_ops,
23+
bool_ops,
24+
comparison_ops,
25+
numeric_ops,
26+
string_ops,
27+
)
28+
29+
PYTHON_TO_BIGFRAMES = {
30+
## operators
31+
operator.add: numeric_ops.add_op,
32+
operator.sub: numeric_ops.sub_op,
33+
operator.mul: numeric_ops.mul_op,
34+
operator.truediv: numeric_ops.div_op,
35+
operator.floordiv: numeric_ops.floordiv_op,
36+
operator.mod: numeric_ops.mod_op,
37+
operator.pow: numeric_ops.pow_op,
38+
operator.pos: numeric_ops.pos_op,
39+
operator.neg: numeric_ops.neg_op,
40+
operator.abs: numeric_ops.abs_op,
41+
operator.eq: comparison_ops.eq_op,
42+
operator.ne: comparison_ops.ne_op,
43+
operator.gt: comparison_ops.gt_op,
44+
operator.lt: comparison_ops.lt_op,
45+
operator.ge: comparison_ops.ge_op,
46+
operator.le: comparison_ops.le_op,
47+
operator.and_: bool_ops.and_op,
48+
operator.or_: bool_ops.or_op,
49+
operator.xor: bool_ops.xor_op,
50+
## math
51+
math.log: numeric_ops.ln_op,
52+
math.log10: numeric_ops.log10_op,
53+
math.log1p: numeric_ops.log1p_op,
54+
math.expm1: numeric_ops.expm1_op,
55+
math.sin: numeric_ops.sin_op,
56+
math.cos: numeric_ops.cos_op,
57+
math.tan: numeric_ops.tan_op,
58+
math.sinh: numeric_ops.sinh_op,
59+
math.cosh: numeric_ops.cosh_op,
60+
math.tanh: numeric_ops.tanh_op,
61+
math.asin: numeric_ops.arcsin_op,
62+
math.acos: numeric_ops.arccos_op,
63+
math.atan: numeric_ops.arctan_op,
64+
math.floor: numeric_ops.floor_op,
65+
math.ceil: numeric_ops.ceil_op,
66+
## str
67+
str.upper: string_ops.upper_op,
68+
str.lower: string_ops.lower_op,
69+
## builtins
70+
len: string_ops.len_op,
71+
abs: numeric_ops.abs_op,
72+
pow: numeric_ops.pow_op,
73+
### builtins -- iterable
74+
all: array_ops.ArrayReduceOp(aggregations.all_op),
75+
any: array_ops.ArrayReduceOp(aggregations.any_op),
76+
sum: array_ops.ArrayReduceOp(aggregations.sum_op),
77+
min: array_ops.ArrayReduceOp(aggregations.min_op),
78+
max: array_ops.ArrayReduceOp(aggregations.max_op),
79+
}
80+
81+
82+
def python_callable_to_op(obj) -> Optional[bigframes.operations.RowOp]:
83+
if obj in PYTHON_TO_BIGFRAMES:
84+
return PYTHON_TO_BIGFRAMES[obj]
85+
return None

bigframes/operations/string_ops.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@
3030
)
3131
len_op = LenOp()
3232

33+
## Specialized len ops for compile-time lowering
34+
StrLenOp = base_ops.create_unary_op(
35+
name="strlen",
36+
type_signature=op_typing.FixedOutputType(
37+
dtypes.is_string_like, dtypes.INT_DTYPE, description="string-like"
38+
),
39+
)
40+
str_len_op = StrLenOp()
41+
42+
ArrayLenOp = base_ops.create_unary_op(
43+
name="arraylen",
44+
type_signature=op_typing.FixedOutputType(
45+
dtypes.is_array_like, dtypes.INT_DTYPE, description="array-like"
46+
),
47+
)
48+
array_len_op = ArrayLenOp()
49+
3350
ReverseOp = base_ops.create_unary_op(
3451
name="reverse", type_signature=op_typing.STRING_TRANSFORM
3552
)

0 commit comments

Comments
 (0)