Skip to content
This repository was archived by the owner on Mar 16, 2026. It is now read-only.

Commit 2492a0c

Browse files
authored
Merge branch 'sqla2' into sqla2
2 parents a377a31 + cb34e6b commit 2492a0c

7 files changed

Lines changed: 174 additions & 24 deletions

File tree

samples/snippets/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ requests==2.31.0
2828
rsa==4.9
2929
shapely==2.0.2
3030
six==1.16.0
31-
sqlalchemy===1.4.27
31+
sqlalchemy===2.0.22
3232
typing-extensions==4.7.1
3333
urllib3==1.26.18

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def readme():
101101
"google-auth>=1.25.0,<3.0.0dev", # Work around pip wack.
102102
"google-cloud-bigquery>=3.3.6,<4.0.0dev",
103103
"packaging",
104-
"sqlalchemy>=1.4",
104+
"sqlalchemy>=2.0,<2.0.23",
105105
],
106106
extras_require=extras,
107107
python_requires=">=3.8, <3.13",

sqlalchemy_bigquery/_struct.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def _setup_getitem(self, name):
103103
def __getattr__(self, name):
104104
if name.lower() in self.expr.type._STRUCT_byname:
105105
return self[name]
106+
else:
107+
raise AttributeError(name)
106108

107109
comparator_factory = Comparator
108110

sqlalchemy_bigquery/base.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -972,8 +972,19 @@ def _get_table(self, connection, table_name, schema=None):
972972
return table
973973

974974
def has_table(self, connection, table_name, schema=None, **kw):
975-
"""
976-
No kw are supported
975+
"""Checks whether a table exists in BigQuery.
976+
977+
Args:
978+
connection (google.cloud.bigquery.client.Client): The client
979+
object used to interact with BigQuery.
980+
table_name (str): The name of the table to check for.
981+
schema (str, optional): The name of the schema to which the table
982+
belongs. Defaults to the default schema.
983+
**kw (dict): Any extra keyword arguments will be ignored.
984+
985+
Returns:
986+
bool: True if the table exists, False otherwise.
987+
977988
"""
978989
try:
979990
self._get_table(connection, table_name, schema)
@@ -1070,8 +1081,10 @@ def __init__(self, *args, **kwargs):
10701081
if isinstance(arg, sqlalchemy.sql.expression.ColumnElement):
10711082
if not (
10721083
isinstance(arg.type, sqlalchemy.sql.sqltypes.ARRAY)
1073-
or (hasattr(arg.type, "impl")
1074-
and isinstance(arg.type.impl, sqlalchemy.sql.sqltypes.ARRAY))
1084+
or (
1085+
hasattr(arg.type, "impl")
1086+
and isinstance(arg.type.impl, sqlalchemy.sql.sqltypes.ARRAY)
1087+
)
10751088
):
10761089
raise TypeError("The argument to unnest must have an ARRAY type.")
10771090
self.type = arg.type.item_type

sqlalchemy_bigquery/requirements.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,12 @@ def schemas(self):
135135
named 'test_schema'."""
136136

137137
return unsupported()
138-
138+
139139
@property
140140
def array_type(self):
141141
"""Target database must support array_type"""
142142
return supported()
143143

