Skip to content

Commit 669b301

Browse files
authored
fix: Select expressions no-longer force use of labels (#129)
* Fixed a dependency problem that caysed test failures in Python 3.6. The source of the dependency bug is in old versions of google-cloud-core that depend on too-old versions of google-api-core. * Provide a bigquery mock based on sqlite So we don't have t mock at the api level. * Don't force labels in select. #78
1 parent 4d169b9 commit 669b301

File tree

6 files changed

+128
-11
lines changed

6 files changed

+128
-11
lines changed

pybigquery/sqlalchemy_bigquery.py

-9
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,6 @@ def __init__(self, dialect, statement, column_keys=None, inline=False, **kwargs)
165165
dialect, statement, column_keys, inline, **kwargs
166166
)
167167

168-
def visit_select(self, *args, **kwargs):
169-
"""
170-
Use labels for every column.
171-
This ensures that fields won't contain duplicate names
172-
"""
173-
174-
args[0].use_labels = True
175-
return super(BigQueryCompiler, self).visit_select(*args, **kwargs)
176-
177168
def visit_column(
178169
self, column, add_to_result_map=None, include_table=True, **kwargs
179170
):

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ def readme():
8181
platforms="Posix; MacOS X; Windows",
8282
install_requires=[
8383
"sqlalchemy>=1.1.9,<1.4.0dev",
84-
"google-auth>=1.2.0,<2.0dev",
84+
"google-auth>=1.14.0,<2.0dev", # Work around pip wack.
8585
"google-cloud-bigquery>=1.12.0",
86+
"google-api-core>=1.19.1", # Work-around bug in cloud core deps.
8687
"future",
8788
],
8889
python_requires=">=3.6, <3.10",

testing/constraints-3.6.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
#
66
# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev",
77
sqlalchemy==1.1.9
8-
google-auth==1.2.0
8+
google-auth==1.14.0
99
google-cloud-bigquery==1.12.0

tests/unit/conftest.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import mock
2+
import pytest
3+
import sqlalchemy
4+
5+
import fauxdbi
6+
7+
8+
@pytest.fixture()
9+
def faux_conn():
10+
with mock.patch(
11+
"google.cloud.bigquery.dbapi.connection.Connection", fauxdbi.Connection
12+
):
13+
engine = sqlalchemy.create_engine("bigquery://myproject/mydataset")
14+
conn = engine.connect()
15+
yield conn
16+
conn.close()

tests/unit/fauxdbi.py

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import google.api_core.exceptions
2+
import google.cloud.bigquery.schema
3+
import google.cloud.bigquery.table
4+
import contextlib
5+
import sqlite3
6+
7+
8+
class Connection:
9+
10+
connection = None
11+
12+
def __init__(self, client=None, bqstorage_client=None):
13+
# share a single connection:
14+
if self.connection is None:
15+
self.__class__.connection = sqlite3.connect(":memory:")
16+
self._client = FauxClient(client, self.connection)
17+
18+
def cursor(self):
19+
return Cursor(self.connection)
20+
21+
def commit(self):
22+
pass
23+
24+
def rollback(self):
25+
pass
26+
27+
def close(self):
28+
self.connection.close()
29+
30+
31+
class Cursor:
32+
33+
arraysize = 1
34+
35+
def __init__(self, connection):
36+
self.connection = connection
37+
self.cursor = connection.cursor()
38+
39+
def execute(self, operation, parameters=None):
40+
if parameters:
41+
parameters = {
42+
name: "null" if value is None else repr(value)
43+
for name, value in parameters.items()
44+
}
45+
operation %= parameters
46+
self.cursor.execute(operation, parameters)
47+
self.description = self.cursor.description
48+
self.rowcount = self.cursor.rowcount
49+
50+
def executemany(self, operation, parameters_list):
51+
for parameters in parameters_list:
52+
self.execute(operation, parameters)
53+
54+
def close(self):
55+
self.cursor.close()
56+
57+
def fetchone(self):
58+
return self.cursor.fetchone()
59+
60+
def fetchmany(self, size=None):
61+
self.cursor.fetchmany(size or self.arraysize)
62+
63+
def fetchall(self):
64+
return self.cursor.fetchall()
65+
66+
def setinputsizes(self, sizes):
67+
pass
68+
69+
def setoutputsize(self, size, column=None):
70+
pass
71+
72+
73+
class FauxClient:
74+
def __init__(self, client, connection):
75+
self._client = client
76+
self.project = client.project
77+
self.connection = connection
78+
79+
def get_table(self, table_ref):
80+
table_name = table_ref.table_id
81+
with contextlib.closing(self.connection.cursor()) as cursor:
82+
cursor.execute(
83+
f"select name from sqlite_master"
84+
f" where type='table' and name='{table_name}'"
85+
)
86+
if list(cursor):
87+
cursor.execute("PRAGMA table_info('{table_name}')")
88+
schema = [
89+
google.cloud.bigquery.schema.SchemaField(
90+
name=name,
91+
field_type=type_,
92+
mode="REQUIRED" if notnull else "NULLABLE",
93+
)
94+
for cid, name, type_, notnull, dflt_value, pk in cursor
95+
]
96+
return google.cloud.bigquery.table.Table(table_ref, schema)
97+
else:
98+
raise google.api_core.exceptions.NotFound(table_ref)

tests/unit/test_select.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sqlalchemy
2+
3+
4+
def test_labels_not_forced(faux_conn):
5+
metadata = sqlalchemy.MetaData()
6+
table = sqlalchemy.Table(
7+
"some_table", metadata, sqlalchemy.Column("id", sqlalchemy.Integer)
8+
)
9+
metadata.create_all(faux_conn.engine)
10+
result = faux_conn.execute(sqlalchemy.select([table.c.id]))
11+
assert result.keys() == ["id"] # Look! Just the column name!

0 commit comments

Comments
 (0)