Skip to content

Commit 3ee3dbc

Browse files
committed
feat(bigquery): add bigquery_federated_query_spanner sample to run a federated query against Cloud Spanner
Problem: The BigQuery documentation (https://cloud.google.com/bigquery/docs/spanner-federated-queries) needs a standalone, tested Python code sample demonstrating how to run a federated query against Cloud Spanner (region tag `bigquery_federated_query_spanner`). Solution: - Add `federated_query_spanner(project_id: str, connection_id: str) -> None` in `bigquery/federated_queries/bigquery_federated_query_spanner.py` (Query a Cloud Spanner database table in real time from BigQuery using EXTERNAL_QUERY.) - Add unit tests (`bigquery/federated_queries/bigquery_federated_query_spanner_test.py`) covering both the success path and API error handling (`GoogleAPICallError`)
1 parent 186be98 commit 3ee3dbc

5 files changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START bigquery_federated_query_spanner]
16+
import sys
17+
18+
from google.api_core import exceptions
19+
from google.cloud import bigquery
20+
21+
22+
def federated_query_spanner(
23+
project_id: str,
24+
connection_id: str,
25+
) -> None:
26+
"""Runs a federated query from BigQuery to Cloud Spanner using EXTERNAL_QUERY.
27+
28+
Args:
29+
project_id: Google Cloud project ID (for example, 'example-project').
30+
connection_id: Fully qualified BigQuery Cloud Spanner connection ID
31+
(for example, 'example-project.us.example-spanner-conn').
32+
"""
33+
client = bigquery.Client(project=project_id)
34+
# Push down the order aggregation and filter to Cloud Spanner via EXTERNAL_QUERY,
35+
# then join the summarized results with the public theLook eCommerce users table
36+
# in bigquery-public-data (https://cloud.google.com/bigquery/public-data).
37+
external_sql = (
38+
"SELECT user_id, COUNT(order_id) AS completed_orders, "
39+
"MIN(created_at) AS first_order_date "
40+
"FROM orders WHERE status = 'Complete' GROUP BY user_id"
41+
)
42+
query = f"""
43+
SELECT
44+
u.id AS user_id,
45+
u.country,
46+
rq.completed_orders,
47+
rq.first_order_date
48+
FROM EXTERNAL_QUERY('{connection_id}', "{external_sql}") AS rq
49+
INNER JOIN `bigquery-public-data.thelook_ecommerce.users` AS u
50+
ON u.id = rq.user_id
51+
ORDER BY user_id ASC
52+
LIMIT 10;
53+
"""
54+
55+
try:
56+
print(f"Running Spanner federated query via connection: {connection_id}")
57+
query_job = client.query(query)
58+
rows = list(query_job.result())
59+
except exceptions.NotFound as err:
60+
print(
61+
f"error: Spanner connection '{connection_id}' not found in project "
62+
f"'{project_id}': {err.message}",
63+
file=sys.stderr,
64+
)
65+
return
66+
except exceptions.GoogleAPICallError as err:
67+
print(
68+
f"error: could not run Spanner federated query on connection "
69+
f"'{connection_id}' in project '{project_id}': {err.message}",
70+
file=sys.stderr,
71+
)
72+
return
73+
74+
print(f"Resource ID: {connection_id}")
75+
print(f"Job ID: {query_job.job_id}")
76+
print(f"Rows Returned: {len(rows)}")
77+
78+
79+
# [END bigquery_federated_query_spanner]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest import mock
16+
17+
import bigquery_federated_query_spanner
18+
from google.api_core import exceptions
19+
import pytest
20+
21+
22+
def test_federated_query_spanner_success(
23+
monkeypatch: pytest.MonkeyPatch,
24+
capsys: pytest.CaptureFixture[str],
25+
) -> None:
26+
mock_client = mock.MagicMock()
27+
mock_job = mock.MagicMock()
28+
mock_job.job_id = "example-job-123"
29+
mock_row = mock.MagicMock()
30+
mock_job.result.return_value = [mock_row]
31+
mock_client.query.return_value = mock_job
32+
monkeypatch.setattr(
33+
bigquery_federated_query_spanner.bigquery,
34+
"Client",
35+
lambda project=None: mock_client,
36+
)
37+
38+
bigquery_federated_query_spanner.federated_query_spanner(
39+
project_id="example-project",
40+
connection_id="example-project.us.example-spanner-conn",
41+
)
42+
43+
captured = capsys.readouterr()
44+
assert "Resource ID:" in captured.out
45+
assert "example-project" in captured.out
46+
assert mock_client.query.called
47+
48+
49+
def test_federated_query_spanner_error(
50+
monkeypatch: pytest.MonkeyPatch,
51+
capsys: pytest.CaptureFixture[str],
52+
) -> None:
53+
mock_client = mock.MagicMock()
54+
mock_job = mock.MagicMock()
55+
mock_job.result.side_effect = exceptions.NotFound("resource error")
56+
mock_client.query.return_value = mock_job
57+
monkeypatch.setattr(
58+
bigquery_federated_query_spanner.bigquery,
59+
"Client",
60+
lambda project=None: mock_client,
61+
)
62+
63+
bigquery_federated_query_spanner.federated_query_spanner(
64+
project_id="example-project",
65+
connection_id="example-project.us.example-spanner-conn",
66+
)
67+
68+
captured = capsys.readouterr()
69+
assert "error:" in captured.err
70+
assert "example-project" in captured.err
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
TEST_CONFIG_OVERRIDE = {
16+
# You can opt out from the test for specific Python versions.
17+
"ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"],
18+
# Enforce type hints on new samples.
19+
"enforce_type_hints": True,
20+
"gcloud_project_env": "GOOGLE_CLOUD_PROJECT",
21+
"pip_version_override": None,
22+
"envs": {},
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest==9.0.3; python_version >= "3.10"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
google-cloud-bigquery==3.47.0

0 commit comments

Comments
 (0)