From b5dfa5e10d8f24eda858ee46e4983e2440aada56 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:16:19 +0000 Subject: [PATCH 1/7] Fix unnest breakage for other dialects BigQuery's unnest function implementation overrides and breaks this function for other dialects. This change makes the unnest function dialect-specific to BigQuery to avoid conflicts with other dialects. --- setup.cfg | 8 ++++---- sqlalchemy_bigquery/base.py | 16 +++++++++++++++- tests/unit/test_compiler.py | 30 +++++++++++++++--------------- tests/unit/test_select.py | 4 ++-- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/setup.cfg b/setup.cfg index af3619bb..07e5991b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,10 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -[sqla_testing] -requirement_cls=sqlalchemy_bigquery.requirements:Requirements -profile_file=.sqlalchemy_dialect_compliance-profiles.txt +#[sqla_testing] +#requirement_cls=sqlalchemy_bigquery.requirements:Requirements +#profile_file=.sqlalchemy_dialect_compliance-profiles.txt [tool:pytest] -addopts= --tb native -v -r fxX +#addopts= --tb native -v -r fxX python_files=tests/*test_*.py diff --git a/sqlalchemy_bigquery/base.py b/sqlalchemy_bigquery/base.py index 4008a7e1..51855f2c 100644 --- a/sqlalchemy_bigquery/base.py +++ b/sqlalchemy_bigquery/base.py @@ -1377,7 +1377,12 @@ def get_view_definition(self, connection, view_name, schema=None, **kw): return view.view_query -class unnest(sqlalchemy.sql.functions.GenericFunction): +# unnest is a reserved keyword in some dialects. +# It is defined here to avoid conflicts. +# https://github.com/googleapis/python-bigquery-sqlalchemy/issues/882 +class _unnest(sqlalchemy.sql.expression.FunctionElement): + inherit_cache = True + def __init__(self, *args, **kwargs): expr = kwargs.pop("expr", None) if expr is not None: @@ -1395,9 +1400,18 @@ def __init__(self, *args, **kwargs): ): raise TypeError("The argument to unnest must have an ARRAY type.") self.type = arg.type.item_type + super().__init__(*args, **kwargs) +@compiles(_unnest, "bigquery") +def bigquery_unnest(element, compiler, **kw): + return "UNNEST({})".format(compiler.process(element.clauses, **kw)) + + +sqlalchemy.sql.functions._FunctionGenerator.unnest = _unnest + + dialect = BigQueryDialect try: diff --git a/tests/unit/test_compiler.py b/tests/unit/test_compiler.py index 8325ba97..54132199 100644 --- a/tests/unit/test_compiler.py +++ b/tests/unit/test_compiler.py @@ -93,7 +93,7 @@ def test_no_alias_for_known_tables(faux_conn, metadata): expected_sql = ( "SELECT `table1`.`foo` \n" - "FROM `table1`, unnest(`table1`.`bar`) AS `anon_1` \n" + "FROM `table1`, UNNEST(`table1`.`bar`) AS `anon_1` \n" "WHERE `anon_1` = %(param_1:INT64)s" ) found_sql = q.compile(faux_conn).string @@ -116,7 +116,7 @@ def test_no_alias_for_known_tables_cte(faux_conn, metadata): expected_initial_sql = ( "SELECT `table1`.`foo`, `bar` \n" - "FROM `table1`, unnest(`table1`.`bars`) AS `bar`" + "FROM `table1`, UNNEST(`table1`.`bars`) AS `bar`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -127,7 +127,7 @@ def test_no_alias_for_known_tables_cte(faux_conn, metadata): expected_cte_sql = ( "WITH `cte` AS \n" "(SELECT `table1`.`foo` AS `foo`, `bar` \n" - "FROM `table1`, unnest(`table1`.`bars`) AS `bar`)\n" + "FROM `table1`, UNNEST(`table1`.`bars`) AS `bar`)\n" " SELECT `cte`.`foo`, `cte`.`bar` \n" "FROM `cte`" ) @@ -196,7 +196,7 @@ def test_no_implicit_join_asterix_for_inner_unnest_before_2_0(faux_conn, metadat q = prepare_implicit_join_base_query(faux_conn, metadata, True, False) expected_initial_sql = ( "SELECT `table1`.`foo`, `table2`.`bar` \n" - "FROM `table2`, unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" + "FROM `table2`, UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -207,7 +207,7 @@ def test_no_implicit_join_asterix_for_inner_unnest_before_2_0(faux_conn, metadat expected_outer_sql = ( "SELECT * \n" "FROM (SELECT `table1`.`foo` AS `foo`, `table2`.`bar` AS `bar` \n" - "FROM `table2`, unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" + "FROM `table2`, UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" ) found_outer_sql = q.compile(faux_conn).string assert found_outer_sql == expected_outer_sql @@ -219,7 +219,7 @@ def test_no_implicit_join_asterix_for_inner_unnest(faux_conn, metadata): q = prepare_implicit_join_base_query(faux_conn, metadata, True, False) expected_initial_sql = ( "SELECT `table1`.`foo`, `table2`.`bar` \n" - "FROM unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`" + "FROM UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -230,7 +230,7 @@ def test_no_implicit_join_asterix_for_inner_unnest(faux_conn, metadata): expected_outer_sql = ( "SELECT * \n" "FROM (SELECT `table1`.`foo` AS `foo`, `table2`.`bar` AS `bar` \n" - "FROM unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`) AS `anon_1`" + "FROM UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`) AS `anon_1`" ) found_outer_sql = q.compile(faux_conn).string assert found_outer_sql == expected_outer_sql @@ -242,7 +242,7 @@ def test_no_implicit_join_for_inner_unnest_before_2_0(faux_conn, metadata): q = prepare_implicit_join_base_query(faux_conn, metadata, True, False) expected_initial_sql = ( "SELECT `table1`.`foo`, `table2`.`bar` \n" - "FROM `table2`, unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" + "FROM `table2`, UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -253,7 +253,7 @@ def test_no_implicit_join_for_inner_unnest_before_2_0(faux_conn, metadata): expected_outer_sql = ( "SELECT `anon_1`.`foo` \n" "FROM (SELECT `table1`.`foo` AS `foo`, `table2`.`bar` AS `bar` \n" - "FROM `table2`, unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" + "FROM `table2`, UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" ) found_outer_sql = q.compile(faux_conn).string assert found_outer_sql == expected_outer_sql @@ -265,7 +265,7 @@ def test_no_implicit_join_for_inner_unnest(faux_conn, metadata): q = prepare_implicit_join_base_query(faux_conn, metadata, True, False) expected_initial_sql = ( "SELECT `table1`.`foo`, `table2`.`bar` \n" - "FROM unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`" + "FROM UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -276,7 +276,7 @@ def test_no_implicit_join_for_inner_unnest(faux_conn, metadata): expected_outer_sql = ( "SELECT `anon_1`.`foo` \n" "FROM (SELECT `table1`.`foo` AS `foo`, `table2`.`bar` AS `bar` \n" - "FROM unnest(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`) AS `anon_1`" + "FROM UNNEST(`table2`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`, `table2`) AS `anon_1`" ) found_outer_sql = q.compile(faux_conn).string assert found_outer_sql == expected_outer_sql @@ -289,7 +289,7 @@ def test_no_implicit_join_asterix_for_inner_unnest_no_table2_column( q = prepare_implicit_join_base_query(faux_conn, metadata, False, False) expected_initial_sql = ( "SELECT `table1`.`foo` \n" - "FROM `table2` `table2_1`, unnest(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" + "FROM `table2` `table2_1`, UNNEST(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -300,7 +300,7 @@ def test_no_implicit_join_asterix_for_inner_unnest_no_table2_column( expected_outer_sql = ( "SELECT * \n" "FROM (SELECT `table1`.`foo` AS `foo` \n" - "FROM `table2` `table2_1`, unnest(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" + "FROM `table2` `table2_1`, UNNEST(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" ) found_outer_sql = q.compile(faux_conn).string assert found_outer_sql == expected_outer_sql @@ -311,7 +311,7 @@ def test_no_implicit_join_for_inner_unnest_no_table2_column(faux_conn, metadata) q = prepare_implicit_join_base_query(faux_conn, metadata, False, False) expected_initial_sql = ( "SELECT `table1`.`foo` \n" - "FROM `table2` `table2_1`, unnest(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" + "FROM `table2` `table2_1`, UNNEST(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`" ) found_initial_sql = q.compile(faux_conn).string assert found_initial_sql == expected_initial_sql @@ -322,7 +322,7 @@ def test_no_implicit_join_for_inner_unnest_no_table2_column(faux_conn, metadata) expected_outer_sql = ( "SELECT `anon_1`.`foo` \n" "FROM (SELECT `table1`.`foo` AS `foo` \n" - "FROM `table2` `table2_1`, unnest(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" + "FROM `table2` `table2_1`, UNNEST(`table2_1`.`foos`) AS `unnested_foos` JOIN `table1` ON `table1`.`foo` = `unnested_foos`) AS `anon_1`" ) found_outer_sql = q.compile(faux_conn).string assert found_outer_sql == expected_outer_sql diff --git a/tests/unit/test_select.py b/tests/unit/test_select.py index a600bdf9..72f5acf9 100644 --- a/tests/unit/test_select.py +++ b/tests/unit/test_select.py @@ -419,7 +419,7 @@ def test_unnest(faux_conn, alias): query = fcall.column_valued("foo_objects") compiled = str(sqlalchemy.select(query).compile(faux_conn.engine)) assert " ".join(compiled.strip().split()) == ( - "SELECT `foo_objects` FROM `t` `t_1`, unnest(`t_1`.`objects`) AS `foo_objects`" + "SELECT `foo_objects` FROM `t` `t_1`, UNNEST(`t_1`.`objects`) AS `foo_objects`" ) @@ -450,7 +450,7 @@ def test_unnest_w_no_table_references(faux_conn, alias): query = fcall.column_valued() compiled = str(sqlalchemy.select(query).compile(faux_conn.engine)) assert " ".join(compiled.strip().split()) == ( - "SELECT `anon_1` FROM unnest(%(unnest_1)s) AS `anon_1`" + "SELECT `anon_1` FROM UNNEST(%(unnest_1)s) AS `anon_1`" ) From a7825b2431d4bc944622949e04c6cc4c29a70d4b Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Wed, 1 Oct 2025 07:26:26 -0400 Subject: [PATCH 2/7] Update setup.cfg --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 07e5991b..956ac502 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,9 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -#[sqla_testing] -#requirement_cls=sqlalchemy_bigquery.requirements:Requirements -#profile_file=.sqlalchemy_dialect_compliance-profiles.txt +[sqla_testing] +requirement_cls=sqlalchemy_bigquery.requirements:Requirements +profile_file=.sqlalchemy_dialect_compliance-profiles.txt [tool:pytest] #addopts= --tb native -v -r fxX From 2308fa658be1bce61e6678b36b3472b7374fdbe2 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Wed, 1 Oct 2025 07:26:32 -0400 Subject: [PATCH 3/7] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 956ac502..af3619bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,5 +19,5 @@ requirement_cls=sqlalchemy_bigquery.requirements:Requirements profile_file=.sqlalchemy_dialect_compliance-profiles.txt [tool:pytest] -#addopts= --tb native -v -r fxX +addopts= --tb native -v -r fxX python_files=tests/*test_*.py From 15ccd0a51f4153f847b4daa9ae166d8c15aa3b4e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:27:27 +0000 Subject: [PATCH 4/7] Revert changes to setup.cfg per PR feedback From d56c3e43285bb3d21fe815ef2d2eb2afb8a4ca6a Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Wed, 8 Oct 2025 16:39:42 -0400 Subject: [PATCH 5/7] Update tests/unit/test_select.py --- tests/unit/test_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_select.py b/tests/unit/test_select.py index 72f5acf9..e8f0f498 100644 --- a/tests/unit/test_select.py +++ b/tests/unit/test_select.py @@ -450,7 +450,7 @@ def test_unnest_w_no_table_references(faux_conn, alias): query = fcall.column_valued() compiled = str(sqlalchemy.select(query).compile(faux_conn.engine)) assert " ".join(compiled.strip().split()) == ( - "SELECT `anon_1` FROM UNNEST(%(unnest_1)s) AS `anon_1`" + "SELECT `anon_1` FROM UNNEST(%(param_1)s) AS `anon_1`" ) From 50587d802109ad99a52518bbb8013a610478dcf8 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 16 Oct 2025 06:07:30 -0400 Subject: [PATCH 6/7] updates unnest test, adds column label --- tests/unit/test_sqlalchemy_bigquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_sqlalchemy_bigquery.py b/tests/unit/test_sqlalchemy_bigquery.py index 85408aef..f9119392 100644 --- a/tests/unit/test_sqlalchemy_bigquery.py +++ b/tests/unit/test_sqlalchemy_bigquery.py @@ -226,7 +226,7 @@ def test_unnest_function(args, kw): f = sqlalchemy.func.unnest(*args, **kw) assert isinstance(f.type, sqlalchemy.String) - assert isinstance(sqlalchemy.select(f).subquery().c.unnest.type, sqlalchemy.String) + assert isinstance(sqlalchemy.select(f.label("unnested_value")).subquery().c.unnested_value.type, sqlalchemy.String) @mock.patch("sqlalchemy_bigquery._helpers.create_bigquery_client") From 52c385a4ea8909a8f00d2baddc544047b2d4907b Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Thu, 16 Oct 2025 10:09:38 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20po?= =?UTF-8?q?st-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- tests/unit/test_sqlalchemy_bigquery.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_sqlalchemy_bigquery.py b/tests/unit/test_sqlalchemy_bigquery.py index f9119392..41249e36 100644 --- a/tests/unit/test_sqlalchemy_bigquery.py +++ b/tests/unit/test_sqlalchemy_bigquery.py @@ -226,7 +226,10 @@ def test_unnest_function(args, kw): f = sqlalchemy.func.unnest(*args, **kw) assert isinstance(f.type, sqlalchemy.String) - assert isinstance(sqlalchemy.select(f.label("unnested_value")).subquery().c.unnested_value.type, sqlalchemy.String) + assert isinstance( + sqlalchemy.select(f.label("unnested_value")).subquery().c.unnested_value.type, + sqlalchemy.String, + ) @mock.patch("sqlalchemy_bigquery._helpers.create_bigquery_client")