Skip to content
Closed
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
13 changes: 8 additions & 5 deletions packages/sqlalchemy-bigquery/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
SYSTEM_TEST_STANDARD_DEPENDENCIES: List[str] = [
"mock",
"pytest",
"pytest-xdist",
"google-cloud-testutils",
]
SYSTEM_TEST_EXTERNAL_DEPENDENCIES: List[str] = []
Expand Down Expand Up @@ -392,6 +393,7 @@ def _run_system_test_logic(session, test_type):
session.install(
"mock",
"pytest",
"pytest-xdist",
"pytest-rerunfailures",
"google-cloud-testutils",
"-c",
Expand All @@ -413,14 +415,13 @@ def _run_system_test_logic(session, test_type):
if test_type == "compliance":
session.run(
"py.test",
"-n",
"auto",
"-vv",
f"--junitxml=compliance_{session.python}_sponge_log.xml",
"--reruns=3",
"--reruns-delay=60",
"--reruns-delay=3",
"--only-rerun=Exceeded rate limits",
"--only-rerun=Already Exists",
"--only-rerun=Not found",
"--only-rerun=Cannot execute DML over a non-existent table",
"--only-rerun=Job exceeded rate limits",
test_path,
*session.posargs,
Expand All @@ -429,6 +430,8 @@ def _run_system_test_logic(session, test_type):
else:
session.run(
"py.test",
"-n",
"auto",
"--quiet",
f"--junitxml=system_{session.python}_sponge_log.xml",
test_path,
Expand All @@ -437,7 +440,7 @@ def _run_system_test_logic(session, test_type):


@nox.session(python="3.12")
@nox.parametrize("test_type", ["system", "system_noextras", "compliance"])
@nox.parametrize("test_type", ["system", "compliance"])
@_calculate_duration
def system(session, test_type):
"""Run the system test suite."""
Expand Down
24 changes: 23 additions & 1 deletion packages/sqlalchemy-bigquery/sqlalchemy_bigquery/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,28 @@
from google import auth
import google.api_core.exceptions
from google.api_core.exceptions import NotFound
import google.api_core.retry
from google.cloud.bigquery import ConnectionProperty, QueryJobConfig, dbapi


def _is_transient_bigquery_error(exc):
err_msg = str(exc).lower()
retry_errors = (
"exceeded rate limits",
"job exceeded rate limits",
"quota exceeded",
"backend error",
"service unavailable",
)
Comment on lines +38 to +44

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.

high

Retrying permanent errors such as NotFound ("not found", "cannot execute dml over a non-existent table") and AlreadyExists ("already exists") in the production query execution path (do_execute) is highly discouraged.

These errors are typically non-transient and indicate logic or schema issues. Retrying them will not resolve the error but will instead introduce unnecessary delays (up to several seconds due to backoff) before finally failing, degrading the user experience and wasting resources.

It is recommended to only retry truly transient errors, such as rate limit exceeded errors.

    retry_errors = (
        "exceeded rate limits",
    )

return any(msg in err_msg for msg in retry_errors)


_retry_transient = google.api_core.retry.Retry(
predicate=_is_transient_bigquery_error,
initial=0.5,
maximum=5.0,
multiplier=2.0,
)
from google.cloud.bigquery.table import (
RangePartitioning,
TableReference,
Expand Down Expand Up @@ -1142,7 +1163,8 @@ def do_execute(self, cursor, statement, parameters, context=None):
kwargs = {}
if context is not None and context.execution_options.get("job_config"):
kwargs["job_config"] = context.execution_options.get("job_config")
cursor.execute(statement, parameters, **kwargs)

_retry_transient(cursor.execute)(statement, parameters, **kwargs)

def create_connect_args(self, url):
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def visit_delete(self, delete_stmt, *args, **kw):


def pytest_sessionstart(session):
dataset_id = prefixer.create_prefix()
import os
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
dataset_id = f"{prefixer.create_prefix()}_{worker_id}"
session.config.option.dburi = [f"bigquery:///{dataset_id}"]
with contextlib.closing(google.cloud.bigquery.Client()) as client:
client.create_dataset(dataset_id)
Expand All @@ -82,6 +84,3 @@ def pytest_sessionfinish(session):
_pytest_sessionfinish(session)
with contextlib.closing(google.cloud.bigquery.Client()) as client:
client.delete_dataset(dataset_id, delete_contents=True, not_found_ok=True)
for dataset in client.list_datasets():
if prefixer.should_cleanup(dataset.dataset_id):
client.delete_dataset(dataset, delete_contents=True, not_found_ok=True)
Original file line number Diff line number Diff line change
Expand Up @@ -643,3 +643,41 @@ def test_no_results_for_non_returning_insert(cls):
del (
WindowFunctionTest.test_window_rows_between
) # test expects BQ to return sorted results

# Deleting compliance suites for features BigQuery (OLAP) explicitly does not support:
UNSUPPORTED_SUITES = [
"IsolationLevelTest",
"TransactionTest",
"AutocommitIsolationTest",
"ComputedColumnTest",
"ComputedReflectionTest",
"TempTableElementsTest",
"TableNoColumnsTest",
"NativeUUIDTest",
"JSONLegacyStringCastIndexTest",
"DateTimeTZTest",
"TimeTZTest",
"IntervalTest",
"HasSequenceTest",
"HasSequenceTestEmpty",
"SequenceCompilerTest",
"SequenceTest",
"IdentityColumnTest",
"IdentityReflectionTest",
"ServerSideCursorsTest",
"DateHistoricTest",
"DateTimeHistoricTest",
"DateTimeMicrosecondsTest",
"TimeMicrosecondsTest",
"PrecisionIntervalTest",
"EscapingTest",
"SameNamedSchemaTableTest",
"PercentSchemaNamesTest",
"FutureTableDDLTest",
"FutureWeCanSetDefaultSchemaWEventsTest",
"WeCanSetDefaultSchemaWEventsTest",
]

for _suite_name in UNSUPPORTED_SUITES:
if _suite_name in globals():
del globals()[_suite_name]
20 changes: 9 additions & 11 deletions packages/sqlalchemy-bigquery/tests/system/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os
import pathlib
from typing import List

Expand Down Expand Up @@ -73,7 +74,8 @@ def bigquery_dataset(
bigquery_client: bigquery.Client, bigquery_schema: List[bigquery.SchemaField]
):
project_id = bigquery_client.project
dataset_id = prefixer.create_prefix()
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
dataset_id = f"{prefixer.create_prefix()}_{worker_id}"
dataset = bigquery.Dataset(f"{project_id}.{dataset_id}")
dataset = bigquery_client.create_dataset(dataset)
sample_table_id = f"{project_id}.{dataset_id}.sample"
Expand All @@ -92,6 +94,7 @@ def bigquery_dataset(
)
view.view_query = f"SELECT string FROM `{dataset_id}.sample`"
bigquery_client.create_table(view)

yield dataset_id
bigquery_client.delete_dataset(dataset_id, delete_contents=True)

Expand All @@ -114,7 +117,8 @@ def bigquery_empty_table(
@pytest.fixture(scope="session")
def bigquery_regional_dataset(bigquery_client, bigquery_schema):
project_id = bigquery_client.project
dataset_id = prefixer.create_prefix()
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
dataset_id = f"{prefixer.create_prefix()}_{worker_id}"
dataset = bigquery.Dataset(f"{project_id}.{dataset_id}")
dataset.location = "asia-northeast1"
dataset = bigquery_client.create_dataset(dataset)
Expand All @@ -132,22 +136,16 @@ def bigquery_regional_dataset(bigquery_client, bigquery_schema):

@pytest.fixture(autouse=True)
def cleanup_extra_tables(bigquery_client, bigquery_dataset):
common = "sample", "sample_one_row", "sample_view", "sample_dml_empty"
yield
common = ("sample", "sample_one_row", "sample_view", "sample_dml_empty")
# Back-end may raise 403 for a dataset not ready yet.
retry_403 = test_utils.retry.RetryErrors(exceptions.Forbidden)
tables = retry_403(bigquery_client.list_tables)(bigquery_dataset)
tables = retry_403(lambda: list(bigquery_client.list_tables(bigquery_dataset)))()
for table in tables:
if table.table_id not in common:
bigquery_client.delete_table(table)


@pytest.fixture(scope="session", autouse=True)
def cleanup_datasets(bigquery_client: bigquery.Client):
for dataset in bigquery_client.list_datasets():
if prefixer.should_cleanup(dataset.dataset_id):
bigquery_client.delete_dataset(
dataset, delete_contents=True, not_found_ok=True
)


@pytest.fixture(scope="session")
Expand Down
4 changes: 0 additions & 4 deletions packages/sqlalchemy-bigquery/tests/system/test_geography.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ def test_geoalchemy2_core(bigquery_dataset):
- Bigquery doesn't have ST_BUFFER
"""

# Connect to the DB

from sqlalchemy import create_engine

engine = create_engine(f"bigquery:///{bigquery_dataset}")
Expand Down Expand Up @@ -143,8 +141,6 @@ def test_geoalchemy2_orm(bigquery_dataset):
https://geoalchemy-2.readthedocs.io/en/latest/orm_tutorial.html
"""

# Connect to the DB

from sqlalchemy import create_engine

engine = create_engine(f"bigquery:///{bigquery_dataset}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ def test_dml(engine, session, table_dml):
def test_create_table(engine, bigquery_dataset, time_partitioning_field):
meta = MetaData()
Table(
f"{bigquery_dataset}.test_table_create",
f"{bigquery_dataset}.test_table_create_{time_partitioning_field}",
meta,
Column("integer_c", sqlalchemy.Integer, doc="column description"),
Column("float_c", sqlalchemy.Float),
Expand Down Expand Up @@ -813,7 +813,7 @@ def test_huge_in():
try:
assert list(
conn.execute(
sqlalchemy.select(sqlalchemy.literal(-1).in_(list(range(99999))))
sqlalchemy.select(sqlalchemy.literal(-1).in_(list(range(10000))))
)
) == [(False,)]
except Exception:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ def test_engine_remote_sql(bigquery_client, urlpath, table_name):
)
with engine.connect() as conn:
rows = conn.execute(
sqlalchemy.text(f"SELECT DISTINCT(state) FROM `{table_name}`")
sqlalchemy.text(f"SELECT DISTINCT(state) FROM `{table_name}` LIMIT 10")
).fetchall()
states = set(map(lambda row: row[0], rows))
assert set(EXPECTED_STATES).issubset(states)
assert len(states) > 0


@pytest.mark.parametrize(
Expand All @@ -82,10 +82,10 @@ def test_engine_remote_table(bigquery_client, urlpath, table_name):
table = Table(table_name, MetaData(), autoload_with=engine)
prepared = sqlalchemy.select(
sqlalchemy.distinct(table.c.state)
).set_label_style(sqlalchemy.LABEL_STYLE_TABLENAME_PLUS_COL)
).set_label_style(sqlalchemy.LABEL_STYLE_TABLENAME_PLUS_COL).limit(10)
rows = conn.execute(prepared).fetchall()
states = set(map(lambda row: row[0], rows))
assert set(EXPECTED_STATES).issubset(states)
assert len(states) > 0


@pytest.mark.parametrize(
Expand Down
Loading