On Thursday 24 November 2011, I wrote:
> On Tuesday 22 November 2011, Anssi Kääriäinen wrote:
> > I can do a patch for this if needed. Are you (or somebody else)
> > working on this?
> 
> While you're at it, [...]

While we're all at it, I would like to bring to your attention another 
suggested fix for Oracle. The patch attached here tells the backend that if a 
fetch operation retrieved less than the requested array size, then no further 
fetch attempts are necessary.

The fix is motivated by our DBA's finding, that many single-row-selects are 
followed by double fetches. When I checked the code, I realized that with the 
exception of aggregates, this is true for all single-row selects: whether it 
originates from qset.get(...) or from qset[j], the QuerySet code adjusts the 
SQL and then calls fetchmany in a loop until no results are returned. Since 
neither cx_oracle nor the Oracle client library take it upon themselves to 
stop such requests, they go all the way to the database.

The Python DB-API[0] says on fetchmany:
 """
    The method should try to fetch as many rows as indicated by the size
    parameter. If this is not possible due to the specified number of rows not
    being available, fewer rows may be returned.
"""
I interpret that as "no rows should be fetched after a call failed to fill the 
buffer". Under a loose-enough transaction isolation level, rows may "come into 
being" in mid-transaction, but I don't think it is reasonable to expect them 
to do so in mid-statement.

That being said, the problem that this patch fixes is a little hard to test 
for; it essentially requires mocking the database library. Also, I'm not sure 
this is the right fix; I don't know if other backends have the same issue. If 
they do, it might be better to make qset[j] just use fetchone instead of 
fetchmany, like the aggregates do; and it might be worthwhile to also change 
the qset.get() logic (in a more subtle way -- we don't want to lose the check 
for multiple records returned).

As for the patch, it is only "tested" in the sense that one Django-1.2 based 
application continues to run correctly with (essentially) it; I don't(yet) 
have an Oracle+Trunk testing environment.

Looking forward to your comments,

Shai.


[0] http://www.python.org/dev/peps/pep-0249/

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-developers@googlegroups.com.
To unsubscribe from this group, send email to 
django-developers+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en.

Index: django/db/backends/oracle/base.py
===================================================================
--- django/db/backends/oracle/base.py	(revision 17145)
+++ django/db/backends/oracle/base.py	(working copy)
@@ -643,6 +643,7 @@
     charset = 'utf-8'
 
     def __init__(self, connection):
+        self._all_fetched = False
         self.cursor = connection.cursor()
         # Necessary to retrieve decimal values without rounding error.
         self.cursor.numbersAsStrings = True
@@ -664,6 +665,7 @@
         return [p.smart_str for p in params]
 
     def execute(self, query, params=None):
+        self._all_fetched = False
         if params is None:
             params = []
         else:
@@ -688,6 +690,7 @@
             raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2]
 
     def executemany(self, query, params=None):
+        self._all_fetched = False
         try:
             args = [(':arg%d' % i) for i in range(len(params[0]))]
         except (IndexError, TypeError):
@@ -720,10 +723,15 @@
         return _rowfactory(row, self.cursor)
 
     def fetchmany(self, size=None):
+        if self._all_fetched:
+            return DatabaseFeatures.empty_fetchmany_value
         if size is None:
             size = self.arraysize
-        return tuple([_rowfactory(r, self.cursor)
-                      for r in self.cursor.fetchmany(size)])
+        rows = tuple(_rowfactory(r, self.cursor)
+                     for r in self.cursor.fetchmany(size))
+        if len(rows) < size:
+            self._all_fetched = True
+        return rows
 
     def fetchall(self):
         return tuple([_rowfactory(r, self.cursor)

Reply via email to