Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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 <libc@google.com>
  • Loading branch information
mf2199 and c24t committed Nov 11, 2020
1 parent bf4b278 commit 2493fa1
Show file tree
Hide file tree
Showing 20 changed files with 3,774 additions and 1 deletion.
93 changes: 93 additions & 0 deletions google/cloud/spanner_dbapi/__init__.py
@@ -0,0 +1,93 @@
# Copyright 2020 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

from google.cloud.spanner_dbapi.connection import Connection
from google.cloud.spanner_dbapi.connection import connect

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.

# Threads may share the module, but not connections. This is a paranoid threadsafety
# level, but it is necessary for starters to use when debugging failures.
# Eventually once transactions are working properly, we'll update the
# threadsafety level.
threadsafety = 1


__all__ = [
"Connection",
"connect",
"Cursor",
"DatabaseError",
"DataError",
"Error",
"IntegrityError",
"InterfaceError",
"InternalError",
"NotSupportedError",
"OperationalError",
"ProgrammingError",
"Warning",
"DEFAULT_USER_AGENT",
"apilevel",
"paramstyle",
"threadsafety",
"get_param_types",
"Binary",
"Date",
"DateFromTicks",
"Time",
"TimeFromTicks",
"Timestamp",
"TimestampFromTicks",
"BINARY",
"STRING",
"NUMBER",
"DATETIME",
"ROWID",
"TimestampStr",
]
158 changes: 158 additions & 0 deletions google/cloud/spanner_dbapi/_helpers.py
@@ -0,0 +1,158 @@
# Copyright 2020 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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)
transaction.execute_update(sql, params=params, param_types=param_types)


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

0 comments on commit 2493fa1

Please sign in to comment.