diff --git a/packages/bigframes/bigframes/bigquery/_operations/geo.py b/packages/bigframes/bigframes/bigquery/_operations/geo.py index cd1d22b16489..e9ea711c9690 100644 --- a/packages/bigframes/bigframes/bigquery/_operations/geo.py +++ b/packages/bigframes/bigframes/bigquery/_operations/geo.py @@ -99,7 +99,7 @@ def st_area( bigframes.pandas.Series: Series of float representing the areas. """ - series = series._apply_unary_op(ops.geo_area_op) + series = series._apply_nary_op(ops.googlesql.ST_AREA, []) series.name = None return series @@ -223,7 +223,7 @@ def st_centroid( bigframes.pandas.Series: A series of geography objects representing the centroids. """ - series = series._apply_unary_op(ops.geo_st_centroid_op) + series = series._apply_nary_op(ops.googlesql.ST_CENTROID, []) series.name = None return series @@ -753,6 +753,4 @@ def st_simplify( Returns: a Series containing the simplified GEOGRAPHY data. """ - return geography._apply_unary_op( - ops.GeoStSimplifyOp(tolerance_meters=tolerance_meters) - ) + return geography._apply_nary_op(ops.googlesql.ST_SIMPLIFY, [tolerance_meters]) diff --git a/packages/bigframes/bigframes/bigquery/_operations/mathematical.py b/packages/bigframes/bigframes/bigquery/_operations/mathematical.py index 476d012bea4d..5e6a299f83f3 100644 --- a/packages/bigframes/bigframes/bigquery/_operations/mathematical.py +++ b/packages/bigframes/bigframes/bigquery/_operations/mathematical.py @@ -20,6 +20,7 @@ import bigframes.core.expression from bigframes import dtypes from bigframes import operations as ops +from bigframes.operations import googlesql def rand() -> bigframes.core.col.Expression: @@ -47,12 +48,9 @@ def rand() -> bigframes.core.col.Expression: :func:`~bigframes.pandas.DataFrame.assign` and other methods. See :func:`bigframes.pandas.col`. """ - op = ops.SqlScalarOp( - _output_type=dtypes.FLOAT_DTYPE, - sql_template="RAND()", - is_deterministic=False, + return bigframes.core.col.Expression( + bigframes.core.expression.OpExpression(googlesql.RAND, ()) ) - return bigframes.core.col.Expression(bigframes.core.expression.OpExpression(op, ())) def hparam_range(min: float, max: float) -> bigframes.core.col.Expression: diff --git a/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py b/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py index 07e3110a968e..0286b1913fda 100644 --- a/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py +++ b/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py @@ -94,6 +94,14 @@ def _( ] return self.compile_row_op(expression.op, inputs) + @compile_expression.register + def _( + self, + expression: ex.Omitted, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + return bigframes_vendored.ibis.omitted() + def compile_row_op( self, op: ops.RowOp, inputs: typing.Sequence[ibis_types.Value] ) -> ibis_types.Value: diff --git a/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_registry.py b/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_registry.py index 03ec72f4e44b..abcc50555a31 100644 --- a/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_registry.py +++ b/packages/bigframes/bigframes/core/compile/ibis_compiler/scalar_op_registry.py @@ -35,6 +35,7 @@ from bigframes.core.compile.ibis_compiler.scalar_op_compiler import ( scalar_op_compiler, # TODO(tswast): avoid import of variables ) +from bigframes.operations.googlesql import CallingConvention _ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) _NAN = typing.cast(ibis_types.NumericValue, ibis_types.literal(np.nan)) @@ -1890,6 +1891,67 @@ def case_when_op(*cases_and_outputs: ibis_types.Value) -> ibis_types.Value: return case_val.end() # type: ignore +@scalar_op_compiler.register_nary_op(ops.GoogleSqlScalarOp, pass_op=True) +def googlesql_scalar_op_impl(*operands: ibis_types.Value, op: ops.GoogleSqlScalarOp): + final_operands = [] + arg_templates = [] + if op.calling_convention == CallingConvention.FUNCTION: + for i, operand in enumerate(operands): + if i < len(op.args): + arg_spec = op.args[i] + else: + assert op.args[-1].is_vararg, ( + f"Too many arguments, for {op.sql_name}, expected {len(op.args)}" + ) + arg_spec = op.args[-1] + if operand.op().omitted: + assert arg_spec.optional, f"Argument omitted, but not optional" + continue + + target_idx = len(final_operands) + final_operands.append(operand) + if arg_spec.arg_name: + arg_templates.append(f"{arg_spec.arg_name} => {{{target_idx}}}") + else: + arg_templates.append(f"{{{target_idx}}}") + args_template = ", ".join(arg_templates) + sql_template = f"{op.sql_name}({args_template})" + return ibis_generic.SqlScalar( + sql_template, + values=tuple( + typing.cast(ibis_generic.Value, expr.op()) for expr in final_operands + ), + output_type=bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + op.output_type() + ), + ).to_expr() + elif op.calling_convention == CallingConvention.PREFIX: + assert len(operands) == 1, "prefix op expects exactly 1 arg" + return ibis_generic.SqlScalar( + f"{op.sql_name} {{0}}", + values=tuple( + typing.cast(ibis_generic.Value, expr.op()) for expr in operands + ), + output_type=bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + op.output_type() + ), + ).to_expr() + elif op.calling_convention == CallingConvention.INFIX: + assert len(operands) == 2, "infix op expects exactly 2 args" + return ibis_generic.SqlScalar( + f"{{0}} {op.sql_name} {{1}}", + values=tuple( + typing.cast(ibis_generic.Value, expr.op()) for expr in operands + ), + output_type=bigframes.core.compile.ibis_types.bigframes_dtype_to_ibis_dtype( + op.output_type() + ), + ).to_expr() + raise NotImplementedError( + f"Calling convention {op.calling_convention} not supported for {op}" + ) + + @scalar_op_compiler.register_nary_op(ops.SqlScalarOp, pass_op=True) def sql_scalar_op_impl(*operands: ibis_types.Value, op: ops.SqlScalarOp): return ibis_generic.SqlScalar( diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/expression_compiler.py b/packages/bigframes/bigframes/core/compile/sqlglot/expression_compiler.py index e7b492c9fb92..7bba5cbce2b1 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/expression_compiler.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/expression_compiler.py @@ -90,9 +90,10 @@ def _(self, expr: agg_exprs.WindowExpression) -> sge.Expression: @compile_expression.register def _(self, expr: ex.OpExpression) -> sge.Expression: - # Non-recursively compiles the children scalar expressions. inputs = tuple( TypedExpr(self.compile_expression(sub_expr), sub_expr.output_type) + if not isinstance(sub_expr, ex.Omitted) + else TypedExpr(sge.Null, None, is_omitted=True) for sub_expr in expr.inputs ) return self.compile_row_op(expr.op, inputs) diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/expressions/generic_ops.py b/packages/bigframes/bigframes/core/compile/sqlglot/expressions/generic_ops.py index 637c18074eef..ac3c8951f81f 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/expressions/generic_ops.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/expressions/generic_ops.py @@ -20,6 +20,7 @@ import bigframes.core.compile.sqlglot.expression_compiler as expression_compiler from bigframes import dtypes from bigframes import operations as ops +from bigframes.operations.googlesql import CallingConvention from bigframes.core.compile.sqlglot import sql, sqlglot_types from bigframes.core.compile.sqlglot.expressions.typed_expr import TypedExpr @@ -82,6 +83,35 @@ def _(expr: TypedExpr) -> sge.Expression: return sge.BitwiseNot(this=sge.paren(expr.expr)) +@register_nary_op(ops.GoogleSqlScalarOp, pass_op=True) +def _(*operands: TypedExpr, op: ops.GoogleSqlScalarOp) -> sge.Expression: + arg_templates = [] + if op.calling_convention == CallingConvention.FUNCTION: + for i, operand in enumerate(operands): + if i < len(op.args): + arg_spec = op.args[i] + else: + assert op.args[-1].is_vararg, f"Too many arguments, for {op.sql_name}, expected {len(op.args)}" + arg_spec = op.args[-1] + if operand.is_omitted: + assert arg_spec.optional, f"Argument omitted, but not optional" + continue + elif arg_spec.arg_name: + arg_templates.append(f"{arg_spec.arg_name} => {operand.expr.sql(dialect='bigquery')}") + else: + arg_templates.append(operand.expr.sql(dialect='bigquery')) + args_template = ", ".join(arg_templates) + return sg.parse_one(f"{op.sql_name}({args_template})", dialect="bigquery") + elif op.calling_convention == CallingConvention.PREFIX: + assert len(operands) == 1, "prefix op expects exactly 1 arg" + return sg.parse_one(f"{op.sql_name} {operands[0].expr.sql(dialect='bigquery')}", dialect="bigquery") + elif op.calling_convention == CallingConvention.INFIX: + assert len(operands) == 2, 'infix op expects exactly 2 args' + return sg.parse_one(f"{operands[0].expr.sql(dialect='bigquery')} {op.sql_name} {operands[1].expr.sql(dialect='bigquery')}", dialect="bigquery") + + raise NotImplementedError(f"Calling convention {op.calling_convention} not supported for {op}") + + @register_nary_op(ops.SqlScalarOp, pass_op=True) def _(*operands: TypedExpr, op: ops.SqlScalarOp) -> sge.Expression: return sg.parse_one( diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/expressions/typed_expr.py b/packages/bigframes/bigframes/core/compile/sqlglot/expressions/typed_expr.py index 4623b8c9b43b..d8c38c2e7188 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/expressions/typed_expr.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/expressions/typed_expr.py @@ -25,3 +25,6 @@ class TypedExpr: expr: sge.Expression dtype: dtypes.ExpressionType + + # kludge to support optional args in argument lists + is_omitted: bool = False diff --git a/packages/bigframes/bigframes/core/expression.py b/packages/bigframes/bigframes/core/expression.py index 3ad5a9308185..ce2b9d9cebe7 100644 --- a/packages/bigframes/bigframes/core/expression.py +++ b/packages/bigframes/bigframes/core/expression.py @@ -364,6 +364,56 @@ def output_type(self) -> dtypes.ExpressionType: return self.dtype +@dataclasses.dataclass(frozen=True) +class Omitted(Expression): + """Represents an omitted optional arg used calling a function.""" + + @property + def free_variables(self) -> typing.Tuple[Hashable, ...]: + return () + + @property + def is_const(self) -> bool: + return True + + @property + def column_references(self) -> typing.Tuple[ids.ColumnId, ...]: + return () + + @property + def is_resolved(self): + return True # vacuously + + @property + def output_type(self) -> dtypes.ExpressionType: + return None + + def bind_refs( + self, + bindings: Mapping[ids.ColumnId, Expression], + allow_partial_bindings: bool = False, + ) -> UnboundVariableExpression: + return self + + def bind_variables( + self, + bindings: Mapping[Hashable, Expression], + allow_partial_bindings: bool = False, + ) -> Expression: + return self + + @property + def is_bijective(self) -> bool: + return True + + @property + def is_identity(self) -> bool: + return True + + def transform_children(self, t: Callable[[Expression], Expression]) -> Expression: + return self + + @dataclasses.dataclass(frozen=True) class OpExpression(Expression): """An expression representing a scalar operation applied to 1 or more argument sub-expressions.""" diff --git a/packages/bigframes/bigframes/operations/__init__.py b/packages/bigframes/bigframes/operations/__init__.py index bcc21e18cce0..164afb9a15cc 100644 --- a/packages/bigframes/bigframes/operations/__init__.py +++ b/packages/bigframes/bigframes/operations/__init__.py @@ -231,6 +231,7 @@ timestamp_add_op, timestamp_sub_op, ) +from bigframes.operations.googlesql import GoogleSqlScalarOp __all__ = [ # Base ops @@ -446,4 +447,6 @@ "ToArrayOp", "ArrayReduceOp", "ArrayMapOp", + # GoogleSql + "GoogleSqlScalarOp", ] diff --git a/packages/bigframes/bigframes/operations/ai_ops.py b/packages/bigframes/bigframes/operations/ai_ops.py index 0d9438741f46..b3b5f139112d 100644 --- a/packages/bigframes/bigframes/operations/ai_ops.py +++ b/packages/bigframes/bigframes/operations/ai_ops.py @@ -15,7 +15,8 @@ from __future__ import annotations import dataclasses -from typing import ClassVar, Literal, Tuple +import typing +from typing import Literal, Tuple import pandas as pd import pyarrow as pa @@ -26,7 +27,7 @@ @dataclasses.dataclass(frozen=True) class AIGenerate(base_ops.NaryOp): - name: ClassVar[str] = "ai_generate" + name: typing.ClassVar[str] = "ai_generate" prompt_context: Tuple[str | None, ...] connection_id: str | None @@ -54,7 +55,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIGenerateBool(base_ops.NaryOp): - name: ClassVar[str] = "ai_generate_bool" + name: typing.ClassVar[str] = "ai_generate_bool" prompt_context: Tuple[str | None, ...] connection_id: str | None @@ -76,7 +77,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIGenerateInt(base_ops.NaryOp): - name: ClassVar[str] = "ai_generate_int" + name: typing.ClassVar[str] = "ai_generate_int" prompt_context: Tuple[str | None, ...] connection_id: str | None @@ -98,7 +99,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIGenerateDouble(base_ops.NaryOp): - name: ClassVar[str] = "ai_generate_double" + name: typing.ClassVar[str] = "ai_generate_double" prompt_context: Tuple[str | None, ...] connection_id: str | None @@ -120,7 +121,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIEmbed(base_ops.UnaryOp): - name: ClassVar[str] = "ai_embed" + name: typing.ClassVar[str] = "ai_embed" endpoint: str | None model: str | None @@ -142,7 +143,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIIf(base_ops.NaryOp): - name: ClassVar[str] = "ai_if" + name: typing.ClassVar[str] = "ai_if" prompt_context: Tuple[str | None, ...] connection_id: str | None @@ -156,7 +157,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIClassify(base_ops.NaryOp): - name: ClassVar[str] = "ai_classify" + name: typing.ClassVar[str] = "ai_classify" prompt_context: Tuple[str | None, ...] categories: tuple[str, ...] @@ -172,7 +173,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AIScore(base_ops.NaryOp): - name: ClassVar[str] = "ai_score" + name: typing.ClassVar[str] = "ai_score" prompt_context: Tuple[str | None, ...] connection_id: str | None @@ -185,7 +186,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT @dataclasses.dataclass(frozen=True) class AISimilarity(base_ops.BinaryOp): - name: ClassVar[str] = "ai_similarity" + name: typing.ClassVar[str] = "ai_similarity" endpoint: str | None model: str | None diff --git a/packages/bigframes/bigframes/operations/googlesql.py b/packages/bigframes/bigframes/operations/googlesql.py new file mode 100644 index 000000000000..3e1e3ea2bc81 --- /dev/null +++ b/packages/bigframes/bigframes/operations/googlesql.py @@ -0,0 +1,118 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import dataclasses +import typing +from enum import Enum, auto +from typing import Callable, Iterable + +import bigframes.operations as ops +import bigframes.operations.type as op_typing +from bigframes import dtypes + + +class CallingConvention(Enum): + FUNCTION = auto() # standard: name(arg1, arg2) + INFIX = auto() # operator: arg1 name arg2 (e.g., +) + PREFIX = auto() # operator: name arg1 (e.g., NOT) + SPECIAL = auto() # Custom compilation template (e.g., CAST, EXTRACT) + + +@dataclasses.dataclass(frozen=True) +class ArgSpec: + arg_name: str | None = None + optional: bool = False + is_vararg: bool = False + const_only: bool = False + + +@dataclasses.dataclass(frozen=True) +class OpSignature: + # Detailed specs for each parameter. This is particularly relevant for ren + arg_specs: typing.Sequence[ArgSpec] + resolve_return_type: typing.Any + has_varargs: bool = False + + +# Eventually we should migrate every op over to this that can be directly emitted 1:1 as a sql op +# This will allow us to fully lower to pure SQL dialect expressions and emitting sql text is trivial. +@dataclasses.dataclass(frozen=True) +class GoogleSqlScalarOp(ops.NaryOp): + name: typing.ClassVar[str] = "googlesql_scalar" + + # syntax + sql_name: str # for function `sql_name`(a, b), for infix a `sql_name`` b, for prefix `sql_name` a + args: tuple[ArgSpec, ...] + # typing + signature: typing.Callable[..., dtypes.ExpressionType] + # syntax again + calling_convention: CallingConvention = CallingConvention.FUNCTION + + # semantics + is_deterministic: bool = True + + @property + def deterministic(self) -> bool: + return self.is_deterministic + + def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType: + return self.signature(*input_types) + + +RAND = GoogleSqlScalarOp( + "RAND", args=(), is_deterministic=False, signature=lambda: dtypes.FLOAT_DTYPE +) + + +def _check_geo_input( + t: dtypes.ExpressionType, out: dtypes.ExpressionType +) -> dtypes.ExpressionType: + if t is not None and not dtypes.is_geo_like(t): + raise TypeError(f"Type {t} is not supported. Type must be geo-like") + return out + + +def _check_simplify_inputs( + geo: dtypes.ExpressionType, tol: dtypes.ExpressionType +) -> dtypes.ExpressionType: + if geo is not None and not dtypes.is_geo_like(geo): + raise TypeError(f"Type {geo} is not supported. Type must be geo-like") + if tol is not None and not dtypes.is_numeric(tol): + raise TypeError(f"Type {tol} is not supported. Type must be numeric") + return dtypes.GEO_DTYPE + + +ST_AREA = GoogleSqlScalarOp( + "ST_AREA", + args=(ArgSpec(),), + is_deterministic=True, + signature=lambda geo: _check_geo_input(geo, dtypes.FLOAT_DTYPE), +) + +ST_CENTROID = GoogleSqlScalarOp( + "ST_CENTROID", + args=(ArgSpec(),), + is_deterministic=True, + signature=lambda geo: _check_geo_input(geo, dtypes.GEO_DTYPE), +) + +ST_SIMPLIFY = GoogleSqlScalarOp( + "ST_SIMPLIFY", + args=(ArgSpec(), ArgSpec()), + is_deterministic=True, + signature=_check_simplify_inputs, +) diff --git a/packages/bigframes/bigframes/operations/type.py b/packages/bigframes/bigframes/operations/type.py index b53e6cd41ede..0ddf3a113fc0 100644 --- a/packages/bigframes/bigframes/operations/type.py +++ b/packages/bigframes/bigframes/operations/type.py @@ -34,6 +34,9 @@ def as_method(self): """Convert the signature into an object method. Convenience function for constructing ops that use the signature.""" ... + def __call__(self, *args, **kwargs): + return self.as_method(*args, **kwargs) + class UnaryTypeSignature(TypeSignature): @abc.abstractmethod diff --git a/packages/bigframes/tests/unit/bigquery/test_mathematical.py b/packages/bigframes/tests/unit/bigquery/test_mathematical.py index a39aeb103c04..f0cb16ae145f 100644 --- a/packages/bigframes/tests/unit/bigquery/test_mathematical.py +++ b/packages/bigframes/tests/unit/bigquery/test_mathematical.py @@ -26,8 +26,8 @@ def test_rand_returns_expression(): node = expr._value assert isinstance(node, ex.OpExpression) op = node.op - assert isinstance(op, ops.SqlScalarOp) - assert op.sql_template == "RAND()" - assert op._output_type == dtypes.FLOAT_DTYPE + assert isinstance(op, ops.GoogleSqlScalarOp) + assert op.sql_name == "RAND" + assert op.output_type() == dtypes.FLOAT_DTYPE assert not op.is_deterministic assert len(node.inputs) == 0 diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/snapshots/test_compile_geo/test_st_simplify/out.sql b/packages/bigframes/tests/unit/core/compile/sqlglot/snapshots/test_compile_geo/test_st_simplify/out.sql index 1c146e1e1be5..177cb5292b3f 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/snapshots/test_compile_geo/test_st_simplify/out.sql +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/snapshots/test_compile_geo/test_st_simplify/out.sql @@ -1,7 +1,7 @@ WITH `bfcte_0` AS ( SELECT * - FROM UNNEST(ARRAY>[STRUCT('POINT(1 1)', 0)]) + FROM UNNEST(ARRAY>[STRUCT(ST_GEOGFROMTEXT('LINESTRING(0 0, 1 1, 2 0)'), 0)]) ) SELECT ST_SIMPLIFY(`bfcol_0`, 123.125) AS `0` diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/test_compile_geo.py b/packages/bigframes/tests/unit/core/compile/sqlglot/test_compile_geo.py index 50de1488e6c6..0f603978212e 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/test_compile_geo.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/test_compile_geo.py @@ -16,6 +16,7 @@ import bigframes.bigquery as bbq import bigframes.geopandas as gpd +from shapely.geometry import LineString # type: ignore pytest.importorskip("pytest_snapshot") @@ -44,7 +45,7 @@ def test_st_regionstats_without_optional_args(compiler_session, snapshot): def test_st_simplify(compiler_session, snapshot): - geos = gpd.GeoSeries(["POINT(1 1)"], session=compiler_session) + geos = gpd.GeoSeries([LineString([(0, 0), (1, 1), (2, 0)])], session=compiler_session) result = bbq.st_simplify( geos, tolerance_meters=123.125, diff --git a/packages/bigframes/third_party/bigframes_vendored/ibis/expr/api.py b/packages/bigframes/third_party/bigframes_vendored/ibis/expr/api.py index af85e937d561..953ecb2979fa 100644 --- a/packages/bigframes/third_party/bigframes_vendored/ibis/expr/api.py +++ b/packages/bigframes/third_party/bigframes_vendored/ibis/expr/api.py @@ -2467,3 +2467,7 @@ def least(*args: Any) -> ir.Value: └────────────┘ """ return ops.Least(args).to_expr() + + +def omitted() -> ir.Value: + return ops.Omitted().to_expr() diff --git a/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/core.py b/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/core.py index 5ad1c885a4a2..ad0bd095b6c7 100644 --- a/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/core.py +++ b/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/core.py @@ -136,6 +136,10 @@ def to_expr(self): return getattr(ir, typename)(self) + @property + def omitted(self) -> bool: + return False + # convenience aliases Scalar = Value[T, ds.Scalar] diff --git a/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/generic.py b/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/generic.py index 3933a70a9630..6406c55e3e3d 100644 --- a/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/generic.py +++ b/packages/bigframes/third_party/bigframes_vendored/ibis/expr/operations/generic.py @@ -188,6 +188,13 @@ class Impure(Value): pass +@public +class Omitted(Value): + @property + def omitted(self) -> bool: + return True + + @public class TimestampNow(Constant): """Return the current timestamp."""