Repository navigation
feat(bigquery): add bigquery_upgrade_manifest_biglake_table sample to… #14748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||||||
| id, | ||||||||||||||||||
| label, | ||||||||||||||||||
| score | ||||||||||||||||||
| FROM `{table_ref}` | ||||||||||||||||||
| ORDER BY id ASC | ||||||||||||||||||
| LIMIT 10; | ||||||||||||||||||
|
Comment on lines
+56
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
|
||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| try: | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. COMMENTI looked at about seven samples. The ones I saw all appear to have this 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 |
||||||||||||||||||
| 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") | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||
| 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just thinking out loudIn PRs #14740 and #14748, string-target patching is used (e.g. To keep target-safety while retaining mock's auto-generated mock instances and call assertions, there may be utility in using Alternatively, if we want to match the other PRs, you could use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd probably be better to patch |
||
| 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 | ||
| 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": {}, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| pytest==9.0.3; python_version >= "3.10" |
| 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 |
There was a problem hiding this comment.
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 TABLEafter theSELECT, we would not get back the results of theSELECT, we would get back an empty list.This is because:
statement_type = "SCRIPT"ScriptOptionsobject, thekey_result_statementproperty is defined, by default using the enum:KeyResultStatementKind.LASTFor 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:
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.