diff --git a/bigquery/federated_queries/bigquery_federated_view_schema_spanner.py b/bigquery/federated_queries/bigquery_federated_view_schema_spanner.py new file mode 100644 index 0000000000..c8fa57df7c --- /dev/null +++ b/bigquery/federated_queries/bigquery_federated_view_schema_spanner.py @@ -0,0 +1,74 @@ +# 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 +# +# https://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. + +# [START bigquery_federated_view_schema_spanner] +import sys + +from google.api_core import exceptions +from google.cloud import bigquery + + +def federated_view_schema_spanner( + project_id: str, + connection_id: str, +) -> None: + """Views a Cloud Spanner table schema from BigQuery using EXTERNAL_QUERY. + + Args: + project_id: Google Cloud project ID (for example, 'example-project'). + connection_id: Fully qualified BigQuery Cloud Spanner connection ID + (for example, 'example-project.us.example-spanner-conn'). + """ + client = bigquery.Client(project=project_id) + # Query Spanner information_schema.columns inside EXTERNAL_QUERY to inspect table column metadata. + external_sql = ( + "SELECT column_name, spanner_type, is_nullable " + "FROM information_schema.columns " + "WHERE table_name = 'orders' " + "ORDER BY ordinal_position ASC" + ) + query = f""" + SELECT + column_name, + spanner_type, + is_nullable + FROM EXTERNAL_QUERY('{connection_id}', "{external_sql}") + ORDER BY column_name ASC; + """ + + try: + print(f"Inspecting Spanner table schema via connection: {connection_id}") + query_job = client.query(query) + rows = list(query_job.result()) + except exceptions.NotFound as err: + print( + f"error: Spanner connection '{connection_id}' not found in project " + f"'{project_id}': {err.message}", + file=sys.stderr, + ) + return + except exceptions.GoogleAPICallError as err: + print( + f"error: could not inspect Spanner table schema on connection " + f"'{connection_id}' in project '{project_id}': {err.message}", + file=sys.stderr, + ) + return + + print(f"Resource ID: {connection_id}") + print(f"Columns Returned: {len(rows)}") + print(f"Job ID: {query_job.job_id}") + + +# [END bigquery_federated_view_schema_spanner] diff --git a/bigquery/federated_queries/bigquery_federated_view_schema_spanner_test.py b/bigquery/federated_queries/bigquery_federated_view_schema_spanner_test.py new file mode 100644 index 0000000000..29ab09ae80 --- /dev/null +++ b/bigquery/federated_queries/bigquery_federated_view_schema_spanner_test.py @@ -0,0 +1,73 @@ +# 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 +# +# https://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 unittest import mock + +import bigquery_federated_view_schema_spanner +from google.api_core import exceptions +import pytest + + +def test_federated_view_schema_spanner_success( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + mock_client = mock.MagicMock() + mock_job = mock.MagicMock() + mock_job.job_id = "example-job-123" + mock_job.result.return_value = [ + mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock(), + ] + mock_client.query.return_value = mock_job + monkeypatch.setattr( + bigquery_federated_view_schema_spanner.bigquery, + "Client", + lambda project=None: mock_client, + ) + + bigquery_federated_view_schema_spanner.federated_view_schema_spanner( + project_id="example-project", + connection_id="example-project.us.example-spanner-conn", + ) + + captured = capsys.readouterr() + assert "Resource ID: example-project.us.example-spanner-conn" in captured.out + assert "Columns Returned: 3" in captured.out + assert mock_client.query.called + + +def test_federated_view_schema_spanner_error( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + mock_client = mock.MagicMock() + mock_job = mock.MagicMock() + mock_job.result.side_effect = exceptions.NotFound("connection not found") + mock_client.query.return_value = mock_job + monkeypatch.setattr( + bigquery_federated_view_schema_spanner.bigquery, + "Client", + lambda project=None: mock_client, + ) + + bigquery_federated_view_schema_spanner.federated_view_schema_spanner( + project_id="example-project", + connection_id="example-project.us.example-spanner-conn", + ) + + captured = capsys.readouterr() + assert "error:" in captured.err + assert "example-spanner-conn" in captured.err diff --git a/bigquery/federated_queries/noxfile_config.py b/bigquery/federated_queries/noxfile_config.py new file mode 100644 index 0000000000..63902aa8c7 --- /dev/null +++ b/bigquery/federated_queries/noxfile_config.py @@ -0,0 +1,23 @@ +# 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 +# +# https://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. + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"], + # Enforce type hints on new samples. + "enforce_type_hints": True, + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + "pip_version_override": None, + "envs": {}, +} diff --git a/bigquery/federated_queries/requirements-test.txt b/bigquery/federated_queries/requirements-test.txt new file mode 100644 index 0000000000..c9e154ba44 --- /dev/null +++ b/bigquery/federated_queries/requirements-test.txt @@ -0,0 +1 @@ +pytest==9.0.3; python_version >= "3.10" diff --git a/bigquery/federated_queries/requirements.txt b/bigquery/federated_queries/requirements.txt new file mode 100644 index 0000000000..dc6cf8e432 --- /dev/null +++ b/bigquery/federated_queries/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigquery==3.47.0