Commits

Mike Bayer committed 7f34944

- Simplified the check for ResultProxy "autoclose without results"
to be based solely on presence of cursor.description.
All the regexp-based guessing about statements returning rows
has been removed [ticket:1212].

  • Participants
  • Parent commits ba52005

Comments (0)

Files changed (11)

       upon disposal to keep it outside of cyclic garbage collection.
 
 - sql
+    - Simplified the check for ResultProxy "autoclose without results"
+      to be based solely on presence of cursor.description.   
+      All the regexp-based guessing about statements returning rows 
+      has been removed [ticket:1212].
+      
     - Further simplified SELECT compilation and its relationship to
       result row processing.
 
     - No longer expects include_columns in table reflection to be
       lower case.
 
-    - added 'EXPLAIN' to the list of 'returns rows', but this 
-      issue will be addressed more fully by [ticket:1212].
-      
 - misc
     - util.flatten_iterator() func doesn't interpret strings with
       __iter__() methods as iterators, such as in pypy [ticket:1077].

File lib/sqlalchemy/databases/firebird.py

        'BLOB': lambda r: r['stype']==1 and FBText() or FBBinary()
       }
 
-
-SELECT_RE = re.compile(
-    r'\s*(?:SELECT|(UPDATE|INSERT|DELETE))',
-    re.I | re.UNICODE)
-
-RETURNING_RE = re.compile(
-    'RETURNING',
-    re.I | re.UNICODE)
-
-# This finds if the RETURNING is not inside a quoted/commented values. Handles string literals,
-# quoted identifiers, dollar quotes, SQL comments and C style multiline comments. This does not
-# handle correctly nested C style quotes, lets hope no one does the following:
-# UPDATE tbl SET x=y /* foo /* bar */ RETURNING */
-RETURNING_QUOTED_RE = re.compile(
-    """\s*(?:UPDATE|INSERT|DELETE)\s
-        (?: # handle quoted and commented tokens separately
-            [^'"$/-] # non quote/comment character
-            | -(?!-) # a dash that does not begin a comment
-            | /(?!\*) # a slash that does not begin a comment
-            | "(?:[^"]|"")*" # quoted literal
-            | '(?:[^']|'')*' # quoted string
-            | --[^\\n]*(?=\\n) # SQL comment, leave out line ending as that counts as whitespace
-                               # for the returning token
-            | /\*([^*]|\*(?!/))*\*/ # C style comment, doesn't handle nesting
-        )*
-        \sRETURNING\s""", re.I | re.UNICODE | re.VERBOSE)
-
 RETURNING_KW_NAME = 'firebird_returning'
 
 class FBExecutionContext(default.DefaultExecutionContext):
-    def returns_rows_text(self, statement):
-        m = SELECT_RE.match(statement)
-        return m and (not m.group(1) or (RETURNING_RE.search(statement)
-                                         and RETURNING_QUOTED_RE.match(statement)))
-
-    def returns_rows_compiled(self, compiled):
-        return (isinstance(compiled.statement, sql.expression.Selectable) or
-                ((compiled.isupdate or compiled.isinsert or compiled.isdelete) and
-                 RETURNING_KW_NAME in compiled.statement.kwargs))
+    pass
 
 
 class FBDialect(default.DefaultDialect):

File lib/sqlalchemy/databases/mssql.py

                 self._last_inserted_ids = [int(row[0])] + self._last_inserted_ids[1:]
         super(MSSQLExecutionContext, self).post_exec()
 
-    _ms_is_select = re.compile(r'\s*(?:SELECT|sp_columns|EXEC)',
-                               re.I | re.UNICODE)
-
-    def returns_rows_text(self, statement):
-        return self._ms_is_select.match(statement) is not None
-
 
 class MSSQLExecutionContext_pyodbc (MSSQLExecutionContext):
     def pre_exec(self):

File lib/sqlalchemy/databases/mysql.py

 AUTOCOMMIT_RE = re.compile(
     r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER|LOAD +DATA|REPLACE)',
     re.I | re.UNICODE)
