diff options
Diffstat (limited to 'lib/sqlalchemy/engine/base.py')
-rw-r--r-- | lib/sqlalchemy/engine/base.py | 382 |
1 files changed, 225 insertions, 157 deletions
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 844183628..4dc9665c0 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -20,6 +20,7 @@ __all__ = [ 'connection_memoize'] import inspect, StringIO, sys, operator +from itertools import izip from sqlalchemy import exc, schema, util, types, log from sqlalchemy.sql import expression @@ -1536,16 +1537,20 @@ class Engine(Connectable): def _proxy_connection_cls(cls, proxy): class ProxyConnection(cls): def execute(self, object, *multiparams, **params): - return proxy.execute(self, super(ProxyConnection, self).execute, object, *multiparams, **params) + return proxy.execute(self, super(ProxyConnection, self).execute, + object, *multiparams, **params) def _execute_clauseelement(self, elem, multiparams=None, params=None): - return proxy.execute(self, super(ProxyConnection, self).execute, elem, *(multiparams or []), **(params or {})) + return proxy.execute(self, super(ProxyConnection, self).execute, + elem, *(multiparams or []), **(params or {})) def _cursor_execute(self, cursor, statement, parameters, context=None): - return proxy.cursor_execute(super(ProxyConnection, self)._cursor_execute, cursor, statement, parameters, context, False) + return proxy.cursor_execute(super(ProxyConnection, self)._cursor_execute, + cursor, statement, parameters, context, False) def _cursor_executemany(self, cursor, statement, parameters, context=None): - return proxy.cursor_execute(super(ProxyConnection, self)._cursor_executemany, cursor, statement, parameters, context, True) + return proxy.cursor_execute(super(ProxyConnection, self)._cursor_executemany, + cursor, statement, parameters, context, True) def _begin_impl(self): return proxy.begin(self, super(ProxyConnection, self)._begin_impl) @@ -1560,27 +1565,125 @@ def _proxy_connection_cls(cls, proxy): return proxy.savepoint(self, super(ProxyConnection, self)._savepoint_impl, name=name) def _rollback_to_savepoint_impl(self, name, context): - return proxy.rollback_savepoint(self, super(ProxyConnection, self)._rollback_to_savepoint_impl, name, context) + return proxy.rollback_savepoint(self, + super(ProxyConnection, self)._rollback_to_savepoint_impl, + name, context) def _release_savepoint_impl(self, name, context): - return proxy.release_savepoint(self, super(ProxyConnection, self)._release_savepoint_impl, name, context) + return proxy.release_savepoint(self, + super(ProxyConnection, self)._release_savepoint_impl, + name, context) def _begin_twophase_impl(self, xid): - return proxy.begin_twophase(self, super(ProxyConnection, self)._begin_twophase_impl, xid) + return proxy.begin_twophase(self, + super(ProxyConnection, self)._begin_twophase_impl, xid) def _prepare_twophase_impl(self, xid): - return proxy.prepare_twophase(self, super(ProxyConnection, self)._prepare_twophase_impl, xid) + return proxy.prepare_twophase(self, + super(ProxyConnection, self)._prepare_twophase_impl, xid) def _rollback_twophase_impl(self, xid, is_prepared): - return proxy.rollback_twophase(self, super(ProxyConnection, self)._rollback_twophase_impl, xid, is_prepared) + return proxy.rollback_twophase(self, + super(ProxyConnection, self)._rollback_twophase_impl, + xid, is_prepared) def _commit_twophase_impl(self, xid, is_prepared): - return proxy.commit_twophase(self, super(ProxyConnection, self)._commit_twophase_impl, xid, is_prepared) + return proxy.commit_twophase(self, + super(ProxyConnection, self)._commit_twophase_impl, + xid, is_prepared) return ProxyConnection +# This reconstructor is necessary so that pickles with the C extension or +# without use the same Binary format. +# We need a different reconstructor on the C extension so that we can +# add extra checks that fields have correctly been initialized by +# __setstate__. +try: + from sqlalchemy.cresultproxy import rowproxy_reconstructor + + # this is a hack so that the reconstructor function is pickled with the + # same name as without the C extension. + # BUG: It fails for me if I run the "python" interpreter and + # then say "import sqlalchemy": + # TypeError: 'builtin_function_or_method' object has only read-only attributes (assign to .__module__) + # However, if I run the tests with nosetests, it succeeds ! + # I've verified with pdb etc. that this is the case. + #rowproxy_reconstructor.__module__ = 'sqlalchemy.engine.base' + +except ImportError: + def rowproxy_reconstructor(cls, state): + obj = cls.__new__(cls) + obj.__setstate__(state) + return obj + +try: + from sqlalchemy.cresultproxy import BaseRowProxy +except ImportError: + class BaseRowProxy(object): + __slots__ = ('_parent', '_row', '_processors', '_keymap') + + def __init__(self, parent, row, processors, keymap): + """RowProxy objects are constructed by ResultProxy objects.""" + + self._parent = parent + self._row = row + self._processors = processors + self._keymap = keymap + + def __reduce__(self): + return (rowproxy_reconstructor, + (self.__class__, self.__getstate__())) + + def values(self): + """Return the values represented by this RowProxy as a list.""" + return list(self) + + def __iter__(self): + for processor, value in izip(self._processors, self._row): + if processor is None: + yield value + else: + yield processor(value) + + def __len__(self): + return len(self._row) -class RowProxy(object): + def __getitem__(self, key): + try: + processor, index = self._keymap[key] + except KeyError: + processor, index = self._parent._key_fallback(key) + except TypeError: + if isinstance(key, slice): + l = [] + for processor, value in izip(self._processors[key], + self._row[key]): + if processor is None: + l.append(value) + else: + l.append(processor(value)) + return tuple(l) + else: + raise + if index is None: + raise exc.InvalidRequestError( + "Ambiguous column name '%s' in result set! " + "try 'use_labels' option on select statement." % key) + if processor is not None: + return processor(self._row[index]) + else: + return self._row[index] + + def __getattr__(self, name): + try: + # TODO: no test coverage here + return self[name] + except KeyError, e: + raise AttributeError(e.args[0]) + + +class RowProxy(BaseRowProxy): """Proxy values from a single cursor row. Mostly follows "ordered dictionary" behavior, mapping result @@ -1589,38 +1692,22 @@ class RowProxy(object): mapped to the original Columns that produced this result set (for results that correspond to constructed SQL expressions). """ + __slots__ = () - __slots__ = ['__parent', '__row', '__colfuncs'] - - def __init__(self, parent, row): - - self.__parent = parent - self.__row = row - self.__colfuncs = parent._colfuncs - if self.__parent._echo: - self.__parent.logger.debug("Row %r", row) - def __contains__(self, key): - return self.__parent._has_key(self.__row, key) + return self._parent._has_key(self._row, key) - def __len__(self): - return len(self.__row) - def __getstate__(self): return { - '__row':[self.__colfuncs[i][0](self.__row) for i in xrange(len(self.__row))], - '__parent':self.__parent + '_parent': self._parent, + '_row': tuple(self) } - - def __setstate__(self, d): - self.__row = d['__row'] - self.__parent = d['__parent'] - self.__colfuncs = self.__parent._colfuncs - - def __iter__(self): - row = self.__row - for func in self.__parent._colfunc_list: - yield func(row) + + def __setstate__(self, state): + self._parent = parent = state['_parent'] + self._row = state['_row'] + self._processors = parent._processors + self._keymap = parent._keymap __hash__ = None @@ -1636,33 +1723,7 @@ class RowProxy(object): def has_key(self, key): """Return True if this RowProxy contains the given key.""" - return self.__parent._has_key(self.__row, key) - - def __getitem__(self, key): - # the fallback and slices are only useful for __getitem__ anyway - try: - return self.__colfuncs[key][0](self.__row) - except KeyError: - k = self.__parent._key_fallback(key) - if k is None: - raise exc.NoSuchColumnError( - "Could not locate column in row for column '%s'" % key) - else: - # save on KeyError + _key_fallback() lookup next time around - self.__colfuncs[key] = k - return k[0](self.__row) - except TypeError: - if isinstance(key, slice): - return tuple(func(self.__row) for func in self.__parent._colfunc_list[key]) - else: - raise - - def __getattr__(self, name): - try: - # TODO: no test coverage here - return self[name] - except KeyError, e: - raise AttributeError(e.args[0]) + return self._parent._has_key(self._row, key) def items(self): """Return a list of tuples, each tuple containing a key/value pair.""" @@ -1672,24 +1733,25 @@ class RowProxy(object): def keys(self): """Return the list of keys as strings represented by this RowProxy.""" - return self.__parent.keys + return self._parent.keys def iterkeys(self): - return iter(self.__parent.keys) - - def values(self): - """Return the values represented by this RowProxy as a list.""" - - return list(self) + return iter(self._parent.keys) def itervalues(self): return iter(self) + class ResultMetaData(object): """Handle cursor.description, applying additional info from an execution context.""" def __init__(self, parent, metadata): - self._colfuncs = colfuncs = {} + self._processors = processors = [] + + # We do not strictly need to store the processor in the key mapping, + # though it is faster in the Python version (probably because of the + # saved attribute lookup self._processors) + self._keymap = keymap = {} self.keys = [] self._echo = parent._echo context = parent.context @@ -1720,29 +1782,25 @@ class ResultMetaData(object): processor = type_.dialect_impl(dialect).\ result_processor(dialect, coltype) - if processor: - def make_colfunc(processor, index): - def getcol(row): - return processor(row[index]) - return getcol - rec = (make_colfunc(processor, i), i, "colfunc") - else: - rec = (operator.itemgetter(i), i, "itemgetter") + processors.append(processor) + rec = (processor, i) - # indexes as keys - colfuncs[i] = rec + # indexes as keys. This is only needed for the Python version of + # RowProxy (the C version uses a faster path for integer indexes). + keymap[i] = rec # Column names as keys - if colfuncs.setdefault(name.lower(), rec) is not rec: - #XXX: why not raise directly? because several columns colliding - #by name is not a problem as long as the user don't use them (ie - #use the more precise ColumnElement - colfuncs[name.lower()] = (self._ambiguous_processor(name), i, "ambiguous") - + if keymap.setdefault(name.lower(), rec) is not rec: + # We do not raise an exception directly because several + # columns colliding by name is not a problem as long as the + # user does not try to access them (ie use an index directly, + # or the more precise ColumnElement) + keymap[name.lower()] = (processor, None) + # store the "origname" if we truncated (sqlite only) if origname and \ - colfuncs.setdefault(origname.lower(), rec) is not rec: - colfuncs[origname.lower()] = (self._ambiguous_processor(origname), i, "ambiguous") + keymap.setdefault(origname.lower(), rec) is not rec: + keymap[origname.lower()] = (processor, None) if dialect.requires_name_normalize: colname = dialect.normalize_name(colname) @@ -1750,76 +1808,67 @@ class ResultMetaData(object): self.keys.append(colname) if obj: for o in obj: - colfuncs[o] = rec + keymap[o] = rec if self._echo: self.logger = context.engine.logger self.logger.debug( "Col %r", tuple(x[0] for x in metadata)) - @util.memoized_property - def _colfunc_list(self): - funcs = self._colfuncs - return [funcs[i][0] for i in xrange(len(self.keys))] - def _key_fallback(self, key): - funcs = self._colfuncs - + map = self._keymap + result = None if isinstance(key, basestring): - key = key.lower() - if key in funcs: - return funcs[key] - + result = map.get(key.lower()) # fallback for targeting a ColumnElement to a textual expression # this is a rare use case which only occurs when matching text() - # constructs to ColumnElements - if isinstance(key, expression.ColumnElement): - if key._label and key._label.lower() in funcs: - return funcs[key._label.lower()] - elif hasattr(key, 'name') and key.name.lower() in funcs: - return funcs[key.name.lower()] - - return None + # constructs to ColumnElements, and after a pickle/unpickle roundtrip + elif isinstance(key, expression.ColumnElement): + if key._label and key._label.lower() in map: + result = map[key._label.lower()] + elif hasattr(key, 'name') and key.name.lower() in map: + result = map[key.name.lower()] + if result is None: + raise exc.NoSuchColumnError( + "Could not locate column in row for column '%s'" % key) + else: + map[key] = result + return result def _has_key(self, row, key): - if key in self._colfuncs: + if key in self._keymap: return True else: - key = self._key_fallback(key) - return key is not None + try: + self._key_fallback(key) + return True + except exc.NoSuchColumnError: + return False - @classmethod - def _ambiguous_processor(cls, colname): - def process(value): - raise exc.InvalidRequestError( - "Ambiguous column name '%s' in result set! " - "try 'use_labels' option on select statement." % colname) - return process - def __len__(self): return len(self.keys) def __getstate__(self): return { - '_pickled_colfuncs':dict( - (key, (i, type_)) - for key, (fn, i, type_) in self._colfuncs.iteritems() + '_pickled_keymap': dict( + (key, index) + for key, (processor, index) in self._keymap.iteritems() if isinstance(key, (basestring, int)) ), - 'keys':self.keys + 'keys': self.keys } def __setstate__(self, state): - pickled_colfuncs = state['_pickled_colfuncs'] - self._colfuncs = d = {} - for key, (index, type_) in pickled_colfuncs.iteritems(): - if type_ == 'ambiguous': - d[key] = (self._ambiguous_processor(key), index, type_) - else: - d[key] = (operator.itemgetter(index), index, "itemgetter") + # the row has been processed at pickling time so we don't need any + # processor anymore + self._processors = [None for _ in xrange(len(state['keys']))] + self._keymap = keymap = {} + for key, index in state['_pickled_keymap'].iteritems(): + keymap[key] = (None, index) self.keys = state['keys'] self._echo = False - + + class ResultProxy(object): """Wraps a DB-API cursor object to provide easier access to row columns. @@ -2031,13 +2080,27 @@ class ResultProxy(object): def _fetchall_impl(self): return self.cursor.fetchall() + def process_rows(self, rows): + process_row = self._process_row + metadata = self._metadata + keymap = metadata._keymap + processors = metadata._processors + if self._echo: + log = self.context.engine.logger.debug + l = [] + for row in rows: + log("Row %r", row) + l.append(process_row(metadata, row, processors, keymap)) + return l + else: + return [process_row(metadata, row, processors, keymap) + for row in rows] + def fetchall(self): """Fetch all rows, just like DB-API ``cursor.fetchall()``.""" try: - process_row = self._process_row - metadata = self._metadata - l = [process_row(metadata, row) for row in self._fetchall_impl()] + l = self.process_rows(self._fetchall_impl()) self.close() return l except Exception, e: @@ -2053,9 +2116,7 @@ class ResultProxy(object): """ try: - process_row = self._process_row - metadata = self._metadata - l = [process_row(metadata, row) for row in self._fetchmany_impl(size)] + l = self.process_rows(self._fetchmany_impl(size)) if len(l) == 0: self.close() return l @@ -2074,7 +2135,7 @@ class ResultProxy(object): try: row = self._fetchone_impl() if row is not None: - return self._process_row(self._metadata, row) + return self.process_rows([row])[0] else: self.close() return None @@ -2096,13 +2157,12 @@ class ResultProxy(object): try: if row is not None: - return self._process_row(self._metadata, row) + return self.process_rows([row])[0] else: return None finally: self.close() - def scalar(self): """Fetch the first column of the first row, and close the result set. @@ -2210,9 +2270,18 @@ class FullyBufferedResultProxy(ResultProxy): return ret class BufferedColumnRow(RowProxy): - def __init__(self, parent, row): - row = [parent._orig_colfuncs[i][0](row) for i in xrange(len(row))] - super(BufferedColumnRow, self).__init__(parent, row) + def __init__(self, parent, row, processors, keymap): + # preprocess row + row = list(row) + # this is a tad faster than using enumerate + index = 0 + for processor in parent._orig_processors: + if processor is not None: + row[index] = processor(row[index]) + index += 1 + row = tuple(row) + super(BufferedColumnRow, self).__init__(parent, row, + processors, keymap) class BufferedColumnResultProxy(ResultProxy): """A ResultProxy with column buffering behavior. @@ -2221,7 +2290,7 @@ class BufferedColumnResultProxy(ResultProxy): fetchone() is called. If fetchmany() or fetchall() are called, the full grid of results is fetched. This is to operate with databases where result rows contain "live" results that fall out - of scope unless explicitly fetched. Currently this includes + of scope unless explicitly fetched. Currently this includes cx_Oracle LOB objects. """ @@ -2230,17 +2299,16 @@ class BufferedColumnResultProxy(ResultProxy): def _init_metadata(self): super(BufferedColumnResultProxy, self)._init_metadata() - self._metadata._orig_colfuncs = self._metadata._colfuncs - self._metadata._colfuncs = colfuncs = {} - # replace the parent's _colfuncs dict, replacing - # column processors with straight itemgetters. - # the original _colfuncs dict is used when each row - # is constructed. - for k, (colfunc, index, type_) in self._metadata._orig_colfuncs.iteritems(): - if type_ == "colfunc": - colfuncs[k] = (operator.itemgetter(index), index, "itemgetter") - else: - colfuncs[k] = (colfunc, index, type_) + metadata = self._metadata + # orig_processors will be used to preprocess each row when they are + # constructed. + metadata._orig_processors = metadata._processors + # replace the all type processors by None processors. + metadata._processors = [None for _ in xrange(len(metadata.keys))] + keymap = {} + for k, (func, index) in metadata._keymap.iteritems(): + keymap[k] = (None, index) + self._metadata._keymap = keymap def fetchall(self): # can't call cursor.fetchall(), since rows must be |