Skip to content

Commit 4d40f07

Browse files
Implemented dbapi2 interface in dbapi module. See [1] for details.
The main motivation for the module creation was the integration Django with Tarantool database through django-tarantool database backend [2] which requires dbapi connector to the database. All the optional extensions and methods were ignored because Django does not require them. Anyway, fell free to suggest its implementation as needed. Unit tests were taken from dbapi-compliance [3] package. Interactive transactions are not currently supported by Tarantool and theirs implementation will be added in the connector when the feature is stable it Tarantool itself. [1] https://www.python.org/dev/peps/pep-0249/ [2] https://github.com/artembo/django-tarantool [3] https://github.com/baztian/dbapi-compliance/ Co-authored-by: Denis Ignatenko <[email protected]>
1 parent 7900184 commit 4d40f07

File tree

7 files changed

+375
-14
lines changed

7 files changed

+375
-14
lines changed

Diff for: tarantool/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@ def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None,
7575

7676
__all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema',
7777
'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning',
78-
'SchemaError']
78+
'SchemaError', 'dbapi']

Diff for: tarantool/connection.py

+24-8
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
import msgpack
2020

21-
import tarantool.error
2221
from tarantool.response import Response
2322
from tarantool.request import (
2423
Request,
24+
RequestSQL,
2525
# RequestOK,
2626
RequestCall,
2727
RequestDelete,
@@ -34,8 +34,7 @@
3434
RequestSubscribe,
3535
RequestUpdate,
3636
RequestUpsert,
37-
RequestAuthenticate,
38-
RequestExecute
37+
RequestAuthenticate
3938
)
4039
from tarantool.space import Space
4140
from tarantool.const import (
@@ -50,12 +49,20 @@
5049
ITERATOR_ALL
5150
)
5251
from tarantool.error import (
52+
Error,
5353
NetworkError,
5454
DatabaseError,
5555
InterfaceError,
5656
SchemaError,
5757
NetworkWarning,
58+
OperationalError,
59+
DataError,
60+
IntegrityError,
61+
InternalError,
62+
ProgrammingError,
63+
NotSupportedError,
5864
SchemaReloadException,
65+
Warning,
5966
warn
6067
)
6168
from tarantool.schema import Schema
@@ -77,11 +84,19 @@ class Connection(object):
7784
Also this class provides low-level interface to data manipulation
7885
(insert/delete/update/select).
7986
'''
80-
Error = tarantool.error
87+
# DBAPI Extension: supply exceptions as attributes on the connection
88+
Error = Error
8189
DatabaseError = DatabaseError
8290
InterfaceError = InterfaceError
8391
SchemaError = SchemaError
8492
NetworkError = NetworkError
93+
Warning = Warning
94+
DataError = DataError
95+
OperationalError = OperationalError
96+
IntegrityError = IntegrityError
97+
InternalError = InternalError
98+
ProgrammingError = ProgrammingError
99+
NotSupportedError = NotSupportedError
85100

86101
def __init__(self, host, port,
87102
user=None,
@@ -251,17 +266,18 @@ def _read_response(self):
251266

252267
def _send_request_wo_reconnect(self, request):
253268
'''
254-
:rtype: `Response` instance
269+
:rtype: `Response` instance or subclass
255270
256271
:raise: NetworkError
257272
'''
258-
assert isinstance(request, Request)
273+
if not issubclass(type(request), Request):
274+
raise
259275

260276
response = None
261277
while True:
262278
try:
263279
self._socket.sendall(bytes(request))
264-
response = Response(self, self._read_response())
280+
response = request.response_class(self, self._read_response())
265281
break
266282
except SchemaReloadException as e:
267283
self.update_schema(e.schema_version)
@@ -803,6 +819,6 @@ def execute(self, query, params=None):
803819
'''
804820
if not params:
805821
params = []
806-
request = RequestExecute(self, query, params)
822+
request = RequestSQL(self, query, params)
807823
response = self._send_request(request)
808824
return response

Diff for: tarantool/const.py

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
IPROTO_SQL_TEXT = 0x40
3535
IPROTO_SQL_BIND = 0x41
3636
IPROTO_SQL_INFO = 0x42
37+
IPROTO_SQL_INFO_ROW_COUNT = 0x00
38+
IPROTO_SQL_INFO_AUTOINCREMENT_ID = 0x01
3739

3840
IPROTO_GREETING_SIZE = 128
3941
IPROTO_BODY_MAX_LEN = 2147483648