-SELECT_RE = re.compile(
-    r'\s*(?:SELECT|SHOW|DESCRIBE|XA RECOVER|CALL|EXPLAIN)',
-    re.I | re.UNICODE)
 SET_RE = re.compile(
     r'\s*SET\s+(?:(?:GLOBAL|SESSION)\s+)?\w',
     re.I | re.UNICODE)
             # which is probably a programming error anyhow.
             self.connection.info.pop(('mysql', 'charset'), None)
 
-    def returns_rows_text(self, statement):
-        return SELECT_RE.match(statement)
-
     def should_autocommit_text(self, statement):
         return AUTOCOMMIT_RE.match(statement)
 

File lib/sqlalchemy/databases/oracle.py

 from sqlalchemy import types as sqltypes
 
 
-SELECT_REGEXP = re.compile(r'(\s*/\*\+.*?\*/)?\s*SELECT', re.I | re.UNICODE)
-
 class OracleNumeric(sqltypes.Numeric):
     def get_col_spec(self):
         if self.precision is None:
                     self.out_parameters[name] = self.cursor.var(dbtype)
                     self.parameters[0][name] = self.out_parameters[name]
 
-    def returns_rows_text(self, statement):
-        return SELECT_REGEXP.match(statement)
-
     def create_cursor(self):
         c = self._connection.connection.cursor()
         if self.dialect.arraysize:

File lib/sqlalchemy/databases/postgres.py

     'interval':PGInterval,
 }
 
+# TODO: filter out 'FOR UPDATE' statements
 SERVER_SIDE_CURSOR_RE = re.compile(
     r'\s*SELECT',
     re.I | re.UNICODE)
 
-SELECT_RE = re.compile(
-    r'\s*(?:SELECT|FETCH|(UPDATE|INSERT))',
-    re.I | re.UNICODE)
-
-RETURNING_RE = re.compile(
-    'RETURNING',
-    re.I | re.UNICODE)
-
-# This finds if the RETURNING is not inside a quoted/commented values. Handles string literals,
-# quoted identifiers, dollar quotes, SQL comments and C style multiline comments. This does not
-# handle correctly nested C style quotes, lets hope no one does the following:
-# UPDATE tbl SET x=y /* foo /* bar */ RETURNING */
-RETURNING_QUOTED_RE = re.compile(
-    """\s*(?:UPDATE|INSERT)\s
-        (?: # handle quoted and commented tokens separately
-            [^'"$/-] # non quote/comment character
-            | -(?!-) # a dash that does not begin a comment
-            | /(?!\*) # a slash that does not begin a comment
-            | "(?:[^"]|"")*" # quoted literal
-            | '(?:[^']|'')*' # quoted string
-            | \$(?P<dquote>[^$]*)\$.*?\$(?P=dquote)\$ # dollar quotes
-            | --[^\\n]*(?=\\n) # SQL comment, leave out line ending as that counts as whitespace
-                            # for the returning token
-            | /\*([^*]|\*(?!/))*\*/ # C style comment, doesn't handle nesting
-        )*
-        \sRETURNING\s""", re.I | re.UNICODE | re.VERBOSE)
-
 class PGExecutionContext(default.DefaultExecutionContext):
-    def returns_rows_text(self, statement):
-        m = SELECT_RE.match(statement)
-        return m and (not m.group(1) or (RETURNING_RE.search(statement)
-           and RETURNING_QUOTED_RE.match(statement)))
-
-    def returns_rows_compiled(self, compiled):
-        return isinstance(compiled.statement, expression.Selectable) or \
-            (
-                (compiled.isupdate or compiled.isinsert) and "postgres_returning" in compiled.statement.kwargs
-            )
-
     def create_cursor(self):
-        self.__is_server_side = \
+        # TODO: coverage for server side cursors + select.for_update()
+        is_server_side = \
             self.dialect.server_side_cursors and \
