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

Commit 927e178

Browse files
authored
chore: unit test coverage and additional refactoring for spanner_dbapi (#532)
Refactoring and testing changes.
1 parent 2f2cd86 commit 927e178

20 files changed

Lines changed: 1712 additions & 825 deletions

google/cloud/spanner_dbapi/__init__.py

Lines changed: 35 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,39 @@
66

77
"""Connection-based DB API for Cloud Spanner."""
88

9-
from google.cloud import spanner_v1
10-
11-
from .connection import Connection
12-
from .exceptions import (
13-
DatabaseError,
14-
DataError,
15-
Error,
16-
IntegrityError,
17-
InterfaceError,
18-
InternalError,
19-
NotSupportedError,
20-
OperationalError,
21-
ProgrammingError,
22-
Warning,
23-
)
24-
from .parse_utils import get_param_types
25-
from .types import (
26-
BINARY,
27-
DATETIME,
28-
NUMBER,
29-
ROWID,
30-
STRING,
31-
Binary,
32-
Date,
33-
DateFromTicks,
34-
Time,
35-
TimeFromTicks,
36-
Timestamp,
37-
TimestampFromTicks,
38-
)
39-
from .version import google_client_info
9+
from google.cloud.spanner_dbapi.connection import Connection
10+
from google.cloud.spanner_dbapi.connection import connect
11+
12+
from google.cloud.spanner_dbapi.cursor import Cursor
13+
14+
from google.cloud.spanner_dbapi.exceptions import DatabaseError
15+
from google.cloud.spanner_dbapi.exceptions import DataError
16+
from google.cloud.spanner_dbapi.exceptions import Error
17+
from google.cloud.spanner_dbapi.exceptions import IntegrityError
18+
from google.cloud.spanner_dbapi.exceptions import InterfaceError
19+
from google.cloud.spanner_dbapi.exceptions import InternalError
20+
from google.cloud.spanner_dbapi.exceptions import NotSupportedError
21+
from google.cloud.spanner_dbapi.exceptions import OperationalError
22+
from google.cloud.spanner_dbapi.exceptions import ProgrammingError
23+
from google.cloud.spanner_dbapi.exceptions import Warning
24+
25+
from google.cloud.spanner_dbapi.parse_utils import get_param_types
26+
27+
from google.cloud.spanner_dbapi.types import BINARY
28+
from google.cloud.spanner_dbapi.types import DATETIME
29+
from google.cloud.spanner_dbapi.types import NUMBER
30+
from google.cloud.spanner_dbapi.types import ROWID
31+
from google.cloud.spanner_dbapi.types import STRING
32+
from google.cloud.spanner_dbapi.types import Binary
33+
from google.cloud.spanner_dbapi.types import Date
34+
from google.cloud.spanner_dbapi.types import DateFromTicks
35+
from google.cloud.spanner_dbapi.types import Time
36+
from google.cloud.spanner_dbapi.types import TimeFromTicks
37+
from google.cloud.spanner_dbapi.types import Timestamp
38+
from google.cloud.spanner_dbapi.types import TimestampStr
39+
from google.cloud.spanner_dbapi.types import TimestampFromTicks
40+
41+
from google.cloud.spanner_dbapi.version import DEFAULT_USER_AGENT
4042

4143
apilevel = "2.0" # supports DP-API 2.0 level.
4244
paramstyle = "format" # ANSI C printf format codes, e.g. ...WHERE name=%s.
@@ -48,66 +50,10 @@
4850
threadsafety = 1
4951

5052

51-
def connect(
52-
instance_id,
53-
database_id,
54-
project=None,
55-
credentials=None,
56-
pool=None,
57-
user_agent=None,
58-
):
59-
"""
60-
Create a connection to Cloud Spanner database.
61-
62-
:type instance_id: :class:`str`
63-
:param instance_id: ID of the instance to connect to.
64-
65-
:type database_id: :class:`str`
66-
:param database_id: The name of the database to connect to.
67-
68-
:type project: :class:`str`
69-
:param project: (Optional) The ID of the project which owns the
70-
instances, tables and data. If not provided, will
71-
attempt to determine from the environment.
72-
73-
:type credentials: :class:`google.auth.credentials.Credentials`
74-
:param credentials: (Optional) The authorization credentials to attach to requests.
75-
These credentials identify this application to the service.
76-
If none are specified, the client will attempt to ascertain
77-
the credentials from the environment.
78-
79-
:type pool: Concrete subclass of
80-
:class:`~google.cloud.spanner_v1.pool.AbstractSessionPool`.
81-
:param pool: (Optional). Session pool to be used by database.
82-
83-
:type user_agent: :class:`str`
84-
:param user_agent: (Optional) User agent to be used with this connection requests.
85-
86-
:rtype: :class:`google.cloud.spanner_dbapi.connection.Connection`
87-
:returns: Connection object associated with the given Cloud Spanner resource.
88-
89-
:raises: :class:`ValueError` in case of given instance/database
90-
doesn't exist.
91-
"""
92-
client = spanner_v1.Client(
93-
project=project,
94-
credentials=credentials,
95-
client_info=google_client_info(user_agent),
96-
)
97-
98-
instance = client.instance(instance_id)
99-
if not instance.exists():
100-
raise ValueError("instance '%s' does not exist." % instance_id)
101-
102-
database = instance.database(database_id, pool=pool)
103-
if not database.exists():
104-
raise ValueError("database '%s' does not exist." % database_id)
105-
106-
return Connection(instance, database)
107-
108-
10953
__all__ = [
11054
"Connection",
55+
"connect",
56+
"Cursor",
11157
"DatabaseError",
11258
"DataError",
11359
"Error",
@@ -120,7 +66,6 @@ def connect(
12066
"Warning",
12167
"DEFAULT_USER_AGENT",
12268
"apilevel",
123-
"connect",
12469
"paramstyle",
12570
"threadsafety",
12671
"get_param_types",
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
from google.cloud.spanner_dbapi.parse_utils import get_param_types
8+
from google.cloud.spanner_dbapi.parse_utils import parse_insert
9+
from google.cloud.spanner_dbapi.parse_utils import sql_pyformat_args_to_spanner
10+
from google.cloud.spanner_v1 import param_types
11+
12+
13+
SQL_LIST_TABLES = """
14+
SELECT
15+
t.table_name
16+
FROM
17+
information_schema.tables AS t
18+
WHERE
19+
t.table_catalog = '' and t.table_schema = ''
20+
"""
21+
22+
SQL_GET_TABLE_COLUMN_SCHEMA = """SELECT
23+
COLUMN_NAME, IS_NULLABLE, SPANNER_TYPE
24+
FROM
25+
INFORMATION_SCHEMA.COLUMNS
26+
WHERE
27+
TABLE_SCHEMA = ''
28+
AND
29+
TABLE_NAME = @table_name
30+
"""
31+
32+
# This table maps spanner_types to Spanner's data type sizes as per
33+
# https://cloud.google.com/spanner/docs/data-types#allowable-types
34+
# It is used to map `display_size` to a known type for Cursor.description
35+
# after a row fetch.
36+
# Since ResultMetadata
37+
# https://cloud.google.com/spanner/docs/reference/rest/v1/ResultSetMetadata
38+
# does not send back the actual size, we have to lookup the respective size.
39+
# Some fields' sizes are dependent upon the dynamic data hence aren't sent back
40+
# by Cloud Spanner.
41+
code_to_display_size = {
42+
param_types.BOOL.code: 1,
43+
param_types.DATE.code: 4,
44+
param_types.FLOAT64.code: 8,
45+
param_types.INT64.code: 8,
46+
param_types.TIMESTAMP.code: 12,
47+
}
48+
49+
50+
def _execute_insert_heterogenous(transaction, sql_params_list):
51+
for sql, params in sql_params_list:
52+
sql, params = sql_pyformat_args_to_spanner(sql, params)
53+
param_types = get_param_types(params)
54+
res = transaction.execute_sql(
55+
sql, params=params, param_types=param_types
56+
)
57+
# TODO: File a bug with Cloud Spanner and the Python client maintainers
58+
# about a lost commit when res isn't read from.
59+
_ = list(res)
60+
61+
62+
def _execute_insert_homogenous(transaction, parts):
63+
# Perform an insert in one shot.
64+
table = parts.get("table")
65+
columns = parts.get("columns")
66+
values = parts.get("values")
67+
return transaction.insert(table, columns, values)
68+
69+
70+
def handle_insert(connection, sql, params):
71+
parts = parse_insert(sql, params)
72+
73+
# The split between the two styles exists because:
74+
# in the common case of multiple values being passed
75+
# with simple pyformat arguments,
76+
# SQL: INSERT INTO T (f1, f2) VALUES (%s, %s, %s)
77+
# Params: [(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,)]
78+
# we can take advantage of a single RPC with:
79+
# transaction.insert(table, columns, values)
80+
# instead of invoking:
81+
# with transaction:
82+
# for sql, params in sql_params_list:
83+
# transaction.execute_sql(sql, params, param_types)
84+
# which invokes more RPCs and is more costly.
85+
86+
if parts.get("homogenous"):
87+
# The common case of multiple values being passed in
88+
# non-complex pyformat args and need to be uploaded in one RPC.
89+
return connection.database.run_in_transaction(
90+
_execute_insert_homogenous, parts
91+
)
92+
else:
93+
# All the other cases that are esoteric and need
94+
# transaction.execute_sql
95+
sql_params_list = parts.get("sql_params_list")
96+
return connection.database.run_in_transaction(
97+
_execute_insert_heterogenous, sql_params_list
98+
)
99+
100+
101+
class ColumnInfo:
102+
"""Row column description object."""
103+
104+
def __init__(
105+
self,
106+
name,
107+
type_code,
108+
display_size=None,
109+
internal_size=None,
110+
precision=None,
111+
scale=None,
112+
null_ok=False,
113+
):
114+
self.name = name
115+
self.type_code = type_code
116+
self.display_size = display_size
117+
self.internal_size = internal_size
118+
self.precision = precision
119+
self.scale = scale
120+
self.null_ok = null_ok
121+
122+
self.fields = (
123+
self.name,
124+
self.type_code,
125+
self.display_size,
126+
self.internal_size,
127+
self.precision,
128+
self.scale,
129+
self.null_ok,
130+
)
131+
132+
def __repr__(self):
133+
return self.__str__()
134+
135+
def __getitem__(self, index):
136+
return self.fields[index]
137+
138+
def __str__(self):
139+
str_repr = ", ".join(
140+
filter(
141+
lambda part: part is not None,
142+
[
143+
"name='%s'" % self.name,
144+
"type_code=%d" % self.type_code,
145+
"display_size=%d" % self.display_size
146+
if self.display_size
147+
else None,
148+
"internal_size=%d" % self.internal_size
149+
if self.internal_size
150+
else None,
151+
"precision='%s'" % self.precision
152+
if self.precision
153+
else None,
154+
"scale='%s'" % self.scale if self.scale else None,
155+
"null_ok='%s'" % self.null_ok if self.null_ok else None,
156+
],
157+
)
158+
)
159+
return "ColumnInfo(%s)" % str_repr

0 commit comments

Comments
 (0)