Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d11a0c4

Browse files
committed
dbapi: make duplicate table error return DatabaseError + refactor
spanner-python has a bug due to improper status code extraction from long running operations as per googleapis/python-spanner#38 This change adds error translation for update_ddl invocations. While here, refactored common code for Connection present in both: * autocommit_off_connection.py * autocommit_on_connection.py which makes it easier to add error handling in one place instead of in 2 places. Fixes #344
1 parent 44736b5 commit d11a0c4

File tree

3 files changed

+113
-162
lines changed

3 files changed

+113
-162
lines changed

spanner/dbapi/autocommit_off_connection.py

Lines changed: 8 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,34 @@
55
# https://developers.google.com/open-source/licenses/bsd
66

77
from .autocommit_off_cursor import Cursor
8-
from .exceptions import Error
8+
from .base_connection import BaseConnection
99
from .periodic_auto_refresh import PeriodicAutoRefreshingTransaction
10-
from .utils import get_table_column_schema as get_table_column_schema_impl
1110

1211

13-
class Connection(object):
12+
class Connection(BaseConnection):
1413
def __init__(self, db_handle, session, discard_session):
14+
super(Connection, self).__init__(db_handle)
1515
self.__sess = session
1616
self.__discard_session = discard_session
1717
self.__txn = None
18-
self.__dbhandle = db_handle
19-
self.__closed = False
2018
self.__on_transaction_clean_up = None
21-
self.__ddl_statements = []
22-
23-
def __raise_if_already_closed(self):
24-
"""
25-
Raises an exception if attempting to use an already closed connection.
26-
"""
27-
if self.__closed:
28-
raise Error('attempting to use an already closed connection')
19+
self._ddl_statements = []
2920

3021
def close(self):
3122
self.rollback()
3223
self.__clear()
33-
self.__closed = True
24+
self._closed = True
3425

3526
def __enter__(self):
3627
return self
3728

3829
def __clear(self):
39-
self.__dbhandle = None
30+
self._dbhandle = None
4031
self.__discard_session()
4132
self.__sess = None
4233

4334
def __exit__(self, etype, value, traceback):
44-
self.__raise_if_already_closed()
35+
self._raise_if_already_closed()
4536

4637
self.run_prior_DDL_statements()
4738

@@ -83,7 +74,7 @@ def rollback(self):
8374
self.__txn.stop()
8475

8576
def cursor(self):
86-
self.__raise_if_already_closed()
77+
self._raise_if_already_closed()
8778

8879
cur = Cursor(self)
8980
self.__on_transaction_clean_up = cur._clear_transaction_state
@@ -115,68 +106,3 @@ def get_txn(self):
115106
self.__txn = self.get_txn()
116107

117108
return self.__txn
118-
119-
def append_ddl_statement(self, ddl_statement):
120-
self.__ddl_statements.append(ddl_statement)
121-
122-
def run_prior_DDL_statements(self):
123-
"""
124-
Runs the list of saved Data Definition Language (DDL) statements on the underlying
125-
database. Note that each DDL statement MUST NOT contain a semicolon.
126-
127-
Args:
128-
ddl_statements: a list of DDL statements, each without a semicolon.
129-
130-
Returns:
131-
google.api_core.operation.Operation.result()
132-
"""
133-
self.__raise_if_already_closed()
134-
135-
if not self.__ddl_statements:
136-
return
137-
138-
# DDL and Transactions in Cloud Spanner don't mix thus before any DDL is executed,
139-
# any prior transaction MUST have been committed. This behavior is also present
140-
# on MySQL. Please see:
141-
# * https://gist.github.com/odeke-em/8e02576d8523e07eb27b43a772aecc92
142-
# * https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
143-
# * https://wiki.postgresql.org/wiki/Transactional_DDL_in_PostgreSQL:_A_Competitive_Analysis
144-
self.commit()
145-
146-
ddl_statements = self.__ddl_statements
147-
self.__ddl_statements = []
148-
return self.__dbhandle.update_ddl(ddl_statements).result()
149-
150-
def __update_ddl(self, ddl_statements):
151-
"""
152-
Runs the list of Data Definition Language (DDL) statements on the specified
153-
database. Note that each DDL statement MUST NOT contain a semicolon.
154-
Args:
155-
ddl_statements: a list of DDL statements, each without a semicolon.
156-
Returns:
157-
google.api_core.operation.Operation
158-
"""
159-
# Synchronously wait on the operation's completion.
160-
return self.__dbhandle.update_ddl(ddl_statements).result()
161-
162-
def list_tables(self):
163-
return self.run_sql_in_snapshot("""
164-
SELECT
165-
t.table_name
166-
FROM
167-
information_schema.tables AS t
168-
WHERE
169-
t.table_catalog = '' and t.table_schema = ''
170-
""")
171-
172-
def run_sql_in_snapshot(self, sql):
173-
# Some SQL e.g. for INFORMATION_SCHEMA cannot be run in read-write transactions
174-
# hence this method exists to circumvent that limit.
175-
self.run_prior_DDL_statements()
176-
177-
with self.__dbhandle.snapshot() as snapshot:
178-
res = snapshot.execute_sql(sql)
179-
return list(res)
180-
181-
def get_table_column_schema(self, table_name):
182-
return get_table_column_schema_impl(self.__connection, table_name)