-            ((self.compiled and isinstance(self.compiled.statement, expression.Selectable)) \
+            ((self.compiled and isinstance(self.compiled.statement, expression.Selectable) and not self.compiled.statement.for_update) \
             or \
             (
                 (not self.compiled or isinstance(self.compiled.statement, expression._TextClause)) 
                 and self.statement and SERVER_SIDE_CURSOR_RE.match(self.statement))
             )
 
-        if self.__is_server_side:
+        self.__is_server_side = is_server_side
+        if is_server_side:
             # use server-side cursors:
             # http://lists.initd.org/pipermail/psycopg/2007-January/005251.html
             ident = "c_%s_%s" % (hex(id(self))[2:], hex(random.randint(0, 65535))[2:])

File lib/sqlalchemy/databases/sqlite.py

 from sqlalchemy.sql import compiler, functions as sql_functions
 from types import NoneType
 
-SELECT_REGEXP = re.compile(r'\s*(?:SELECT|PRAGMA)', re.I | re.UNICODE)
-
 class SLNumeric(sqltypes.Numeric):
     def bind_processor(self, dialect):
         type_ = self.asdecimal and str or float
             if not len(self._last_inserted_ids) or self._last_inserted_ids[0] is None:
                 self._last_inserted_ids = [self.cursor.lastrowid] + self._last_inserted_ids[1:]
 
-    def returns_rows_text(self, statement):
-        return SELECT_REGEXP.match(statement)
-
 class SQLiteDialect(default.DefaultDialect):
     name = 'sqlite'
     supports_alter = False

File lib/sqlalchemy/engine/base.py

     should_autocommit
       True if the statement is a "committable" statement
 
-    returns_rows
-      True if the statement should return result rows
-
     postfetch_cols
      a list of Column objects for which a server-side default
      or inline SQL expression value was fired off.  applies to inserts and updates.
         context = self.__create_execution_context(statement=statement, parameters=parameters)
         self.__execute_raw(context)
         self._autocommit(context)
-        return context.result()
+        return context.get_result_proxy()
 
     def __distill_params(self, multiparams, params):
         """given arguments from the calling form *multiparams, **params, return a list
         self.__execute_raw(context)
         context.post_execution()
         self._autocommit(context)
-        return context.result()
+        return context.get_result_proxy()
 
     def __execute_raw(self, context):
         if context.executemany:
 
         self.__parent = parent
         self.__row = row
-        if self.__parent._ResultProxy__echo:
+        if self.__parent._echo:
             self.__parent.context.engine.logger.debug("Row " + repr(row))
 
     def close(self):
         self.closed = False
         self.cursor = context.cursor
         self.connection = context.root_connection
-        self.__echo = context.engine._should_log_info
-        if context.returns_rows:
-            self._init_metadata()
-        else:
-            self.close()
+        self._echo = context.engine._should_log_info
+        self._init_metadata()
     
     @property
     def rowcount(self):
         return self.context.out_parameters
 
     def _init_metadata(self):
+        metadata = self.cursor.description
+        if metadata is None:
+            # no results, close
+            self.close()
+            return
+            
         self._props = util.PopulateDict(None)
         self._props.creator = self.__key_fallback()
         self.keys = []
-        metadata = self.cursor.description
 
-        if metadata is not None:
-            typemap = self.dialect.dbapi_type_map
+        typemap = self.dialect.dbapi_type_map
 
-            for i, item in enumerate(metadata):
-                colname = item[0].decode(self.dialect.encoding)
+        for i, item in enumerate(metadata):
+            colname = item[0].decode(self.dialect.encoding)
 
-                if '.' in colname:
-                    # sqlite will in some circumstances prepend table name to colnames, so strip
-                    origname = colname
-                    colname = colname.split('.')[-1]
-                else:
-                    origname = None
+            if '.' in colname:
+                # sqlite will in some circumstances prepend table name to colnames, so strip
+                origname = colname
+                colname = colname.split('.')[-1]
+            else:
+                origname = None
 
-                if self.context.result_map:
-                    try:
-                        (name, obj, type_) = self.context.result_map[colname.lower()]
-                    except KeyError:
-                        (name, obj, type_) = (colname, None, typemap.get(item[1], types.NULLTYPE))
-                else:
+            if self.context.result_map:
+                try:
+                    (name, obj, type_) = self.context.result_map[colname.lower()]
+                except KeyError:
                     (name, obj, type_) = (colname, None, typemap.get(item[1], types.NULLTYPE))
+            else:
+                (name, obj, type_) = (colname, None, typemap.get(item[1], types.NULLTYPE))
 
-                rec = (type_, type_.dialect_impl(self.dialect).result_processor(self.dialect), i)
+            rec = (type_, type_.dialect_impl(self.dialect).result_processor(self.dialect), i)
 
-                if self._props.setdefault(name.lower(), rec) is not rec:
-                    self._props[name.lower()] = (type_, self.__ambiguous_processor(name), 0)
+            if self._props.setdefault(name.lower(), rec) is not rec:
+                self._props[name.lower()] = (type_, self.__ambiguous_processor(name), 0)
 
-                # store the "origname" if we truncated (sqlite only)
-                if origname:
-                    if self._props.setdefault(origname.lower(), rec) is not rec:
-                        self._props[origname.lower()] = (type_, self.__ambiguous_processor(origname), 0)
+            # store the "origname" if we truncated (sqlite only)
+            if origname:
+                if self._props.setdefault(origname.lower(), rec) is not rec:
+                    self._props[origname.lower()] = (type_, self.__ambiguous_processor(origname), 0)
 
-                self.keys.append(colname)
-                self._props[i] = rec
-                if obj:
-                    for o in obj:
-                        self._props[o] = rec
+            self.keys.append(colname)
+            self._props[i] = rec
+            if obj:
+                for o in obj:
+                    self._props[o] = rec
 
-            if self.__echo:
-                self.context.engine.logger.debug(
-                    "Col " + repr(tuple(x[0] for x in metadata)))
+        if self._echo:
+            self.context.engine.logger.debug(
+                "Col " + repr(tuple(x[0] for x in metadata)))
     
     def __key_fallback(self):
         # create a closure without 'self' to avoid circular references
 
     def __ambiguous_processor(self, colname):
         def process(value):
-            raise exc.InvalidRequestError("Ambiguous column name '%s' in result set! try 'use_labels' option on select statement." % colname)
+            raise exc.InvalidRequestError("Ambiguous column name '%s' in result set! "
+                        "try 'use_labels' option on select statement." % colname)
         return process
 
     def close(self):
-        """Close this ResultProxy, and the underlying DB-API cursor corresponding to the execution.
+        """Close this ResultProxy.
+        
+        Closes the underlying DBAPI cursor corresponding to the execution.
 
         If this ResultProxy was generated from an implicit execution,
         the underlying Connection will also be closed (returns the
-        underlying DB-API connection to the connection pool.)
+        underlying DBAPI connection to the connection pool.)
 
-        This method is also called automatically when all result rows
-        are exhausted.
+        This method is called automatically when:
+        
+            * all result rows are exhausted using the fetchXXX() methods.
+            * cursor.description is None.
+        
         """
         if not self.closed:
             self.closed = True
     The pre-fetching behavior fetches only one row initially, and then
     grows its buffer size by a fixed amount with each successive need
     for additional rows up to a size of 100.
+    
     """
+
     def _init_metadata(self):
         self.__buffer_rows()
         super(BufferedRowResultProxy, self)._init_metadata()
     of scope unless explicitly fetched.  Currently this includes just
     cx_Oracle LOB objects, but this behavior is known to exist in
     other DB-APIs as well (Pygresql, currently unsupported).
+    
     """
+
     _process_row = BufferedColumnRow
 
     def _get_col(self, row, key):
     """A visitor that can gather text into a buffer and execute the contents of the buffer."""
 
     def __init__(self, connection):
-        """Construct a new SchemaIterator.
-        """
+        """Construct a new SchemaIterator."""
+        
         self.connection = connection
         self.buffer = StringIO.StringIO()
 
     DefaultRunners are used internally by Engines and Dialects.
     Specific database modules should provide their own subclasses of
     DefaultRunner to allow database-specific behavior.
+
     """
 
     def __init__(self, context):
             return None
 
     def visit_passive_default(self, default):
-        """Do nothing.
-
-        Passive defaults by definition return None on the app side,
-        and are post-fetched to get the DB-side value.
-        """
-
         return None
 
     def visit_sequence(self, seq):
-        """Do nothing.
-
-        """
-
         return None
 
     def exec_default_sql(self, default):
         return conn._execute_compiled(c).scalar()
 
     def execute_string(self, stmt, params=None):
-        """execute a string statement, using the raw cursor,
-        and return a scalar result."""
+        """execute a string statement, using the raw cursor, and return a scalar result."""
+        
         conn = self.context._connection
         if isinstance(stmt, unicode) and not self.dialect.supports_unicode_statements:
             stmt = stmt.encode(self.dialect.encoding)

File lib/sqlalchemy/engine/default.py

 
 AUTOCOMMIT_REGEXP = re.compile(r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER)',
                                re.I | re.UNICODE)
-SELECT_REGEXP = re.compile(r'\s*SELECT', re.I | re.UNICODE)
-
 
 class DefaultDialect(base.Dialect):
     """Default implementation of Dialect"""
             self.isinsert = compiled.isinsert
             self.isupdate = compiled.isupdate
             if isinstance(compiled.statement, expression._TextClause):
-                self.returns_rows = self.returns_rows_text(self.statement)
                 self.should_autocommit = compiled.statement._autocommit or self.should_autocommit_text(self.statement)
             else:
-                self.returns_rows = self.returns_rows_compiled(compiled)
                 self.should_autocommit = getattr(compiled.statement, '_autocommit', False) or self.should_autocommit_compiled(compiled)
 
             if not parameters:
                 self.statement = statement
             self.isinsert = self.isupdate = False
             self.cursor = self.create_cursor()
-            self.returns_rows = self.returns_rows_text(statement)
             self.should_autocommit = self.should_autocommit_text(statement)
         else:
             # no statement. used for standalone ColumnDefault execution.
             self.statement = None
-            self.isinsert = self.isupdate = self.executemany = self.returns_rows = self.should_autocommit = False
+            self.isinsert = self.isupdate = self.executemany = self.should_autocommit = False
             self.cursor = self.create_cursor()
 
-    connection = property(lambda s:s._connection._branch())
+    @property
+    def connection(self):
+        return self._connection._branch()
 
     def __encode_param_keys(self, params):
         """apply string encoding to the keys of dictionary-based bind parameters.
                 parameters.append(param)
         return parameters
 
-    def returns_rows_compiled(self, compiled):
-        return isinstance(compiled.statement, expression.Selectable)
-
-    def returns_rows_text(self, statement):
-        return SELECT_REGEXP.match(statement)
-
     def should_autocommit_compiled(self, compiled):
         return isinstance(compiled.statement, expression._UpdateBase)
 
     def post_execution(self):
         self.post_exec()
 
-    def result(self):
-        return self.get_result_proxy()
-
     def pre_exec(self):
         pass
 
     def post_exec(self):
         pass
-
+    
     def get_result_proxy(self):
         return base.ResultProxy(self)
 

File test/profiling/zoomark.py

     def test_profile_4_expressions(self):
         self.test_baseline_4_expressions()
 
-    @profiling.function_call_count(1523, {'2.4': 1084})
+    @profiling.function_call_count(1442, {'2.4': 1084})
     def test_profile_5_aggregates(self):
         self.test_baseline_5_aggregates()
 

File test/profiling/zoomark_orm.py

     def test_profile_4_expressions(self):
         self.test_baseline_4_expressions()
 
-    @profiling.function_call_count(1507)
+    @profiling.function_call_count(1426)
     def test_profile_5_aggregates(self):
         self.test_baseline_5_aggregates()