From b3d3795de0d45fe4adda7393881f0f955409a45d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 7 Mar 2015 12:48:13 -0500 Subject: - The SQL compiler now generates the mapping of expected columns such that they are matched to the received result set positionally, rather than by name. Originally, this was seen as a way to handle cases where we had columns returned with difficult-to-predict names, though in modern use that issue has been overcome by anonymous labeling. In this version, the approach basically reduces function call count per-result by a few dozen calls, or more for larger sets of result columns. The approach still degrades into a modern version of the old approach if textual elements modify the result map, or if any discrepancy in size exists between the compiled set of columns versus what was received, so there's no issue for partially or fully textual compilation scenarios where these lists might not line up. fixes #918 - callcounts still need to be adjusted down for this so zoomark tests won't pass at the moment --- lib/sqlalchemy/engine/default.py | 11 ++- lib/sqlalchemy/engine/result.py | 204 +++++++++++++++++++++++++-------------- 2 files changed, 139 insertions(+), 76 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 17d2e2531..9d2bbfb15 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -461,9 +461,9 @@ class DefaultExecutionContext(interfaces.ExecutionContext): is_crud = False isddl = False executemany = False - result_map = None compiled = None statement = None + _result_columns = None _is_implicit_returning = False _is_explicit_returning = False @@ -525,7 +525,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): # compiled clauseelement. process bind params, process table defaults, # track collections used by ResultProxy to target and process results - self.result_map = compiled.result_map + self._result_columns = compiled._result_columns self.unicode_statement = util.text_type(compiled) if not dialect.supports_unicode_statements: @@ -663,6 +663,13 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.cursor = self.create_cursor() return self + @property + def result_map(self): + if self._result_columns: + return self.compiled.result_map + else: + return None + @util.memoized_property def engine(self): return self.root_connection.engine diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 3995942ef..6eca54a34 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -187,86 +187,142 @@ class ResultMetaData(object): context.""" def __init__(self, parent, metadata): - 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 = [] context = parent.context dialect = context.dialect typemap = dialect.dbapi_type_map translate_colname = context._translate_colname - self.case_sensitive = dialect.case_sensitive - - # high precedence key values. - primary_keymap = {} - - for i, rec in enumerate(metadata): - colname = rec[0] - coltype = rec[1] - - if dialect.description_encoding: - colname = dialect._description_decoder(colname) + self.case_sensitive = case_sensitive = dialect.case_sensitive + if context._result_columns: + num_ctx_cols = len(context._result_columns) + else: + num_ctx_cols = None + + if num_ctx_cols and \ + context.compiled._ordered_columns and \ + num_ctx_cols == len(metadata): + # case 1 - SQL expression statement, number of columns + # in result matches number of cols in compiled. This is the + # vast majority case for SQL expression constructs. In this + # case we don't bother trying to parse or match up to + # the colnames in the result description. + raw = [ + ( + idx, + key, + name.lower() if not case_sensitive else name, + context.get_result_processor( + type_, key, metadata[idx][1] + ), + obj, + None + ) for idx, (key, name, obj, type_) + in enumerate(context._result_columns) + ] + self.keys = [ + elem[1] for elem in context._result_columns + ] + else: + # case 2 - raw string, or number of columns in result does + # not match number of cols in compiled. The raw string case + # is very common. The latter can happen + # when text() is used with only a partial typemap, or + # in the extremely unlikely cases where the compiled construct + # has a single element with multiple col expressions in it + # (e.g. has commas embedded) or there's some kind of statement + # that is adding extra columns. + # In all these cases we fall back to the "named" approach + # that SQLAlchemy has used up through 0.9. + + raw = [] + self.keys = [] + untranslated = None + for idx, rec in enumerate(metadata): + colname = rec[0] + coltype = rec[1] + + if dialect.description_encoding: + colname = dialect._description_decoder(colname) + + if translate_colname: + colname, untranslated = translate_colname(colname) + + if dialect.requires_name_normalize: + colname = dialect.normalize_name(colname) + + self.keys.append(colname) + if not case_sensitive: + colname = colname.lower() + + if num_ctx_cols: + try: + ctx_rec = context.result_map[colname] + except KeyError: + mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) + obj = None + else: + obj = ctx_rec[1] + mapped_type = ctx_rec[2] + else: + mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) + obj = None + processor = context.get_result_processor( + mapped_type, colname, coltype) + + raw.append( + (idx, colname, colname, processor, obj, untranslated) + ) + + # keymap indexes by integer index... + self._keymap = dict([ + (elem[0], (elem[3], elem[4], elem[0])) + for elem in raw + ]) + + # processors in key order for certain per-row + # views like __iter__ and slices + self._processors = [elem[3] for elem in raw] + + if num_ctx_cols: + # keymap by primary string... + by_key = dict([ + (elem[2], (elem[3], elem[4], elem[0])) + for elem in raw + ]) + + # if by-primary-string dictionary smaller (or bigger?!) than + # number of columns, assume we have dupes, rewrite + # dupe records with "None" for index which results in + # ambiguous column exception when accessed. + if len(by_key) != num_ctx_cols: + seen = set() + for idx in range(num_ctx_cols): + key = raw[idx][1] + if key in seen: + by_key[key] = (None, by_key[key][1], None) + seen.add(key) + + # update keymap with secondary "object"-based keys + self._keymap.update([ + (obj_elem, by_key[elem[2]]) + for elem in raw if elem[4] + for obj_elem in elem[4] + ]) + + # update keymap with primary string names taking + # precedence + self._keymap.update(by_key) + else: + self._keymap.update([ + (elem[2], (elem[3], elem[4], elem[0])) + for elem in raw + ]) + # update keymap with "translated" names (sqlite-only thing) if translate_colname: - colname, untranslated = translate_colname(colname) - - if dialect.requires_name_normalize: - colname = dialect.normalize_name(colname) - - if context.result_map: - try: - name, obj, type_ = context.result_map[ - colname if self.case_sensitive else colname.lower()] - except KeyError: - name, obj, type_ = \ - colname, None, typemap.get(coltype, sqltypes.NULLTYPE) - else: - name, obj, type_ = \ - colname, None, typemap.get(coltype, sqltypes.NULLTYPE) - - processor = context.get_result_processor(type_, colname, coltype) - - processors.append(processor) - rec = (processor, obj, i) - - # indexes as keys. This is only needed for the Python version of - # RowProxy (the C version uses a faster path for integer indexes). - primary_keymap[i] = rec - - # populate primary keymap, looking for conflicts. - if primary_keymap.setdefault( - name if self.case_sensitive - else name.lower(), - rec) is not rec: - # place a record that doesn't have the "index" - this - # is interpreted later as an AmbiguousColumnError, - # but only when actually accessed. Columns - # colliding by name is not a problem if those names - # aren't used; integer access is always - # unambiguous. - primary_keymap[name - if self.case_sensitive - else name.lower()] = rec = (None, obj, None) - - self.keys.append(colname) - if obj: - for o in obj: - keymap[o] = rec - # technically we should be doing this but we - # are saving on callcounts by not doing so. - # if keymap.setdefault(o, rec) is not rec: - # keymap[o] = (None, obj, None) - - if translate_colname and \ - untranslated: - keymap[untranslated] = rec - - # overwrite keymap values with those of the - # high precedence keymap. - keymap.update(primary_keymap) + self._keymap.update([ + (elem[5], self._keymap[elem[2]]) + for elem in raw if elem[5] + ]) @util.pending_deprecation("0.8", "sqlite dialect uses " "_translate_colname() now") -- cgit v1.2.1 From 17b2fd3fba8eef5bdd4040f2d71c75e4aeb4e215 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 7 Mar 2015 22:51:12 -0500 Subject: - the change for #918 was of course not nearly that simple. The "wrapping" employed by the mssql and oracle dialects using the "iswrapper" argument was not being used intelligently by the compiler, and the result map was being written incorrectly, using *more* columns in the result map than were actually returned by the statement, due to "row number" columns that are inside the subquery. The compiler now writes out result map on the "top level" select in all cases fully, and for the mssql/oracle wrapping case extracts out the "proxied" columns in a second step, which only includes those columns that are proxied outwards to the top level. This change might have implications for 3rd party dialects that might be imitating oracle's approach. They can safely continue to use the "iswrapper" kw which is now ignored, but they may need to also add the _select_wraps argument as well. --- lib/sqlalchemy/engine/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 9d2bbfb15..62469d720 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -663,7 +663,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.cursor = self.create_cursor() return self - @property + @util.memoized_property def result_map(self): if self._result_columns: return self.compiled.result_map -- cgit v1.2.1 From 9854114f578833aee49b65e440319a0ec26daab6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 8 Mar 2015 11:29:10 -0400 Subject: foo --- lib/sqlalchemy/engine/default.py | 15 +++------------ lib/sqlalchemy/engine/result.py | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 62469d720..bdd9d2715 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -463,7 +463,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): executemany = False compiled = None statement = None - _result_columns = None + result_column_struct = None _is_implicit_returning = False _is_explicit_returning = False @@ -522,10 +522,8 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.execution_options = compiled.statement._execution_options.union( connection._execution_options) - # compiled clauseelement. process bind params, process table defaults, - # track collections used by ResultProxy to target and process results - - self._result_columns = compiled._result_columns + self.result_column_struct = ( + compiled._result_columns, compiled._ordered_columns) self.unicode_statement = util.text_type(compiled) if not dialect.supports_unicode_statements: @@ -663,13 +661,6 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.cursor = self.create_cursor() return self - @util.memoized_property - def result_map(self): - if self._result_columns: - return self.compiled.result_map - else: - return None - @util.memoized_property def engine(self): return self.root_connection.engine diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 6eca54a34..893cd2a30 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -193,13 +193,14 @@ class ResultMetaData(object): translate_colname = context._translate_colname self.case_sensitive = case_sensitive = dialect.case_sensitive - if context._result_columns: - num_ctx_cols = len(context._result_columns) + if context.result_column_struct: + result_columns, cols_are_ordered = context.result_column_struct + num_ctx_cols = len(result_columns) else: num_ctx_cols = None if num_ctx_cols and \ - context.compiled._ordered_columns and \ + cols_are_ordered and \ num_ctx_cols == len(metadata): # case 1 - SQL expression statement, number of columns # in result matches number of cols in compiled. This is the @@ -217,10 +218,10 @@ class ResultMetaData(object): obj, None ) for idx, (key, name, obj, type_) - in enumerate(context._result_columns) + in enumerate(result_columns) ] self.keys = [ - elem[1] for elem in context._result_columns + elem[1] for elem in result_columns ] else: # case 2 - raw string, or number of columns in result does @@ -234,6 +235,9 @@ class ResultMetaData(object): # In all these cases we fall back to the "named" approach # that SQLAlchemy has used up through 0.9. + if num_ctx_cols: + result_map = self._create_result_map(result_columns) + raw = [] self.keys = [] untranslated = None @@ -256,7 +260,7 @@ class ResultMetaData(object): if num_ctx_cols: try: - ctx_rec = context.result_map[colname] + ctx_rec = result_map[colname] except KeyError: mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) obj = None @@ -296,8 +300,8 @@ class ResultMetaData(object): # ambiguous column exception when accessed. if len(by_key) != num_ctx_cols: seen = set() - for idx in range(num_ctx_cols): - key = raw[idx][1] + for rec in raw: + key = rec[1] if key in seen: by_key[key] = (None, by_key[key][1], None) seen.add(key) @@ -324,6 +328,22 @@ class ResultMetaData(object): for elem in raw if elem[5] ]) + @classmethod + def _create_result_map(cls, result_columns): + d = {} + for elem in result_columns: + key, rec = elem[0], elem[1:] + if key in d: + # conflicting keyname, just double up the list + # of objects. this will cause an "ambiguous name" + # error if an attempt is made by the result set to + # access. + e_name, e_obj, e_type = d[key] + d[key] = e_name, e_obj + rec[1], e_type + else: + d[key] = rec + return d + @util.pending_deprecation("0.8", "sqlite dialect uses " "_translate_colname() now") def _set_keymap_synonym(self, name, origname): -- cgit v1.2.1 From b8a39b8636c8abef350e17ecca61e2a215931c9b Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 9 Mar 2015 15:24:37 -0400 Subject: - fix a potential race condition where the per-mapper LRUCache used by persistence.py could theoretically hit the limit of the cache (100 items by default) and at some points fail to have a key that we check for, due to the cleanup. This has never been observed so its likely that so far, the total number of INSERT, UPDATE and DELETE statement structures in real apps has not exceeded 100 on a per-mapper basis; this could happen for apps that run a very wide variety of attribute modified combinations into the unit of work, *and* which have very high concurrency going on. This change will be a lot more significant when we open up use of LRUCache + compiled cache with the baked query extension. --- lib/sqlalchemy/engine/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 305fa4620..067d32538 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -991,9 +991,8 @@ class Connection(Connectable): dialect = self.dialect if 'compiled_cache' in self._execution_options: key = dialect, elem, tuple(sorted(keys)), len(distilled_params) > 1 - if key in self._execution_options['compiled_cache']: - compiled_sql = self._execution_options['compiled_cache'][key] - else: + compiled_sql = self._execution_options['compiled_cache'].get(key) + if compiled_sql is None: compiled_sql = elem.compile( dialect=dialect, column_keys=keys, inline=len(distilled_params) > 1) -- cgit v1.2.1 From 50866d2f857dd45bb2d186a0fa076768437d62a3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 10 Mar 2015 15:24:28 -0400 Subject: - copyright 2015 --- lib/sqlalchemy/engine/__init__.py | 2 +- lib/sqlalchemy/engine/base.py | 2 +- lib/sqlalchemy/engine/default.py | 2 +- lib/sqlalchemy/engine/interfaces.py | 2 +- lib/sqlalchemy/engine/reflection.py | 2 +- lib/sqlalchemy/engine/result.py | 2 +- lib/sqlalchemy/engine/strategies.py | 2 +- lib/sqlalchemy/engine/threadlocal.py | 2 +- lib/sqlalchemy/engine/url.py | 2 +- lib/sqlalchemy/engine/util.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 7a14cdfb5..0678dd201 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -1,5 +1,5 @@ # engine/__init__.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 067d32538..6be446313 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1,5 +1,5 @@ # engine/base.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index bdd9d2715..b46acb650 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -1,5 +1,5 @@ # engine/default.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 5f0d74328..da8fa81eb 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -1,5 +1,5 @@ # engine/interfaces.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 6e102aad6..59eed51ec 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -1,5 +1,5 @@ # engine/reflection.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 893cd2a30..3c31ae1ea 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -1,5 +1,5 @@ # engine/result.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index fd665ad03..1fd105d67 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -1,5 +1,5 @@ # engine/strategies.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/threadlocal.py b/lib/sqlalchemy/engine/threadlocal.py index e64ab09f4..0d6e1c0f1 100644 --- a/lib/sqlalchemy/engine/threadlocal.py +++ b/lib/sqlalchemy/engine/threadlocal.py @@ -1,5 +1,5 @@ # engine/threadlocal.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 6544cfbf3..d045961dd 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -1,5 +1,5 @@ # engine/url.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/util.py b/lib/sqlalchemy/engine/util.py index d9eb1df10..3734c9960 100644 --- a/lib/sqlalchemy/engine/util.py +++ b/lib/sqlalchemy/engine/util.py @@ -1,5 +1,5 @@ # engine/util.py -# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under -- cgit v1.2.1 From b7e151ac5cf5a0c13b9a30bc6841ed0cfe322536 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 17 Mar 2015 12:32:33 -0400 Subject: - The "auto close" for :class:`.ResultProxy` is now a "soft" close. That is, after exhausing all rows using the fetch methods, the DBAPI cursor is released as before and the object may be safely discarded, but the fetch methods may continue to be called for which they will return an end-of-result object (None for fetchone, empty list for fetchmany and fetchall). Only if :meth:`.ResultProxy.close` is called explicitly will these methods raise the "result is closed" error. fixes #3330 fixes #3329 --- lib/sqlalchemy/engine/base.py | 4 +- lib/sqlalchemy/engine/default.py | 8 +- lib/sqlalchemy/engine/result.py | 171 +++++++++++++++++++++++++++++++-------- 3 files changed, 143 insertions(+), 40 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 6be446313..5921ab9ba 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1160,12 +1160,12 @@ class Connection(Connectable): else: result = context.get_result_proxy() if result._metadata is None: - result.close(_autoclose_connection=False) + result._soft_close(_autoclose_connection=False) if context.should_autocommit and self._root.__transaction is None: self._root._commit_impl(autocommit=True) - if result.closed and self.should_close_with_result: + if result._soft_closed and self.should_close_with_result: self.close() return result diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index b46acb650..3eebc6c06 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -815,15 +815,15 @@ class DefaultExecutionContext(interfaces.ExecutionContext): row = result.fetchone() self.returned_defaults = row self._setup_ins_pk_from_implicit_returning(row) - result.close(_autoclose_connection=False) + result._soft_close(_autoclose_connection=False) result._metadata = None elif not self._is_explicit_returning: - result.close(_autoclose_connection=False) + result._soft_close(_autoclose_connection=False) result._metadata = None elif self.isupdate and self._is_implicit_returning: row = result.fetchone() self.returned_defaults = row - result.close(_autoclose_connection=False) + result._soft_close(_autoclose_connection=False) result._metadata = None elif result._metadata is None: @@ -831,7 +831,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): # (which requires open cursor on some drivers # such as kintersbasdb, mxodbc) result.rowcount - result.close(_autoclose_connection=False) + result._soft_close(_autoclose_connection=False) return result def _setup_ins_pk_from_lastrowid(self): diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 3c31ae1ea..6d19cb6d0 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -479,11 +479,12 @@ class ResultProxy(object): out_parameters = None _can_close_connection = False _metadata = None + _soft_closed = False + closed = False def __init__(self, context): self.context = context self.dialect = context.dialect - self.closed = False self.cursor = self._saved_cursor = context.cursor self.connection = context.root_connection self._echo = self.connection._echo and \ @@ -620,33 +621,79 @@ class ResultProxy(object): return self._saved_cursor.description - def close(self, _autoclose_connection=True): - """Close this ResultProxy. - - Closes the underlying DBAPI cursor corresponding to the execution. - - Note that any data cached within this ResultProxy is still available. - For some types of results, this may include buffered rows. + def _soft_close(self, _autoclose_connection=True): + """Soft close this :class:`.ResultProxy`. - If this ResultProxy was generated from an implicit execution, - the underlying Connection will also be closed (returns the - underlying DBAPI connection to the connection pool.) + This releases all DBAPI cursor resources, but leaves the + ResultProxy "open" from a semantic perspective, meaning the + fetchXXX() methods will continue to return empty results. This method is called automatically when: * all result rows are exhausted using the fetchXXX() methods. * cursor.description is None. + This method is **not public**, but is documented in order to clarify + the "autoclose" process used. + + .. versionadded:: 1.0.0 + + .. seealso:: + + :meth:`.ResultProxy.close` + + + """ + if self._soft_closed: + return + self._soft_closed = True + cursor = self.cursor + self.connection._safe_close_cursor(cursor) + if _autoclose_connection and \ + self.connection.should_close_with_result: + self.connection.close() + self.cursor = None + + def close(self): + """Close this ResultProxy. + + This closes out the underlying DBAPI cursor corresonding + to the statement execution, if one is stil present. Note that the + DBAPI cursor is automatically released when the :class:`.ResultProxy` + exhausts all available rows. :meth:`.ResultProxy.close` is generally + an optional method except in the case when discarding a + :class:`.ResultProxy` that still has additional rows pending for fetch. + + In the case of a result that is the product of + :ref:`connectionless execution `, + the underyling :class:`.Connection` object is also closed, which + :term:`releases` DBAPI connection resources. + + After this method is called, it is no longer valid to call upon + the fetch methods, which will raise a :class:`.ResourceClosedError` + on subsequent use. + + .. versionchanged:: 1.0.0 - the :meth:`.ResultProxy.close` method + has been separated out from the process that releases the underlying + DBAPI cursor resource. The "auto close" feature of the + :class:`.Connection` now performs a so-called "soft close", which + releases the underlying DBAPI cursor, but allows the + :class:`.ResultProxy` to still behave as an open-but-exhausted + result set; the actual :meth:`.ResultProxy.close` method is never + called. It is still safe to discard a :class:`.ResultProxy` + that has been fully exhausted without calling this method. + + .. seealso:: + + :ref:`connections_toplevel` + + :meth:`.ResultProxy._soft_close` + """ if not self.closed: + self._soft_close() self.closed = True - self.connection._safe_close_cursor(self.cursor) - if _autoclose_connection and \ - self.connection.should_close_with_result: - self.connection.close() - # allow consistent errors - self.cursor = None def __iter__(self): while True: @@ -837,7 +884,7 @@ class ResultProxy(object): try: return self.cursor.fetchone() except AttributeError: - self._non_result() + return self._non_result(None) def _fetchmany_impl(self, size=None): try: @@ -846,22 +893,24 @@ class ResultProxy(object): else: return self.cursor.fetchmany(size) except AttributeError: - self._non_result() + return self._non_result([]) def _fetchall_impl(self): try: return self.cursor.fetchall() except AttributeError: - self._non_result() + return self._non_result([]) - def _non_result(self): + def _non_result(self, default): if self._metadata is None: raise exc.ResourceClosedError( "This result object does not return rows. " "It has been closed automatically.", ) - else: + elif self.closed: raise exc.ResourceClosedError("This result object is closed.") + else: + return default def process_rows(self, rows): process_row = self._process_row @@ -880,11 +929,25 @@ class ResultProxy(object): for row in rows] def fetchall(self): - """Fetch all rows, just like DB-API ``cursor.fetchall()``.""" + """Fetch all rows, just like DB-API ``cursor.fetchall()``. + + After all rows have been exhausted, the underlying DBAPI + cursor resource is released, and the object may be safely + discarded. + + Subsequent calls to :meth:`.ResultProxy.fetchall` will return + an empty list. After the :meth:`.ResultProxy.close` method is + called, the method will raise :class:`.ResourceClosedError`. + + .. versionchanged:: 1.0.0 - Added "soft close" behavior which + allows the result to be used in an "exhausted" state prior to + calling the :meth:`.ResultProxy.close` method. + + """ try: l = self.process_rows(self._fetchall_impl()) - self.close() + self._soft_close() return l except Exception as e: self.connection._handle_dbapi_exception( @@ -895,15 +958,25 @@ class ResultProxy(object): """Fetch many rows, just like DB-API ``cursor.fetchmany(size=cursor.arraysize)``. - If rows are present, the cursor remains open after this is called. - Else the cursor is automatically closed and an empty list is returned. + After all rows have been exhausted, the underlying DBAPI + cursor resource is released, and the object may be safely + discarded. + + Calls to :meth:`.ResultProxy.fetchmany` after all rows have been + exhuasted will return + an empty list. After the :meth:`.ResultProxy.close` method is + called, the method will raise :class:`.ResourceClosedError`. + + .. versionchanged:: 1.0.0 - Added "soft close" behavior which + allows the result to be used in an "exhausted" state prior to + calling the :meth:`.ResultProxy.close` method. """ try: l = self.process_rows(self._fetchmany_impl(size)) if len(l) == 0: - self.close() + self._soft_close() return l except Exception as e: self.connection._handle_dbapi_exception( @@ -913,8 +986,18 @@ class ResultProxy(object): def fetchone(self): """Fetch one row, just like DB-API ``cursor.fetchone()``. - If a row is present, the cursor remains open after this is called. - Else the cursor is automatically closed and None is returned. + After all rows have been exhausted, the underlying DBAPI + cursor resource is released, and the object may be safely + discarded. + + Calls to :meth:`.ResultProxy.fetchone` after all rows have + been exhausted will return ``None``. + After the :meth:`.ResultProxy.close` method is + called, the method will raise :class:`.ResourceClosedError`. + + .. versionchanged:: 1.0.0 - Added "soft close" behavior which + allows the result to be used in an "exhausted" state prior to + calling the :meth:`.ResultProxy.close` method. """ try: @@ -922,7 +1005,7 @@ class ResultProxy(object): if row is not None: return self.process_rows([row])[0] else: - self.close() + self._soft_close() return None except Exception as e: self.connection._handle_dbapi_exception( @@ -934,9 +1017,12 @@ class ResultProxy(object): Returns None if no row is present. + After calling this method, the object is fully closed, + e.g. the :meth:`.ResultProxy.close` method will have been called. + """ if self._metadata is None: - self._non_result() + return self._non_result(None) try: row = self._fetchone_impl() @@ -958,6 +1044,9 @@ class ResultProxy(object): Returns None if no row is present. + After calling this method, the object is fully closed, + e.g. the :meth:`.ResultProxy.close` method will have been called. + """ row = self.first() if row is not None: @@ -1001,13 +1090,19 @@ class BufferedRowResultProxy(ResultProxy): } def __buffer_rows(self): + if self.cursor is None: + return size = getattr(self, '_bufsize', 1) self.__rowbuffer = collections.deque(self.cursor.fetchmany(size)) self._bufsize = self.size_growth.get(size, size) + def _soft_close(self, **kw): + self.__rowbuffer.clear() + super(BufferedRowResultProxy, self)._soft_close(**kw) + def _fetchone_impl(self): - if self.closed: - return None + if self.cursor is None: + return self._non_result(None) if not self.__rowbuffer: self.__buffer_rows() if not self.__rowbuffer: @@ -1026,6 +1121,8 @@ class BufferedRowResultProxy(ResultProxy): return result def _fetchall_impl(self): + if self.cursor is None: + return self._non_result([]) self.__rowbuffer.extend(self.cursor.fetchall()) ret = self.__rowbuffer self.__rowbuffer = collections.deque() @@ -1048,11 +1145,15 @@ class FullyBufferedResultProxy(ResultProxy): def _buffer_rows(self): return collections.deque(self.cursor.fetchall()) + def _soft_close(self, **kw): + self.__rowbuffer.clear() + super(FullyBufferedResultProxy, self)._soft_close(**kw) + def _fetchone_impl(self): if self.__rowbuffer: return self.__rowbuffer.popleft() else: - return None + return self._non_result(None) def _fetchmany_impl(self, size=None): if size is None: @@ -1066,6 +1167,8 @@ class FullyBufferedResultProxy(ResultProxy): return result def _fetchall_impl(self): + if not self.cursor: + return self._non_result([]) ret = self.__rowbuffer self.__rowbuffer = collections.deque() return ret -- cgit v1.2.1 From 1b83b588f5573799dee8d508be13c252c2e57115 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 8 Apr 2015 11:59:12 -0400 Subject: - Fixed a regression where the "last inserted id" mechanics would fail to store the correct value for MSSQL on an INSERT where the primary key value was present in the insert params before execution. fixes #3360 --- lib/sqlalchemy/engine/default.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 3eebc6c06..763e85f82 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -840,18 +840,26 @@ class DefaultExecutionContext(interfaces.ExecutionContext): compiled_params = self.compiled_parameters[0] lastrowid = self.get_lastrowid() - autoinc_col = table._autoincrement_column - if autoinc_col is not None: - # apply type post processors to the lastrowid - proc = autoinc_col.type._cached_result_processor( - self.dialect, None) - if proc is not None: - lastrowid = proc(lastrowid) - self.inserted_primary_key = [ - lastrowid if c is autoinc_col else - compiled_params.get(key_getter(c), None) - for c in table.primary_key - ] + if lastrowid is not None: + autoinc_col = table._autoincrement_column + if autoinc_col is not None: + # apply type post processors to the lastrowid + proc = autoinc_col.type._cached_result_processor( + self.dialect, None) + if proc is not None: + lastrowid = proc(lastrowid) + self.inserted_primary_key = [ + lastrowid if c is autoinc_col else + compiled_params.get(key_getter(c), None) + for c in table.primary_key + ] + else: + # don't have a usable lastrowid, so + # do the same as _setup_ins_pk_from_empty + self.inserted_primary_key = [ + compiled_params.get(key_getter(c), None) + for c in table.primary_key + ] def _setup_ins_pk_from_empty(self): key_getter = self.compiled._key_getters_for_crud_column[2] -- cgit v1.2.1 From 0e98795ff2c7a164b4da164d7b26af3faabf84d1 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 30 Apr 2015 17:51:14 -0400 Subject: - New features added to support engine/pool plugins with advanced functionality. Added a new "soft invalidate" feature to the connection pool at the level of the checked out connection wrapper as well as the :class:`._ConnectionRecord`. This works similarly to a modern pool invalidation in that connections aren't actively closed, but are recycled only on next checkout; this is essentially a per-connection version of that feature. A new event :class:`.PoolEvents.soft_invalidate` is added to complement it. fixes #3379 - Added new flag :attr:`.ExceptionContext.invalidate_pool_on_disconnect`. Allows an error handler within :meth:`.ConnectionEvents.handle_error` to maintain a "disconnect" condition, but to handle calling invalidate on individual connections in a specific manner within the event. - Added new event :class:`.DialectEvents.do_connect`, which allows interception / replacement of when the :meth:`.Dialect.connect` hook is called to create a DBAPI connection. Also added dialect plugin hooks :meth:`.Dialect.get_dialect_cls` and :meth:`.Dialect.engine_created` which allow external plugins to add events to existing dialects using entry points. fixes #3355 --- lib/sqlalchemy/engine/base.py | 10 ++++++- lib/sqlalchemy/engine/interfaces.py | 53 +++++++++++++++++++++++++++++++++++++ lib/sqlalchemy/engine/strategies.py | 16 +++++++++-- 3 files changed, 76 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 5921ab9ba..af310c450 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1254,6 +1254,8 @@ class Connection(Connectable): if context: context.is_disconnect = self._is_disconnect + invalidate_pool_on_disconnect = True + if self._reentrant_error: util.raise_from_cause( exc.DBAPIError.instance(statement, @@ -1316,6 +1318,11 @@ class Connection(Connectable): sqlalchemy_exception.connection_invalidated = \ self._is_disconnect = ctx.is_disconnect + # set up potentially user-defined value for + # invalidate pool. + invalidate_pool_on_disconnect = \ + ctx.invalidate_pool_on_disconnect + if should_wrap and context: context.handle_dbapi_exception(e) @@ -1340,7 +1347,8 @@ class Connection(Connectable): del self._is_disconnect if not self.invalidated: dbapi_conn_wrapper = self.__connection - self.engine.pool._invalidate(dbapi_conn_wrapper, e) + if invalidate_pool_on_disconnect: + self.engine.pool._invalidate(dbapi_conn_wrapper, e) self.invalidate(e) if self.should_close_with_result: self.close() diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index da8fa81eb..2dd192162 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -733,6 +733,41 @@ class Dialect(object): raise NotImplementedError() + @classmethod + def get_dialect_cls(cls, url): + """Given a URL, return the :class:`.Dialect` that will be used. + + This is a hook that allows an external plugin to provide functionality + around an existing dialect, by allowing the plugin to be loaded + from the url based on an entrypoint, and then the plugin returns + the actual dialect to be used. + + By default this just returns the cls. + + .. versionadded:: 1.0.3 + + """ + return cls + + @classmethod + def engine_created(cls, engine): + """A convenience hook called before returning the final :class:`.Engine`. + + If the dialect returned a different class from the + :meth:`.get_dialect_cls` + method, then the hook is called on both classes, first on + the dialect class returned by the :meth:`.get_dialect_cls` method and + then on the class on which the method was called. + + The hook should be used by dialects and/or wrappers to apply special + events to the engine or its components. In particular, it allows + a dialect-wrapping class to apply dialect-level events. + + .. versionadded:: 1.0.3 + + """ + pass + class ExecutionContext(object): """A messenger object for a Dialect that corresponds to a single @@ -1085,3 +1120,21 @@ class ExceptionContext(object): changing this flag. """ + + invalidate_pool_on_disconnect = True + """Represent whether all connections in the pool should be invalidated + when a "disconnect" condition is in effect. + + Setting this flag to False within the scope of the + :meth:`.ConnectionEvents.handle_error` event will have the effect such + that the full collection of connections in the pool will not be + invalidated during a disconnect; only the current connection that is the + subject of the error will actually be invalidated. + + The purpose of this flag is for custom disconnect-handling schemes where + the invalidation of other connections in the pool is to be performed + based on other conditions, or even on a per-connection basis. + + .. versionadded:: 1.0.3 + + """ \ No newline at end of file diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index 1fd105d67..a802e5d90 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -48,7 +48,8 @@ class DefaultEngineStrategy(EngineStrategy): # create url.URL object u = url.make_url(name_or_url) - dialect_cls = u.get_dialect() + entrypoint = u.get_dialect() + dialect_cls = entrypoint.get_dialect_cls(u) if kwargs.pop('_coerce_config', False): def pop_kwarg(key, default=None): @@ -81,11 +82,18 @@ class DefaultEngineStrategy(EngineStrategy): # assemble connection arguments (cargs, cparams) = dialect.create_connect_args(u) cparams.update(pop_kwarg('connect_args', {})) + cargs = list(cargs) # allow mutability # look for existing pool or create pool = pop_kwarg('pool', None) if pool is None: - def connect(): + def connect(connection_record=None): + if dialect._has_events: + for fn in dialect.dispatch.do_connect: + connection = fn( + dialect, connection_record, cargs, cparams) + if connection is not None: + return connection return dialect.connect(*cargs, **cparams) creator = pop_kwarg('creator', connect) @@ -157,6 +165,10 @@ class DefaultEngineStrategy(EngineStrategy): dialect.initialize(c) event.listen(pool, 'first_connect', first_connect, once=True) + dialect_cls.engine_created(engine) + if entrypoint is not dialect_cls: + entrypoint.engine_created(engine) + return engine -- cgit v1.2.1 From d178707ecaeb547470e2b7b37b9a939abc69cbd0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 May 2015 12:35:21 -0400 Subject: - Added support for the case of the misbehaving DBAPI that has pep-249 exception names linked to exception classes of an entirely different name, preventing SQLAlchemy's own exception wrapping from wrapping the error appropriately. The SQLAlchemy dialect in use needs to implement a new accessor :attr:`.DefaultDialect.dbapi_exception_translation_map` to support this feature; this is implemented now for the py-postgresql dialect. fixes #3421 --- lib/sqlalchemy/engine/base.py | 6 ++++-- lib/sqlalchemy/engine/default.py | 9 +++++++++ lib/sqlalchemy/engine/interfaces.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index af310c450..7ebe39bbf 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1261,7 +1261,8 @@ class Connection(Connectable): exc.DBAPIError.instance(statement, parameters, e, - self.dialect.dbapi.Error), + self.dialect.dbapi.Error, + dialect=self.dialect), exc_info ) self._reentrant_error = True @@ -1277,7 +1278,8 @@ class Connection(Connectable): parameters, e, self.dialect.dbapi.Error, - connection_invalidated=self._is_disconnect) + connection_invalidated=self._is_disconnect, + dialect=self.dialect) else: sqlalchemy_exception = None diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 763e85f82..9330a602c 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -157,6 +157,15 @@ class DefaultDialect(interfaces.Dialect): reflection_options = () + dbapi_exception_translation_map = util.immutabledict() + """mapping used in the extremely unusual case that a DBAPI's + published exceptions don't actually have the __name__ that they + are linked towards. + + .. versionadded:: 1.0.5 + + """ + def __init__(self, convert_unicode=False, encoding='utf-8', paramstyle=None, dbapi=None, implicit_returning=None, diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 2dd192162..73a8b4635 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -150,6 +150,16 @@ class Dialect(object): This will prevent types.Boolean from generating a CHECK constraint when that type is used. + dbapi_exception_translation_map + A dictionary of names that will contain as values the names of + pep-249 exceptions ("IntegrityError", "OperationalError", etc) + keyed to alternate class names, to support the case where a + DBAPI has exception classes that aren't named as they are + referred to (e.g. IntegrityError = MyException). In the vast + majority of cases this dictionary is empty. + + .. versionadded:: 1.0.5 + """ _has_events = False -- cgit v1.2.1 From 04c625467e65ec4189d4fd73e0e10c727f04dce6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 22 May 2015 13:51:00 -0400 Subject: - Adjustments to the engine plugin hook, such that the :meth:`.URL.get_dialect` method will continue to return the ultimate :class:`.Dialect` object when a dialect plugin is used, without the need for the caller to be aware of the :meth:`.Dialect.get_dialect_cls` method. reference #3379 --- lib/sqlalchemy/engine/strategies.py | 3 +-- lib/sqlalchemy/engine/url.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index a802e5d90..e2a086de4 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -48,8 +48,7 @@ class DefaultEngineStrategy(EngineStrategy): # create url.URL object u = url.make_url(name_or_url) - entrypoint = u.get_dialect() - dialect_cls = entrypoint.get_dialect_cls(u) + entrypoint, dialect_cls = u._get_dialect_plus_entrypoint() if kwargs.pop('_coerce_config', False): def pop_kwarg(key, default=None): diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index d045961dd..07f6a5730 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -117,11 +117,7 @@ class URL(object): else: return self.drivername.split('+')[1] - def get_dialect(self): - """Return the SQLAlchemy database dialect class corresponding - to this URL's driver name. - """ - + def _get_dialect_plus_entrypoint(self): if '+' not in self.drivername: name = self.drivername else: @@ -133,9 +129,17 @@ class URL(object): if hasattr(cls, 'dialect') and \ isinstance(cls.dialect, type) and \ issubclass(cls.dialect, Dialect): - return cls.dialect + return cls.dialect, cls.dialect else: - return cls + dialect_cls = cls.get_dialect_cls(self) + return cls, dialect_cls + + def get_dialect(self): + """Return the SQLAlchemy database dialect class corresponding + to this URL's driver name. + """ + entrypoint, dialect_cls = self._get_dialect_plus_entrypoint() + return dialect_cls def translate_connect_args(self, names=[], **kw): """Translate url attributes into a dictionary of connection arguments. -- cgit v1.2.1 From e9921ad356fee4edb56007ae39793fb2211f13cf Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 23 May 2015 09:07:36 -0400 Subject: - fix some tests related to the URL change and try to make the URL design a little simpler --- lib/sqlalchemy/engine/strategies.py | 3 ++- lib/sqlalchemy/engine/url.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index e2a086de4..a539ee9f7 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -48,7 +48,8 @@ class DefaultEngineStrategy(EngineStrategy): # create url.URL object u = url.make_url(name_or_url) - entrypoint, dialect_cls = u._get_dialect_plus_entrypoint() + entrypoint = u._get_entrypoint() + dialect_cls = entrypoint.get_dialect_cls(u) if kwargs.pop('_coerce_config', False): def pop_kwarg(key, default=None): diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 07f6a5730..32e3f8a6b 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -117,7 +117,13 @@ class URL(object): else: return self.drivername.split('+')[1] - def _get_dialect_plus_entrypoint(self): + def _get_entrypoint(self): + """Return the "entry point" dialect class. + + This is normally the dialect itself except in the case when the + returned class implements the get_dialect_cls() method. + + """ if '+' not in self.drivername: name = self.drivername else: @@ -129,16 +135,16 @@ class URL(object): if hasattr(cls, 'dialect') and \ isinstance(cls.dialect, type) and \ issubclass(cls.dialect, Dialect): - return cls.dialect, cls.dialect + return cls.dialect else: - dialect_cls = cls.get_dialect_cls(self) - return cls, dialect_cls + return cls def get_dialect(self): """Return the SQLAlchemy database dialect class corresponding to this URL's driver name. """ - entrypoint, dialect_cls = self._get_dialect_plus_entrypoint() + entrypoint = self._get_entrypoint() + dialect_cls = entrypoint.get_dialect_cls(self) return dialect_cls def translate_connect_args(self, names=[], **kw): -- cgit v1.2.1 From a50dcb31b9757ca7602b85458615b7c267454cf9 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 26 May 2015 10:56:23 -0400 Subject: - Fixed bug where known boolean values used by :func:`.engine_from_config` were not being parsed correctly; these included ``pool_threadlocal`` and the psycopg2 argument ``use_native_unicode``. fixes #3435 - add legacy_schema_aliasing config parsing for mssql - move use_native_unicode config arg to the psycopg2 dialect --- lib/sqlalchemy/engine/__init__.py | 6 +++--- lib/sqlalchemy/engine/default.py | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 0678dd201..f1eacf6a6 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -394,9 +394,9 @@ def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): 'prefix' argument indicates the prefix to be searched for. A select set of keyword arguments will be "coerced" to their - expected type based on string values. In a future release, this - functionality will be expanded and include dialect-specific - arguments. + expected type based on string values. The set of arguments + is extensible per-dialect using the ``engine_config_types`` accessor. + """ options = dict((key[len(prefix):], configuration[key]) diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 9330a602c..9a7b80bfd 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -61,14 +61,13 @@ class DefaultDialect(interfaces.Dialect): engine_config_types = util.immutabledict([ ('convert_unicode', util.bool_or_str('force')), - ('pool_timeout', int), + ('pool_timeout', util.asint), ('echo', util.bool_or_str('debug')), ('echo_pool', util.bool_or_str('debug')), - ('pool_recycle', int), - ('pool_size', int), - ('max_overflow', int), - ('pool_threadlocal', bool), - ('use_native_unicode', bool), + ('pool_recycle', util.asint), + ('pool_size', util.asint), + ('max_overflow', util.asint), + ('pool_threadlocal', util.asbool), ]) # if the NUMERIC type -- cgit v1.2.1 From 54b15aaf37917c946e3f4fa45cc1262e14b22ef4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 6 Jun 2015 17:50:32 -0400 Subject: - Added new engine event :meth:`.ConnectionEvents.engine_disposed`. Called after the :meth:`.Engine.dispose` method is called. --- lib/sqlalchemy/engine/base.py | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 7ebe39bbf..59754a436 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1834,6 +1834,7 @@ class Engine(Connectable, log.Identified): """ self.pool.dispose() self.pool = self.pool.recreate() + self.dispatch.engine_disposed(self) def _execute_default(self, default): with self.contextual_connect() as conn: -- cgit v1.2.1 From 235165d54618031f525f029e5686a03c273a7c7e Mon Sep 17 00:00:00 2001 From: Morgan McClure Date: Sat, 13 Jun 2015 13:59:27 -0700 Subject: BufferedRowResultProxy should stop growing at 100 --- lib/sqlalchemy/engine/result.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 6d19cb6d0..56c81c93e 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -1083,10 +1083,7 @@ class BufferedRowResultProxy(ResultProxy): 5: 10, 10: 20, 20: 50, - 50: 100, - 100: 250, - 250: 500, - 500: 1000 + 50: 100 } def __buffer_rows(self): -- cgit v1.2.1 From a8c6cce404caf4a9c20faefc8f11a3e37db3ea05 Mon Sep 17 00:00:00 2001 From: Morgan McClure Date: Sat, 13 Jun 2015 19:27:55 -0700 Subject: Added max_row_buffer attribute to the context execution options and use it to prevent excess memory usage with yield_per --- lib/sqlalchemy/engine/result.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 56c81c93e..41b30c983 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -1067,7 +1067,7 @@ class BufferedRowResultProxy(ResultProxy): 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. + for additional rows up to a size of 1000. """ def _init_metadata(self): @@ -1083,7 +1083,10 @@ class BufferedRowResultProxy(ResultProxy): 5: 10, 10: 20, 20: 50, - 50: 100 + 50: 100, + 100: 250, + 250: 500, + 500: 1000 } def __buffer_rows(self): @@ -1092,6 +1095,8 @@ class BufferedRowResultProxy(ResultProxy): size = getattr(self, '_bufsize', 1) self.__rowbuffer = collections.deque(self.cursor.fetchmany(size)) self._bufsize = self.size_growth.get(size, size) + if self.context.execution_options.get('max_row_buffer') is not None: + self._bufsize = min(self.context.execution_options['max_row_buffer'], self._bufsize) def _soft_close(self, **kw): self.__rowbuffer.clear() -- cgit v1.2.1 From 9ccdea3a0fe57931e779b44eb2c278b78eea3d95 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 14 Jun 2015 16:43:16 -0400 Subject: - add test cases for pullreq github:182, where we add a new "max_row_buffer" execution option for BufferedRowResultProxy - also add documentation, changelog and version notes - rework the max_row_buffer argument to be interpreted from the execution options upfront when the BufferedRowResultProxy is first initialized. --- lib/sqlalchemy/engine/result.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 41b30c983..b2b78dee8 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -1068,9 +1068,26 @@ class BufferedRowResultProxy(ResultProxy): 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 1000. + + The size argument is configurable using the ``max_row_buffer`` + execution option:: + + with psycopg2_engine.connect() as conn: + + result = conn.execution_options( + stream_results=True, max_row_buffer=50 + ).execute("select * from table") + + .. versionadded:: 1.0.6 Added the ``max_row_buffer`` option. + + .. seealso:: + + :ref:`psycopg2_execution_options` """ def _init_metadata(self): + self._max_row_buffer = self.context.execution_options.get( + 'max_row_buffer', None) self.__buffer_rows() super(BufferedRowResultProxy, self)._init_metadata() @@ -1095,8 +1112,8 @@ class BufferedRowResultProxy(ResultProxy): size = getattr(self, '_bufsize', 1) self.__rowbuffer = collections.deque(self.cursor.fetchmany(size)) self._bufsize = self.size_growth.get(size, size) - if self.context.execution_options.get('max_row_buffer') is not None: - self._bufsize = min(self.context.execution_options['max_row_buffer'], self._bufsize) + if self._max_row_buffer is not None: + self._bufsize = min(self._max_row_buffer, self._bufsize) def _soft_close(self, **kw): self.__rowbuffer.clear() -- cgit v1.2.1 From 3d78705cf4981e460d6d1b5bb08870286fc3fe93 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 19 Jun 2015 11:49:49 -0400 Subject: - add explciit section on engine disposal, fixes #3461 --- lib/sqlalchemy/engine/base.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 59754a436..dea92e512 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1811,25 +1811,21 @@ class Engine(Connectable, log.Identified): def dispose(self): """Dispose of the connection pool used by this :class:`.Engine`. + This has the effect of fully closing all **currently checked in** + database connections. Connections that are still checked out + will **not** be closed, however they will no longer be associated + with this :class:`.Engine`, so when they are closed individually + they will close out fully. + A new connection pool is created immediately after the old one has been disposed. This new pool, like all SQLAlchemy connection pools, does not make any actual connections to the database until one is - first requested. - - This method has two general use cases: + first requested, so as long as the :class:`.Engine` isn't used again, + no new connections will be made. - * When a dropped connection is detected, it is assumed that all - connections held by the pool are potentially dropped, and - the entire pool is replaced. - - * An application may want to use :meth:`dispose` within a test - suite that is creating multiple engines. + .. seealso:: - It is critical to note that :meth:`dispose` does **not** guarantee - that the application will release all open database connections - only - those connections that are checked into the pool are closed. - Connections which remain checked out or have been detached from - the engine are not affected. + :ref:`engine_disposal` """ self.pool.dispose() -- cgit v1.2.1 From 7aa2100db3e6f768a280b4dfdb675d6709f94625 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 19 Jun 2015 14:54:26 -0400 Subject: - more edits, references #3461 --- lib/sqlalchemy/engine/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index dea92e512..c5eabac0d 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1814,8 +1814,10 @@ class Engine(Connectable, log.Identified): This has the effect of fully closing all **currently checked in** database connections. Connections that are still checked out will **not** be closed, however they will no longer be associated - with this :class:`.Engine`, so when they are closed individually - they will close out fully. + with this :class:`.Engine`, so when they are closed individually, + eventually the :class:`.Pool` which they are associated with will + be garbage collected and they will be closed out fully, if + not already closed on checkin. A new connection pool is created immediately after the old one has been disposed. This new pool, like all SQLAlchemy connection pools, -- cgit v1.2.1 From f31c288b65281511338c518bdf7fbe78c985af58 Mon Sep 17 00:00:00 2001 From: jakeogh Date: Sat, 27 Jun 2015 08:40:44 +0000 Subject: add MINVALUE support to Sequence() --- lib/sqlalchemy/engine/interfaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 73a8b4635..a58144867 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -252,7 +252,7 @@ class Dialect(object): sequence a dictionary of the form - {'name' : str, 'start' :int, 'increment': int} + {'name' : str, 'start' :int, 'increment': int, 'minvalue': int} Additional column attributes may be present. """ @@ -1147,4 +1147,4 @@ class ExceptionContext(object): .. versionadded:: 1.0.3 - """ \ No newline at end of file + """ -- cgit v1.2.1 From ad7caa69884bddf6f35da2facc516ab08904c71e Mon Sep 17 00:00:00 2001 From: jakeogh Date: Sat, 27 Jun 2015 18:37:09 +0000 Subject: add MAXVALUE support to Sequence() --- lib/sqlalchemy/engine/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index a58144867..1779f6d48 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -252,7 +252,7 @@ class Dialect(object): sequence a dictionary of the form - {'name' : str, 'start' :int, 'increment': int, 'minvalue': int} + {'name' : str, 'start' :int, 'increment': int, 'minvalue': int, 'maxvalue': int} Additional column attributes may be present. """ -- cgit v1.2.1 From 85ebe01349e0b4314d9e25cacc6701d6fed7b87e Mon Sep 17 00:00:00 2001 From: jakeogh Date: Sat, 27 Jun 2015 18:48:46 +0000 Subject: add NO MINVALUE and NO MAXVALUE support to Sequence() --- lib/sqlalchemy/engine/interfaces.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 1779f6d48..9a31f05ae 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -252,7 +252,8 @@ class Dialect(object): sequence a dictionary of the form - {'name' : str, 'start' :int, 'increment': int, 'minvalue': int, 'maxvalue': int} + {'name' : str, 'start' :int, 'increment': int, 'minvalue': int, + 'maxvalue': int, 'nominvalue': bool, 'nomaxvalue': bool} Additional column attributes may be present. """ -- cgit v1.2.1 From 66b9a71ce7457b618e9040c1c0bbc2dbd1966354 Mon Sep 17 00:00:00 2001 From: jakeogh Date: Sat, 27 Jun 2015 20:49:46 +0000 Subject: add CYCLE support to Sequence() and docstrings for NO MINVALUE and NO MAXVALUE --- lib/sqlalchemy/engine/interfaces.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 9a31f05ae..3bad765df 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -253,7 +253,8 @@ class Dialect(object): sequence a dictionary of the form {'name' : str, 'start' :int, 'increment': int, 'minvalue': int, - 'maxvalue': int, 'nominvalue': bool, 'nomaxvalue': bool} + 'maxvalue': int, 'nominvalue': bool, 'nomaxvalue': bool, + 'cycle': bool} Additional column attributes may be present. """ -- cgit v1.2.1 From 18a078654da286c0adf51a20a21398e357ed12ed Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Wed, 4 Jun 2014 09:56:13 +0100 Subject: Remove RootTransaction<->RootTransaction reference cycle (cherry picked from commit 3ef00e816da042d4932be53b86f76db17c800842) --- lib/sqlalchemy/engine/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index c5eabac0d..eaa435d45 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1531,9 +1531,13 @@ class Transaction(object): def __init__(self, connection, parent): self.connection = connection - self._parent = parent or self + self._actual_parent = parent self.is_active = True + @property + def _parent(self): + return self._actual_parent or self + def close(self): """Close this :class:`.Transaction`. -- cgit v1.2.1 From d7ceb63c94e4f8ade58f9d9c9462f7acd5037cd6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 19 Jul 2015 12:20:00 -0400 Subject: - Fixed regression where :meth:`.ResultProxy.keys` would return un-adjusted internal symbol names for "anonymous" labels, which are the "foo_1" types of labels we see generated for SQL functions without labels and similar. This was a side effect of the performance enhancements implemented as part of references #918. fixes #3483 --- lib/sqlalchemy/engine/result.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index b2b78dee8..3fcab873b 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -221,7 +221,7 @@ class ResultMetaData(object): in enumerate(result_columns) ] self.keys = [ - elem[1] for elem in result_columns + elem[0] for elem in result_columns ] else: # case 2 - raw string, or number of columns in result does @@ -236,7 +236,8 @@ class ResultMetaData(object): # that SQLAlchemy has used up through 0.9. if num_ctx_cols: - result_map = self._create_result_map(result_columns) + result_map = self._create_result_map( + result_columns, case_sensitive) raw = [] self.keys = [] @@ -329,10 +330,12 @@ class ResultMetaData(object): ]) @classmethod - def _create_result_map(cls, result_columns): + def _create_result_map(cls, result_columns, case_sensitive=True): d = {} for elem in result_columns: key, rec = elem[0], elem[1:] + if not case_sensitive: + key = key.lower() if key in d: # conflicting keyname, just double up the list # of objects. this will cause an "ambiguous name" -- cgit v1.2.1 From ddad19052965b4f1ed75ad0eb33217da18aa81e4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 19 Jul 2015 16:32:31 -0400 Subject: - Fixed regression where new methods on :class:`.ResultProxy` used by the ORM :class:`.Query` object (part of the performance enhancements of :ticket:`3175`) would not raise the "this result does not return rows" exception in the case where the driver (typically MySQL) fails to generate cursor.description correctly; an AttributeError against NoneType would be raised instead. fixes #3481 --- lib/sqlalchemy/engine/result.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 3fcab873b..74a0fce77 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -495,10 +495,20 @@ class ResultProxy(object): self._init_metadata() def _getter(self, key): - return self._metadata._getter(key) + try: + getter = self._metadata._getter + except AttributeError: + return self._non_result(None) + else: + return getter(key) def _has_key(self, key): - return self._metadata._has_key(key) + try: + has_key = self._metadata._has_key + except AttributeError: + return self._non_result(None) + else: + return has_key(key) def _init_metadata(self): metadata = self._cursor_description() -- cgit v1.2.1 From 1195155937e46ea79ea38321cc69cd0310372636 Mon Sep 17 00:00:00 2001 From: Eric Siegerman Date: Wed, 23 Sep 2015 15:35:35 -0400 Subject: A few minor rewordings (cherry picked from commit ea084bdc656a6a64db1ee582630d415bc8154505) --- lib/sqlalchemy/engine/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index f1eacf6a6..114e8f46d 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -389,9 +389,9 @@ def create_engine(*args, **kwargs): def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): """Create a new Engine instance using a configuration dictionary. - The dictionary is typically produced from a config file where keys - are prefixed, such as sqlalchemy.url, sqlalchemy.echo, etc. The - 'prefix' argument indicates the prefix to be searched for. + The *configuration* dictionary is typically produced from a config file + in which keys are prefixed, e.g. sqlalchemy.url, sqlalchemy.echo, etc. + The 'prefix' argument indicates the prefix to be searched for. A select set of keyword arguments will be "coerced" to their expected type based on string values. The set of arguments -- cgit v1.2.1 From f29be1e6f7f9de812633efdf6323dc5e87b18cd2 Mon Sep 17 00:00:00 2001 From: Eric Siegerman Date: Wed, 23 Sep 2015 15:37:23 -0400 Subject: Add some markup (cherry picked from commit 81eefe038ea44a5314002483dde9cf00580df1bd) --- lib/sqlalchemy/engine/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 114e8f46d..4b6e752ba 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -390,8 +390,8 @@ def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): """Create a new Engine instance using a configuration dictionary. The *configuration* dictionary is typically produced from a config file - in which keys are prefixed, e.g. sqlalchemy.url, sqlalchemy.echo, etc. - The 'prefix' argument indicates the prefix to be searched for. + in which keys are prefixed, e.g. ``sqlalchemy.url``, ``sqlalchemy.echo``, + etc. The 'prefix' argument indicates the prefix to be searched for. A select set of keyword arguments will be "coerced" to their expected type based on string values. The set of arguments -- cgit v1.2.1 From 0854f82993c9bc07a64cc52bb4c092d1f5f11b8c Mon Sep 17 00:00:00 2001 From: Eric Siegerman Date: Wed, 23 Sep 2015 16:39:11 -0400 Subject: Add a lot more detail (cherry picked from commit 5db5e18d3babdb3ee857c075c774a585505b78ce) --- lib/sqlalchemy/engine/__init__.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 4b6e752ba..0b0d50329 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -389,14 +389,33 @@ def create_engine(*args, **kwargs): def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): """Create a new Engine instance using a configuration dictionary. - The *configuration* dictionary is typically produced from a config file - in which keys are prefixed, e.g. ``sqlalchemy.url``, ``sqlalchemy.echo``, - etc. The 'prefix' argument indicates the prefix to be searched for. + The dictionary is typically produced from a config file. + + The keys of interest to ``engine_from_config()`` should be prefixed, e.g. + ``sqlalchemy.url``, ``sqlalchemy.echo``, etc. The 'prefix' argument + indicates the prefix to be searched for. Each matching key (after the + prefix is stripped) is treated as though it were the corresponding keyword + argument to a :func:`.create_engine` call. + + The only required key is (assuming the default prefix) ``sqlalchemy.url``, + which provides the :ref:`database URL `. A select set of keyword arguments will be "coerced" to their expected type based on string values. The set of arguments is extensible per-dialect using the ``engine_config_types`` accessor. + :param configuration: A dictionary (typically produced from a config file, + but this is not a requirement). Items whose keys start with the value + of 'prefix' will have that prefix stripped, and will then be passed to + :ref:`create_engine`. + + :param prefix: Prefix to match and then strip from keys + in 'configuration'. + + :param kwargs: Each keyword argument to ``engine_from_config()`` itself + overrides the corresponding item taken from the 'configuration' + dictionary. Keyword arguments should *not* be prefixed. + """ options = dict((key[len(prefix):], configuration[key]) -- cgit v1.2.1 From 6ab120558078bdcbfbe06d2ca55bd7a0d417bbb4 Mon Sep 17 00:00:00 2001 From: pgjones Date: Fri, 30 Oct 2015 20:20:58 +0000 Subject: Change generator termination from StopIteration to return. From [PEP 479](https://www.python.org/dev/peps/pep-0479/) the correct way to terminate a generator is to return (which implicitly raises StopIteration) rather than raise StopIteration. Without this change using sqlalchemy in python 3.5 or greater results in these warnings PendingDeprecationWarning: generator '__iter__' raised StopIteration which this commit should remove. --- lib/sqlalchemy/engine/result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 74a0fce77..7d1425c28 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -712,7 +712,7 @@ class ResultProxy(object): while True: row = self.fetchone() if row is None: - raise StopIteration + return else: yield row -- cgit v1.2.1 From 197ffa2be2cadce3df8bfb0799b3c80158250286 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 5 Jan 2016 10:25:36 -0500 Subject: - Fixed 1.0 regression where the eager fetch of cursor.rowcount was no longer called for an UPDATE or DELETE statement emitted via plain text or via the :func:`.text` construct, affecting those drivers that erase cursor.rowcount once the cursor is closed such as SQL Server ODBC and Firebird drivers. fixes #3622 --- lib/sqlalchemy/engine/base.py | 2 +- lib/sqlalchemy/engine/default.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index eaa435d45..31e253eed 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1155,7 +1155,7 @@ class Connection(Connectable): if context.compiled: context.post_exec() - if context.is_crud: + if context.is_crud or context.is_text: result = context._setup_crud_result_proxy() else: result = context.get_result_proxy() diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 9a7b80bfd..87278c2be 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -467,6 +467,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): isupdate = False isdelete = False is_crud = False + is_text = False isddl = False executemany = False compiled = None @@ -543,6 +544,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.isinsert = compiled.isinsert self.isupdate = compiled.isupdate self.isdelete = compiled.isdelete + self.is_text = compiled.isplaintext if not parameters: self.compiled_parameters = [compiled.construct_params()] @@ -622,6 +624,7 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.root_connection = connection self._dbapi_connection = dbapi_connection self.dialect = connection.dialect + self.is_text = True # plain text statement self.execution_options = connection._execution_options -- cgit v1.2.1 From c8b7729338ba32a726be72b5409b4651326042e9 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 6 Jan 2016 17:20:57 -0500 Subject: - Added a new entrypoint system to the engine to allow "plugins" to be stated in the query string for a URL. Custom plugins can be written which will be given the chance up front to alter and/or consume the engine's URL and keyword arguments, and then at engine create time will be given the engine itself to allow additional modifications or event registration. Plugins are written as a subclass of :class:`.CreateEnginePlugin`; see that class for details. fixes #3536 --- lib/sqlalchemy/engine/__init__.py | 3 +- lib/sqlalchemy/engine/interfaces.py | 105 ++++++++++++++++++++++++++++++++++++ lib/sqlalchemy/engine/strategies.py | 7 +++ lib/sqlalchemy/engine/url.py | 10 +++- 4 files changed, 123 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 0b0d50329..02c35d6a9 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -53,6 +53,7 @@ url.py from .interfaces import ( Connectable, + CreateEnginePlugin, Dialect, ExecutionContext, ExceptionContext, @@ -390,7 +391,7 @@ def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): """Create a new Engine instance using a configuration dictionary. The dictionary is typically produced from a config file. - + The keys of interest to ``engine_from_config()`` should be prefixed, e.g. ``sqlalchemy.url``, ``sqlalchemy.echo``, etc. The 'prefix' argument indicates the prefix to be searched for. Each matching key (after the diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 3bad765df..41325878c 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -781,6 +781,111 @@ class Dialect(object): pass +class CreateEnginePlugin(object): + """A set of hooks intended to augment the construction of an + :class:`.Engine` object based on entrypoint names in a URL. + + The purpose of :class:`.CreateEnginePlugin` is to allow third-party + systems to apply engine, pool and dialect level event listeners without + the need for the target application to be modified; instead, the plugin + names can be added to the database URL. Target applications for + :class:`.CreateEnginePlugin` include: + + * connection and SQL performance tools, e.g. which use events to track + number of checkouts and/or time spent with statements + + * connectivity plugins such as proxies + + Plugins are registered using entry points in a similar way as that + of dialects:: + + entry_points={ + 'sqlalchemy.plugins': [ + 'myplugin = myapp.plugins:MyPlugin' + ] + + A plugin that uses the above names would be invoked from a database + URL as in:: + + from sqlalchemy import create_engine + + engine = create_engine( + "mysql+pymysql://scott:tiger@localhost/test?plugin=myplugin") + + The ``plugin`` argument supports multiple instances, so that a URL + may specify multiple plugins; they are loaded in the order stated + in the URL:: + + engine = create_engine( + "mysql+pymysql://scott:tiger@localhost/" + "test?plugin=plugin_one&plugin=plugin_twp&plugin=plugin_three") + + A plugin can receive additional arguments from the URL string as + well as from the keyword arguments passed to :func:`.create_engine`. + The :class:`.URL` object and the keyword dictionary are passed to the + constructor so that these arguments can be extracted from the url's + :attr:`.URL.query` collection as well as from the dictionary:: + + class MyPlugin(CreateEnginePlugin): + def __init__(self, url, kwargs): + self.my_argument_one = url.query.pop('my_argument_one') + self.my_argument_two = url.query.pop('my_argument_two') + self.my_argument_three = kwargs.pop('my_argument_three', None) + + Arguments like those illustrated above would be consumed from the + following:: + + from sqlalchemy import create_engine + + engine = create_engine( + "mysql+pymysql://scott:tiger@localhost/" + "test?plugin=myplugin&my_argument_one=foo&my_argument_two=bar", + my_argument_three='bat') + + The URL and dictionary are used for subsequent setup of the engine + as they are, so the plugin can modify their arguments in-place. + Arguments that are only understood by the plugin should be popped + or otherwise removed so that they aren't interpreted as erroneous + arguments afterwards. + + When the engine creation process completes and produces the + :class:`.Engine` object, it is again passed to the plugin via the + :meth:`.CreateEnginePlugin.engine_created` hook. In this hook, additional + changes can be made to the engine, most typically involving setup of + events (e.g. those defined in :ref:`core_event_toplevel`). + + .. versionadded:: 1.1 + + """ + def __init__(self, url, kwargs): + """Contruct a new :class:`.CreateEnginePlugin`. + + The plugin object is instantiated individually for each call + to :func:`.create_engine`. A single :class:`.Engine` will be + passed to the :meth:`.CreateEnginePlugin.engine_created` method + corresponding to this URL. + + :param url: the :class:`.URL` object. The plugin should inspect + what it needs here as well as remove its custom arguments from the + :attr:`.URL.query` collection. The URL can be modified in-place + in any other way as well. + :param kwargs: The keyword arguments passed to :func`.create_engine`. + The plugin can read and modify this dictionary in-place, to affect + the ultimate arguments used to create the engine. It should + remove its custom arguments from the dictionary as well. + + """ + self.url = url + + def engine_created(self, engine): + """Receive the :class:`.Engine` object when it is fully constructed. + + The plugin may make additional changes to the engine, such as + registering engine or connection pool events. + + """ + + class ExecutionContext(object): """A messenger object for a Dialect that corresponds to a single execution. diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index a539ee9f7..0d0414ed1 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -48,6 +48,10 @@ class DefaultEngineStrategy(EngineStrategy): # create url.URL object u = url.make_url(name_or_url) + plugins = u._instantiate_plugins(kwargs) + + u.query.pop('plugin', None) + entrypoint = u._get_entrypoint() dialect_cls = entrypoint.get_dialect_cls(u) @@ -169,6 +173,9 @@ class DefaultEngineStrategy(EngineStrategy): if entrypoint is not dialect_cls: entrypoint.engine_created(engine) + for plugin in plugins: + plugin.engine_created(engine) + return engine diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 32e3f8a6b..9a955948a 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -17,7 +17,7 @@ be used directly and is also accepted directly by ``create_engine()``. import re from .. import exc, util from . import Dialect -from ..dialects import registry +from ..dialects import registry, plugins class URL(object): @@ -117,6 +117,14 @@ class URL(object): else: return self.drivername.split('+')[1] + def _instantiate_plugins(self, kwargs): + plugin_names = util.to_list(self.query.get('plugin', ())) + + return [ + plugins.load(plugin_name)(self, kwargs) + for plugin_name in plugin_names + ] + def _get_entrypoint(self): """Return the "entry point" dialect class. -- cgit v1.2.1 From 89facbed8855d1443dbe37919ff0645aea640ed0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 8 Jan 2016 22:11:09 -0500 Subject: - Multi-tenancy schema translation for :class:`.Table` objects is added. This supports the use case of an application that uses the same set of :class:`.Table` objects in many schemas, such as schema-per-user. A new execution option :paramref:`.Connection.execution_options.schema_translate_map` is added. fixes #2685 - latest tox doesn't like the {posargs} in the profile rerunner --- lib/sqlalchemy/engine/base.py | 42 +++++++++++++++++++++++++++++++++---- lib/sqlalchemy/engine/default.py | 8 +++++++ lib/sqlalchemy/engine/reflection.py | 3 ++- lib/sqlalchemy/engine/strategies.py | 3 +++ 4 files changed, 51 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 31e253eed..88f53abcf 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -44,6 +44,8 @@ class Connection(Connectable): """ + _schema_translate_map = None + def __init__(self, engine, connection=None, close_with_result=False, _branch_from=None, _execution_options=None, _dispatch=None, @@ -140,6 +142,13 @@ class Connection(Connectable): c.__dict__ = self.__dict__.copy() return c + def _get_effective_schema(self, table): + effective_schema = table.schema + if self._schema_translate_map: + effective_schema = self._schema_translate_map.get( + effective_schema, effective_schema) + return effective_schema + def __enter__(self): return self @@ -277,6 +286,19 @@ class Connection(Connectable): of many DBAPIs. The flag is currently understood only by the psycopg2 dialect. + :param schema_translate_map: Available on: Connection, Engine. + A dictionary mapping schema names to schema names, that will be + applied to the :paramref:`.Table.schema` element of each + :class:`.Table` encountered when SQL or DDL expression elements + are compiled into strings; the resulting schema name will be + converted based on presence in the map of the original name. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`schema_translating` + """ c = self._clone() c._execution_options = c._execution_options.union(opt) @@ -959,7 +981,9 @@ class Connection(Connectable): dialect = self.dialect - compiled = ddl.compile(dialect=dialect) + compiled = ddl.compile( + dialect=dialect, + schema_translate_map=self._schema_translate_map) ret = self._execute_context( dialect, dialect.execution_ctx_cls._init_ddl, @@ -990,17 +1014,27 @@ class Connection(Connectable): dialect = self.dialect if 'compiled_cache' in self._execution_options: - key = dialect, elem, tuple(sorted(keys)), len(distilled_params) > 1 + key = ( + dialect, elem, tuple(sorted(keys)), + tuple( + (k, self._schema_translate_map[k]) + for k in sorted(self._schema_translate_map) + ) if self._schema_translate_map else None, + len(distilled_params) > 1 + ) compiled_sql = self._execution_options['compiled_cache'].get(key) if compiled_sql is None: compiled_sql = elem.compile( dialect=dialect, column_keys=keys, - inline=len(distilled_params) > 1) + inline=len(distilled_params) > 1, + schema_translate_map=self._schema_translate_map + ) self._execution_options['compiled_cache'][key] = compiled_sql else: compiled_sql = elem.compile( dialect=dialect, column_keys=keys, - inline=len(distilled_params) > 1) + inline=len(distilled_params) > 1, + schema_translate_map=self._schema_translate_map) ret = self._execute_context( dialect, diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 87278c2be..160fe545e 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -398,10 +398,18 @@ class DefaultDialect(interfaces.Dialect): if not branch: self._set_connection_isolation(connection, isolation_level) + if 'schema_translate_map' in opts: + @event.listens_for(engine, "engine_connect") + def set_schema_translate_map(connection, branch): + connection._schema_translate_map = opts['schema_translate_map'] + def set_connection_execution_options(self, connection, opts): if 'isolation_level' in opts: self._set_connection_isolation(connection, opts['isolation_level']) + if 'schema_translate_map' in opts: + connection._schema_translate_map = opts['schema_translate_map'] + def _set_connection_isolation(self, connection, level): if connection.in_transaction(): util.warn( diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 59eed51ec..dca99e1ce 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -529,7 +529,8 @@ class Inspector(object): """ dialect = self.bind.dialect - schema = table.schema + schema = self.bind._get_effective_schema(table) + table_name = table.name # get table-level arguments that are specifically diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index 0d0414ed1..cb3e6fa8a 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -233,6 +233,9 @@ class MockEngineStrategy(EngineStrategy): dialect = property(attrgetter('_dialect')) name = property(lambda s: s._dialect.name) + def _get_effective_schema(self, table): + return table.schema + def contextual_connect(self, **kwargs): return self -- cgit v1.2.1 From 331caf11d307430552139c032a822b52da8c1e75 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 9 Jan 2016 22:25:56 -0500 Subject: - ensure we use a Connection for effective schema here since Engine doesn't have it; keep it simple --- lib/sqlalchemy/engine/base.py | 1 + lib/sqlalchemy/engine/reflection.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 88f53abcf..79b5f57d1 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -69,6 +69,7 @@ class Connection(Connectable): self.should_close_with_result = False self.dispatch = _dispatch self._has_events = _branch_from._has_events + self._schema_translate_map = _branch_from._schema_translate_map else: self.__connection = connection \ if connection is not None else engine.raw_connection() diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index dca99e1ce..17d9958bb 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -529,7 +529,8 @@ class Inspector(object): """ dialect = self.bind.dialect - schema = self.bind._get_effective_schema(table) + with self.bind.connect() as conn: + schema = conn._get_effective_schema(table) table_name = table.name -- cgit v1.2.1 From 6fbfadc7388dad4576ad99ce597e0878ee1d297f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 11 Jan 2016 14:35:56 -0500 Subject: - reorganize schema_translate_map to be succinct and gain the performance back by using an attrgetter for the default case --- lib/sqlalchemy/engine/base.py | 56 ++++++++++++++++++++++++++----------- lib/sqlalchemy/engine/default.py | 10 +++++-- lib/sqlalchemy/engine/interfaces.py | 2 +- lib/sqlalchemy/engine/reflection.py | 3 +- lib/sqlalchemy/engine/strategies.py | 6 ++-- 5 files changed, 52 insertions(+), 25 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 79b5f57d1..0b928566d 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -14,6 +14,7 @@ from __future__ import with_statement import sys from .. import exc, util, log, interfaces from ..sql import util as sql_util +from ..sql import schema from .interfaces import Connectable, ExceptionContext from .util import _distill_params import contextlib @@ -44,7 +45,21 @@ class Connection(Connectable): """ - _schema_translate_map = None + schema_for_object = schema._schema_getter(None) + """Return the ".schema" attribute for an object. + + Used for :class:`.Table`, :class:`.Sequence` and similar objects, + and takes into account + the :paramref:`.Connection.execution_options.schema_translate_map` + parameter. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`schema_translating` + + """ def __init__(self, engine, connection=None, close_with_result=False, _branch_from=None, _execution_options=None, @@ -69,7 +84,7 @@ class Connection(Connectable): self.should_close_with_result = False self.dispatch = _dispatch self._has_events = _branch_from._has_events - self._schema_translate_map = _branch_from._schema_translate_map + self.schema_for_object = _branch_from.schema_for_object else: self.__connection = connection \ if connection is not None else engine.raw_connection() @@ -143,13 +158,6 @@ class Connection(Connectable): c.__dict__ = self.__dict__.copy() return c - def _get_effective_schema(self, table): - effective_schema = table.schema - if self._schema_translate_map: - effective_schema = self._schema_translate_map.get( - effective_schema, effective_schema) - return effective_schema - def __enter__(self): return self @@ -984,7 +992,8 @@ class Connection(Connectable): compiled = ddl.compile( dialect=dialect, - schema_translate_map=self._schema_translate_map) + schema_translate_map=self.schema_for_object + if not self.schema_for_object.is_default else None) ret = self._execute_context( dialect, dialect.execution_ctx_cls._init_ddl, @@ -1017,10 +1026,7 @@ class Connection(Connectable): if 'compiled_cache' in self._execution_options: key = ( dialect, elem, tuple(sorted(keys)), - tuple( - (k, self._schema_translate_map[k]) - for k in sorted(self._schema_translate_map) - ) if self._schema_translate_map else None, + self.schema_for_object.hash_key, len(distilled_params) > 1 ) compiled_sql = self._execution_options['compiled_cache'].get(key) @@ -1028,14 +1034,16 @@ class Connection(Connectable): compiled_sql = elem.compile( dialect=dialect, column_keys=keys, inline=len(distilled_params) > 1, - schema_translate_map=self._schema_translate_map + schema_translate_map=self.schema_for_object + if not self.schema_for_object.is_default else None ) self._execution_options['compiled_cache'][key] = compiled_sql else: compiled_sql = elem.compile( dialect=dialect, column_keys=keys, inline=len(distilled_params) > 1, - schema_translate_map=self._schema_translate_map) + schema_translate_map=self.schema_for_object + if not self.schema_for_object.is_default else None) ret = self._execute_context( dialect, @@ -1721,6 +1729,22 @@ class Engine(Connectable, log.Identified): _has_events = False _connection_cls = Connection + schema_for_object = schema._schema_getter(None) + """Return the ".schema" attribute for an object. + + Used for :class:`.Table`, :class:`.Sequence` and similar objects, + and takes into account + the :paramref:`.Connection.execution_options.schema_translate_map` + parameter. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`schema_translating` + + """ + def __init__(self, pool, dialect, url, logging_name=None, echo=None, proxy=None, execution_options=None diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 160fe545e..6c42af8b1 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -16,7 +16,7 @@ as the base class for their own corresponding classes. import re import random from . import reflection, interfaces, result -from ..sql import compiler, expression +from ..sql import compiler, expression, schema from .. import types as sqltypes from .. import exc, util, pool, processors import codecs @@ -399,16 +399,20 @@ class DefaultDialect(interfaces.Dialect): self._set_connection_isolation(connection, isolation_level) if 'schema_translate_map' in opts: + getter = schema._schema_getter(opts['schema_translate_map']) + engine.schema_for_object = getter + @event.listens_for(engine, "engine_connect") def set_schema_translate_map(connection, branch): - connection._schema_translate_map = opts['schema_translate_map'] + connection.schema_for_object = getter def set_connection_execution_options(self, connection, opts): if 'isolation_level' in opts: self._set_connection_isolation(connection, opts['isolation_level']) if 'schema_translate_map' in opts: - connection._schema_translate_map = opts['schema_translate_map'] + getter = schema._schema_getter(opts['schema_translate_map']) + connection.schema_for_object = getter def _set_connection_isolation(self, connection, level): if connection.in_transaction(): diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 41325878c..c84823d1e 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -7,7 +7,7 @@ """Define core interfaces used by the engine system.""" -from .. import util, event +from .. import util # backwards compat from ..sql.compiler import Compiled, TypeCompiler diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 17d9958bb..6880660ce 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -529,8 +529,7 @@ class Inspector(object): """ dialect = self.bind.dialect - with self.bind.connect() as conn: - schema = conn._get_effective_schema(table) + schema = self.bind.schema_for_object(table) table_name = table.name diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index cb3e6fa8a..d8e2d4764 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -18,8 +18,9 @@ New strategies can be added via new ``EngineStrategy`` classes. from operator import attrgetter from sqlalchemy.engine import base, threadlocal, url -from sqlalchemy import util, exc, event +from sqlalchemy import util, event from sqlalchemy import pool as poollib +from sqlalchemy.sql import schema strategies = {} @@ -233,8 +234,7 @@ class MockEngineStrategy(EngineStrategy): dialect = property(attrgetter('_dialect')) name = property(lambda s: s._dialect.name) - def _get_effective_schema(self, table): - return table.schema + schema_for_object = schema._schema_getter(None) def contextual_connect(self, **kwargs): return self -- cgit v1.2.1 From 1f7a1f777d8fe1bdea1e793c8ec8ebb7c625e347 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 14 Jan 2016 18:06:26 -0500 Subject: - A deep improvement to the recently added :meth:`.TextClause.columns` method, and its interaction with result-row processing, now allows the columns passed to the method to be positionally matched with the result columns in the statement, rather than matching on name alone. The advantage to this includes that when linking a textual SQL statement to an ORM or Core table model, no system of labeling or de-duping of common column names needs to occur, which also means there's no need to worry about how label names match to ORM columns and so-forth. In addition, the :class:`.ResultProxy` has been further enhanced to map column and string keys to a row with greater precision in some cases. fixes #3501 - reorganize the initialization of ResultMetaData for readability and complexity; use the name "cursor_description", define the task of "merging" cursor_description with compiled column information as its own function, and also define "name extraction" as a separate task. - fully change the name we use in the "ambiguous column" error to be the actual name that was ambiguous, modify the C ext also --- lib/sqlalchemy/engine/default.py | 3 +- lib/sqlalchemy/engine/result.py | 405 ++++++++++++++++++++++++++------------- 2 files changed, 275 insertions(+), 133 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 6c42af8b1..3e5f339b1 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -544,7 +544,8 @@ class DefaultExecutionContext(interfaces.ExecutionContext): connection._execution_options) self.result_column_struct = ( - compiled._result_columns, compiled._ordered_columns) + compiled._result_columns, compiled._ordered_columns, + compiled._textual_ordered_columns) self.unicode_statement = util.text_type(compiled) if not dialect.supports_unicode_statements: diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 7d1425c28..cc4ac74cd 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -84,8 +84,8 @@ except ImportError: raise if index is None: raise exc.InvalidRequestError( - "Ambiguous column name '%s' in result set! " - "try 'use_labels' option on select statement." % key) + "Ambiguous column name '%s' in " + "result set column descriptions" % obj) if processor is not None: return processor(self._row[index]) else: @@ -186,97 +186,29 @@ class ResultMetaData(object): """Handle cursor.description, applying additional info from an execution context.""" - def __init__(self, parent, metadata): + __slots__ = ( + '_keymap', 'case_sensitive', 'matched_on_name', + '_processors', 'keys', '_orig_processors') + + def __init__(self, parent, cursor_description): context = parent.context dialect = context.dialect - typemap = dialect.dbapi_type_map - translate_colname = context._translate_colname - self.case_sensitive = case_sensitive = dialect.case_sensitive + self.case_sensitive = dialect.case_sensitive + self.matched_on_name = False if context.result_column_struct: - result_columns, cols_are_ordered = context.result_column_struct + result_columns, cols_are_ordered, textual_ordered = \ + context.result_column_struct num_ctx_cols = len(result_columns) else: - num_ctx_cols = None - - if num_ctx_cols and \ - cols_are_ordered and \ - num_ctx_cols == len(metadata): - # case 1 - SQL expression statement, number of columns - # in result matches number of cols in compiled. This is the - # vast majority case for SQL expression constructs. In this - # case we don't bother trying to parse or match up to - # the colnames in the result description. - raw = [ - ( - idx, - key, - name.lower() if not case_sensitive else name, - context.get_result_processor( - type_, key, metadata[idx][1] - ), - obj, - None - ) for idx, (key, name, obj, type_) - in enumerate(result_columns) - ] - self.keys = [ - elem[0] for elem in result_columns - ] - else: - # case 2 - raw string, or number of columns in result does - # not match number of cols in compiled. The raw string case - # is very common. The latter can happen - # when text() is used with only a partial typemap, or - # in the extremely unlikely cases where the compiled construct - # has a single element with multiple col expressions in it - # (e.g. has commas embedded) or there's some kind of statement - # that is adding extra columns. - # In all these cases we fall back to the "named" approach - # that SQLAlchemy has used up through 0.9. - - if num_ctx_cols: - result_map = self._create_result_map( - result_columns, case_sensitive) - - raw = [] - self.keys = [] - untranslated = None - for idx, rec in enumerate(metadata): - colname = rec[0] - coltype = rec[1] - - if dialect.description_encoding: - colname = dialect._description_decoder(colname) - - if translate_colname: - colname, untranslated = translate_colname(colname) - - if dialect.requires_name_normalize: - colname = dialect.normalize_name(colname) - - self.keys.append(colname) - if not case_sensitive: - colname = colname.lower() - - if num_ctx_cols: - try: - ctx_rec = result_map[colname] - except KeyError: - mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) - obj = None - else: - obj = ctx_rec[1] - mapped_type = ctx_rec[2] - else: - mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) - obj = None - processor = context.get_result_processor( - mapped_type, colname, coltype) + result_columns = cols_are_ordered = \ + num_ctx_cols = textual_ordered = False - raw.append( - (idx, colname, colname, processor, obj, untranslated) - ) + # merge cursor.description with the column info + # present in the compiled structure, if any + raw = self._merge_cursor_description( + context, cursor_description, result_columns, + num_ctx_cols, cols_are_ordered, textual_ordered) # keymap indexes by integer index... self._keymap = dict([ @@ -288,12 +220,16 @@ class ResultMetaData(object): # views like __iter__ and slices self._processors = [elem[3] for elem in raw] + # keymap by primary string... + by_key = dict([ + (elem[2], (elem[3], elem[4], elem[0])) + for elem in raw + ]) + + # for compiled SQL constructs, copy additional lookup keys into + # the key lookup map, such as Column objects, labels, + # column keys and other names if num_ctx_cols: - # keymap by primary string... - by_key = dict([ - (elem[2], (elem[3], elem[4], elem[0])) - for elem in raw - ]) # if by-primary-string dictionary smaller (or bigger?!) than # number of columns, assume we have dupes, rewrite @@ -304,30 +240,250 @@ class ResultMetaData(object): for rec in raw: key = rec[1] if key in seen: - by_key[key] = (None, by_key[key][1], None) + # this is an "ambiguous" element, replacing + # the full record in the map + by_key[key] = (None, key, None) seen.add(key) - # update keymap with secondary "object"-based keys + # copy secondary elements from compiled columns + # into self._keymap, write in the potentially "ambiguous" + # element + self._keymap.update([ + (obj_elem, by_key[elem[2]]) + for elem in raw if elem[4] + for obj_elem in elem[4] + ]) + + # if we did a pure positional match, then reset the + # original "expression element" back to the "unambiguous" + # entry. This is a new behavior in 1.1 which impacts + # TextAsFrom but also straight compiled SQL constructs. + if not self.matched_on_name: + self._keymap.update([ + (elem[4][0], (elem[3], elem[4], elem[0])) + for elem in raw if elem[4] + ]) + else: + # no dupes - copy secondary elements from compiled + # columns into self._keymap + self._keymap.update([ + (obj_elem, (elem[3], elem[4], elem[0])) + for elem in raw if elem[4] + for obj_elem in elem[4] + ]) + + # update keymap with primary string names taking + # precedence + self._keymap.update(by_key) + + # update keymap with "translated" names (sqlite-only thing) + if not num_ctx_cols and context._translate_colname: self._keymap.update([ - (obj_elem, by_key[elem[2]]) - for elem in raw if elem[4] - for obj_elem in elem[4] + (elem[5], self._keymap[elem[2]]) + for elem in raw if elem[5] ]) - # update keymap with primary string names taking - # precedence - self._keymap.update(by_key) + def _merge_cursor_description( + self, context, cursor_description, result_columns, + num_ctx_cols, cols_are_ordered, textual_ordered): + """Merge a cursor.description with compiled result column information. + + There are at least four separate strategies used here, selected + depending on the type of SQL construct used to start with. + + The most common case is that of the compiled SQL expression construct, + which generated the column names present in the raw SQL string and + which has the identical number of columns as were reported by + cursor.description. In this case, we assume a 1-1 positional mapping + between the entries in cursor.description and the compiled object. + This is also the most performant case as we disregard extracting / + decoding the column names present in cursor.description since we + already have the desired name we generated in the compiled SQL + construct. + + The next common case is that of the completely raw string SQL, + such as passed to connection.execute(). In this case we have no + compiled construct to work with, so we extract and decode the + names from cursor.description and index those as the primary + result row target keys. + + The remaining fairly common case is that of the textual SQL + that includes at least partial column information; this is when + we use a :class:`.TextAsFrom` construct. This contruct may have + unordered or ordered column information. In the ordered case, we + merge the cursor.description and the compiled construct's information + positionally, and warn if there are additional description names + present, however we still decode the names in cursor.description + as we don't have a guarantee that the names in the columns match + on these. In the unordered case, we match names in cursor.description + to that of the compiled construct based on name matching. + In both of these cases, the cursor.description names and the column + expression objects and names are indexed as result row target keys. + + The final case is much less common, where we have a compiled + non-textual SQL expression construct, but the number of columns + in cursor.description doesn't match what's in the compiled + construct. We make the guess here that there might be textual + column expressions in the compiled construct that themselves include + a comma in them causing them to split. We do the same name-matching + as with textual non-ordered columns. + + The name-matched system of merging is the same as that used by + SQLAlchemy for all cases up through te 0.9 series. Positional + matching for compiled SQL expressions was introduced in 1.0 as a + major performance feature, and positional matching for textual + :class:`.TextAsFrom` objects in 1.1. As name matching is no longer + a common case, it was acceptable to factor it into smaller generator- + oriented methods that are easier to understand, but incur slightly + more performance overhead. + + """ + + case_sensitive = context.dialect.case_sensitive + + if num_ctx_cols and \ + cols_are_ordered and \ + not textual_ordered and \ + num_ctx_cols == len(cursor_description): + self.keys = [elem[0] for elem in result_columns] + # pure positional 1-1 case; doesn't need to read + # the names from cursor.description + return [ + ( + idx, + key, + name.lower() if not case_sensitive else name, + context.get_result_processor( + type_, key, cursor_description[idx][1] + ), + obj, + None + ) for idx, (key, name, obj, type_) + in enumerate(result_columns) + ] else: - self._keymap.update([ - (elem[2], (elem[3], elem[4], elem[0])) - for elem in raw - ]) - # update keymap with "translated" names (sqlite-only thing) + # name-based or text-positional cases, where we need + # to read cursor.description names + if textual_ordered: + # textual positional case + raw_iterator = self._merge_textual_cols_by_position( + context, cursor_description, result_columns) + elif num_ctx_cols: + # compiled SQL with a mismatch of description cols + # vs. compiled cols, or textual w/ unordered columns + raw_iterator = self._merge_cols_by_name( + context, cursor_description, result_columns) + else: + # no compiled SQL, just a raw string + raw_iterator = self._merge_cols_by_none( + context, cursor_description) + + return [ + ( + idx, colname, colname, + context.get_result_processor( + mapped_type, colname, coltype), + obj, untranslated) + + for idx, colname, mapped_type, coltype, obj, untranslated + in raw_iterator + ] + + def _colnames_from_description(self, context, cursor_description): + """Extract column names and data types from a cursor.description. + + Applies unicode decoding, column translation, "normalization", + and case sensitivity rules to the names based on the dialect. + + """ + + dialect = context.dialect + case_sensitive = dialect.case_sensitive + translate_colname = context._translate_colname + description_decoder = dialect._description_decoder \ + if dialect.description_encoding else None + normalize_name = dialect.normalize_name \ + if dialect.requires_name_normalize else None + untranslated = None + + self.keys = [] + + for idx, rec in enumerate(cursor_description): + colname = rec[0] + coltype = rec[1] + + if description_decoder: + colname = description_decoder(colname) + if translate_colname: - self._keymap.update([ - (elem[5], self._keymap[elem[2]]) - for elem in raw if elem[5] - ]) + colname, untranslated = translate_colname(colname) + + if normalize_name: + colname = normalize_name(colname) + + self.keys.append(colname) + if not case_sensitive: + colname = colname.lower() + + yield idx, colname, untranslated, coltype + + def _merge_textual_cols_by_position( + self, context, cursor_description, result_columns): + dialect = context.dialect + typemap = dialect.dbapi_type_map + num_ctx_cols = len(result_columns) if result_columns else None + + if num_ctx_cols > len(cursor_description): + util.warn( + "Number of columns in textual SQL (%d) is " + "smaller than number of columns requested (%d)" % ( + num_ctx_cols, len(cursor_description) + )) + + seen = set() + for idx, colname, untranslated, coltype in \ + self._colnames_from_description(context, cursor_description): + if idx < num_ctx_cols: + ctx_rec = result_columns[idx] + obj = ctx_rec[2] + mapped_type = ctx_rec[3] + if obj[0] in seen: + raise exc.InvalidRequestError( + "Duplicate column expression requested " + "in textual SQL: %r" % obj[0]) + seen.add(obj[0]) + else: + mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) + obj = None + + yield idx, colname, mapped_type, coltype, obj, untranslated + + def _merge_cols_by_name(self, context, cursor_description, result_columns): + dialect = context.dialect + typemap = dialect.dbapi_type_map + case_sensitive = dialect.case_sensitive + result_map = self._create_result_map(result_columns, case_sensitive) + + self.matched_on_name = True + for idx, colname, untranslated, coltype in \ + self._colnames_from_description(context, cursor_description): + try: + ctx_rec = result_map[colname] + except KeyError: + mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) + obj = None + else: + obj = ctx_rec[1] + mapped_type = ctx_rec[2] + yield idx, colname, mapped_type, coltype, obj, untranslated + + def _merge_cols_by_none(self, context, cursor_description): + dialect = context.dialect + typemap = dialect.dbapi_type_map + for idx, colname, untranslated, coltype in \ + self._colnames_from_description(context, cursor_description): + mapped_type = typemap.get(coltype, sqltypes.NULLTYPE) + yield idx, colname, mapped_type, coltype, None, untranslated @classmethod def _create_result_map(cls, result_columns, case_sensitive=True): @@ -347,22 +503,6 @@ class ResultMetaData(object): d[key] = rec return d - @util.pending_deprecation("0.8", "sqlite dialect uses " - "_translate_colname() now") - def _set_keymap_synonym(self, name, origname): - """Set a synonym for the given name. - - Some dialects (SQLite at the moment) may use this to - adjust the column names that are significant within a - row. - - """ - rec = (processor, obj, i) = self._keymap[origname if - self.case_sensitive - else origname.lower()] - if self._keymap.setdefault(name, rec) is not rec: - self._keymap[name] = (processor, obj, None) - def _key_fallback(self, key, raiseerr=True): map = self._keymap result = None @@ -427,8 +567,8 @@ class ResultMetaData(object): if index is None: raise exc.InvalidRequestError( - "Ambiguous column name '%s' in result set! " - "try 'use_labels' option on select statement." % key) + "Ambiguous column name '%s' in " + "result set column descriptions" % obj) return operator.itemgetter(index) @@ -441,6 +581,7 @@ class ResultMetaData(object): ), 'keys': self.keys, "case_sensitive": self.case_sensitive, + "matched_on_name": self.matched_on_name } def __setstate__(self, state): @@ -454,7 +595,7 @@ class ResultMetaData(object): keymap[key] = (None, None, index) self.keys = state['keys'] self.case_sensitive = state['case_sensitive'] - self._echo = False + self.matched_on_name = state['matched_on_name'] class ResultProxy(object): @@ -511,20 +652,20 @@ class ResultProxy(object): return has_key(key) def _init_metadata(self): - metadata = self._cursor_description() - if metadata is not None: + cursor_description = self._cursor_description() + if cursor_description is not None: if self.context.compiled and \ 'compiled_cache' in self.context.execution_options: if self.context.compiled._cached_metadata: self._metadata = self.context.compiled._cached_metadata else: self._metadata = self.context.compiled._cached_metadata = \ - ResultMetaData(self, metadata) + ResultMetaData(self, cursor_description) else: - self._metadata = ResultMetaData(self, metadata) + self._metadata = ResultMetaData(self, cursor_description) if self._echo: self.context.engine.logger.debug( - "Col %r", tuple(x[0] for x in metadata)) + "Col %r", tuple(x[0] for x in cursor_description)) def keys(self): """Return the current set of string keys for rows.""" -- cgit v1.2.1 From 39837686b068a6e7016169f31a96a058546e4bdd Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 19 Jan 2016 16:47:16 -0500 Subject: - calling str() on a core sql construct has been made more "friendly", when the construct contains non-standard sql elements such as returning, array index operations, or dialect-specific or custom datatypes. a string is now returned in these cases rendering an approximation of the construct (typically the postgresql-style version of it) rather than raising an error. fixes #3631 - add within_group to top-level imports - add eq_ignore_whitespace to sqlalchemy.testing imports --- lib/sqlalchemy/engine/default.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 3e5f339b1..9f845e79d 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -474,6 +474,23 @@ class DefaultDialect(interfaces.Dialect): self.set_isolation_level(dbapi_conn, self.default_isolation_level) +class StrCompileDialect(DefaultDialect): + + statement_compiler = compiler.StrSQLCompiler + ddl_compiler = compiler.DDLCompiler + type_compiler = compiler.StrSQLTypeCompiler + preparer = compiler.IdentifierPreparer + + supports_sequences = True + sequences_optional = True + preexecute_autoincrement_sequences = False + implicit_returning = False + + supports_native_boolean = True + + supports_simple_order_by_label = True + + class DefaultExecutionContext(interfaces.ExecutionContext): isinsert = False isupdate = False -- cgit v1.2.1 From de0d144a395c31eb74084177df95a4858b830f88 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 27 Jan 2016 11:35:43 -0500 Subject: - dont set up integer index in keymap if we're on cexts --- lib/sqlalchemy/engine/result.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index cc4ac74cd..39f4fc50c 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -35,7 +35,10 @@ except ImportError: try: from sqlalchemy.cresultproxy import BaseRowProxy + _baserowproxy_usecext = True except ImportError: + _baserowproxy_usecext = False + class BaseRowProxy(object): __slots__ = ('_parent', '_row', '_processors', '_keymap') @@ -210,11 +213,13 @@ class ResultMetaData(object): context, cursor_description, result_columns, num_ctx_cols, cols_are_ordered, textual_ordered) - # keymap indexes by integer index... - self._keymap = dict([ - (elem[0], (elem[3], elem[4], elem[0])) - for elem in raw - ]) + self._keymap = {} + if not _baserowproxy_usecext: + # keymap indexes by integer index... + self._keymap.update([ + (elem[0], (elem[3], elem[4], elem[0])) + for elem in raw + ]) # processors in key order for certain per-row # views like __iter__ and slices -- cgit v1.2.1 From 8aa95fa2cd0e15b77af3e5976436adc6ed6123b9 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 27 Jan 2016 12:41:01 -0500 Subject: Revert "- dont set up integer index in keymap if we're on cexts" This reverts commit de0d144a395c31eb74084177df95a4858b830f88. Apparently the test suite is not using the cextensions correctly at the moment. --- lib/sqlalchemy/engine/result.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 39f4fc50c..cc4ac74cd 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -35,10 +35,7 @@ except ImportError: try: from sqlalchemy.cresultproxy import BaseRowProxy - _baserowproxy_usecext = True except ImportError: - _baserowproxy_usecext = False - class BaseRowProxy(object): __slots__ = ('_parent', '_row', '_processors', '_keymap') @@ -213,13 +210,11 @@ class ResultMetaData(object): context, cursor_description, result_columns, num_ctx_cols, cols_are_ordered, textual_ordered) - self._keymap = {} - if not _baserowproxy_usecext: - # keymap indexes by integer index... - self._keymap.update([ - (elem[0], (elem[3], elem[4], elem[0])) - for elem in raw - ]) + # keymap indexes by integer index... + self._keymap = dict([ + (elem[0], (elem[3], elem[4], elem[0])) + for elem in raw + ]) # processors in key order for certain per-row # views like __iter__ and slices -- cgit v1.2.1 From 516a442f233d90eb7b8bb844e2dea7865bb21f66 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 27 Jan 2016 14:49:40 -0500 Subject: - reinstate "dont set up integer index in keymap if we're on cexts", and this time also fix the cext itself to properly handle int vs. long on py2k --- lib/sqlalchemy/engine/result.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index cc4ac74cd..39f4fc50c 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -35,7 +35,10 @@ except ImportError: try: from sqlalchemy.cresultproxy import BaseRowProxy + _baserowproxy_usecext = True except ImportError: + _baserowproxy_usecext = False + class BaseRowProxy(object): __slots__ = ('_parent', '_row', '_processors', '_keymap') @@ -210,11 +213,13 @@ class ResultMetaData(object): context, cursor_description, result_columns, num_ctx_cols, cols_are_ordered, textual_ordered) - # keymap indexes by integer index... - self._keymap = dict([ - (elem[0], (elem[3], elem[4], elem[0])) - for elem in raw - ]) + self._keymap = {} + if not _baserowproxy_usecext: + # keymap indexes by integer index... + self._keymap.update([ + (elem[0], (elem[3], elem[4], elem[0])) + for elem in raw + ]) # processors in key order for certain per-row # views like __iter__ and slices -- cgit v1.2.1 From 859379e2fcc4506d036700ba1eca4c0ae526a8ee Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 29 Jan 2016 11:20:22 -0500 Subject: - happy new year --- lib/sqlalchemy/engine/__init__.py | 2 +- lib/sqlalchemy/engine/base.py | 2 +- lib/sqlalchemy/engine/default.py | 2 +- lib/sqlalchemy/engine/interfaces.py | 2 +- lib/sqlalchemy/engine/reflection.py | 2 +- lib/sqlalchemy/engine/result.py | 2 +- lib/sqlalchemy/engine/strategies.py | 2 +- lib/sqlalchemy/engine/threadlocal.py | 2 +- lib/sqlalchemy/engine/url.py | 2 +- lib/sqlalchemy/engine/util.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 02c35d6a9..adca6694e 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -1,5 +1,5 @@ # engine/__init__.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 0b928566d..ef34eef01 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1,5 +1,5 @@ # engine/base.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 9f845e79d..3ed2d5ee8 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -1,5 +1,5 @@ # engine/default.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index c84823d1e..26731f9a5 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -1,5 +1,5 @@ # engine/interfaces.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 6880660ce..eaa5e2e48 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -1,5 +1,5 @@ # engine/reflection.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 39f4fc50c..3305c4ce5 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -1,5 +1,5 @@ # engine/result.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index d8e2d4764..82800a918 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -1,5 +1,5 @@ # engine/strategies.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/threadlocal.py b/lib/sqlalchemy/engine/threadlocal.py index 0d6e1c0f1..505d1fadd 100644 --- a/lib/sqlalchemy/engine/threadlocal.py +++ b/lib/sqlalchemy/engine/threadlocal.py @@ -1,5 +1,5 @@ # engine/threadlocal.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index 9a955948a..cdb3bb7bb 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -1,5 +1,5 @@ # engine/url.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under diff --git a/lib/sqlalchemy/engine/util.py b/lib/sqlalchemy/engine/util.py index 3734c9960..d28d87098 100644 --- a/lib/sqlalchemy/engine/util.py +++ b/lib/sqlalchemy/engine/util.py @@ -1,5 +1,5 @@ # engine/util.py -# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors # # # This module is part of SQLAlchemy and is released under -- cgit v1.2.1 From 591e0cf08a798fb16e0ee9b56df5c3141aa48959 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 17 Feb 2016 13:31:29 -0500 Subject: - All string formatting of bound parameter sets and result rows for logging, exception, and ``repr()`` purposes now truncate very large scalar values within each collection, including an "N characters truncated" notation, similar to how the display for large multiple-parameter sets are themselves truncated. fixes #2837 --- lib/sqlalchemy/engine/result.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 3305c4ce5..c069fcedf 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -10,7 +10,7 @@ and :class:`.RowProxy.""" from .. import exc, util -from ..sql import expression, sqltypes +from ..sql import expression, sqltypes, util as sql_util import collections import operator @@ -153,7 +153,7 @@ class RowProxy(BaseRowProxy): return self._op(other, operator.ne) def __repr__(self): - return repr(tuple(self)) + return repr(sql_util._repr_row(self)) def has_key(self, key): """Return True if this RowProxy contains the given key.""" @@ -1080,7 +1080,7 @@ class ResultProxy(object): log = self.context.engine.logger.debug l = [] for row in rows: - log("Row %r", row) + log("Row %r", sql_util._repr_row(row)) l.append(process_row(metadata, row, processors, keymap)) return l else: -- cgit v1.2.1 From 8ad968f33100baeb3b13c7e0b724b6b79ab4277f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 20 Feb 2016 20:22:38 -0500 Subject: - reworked the way the "select_wraps_for" expression is handled within visit_select(); this attribute was added in the 1.0 series to accommodate the subquery wrapping behavior of SQL Server and Oracle while also working with positional column targeting and no longer relying upon "key fallback" in order to target columns in such a statement. The IBM DB2 third-party dialect also has this use case, but its implementation is using regular expressions to rewrite the textual SELECT only and does not make use of a "wrapped" select at this time. The logic no longer attempts to reconcile proxy set collections as this was not deterministic, and instead assumes that the select() and the wrapper select() match their columns postionally, at least for the column positions they have in common, so it is now very simple and safe. fixes #3657. - as a side effect of #3657 it was also revealed that the strategy of calling upon a ResultProxy._getter was not correctly calling into NoSuchColumnError when an expected column was not present, and instead returned None up to loading.instances() to produce NoneType failures; added a raiseerr argument to _getter() which is called when we aren't expecting None, fixes #3658. --- lib/sqlalchemy/engine/result.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/sqlalchemy/engine') diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index c069fcedf..f09b0b40b 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -561,11 +561,11 @@ class ResultMetaData(object): else: return self._key_fallback(key, False) is not None - def _getter(self, key): + def _getter(self, key, raiseerr=True): if key in self._keymap: processor, obj, index = self._keymap[key] else: - ret = self._key_fallback(key, False) + ret = self._key_fallback(key, raiseerr) if ret is None: return None processor, obj, index = ret @@ -640,13 +640,13 @@ class ResultProxy(object): context.engine._should_log_debug() self._init_metadata() - def _getter(self, key): + def _getter(self, key, raiseerr=True): try: getter = self._metadata._getter except AttributeError: return self._non_result(None) else: - return getter(key) + return getter(key, raiseerr) def _has_key(self, key): try: -- cgit v1.2.1