spanner/dbapi/autocommit_on_connection.py

Lines changed: 6 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,14 @@
55
# https://developers.google.com/open-source/licenses/bsd
66

77
from .autocommit_on_cursor import Cursor
8-
from .exceptions import Error
9-
from .utils import get_table_column_schema as get_table_column_schema_impl
8+
from .base_connection import BaseConnection
109

1110

12-
class Connection(object):
13-
def __init__(self, db_handle, *args, **kwargs):
14-
self.__dbhandle = db_handle
15-
self.__closed = False
16-
self.__ddl_statements = []
17-
18-
def __raise_if_already_closed(self):
19-
"""
20-
Raises an exception if attempting to use an already closed connection.
21-
"""
22-
if self.__closed:
23-
raise Error('attempting to use an already closed connection')
24-
11+
class Connection(BaseConnection):
2512
def close(self):
2613
self.rollback()
2714
self.__dbhandle = None
28-
self.__closed = True
15+
self._closed = True
2916

3017
def __enter__(self):
3118
return self
@@ -35,77 +22,16 @@ def __exit__(self, etype, value, traceback):
3522
self.close()
3623

3724
def commit(self):
38-
self.__raise_if_already_closed()
25+
self._raise_if_already_closed()
3926

4027
self.run_prior_DDL_statements()
4128

4229
def rollback(self):
43-
self.__raise_if_already_closed()
30+
self._raise_if_already_closed()
4431

4532
# TODO: to be added.
4633

4734
def cursor(self):
48-
self.__raise_if_already_closed()
35+
self._raise_if_already_closed()
4936

5037
return Cursor(self)
51-
52-
def __handle_update_ddl(self, ddl_statements):
53-
"""
54-
Runs the list of Data Definition Language (DDL) statements on the underlying
55-
database. Note that each DDL statement MUST NOT contain a semicolon.
56-
Args:
57-
ddl_statements: a list of DDL statements, each without a semicolon.
58-
Returns:
59-
google.api_core.operation.Operation.result()
60-
"""
61-
self.__raise_if_already_closed()
62-
63-
# Synchronously wait on the operation's completion.
64-
return self.__dbhandle.update_ddl(ddl_statements).result()
65-
66-
def read_snapshot(self):
67-
self.__raise_if_already_closed()
68-
69-
return self.__dbhandle.snapshot()
70-
71-
def in_transaction(self, fn, *args, **kwargs):
72-
self.__raise_if_already_closed()
73-
74-
return self.__dbhandle.run_in_transaction(fn, *args, **kwargs)
75-
76-
def append_ddl_statement(self, ddl_statement):
77-
self.__raise_if_already_closed()
78-
79-
self.__ddl_statements.append(ddl_statement)
80-
81-
def run_prior_DDL_statements(self):
82-
self.__raise_if_already_closed()
83-
84-
if not self.__ddl_statements:
85-
return
86-
87-
ddl_statements = self.__ddl_statements
88-
self.__ddl_statements = []
89-
return self.__handle_update_ddl(ddl_statements)
90-
91-
def list_tables(self):
92-
return self.run_sql_in_snapshot("""
93-
SELECT
94-
t.table_name
95-
FROM
96-
information_schema.tables AS t
97-
WHERE
98-
t.table_catalog = '' and t.table_schema = ''
99-
""")
100-
101-
def run_sql_in_snapshot(self, sql):
102-
# Some SQL e.g. for INFORMATION_SCHEMA cannot be run in read-write transactions
103-
# hence this method exists to circumvent that limit.
104-
self.run_prior_DDL_statements()
105-
106-
with self.__dbhandle.snapshot() as snapshot:
107-
res = snapshot.execute_sql(sql)
108-
return list(res)
109-
110-
def get_table_column_schema(self, table_name):
111-
return get_table_column_schema_impl(self.__dbhandle, table_name)

