Skip to content

Commit 2663265

Browse files
author
James William Pye
committed
Refactor the implementation of Cursor and Chunks.
The prior implementation was based on the previous API design where a statement could "only" be executed to have a cursor returned. In order to get 0.8 out the door, the effect of that design remained. The current API provides multiple execution methods to match the user's needs. In turn, Cursors no longer have local buffer state, and Chunks have extremely simple state. Stream(Chunks)-vs-Random Access(Cursor). This removes the chunksize keyword argument. If the size needs to be configured, the user will need to instantiate the chunk and modify the property on the Chunk object. chunksize has no effect on Cursor objects.
1 parent 50e1d40 commit 2663265

6 files changed

Lines changed: 520 additions & 817 deletions

File tree

postgresql/api.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
__all__ = [
3131
'Message',
3232
'PreparedStatement',
33+
'Chunks',
34+
'Rows',
3335
'Cursor',
3436
'Connector',
3537
'Database',
@@ -166,11 +168,17 @@ class Result(Element):
166168
These objects represent a binding of parameters to a given statement object.
167169
168170
For results that were constructed on the server and a reference passed back
169-
to the client, parameters may be None.
171+
to the client, statement and parameters may be None.
170172
"""
171173
_e_label = 'RESULT'
172174
_e_factors = ('statement', 'parameters', 'cursor_id')
173175

176+
@abstractmethod
177+
def close(self) -> None:
178+
"""
179+
Close the Result handle.
180+
"""
181+
174182
@propertydoc
175183
@abstractproperty
176184
def cursor_id(self) -> str:
@@ -246,15 +254,11 @@ def statement(self) -> ("PreparedStatement", None):
246254
"""
247255

248256
class Chunks(
257+
Result,
249258
collections.Iterator,
250259
collections.Iterable,
251260
):
252-
"""
253-
A `Chunks` object is an interface to an iterator of row-sets produced
254-
by a cursor.
255-
"""
256-
def __iter__(self):
257-
return self
261+
pass
258262

259263
class Cursor(
260264
Result,
@@ -309,12 +313,9 @@ def read(self,
309313
`direction` can be used to override the default configured direction.
310314
311315
This alters the cursor's position.
312-
"""
313316
314-
@abstractmethod
315-
def close(self) -> None:
316-
"""
317-
Close the cursor.
317+
Read does not directly correlate to FETCH. If zero is given as the
318+
quantity, an empty sequence *must* be returned.
318319
"""
319320

320321
@abstractmethod
@@ -472,7 +473,7 @@ def __call__(self, *parameters : "Positional Parameters") -> ["Row"]:
472473
"""
473474

474475
@abstractmethod
475-
def rows(self, *parameters, chunksize = None) -> "Iterator(Row)":
476+
def rows(self, *parameters) -> collections.Iterable:
476477
"""
477478
Return an iterator producing rows produced by the cursor
478479
created from the statement bound with the given parameters.
@@ -489,7 +490,7 @@ def rows(self, *parameters, chunksize = None) -> "Iterator(Row)":
489490
"""
490491

491492
@abstractmethod
492-
def chunks(self, *parameters, chunksize = None) -> Chunks:
493+
def chunks(self, *parameters) -> collections.Iterable:
493494
"""
494495
Return an iterator producing sequences of rows produced by the cursor
495496
created from the statement bound with the given parameters.

postgresql/documentation/changes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Extend `postgresql.open` to take keyword arguments.
1414
* Refactor `postgresql.api.InterfaceElement`.
1515
* Refactor driver.pq3.Connection to use protocol.client3.Connection.
16+
* Refactor driver.pq3.Cursor into types selected by PreparedStatements.
1617
1718
0.8.1
1819
-----

postgresql/driver/dbapi20.py

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,44 @@ def dbapi_type(typid):
8181
elif typid == pg_type.OIDOID:
8282
return ROWID
8383

84+
class Portal(object):
85+
def __init__(self, chunks):
86+
self.chunks = chunks
87+
self.buf = []
88+
self.pos = 0
89+
90+
def __next__(self):
91+
try:
92+
r = self.buf[self.pos]
93+
self.pos += 1
94+
return r
95+
except IndexError:
96+
self.buf = next(self.chunks)
97+
self.pos = 0
98+
return self.__next__()
99+
100+
def readall(self):
101+
self.buf = self.buf[self.pos:]
102+
self.pos = 0
103+
for x in self.chunks:
104+
self.buf.extend(x)
105+
r = self.buf
106+
self.buf = []
107+
return r
108+
109+
def read(self, amount):
110+
try:
111+
while (len(self.buf) - self.pos) < amount:
112+
self.buf.extend(next(self.chunks))
113+
end = self.pos + amount
114+
except StopIteration:
115+
end = len(self.buf)
116+
117+
r = self.buf[self.pos:end]
118+
del self.buf[:end]
119+
self.pos = 0
120+
return r
121+
84122
class Cursor(object):
85123
rowcount = -1
86124
arraysize = 1
@@ -92,6 +130,25 @@ def __init__(self, C):
92130
self.description = ()
93131
self.__portals = []
94132

133+
# Describe the "real" cursor as a "portal".
134+
# This should keep ambiguous terminology out of adaptor.
135+
def _portal():
136+
def fget(self):
137+
if self.__portals is None:
138+
raise Error("access on closed cursor")
139+
try:
140+
p = self.__portals[0]
141+
except IndexError:
142+
raise InterfaceError("no portal on stack")
143+
return p
144+
def fdel(self):
145+
try:
146+
del self.__portals[0]
147+
except IndexError:
148+
raise InterfaceError("no portal on stack")
149+
return locals()
150+
_portal = property(**_portal())
151+
95152
def setinputsizes(self, sizes):
96153
pass
97154

@@ -104,7 +161,7 @@ def callproc(self, proname, args):
104161
'$%d' %(x,) for x in range(1, len(args) + 1)
105162
])
106163
))
107-
self.__portals.insert(0, p._cursor(*args))
164+
self.__portals.insert(0, Portal(p.chunks(*args)))
108165
return args
109166

110167
def fetchone(self):
@@ -123,7 +180,7 @@ def fetchmany(self, arraysize = None):
123180
return self._portal.read(arraysize or self.arraysize or 1)
124181

125182
def fetchall(self):
126-
return self._portal.read()
183+
return self._portal.readall()
127184

128185
def nextset(self):
129186
del self._portal
@@ -186,17 +243,18 @@ def execute(self, statement, parameters = ()):
186243
)
187244
)
188245
ps = self.database.prepare(sql)
189-
c = ps._cursor(*pxf(parameters))
246+
c = ps.chunks(*pxf(parameters))
190247
if ps._output is not None and len(ps._output) > 0:
191248
# name, relationId, columnNumber, typeId, typlen, typmod, format
192249
self.description = tuple([
193250
(self.database.typio.decode(x[0]), dbapi_type(x[3]),
194251
None, None, None, None, None)
195252
for x in ps._output
196253
])
197-
self.__portals.insert(0, c)
254+
self.__portals.insert(0, Portal(c))
198255
else:
199256
self.description = None
257+
# execute bumps any current portal
200258
if self.__portals:
201259
del self._portal
202260
return self
@@ -217,25 +275,6 @@ def close(self):
217275
self.__portals = None
218276
for p in ps: p.close()
219277

220-
# Describe the "real" cursor as a "portal".
221-
# This should keep ambiguous terminology out of adaptor.
222-
def _portal():
223-
def fget(self):
224-
if self.__portals is None:
225-
raise Error("access on closed cursor")
226-
try:
227-
p = self.__portals[0]
228-
except IndexError:
229-
raise InterfaceError("no portal on stack")
230-
return p
231-
def fdel(self):
232-
try:
233-
del self.__portals[0]
234-
except IndexError:
235-
raise InterfaceError("no portal on stack")
236-
return locals()
237-
_portal = property(**_portal())
238-
239278
class Connection(object):
240279
"""
241280
DB-API 2.0 connection implementation for PG-API connection objects.

0 commit comments

Comments
 (0)