From 5933fc3cef5f55d33eb8295b9c26fa91af2199af Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Wed, 13 Aug 2025 20:59:17 +0000 Subject: [PATCH 1/2] chore: implement mul_op and div_op compilers --- .../sqlglot/expressions/binary_compiler.py | 55 +++++++++++++++---- .../system/small/engines/test_numeric_ops.py | 6 +- .../test_div_numeric/out.sql | 54 ++++++++++++++++++ .../test_div_timedelta/out.sql | 18 ++++++ .../test_mul_numeric/out.sql | 54 ++++++++++++++++++ .../test_mul_timedelta/out.sql | 24 ++++++++ .../expressions/test_binary_compiler.py | 43 +++++++++++++++ 7 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_numeric/out.sql create mode 100644 tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql create mode 100644 tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_numeric/out.sql create mode 100644 tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql diff --git a/bigframes/core/compile/sqlglot/expressions/binary_compiler.py b/bigframes/core/compile/sqlglot/expressions/binary_compiler.py index c46019d909..ff3833a4d5 100644 --- a/bigframes/core/compile/sqlglot/expressions/binary_compiler.py +++ b/bigframes/core/compile/sqlglot/expressions/binary_compiler.py @@ -73,6 +73,51 @@ def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: ) +@BINARY_OP_REGISTRATION.register(ops.div_op) +def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: + left_expr = left.expr + if left.dtype == dtypes.BOOL_DTYPE: + left_expr = sge.Cast(this=left_expr, to="INT64") + right_expr = right.expr + if right.dtype == dtypes.BOOL_DTYPE: + right_expr = sge.Cast(this=right_expr, to="INT64") + + result = sge.func("IEEE_DIVIDE", left_expr, right_expr) + if dtypes.is_numeric(right.dtype) and left.dtype == dtypes.TIMEDELTA_DTYPE: + return sge.Cast(this=sge.Floor(this=result), to="INT64") + else: + return result + + +@BINARY_OP_REGISTRATION.register(ops.ge_op) +def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: + return sge.GTE(this=left.expr, expression=right.expr) + + +@BINARY_OP_REGISTRATION.register(ops.JSONSet) +def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: + return sge.func("JSON_SET", left.expr, sge.convert(op.json_path), right.expr) + + +@BINARY_OP_REGISTRATION.register(ops.mul_op) +def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: + left_expr = left.expr + if left.dtype == dtypes.BOOL_DTYPE: + left_expr = sge.Cast(this=left_expr, to="INT64") + right_expr = right.expr + if right.dtype == dtypes.BOOL_DTYPE: + right_expr = sge.Cast(this=right_expr, to="INT64") + + result = sge.Mul(this=left_expr, expression=right_expr) + + if (dtypes.is_numeric(left.dtype) and right.dtype == dtypes.TIMEDELTA_DTYPE) or ( + dtypes.is_numeric(right.dtype) and left.dtype == dtypes.TIMEDELTA_DTYPE + ): + return sge.Cast(this=sge.Floor(this=result), to="INT64") + else: + return result + + @BINARY_OP_REGISTRATION.register(ops.sub_op) def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: if dtypes.is_numeric(left.dtype) and dtypes.is_numeric(right.dtype): @@ -113,13 +158,3 @@ def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: raise TypeError( f"Cannot subtract type {left.dtype} and {right.dtype}. {constants.FEEDBACK_LINK}" ) - - -@BINARY_OP_REGISTRATION.register(ops.ge_op) -def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: - return sge.GTE(this=left.expr, expression=right.expr) - - -@BINARY_OP_REGISTRATION.register(ops.JSONSet) -def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: - return sge.func("JSON_SET", left.expr, sge.convert(op.json_path), right.expr) diff --git a/tests/system/small/engines/test_numeric_ops.py b/tests/system/small/engines/test_numeric_ops.py index 7e5b85857b..b46a2f1c56 100644 --- a/tests/system/small/engines/test_numeric_ops.py +++ b/tests/system/small/engines/test_numeric_ops.py @@ -71,7 +71,7 @@ def test_engines_project_sub( assert_equivalence_execution(arr.node, REFERENCE_ENGINE, engine) -@pytest.mark.parametrize("engine", ["polars", "bq"], indirect=True) +@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True) def test_engines_project_mul( scalars_array_value: array_value.ArrayValue, engine, @@ -80,7 +80,7 @@ def test_engines_project_mul( assert_equivalence_execution(arr.node, REFERENCE_ENGINE, engine) -@pytest.mark.parametrize("engine", ["polars", "bq"], indirect=True) +@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True) def test_engines_project_div(scalars_array_value: array_value.ArrayValue, engine): # TODO: Duration div is sensitive to zeroes # TODO: Numeric col is sensitive to scale shifts @@ -90,7 +90,7 @@ def test_engines_project_div(scalars_array_value: array_value.ArrayValue, engine assert_equivalence_execution(arr.node, REFERENCE_ENGINE, engine) -@pytest.mark.parametrize("engine", ["polars", "bq"], indirect=True) +@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True) def test_engines_project_div_durations( scalars_array_value: array_value.ArrayValue, engine ): diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_numeric/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_numeric/out.sql new file mode 100644 index 0000000000..c1f4e0cb69 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_numeric/out.sql @@ -0,0 +1,54 @@ +WITH `bfcte_0` AS ( + SELECT + `bool_col` AS `bfcol_0`, + `int64_col` AS `bfcol_1`, + `rowindex` AS `bfcol_2` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` +), `bfcte_1` AS ( + SELECT + *, + `bfcol_2` AS `bfcol_6`, + `bfcol_1` AS `bfcol_7`, + `bfcol_0` AS `bfcol_8`, + IEEE_DIVIDE(`bfcol_1`, `bfcol_1`) AS `bfcol_9` + FROM `bfcte_0` +), `bfcte_2` AS ( + SELECT + *, + `bfcol_6` AS `bfcol_14`, + `bfcol_7` AS `bfcol_15`, + `bfcol_8` AS `bfcol_16`, + `bfcol_9` AS `bfcol_17`, + IEEE_DIVIDE(`bfcol_7`, 1) AS `bfcol_18` + FROM `bfcte_1` +), `bfcte_3` AS ( + SELECT + *, + `bfcol_14` AS `bfcol_24`, + `bfcol_15` AS `bfcol_25`, + `bfcol_16` AS `bfcol_26`, + `bfcol_17` AS `bfcol_27`, + `bfcol_18` AS `bfcol_28`, + IEEE_DIVIDE(`bfcol_15`, CAST(`bfcol_16` AS INT64)) AS `bfcol_29` + FROM `bfcte_2` +), `bfcte_4` AS ( + SELECT + *, + `bfcol_24` AS `bfcol_36`, + `bfcol_25` AS `bfcol_37`, + `bfcol_26` AS `bfcol_38`, + `bfcol_27` AS `bfcol_39`, + `bfcol_28` AS `bfcol_40`, + `bfcol_29` AS `bfcol_41`, + IEEE_DIVIDE(CAST(`bfcol_26` AS INT64), `bfcol_25`) AS `bfcol_42` + FROM `bfcte_3` +) +SELECT + `bfcol_36` AS `rowindex`, + `bfcol_37` AS `int64_col`, + `bfcol_38` AS `bool_col`, + `bfcol_39` AS `int_div_int`, + `bfcol_40` AS `int_div_1`, + `bfcol_41` AS `int_div_bool`, + `bfcol_42` AS `bool_div_int` +FROM `bfcte_4` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql new file mode 100644 index 0000000000..bc4f94d306 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql @@ -0,0 +1,18 @@ +WITH `bfcte_0` AS ( + SELECT + `date_col` AS `bfcol_0`, + `rowindex` AS `bfcol_1`, + `timestamp_col` AS `bfcol_2` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` +), `bfcte_1` AS ( + SELECT + *, + 43200000000 AS `bfcol_6` + FROM `bfcte_0` +) +SELECT + `bfcol_1` AS `rowindex`, + `bfcol_2` AS `timestamp_col`, + `bfcol_0` AS `date_col`, + `bfcol_6` AS `timedelta_div_numeric` +FROM `bfcte_1` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_numeric/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_numeric/out.sql new file mode 100644 index 0000000000..a9c81f4744 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_numeric/out.sql @@ -0,0 +1,54 @@ +WITH `bfcte_0` AS ( + SELECT + `bool_col` AS `bfcol_0`, + `int64_col` AS `bfcol_1`, + `rowindex` AS `bfcol_2` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` +), `bfcte_1` AS ( + SELECT + *, + `bfcol_2` AS `bfcol_6`, + `bfcol_1` AS `bfcol_7`, + `bfcol_0` AS `bfcol_8`, + `bfcol_1` * `bfcol_1` AS `bfcol_9` + FROM `bfcte_0` +), `bfcte_2` AS ( + SELECT + *, + `bfcol_6` AS `bfcol_14`, + `bfcol_7` AS `bfcol_15`, + `bfcol_8` AS `bfcol_16`, + `bfcol_9` AS `bfcol_17`, + `bfcol_7` * 1 AS `bfcol_18` + FROM `bfcte_1` +), `bfcte_3` AS ( + SELECT + *, + `bfcol_14` AS `bfcol_24`, + `bfcol_15` AS `bfcol_25`, + `bfcol_16` AS `bfcol_26`, + `bfcol_17` AS `bfcol_27`, + `bfcol_18` AS `bfcol_28`, + `bfcol_15` * CAST(`bfcol_16` AS INT64) AS `bfcol_29` + FROM `bfcte_2` +), `bfcte_4` AS ( + SELECT + *, + `bfcol_24` AS `bfcol_36`, + `bfcol_25` AS `bfcol_37`, + `bfcol_26` AS `bfcol_38`, + `bfcol_27` AS `bfcol_39`, + `bfcol_28` AS `bfcol_40`, + `bfcol_29` AS `bfcol_41`, + CAST(`bfcol_26` AS INT64) * `bfcol_25` AS `bfcol_42` + FROM `bfcte_3` +) +SELECT + `bfcol_36` AS `rowindex`, + `bfcol_37` AS `int64_col`, + `bfcol_38` AS `bool_col`, + `bfcol_39` AS `int_mul_int`, + `bfcol_40` AS `int_mul_1`, + `bfcol_41` AS `int_mul_bool`, + `bfcol_42` AS `bool_mul_int` +FROM `bfcte_4` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql new file mode 100644 index 0000000000..33db921e78 --- /dev/null +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql @@ -0,0 +1,24 @@ +WITH `bfcte_0` AS ( + SELECT + `date_col` AS `bfcol_0`, + `rowindex` AS `bfcol_1`, + `timestamp_col` AS `bfcol_2` + FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` +), `bfcte_1` AS ( + SELECT + *, + 172800000000 AS `bfcol_6` + FROM `bfcte_0` +), `bfcte_2` AS ( + SELECT + *, + 172800000000 AS `bfcol_7` + FROM `bfcte_1` +) +SELECT + `bfcol_1` AS `rowindex`, + `bfcol_2` AS `timestamp_col`, + `bfcol_0` AS `date_col`, + `bfcol_6` AS `timedelta_mul_numeric`, + `bfcol_7` AS `numeric_mul_timedelta` +FROM `bfcte_2` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py b/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py index 05d9c26945..adc5f9c9d8 100644 --- a/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py +++ b/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py @@ -82,6 +82,27 @@ def test_add_unsupported_raises(scalar_types_df: bpd.DataFrame): _apply_binary_op(scalar_types_df, ops.add_op, "int64_col", "string_col") +def test_div_numeric(scalar_types_df: bpd.DataFrame, snapshot): + bf_df = scalar_types_df[["int64_col", "bool_col"]] + + bf_df["int_div_int"] = bf_df["int64_col"] / bf_df["int64_col"] + bf_df["int_div_1"] = bf_df["int64_col"] / 1 + + bf_df["int_div_bool"] = bf_df["int64_col"] / bf_df["bool_col"] + bf_df["bool_div_int"] = bf_df["bool_col"] / bf_df["int64_col"] + + snapshot.assert_match(bf_df.sql, "out.sql") + + +def test_div_timedelta(scalar_types_df: bpd.DataFrame, snapshot): + bf_df = scalar_types_df[["timestamp_col", "date_col"]] + timedelta = pd.Timedelta(1, unit="d") + + bf_df["timedelta_div_numeric"] = timedelta / 2 + + snapshot.assert_match(bf_df.sql, "out.sql") + + def test_json_set(json_types_df: bpd.DataFrame, snapshot): bf_df = json_types_df[["json_col"]] sql = _apply_binary_op( @@ -122,3 +143,25 @@ def test_sub_unsupported_raises(scalar_types_df: bpd.DataFrame): with pytest.raises(TypeError): _apply_binary_op(scalar_types_df, ops.sub_op, "int64_col", "string_col") + + +def test_mul_numeric(scalar_types_df: bpd.DataFrame, snapshot): + bf_df = scalar_types_df[["int64_col", "bool_col"]] + + bf_df["int_mul_int"] = bf_df["int64_col"] * bf_df["int64_col"] + bf_df["int_mul_1"] = bf_df["int64_col"] * 1 + + bf_df["int_mul_bool"] = bf_df["int64_col"] * bf_df["bool_col"] + bf_df["bool_mul_int"] = bf_df["bool_col"] * bf_df["int64_col"] + + snapshot.assert_match(bf_df.sql, "out.sql") + + +def test_mul_timedelta(scalar_types_df: bpd.DataFrame, snapshot): + bf_df = scalar_types_df[["timestamp_col", "date_col"]] + timedelta = pd.Timedelta(1, unit="d") + + bf_df["timedelta_mul_numeric"] = timedelta * 2 + bf_df["numeric_mul_timedelta"] = 2 * timedelta + + snapshot.assert_match(bf_df.sql, "out.sql") From dc1344739970aeadf37304b1bd9917b3ffb34c55 Mon Sep 17 00:00:00 2001 From: Chelsea Lin Date: Wed, 13 Aug 2025 23:53:42 +0000 Subject: [PATCH 2/2] address comments --- .../sqlglot/expressions/binary_compiler.py | 4 ++-- .../test_div_timedelta/out.sql | 15 +++++++----- .../test_mul_timedelta/out.sql | 23 ++++++++++++------- .../expressions/test_binary_compiler.py | 11 ++++----- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/bigframes/core/compile/sqlglot/expressions/binary_compiler.py b/bigframes/core/compile/sqlglot/expressions/binary_compiler.py index ff3833a4d5..fa640ee0b2 100644 --- a/bigframes/core/compile/sqlglot/expressions/binary_compiler.py +++ b/bigframes/core/compile/sqlglot/expressions/binary_compiler.py @@ -83,7 +83,7 @@ def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: right_expr = sge.Cast(this=right_expr, to="INT64") result = sge.func("IEEE_DIVIDE", left_expr, right_expr) - if dtypes.is_numeric(right.dtype) and left.dtype == dtypes.TIMEDELTA_DTYPE: + if left.dtype == dtypes.TIMEDELTA_DTYPE and dtypes.is_numeric(right.dtype): return sge.Cast(this=sge.Floor(this=result), to="INT64") else: return result @@ -111,7 +111,7 @@ def _(op, left: TypedExpr, right: TypedExpr) -> sge.Expression: result = sge.Mul(this=left_expr, expression=right_expr) if (dtypes.is_numeric(left.dtype) and right.dtype == dtypes.TIMEDELTA_DTYPE) or ( - dtypes.is_numeric(right.dtype) and left.dtype == dtypes.TIMEDELTA_DTYPE + left.dtype == dtypes.TIMEDELTA_DTYPE and dtypes.is_numeric(right.dtype) ): return sge.Cast(this=sge.Floor(this=result), to="INT64") else: diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql index bc4f94d306..6e05302fc9 100644 --- a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_div_timedelta/out.sql @@ -1,18 +1,21 @@ WITH `bfcte_0` AS ( SELECT - `date_col` AS `bfcol_0`, + `int64_col` AS `bfcol_0`, `rowindex` AS `bfcol_1`, `timestamp_col` AS `bfcol_2` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` ), `bfcte_1` AS ( SELECT *, - 43200000000 AS `bfcol_6` + `bfcol_1` AS `bfcol_6`, + `bfcol_2` AS `bfcol_7`, + `bfcol_0` AS `bfcol_8`, + CAST(FLOOR(IEEE_DIVIDE(86400000000, `bfcol_0`)) AS INT64) AS `bfcol_9` FROM `bfcte_0` ) SELECT - `bfcol_1` AS `rowindex`, - `bfcol_2` AS `timestamp_col`, - `bfcol_0` AS `date_col`, - `bfcol_6` AS `timedelta_div_numeric` + `bfcol_6` AS `rowindex`, + `bfcol_7` AS `timestamp_col`, + `bfcol_8` AS `int64_col`, + `bfcol_9` AS `timedelta_div_numeric` FROM `bfcte_1` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql index 33db921e78..082a714845 100644 --- a/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql +++ b/tests/unit/core/compile/sqlglot/expressions/snapshots/test_binary_compiler/test_mul_timedelta/out.sql @@ -1,24 +1,31 @@ WITH `bfcte_0` AS ( SELECT - `date_col` AS `bfcol_0`, + `int64_col` AS `bfcol_0`, `rowindex` AS `bfcol_1`, `timestamp_col` AS `bfcol_2` FROM `bigframes-dev`.`sqlglot_test`.`scalar_types` ), `bfcte_1` AS ( SELECT *, - 172800000000 AS `bfcol_6` + `bfcol_1` AS `bfcol_6`, + `bfcol_2` AS `bfcol_7`, + `bfcol_0` AS `bfcol_8`, + CAST(FLOOR(86400000000 * `bfcol_0`) AS INT64) AS `bfcol_9` FROM `bfcte_0` ), `bfcte_2` AS ( SELECT *, - 172800000000 AS `bfcol_7` + `bfcol_6` AS `bfcol_14`, + `bfcol_7` AS `bfcol_15`, + `bfcol_8` AS `bfcol_16`, + `bfcol_9` AS `bfcol_17`, + CAST(FLOOR(`bfcol_8` * 86400000000) AS INT64) AS `bfcol_18` FROM `bfcte_1` ) SELECT - `bfcol_1` AS `rowindex`, - `bfcol_2` AS `timestamp_col`, - `bfcol_0` AS `date_col`, - `bfcol_6` AS `timedelta_mul_numeric`, - `bfcol_7` AS `numeric_mul_timedelta` + `bfcol_14` AS `rowindex`, + `bfcol_15` AS `timestamp_col`, + `bfcol_16` AS `int64_col`, + `bfcol_17` AS `timedelta_mul_numeric`, + `bfcol_18` AS `numeric_mul_timedelta` FROM `bfcte_2` \ No newline at end of file diff --git a/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py b/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py index adc5f9c9d8..6521a92df0 100644 --- a/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py +++ b/tests/unit/core/compile/sqlglot/expressions/test_binary_compiler.py @@ -95,10 +95,9 @@ def test_div_numeric(scalar_types_df: bpd.DataFrame, snapshot): def test_div_timedelta(scalar_types_df: bpd.DataFrame, snapshot): - bf_df = scalar_types_df[["timestamp_col", "date_col"]] + bf_df = scalar_types_df[["timestamp_col", "int64_col"]] timedelta = pd.Timedelta(1, unit="d") - - bf_df["timedelta_div_numeric"] = timedelta / 2 + bf_df["timedelta_div_numeric"] = timedelta / bf_df["int64_col"] snapshot.assert_match(bf_df.sql, "out.sql") @@ -158,10 +157,10 @@ def test_mul_numeric(scalar_types_df: bpd.DataFrame, snapshot): def test_mul_timedelta(scalar_types_df: bpd.DataFrame, snapshot): - bf_df = scalar_types_df[["timestamp_col", "date_col"]] + bf_df = scalar_types_df[["timestamp_col", "int64_col"]] timedelta = pd.Timedelta(1, unit="d") - bf_df["timedelta_mul_numeric"] = timedelta * 2 - bf_df["numeric_mul_timedelta"] = 2 * timedelta + bf_df["timedelta_mul_numeric"] = timedelta * bf_df["int64_col"] + bf_df["numeric_mul_timedelta"] = bf_df["int64_col"] * timedelta snapshot.assert_match(bf_df.sql, "out.sql")