spanner/dbapi/base_connection.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
import google.api_core.exceptions as grpc_exceptions
8+
9+
from .exceptions import DatabaseError, Error
10+
from .utils import get_table_column_schema as get_table_column_schema_impl
11+
12+
13+
class BaseConnection(object):
14+
def __init__(self, db_handle, *args, **kwargs):
15+
self._dbhandle = db_handle
16+
self._closed = False
17+
self._ddl_statements = []
18+
19+
def _raise_if_already_closed(self):
20+
"""
21+
Raises an exception if attempting to use an already closed connection.
22+
"""
23+
if self._closed:
24+
raise Error('attempting to use an already closed connection')
25+
26+
def __handle_update_ddl(self, ddl_statements):
27+
"""
28+
Runs the list of Data Definition Language (DDL) statements on the underlying
29+
database. Note that each DDL statement MUST NOT contain a semicolon.
30+
Args:
31+
ddl_statements: a list of DDL statements, each without a semicolon.
32+
Returns:
33+
google.api_core.operation.Operation.result()
34+
"""
35+
self._raise_if_already_closed()
36+
37+
# Synchronously wait on the operation's completion.
38+
return self._dbhandle.update_ddl(ddl_statements).result()
39+
40+
def read_snapshot(self):
41+
self._raise_if_already_closed()
42+
43+
return self._dbhandle.snapshot()
44+
45+
def in_transaction(self, fn, *args, **kwargs):
46+
self._raise_if_already_closed()
47+
48+
return self._dbhandle.run_in_transaction(fn, *args, **kwargs)
49+
50+
def append_ddl_statement(self, ddl_statement):
51+
self._raise_if_already_closed()
52+
53+
self._ddl_statements.append(ddl_statement)
54+
55+
def run_prior_DDL_statements(self):
56+
self._raise_if_already_closed()
57+
58+
if not self._ddl_statements:
59+
return
60+
61+
ddl_statements = self._ddl_statements
62+
self._ddl_statements = []
63+
64+
try:
65+
self.__handle_update_ddl(ddl_statements)
66+
except (grpc_exceptions.AlreadyExists, grpc_exceptions.FailedPrecondition) as e:
67+
raise DatabaseError(e.details if hasattr(e, 'details') else e)
68+
except Exception as e:
69+
# spanner-python's DatabaseAdmin is buggy and mishandles the gRPC FailedPrecondition
70+
# by instead return None, yet with a message.
71+
# See issue https://github.com/googleapis/python-spanner/issues/38
72+
# For now we'll check the returned message until that issue is fixed.
73+
# TODO: Remove this check when issue 38 is fixed in spanner-python.
74+
if e.message.startswith('Duplicate name'):
75+
raise DatabaseError(e.details if hasattr(e, 'details') else e)
76+
else:
77+
raise e
78+
79+
def list_tables(self):
80+
return self.run_sql_in_snapshot("""
81+
SELECT
82+
t.table_name
83+
FROM
84+
information_schema.tables AS t
85+
WHERE
86+
t.table_catalog = '' and t.table_schema = ''
87+
""")
88+
89+
def run_sql_in_snapshot(self, sql):
90+
# Some SQL e.g. for INFORMATION_SCHEMA cannot be run in read-write transactions
91+
# hence this method exists to circumvent that limit.
92+
self.run_prior_DDL_statements()
93+
94+
with self._dbhandle.snapshot() as snapshot:
95+
res = snapshot.execute_sql(sql)
96+
return list(res)
97+
98+
def get_table_column_schema(self, table_name):
99+
return get_table_column_schema_impl(self._dbhandle, table_name)

0 commit comments

Comments
 (0)