Skip to content

WIP: DB-API CURSOR CLASS #8

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

Draft
wants to merge 23 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
11222db
feat: release 2.2a0 (#457)
larkee May 26, 2020
5551b58
feat: [WIP] The first stage of `nox` implementation (#468)
mf2199 Aug 25, 2020
bfde221
feat: refactor connect() function, cover it with unit tests (#462)
Aug 27, 2020
e017901
feat: Stage 2 of `nox` implementation - adding `docs` target (#473)
mf2199 Aug 29, 2020
144bdc2
fix: Fix black, isort compatibility (#469)
c24t Aug 31, 2020
7a1f6a6
feat: Stage 3-4 of `nox` implementation - adding auto-format targets …
mf2199 Aug 31, 2020
acd9209
feat: Stage 5 of `nox` implementation - adding coverage targets (#479)
mf2199 Aug 31, 2020
6028f88
feat: cursor must detect if the parent connection is closed (#463)
Sep 1, 2020
94ba284
feat: Stage 6 of `nox` implementation - enabling system tests (#480)
mf2199 Sep 8, 2020
59eb432
chore: Code refactoring to follow common Google API scheme - Stage I …
mf2199 Sep 14, 2020
048566c
chore: release 2.2a1 (#499)
c24t Sep 15, 2020
f5719f8
Merge branch 'master' of https://github.com/googleapis/python-spanner…
Sep 15, 2020
acd6728
feat: BASELINE for Connection class implementation
mf2199 Sep 29, 2020
9f75747
feat: BASELINE for Cursor class implementation
mf2199 Sep 29, 2020
20edac9
refactor: refactor Connection() class and cover it with unit tests
Sep 30, 2020
ba6afc7
Merge branch 'master' into dbapi-connection
Sep 30, 2020
7e99914
format: docstrings
mf2199 Oct 1, 2020
9f3907b
refactor: reworked `Connection` class
paul1319 Oct 1, 2020
770202c
Merge remote-tracking branch 'origin/dbapi-connection' into dbapi-con…
paul1319 Oct 1, 2020
b94ae81
Merge branch 'dbapi-connection' into dbapi-cursor
paul1319 Oct 8, 2020
1d0358d
refactor: reworked `Cursor` class
paul1319 Oct 8, 2020
e6ca84e
added tests
paul1319 Oct 8, 2020
2c164e1
lint fix
paul1319 Oct 8, 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
2 changes: 1 addition & 1 deletion google/cloud/spanner_dbapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def connect(
if not database.exists():
raise ValueError("database '%s' does not exist." % database_id)

return Connection(database)
return Connection(instance, database)


__all__ = [
Expand Down
175 changes: 66 additions & 109 deletions google/cloud/spanner_dbapi/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,141 +4,98 @@
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

from collections import namedtuple

from google.cloud import spanner_v1 as spanner
"""DB-API driver Connection implementation for Google Cloud Spanner.

from .cursor import Cursor
from .exceptions import InterfaceError
See
https://www.python.org/dev/peps/pep-0249/#connection-objects
"""

ColumnDetails = namedtuple("column_details", ["null_ok", "spanner_type"])
from collections import namedtuple
from weakref import WeakSet

from .cursor import Cursor
from .exceptions import InterfaceError, Warning
from .enums import AutocommitDMLModes, TransactionModes

class Connection:
def __init__(self, db_handle):
self._dbhandle = db_handle
self._ddl_statements = []

self.is_closed = False
ColumnDetails = namedtuple("column_details", ["null_ok", "spanner_type"])

def cursor(self):
self._raise_if_closed()

return Cursor(self)
class Connection(object):
"""Representation of a connection to a Cloud Spanner database.

def _raise_if_closed(self):
"""Raise an exception if this connection is closed.
You most likely don't need to instantiate `Connection` objects
directly, use the `connect` module function instead.

Helper to check the connection state before
running a SQL/DDL/DML query.
:type instance: :class:`~google.cloud.spanner_v1.instance.Instance`
:param instance: Cloud Spanner instance to connect to.

:raises: :class:`InterfaceError` if this connection is closed.
"""
if self.is_closed:
raise InterfaceError("connection is already closed")
:type database: :class:`~google.cloud.spanner_v1.database.Database`
:param database: Cloud Spanner database to connect to.
"""

def __handle_update_ddl(self, ddl_statements):
"""
Run the list of Data Definition Language (DDL) statements on the underlying
database. Each DDL statement MUST NOT contain a semicolon.
Args:
ddl_statements: a list of DDL statements, each without a semicolon.
Returns:
google.api_core.operation.Operation.result()
"""
self._raise_if_closed()
# Synchronously wait on the operation's completion.
return self._dbhandle.update_ddl(ddl_statements).result()

def read_snapshot(self):
self._raise_if_closed()
return self._dbhandle.snapshot()

def in_transaction(self, fn, *args, **kwargs):
self._raise_if_closed()
return self._dbhandle.run_in_transaction(fn, *args, **kwargs)

def append_ddl_statement(self, ddl_statement):
self._raise_if_closed()
self._ddl_statements.append(ddl_statement)

def run_prior_DDL_statements(self):
self._raise_if_closed()

if not self._ddl_statements:
return

ddl_statements = self._ddl_statements
self._ddl_statements = []

return self.__handle_update_ddl(ddl_statements)

def list_tables(self):
return self.run_sql_in_snapshot(
"""
SELECT
t.table_name
FROM
information_schema.tables AS t
WHERE
t.table_catalog = '' and t.table_schema = ''
"""
def __init__(self, instance, database):
self.instance = instance
self.database = database
self.autocommit = True
self.read_only = False
self.transaction_mode = (
TransactionModes.READ_ONLY
if self.read_only
else TransactionModes.READ_WRITE
)
self.autocommit_dml_mode = AutocommitDMLModes.TRANSACTIONAL
self.timeout_secs = 0
self.read_timestamp = None
self.commit_timestamp = None
self._is_closed = False
self._inside_transaction = not self.autocommit
self._transaction_started = False
self._cursors = WeakSet()
self.read_only_staleness = {}

@property
def is_closed(self):
return self._is_closed

@property
def inside_transaction(self):
return self._inside_transaction

@property
def transaction_started(self):
return self._transaction_started

def run_sql_in_snapshot(self, sql, params=None, param_types=None):
# Some SQL e.g. for INFORMATION_SCHEMA cannot be run in read-write transactions
# hence this method exists to circumvent that limit.
self.run_prior_DDL_statements()

with self._dbhandle.snapshot() as snapshot:
res = snapshot.execute_sql(
sql, params=params, param_types=param_types
)
return list(res)

def get_table_column_schema(self, table_name):
rows = self.run_sql_in_snapshot(
"""SELECT
COLUMN_NAME, IS_NULLABLE, SPANNER_TYPE
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = ''
AND
TABLE_NAME = @table_name""",
params={"table_name": table_name},
param_types={"table_name": spanner.param_types.STRING},
)
def cursor(self):
"""Returns cursor for current connection"""
if self._is_closed:
raise InterfaceError("connection is already closed")

column_details = {}
for column_name, is_nullable, spanner_type in rows:
column_details[column_name] = ColumnDetails(
null_ok=is_nullable == "YES", spanner_type=spanner_type
)
return column_details
return Cursor(self)

def close(self):
"""Close this connection.

The connection will be unusable from this point forward.
"""
self.rollback()
self.__dbhandle = None
self.is_closed = True
self._is_closed = True

def commit(self):
self._raise_if_closed()

self.run_prior_DDL_statements()
"""Commit all the pending transactions."""
raise Warning(
"Cloud Spanner DB API always works in `autocommit` mode."
"See https://github.com/googleapis/python-spanner-django#transaction-management-isnt-supported"
)

def rollback(self):
self._raise_if_closed()

# TODO: to be added.
"""Rollback all the pending transactions."""
raise Warning(
"Cloud Spanner DB API always works in `autocommit` mode."
"See https://github.com/googleapis/python-spanner-django#transaction-management-isnt-supported"
)

def __enter__(self):
return self

def __exit__(self, etype, value, traceback):
self.commit()
def __exit__(self, exc_type, exc_value, traceback):
self.close()
Loading