Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions bigquery/biglake/bigquery_upgrade_manifest_biglake_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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_upgrade_manifest_biglake_table]
import sys

from google.api_core import exceptions
from google.cloud import bigquery


def upgrade_manifest_biglake_table(
project_id: str,
dataset_id: str,
table_id: str,
connection_id: str,
manifest_uri: str,
) -> None:
"""Upgrades a manifest external table to a BigLake table with metadata caching.
Args:
project_id: Google Cloud project ID (for example, 'example-project').
dataset_id: BigQuery dataset ID containing the external table
(for example, 'example_dataset').
table_id: BigQuery external table ID to upgrade
(for example, 'example_manifest_table').
connection_id: Fully qualified BigQuery Cloud Resource connection ID
(for example, 'example-project.us.example_biglake_conn').
manifest_uri: Cloud Storage URI of the SymLinkTextInputFormat manifest file
(for example, 'gs://example-bucket/manifests/symlink.manifest').
"""
client = bigquery.Client(project=project_id)
table_ref = f"{project_id}.{dataset_id}.{table_id}"
# Upgrading the manifest external table with a Cloud Resource connection and AUTOMATIC metadata
# caching accelerates open-table-format reads and enforces BigLake governance.
sql = f"""
CREATE OR REPLACE EXTERNAL TABLE `{table_ref}`
WITH CONNECTION `{connection_id}`
OPTIONS (
format = 'PARQUET',
uris = ['{manifest_uri}'],
file_set_spec_type = 'NEW_LINE_DELIMITED_MANIFEST',
max_staleness = INTERVAL 1 HOUR,
metadata_cache_mode = 'AUTOMATIC'
);
SELECT

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iennae

Thinking out loud

My understanding is that there is a misconception about multi-part SQL statements.

You prolly know this, but in case others come behind us I will provide some context. Some folks believe that multi-part SQL statements don't return results the way we might expect. For example if I included some form of Data Definition Language (DDL) statement like DROP TABLE after the SELECT, we would not get back the results of the SELECT, we would get back an empty list.

This is because:

  • Multi-statement queries are execute as a statement_type = "SCRIPT"
  • In the ScriptOptions object, the key_result_statement property is defined, by default using the enum: KeyResultStatementKind.LAST

For this reason, I wonder if there is merit to separating the DDL portion (CREATE OR REPLACE EXTERNAL TABLE) of this example from the Data Query Language (DQL) portion (SELECT) as part of demonstrating good hygiene.

Something like this so they can be called separately:

# 1. Create or replace the BigLake external table
client.query(ddl_sql).result()

# 2. Query the newly created external table
query_job = client.query(select_sql)
rows = list(query_job.result())

Pros: two calls makes it easier to do troubleshooting if one of the major steps fails. Searching audit logs, job history, etc should be easier especially since customers won't have to drill down into parent jobs to find child jobs.
Cons: There is a small performance hit when a client.query() provisions and registers each top-level job.

id,
label,
score
FROM `{table_ref}`
ORDER BY id ASC
LIMIT 10;
Comment on lines +56 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SELECT query hardcodes specific column names (id, label, score). Since this is a generic code sample designed to be run against any manifest-backed external table, hardcoding these columns will cause the query to fail if the user's Parquet files have a different schema. Consider using SELECT * to make the query schema-agnostic and reusable.

Suggested change
SELECT
id,
label,
score
FROM `{table_ref}`
ORDER BY id ASC
LIMIT 10;
SELECT * FROM {table_ref} LIMIT 10;

"""

try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

COMMENT

I looked at about seven samples. The ones I saw all appear to have this try/except/except pattern.

I am worried that this degrades the signal-to-noise ratio in our reference samples, documentation, and quickstarts.

I know the Samples Style Guide says to include Error Handling.

The catch blocks take up roughly 20 lines of repetitive boilerplate code that prints to stderr and exits early. I wonder if this could be handled differently.

