Skip to content

Commit 2493fa1

Browse files
mf2199c24t
andauthored
feat: DB-API driver + unit tests (#160)
* feat: DB-API driver + unit tests * chore: imports in test files rearranged * chore: added coding directive * chore: * chore: encoding directive * chore: skipping Python 2 incompatible tests * chore: skipping Python 2 incompatible tests * chore: skipping Python 2 incompatible tests * chore: skipping Python 2 incompatible tests * chore: lint format * chore: license headers updated * chore: minor fixes Co-authored-by: Chris Kleinknecht <[email protected]>
1 parent bf4b278 commit 2493fa1

20 files changed

+3774
-1
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2020 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Connection-based DB API for Cloud Spanner."""
16+
17+
from google.cloud.spanner_dbapi.connection import Connection
18+
from google.cloud.spanner_dbapi.connection import connect
19+
20+
from google.cloud.spanner_dbapi.cursor import Cursor
21+
22+
from google.cloud.spanner_dbapi.exceptions import DatabaseError
23+
from google.cloud.spanner_dbapi.exceptions import DataError
24+
from google.cloud.spanner_dbapi.exceptions import Error
25+
from google.cloud.spanner_dbapi.exceptions import IntegrityError
26+
from google.cloud.spanner_dbapi.exceptions import InterfaceError
27+
from google.cloud.spanner_dbapi.exceptions import InternalError
28+
from google.cloud.spanner_dbapi.exceptions import NotSupportedError
29+
from google.cloud.spanner_dbapi.exceptions import OperationalError
30+
from google.cloud.spanner_dbapi.exceptions import ProgrammingError
31+
from google.cloud.spanner_dbapi.exceptions import Warning
32+
33+
from google.cloud.spanner_dbapi.parse_utils import get_param_types
34+
35+
from google.cloud.spanner_dbapi.types import BINARY
36+
from google.cloud.spanner_dbapi.types import DATETIME
37+
from google.cloud.spanner_dbapi.types import NUMBER
38+
from google.cloud.spanner_dbapi.types import ROWID
39+
from google.cloud.spanner_dbapi.types import STRING
40+
from google.cloud.spanner_dbapi.types import Binary
41+
from google.cloud.spanner_dbapi.types import Date
42+
from google.cloud.spanner_dbapi.types import DateFromTicks
43+
from google.cloud.spanner_dbapi.types import Time
44+
from google.cloud.spanner_dbapi.types import TimeFromTicks
45+
from google.cloud.spanner_dbapi.types import Timestamp
46+
from google.cloud.spanner_dbapi.types import TimestampStr
47+
from google.cloud.spanner_dbapi.types import TimestampFromTicks
48+
49+
from google.cloud.spanner_dbapi.version import DEFAULT_USER_AGENT
50+
51+
apilevel = "2.0" # supports DP-API 2.0 level.
52+
paramstyle = "format" # ANSI C printf format codes, e.g. ...WHERE name=%s.
53+
54+
# Threads may share the module, but not connections. This is a paranoid threadsafety
55+
# level, but it is necessary for starters to use when debugging failures.
56+
# Eventually once transactions are working properly, we'll update the
57+
# threadsafety level.
58+
threadsafety = 1
59+
60+
61+
__all__ = [
62+
"Connection",
63+
"connect",
64+
"Cursor",
65+
"DatabaseError",
66+
"DataError",
67+
"Error",
68+
"IntegrityError",
69+
"InterfaceError",
70+
"InternalError",
71+
"NotSupportedError",
72+
"OperationalError",
73+
"ProgrammingError",
74+
"Warning",
75+
"DEFAULT_USER_AGENT",
76+
"apilevel",
77+
"paramstyle",
78+
"threadsafety",
79+
"get_param_types",
80+
"Binary",
81+
"Date",
82+
"DateFromTicks",
83+
"Time",
84+
"TimeFromTicks",
85+
"Timestamp",
86+
"TimestampFromTicks",
87+
"BINARY",
88+
"STRING",
89+
"NUMBER",
90+
"DATETIME",
91+
"ROWID",
92+
"TimestampStr",
93+
]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2020 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.cloud.spanner_dbapi.parse_utils import get_param_types
16+
from google.cloud.spanner_dbapi.parse_utils import parse_insert
17+
from google.cloud.spanner_dbapi.parse_utils import sql_pyformat_args_to_spanner
18+
from google.cloud.spanner_v1 import param_types
19+
20+
21+
SQL_LIST_TABLES = """
22+
SELECT
23+
t.table_name
24+
FROM
25+
information_schema.tables AS t
26+
WHERE
27+
t.table_catalog = '' and t.table_schema = ''
28+
"""
29+
30+
SQL_GET_TABLE_COLUMN_SCHEMA = """SELECT
31+
COLUMN_NAME, IS_NULLABLE, SPANNER_TYPE
32+
FROM
33+
INFORMATION_SCHEMA.COLUMNS
34+
WHERE
35+
TABLE_SCHEMA = ''
36+
AND
37+
TABLE_NAME = @table_name
38+
"""
39+
40+
# This table maps spanner_types to Spanner's data type sizes as per
41+
# https://cloud.google.com/spanner/docs/data-types#allowable-types
42+
# It is used to map `display_size` to a known type for Cursor.description
43+
# after a row fetch.
44+
# Since ResultMetadata
45+
# https://cloud.google.com/spanner/docs/reference/rest/v1/ResultSetMetadata
46+
# does not send back the actual size, we have to lookup the respective size.
47+
# Some fields' sizes are dependent upon the dynamic data hence aren't sent back
48+
# by Cloud Spanner.
49+
code_to_display_size = {
50+
param_types.BOOL.code: 1,
51+
param_types.DATE.code: 4,
52+
param_types.FLOAT64.code: 8,
53+
param_types.INT64.code: 8,
54+
param_types.TIMESTAMP.code: 12,
55+
}
56+
57+
58+
def _execute_insert_heterogenous(transaction, sql_params_list):
59+
for sql, params in sql_params_list:
60+
sql, params = sql_pyformat_args_to_spanner(sql, params)
61+
param_types = get_param_types(params)
62+
transaction.execute_update(sql, params=params, param_types=param_types)
63+
64+
65+
def _execute_insert_homogenous(transaction, parts):
66+
# Perform an insert in one shot.
67+
table = parts.get("table")
68+
columns = parts.get("columns")
69+
values = parts.get("values")
70+
return transaction.insert(table, columns, values)
71+
72+
73+
def handle_insert(connection, sql, params):
74+
parts = parse_insert(sql, params)
75+
76+
# The split between the two styles exists because:
77+
# in the common case of multiple values being passed
78+
# with simple pyformat arguments,
79+
# SQL: INSERT INTO T (f1, f2) VALUES (%s, %s, %s)
80+
# Params: [(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,)]
81+
# we can take advantage of a single RPC with:
82+
# transaction.insert(table, columns, values)
83+
# instead of invoking:
84+
# with transaction:
85+
# for sql, params in sql_params_list:
86+
# transaction.execute_sql(sql, params, param_types)
87+
# which invokes more RPCs and is more costly.
88+
89+
if parts.get("homogenous"):
90+
# The common case of multiple values being passed in
91+
# non-complex pyformat args and need to be uploaded in one RPC.
92+
return connection.database.run_in_transaction(_execute_insert_homogenous, parts)
93+
else:
94+
# All the other cases that are esoteric and need
95+
# transaction.execute_sql
96+
sql_params_list = parts.get("sql_params_list")
97+
return connection.database.run_in_transaction(
98+
_execute_insert_heterogenous, sql_params_list
99+
)
100+
101+
102+
class ColumnInfo:
103+
"""Row column description object."""
104+
105+
def __init__(
106+
self,
107+
name,
108+
type_code,
109+
display_size=None,
110+
internal_size=None,
111+
precision=None,
112+
scale=None,
113+
null_ok=False,
114+
):
115+
self.name = name
116+
self.type_code = type_code
117+
self.display_size = display_size
118+
self.internal_size = internal_size
119+
self.precision = precision
120+
self.scale = scale
121+
self.null_ok = null_ok
122+
123+
self.fields = (
124+
self.name,
125+
self.type_code,
126+
self.display_size,
127+
self.internal_size,
128+
self.precision,
129+
self.scale,
130+
self.null_ok,
131+
)
132+
133+
def __repr__(self):
134+
return self.__str__()
135+
136+
def __getitem__(self, index):
137+
return self.fields[index]
138+
139+
def __str__(self):
140+
str_repr = ", ".join(
141+
filter(
142+
lambda part: part is not None,
143+
[
144+
"name='%s'" % self.name,
145+
"type_code=%d" % self.type_code,
146+
"display_size=%d" % self.display_size
147+
if self.display_size
148+
else None,
149+
"internal_size=%d" % self.internal_size
150+
if self.internal_size
151+
else None,
152+
"precision='%s'" % self.precision if self.precision else None,
153+
"scale='%s'" % self.scale if self.scale else None,
154+
"null_ok='%s'" % self.null_ok if self.null_ok else None,
155+
],
156+
)
157+
)
158+
return "ColumnInfo(%s)" % str_repr

0 commit comments

Comments
 (0)