8000 chore: unit test coverage and additional refactoring for `spanner_dbapi` by mf2199 · Pull Request #532 · googleapis/python-spanner-django · GitHub
[go: up one dir, main page]

Skip to content

chore: unit test coverage and additional refactoring for spanner_dbapi #532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Oct 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
92cc0b3
chore: updated docstrings
mf2199 Oct 6, 2020
c70ee50
Merge branch 'master' into refactor-cursor-alt
mf2199 Oct 6, 2020
da4eb61
chore: methods rearranged
mf2199 Oct 6, 2020
d27ff71
chore:
mf2199 Oct 6, 2020
eaa024f
chore:
mf2199 Oct 7, 2020
3cde688
fix: lint
mf2199 Oct 7, 2020
75c6f1c
chore: refactor
mf2199 Oct 7, 2020
4383d1c
chore: refactor
mf2199 Oct 8, 2020
f05307d
Merge remote-tracking branch 'upstream/master' into refactor-cursor-alt
mf2199 Oct 8, 2020
a5d6d30
fix: conflicts in `connection.py`
mf2199 Oct 8, 2020
2693582
test: coverage for `connection.py`
mf2199 Oct 9, 2020
dd38239
test: [WIP] `cursor.py` coverage
mf2199 Oct 10, 2020
03b440e
Merge branch 'master' into refactor-cursor-alt
mf2199 Oct 10, 2020
4640c46
fix: unittest assertions
mf2199 Oct 10, 2020
023ea90
chore: unit tests refactored
mf2199 Oct 10, 2020
9abee91
chore: unit test refact 8000 ored
mf2199 Oct 10, 2020
7dc3875
fix: missing arg
mf2199 Oct 10, 2020
02b3bd4
test: unit test coverage for `cursor.py'
mf2199 Oct 11, 2020
5e0ee36
fix: assertion call
mf2199 Oct 11, 2020
628f04a
fix: assertion call
mf2199 Oct 11, 2020
58c0bb6
test: unit test coverage for `parser.py`
mf2199 Oct 11, 2020
301ca84
fix: lint
mf2199 Oct 11, 2020
2e5ba9b
test: unit test coverage for `parse_utils.py`
mf2199 Oct 11, 2020
b4fde4b
test: unit test coverage for `_helpers.py`
mf2199 Oct 11, 2020
d585579
chore: `connection` attrubute made public in `cursor.py`
mf2199 Oct 12, 2020
7d8b016
Merge branch 'master' into refactor-cursor-alt
mf2199 Oct 14, 2020
a73e1d7
Merge branch 'master' into refactor-cursor-alt
c24t Oct 21, 2020
9337fd8
Merge branch 'refactor-cursor-alt' of https://github.com/q-logic/pyth…
mf2199 Oct 24, 2020
b535bf1
test: unit test for `Connection.autocommit` property setter
mf2199 Oct 24, 2020
81db6f7
test: unit test for `Connection._session_checkout`
mf2199 Oct 24, 2020
d484496
test: unit test for `Connection.transaction_checkout`
mf2199 Oct 24, 2020
4c0b83c
test: unit test for `Connection._release_session`
mf2199 Oct 24, 2020
6580e6a
test: updated unit test for `Connection.close`
mf2199 Oct 24, 2020
4b7f7d8
test: updated unit test for `Connection.commit`
mf2199 Oct 24, 2020
a6fcbdf
test: updated unit test for `Connection.rollback`
mf2199 Oct 24, 2020
b38982a
test: updated unit tests for `Cursor.execute`
mf2199 Oct 24, 2020
594c9bc
chore: cleanup
mf2199 Oct 24, 2020
da15d94
chore: cleanup
mf2199 Oct 24, 2020
2a78830
Merge pull request #21 from q-logic/q-logic-refactor-cursor-alt
mf2199 Oct 24, 2020
41abaeb
fix: implementing suggested changes
mf2199 Oct 25, 2020
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
125 changes: 35 additions & 90 deletions google/cloud/spanner_dbapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,39 @@

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

from google.cloud import spanner_v1

from .connection import Connection
from .exceptions import (
DatabaseError,
DataError,
Error,
IntegrityError,
InterfaceError,
InternalError,
NotSupportedError,
OperationalError,
ProgrammingError,
Warning,
)
from .parse_utils import get_param_types
from .types import (
BINARY,
DATETIME,
NUMBER,
ROWID,
STRING,
Binary,
Date,
DateFromTicks,
Time,
TimeFromTicks,
Timestamp,
TimestampFromTicks,
)
from .version import google_client_info
from google.cloud.spanner_dbapi.connection import Connection
from google.cloud.spanner_dbapi.connection import connect
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW our current isort config will mangle this formatting. Did you do this by hand?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did. It would work either way, and was more of the matter of a consistent visual appearance.


from google.cloud.spanner_dbapi.cursor import Cursor

from google.cloud.spanner_dbapi.exceptions import DatabaseError
from google.cloud.spanner_dbapi.exceptions import DataError
from google.cloud.spanner_dbapi.exceptions import Error
from google.cloud.spanner_dbapi.exceptions import IntegrityError
from google.cloud.spanner_dbapi.exceptions import InterfaceError
from google.cloud.spanner_dbapi.exceptions import InternalError
from google.cloud.spanner_dbapi.exceptions import NotSupportedError
from google.cloud.spanner_dbapi.exceptions import OperationalError
from google.cloud.spanner_dbapi.exceptions import ProgrammingError
from google.cloud.spanner_dbapi.exceptions import Warning

from google.cloud.spanner_dbapi.parse_utils import get_param_types

from google.cloud.spanner_dbapi.types import BINARY
from google.cloud.spanner_dbapi.types import DATETIME
from google.cloud.spanner_dbapi.types import NUMBER
from google.cloud.spanner_dbapi.types import ROWID
from google.cloud.spanner_dbapi.types import STRING
from google.cloud.spanner_dbapi.types import Binary
from google.cloud.spanner_dbapi.types import Date
from google.cloud.spanner_dbapi.types import DateFromTicks
from google.cloud.spanner_dbapi.types import Time
from google.cloud.spanner_dbapi.types import TimeFromTicks
from google.cloud.spanner_dbapi.types import Timestamp
from google.cloud.spanner_dbapi.types import TimestampStr
from google.cloud.spanner_dbapi.types import TimestampFromTicks

from google.cloud.spanner_dbapi.version import DEFAULT_USER_AGENT

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


def connect(
instance_id,
database_id,
project=None,
credentials=None,
pool=None,
user_agent=None,
):
"""
Create a connection to Cloud Spanner database.

:type instance_id: :class:`str`
:param instance_id: ID of the instance to connect to.

:type database_id: :class:`str`
:param database_id: The name of the database to connect to.

:type project: :class:`str`
:param project: (Optional) The ID of the project which owns the
instances, tables and data. If not provided, will
attempt to determine from the environment.

:type credentials: :class:`google.auth.credentials.Credentials`
:param credentials: (Optional) The authorization credentials to attach to requests.
These credentials identify this application to the service.
If none are specified, the client will attempt to ascertain
the credentials from the environment.

:type pool: Concrete subclass of
:class:`~google.cloud.spanner_v1.pool.AbstractSessionPool`.
:param pool: (Optional). Session pool to be used by database.

:type user_agent: :class:`str`
:param user_agent: (Optional) User agent to be used with this connection requests.

:rtype: :class:`google.cloud.spanner_dbapi.connection.Connection`
:returns: Connection object associated with the given Cloud Spanner resource.

:raises: :class:`ValueError` in case of given instance/database
doesn't exist.
"""
client = spanner_v1.Client(
project=project,
credentials=credentials,
client_info=google_client_info(user_agent),
)

instance = client.instance(instance_id)
if not instance.exists():
raise ValueError("instance '%s' does not exist." % instance_id)

database = instance.database(database_id, pool=pool)
if not database.exists():
raise ValueError("database '%s' does not exist." % database_id)

return Connection(instance, database)


__all__ = [
"Connection",
"connect",
"Cursor",
"DatabaseError",
"DataError",
"Error",
Expand All @@ -120,7 +66,6 @@ def connect(
"Warning",
"DEFAULT_USER_AGENT",
"apilevel",
"connect",
"paramstyle",
"threadsafety",
"get_param_types",
Expand Down
159 changes: 159 additions & 0 deletions google/cloud/spanner_dbapi/_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Copyright 2020 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

from google.cloud.spanner_dbapi.parse_utils import get_param_types
from google.cloud.spanner_dbapi.parse_utils import parse_insert
from google.cloud.spanner_dbapi.parse_utils import sql_pyformat_args_to_spanner
from google.cloud.spanner_v1 import param_types


SQL_LIST_TABLES = """
SELECT
t.table_name
FROM
information_schema.tables AS t
WHERE
t.table_catalog = '' and t.table_schema = ''
"""

SQL_GET_TABLE_COLUMN_SCHEMA = """SELECT
COLUMN_NAME, IS_NULLABLE, SPANNER_TYPE
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = ''
AND
TABLE_NAME = @table_name
"""

# This table maps spanner_types to Spanner's data type sizes as per
# https://cloud.google.com/spanner/docs/data-types#allowable-types
# It is used to map `display_size` to a known type for Cursor.description
# after a row fetch.
# Since ResultMetadata
# https://cloud.google.com/spanner/docs/reference/rest/v1/ResultSetMetadata
# does not send back the actual size, we have to lookup the respective size.
# Some fields' sizes are dependent upon the dynamic data hence aren't sent back
# by Cloud Spanner.
code_to_display_size = {
param_types.BOOL.code: 1,
param_types.DATE.code: 4,
param_types.FLOAT64.code: 8,
param_types.INT64.code: 8,
param_types.TIMESTAMP.code: 12,
}


def _execute_insert_heterogenous(transaction, sql_params_list):
for sql, params in sql_params_list:
sql, params = sql_pyformat_args_to_spanner(sql, params)
param_types = get_param_types(params)
res = transaction.execute_sql(
sql, params=params, param_types=param_types
)
# TODO: File a bug with Cloud Spanner and the Python client maintainers
# about a lost commit when res isn't read from.
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know your change just moved this comment, but I wonder what the story is here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a good question for the original code developer who unfortunately is no longer a member of our team. I'm not even sure what's the purpose of res, as the function does not return anything. We should probably revise this method later.

_ = list(res)


def _execute_insert_homogenous(transaction, parts):
# Perform an insert in one shot.
table = parts.get("table")
columns = parts.get("columns")
values = parts.get("values")
return transaction.insert(table, columns, values)


def handle_insert(connection, sql, params):
parts = parse_insert(sql, params)

# The split between the two styles exists because:
# in the common case of multiple values being passed
# with simple pyformat arguments,
# SQL: INSERT INTO T (f1, f2) VALUES (%s, %s, %s)
# Params: [(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,)]
# we can take advantage of a single RPC with:
# transaction.insert(table, columns, values)
# instead of invoking:
# with transaction:
# for sql, params in sql_params_list:
# transaction.execute_sql(sql, params, param_types)
# which invokes more RPCs and is more costly.

if parts.get("homogenous"):
# The common case of multiple values being passed in
# non-complex pyformat args and need to be uploaded in one RPC.
return connection.database.run_in_transaction(
_execute_insert_homogenous, parts
)
else:
# All the other cases that are esoteric and need
# transaction.execute_sql
sql_params_list = parts.get("sql_params_list")
return connection.database.run_in_transaction(
_execute_insert_heterogenous, sql_params_list
)


class ColumnInfo:
"""Row column description object."""

def __init__(
self,
name,
type_code,
display_size=None,
internal_size=None,
precision=None,
scale=None,
null_ok=False,
):
self.name = name
self.type_code = type_code
self.display_size = display_size
self.internal_size = internal_size
self.precision = precision
self.scale = scale
self.null_ok = null_ok

self.fields = (
self.name,
self.type_code,
self.display_size,
self.internal_size,
self.precision,
self.scale,
self.null_ok,
)

def __repr__(self):
return self.__str__()

def __getitem__(self, index):
return self.fields[index]

def __str__(self):
str_repr = ", ".join(
filter(
lambda part: part is not None,
[
"name='%s'" % self.name,
"type_code=%d" % self.type_code,
"display_size=%d" % self.display_size
if self.display_size
else None,
"internal_size=%d" % self.internal_size
if self.internal_size
else None,
"precision='%s'" % self.precision
if self.precision
else None,
"scale='%s'" % self.scale if self.scale else None,
"null_ok='%s'" % self.null_ok if self.null_ok else None,
],
)
)
return "ColumnInfo(%s)" % str_repr
Loading
0