Diff for: tarantool/dbapi.py

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
# -*- coding: utf-8 -*-
2+
from tarantool.error import InterfaceError
3+
from .connection import Connection as BaseConnection
4+
5+
6+
class Cursor:
7+
_lastrowid = 0
8+
_rowcount = 0
9+
description = None
10+
arraysize = 1
11+
autocommit = True
12+
closed = False
13+
rows = None
14+
15+
def __init__(self, conn):
16+
self._c = conn
17+
18+
def callproc(self, procname, *params): # TODO
19+
"""
20+
Call a stored database procedure with the given name. The sequence of
21+
parameters must contain one entry for each argument that the
22+
procedure expects. The result of the call is returned as modified
23+
copy of the input sequence. Input parameters are left untouched,
24+
output and input/output parameters replaced with possibly new values.
25+
26+
The procedure may also provide a result set as output. This must then
27+
be made available through the standard .fetch*() methods.
28+
"""
29+
30+
def close(self):
31+
"""
32+
Close the cursor now (rather than whenever __del__ is called).
33+
34+
The cursor will be unusable from this point forward; an Error (or
35+
subclass) exception will be raised if any operation is attempted with
36+
the cursor.
37+
"""
38+
self._c = None
39+
40+
@staticmethod
41+
def _extract_last_row_id(body):
42+
try:
43+
val = next(iter(body.values())).get(1)[-1]
44+
except TypeError:
45+
val = -1
46+
return val
47+
48+
def execute(self, query, params=None):
49+
"""
50+
Prepare and execute a database operation (query or command).
51+
52+
Parameters may be provided as sequence or mapping and will be bound
53+
to variables in the operation. Variables are specified in a
54+
database-specific notation (see the module's paramstyle attribute for
55+
details).
56+
57+
A reference to the operation will be retained by the cursor. If the
58+
same operation object is passed in again, then the cursor can
59+
optimize its behavior. This is most effective for algorithms where
60+
the same operation is used, but different parameters are bound to it
61+
(many times).
62+
63+
For maximum efficiency when reusing an operation, it is best to use
64+
the .setinputsizes() method to specify the parameter types and sizes
65+
ahead of time. It is legal for a parameter to not match the
66+
predefined information; the implementation should compensate,
67+
possibly with a loss of efficiency.
68+
69+
The parameters may also be specified as list of tuples to e.g. insert
70+
multiple rows in a single operation, but this kind of usage is
71+
deprecated: .executemany() should be used instead.
72+
73+
Return values are not defined.
74+
"""
75+
if self.closed:
76+
raise self._c.ProgrammingError
77+
78+
response = self._c.execute(query, params)
79+
self.rows = tuple(response.data) if len(
80+
response.body) > 1 else None
81+
82+
self._rowcount = response.rowcount
83+
self._lastrowid = response.lastrowid
84+
85+
def executemany(self, query, param_sets):
86+
rowcounts = []
87+
for params in param_sets:
88+
self.execute(query, params)
89+
rowcounts.append(self.rowcount)
90+
91+
self._rowcount = -1 if -1 in rowcounts else sum(rowcounts)
92+
93+
@property
94+
def lastrowid(self):
95+
"""
96+
This read-only attribute provides the rowid of the last modified row
97+
(most databases return a rowid only when a single INSERT operation is
98+
performed). If the operation does not set a rowid or if the database
99+
does not support rowids, this attribute should be set to None.
100+
101+
The semantics of .lastrowid are undefined in case the last executed
102+
statement modified more than one row, e.g. when using INSERT with
103+
.executemany().
104+
105+
Warning Message: "DB-API extension cursor.lastrowid used"
106+
"""
107+
return self._lastrowid
108+
109+
@property
110+
def rowcount(self):
111+
"""
112+
This read-only attribute specifies the number of rows that the last
113+
.execute*() produced (for DQL statements like SELECT) or affected (
114+
for DML statements like UPDATE or INSERT).
115+
116+
The attribute is -1 in case no .execute*() has been performed on the
117+
cursor or the rowcount of the last operation is cannot be determined
118+
by the interface.
119+
120+
Note:
121+
Future versions of the DB API specification could redefine the latter
122+
case to have the object return None instead of -1.
123+
"""
124+
return self._rowcount
125+
126+
def fetchone(self):
127+
"""
128+
Fetch the next row of a query result set, returning a single
129+
sequence, or None when no more data is available.
130+
131+
An Error (or subclass) exception is raised if the previous call to
132+
.execute*() did not produce any result set or no call was issued yet.
133+
"""
134+
if self.rows is None:
135+
raise self._c.ProgrammingError('Nothing to fetch')
136+
return self.fetchmany(1)[0] if len(self.rows) else None
137+
138+
def fetchmany(self, size=None):
139+
"""
140+
Fetch the next set of rows of a query result, returning a sequence of
141+
sequences (e.g. a list of tuples). An empty sequence is returned when
142+
no more rows are available.
143+
144+
The number of rows to fetch per call is specified by the parameter.
145+
If it is not given, the cursor's arraysize determines the number of
146+
rows to be fetched. The method should try to fetch as many rows as
147+
indicated by the size parameter. If this is not possible due to the
148+
specified number of rows not being available, fewer rows may be
149+
returned.
150+
151+
An Error (or subclass) exception is raised if the previous call to
152+
.execute*() did not produce any result set or no call was issued yet.
153+
154+
Note there are performance considerations involved with the size
155+
parameter. For optimal performance, it is usually best to use the
156+
.arraysize attribute. If the size parameter is used, then it is best
157+
for it to retain the same value from one .fetchmany() call to the next.
158+
"""
159+
size = size or self.arraysize
160+
161+
if self.rows is None:
162+
raise self._c.ProgrammingError('Nothing to fetch')
163+
164+
if len(self.rows) < size:
165+
items = self.rows
166+
self.rows = []
167+
else:
168+
items, self.rows = self.rows[:size], self.rows[size:]
169+
170+
return items if len(items) else []
171+
172+
def fetchall(self):
173+
"""Fetch all (remaining) rows of a query result, returning them as a
174+
sequence of sequences (e.g. a list of tuples). Note that the cursor's
175+
arraysize attribute can affect the performance of this operation.
176+
177+
An Error (or subclass) exception is raised if the previous call to
178+
.execute*() did not produce any result set or no call was issued yet.
179+
"""
180+
if self.rows is None:
181+
raise self._c.ProgrammingError('Nothing to fetch')
182+
183+
items = self.rows[:]
184+
self.rows = []
185+
return items
186+
187+
def setinputsizes(self, sizes):
188+
"""This can be used before a call to .execute*() to predefine memory
189+
areas for the operation's parameters.
190+
sizes is specified as a sequence - one item for each input parameter.
191+
The item should be a Type Object that corresponds to the input that
192+
will be used, or it should be an integer specifying the maximum
193+
length of a string parameter. If the item is None, then no predefined
194+
memory area will be reserved for that column (this is useful to avoid
195+
predefined areas for large inputs).
196+
197+
This method would be used before the .execute*() method is invoked.
198+
199+
Implementations are free to have this method do nothing and users are
200+
free to not use it."""
201+
202+
def setoutputsize(self, size, column=None):
203+
"""Set a column buffer size for fetches of large columns (e.g. LONGs,
204+
BLOBs, etc.). The column is specified as an index into the result
205+
sequence. Not specifying the column will set the default size for all
206+
large columns in the cursor.
207+
This method would be used before the .execute*() method is invoked.
208+
Implementations are free to have this method do nothing and users are
209+
free to not use it."""
210+
211+
212+
class Connection(BaseConnection):
213+
_cursor = None
214+
paramstyle = 'qmark'
215+
apilevel = "2.0"
216+
threadsafety = 0
217+
218+
server_version = 2
219+
220+
def connect(self):
221+
super(Connection, self).connect()
222+
return self
223+
224+
def commit(self):
225+
"""
226+
Commit any pending transaction to the database.
227+
228+
Note that if the database supports an auto-commit feature, this must
229+
be initially off. An interface method may be provided to turn it back
230+
on.
231+
232+
Database modules that do not support transactions should implement
233+
this method with void functionality.
234+
"""
235+
if self._socket is None:
236+
raise self.ProgrammingError
237+
238+
def rollback(self):
239+
"""
240+
In case a database does provide transactions this method causes the
241+
database to roll back to the start of any pending transaction.
242+
Closing a connection without committing the changes first will cause
243+
an implicit rollback to be performed.
244+
"""
245+
if self._socket is None:
246+
raise self.ProgrammingError
247+
248+
def execute(self, query, params=None):
249+
if self._socket is None:
250+
raise self.ProgrammingError('Can not execute on closed connection')
251+
return super(Connection, self).execute(query, params)
252+
253+
def close(self):
254+
"""
255+
Close the connection now (rather than whenever .__del__() is called).
256+
257+
The connection will be unusable from this point forward; an Error (or
258+
subclass) exception will be raised if any operation is attempted with
259+
the connection. The same applies to all cursor objects trying to use
260+
the connection. Note that closing a connection without committing the
261+
changes first will cause an implicit rollback to be performed.
262+
"""
263+
if self._socket:
264+
self._socket.close()
265+
self._socket = None
266+
else:
267+
raise self.ProgrammingError('Connection already closed')
268+
269+
def cursor(self, params=None):
270+
"""
271+
Return a new Cursor Object using the connection.
272+
273+
If the database does not provide a direct cursor concept, the module
274+
will have to emulate cursors using other means to the extent needed
275+
by this specification.
276+
"""
277+
return Cursor(self)

0 commit comments

Comments
 (0)