144-
145144
@property
146145
def implicit_default_schema(self):
147146
"""target system has a strong concept of 'default' schema that can

tests/sqlalchemy_dialect_compliance/test_dialect_compliance.py

Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@
4444
QuotedNameArgumentTest,
4545
SimpleUpdateDeleteTest as _SimpleUpdateDeleteTest,
4646
TimestampMicrosecondsTest as _TimestampMicrosecondsTest,
47-
TrueDivTest as _TrueDivTest,
48-
IntegerTest as _IntegerTest,
49-
NumericTest as _NumericTest,
5047
)
5148

5249
from sqlalchemy.testing.suite.test_types import (
@@ -63,15 +60,17 @@
6360

6461
if packaging.version.parse(sqlalchemy.__version__) >= packaging.version.parse("2.0"):
6562
from sqlalchemy.sql import type_coerce
63+
from sqlalchemy.testing.suite import (
64+
TrueDivTest as _TrueDivTest,
65+
IntegerTest as _IntegerTest,
66+
NumericTest as _NumericTest,
67+
DifficultParametersTest as _DifficultParametersTest,
68+
FetchLimitOffsetTest as _FetchLimitOffsetTest,
69+
)
6670

6771
class TimestampMicrosecondsTest(_TimestampMicrosecondsTest):
6872
data = datetime.datetime(2012, 10, 15, 12, 57, 18, 396, tzinfo=pytz.UTC)
6973

70-
# TimestampMicrosecondsTest literal() no literal_execute parameter? Go back and add to literal()"
71-
@pytest.mark.skip("")
72-
def test_literal(self, literal_round_trip):
73-
pass
74-
7574
def test_select_direct(self, connection):
7675
# This func added because this test was failing when passed the
7776
# UTC timezone.
@@ -106,14 +105,12 @@ def test_round_trip_executemany(self, connection):
106105
test_round_trip_executemany
107106
)
108107

109-
# TrueDivTest issue because 1.4 always rounded down, but 2.0 rounds based on the data types. The assertion cannot reconcile 1.5==1 thusly
110108
class TrueDivTest(_TrueDivTest):
111-
@pytest.mark.skip("SQLAlchemy 2.0 rounds based on datatype")
109+
@pytest.mark.skip("BQ rounds based on datatype")
112110
def test_floordiv_integer(self):
113-
# TODO: possibly compare rounded result instead?
114111
pass
115112

116-
@pytest.mark.skip("SQLAlchemy 2.0 rounds based on datatype")
113+
@pytest.mark.skip("BQ rounds based on datatype")
117114
def test_floordiv_integer_bound(self):
118115
pass
119116

@@ -151,8 +148,9 @@ class InsertBehaviorTest(_InsertBehaviorTest):
151148
def test_insert_from_select_autoinc(cls):
152149
pass
153150

154-
# TODO: Find cause of error
155-
@pytest.mark.skip("")
151+
@pytest.mark.skip(
152+
"BQ has no autoinc and client-side defaults can't work for select."
153+
)
156154
def test_no_results_for_non_returning_insert(cls):
157155
pass
158156

@@ -178,7 +176,7 @@ def run(type_, input_, output, filter_=None, check_scale=False):
178176

179177
where_expr = True
180178

181-
# Adding where clause
179+
# Adding where clause for 2.0 compatibility
182180
connection.execute(t.delete().where(where_expr))
183181

184182
# test that this is actually a number!
@@ -203,6 +201,142 @@ def run(type_, input_, output, filter_=None, check_scale=False):
203201

204202
return run
205203

204+
class DifficultParametersTest(_DifficultParametersTest):
205+
# removed parameters that dont work with bigquery
206+
tough_parameters = testing.combinations(
207+
("boring",),
208+
("per cent",),
209+
("per % cent",),
210+
("%percent",),
211+
("col:ons",),
212+
("_starts_with_underscore",),
213+
("more :: %colons%",),
214+
("_name",),
215+
("___name",),
216+
("42numbers",),
217+
("percent%signs",),
218+
("has spaces",),
219+
("1param",),
220+
("1col:on",),
221+
argnames="paramname",
222+
)
223+
224+
@tough_parameters
225+
@config.requirements.unusual_column_name_characters
226+
def test_round_trip_same_named_column(self, paramname, connection, metadata):
227+
name = paramname
228+
229+
t = Table(
230+
"t",
231+
metadata,
232+
Column("id", Integer, primary_key=True),
233+
Column(name, String(50), nullable=False),
234+
)
235+
236+
# table is created
237+
t.create(connection)
238+
239+
# automatic param generated by insert
240+
connection.execute(t.insert().values({"id": 1, name: "some name"}))
241+
242+
# automatic param generated by criteria, plus selecting the column
243+
stmt = select(t.c[name]).where(t.c[name] == "some name")
244+
245+
eq_(connection.scalar(stmt), "some name")
246+
247+
# use the name in a param explicitly
248+
stmt = select(t.c[name]).where(t.c[name] == bindparam(name))
249+
250+
row = connection.execute(stmt, {name: "some name"}).first()
251+
252+
# name works as the key from cursor.description
253+
eq_(row._mapping[name], "some name")
254+
255+
# use expanding IN
256+
stmt = select(t.c[name]).where(
257+
t.c[name].in_(["some name", "some other_name"])
258+
)
259+
260+
row = connection.execute(stmt).first()
261+
262+
@testing.fixture
263+
def multirow_fixture(self, metadata, connection):
264+
mytable = Table(
265+
"mytable",
266+
metadata,
267+
Column("myid", Integer),
268+
Column("name", String(50)),
269+
Column("desc", String(50)),
270+
)
271+
272+
mytable.create(connection)
273+
274+
connection.execute(
275+
mytable.insert(),
276+
[
277+
{"myid": 1, "name": "a", "desc": "a_desc"},
278+
{"myid": 2, "name": "b", "desc": "b_desc"},
279+
{"myid": 3, "name": "c", "desc": "c_desc"},
280+
{"myid": 4, "name": "d", "desc": "d_desc"},
281+
],
282+
)
283+
yield mytable
284+
285+
@tough_parameters
286+
def test_standalone_bindparam_escape(
287+
self, paramname, connection, multirow_fixture
288+
):
289+
tbl1 = multirow_fixture
290+
stmt = select(tbl1.c.myid).where(
291+
tbl1.c.name == bindparam(paramname, value="x")
292+
)
293+
res = connection.scalar(stmt, {paramname: "c"})
294+
eq_(res, 3)
295+
296+
@tough_parameters
297+
def test_standalone_bindparam_escape_expanding(
298+
self, paramname, connection, multirow_fixture
299+
):
300+
tbl1 = multirow_fixture
301+
stmt = (
302+
select(tbl1.c.myid)
303+
.where(tbl1.c.name.in_(bindparam(paramname, value=["a", "b"])))
304+
.order_by(tbl1.c.myid)
305+
)
306+
307+
res = connection.scalars(stmt, {paramname: ["d", "a"]}).all()
308+
eq_(res, [1, 4])
309+
310+
class FetchLimitOffsetTest(_FetchLimitOffsetTest):
311+
@pytest.mark.skip("BigQuery doesn't allow an offset without a limit.")
312+
def test_simple_offset(self):
313+
pass
314+
315+
test_bound_offset = test_simple_offset
316+
test_expr_offset = test_simple_offset_zero = test_simple_offset
317+
318+
# The original test is missing an order by.
319+
320+
# Also, note that sqlalchemy union is a union distinct, not a
321+
# union all. This test caught that were were getting that wrong.
322+
def test_limit_render_multiple_times(self, connection):
323+
table = self.tables.some_table
324+
stmt = select(table.c.id).order_by(table.c.id).limit(1).scalar_subquery()
325+
326+
u = sqlalchemy.union(select(stmt), select(stmt)).subquery().select()
327+
328+
self._assert_result(
329+
connection,
330+
u,
331+
[(1,)],
332+
)
333+
334+
# from else statement ....
335+
del DistinctOnTest # expects unquoted table names.
336+
del HasIndexTest # BQ doesn't do the indexes that SQLA is loooking for.
337+
del IdentityAutoincrementTest # BQ doesn't do autoincrement
338+
339+
206340
elif packaging.version.parse(sqlalchemy.__version__) < packaging.version.parse("1.4"):
207341
from sqlalchemy.testing.suite import LimitOffsetTest as _LimitOffsetTest
208342

tests/system/test_geography.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ def test_geoalchemy2_core(bigquery_dataset):
128128
int(
129129
list(
130130
conn.execute(
131-
select(lake_table.c.geog.st_area(), lake_table.c.name == "test2")
131+
select(lake_table.c.geog.st_area()).where(
132+
lake_table.c.name == "test2"
133+
)
132134
)
133135
)[0][0]
134136
)

0 commit comments

Comments
 (0)