print(
f"Upgrading manifest external table to BigLake with metadata caching: {table_ref}"
)
query_job = client.query(sql)
rows = list(query_job.result())
except exceptions.NotFound as err:
print(
f"error: manifest external table '{table_id}' or connection '{connection_id}' "
f"not found in dataset '{dataset_id}' for project '{project_id}' "
f"(manifest '{manifest_uri}'): {err.message}",
file=sys.stderr,
)
return
except exceptions.GoogleAPICallError as err:
print(
f"error: failed to upgrade manifest external table '{table_id}' in dataset "
f"'{dataset_id}' for project '{project_id}' (connection '{connection_id}', "
f"manifest '{manifest_uri}'): {err.message}",
file=sys.stderr,
)
return

print(f"Resource ID: {table_ref}")
print(f"Connection ID: {connection_id}")
print(f"Job ID: {query_job.job_id}")
print(f"Manifest URI: {manifest_uri}")
print("Metadata Cache Mode: AUTOMATIC")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's no point in printing the metadata cache mode here, because it's a hardcoded value and the response QueryObject doesnt include wether the table created is using metadata cache, it would make more sense to use a get_table() call to inspect the newly created table and inspect the resulted Table object TableObject.external_data_configuration.metadata_cache_mode and print it's value, then it could be asserted during testing.

print(f"Rows Returned: {len(rows)}")


# [END bigquery_upgrade_manifest_biglake_table]
Original file line number Diff line number Diff line change
@@ -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_upgrade_manifest_biglake_table
from google.api_core import exceptions
import pytest


def test_upgrade_manifest_biglake_table_success(
capsys: pytest.CaptureFixture[str],
) -> None:
with mock.patch("google.cloud.bigquery.Client") as mock_client_cls:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud

In PRs #14740 and #14748, string-target patching is used (e.g. mock.patch("google.cloud.bigquery.Client")). If the sample refactors how Client is imported, string targets can silently break or patch the wrong namespace. (I understand that it is not likely that these samples will be wildly refactored - hence just thinking out loud. I don't think there is a huge ROI in inducing review churn over this.)

To keep target-safety while retaining mock's auto-generated mock instances and call assertions, there may be utility in using mock.patch.object(module.bigquery, "Client") instead of raw string paths.

Alternatively, if we want to match the other PRs, you could use monkeypatch.setattr with a MagicMock.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd probably be better to patch bigquery_upgrade_manifest_biglage_table.client instance, rather than patch the library.

mock_client = mock_client_cls.return_value
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

bigquery_upgrade_manifest_biglake_table.upgrade_manifest_biglake_table(
project_id="example-project",
dataset_id="example_dataset",
table_id="example_manifest_table",
connection_id="example-project.us.example_biglake_conn",
manifest_uri="gs://example-bucket/manifests/symlink.manifest",
)

mock_client.query.assert_called_once()
captured = capsys.readouterr()
assert (
"Resource ID: example-project.example_dataset.example_manifest_table"
in captured.out
)
assert "Metadata Cache Mode: AUTOMATIC" in captured.out
assert "Rows Returned: 3" in captured.out


def test_upgrade_manifest_biglake_table_not_found(
capsys: pytest.CaptureFixture[str],
) -> None:
with mock.patch("google.cloud.bigquery.Client") as mock_client_cls:
mock_client = mock_client_cls.return_value
mock_job = mock.MagicMock()
mock_job.result.side_effect = exceptions.NotFound("Connection not found")
mock_client.query.return_value = mock_job

bigquery_upgrade_manifest_biglake_table.upgrade_manifest_biglake_table(
project_id="example-project",
dataset_id="example_dataset",
table_id="example_manifest_table",
connection_id="example-project.us.example_biglake_conn",
manifest_uri="gs://example-bucket/manifests/symlink.manifest",
)

captured = capsys.readouterr()
assert "error:" in captured.err
assert "example_manifest_table" in captured.err
23 changes: 23 additions & 0 deletions bigquery/biglake/noxfile_config.py
Original file line number Diff line number Diff line change
@@ -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": {},
}
1 change: 1 addition & 0 deletions bigquery/biglake/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest==9.0.3; python_version >= "3.10"
2 changes: 2 additions & 0 deletions bigquery/biglake/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
google-cloud-bigquery==3.47.0
google-cloud-bigquery-storage==2.43.0
Loading