Skip to content

Commit db379d8

Browse files
authored
fix: avoid 404 if dataset is deleted while listing tables or views (#106)
* fix: avoid 404 if dataset is deleted while listing tables or views * add unit tests * remove unnecessary bigquery import
1 parent cc4e23d commit db379d8

File tree

2 files changed

+151
-4
lines changed

2 files changed

+151
-4
lines changed

pybigquery/sqlalchemy_bigquery.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import operator
2626

2727
from google import auth
28+
import google.api_core.exceptions
2829
from google.cloud.bigquery import dbapi
2930
from google.cloud.bigquery.schema import SchemaField
3031
from google.cloud.bigquery.table import TableReference
@@ -391,10 +392,17 @@ def _get_table_or_view_names(self, connection, table_type, schema=None):
391392
if current_schema is not None and current_schema != dataset.dataset_id:
392393
continue
393394

394-
tables = client.list_tables(dataset.reference)
395-
for table in tables:
396-
if table_type == table.table_type:
397-
result.append(get_table_name(table))
395+
try:
396+
tables = client.list_tables(dataset.reference)
397+
for table in tables:
398+
if table_type == table.table_type:
399+
result.append(get_table_name(table))
400+
except google.api_core.exceptions.NotFound:
401+
# It's possible that the dataset was deleted between when we
402+
# fetched the list of datasets and when we try to list the
403+
# tables from it. See:
404+
# https://github.com/googleapis/python-bigquery-sqlalchemy/issues/105
405+
pass
398406
return result
399407

400408
@staticmethod
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright 2021 The PyBigQuery Authors
2+
#
3+
# Use of this source code is governed by an MIT-style
4+
# license that can be found in the LICENSE file or at
5+
# https://opensource.org/licenses/MIT.
6+
7+
from unittest import mock
8+
9+
import google.api_core.exceptions
10+
from google.cloud import bigquery
11+
from google.cloud.bigquery.dataset import DatasetListItem
12+
from google.cloud.bigquery.table import TableListItem
13+
import pytest
14+
import sqlalchemy
15+
16+
17+
@pytest.fixture
18+
def mock_bigquery_client():
19+
return mock.create_autospec(bigquery.Client, instance=True)
20+
21+
22+
@pytest.fixture
23+
def mock_connection(monkeypatch, mock_bigquery_client):
24+
from pybigquery import sqlalchemy_bigquery
25+
26+
def mock_connect_args(*args, **kwargs):
27+
return ([mock_bigquery_client], {})
28+
29+
monkeypatch.setattr(
30+
sqlalchemy_bigquery.BigQueryDialect, "create_connect_args", mock_connect_args
31+
)
32+
33+
34+
@pytest.fixture
35+
def engine_under_test(mock_connection):
36+
return sqlalchemy.create_engine("bigquery://")
37+
38+
39+
@pytest.fixture
40+
def inspector_under_test(engine_under_test):
41+
from sqlalchemy.engine.reflection import Inspector
42+
43+
return Inspector.from_engine(engine_under_test)
44+
45+
46+
def dataset_item(dataset_id):
47+
return DatasetListItem(
48+
{"datasetReference": {"projectId": "some-project-id", "datasetId": dataset_id}}
49+
)
50+
51+
52+
def table_item(dataset_id, table_id, type_="TABLE"):
53+
return TableListItem(
54+
{
55+
"type": type_,
56+
"tableReference": {
57+
"projectId": "some-project-id",
58+
"datasetId": dataset_id,
59+
"tableId": table_id,
60+
},
61+
}
62+
)
63+
64+
65+
@pytest.mark.parametrize(
66+
["datasets_list", "tables_lists", "expected"],
67+
[
68+
([], [], []),
69+
([dataset_item("dataset_1")], [[]], []),
70+
(
71+
[dataset_item("dataset_1"), dataset_item("dataset_2")],
72+
[
73+
[table_item("dataset_1", "d1t1"), table_item("dataset_1", "d1t2")],
74+
[
75+
table_item("dataset_2", "d2t1"),
76+
table_item("dataset_2", "d2view", type_="VIEW"),
77+
],
78+
],
79+
["dataset_1.d1t1", "dataset_1.d1t2", "dataset_2.d2t1"],
80+
),
81+
(
82+
[dataset_item("dataset_1"), dataset_item("dataset_deleted")],
83+
[
84+
[table_item("dataset_1", "d1t1")],
85+
google.api_core.exceptions.NotFound("dataset_deleted"),
86+
],
87+
["dataset_1.d1t1"],
88+
),
89+
],
90+
)
91+
def test_get_table_names(
92+
engine_under_test, mock_bigquery_client, datasets_list, tables_lists, expected
93+
):
94+
mock_bigquery_client.list_datasets.return_value = datasets_list
95+
mock_bigquery_client.list_tables.side_effect = tables_lists
96+
table_names = engine_under_test.table_names()
97+
mock_bigquery_client.list_datasets.assert_called_once()
98+
assert mock_bigquery_client.list_tables.call_count == len(datasets_list)
99+
assert list(sorted(table_names)) == list(sorted(expected))
100+
101+
102+
@pytest.mark.parametrize(
103+
["datasets_list", "tables_lists", "expected"],
104+
[
105+
([], [], []),
106+
([dataset_item("dataset_1")], [[]], []),
107+
(
108+
[dataset_item("dataset_1"), dataset_item("dataset_2")],
109+
[
110+
[
111+
table_item("dataset_1", "d1t1"),
112+
table_item("dataset_1", "d1view", type_="VIEW"),
113+
],
114+
[
115+
table_item("dataset_2", "d2t1"),
116+
table_item("dataset_2", "d2view", type_="VIEW"),
117+
],
118+
],
119+
["dataset_1.d1view", "dataset_2.d2view"],
120+
),
121+
(
122+
[dataset_item("dataset_1"), dataset_item("dataset_deleted")],
123+
[
124+
[table_item("dataset_1", "d1view", type_="VIEW")],
125+
google.api_core.exceptions.NotFound("dataset_deleted"),
126+
],
127+
["dataset_1.d1view"],
128+
),
129+
],
130+
)
131+
def test_get_view_names(
132+
inspector_under_test, mock_bigquery_client, datasets_list, tables_lists, expected
133+
):
134+
mock_bigquery_client.list_datasets.return_value = datasets_list
135+
mock_bigquery_client.list_tables.side_effect = tables_lists
136+
view_names = inspector_under_test.get_view_names()
137+
mock_bigquery_client.list_datasets.assert_called_once()
138+
assert mock_bigquery_client.list_tables.call_count == len(datasets_list)
139+
assert list(sorted(view_names)) == list(sorted(expected))

0 commit comments

Comments
 (0)