From b00ac3d9af68d65ad50131957241efe18734d269 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 16 Jun 2026 19:23:30 +0000 Subject: [PATCH 1/8] fix: avoid invalid CAST(NULL AS NULL) in SQLGlot compiler --- .../bigframes/core/compile/sqlglot/sql/base.py | 2 ++ .../tests/unit/core/compile/sqlglot/sql/test_base.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py index 8b5eb748f575..a2218e948205 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py @@ -69,6 +69,8 @@ def literal(value: typing.Any, dtype: dtypes.Dtype | None = None) -> sge.Express return sge.Null() if value is None: + if sqlglot_type.upper() == "NULL": + return sge.Null() return cast(sge.Null(), sqlglot_type) if dtypes.is_struct_like(dtype): items = [ diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 5ba77d925d0f..617f3636d403 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -159,3 +159,15 @@ def test_literal_explicit_dtype(value, dtype, expected): def test_literal_for_list(value: list, expected: str): got = sql.to_sql(sql.literal(value)) assert got == expected + + +def test_literal_null_type(): + import unittest.mock as mock + + mock_dtype = mock.Mock() + with mock.patch( + "bigframes.core.compile.sqlglot.sql.base.sgt.from_bigframes_dtype", + return_value="NULL", + ): + got = sql.to_sql(sql.literal(None, dtype=mock_dtype)) + assert got == "NULL" From a42324124698c78465528b1643c8fc863c403687 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Tue, 16 Jun 2026 16:34:11 -0700 Subject: [PATCH 2/8] Update packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py index a2218e948205..f77dcbee4d93 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py @@ -69,7 +69,7 @@ def literal(value: typing.Any, dtype: dtypes.Dtype | None = None) -> sge.Express return sge.Null() if value is None: - if sqlglot_type.upper() == "NULL": + if str(sqlglot_type).upper() == "NULL": return sge.Null() return cast(sge.Null(), sqlglot_type) if dtypes.is_struct_like(dtype): From 9981a5e4c4696a0c812f99a5bbbe20807b878595 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 17 Jun 2026 22:13:55 +0000 Subject: [PATCH 3/8] fix: globally flatten null casts in SQLGlot --- .../bigframes/core/compile/sqlglot/sql/base.py | 15 ++++++++++++--- .../unit/core/compile/sqlglot/sql/test_base.py | 12 ++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py index f77dcbee4d93..4bfe809a3f94 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py @@ -49,6 +49,15 @@ def to_sql(expr: sge.Expression) -> str: """Generate SQL string from the given expression.""" + def _flatten_null_casts(node: sge.Expression) -> sge.Expression: + if ( + isinstance(node, (sge.Cast, sge.TryCast)) + and str(node.to).upper() == "NULL" + ): + return sge.Null() + return node + + expr = expr.transform(_flatten_null_casts) return expr.sql(dialect=DIALECT, pretty=PRETTY) @@ -69,8 +78,6 @@ def literal(value: typing.Any, dtype: dtypes.Dtype | None = None) -> sge.Express return sge.Null() if value is None: - if str(sqlglot_type).upper() == "NULL": - return sge.Null() return cast(sge.Null(), sqlglot_type) if dtypes.is_struct_like(dtype): items = [ @@ -121,8 +128,10 @@ def literal(value: typing.Any, dtype: dtypes.Dtype | None = None) -> sge.Express return sge.convert(value) -def cast(arg: typing.Any, to: str, safe: bool = False) -> sge.Cast | sge.TryCast: +def cast(arg: typing.Any, to: str | sge.DataType, safe: bool = False) -> sge.Expression: """Return a SQL expression that casts the given argument to the specified type.""" + if str(to).upper() == "NULL": + return sge.Null() if safe: return sge.TryCast(this=arg, to=to) else: diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 617f3636d403..918e01bac6cd 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -171,3 +171,15 @@ def test_literal_null_type(): ): got = sql.to_sql(sql.literal(None, dtype=mock_dtype)) assert got == "NULL" + + +def test_cast_to_null_type(): + assert sql.to_sql(sql.cast("abc", "NULL")) == "NULL" + assert sql.to_sql(sql.cast(None, "NULL")) == "NULL" + assert sql.to_sql(sql.cast("abc", "NULL", safe=True)) == "NULL" + + +def test_nested_cast_to_null_type(): + import bigframes_vendored.sqlglot.expressions as sge + nested = sge.Cast(this=sge.Cast(this=sge.Null(), to="NULL"), to="INT64") + assert sql.to_sql(nested) == "CAST(NULL AS INT64)" From d633cf0473326e8a55ba6289d2d6bcebf359840c Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 17 Jun 2026 22:17:26 +0000 Subject: [PATCH 4/8] test: update test case names to follow style guide --- .../tests/unit/core/compile/sqlglot/sql/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 918e01bac6cd..6c58186374f4 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -173,13 +173,13 @@ def test_literal_null_type(): assert got == "NULL" -def test_cast_to_null_type(): +def test_cast_to_null_type_returns_flat_null(): assert sql.to_sql(sql.cast("abc", "NULL")) == "NULL" assert sql.to_sql(sql.cast(None, "NULL")) == "NULL" assert sql.to_sql(sql.cast("abc", "NULL", safe=True)) == "NULL" -def test_nested_cast_to_null_type(): +def test_nested_cast_to_null_type_is_flattened(): import bigframes_vendored.sqlglot.expressions as sge nested = sge.Cast(this=sge.Cast(this=sge.Null(), to="NULL"), to="INT64") assert sql.to_sql(nested) == "CAST(NULL AS INT64)" From f53fda95ba484bb035d5bf8a62a317f0f8b9e5b8 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Wed, 17 Jun 2026 23:28:38 +0000 Subject: [PATCH 5/8] format file --- .../bigframes/bigframes/core/compile/sqlglot/sql/base.py | 6 ++---- .../tests/unit/core/compile/sqlglot/sql/test_base.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py index 4bfe809a3f94..7af4d07de4f8 100644 --- a/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py +++ b/packages/bigframes/bigframes/core/compile/sqlglot/sql/base.py @@ -49,11 +49,9 @@ def to_sql(expr: sge.Expression) -> str: """Generate SQL string from the given expression.""" + def _flatten_null_casts(node: sge.Expression) -> sge.Expression: - if ( - isinstance(node, (sge.Cast, sge.TryCast)) - and str(node.to).upper() == "NULL" - ): + if isinstance(node, (sge.Cast, sge.TryCast)) and str(node.to).upper() == "NULL": return sge.Null() return node diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 6c58186374f4..8ed845ab3e0f 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -181,5 +181,6 @@ def test_cast_to_null_type_returns_flat_null(): def test_nested_cast_to_null_type_is_flattened(): import bigframes_vendored.sqlglot.expressions as sge + nested = sge.Cast(this=sge.Cast(this=sge.Null(), to="NULL"), to="INT64") assert sql.to_sql(nested) == "CAST(NULL AS INT64)" From 44e550509b50a22aff232c03c05748cb4dcafef5 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 18 Jun 2026 01:32:30 +0000 Subject: [PATCH 6/8] format --- .../bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 57af152b40af..8ed845ab3e0f 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -173,7 +173,6 @@ def test_literal_null_type(): assert got == "NULL" - def test_cast_to_null_type_returns_flat_null(): assert sql.to_sql(sql.cast("abc", "NULL")) == "NULL" assert sql.to_sql(sql.cast(None, "NULL")) == "NULL" From 65db01b3f4a9f4a3cf21c6ad6c5e3119c593aa2f Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 18 Jun 2026 01:33:43 +0000 Subject: [PATCH 7/8] refactor null cast tests for style compliance --- .../unit/core/compile/sqlglot/sql/test_base.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 8ed845ab3e0f..94c799967ad1 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -173,14 +173,21 @@ def test_literal_null_type(): assert got == "NULL" -def test_cast_to_null_type_returns_flat_null(): - assert sql.to_sql(sql.cast("abc", "NULL")) == "NULL" - assert sql.to_sql(sql.cast(None, "NULL")) == "NULL" - assert sql.to_sql(sql.cast("abc", "NULL", safe=True)) == "NULL" +@pytest.mark.parametrize( + ("arg", "safe"), + ( + pytest.param("abc", False, id="string"), + pytest.param(None, False, id="none"), + pytest.param("abc", True, id="safe_cast"), + ), +) +def test_cast_to_null_type_returns_flat_null(arg, safe): + assert sql.to_sql(sql.cast(arg, "NULL", safe=safe)) == "NULL" def test_nested_cast_to_null_type_is_flattened(): import bigframes_vendored.sqlglot.expressions as sge nested = sge.Cast(this=sge.Cast(this=sge.Null(), to="NULL"), to="INT64") + assert sql.to_sql(nested) == "CAST(NULL AS INT64)" From 7d4d03ba22056c239208a2d23f52f46d2e2d5a01 Mon Sep 17 00:00:00 2001 From: Shuowei Li Date: Thu, 18 Jun 2026 01:35:24 +0000 Subject: [PATCH 8/8] move local imports in test_base.py to top level --- .../tests/unit/core/compile/sqlglot/sql/test_base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py index 94c799967ad1..edbb77b32c62 100644 --- a/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py +++ b/packages/bigframes/tests/unit/core/compile/sqlglot/sql/test_base.py @@ -15,7 +15,9 @@ import datetime import decimal import re +import unittest.mock as mock +import bigframes_vendored.sqlglot.expressions as sge import numpy as np import pandas as pd import pyarrow as pa @@ -162,8 +164,6 @@ def test_literal_for_list(value: list, expected: str): def test_literal_null_type(): - import unittest.mock as mock - mock_dtype = mock.Mock() with mock.patch( "bigframes.core.compile.sqlglot.sql.base.sgt.from_bigframes_dtype", @@ -186,8 +186,6 @@ def test_cast_to_null_type_returns_flat_null(arg, safe): def test_nested_cast_to_null_type_is_flattened(): - import bigframes_vendored.sqlglot.expressions as sge - nested = sge.Cast(this=sge.Cast(this=sge.Null(), to="NULL"), to="INT64") assert sql.to_sql(nested) == "CAST(NULL AS INT64)"