From 2e44749b76af4e9e1a2fd6e52dd329dc1e980216 Mon Sep 17 00:00:00 2001 From: Tony Locke Date: Sat, 26 Jul 2014 18:56:56 +0100 Subject: Remove spurious print statements in pg8000 dialect --- lib/sqlalchemy/dialects/postgresql/pg8000.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index 68da5b6d7..2793b048d 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -172,11 +172,9 @@ class PGDialect_pg8000(PGDialect): ) def do_begin_twophase(self, connection, xid): - print("begin twophase", xid) connection.connection.tpc_begin((0, xid, '')) def do_prepare_twophase(self, connection, xid): - print("prepare twophase", xid) connection.connection.tpc_prepare() def do_rollback_twophase( -- cgit v1.2.1 From 0dbe9d9aaf22d69e44c486472ff3b412a96cf216 Mon Sep 17 00:00:00 2001 From: Tony Locke Date: Sat, 2 Aug 2014 16:19:46 +0100 Subject: pg8000 now supports sane_multi_rowcount From pg8000-1.9.14 sane_multi_rowcount is supported so this commit updates the dialect accordingly. --- lib/sqlalchemy/dialects/postgresql/pg8000.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index 2793b048d..909b41b82 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -119,7 +119,7 @@ class PGDialect_pg8000(PGDialect): supports_unicode_binds = True default_paramstyle = 'format' - supports_sane_multi_rowcount = False + supports_sane_multi_rowcount = True execution_ctx_cls = PGExecutionContext_pg8000 statement_compiler = PGCompiler_pg8000 preparer = PGIdentifierPreparer_pg8000 -- cgit v1.2.1 From 2645c8427729733fcd3db044abe7901412890214 Mon Sep 17 00:00:00 2001 From: Matt Chisholm Date: Sun, 27 Jul 2014 12:15:51 +0200 Subject: add update() support to MutableDict --- lib/sqlalchemy/ext/mutable.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 7469bcbda..3ef2f979d 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -621,6 +621,10 @@ class MutableDict(Mutable, dict): dict.__delitem__(self, key) self.changed() + def update(self, *a, **kw): + dict.update(self, *a, **kw) + self.changed() + def clear(self): dict.clear(self) self.changed() -- cgit v1.2.1 From 88f7ec6a0efe68305d5d1ee429565c1778ec6a87 Mon Sep 17 00:00:00 2001 From: Matt Chisholm Date: Sun, 27 Jul 2014 12:15:36 +0200 Subject: fix MutableDict.coerce If a class inherited from MutableDict (say, for instance, to add an update() method), coerce() would give back an instance of MutableDict instead of an instance of the derived class. --- lib/sqlalchemy/ext/mutable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 7469bcbda..1a4568f23 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -627,10 +627,10 @@ class MutableDict(Mutable, dict): @classmethod def coerce(cls, key, value): - """Convert plain dictionary to MutableDict.""" - if not isinstance(value, MutableDict): + """Convert plain dictionary to instance of this class.""" + if not isinstance(value, cls): if isinstance(value, dict): - return MutableDict(value) + return cls(value) return Mutable.coerce(key, value) else: return value -- cgit v1.2.1 From 4a4cccfee5a2eb78380e56eb9476e91658656676 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 14 Aug 2014 14:40:28 -0400 Subject: - Removing (or adding) an event listener at the same time that the event is being run itself, either from inside the listener or from a concurrent thread, now raises a RuntimeError, as the collection used is now an instance of ``colletions.deque()`` and does not support changes while being iterated. Previously, a plain Python list was used where removal from inside the event itself would produce silent failures. fixes #3163 --- lib/sqlalchemy/event/api.py | 54 ++++++++++++++++++++++++++++++++++++++++ lib/sqlalchemy/event/attr.py | 15 +++++------ lib/sqlalchemy/event/registry.py | 2 +- 3 files changed, 63 insertions(+), 8 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/event/api.py b/lib/sqlalchemy/event/api.py index 270e95c9c..b3d79bcf4 100644 --- a/lib/sqlalchemy/event/api.py +++ b/lib/sqlalchemy/event/api.py @@ -58,6 +58,32 @@ def listen(target, identifier, fn, *args, **kw): .. versionadded:: 0.9.4 Added ``once=True`` to :func:`.event.listen` and :func:`.event.listens_for`. + .. note:: + + The :func:`.listen` function cannot be called at the same time + that the target event is being run. This has implications + for thread safety, and also means an event cannot be added + from inside the listener function for itself. The list of + events to be run are present inside of a mutable collection + that can't be changed during iteration. + + Event registration and removal is not intended to be a "high + velocity" operation; it is a configurational operation. For + systems that need to quickly associate and deassociate with + events at high scale, use a mutable structure that is handled + from inside of a single listener. + + .. versionchanged:: 1.0.0 - a ``collections.deque()`` object is now + used as the container for the list of events, which explicitly + disallows collection mutation while the collection is being + iterated. + + .. seealso:: + + :func:`.listens_for` + + :func:`.remove` + """ _event_key(target, identifier, fn).listen(*args, **kw) @@ -89,6 +115,10 @@ def listens_for(target, identifier, *args, **kw): .. versionadded:: 0.9.4 Added ``once=True`` to :func:`.event.listen` and :func:`.event.listens_for`. + .. seealso:: + + :func:`.listen` - general description of event listening + """ def decorate(fn): listen(target, identifier, fn, *args, **kw) @@ -120,6 +150,30 @@ def remove(target, identifier, fn): .. versionadded:: 0.9.0 + .. note:: + + The :func:`.remove` function cannot be called at the same time + that the target event is being run. This has implications + for thread safety, and also means an event cannot be removed + from inside the listener function for itself. The list of + events to be run are present inside of a mutable collection + that can't be changed during iteration. + + Event registration and removal is not intended to be a "high + velocity" operation; it is a configurational operation. For + systems that need to quickly associate and deassociate with + events at high scale, use a mutable structure that is handled + from inside of a single listener. + + .. versionchanged:: 1.0.0 - a ``collections.deque()`` object is now + used as the container for the list of events, which explicitly + disallows collection mutation while the collection is being + iterated. + + .. seealso:: + + :func:`.listen` + """ _event_key(target, identifier, fn).remove() diff --git a/lib/sqlalchemy/event/attr.py b/lib/sqlalchemy/event/attr.py index 7641b595a..dba1063cf 100644 --- a/lib/sqlalchemy/event/attr.py +++ b/lib/sqlalchemy/event/attr.py @@ -37,6 +37,7 @@ from . import registry from . import legacy from itertools import chain import weakref +import collections class RefCollection(object): @@ -96,8 +97,8 @@ class _DispatchDescriptor(RefCollection): self.update_subclass(cls) else: if cls not in self._clslevel: - self._clslevel[cls] = [] - self._clslevel[cls].insert(0, event_key._listen_fn) + self._clslevel[cls] = collections.deque() + self._clslevel[cls].appendleft(event_key._listen_fn) registry._stored_in_collection(event_key, self) def append(self, event_key, propagate): @@ -113,13 +114,13 @@ class _DispatchDescriptor(RefCollection): self.update_subclass(cls) else: if cls not in self._clslevel: - self._clslevel[cls] = [] + self._clslevel[cls] = collections.deque() self._clslevel[cls].append(event_key._listen_fn) registry._stored_in_collection(event_key, self) def update_subclass(self, target): if target not in self._clslevel: - self._clslevel[target] = [] + self._clslevel[target] = collections.deque() clslevel = self._clslevel[target] for cls in target.__mro__[1:]: if cls in self._clslevel: @@ -145,7 +146,7 @@ class _DispatchDescriptor(RefCollection): to_clear = set() for dispatcher in self._clslevel.values(): to_clear.update(dispatcher) - dispatcher[:] = [] + dispatcher.clear() registry._clear(self, to_clear) def for_modify(self, obj): @@ -287,7 +288,7 @@ class _ListenerCollection(RefCollection, _CompoundListener): self.parent_listeners = parent._clslevel[target_cls] self.parent = parent self.name = parent.__name__ - self.listeners = [] + self.listeners = collections.deque() self.propagate = set() def for_modify(self, obj): @@ -337,7 +338,7 @@ class _ListenerCollection(RefCollection, _CompoundListener): def clear(self): registry._clear(self, self.listeners) self.propagate.clear() - self.listeners[:] = [] + self.listeners.clear() class _JoinedDispatchDescriptor(object): diff --git a/lib/sqlalchemy/event/registry.py b/lib/sqlalchemy/event/registry.py index a34de3cd7..ba2f671a3 100644 --- a/lib/sqlalchemy/event/registry.py +++ b/lib/sqlalchemy/event/registry.py @@ -243,4 +243,4 @@ class _EventKey(object): def prepend_to_list(self, owner, list_): _stored_in_collection(self, owner) - list_.insert(0, self._listen_fn) + list_.appendleft(self._listen_fn) -- cgit v1.2.1 From 6a21f9e328361d5185fd616e7992a183030f9a10 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 14 Aug 2014 20:00:35 -0400 Subject: - The string keys that are used to determine the columns impacted for an INSERT or UPDATE are now sorted when they contribute towards the "compiled cache" cache key. These keys were previously not deterministically ordered, meaning the same statement could be cached multiple times on equivalent keys, costing both in terms of memory as well as performance. fixes #3165 --- lib/sqlalchemy/engine/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 2dc4d43f2..65753b6dc 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -805,7 +805,7 @@ class Connection(Connectable): dialect = self.dialect if 'compiled_cache' in self._execution_options: - key = dialect, elem, tuple(keys), len(distilled_params) > 1 + 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: -- cgit v1.2.1 From 253523c57f76c515ade82e4db30cff2536fb2a92 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 14 Aug 2014 20:01:59 -0400 Subject: pep8 --- lib/sqlalchemy/orm/persistence.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 295d4a3d0..17ce2e624 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -309,10 +309,10 @@ def _collect_update_commands(base_mapper, uowtransaction, if col is mapper.version_id_col: params[col._label] = \ mapper._get_committed_state_attr_by_column( - row_switch or state, - row_switch and row_switch.dict - or state_dict, - col) + row_switch or state, + row_switch and row_switch.dict + or state_dict, + col) prop = mapper._columntoproperty[col] history = state.manager[prop.key].impl.get_history( @@ -417,8 +417,8 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, if col in pks: params[col._label] = \ mapper._get_state_attr_by_column( - state, - state_dict, col) + state, + state_dict, col) elif col in post_update_cols: prop = mapper._columntoproperty[col] @@ -453,7 +453,7 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, params[col.key] = \ value = \ mapper._get_committed_state_attr_by_column( - state, state_dict, col) + state, state_dict, col) if value is None: raise orm_exc.FlushError( "Can't delete from table " @@ -464,8 +464,8 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, table.c.contains_column(mapper.version_id_col): params[mapper.version_id_col.key] = \ mapper._get_committed_state_attr_by_column( - state, state_dict, - mapper.version_id_col) + state, state_dict, + mapper.version_id_col) return delete -- cgit v1.2.1 From bc509dd50d7b65e35412e2be67bd37a6c19e7119 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 14 Aug 2014 20:47:49 -0400 Subject: - UPDATE statements can now be batched within an ORM flush into more performant executemany() call, similarly to how INSERT statements can be batched; this will be invoked within flush to the degree that subsequent UPDATE statements for the same mapping and table involve the identical columns within the VALUES clause, as well as that no VALUES-level SQL expressions are embedded. - some other inlinings within persistence.py --- lib/sqlalchemy/orm/persistence.py | 103 +++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 41 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 17ce2e624..9d39c39b0 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -248,9 +248,10 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, has_all_pks = True has_all_defaults = True + has_version_id_generator = mapper.version_id_generator is not False \ + and mapper.version_id_col is not None for col in mapper._cols_by_table[table]: - if col is mapper.version_id_col and \ - mapper.version_id_generator is not False: + if has_version_id_generator and col is mapper.version_id_col: val = mapper.version_id_generator(None) params[col.key] = val else: @@ -305,6 +306,7 @@ def _collect_update_commands(base_mapper, uowtransaction, value_params = {} hasdata = hasnull = False + for col in mapper._cols_by_table[table]: if col is mapper.version_id_col: params[col._label] = \ @@ -341,6 +343,7 @@ def _collect_update_commands(base_mapper, uowtransaction, prop = mapper._columntoproperty[col] history = state.manager[prop.key].impl.get_history( state, state_dict, + attributes.PASSIVE_OFF if col in pks else attributes.PASSIVE_NO_INITIALIZE) if history.added: if isinstance(history.added[0], @@ -381,8 +384,7 @@ def _collect_update_commands(base_mapper, uowtransaction, else: hasdata = True elif col in pks: - value = state.manager[prop.key].impl.get( - state, state_dict) + value = history.unchanged[0] if value is None: hasnull = True params[col._label] = value @@ -500,41 +502,63 @@ def _emit_update_statements(base_mapper, uowtransaction, statement = base_mapper._memo(('update', table), update_stmt) - rows = 0 - for state, state_dict, params, mapper, \ - connection, value_params in update: - - if value_params: - c = connection.execute( - statement.values(value_params), - params) + for (connection, paramkeys, hasvalue), \ + records in groupby( + update, + lambda rec: ( + rec[4], + tuple(sorted(rec[2])), + bool(rec[5])) + ): + + rows = 0 + records = list(records) + if hasvalue: + for state, state_dict, params, mapper, \ + connection, value_params in records: + c = connection.execute( + statement.values(value_params), + params) + _postfetch( + mapper, + uowtransaction, + table, + state, + state_dict, + c, + c.context.compiled_parameters[0], + value_params) + rows += c.rowcount else: + multiparams = [rec[2] for rec in records] c = cached_connections[connection].\ - execute(statement, params) - - _postfetch( - mapper, - uowtransaction, - table, - state, - state_dict, - c, - c.context.compiled_parameters[0], - value_params) - rows += c.rowcount - - if connection.dialect.supports_sane_rowcount: - if rows != len(update): - raise orm_exc.StaleDataError( - "UPDATE statement on table '%s' expected to " - "update %d row(s); %d were matched." % - (table.description, len(update), rows)) - - elif needs_version_id: - util.warn("Dialect %s does not support updated rowcount " - "- versioning cannot be verified." % - c.dialect.dialect_description, - stacklevel=12) + execute(statement, multiparams) + + rows += c.rowcount + for state, state_dict, params, mapper, \ + connection, value_params in records: + _postfetch( + mapper, + uowtransaction, + table, + state, + state_dict, + c, + c.context.compiled_parameters[0], + value_params) + + if connection.dialect.supports_sane_rowcount: + if rows != len(records): + raise orm_exc.StaleDataError( + "UPDATE statement on table '%s' expected to " + "update %d row(s); %d were matched." % + (table.description, len(records), rows)) + + elif needs_version_id: + util.warn("Dialect %s does not support updated rowcount " + "- versioning cannot be verified." % + c.dialect.dialect_description, + stacklevel=12) def _emit_insert_statements(base_mapper, uowtransaction, @@ -833,15 +857,12 @@ def _connections_for_states(base_mapper, uowtransaction, states): connection_callable = \ uowtransaction.session.connection_callable else: - connection = None + connection = uowtransaction.transaction.connection(base_mapper) connection_callable = None for state in _sort_states(states): if connection_callable: connection = connection_callable(base_mapper, state.obj()) - elif not connection: - connection = uowtransaction.transaction.connection( - base_mapper) mapper = _state_mapper(state) -- cgit v1.2.1 From b952d892d690ec808829aede84769b2bf089f94d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 00:19:32 -0400 Subject: - modify how class state is tracked here as it seems like things are a little more crazy under xdist mode --- lib/sqlalchemy/testing/plugin/pytestplugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index fd0616327..f4c9efd55 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -115,7 +115,6 @@ def pytest_pycollect_makeitem(collector, name, obj): _current_class = None - def pytest_runtest_setup(item): # here we seem to get called only based on what we collected # in pytest_collection_modifyitems. So to do class-based stuff @@ -126,16 +125,18 @@ def pytest_runtest_setup(item): return # ... so we're doing a little dance here to figure it out... - if item.parent.parent is not _current_class: - + if _current_class is None: class_setup(item.parent.parent) _current_class = item.parent.parent # this is needed for the class-level, to ensure that the # teardown runs after the class is completed with its own # class-level teardown... - item.parent.parent.addfinalizer( - lambda: class_teardown(item.parent.parent)) + def finalize(): + global _current_class + class_teardown(item.parent.parent) + _current_class = None + item.parent.parent.addfinalizer(finalize) test_setup(item) -- cgit v1.2.1 From b0411e80df13d347104a60c512aeb18b6479bb12 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 00:19:57 -0400 Subject: - other test fixes --- lib/sqlalchemy/engine/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 65753b6dc..3728b59fd 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -799,7 +799,7 @@ class Connection(Connectable): if distilled_params: # note this is usually dict but we support RowProxy # as well; but dict.keys() as an iterator is OK - keys = distilled_params[0].keys() + keys = list(distilled_params[0].keys()) else: keys = [] -- cgit v1.2.1 From d768ec2c266ec462a8ff0b782516c494c451f2db Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 14:27:12 -0400 Subject: - don't add the parent attach event within _on_table_attach if we already have a table; this prevents reentrant calls and we aren't supporting columns/etc being moved around between different parents --- lib/sqlalchemy/sql/schema.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 8099dca75..c8e815d24 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -1269,7 +1269,8 @@ class Column(SchemaItem, ColumnClause): def _on_table_attach(self, fn): if self.table is not None: fn(self, self.table) - event.listen(self, 'after_parent_attach', fn) + else: + event.listen(self, 'after_parent_attach', fn) def copy(self, **kw): """Create a copy of this ``Column``, unitialized. -- cgit v1.2.1 From 961217aa923562c21a0113fae41d6841276e6ca5 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 14:38:33 -0400 Subject: - clean up provision and keep sqlite on memory DBs if thats what we start with --- lib/sqlalchemy/testing/plugin/provision.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/testing/plugin/provision.py b/lib/sqlalchemy/testing/plugin/provision.py index baec8a299..c6b9030f5 100644 --- a/lib/sqlalchemy/testing/plugin/provision.py +++ b/lib/sqlalchemy/testing/plugin/provision.py @@ -36,14 +36,8 @@ class register(object): def create_follower_db(follower_ident): for cfg in _configs_for_db_operation(): - url = cfg.db.url - backend = url.get_backend_name() _create_db(cfg, cfg.db, follower_ident) - new_url = sa_url.make_url(str(url)) - - new_url.database = follower_ident - def configure_follower(follower_ident): for cfg in config.Config.all_configs(): @@ -63,7 +57,6 @@ def setup_config(db_url, db_opts, options, file_config, follower_ident): def drop_follower_db(follower_ident): for cfg in _configs_for_db_operation(): - url = cfg.db.url _drop_db(cfg, cfg.db, follower_ident) @@ -110,9 +103,13 @@ def _follower_url_from_main(url, ident): return url -#@_follower_url_from_main.for_db("sqlite") -#def _sqlite_follower_url_from_main(url, ident): -# return sa_url.make_url("sqlite:///%s.db" % ident) +@_follower_url_from_main.for_db("sqlite") +def _sqlite_follower_url_from_main(url, ident): + url = sa_url.make_url(url) + if not url.database or url.database == ':memory:': + return url + else: + return sa_url.make_url("sqlite:///%s.db" % ident) @_create_db.for_db("postgresql") -- cgit v1.2.1 From 5a68f856daee59caf4c9da7d06880eada9d70302 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 14:57:29 -0400 Subject: - TIL that dict.keys() in py3K is not an iterator, it is an iterable view. So copy collections.OrderedDict and use MutableMapping to set up keys, items, values on our own OrderedDict. Conflicts: lib/sqlalchemy/engine/base.py --- lib/sqlalchemy/engine/base.py | 4 ++-- lib/sqlalchemy/util/_collections.py | 46 ++++--------------------------------- 2 files changed, 7 insertions(+), 43 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 3728b59fd..d2cc8890f 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -798,8 +798,8 @@ class Connection(Connectable): distilled_params = _distill_params(multiparams, params) if distilled_params: # note this is usually dict but we support RowProxy - # as well; but dict.keys() as an iterator is OK - keys = list(distilled_params[0].keys()) + # as well; but dict.keys() as an iterable is OK + keys = distilled_params[0].keys() else: keys = [] diff --git a/lib/sqlalchemy/util/_collections.py b/lib/sqlalchemy/util/_collections.py index 5236d0120..fa27897a1 100644 --- a/lib/sqlalchemy/util/_collections.py +++ b/lib/sqlalchemy/util/_collections.py @@ -13,6 +13,7 @@ import operator from .compat import threading, itertools_filterfalse from . import py2k import types +from collections import MutableMapping EMPTY_SET = frozenset() @@ -264,13 +265,11 @@ class OrderedDict(dict): def __iter__(self): return iter(self._list) - if py2k: - def values(self): - return [self[key] for key in self._list] - - def keys(self): - return self._list + keys = MutableMapping.keys + values = MutableMapping.values + items = MutableMapping.items + if py2k: def itervalues(self): return iter([self[key] for key in self._list]) @@ -280,41 +279,6 @@ class OrderedDict(dict): def iteritems(self): return iter(self.items()) - def items(self): - return [(key, self[key]) for key in self._list] - else: - def values(self): - # return (self[key] for key in self) - return (self[key] for key in self._list) - - def keys(self): - # return iter(self) - return iter(self._list) - - def items(self): - # return ((key, self[key]) for key in self) - return ((key, self[key]) for key in self._list) - - _debug_iter = False - if _debug_iter: - # normally disabled to reduce function call - # overhead - def __iter__(self): - len_ = len(self._list) - for item in self._list: - yield item - assert len_ == len(self._list), \ - "Dictionary changed size during iteration" - - def values(self): - return (self[key] for key in self) - - def keys(self): - return iter(self) - - def items(self): - return ((key, self[key]) for key in self) - def __setitem__(self, key, object): if key not in self: try: -- cgit v1.2.1 From 652a24f0303b9bb0e7a326b05709d7660793f90b Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 15:13:13 -0400 Subject: - The :class:`.IdentityMap` exposed from :class:`.Session.identity` now returns lists for ``items()`` and ``values()`` in Py3K. Early porting to Py3K here had these returning iterators, when they technically should be "iterable views"..for now, lists are OK. --- lib/sqlalchemy/orm/identity.py | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/identity.py b/lib/sqlalchemy/orm/identity.py index d9cdd791f..4425fc3a6 100644 --- a/lib/sqlalchemy/orm/identity.py +++ b/lib/sqlalchemy/orm/identity.py @@ -150,7 +150,7 @@ class WeakInstanceDict(IdentityMap): return default return o - def _items(self): + def items(self): values = self.all_states() result = [] for state in values: @@ -159,7 +159,7 @@ class WeakInstanceDict(IdentityMap): result.append((state.key, value)) return result - def _values(self): + def values(self): values = self.all_states() result = [] for state in values: @@ -169,9 +169,10 @@ class WeakInstanceDict(IdentityMap): return result + def __iter__(self): + return iter(self.keys()) + if util.py2k: - items = _items - values = _values def iteritems(self): return iter(self.items()) @@ -179,24 +180,8 @@ class WeakInstanceDict(IdentityMap): def itervalues(self): return iter(self.values()) - def __iter__(self): - return iter(self.keys()) - - else: - def items(self): - return iter(self._items()) - - def values(self): - return iter(self._values()) - - def __iter__(self): - return self.keys() - def all_states(self): - if util.py2k: - return self._dict.values() - else: - return list(self._dict.values()) + return self._dict.values() def discard(self, state): if state.key in self._dict: @@ -217,11 +202,8 @@ class StrongInstanceDict(IdentityMap): def iteritems(self): return self._dict.iteritems() - def __iter__(self): - return iter(self.keys()) - else: - def __iter__(self): - return self.keys() + def __iter__(self): + return iter(self.dict_) def __getitem__(self, key): return self._dict[key] -- cgit v1.2.1 From 239464a98a4b1a2d6e5e39d998911ec7a8fe3666 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 18:33:42 -0400 Subject: - port the _collect_insert_commands optimizations from ticket_3100 --- lib/sqlalchemy/orm/mapper.py | 35 +++++++++++++++++++ lib/sqlalchemy/orm/persistence.py | 71 +++++++++++++++++++-------------------- 2 files changed, 70 insertions(+), 36 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 06ec2bf14..fc15769cd 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1892,6 +1892,41 @@ class Mapper(InspectionAttr): """ + @_memoized_configured_property + def _col_to_propkey(self): + return dict( + ( + table, + [ + (col, self._columntoproperty[col].key) + for col in columns + ] + ) + for table, columns in self._cols_by_table.items() + ) + + @_memoized_configured_property + def _pk_keys_by_table(self): + return dict( + ( + table, + frozenset([col.key for col in pks]) + ) + for table, pks in self._pks_by_table.items() + ) + + @_memoized_configured_property + def _server_default_cols(self): + return dict( + ( + table, + frozenset([ + col for col in columns + if col.server_default is not None]) + ) + for table, columns in self._cols_by_table.items() + ) + @property def selectable(self): """The :func:`.select` construct this :class:`.Mapper` selects from diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 9d39c39b0..b7283e48e 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -18,7 +18,7 @@ import operator from itertools import groupby from .. import sql, util, exc as sa_exc, schema from . import attributes, sync, exc as orm_exc, evaluator -from .base import _state_mapper, state_str, _attr_as_key +from .base import state_str, _attr_as_key from ..sql import expression from . import loading @@ -141,6 +141,7 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): states): has_identity = bool(state.key) + instance_key = state.key or mapper._identity_key_from_state(state) row_switch = None @@ -183,12 +184,12 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): if not has_identity and not row_switch: states_to_insert.append( (state, dict_, mapper, connection, - has_identity, instance_key, row_switch) + has_identity, row_switch) ) else: states_to_update.append( (state, dict_, mapper, connection, - has_identity, instance_key, row_switch) + has_identity, row_switch) ) return states_to_insert, states_to_update @@ -237,43 +238,41 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, """ insert = [] for state, state_dict, mapper, connection, has_identity, \ - instance_key, row_switch in states_to_insert: + row_switch in states_to_insert: + if table not in mapper._pks_by_table: continue - pks = mapper._pks_by_table[table] - params = {} value_params = {} - - has_all_pks = True - has_all_defaults = True - has_version_id_generator = mapper.version_id_generator is not False \ - and mapper.version_id_col is not None - for col in mapper._cols_by_table[table]: - if has_version_id_generator and col is mapper.version_id_col: - val = mapper.version_id_generator(None) - params[col.key] = val + for col, propkey in mapper._col_to_propkey[table]: + if propkey in state_dict: + value = state_dict[propkey] + if isinstance(value, sql.ClauseElement): + value_params[col.key] = value + elif value is not None or ( + not col.primary_key and + not col.server_default and + not col.default): + params[col.key] = value else: - # pull straight from the dict for - # pending objects - prop = mapper._columntoproperty[col] - value = state_dict.get(prop.key, None) + if not col.server_default \ + and not col.default and not col.primary_key: + params[col.key] = None - if value is None: - if col in pks: - has_all_pks = False - elif col.default is None and \ - col.server_default is None: - params[col.key] = value - elif col.server_default is not None and \ - mapper.base_mapper.eager_defaults: - has_all_defaults = False + has_all_pks = mapper._pk_keys_by_table[table].issubset(params) - elif isinstance(value, sql.ClauseElement): - value_params[col] = value - else: - params[col.key] = value + if base_mapper.eager_defaults: + has_all_defaults = mapper._server_default_cols[table].\ + issubset(params) + else: + has_all_defaults = True + + if mapper.version_id_generator is not False \ + and mapper.version_id_col is not None and \ + mapper.version_id_col in mapper._cols_by_table[table]: + params[mapper.version_id_col.key] = \ + mapper.version_id_generator(None) insert.append((state, state_dict, params, mapper, connection, value_params, has_all_pks, @@ -296,7 +295,7 @@ def _collect_update_commands(base_mapper, uowtransaction, update = [] for state, state_dict, mapper, connection, has_identity, \ - instance_key, row_switch in states_to_update: + row_switch in states_to_update: if table not in mapper._pks_by_table: continue @@ -571,7 +570,7 @@ def _emit_insert_statements(base_mapper, uowtransaction, for (connection, pkeys, hasvalue, has_all_pks, has_all_defaults), \ records in groupby(insert, lambda rec: (rec[4], - list(rec[2].keys()), + tuple(sorted(rec[2].keys())), bool(rec[5]), rec[6], rec[7]) ): @@ -762,7 +761,7 @@ def _finalize_insert_update_commands(base_mapper, uowtransaction, """ for state, state_dict, mapper, connection, has_identity, \ - instance_key, row_switch in states_to_insert + \ + row_switch in states_to_insert + \ states_to_update: if mapper._readonly_props: @@ -864,7 +863,7 @@ def _connections_for_states(base_mapper, uowtransaction, states): if connection_callable: connection = connection_callable(base_mapper, state.obj()) - mapper = _state_mapper(state) + mapper = state.manager.mapper yield state, state.dict, mapper, connection -- cgit v1.2.1 From ca69e4560333a1a7e3a2dafd746be851cc89228c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 15 Aug 2014 18:39:26 -0400 Subject: - mutablemapping adds compiler overhead, so screw it --- lib/sqlalchemy/util/_collections.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/util/_collections.py b/lib/sqlalchemy/util/_collections.py index fa27897a1..0904d454e 100644 --- a/lib/sqlalchemy/util/_collections.py +++ b/lib/sqlalchemy/util/_collections.py @@ -13,7 +13,6 @@ import operator from .compat import threading, itertools_filterfalse from . import py2k import types -from collections import MutableMapping EMPTY_SET = frozenset() @@ -265,13 +264,18 @@ class OrderedDict(dict): def __iter__(self): return iter(self._list) - keys = MutableMapping.keys - values = MutableMapping.values - items = MutableMapping.items + def keys(self): + return list(self) + + def values(self): + return [self[key] for key in self._list] + + def items(self): + return [(key, self[key]) for key in self._list] if py2k: def itervalues(self): - return iter([self[key] for key in self._list]) + return iter(self.values()) def iterkeys(self): return iter(self) -- cgit v1.2.1 From 7fa595221400d168a7bb78551d45379290db195f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 16 Aug 2014 13:33:02 -0400 Subject: - max failures 25 - guard against some potential pytest snarkiness --- lib/sqlalchemy/testing/plugin/pytestplugin.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index f4c9efd55..005942913 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -74,6 +74,9 @@ def pytest_collection_modifyitems(session, config, items): # new classes to a module on the fly. rebuilt_items = collections.defaultdict(list) + items[:] = [ + item for item in + items if isinstance(item.parent, pytest.Instance)] test_classes = set(item.parent for item in items) for test_class in test_classes: for sub_cls in plugin_base.generate_sub_tests( -- cgit v1.2.1 From e220ea11de931e86bbbaf373b49a26b906bbffdf Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 16 Aug 2014 13:37:49 -0400 Subject: - need list() here for py3k --- lib/sqlalchemy/orm/identity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/identity.py b/lib/sqlalchemy/orm/identity.py index 4425fc3a6..0fa541194 100644 --- a/lib/sqlalchemy/orm/identity.py +++ b/lib/sqlalchemy/orm/identity.py @@ -181,7 +181,10 @@ class WeakInstanceDict(IdentityMap): return iter(self.values()) def all_states(self): - return self._dict.values() + if util.py2k: + return self._dict.values() + else: + return list(self._dict.values()) def discard(self, state): if state.key in self._dict: -- cgit v1.2.1 From 4b288a9553b3eb44fef44eb1d649ca7dc0007e2d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 16 Aug 2014 13:46:15 -0400 Subject: - support dialects w/o sane multi row count again --- lib/sqlalchemy/orm/persistence.py | 48 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index b7283e48e..8c9b677fe 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -529,22 +529,40 @@ def _emit_update_statements(base_mapper, uowtransaction, value_params) rows += c.rowcount else: - multiparams = [rec[2] for rec in records] - c = cached_connections[connection].\ - execute(statement, multiparams) + if needs_version_id and \ + not connection.dialect.supports_sane_multi_rowcount and \ + connection.dialect.supports_sane_rowcount: + for state, state_dict, params, mapper, \ + connection, value_params in records: + c = cached_connections[connection].\ + execute(statement, params) + _postfetch( + mapper, + uowtransaction, + table, + state, + state_dict, + c, + c.context.compiled_parameters[0], + value_params) + rows += c.rowcount + else: + multiparams = [rec[2] for rec in records] + c = cached_connections[connection].\ + execute(statement, multiparams) - rows += c.rowcount - for state, state_dict, params, mapper, \ - connection, value_params in records: - _postfetch( - mapper, - uowtransaction, - table, - state, - state_dict, - c, - c.context.compiled_parameters[0], - value_params) + rows += c.rowcount + for state, state_dict, params, mapper, \ + connection, value_params in records: + _postfetch( + mapper, + uowtransaction, + table, + state, + state_dict, + c, + c.context.compiled_parameters[0], + value_params) if connection.dialect.supports_sane_rowcount: if rows != len(records): -- cgit v1.2.1 From 589f205d53f031ceb297af760f2acfc777a5bc5d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 16 Aug 2014 13:57:46 -0400 Subject: - changelog for pullreq github:125 - add pg8000 version detection for the "sane multi rowcount" feature --- lib/sqlalchemy/dialects/postgresql/pg8000.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index 909b41b82..4ccc90208 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -133,6 +133,16 @@ class PGDialect_pg8000(PGDialect): } ) + def initialize(self, connection): + if self.dbapi and hasattr(self.dbapi, '__version__'): + self._dbapi_version = tuple([ + int(x) for x in + self.dbapi.__version__.split(".")]) + else: + self._dbapi_version = (99, 99, 99) + self.supports_sane_multi_rowcount = self._dbapi_version >= (1, 9, 14) + super(PGDialect_pg8000, self).initialize(connection) + @classmethod def dbapi(cls): return __import__('pg8000') -- cgit v1.2.1 From b577afcb2bdcd94581606bc911968d8885509769 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 16 Aug 2014 19:49:07 -0400 Subject: - rework profiling, zoomark tests into single tests so that they can be used under xdist --- lib/sqlalchemy/testing/engines.py | 112 ------------------- lib/sqlalchemy/testing/profiling.py | 216 +++++++++++++----------------------- 2 files changed, 78 insertions(+), 250 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/testing/engines.py b/lib/sqlalchemy/testing/engines.py index 9052df570..67c13231e 100644 --- a/lib/sqlalchemy/testing/engines.py +++ b/lib/sqlalchemy/testing/engines.py @@ -7,15 +7,12 @@ from __future__ import absolute_import -import types import weakref -from collections import deque from . import config from .util import decorator from .. import event, pool import re import warnings -from .. import util class ConnectionKiller(object): @@ -339,112 +336,3 @@ def proxying_engine(conn_cls=DBAPIProxyConnection, return testing_engine(options={'creator': mock_conn}) -class ReplayableSession(object): - """A simple record/playback tool. - - This is *not* a mock testing class. It only records a session for later - playback and makes no assertions on call consistency whatsoever. It's - unlikely to be suitable for anything other than DB-API recording. - - """ - - Callable = object() - NoAttribute = object() - - if util.py2k: - Natives = set([getattr(types, t) - for t in dir(types) if not t.startswith('_')]).\ - difference([getattr(types, t) - for t in ('FunctionType', 'BuiltinFunctionType', - 'MethodType', 'BuiltinMethodType', - 'LambdaType', 'UnboundMethodType',)]) - else: - Natives = set([getattr(types, t) - for t in dir(types) if not t.startswith('_')]).\ - union([type(t) if not isinstance(t, type) - else t for t in __builtins__.values()]).\ - difference([getattr(types, t) - for t in ('FunctionType', 'BuiltinFunctionType', - 'MethodType', 'BuiltinMethodType', - 'LambdaType', )]) - - def __init__(self): - self.buffer = deque() - - def recorder(self, base): - return self.Recorder(self.buffer, base) - - def player(self): - return self.Player(self.buffer) - - class Recorder(object): - def __init__(self, buffer, subject): - self._buffer = buffer - self._subject = subject - - def __call__(self, *args, **kw): - subject, buffer = [object.__getattribute__(self, x) - for x in ('_subject', '_buffer')] - - result = subject(*args, **kw) - if type(result) not in ReplayableSession.Natives: - buffer.append(ReplayableSession.Callable) - return type(self)(buffer, result) - else: - buffer.append(result) - return result - - @property - def _sqla_unwrap(self): - return self._subject - - def __getattribute__(self, key): - try: - return object.__getattribute__(self, key) - except AttributeError: - pass - - subject, buffer = [object.__getattribute__(self, x) - for x in ('_subject', '_buffer')] - try: - result = type(subject).__getattribute__(subject, key) - except AttributeError: - buffer.append(ReplayableSession.NoAttribute) - raise - else: - if type(result) not in ReplayableSession.Natives: - buffer.append(ReplayableSession.Callable) - return type(self)(buffer, result) - else: - buffer.append(result) - return result - - class Player(object): - def __init__(self, buffer): - self._buffer = buffer - - def __call__(self, *args, **kw): - buffer = object.__getattribute__(self, '_buffer') - result = buffer.popleft() - if result is ReplayableSession.Callable: - return self - else: - return result - - @property - def _sqla_unwrap(self): - return None - - def __getattribute__(self, key): - try: - return object.__getattribute__(self, key) - except AttributeError: - pass - buffer = object.__getattribute__(self, '_buffer') - result = buffer.popleft() - if result is ReplayableSession.Callable: - return self - elif result is ReplayableSession.NoAttribute: - raise AttributeError(key) - else: - return result diff --git a/lib/sqlalchemy/testing/profiling.py b/lib/sqlalchemy/testing/profiling.py index 75baec987..fcb888f86 100644 --- a/lib/sqlalchemy/testing/profiling.py +++ b/lib/sqlalchemy/testing/profiling.py @@ -14,13 +14,12 @@ in a more fine-grained way than nose's profiling plugin. import os import sys -from .util import gc_collect, decorator +from .util import gc_collect from . import config from .plugin.plugin_base import SkipTest import pstats -import time import collections -from .. import util +import contextlib try: import cProfile @@ -30,64 +29,8 @@ from ..util import jython, pypy, win32, update_wrapper _current_test = None - -def profiled(target=None, **target_opts): - """Function profiling. - - @profiled() - or - @profiled(report=True, sort=('calls',), limit=20) - - Outputs profiling info for a decorated function. - - """ - - profile_config = {'targets': set(), - 'report': True, - 'print_callers': False, - 'print_callees': False, - 'graphic': False, - 'sort': ('time', 'calls'), - 'limit': None} - if target is None: - target = 'anonymous_target' - - @decorator - def decorate(fn, *args, **kw): - elapsed, load_stats, result = _profile( - fn, *args, **kw) - - graphic = target_opts.get('graphic', profile_config['graphic']) - if graphic: - os.system("runsnake %s" % filename) - else: - report = target_opts.get('report', profile_config['report']) - if report: - sort_ = target_opts.get('sort', profile_config['sort']) - limit = target_opts.get('limit', profile_config['limit']) - print(("Profile report for target '%s'" % ( - target, ) - )) - - stats = load_stats() - stats.sort_stats(*sort_) - if limit: - stats.print_stats(limit) - else: - stats.print_stats() - - print_callers = target_opts.get( - 'print_callers', profile_config['print_callers']) - if print_callers: - stats.print_callers() - - print_callees = target_opts.get( - 'print_callees', profile_config['print_callees']) - if print_callees: - stats.print_callees() - - return result - return decorate +# ProfileStatsFile instance, set up in plugin_base +_profile_stats = None class ProfileStatsFile(object): @@ -177,20 +120,23 @@ class ProfileStatsFile(object): self._write() def _header(self): - return \ - "# %s\n"\ - "# This file is written out on a per-environment basis.\n"\ - "# For each test in aaa_profiling, the corresponding function and \n"\ - "# environment is located within this file. If it doesn't exist,\n"\ - "# the test is skipped.\n"\ - "# If a callcount does exist, it is compared to what we received. \n"\ - "# assertions are raised if the counts do not match.\n"\ - "# \n"\ - "# To add a new callcount test, apply the function_call_count \n"\ - "# decorator and re-run the tests using the --write-profiles \n"\ - "# option - this file will be rewritten including the new count.\n"\ - "# \n"\ - "" % (self.fname) + return ( + "# %s\n" + "# This file is written out on a per-environment basis.\n" + "# For each test in aaa_profiling, the corresponding " + "function and \n" + "# environment is located within this file. " + "If it doesn't exist,\n" + "# the test is skipped.\n" + "# If a callcount does exist, it is compared " + "to what we received. \n" + "# assertions are raised if the counts do not match.\n" + "# \n" + "# To add a new callcount test, apply the function_call_count \n" + "# decorator and re-run the tests using the --write-profiles \n" + "# option - this file will be rewritten including the new count.\n" + "# \n" + ) % (self.fname) def _read(self): try: @@ -239,72 +185,66 @@ def function_call_count(variance=0.05): def decorate(fn): def wrap(*args, **kw): - - if cProfile is None: - raise SkipTest("cProfile is not installed") - - if not _profile_stats.has_stats() and not _profile_stats.write: - # run the function anyway, to support dependent tests - # (not a great idea but we have these in test_zoomark) - fn(*args, **kw) - raise SkipTest("No profiling stats available on this " - "platform for this function. Run tests with " - "--write-profiles to add statistics to %s for " - "this platform." % _profile_stats.short_fname) - - gc_collect() - - timespent, load_stats, fn_result = _profile( - fn, *args, **kw - ) - stats = load_stats() - callcount = stats.total_calls - - expected = _profile_stats.result(callcount) - if expected is None: - expected_count = None - else: - line_no, expected_count = expected - - print(("Pstats calls: %d Expected %s" % ( - callcount, - expected_count - ) - )) - stats.print_stats() - # stats.print_callers() - - if expected_count: - deviance = int(callcount * variance) - failed = abs(callcount - expected_count) > deviance - - if failed: - if _profile_stats.write: - _profile_stats.replace(callcount) - else: - raise AssertionError( - "Adjusted function call count %s not within %s%% " - "of expected %s. Rerun with --write-profiles to " - "regenerate this callcount." - % ( - callcount, (variance * 100), - expected_count)) - return fn_result + with count_functions(variance=variance): + return fn(*args, **kw) return update_wrapper(wrap, fn) return decorate -def _profile(fn, *args, **kw): - filename = "%s.prof" % fn.__name__ - - def load_stats(): - st = pstats.Stats(filename) - os.unlink(filename) - return st +@contextlib.contextmanager +def count_functions(variance=0.05): + if cProfile is None: + raise SkipTest("cProfile is not installed") + + if not _profile_stats.has_stats() and not _profile_stats.write: + raise SkipTest("No profiling stats available on this " + "platform for this function. Run tests with " + "--write-profiles to add statistics to %s for " + "this platform." % _profile_stats.short_fname) + + gc_collect() + + pr = cProfile.Profile() + pr.enable() + #began = time.time() + yield + #ended = time.time() + pr.disable() + + #s = compat.StringIO() + stats = pstats.Stats(pr, stream=sys.stdout) + + #timespent = ended - began + callcount = stats.total_calls + + expected = _profile_stats.result(callcount) + if expected is None: + expected_count = None + else: + line_no, expected_count = expected + + print(("Pstats calls: %d Expected %s" % ( + callcount, + expected_count + ) + )) + stats.sort_stats("cumulative") + stats.print_stats() + + if expected_count: + deviance = int(callcount * variance) + failed = abs(callcount - expected_count) > deviance + + if failed: + if _profile_stats.write: + _profile_stats.replace(callcount) + else: + raise AssertionError( + "Adjusted function call count %s not within %s%% " + "of expected %s. Rerun with --write-profiles to " + "regenerate this callcount." + % ( + callcount, (variance * 100), + expected_count)) - began = time.time() - cProfile.runctx('result = fn(*args, **kw)', globals(), locals(), - filename=filename) - ended = time.time() - return ended - began, load_stats, locals()['result'] -- cgit v1.2.1 From ef6042ff461e490c2a3040f18f0a3688b2e601a0 Mon Sep 17 00:00:00 2001 From: Malik Diarra Date: Wed, 13 Aug 2014 01:39:09 +0200 Subject: Adding a tablespace options for postgresql create table --- lib/sqlalchemy/dialects/postgresql/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index c2b1d66f4..0f008642e 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1448,6 +1448,13 @@ class PGDDLCompiler(compiler.DDLCompiler): text += self.define_constraint_deferrability(constraint) return text + def post_create_table(self, table): + table_opts = [] + if table.dialect_options['postgresql']['tablespace']: + table_opts.append('TABLESPACE %s' % table.dialect_options['postgresql']['tablespace']) + + return ' '.join(table_opts) + class PGTypeCompiler(compiler.GenericTypeCompiler): @@ -1707,7 +1714,8 @@ class PGDialect(default.DefaultDialect): "ops": {} }), (schema.Table, { - "ignore_search_path": False + "ignore_search_path": False, + "tablespace": None }) ] -- cgit v1.2.1 From d6873904c40134df787ffe5459d61d3663bf5d5f Mon Sep 17 00:00:00 2001 From: Malik Diarra Date: Sun, 17 Aug 2014 00:39:36 +0200 Subject: Adding oids and on_commit table options --- lib/sqlalchemy/dialects/postgresql/base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 0f008642e..9b30bf894 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1450,6 +1450,14 @@ class PGDDLCompiler(compiler.DDLCompiler): def post_create_table(self, table): table_opts = [] + if table.dialect_options['postgresql']['withoids'] is not None: + if table.dialect_options['postgresql']['withoids']: + table_opts.append('WITH OIDS') + else: + table_opts.append('WITHOUT OIDS') + if table.dialect_options['postgresql']['on_commit']: + on_commit_options = table.dialect_options['postgresql']['on_commit'].replace("_", " ").upper() + table_opts.append('ON COMMIT %s' % on_commit_options) if table.dialect_options['postgresql']['tablespace']: table_opts.append('TABLESPACE %s' % table.dialect_options['postgresql']['tablespace']) @@ -1715,7 +1723,9 @@ class PGDialect(default.DefaultDialect): }), (schema.Table, { "ignore_search_path": False, - "tablespace": None + "tablespace": None, + "withoids" : None, + "on_commit" : None, }) ] -- cgit v1.2.1 From 8e03430acdb98d7e5fef4a48a3120b928ed3266d Mon Sep 17 00:00:00 2001 From: Malik Diarra Date: Sun, 17 Aug 2014 01:39:14 +0200 Subject: quoting tablespace name in create table command in postgresql dialect --- lib/sqlalchemy/dialects/postgresql/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 9b30bf894..b09eaba72 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1459,7 +1459,8 @@ class PGDDLCompiler(compiler.DDLCompiler): on_commit_options = table.dialect_options['postgresql']['on_commit'].replace("_", " ").upper() table_opts.append('ON COMMIT %s' % on_commit_options) if table.dialect_options['postgresql']['tablespace']: - table_opts.append('TABLESPACE %s' % table.dialect_options['postgresql']['tablespace']) + tablespace_name = table.dialect_options['postgresql']['tablespace'] + table_opts.append('TABLESPACE %s' % self.preparer.quote(tablespace_name)) return ' '.join(table_opts) -- cgit v1.2.1 From 2de7f94739ec1873e1dce48797e1e6f12044cf4c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 16 Aug 2014 20:17:08 -0400 Subject: - oldest screwup in the book, forgot the file --- lib/sqlalchemy/testing/replay_fixture.py | 167 +++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 lib/sqlalchemy/testing/replay_fixture.py (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/testing/replay_fixture.py b/lib/sqlalchemy/testing/replay_fixture.py new file mode 100644 index 000000000..b8a0f6df1 --- /dev/null +++ b/lib/sqlalchemy/testing/replay_fixture.py @@ -0,0 +1,167 @@ +from . import fixtures +from . import profiling +from .. import util +import types +from collections import deque +import contextlib +from . import config +from sqlalchemy import MetaData +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + + +class ReplayFixtureTest(fixtures.TestBase): + + @contextlib.contextmanager + def _dummy_ctx(self, *arg, **kw): + yield + + def test_invocation(self): + + dbapi_session = ReplayableSession() + creator = config.db.pool._creator + recorder = lambda: dbapi_session.recorder(creator()) + engine = create_engine( + config.db.url, creator=recorder, + use_native_hstore=False) + self.metadata = MetaData(engine) + self.engine = engine + self.session = Session(engine) + + self.setup_engine() + self._run_steps(ctx=self._dummy_ctx) + self.teardown_engine() + engine.dispose() + + player = lambda: dbapi_session.player() + engine = create_engine( + config.db.url, creator=player, + use_native_hstore=False) + + self.metadata = MetaData(engine) + self.engine = engine + self.session = Session(engine) + + self.setup_engine() + self._run_steps(ctx=profiling.count_functions) + self.teardown_engine() + + def setup_engine(self): + pass + + def teardown_engine(self): + pass + + def _run_steps(self, ctx): + raise NotImplementedError() + + +class ReplayableSession(object): + """A simple record/playback tool. + + This is *not* a mock testing class. It only records a session for later + playback and makes no assertions on call consistency whatsoever. It's + unlikely to be suitable for anything other than DB-API recording. + + """ + + Callable = object() + NoAttribute = object() + + if util.py2k: + Natives = set([getattr(types, t) + for t in dir(types) if not t.startswith('_')]).\ + difference([getattr(types, t) + for t in ('FunctionType', 'BuiltinFunctionType', + 'MethodType', 'BuiltinMethodType', + 'LambdaType', 'UnboundMethodType',)]) + else: + Natives = set([getattr(types, t) + for t in dir(types) if not t.startswith('_')]).\ + union([type(t) if not isinstance(t, type) + else t for t in __builtins__.values()]).\ + difference([getattr(types, t) + for t in ('FunctionType', 'BuiltinFunctionType', + 'MethodType', 'BuiltinMethodType', + 'LambdaType', )]) + + def __init__(self): + self.buffer = deque() + + def recorder(self, base): + return self.Recorder(self.buffer, base) + + def player(self): + return self.Player(self.buffer) + + class Recorder(object): + def __init__(self, buffer, subject): + self._buffer = buffer + self._subject = subject + + def __call__(self, *args, **kw): + subject, buffer = [object.__getattribute__(self, x) + for x in ('_subject', '_buffer')] + + result = subject(*args, **kw) + if type(result) not in ReplayableSession.Natives: + buffer.append(ReplayableSession.Callable) + return type(self)(buffer, result) + else: + buffer.append(result) + return result + + @property + def _sqla_unwrap(self): + return self._subject + + def __getattribute__(self, key): + try: + return object.__getattribute__(self, key) + except AttributeError: + pass + + subject, buffer = [object.__getattribute__(self, x) + for x in ('_subject', '_buffer')] + try: + result = type(subject).__getattribute__(subject, key) + except AttributeError: + buffer.append(ReplayableSession.NoAttribute) + raise + else: + if type(result) not in ReplayableSession.Natives: + buffer.append(ReplayableSession.Callable) + return type(self)(buffer, result) + else: + buffer.append(result) + return result + + class Player(object): + def __init__(self, buffer): + self._buffer = buffer + + def __call__(self, *args, **kw): + buffer = object.__getattribute__(self, '_buffer') + result = buffer.popleft() + if result is ReplayableSession.Callable: + return self + else: + return result + + @property + def _sqla_unwrap(self): + return None + + def __getattribute__(self, key): + try: + return object.__getattribute__(self, key) + except AttributeError: + pass + buffer = object.__getattribute__(self, '_buffer') + result = buffer.popleft() + if result is ReplayableSession.Callable: + return self + elif result is ReplayableSession.NoAttribute: + raise AttributeError(key) + else: + return result -- cgit v1.2.1 From 9eacc8d42ad49527c7fd0fe7e37100edba9eb1dc Mon Sep 17 00:00:00 2001 From: Malik Diarra Date: Sun, 17 Aug 2014 02:48:21 +0200 Subject: Correcting options name from withoids to with_oids --- lib/sqlalchemy/dialects/postgresql/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index b09eaba72..057a5e072 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1450,8 +1450,8 @@ class PGDDLCompiler(compiler.DDLCompiler): def post_create_table(self, table): table_opts = [] - if table.dialect_options['postgresql']['withoids'] is not None: - if table.dialect_options['postgresql']['withoids']: + if table.dialect_options['postgresql']['with_oids'] is not None: + if table.dialect_options['postgresql']['with_oids']: table_opts.append('WITH OIDS') else: table_opts.append('WITHOUT OIDS') @@ -1725,7 +1725,7 @@ class PGDialect(default.DefaultDialect): (schema.Table, { "ignore_search_path": False, "tablespace": None, - "withoids" : None, + "with_oids" : None, "on_commit" : None, }) ] -- cgit v1.2.1 From faa5a9067661039dcc8663e00bdcea2d098c9989 Mon Sep 17 00:00:00 2001 From: Malik Diarra Date: Sun, 17 Aug 2014 16:56:53 +0200 Subject: Adding postgres create table options documentation --- lib/sqlalchemy/dialects/postgresql/base.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 057a5e072..34932520f 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -417,6 +417,22 @@ of :class:`.PGInspector`, which offers additional methods:: .. autoclass:: PGInspector :members: +PostgreSQL specific table options +--------------------------------- + +PostgreSQL provides several CREATE TABLE specific options allowing to +specify how table data are stored. The following options are currently +supported: ``TABLESPACE``, ``ON COMMIT``, ``WITH OIDS``. + +``postgresql_tablespace`` is probably the more common and allows to specify +where in the filesystem the data files for the table will be created (see +http://www.postgresql.org/docs/9.3/static/manage-ag-tablespaces.html) + +.. seealso:: + + `Postgresql CREATE TABLE options + `_ - + on the PostgreSQL website """ from collections import defaultdict -- cgit v1.2.1 From 530d3f07e0c1e70e0f9b80d3b5986253e06dcaf2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 17 Aug 2014 20:06:16 -0400 Subject: - Fixed bug where attribute "set" events or columns with ``@validates`` would have events triggered within the flush process, when those columns were the targets of a "fetch and populate" operation, such as an autoincremented primary key, a Python side default, or a server-side default "eagerly" fetched via RETURNING. fixes #3167 --- lib/sqlalchemy/orm/mapper.py | 29 +++++++++++++++++++---------- lib/sqlalchemy/orm/persistence.py | 12 +++--------- 2 files changed, 22 insertions(+), 19 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index fc15769cd..1e1291857 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1189,14 +1189,6 @@ class Mapper(InspectionAttr): util.ordered_column_set(t.c).\ intersection(all_cols) - # determine cols that aren't expressed within our tables; mark these - # as "read only" properties which are refreshed upon INSERT/UPDATE - self._readonly_props = set( - self._columntoproperty[col] - for col in self._columntoproperty - if not hasattr(col, 'table') or - col.table not in self._cols_by_table) - # if explicit PK argument sent, add those columns to the # primary key mappings if self._primary_key_argument: @@ -1247,6 +1239,15 @@ class Mapper(InspectionAttr): self.primary_key = tuple(primary_key) self._log("Identified primary key columns: %s", primary_key) + # determine cols that aren't expressed within our tables; mark these + # as "read only" properties which are refreshed upon INSERT/UPDATE + self._readonly_props = set( + self._columntoproperty[col] + for col in self._columntoproperty + if self._columntoproperty[col] not in self._primary_key_props and + (not hasattr(col, 'table') or + col.table not in self._cols_by_table)) + def _configure_properties(self): # Column and other ClauseElement objects which are mapped @@ -2342,18 +2343,26 @@ class Mapper(InspectionAttr): dict_ = state.dict manager = state.manager return [ - manager[self._columntoproperty[col].key]. + manager[prop.key]. impl.get(state, dict_, attributes.PASSIVE_RETURN_NEVER_SET) - for col in self.primary_key + for prop in self._primary_key_props ] + @_memoized_configured_property + def _primary_key_props(self): + return [self._columntoproperty[col] for col in self.primary_key] + def _get_state_attr_by_column( self, state, dict_, column, passive=attributes.PASSIVE_RETURN_NEVER_SET): prop = self._columntoproperty[column] return state.manager[prop.key].impl.get(state, dict_, passive=passive) + def _set_committed_state_attr_by_column(self, state, dict_, column, value): + prop = self._columntoproperty[column] + state.manager[prop.key].impl.set_committed_value(state, dict_, value) + def _set_state_attr_by_column(self, state, dict_, column, value): prop = self._columntoproperty[column] state.manager[prop.key].impl.set(state, dict_, value, None) diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 8c9b677fe..d511c0816 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -645,13 +645,7 @@ def _emit_insert_statements(base_mapper, uowtransaction, mapper._pks_by_table[table]): prop = mapper_rec._columntoproperty[col] if state_dict.get(prop.key) is None: - # TODO: would rather say: - # state_dict[prop.key] = pk - mapper_rec._set_state_attr_by_column( - state, - state_dict, - col, pk) - + state_dict[prop.key] = pk _postfetch( mapper_rec, uowtransaction, @@ -836,11 +830,11 @@ def _postfetch(mapper, uowtransaction, table, for col in returning_cols: if col.primary_key: continue - mapper._set_state_attr_by_column(state, dict_, col, row[col]) + dict_[mapper._columntoproperty[col].key] = row[col] for c in prefetch_cols: if c.key in params and c in mapper._columntoproperty: - mapper._set_state_attr_by_column(state, dict_, c, params[c.key]) + dict_[mapper._columntoproperty[c].key] = params[c.key] if postfetch_cols: state._expire_attributes(state.dict, -- cgit v1.2.1 From d39927ec20dd0b66f4ab3aab3e4e67b3814186ce Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 18 Aug 2014 12:50:29 -0400 Subject: - major simplification of _collect_update_commands. in particular, we only call upon the history API fully for primary key columns. We also now skip the whole step of looking at PK columns and using any history at all if no net changes are detected on the object. --- lib/sqlalchemy/orm/mapper.py | 13 ++++ lib/sqlalchemy/orm/persistence.py | 140 ++++++++++++++++---------------------- 2 files changed, 70 insertions(+), 83 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 1e1291857..14dc5d7f8 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1893,6 +1893,19 @@ class Mapper(InspectionAttr): """ + @_memoized_configured_property + def _propkey_to_col(self): + return dict( + ( + table, + dict( + (self._columntoproperty[col].key, col) + for col in columns + ) + ) + for table, columns in self._cols_by_table.items() + ) + @_memoized_configured_property def _col_to_propkey(self): return dict( diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index d511c0816..228cfef3a 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -304,96 +304,70 @@ def _collect_update_commands(base_mapper, uowtransaction, params = {} value_params = {} - hasdata = hasnull = False + propkey_to_col = mapper._propkey_to_col[table] - for col in mapper._cols_by_table[table]: - if col is mapper.version_id_col: - params[col._label] = \ - mapper._get_committed_state_attr_by_column( - row_switch or state, - row_switch and row_switch.dict - or state_dict, - col) + for propkey in set(propkey_to_col).intersection(state.committed_state): + value = state_dict[propkey] + col = propkey_to_col[propkey] - prop = mapper._columntoproperty[col] - history = state.manager[prop.key].impl.get_history( - state, state_dict, attributes.PASSIVE_NO_INITIALIZE - ) - if history.added: - params[col.key] = history.added[0] - hasdata = True + if not state.manager[propkey].impl.is_equal( + value, state.committed_state[propkey]): + if isinstance(value, sql.ClauseElement): + value_params[col] = value else: - if mapper.version_id_generator is not False: - val = mapper.version_id_generator(params[col._label]) - params[col.key] = val - - # HACK: check for history, in case the - # history is only - # in a different table than the one - # where the version_id_col is. - for prop in mapper._columntoproperty.values(): - history = ( - state.manager[prop.key].impl.get_history( - state, state_dict, - attributes.PASSIVE_NO_INITIALIZE)) - if history.added: - hasdata = True + params[col.key] = value + + if mapper.version_id_col is not None: + col = mapper.version_id_col + params[col._label] = \ + mapper._get_committed_state_attr_by_column( + row_switch if row_switch else state, + row_switch.dict if row_switch else state_dict, + col) + + if col.key not in params and \ + mapper.version_id_generator is not False: + val = mapper.version_id_generator(params[col._label]) + params[col.key] = val + + if not (params or value_params): + continue + + pk_params = {} + for col in pks: + propkey = mapper._columntoproperty[col].key + history = state.manager[propkey].impl.get_history( + state, state_dict, attributes.PASSIVE_OFF) + + if row_switch and not history.deleted and history.added: + # row switch present. convert a row that thought + # it would be an INSERT into an UPDATE, by removing + # the PK value from the SET clause and instead putting + # it in the WHERE clause. + del params[col.key] + pk_params[col._label] = history.added[0] + elif history.added: + # we're updating the PK value. + assert history.deleted, ( + "New PK value without an old one not " + "possible for an UPDATE") + # check if an UPDATE of the PK value + # has already occurred as a result of ON UPDATE CASCADE. + # If so, use the new value to locate the row. + if ("pk_cascaded", state, col) in uowtransaction.attributes: + pk_params[col._label] = history.added[0] + else: + # else, use the old value to locate the row + pk_params[col._label] = history.deleted[0] else: - prop = mapper._columntoproperty[col] - history = state.manager[prop.key].impl.get_history( - state, state_dict, - attributes.PASSIVE_OFF if col in pks else - attributes.PASSIVE_NO_INITIALIZE) - if history.added: - if isinstance(history.added[0], - sql.ClauseElement): - value_params[col] = history.added[0] - else: - value = history.added[0] - params[col.key] = value - - if col in pks: - if history.deleted and \ - not row_switch: - # if passive_updates and sync detected - # this was a pk->pk sync, use the new - # value to locate the row, since the - # DB would already have set this - if ("pk_cascaded", state, col) in \ - uowtransaction.attributes: - value = history.added[0] - params[col._label] = value - else: - # use the old value to - # locate the row - value = history.deleted[0] - params[col._label] = value - hasdata = True - else: - # row switch logic can reach us here - # remove the pk from the update params - # so the update doesn't - # attempt to include the pk in the - # update statement - del params[col.key] - value = history.added[0] - params[col._label] = value - if value is None: - hasnull = True - else: - hasdata = True - elif col in pks: - value = history.unchanged[0] - if value is None: - hasnull = True - params[col._label] = value + pk_params[col._label] = history.unchanged[0] - if hasdata: - if hasnull: + if params or value_params: + if None in pk_params.values(): raise orm_exc.FlushError( - "Can't update table " - "using NULL for primary " + "Can't update table using NULL for primary " "key value") + params.update(pk_params) update.append((state, state_dict, params, mapper, connection, value_params)) return update -- cgit v1.2.1 From 06dec268e53e999bd348ef2ca148def066ca30d6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 18 Aug 2014 16:32:48 -0400 Subject: - organize persistence methods in terms of generators, narrow down argument lists and generator items for each function down to just what each function needs. This will help for them to be of more multipurpose use for bulk operations --- lib/sqlalchemy/orm/persistence.py | 187 +++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 93 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 228cfef3a..c7850ac1d 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -40,32 +40,58 @@ def save_obj(base_mapper, states, uowtransaction, single=False): save_obj(base_mapper, [state], uowtransaction, single=True) return - states_to_insert, states_to_update = _organize_states_for_save( - base_mapper, - states, - uowtransaction) - + states_to_update = [] + states_to_insert = [] cached_connections = _cached_connection_dict(base_mapper) - for table, mapper in base_mapper._sorted_tables.items(): - insert = _collect_insert_commands(base_mapper, uowtransaction, - table, states_to_insert) - - update = _collect_update_commands(base_mapper, uowtransaction, - table, states_to_update) - - if update: - _emit_update_statements(base_mapper, uowtransaction, - cached_connections, - mapper, table, update) - - if insert: - _emit_insert_statements(base_mapper, uowtransaction, - cached_connections, - mapper, table, insert) + for (state, dict_, mapper, connection, + has_identity, row_switch) in _organize_states_for_save( + base_mapper, states, uowtransaction + ): + if has_identity or row_switch: + states_to_update.append( + (state, dict_, mapper, connection, + has_identity, row_switch) + ) + else: + states_to_insert.append( + (state, dict_, mapper, connection, + has_identity, row_switch) + ) - _finalize_insert_update_commands(base_mapper, uowtransaction, - states_to_insert, states_to_update) + for table, mapper in base_mapper._sorted_tables.items(): + if table not in mapper._pks_by_table: + continue + insert = ( + (state, state_dict, mapper, connection) + for state, state_dict, mapper, connection, has_identity, + row_switch in states_to_insert + ) + insert = _collect_insert_commands(table, insert) + + update = ( + (state, state_dict, mapper, connection, row_switch) + for state, state_dict, mapper, connection, has_identity, + row_switch in states_to_update + ) + update = _collect_update_commands(uowtransaction, table, update) + + _emit_update_statements(base_mapper, uowtransaction, + cached_connections, + mapper, table, update) + + _emit_insert_statements(base_mapper, uowtransaction, + cached_connections, + mapper, table, insert) + + _finalize_insert_update_commands( + base_mapper, uowtransaction, + ( + (state, state_dict, mapper, connection, has_identity) + for state, state_dict, mapper, connection, has_identity, + row_switch in states_to_insert + states_to_update + ) + ) def post_update(base_mapper, states, uowtransaction, post_update_cols): @@ -75,19 +101,20 @@ def post_update(base_mapper, states, uowtransaction, post_update_cols): """ cached_connections = _cached_connection_dict(base_mapper) - states_to_update = _organize_states_for_post_update( + states_to_update = list(_organize_states_for_post_update( base_mapper, - states, uowtransaction) + states, uowtransaction)) for table, mapper in base_mapper._sorted_tables.items(): + if table not in mapper._pks_by_table: + continue update = _collect_post_update_commands(base_mapper, uowtransaction, table, states_to_update, post_update_cols) - if update: - _emit_post_update_statements(base_mapper, uowtransaction, - cached_connections, - mapper, table, update) + _emit_post_update_statements(base_mapper, uowtransaction, + cached_connections, + mapper, table, update) def delete_obj(base_mapper, states, uowtransaction): @@ -100,19 +127,21 @@ def delete_obj(base_mapper, states, uowtransaction): cached_connections = _cached_connection_dict(base_mapper) - states_to_delete = _organize_states_for_delete( + states_to_delete = list(_organize_states_for_delete( base_mapper, states, - uowtransaction) + uowtransaction)) table_to_mapper = base_mapper._sorted_tables for table in reversed(list(table_to_mapper.keys())): + mapper = table_to_mapper[table] + if table not in mapper._pks_by_table: + continue + delete = _collect_delete_commands(base_mapper, uowtransaction, table, states_to_delete) - mapper = table_to_mapper[table] - _emit_delete_statements(base_mapper, uowtransaction, cached_connections, mapper, table, delete) @@ -133,9 +162,6 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): """ - states_to_insert = [] - states_to_update = [] - for state, dict_, mapper, connection in _connections_for_states( base_mapper, uowtransaction, states): @@ -181,18 +207,8 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): uowtransaction.remove_state_actions(existing) row_switch = existing - if not has_identity and not row_switch: - states_to_insert.append( - (state, dict_, mapper, connection, - has_identity, row_switch) - ) - else: - states_to_update.append( - (state, dict_, mapper, connection, - has_identity, row_switch) - ) - - return states_to_insert, states_to_update + yield (state, dict_, mapper, connection, + has_identity, row_switch) def _organize_states_for_post_update(base_mapper, states, @@ -205,8 +221,7 @@ def _organize_states_for_post_update(base_mapper, states, the execution per state. """ - return list(_connections_for_states(base_mapper, uowtransaction, - states)) + return _connections_for_states(base_mapper, uowtransaction, states) def _organize_states_for_delete(base_mapper, states, uowtransaction): @@ -217,28 +232,21 @@ def _organize_states_for_delete(base_mapper, states, uowtransaction): mapper, the connection to use for the execution per state. """ - states_to_delete = [] - for state, dict_, mapper, connection in _connections_for_states( base_mapper, uowtransaction, states): mapper.dispatch.before_delete(mapper, connection, state) - states_to_delete.append((state, dict_, mapper, - bool(state.key), connection)) - return states_to_delete + yield state, dict_, mapper, bool(state.key), connection -def _collect_insert_commands(base_mapper, uowtransaction, table, - states_to_insert): +def _collect_insert_commands(table, states_to_insert): """Identify sets of values to use in INSERT statements for a list of states. """ - insert = [] - for state, state_dict, mapper, connection, has_identity, \ - row_switch in states_to_insert: + for state, state_dict, mapper, connection in states_to_insert: if table not in mapper._pks_by_table: continue @@ -262,7 +270,7 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, has_all_pks = mapper._pk_keys_by_table[table].issubset(params) - if base_mapper.eager_defaults: + if mapper.base_mapper.eager_defaults: has_all_defaults = mapper._server_default_cols[table].\ issubset(params) else: @@ -274,14 +282,13 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, params[mapper.version_id_col.key] = \ mapper.version_id_generator(None) - insert.append((state, state_dict, params, mapper, - connection, value_params, has_all_pks, - has_all_defaults)) - return insert + yield ( + state, state_dict, params, mapper, + connection, value_params, has_all_pks, + has_all_defaults) -def _collect_update_commands(base_mapper, uowtransaction, - table, states_to_update): +def _collect_update_commands(uowtransaction, table, states_to_update): """Identify sets of values to use in UPDATE statements for a list of states. @@ -293,9 +300,7 @@ def _collect_update_commands(base_mapper, uowtransaction, """ - update = [] - for state, state_dict, mapper, connection, has_identity, \ - row_switch in states_to_update: + for state, state_dict, mapper, connection, row_switch in states_to_update: if table not in mapper._pks_by_table: continue @@ -368,9 +373,9 @@ def _collect_update_commands(base_mapper, uowtransaction, "Can't update table using NULL for primary " "key value") params.update(pk_params) - update.append((state, state_dict, params, mapper, - connection, value_params)) - return update + yield ( + state, state_dict, params, mapper, + connection, value_params) def _collect_post_update_commands(base_mapper, uowtransaction, table, @@ -380,7 +385,6 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, """ - update = [] for state, state_dict, mapper, connection in states_to_update: if table not in mapper._pks_by_table: continue @@ -405,9 +409,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, params[col.key] = value hasdata = True if hasdata: - update.append((state, state_dict, params, mapper, - connection)) - return update + yield params, connection def _collect_delete_commands(base_mapper, uowtransaction, table, @@ -415,15 +417,12 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, """Identify values to use in DELETE statements for a list of states to be deleted.""" - delete = util.defaultdict(list) - for state, state_dict, mapper, has_identity, connection \ in states_to_delete: if not has_identity or table not in mapper._pks_by_table: continue params = {} - delete[connection].append(params) for col in mapper._pks_by_table[table]: params[col.key] = \ value = \ @@ -441,7 +440,7 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, mapper._get_committed_state_attr_by_column( state, state_dict, mapper.version_id_col) - return delete + yield params, connection def _emit_update_statements(base_mapper, uowtransaction, @@ -481,8 +480,7 @@ def _emit_update_statements(base_mapper, uowtransaction, lambda rec: ( rec[4], tuple(sorted(rec[2])), - bool(rec[5])) - ): + bool(rec[5]))): rows = 0 records = list(records) @@ -652,11 +650,10 @@ def _emit_post_update_statements(base_mapper, uowtransaction, # also group them into common (connection, cols) sets # to support executemany(). for key, grouper in groupby( - update, lambda rec: (rec[4], list(rec[2].keys())) + update, lambda rec: (rec[1], sorted(rec[0])) ): connection = key[0] - multiparams = [params for state, state_dict, - params, mapper, conn in grouper] + multiparams = [params for params, conn in grouper] cached_connections[connection].\ execute(statement, multiparams) @@ -686,8 +683,15 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections, return table.delete(clause) - for connection, del_objects in delete.items(): - statement = base_mapper._memo(('delete', table), delete_stmt) + statement = base_mapper._memo(('delete', table), delete_stmt) + for connection, recs in groupby( + delete, + lambda rec: rec[1] + ): + del_objects = [ + params + for params, connection in recs + ] connection = cached_connections[connection] @@ -740,15 +744,12 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections, ) -def _finalize_insert_update_commands(base_mapper, uowtransaction, - states_to_insert, states_to_update): +def _finalize_insert_update_commands(base_mapper, uowtransaction, states): """finalize state on states that have been inserted or updated, including calling after_insert/after_update events. """ - for state, state_dict, mapper, connection, has_identity, \ - row_switch in states_to_insert + \ - states_to_update: + for state, state_dict, mapper, connection, has_identity in states: if mapper._readonly_props: readonly = state.unmodified_intersection( -- cgit v1.2.1 From 4ade138769a74ee2beda184e89d89238426d3741 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 18 Aug 2014 16:44:07 -0400 Subject: - further reorganize collect_insert_commands to distinguish between setting up given values vs. defaults. again trying to shoot for making this of more general use --- lib/sqlalchemy/orm/persistence.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index c7850ac1d..f17b1d79c 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -253,20 +253,28 @@ def _collect_insert_commands(table, states_to_insert): params = {} value_params = {} - for col, propkey in mapper._col_to_propkey[table]: - if propkey in state_dict: - value = state_dict[propkey] - if isinstance(value, sql.ClauseElement): - value_params[col.key] = value - elif value is not None or ( - not col.primary_key and - not col.server_default and - not col.default): - params[col.key] = value + + propkey_to_col = mapper._propkey_to_col[table] + + for propkey in set(propkey_to_col).intersection(state_dict): + value = state_dict[propkey] + col = propkey_to_col[propkey] + if value is None: + continue + elif isinstance(value, sql.ClauseElement): + value_params[col.key] = value else: - if not col.server_default \ - and not col.default and not col.primary_key: - params[col.key] = None + params[col.key] = value + + for colkey in ( + set( + col.key for col in + mapper._cols_by_table[table] + if not col.primary_key and + not col.server_default and not col.default + ).difference(params).difference(value_params) + ): + params[colkey] = None has_all_pks = mapper._pk_keys_by_table[table].issubset(params) -- cgit v1.2.1 From 4ed640ba907b529d79c634baf37792ce14e59805 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 18 Aug 2014 17:02:52 -0400 Subject: - move out checks for table in mapper._pks_by_table --- lib/sqlalchemy/orm/persistence.py | 48 ++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 16 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index f17b1d79c..c949e4776 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -63,16 +63,18 @@ def save_obj(base_mapper, states, uowtransaction, single=False): if table not in mapper._pks_by_table: continue insert = ( - (state, state_dict, mapper, connection) - for state, state_dict, mapper, connection, has_identity, + (state, state_dict, sub_mapper, connection) + for state, state_dict, sub_mapper, connection, has_identity, row_switch in states_to_insert + if table in sub_mapper._pks_by_table ) insert = _collect_insert_commands(table, insert) update = ( - (state, state_dict, mapper, connection, row_switch) - for state, state_dict, mapper, connection, has_identity, + (state, state_dict, sub_mapper, connection, row_switch) + for state, state_dict, sub_mapper, connection, has_identity, row_switch in states_to_update + if table in sub_mapper._pks_by_table ) update = _collect_update_commands(uowtransaction, table, update) @@ -108,8 +110,16 @@ def post_update(base_mapper, states, uowtransaction, post_update_cols): for table, mapper in base_mapper._sorted_tables.items(): if table not in mapper._pks_by_table: continue + + update = ( + (state, state_dict, sub_mapper, connection) + for + state, state_dict, sub_mapper, connection in states_to_update + if table in sub_mapper._pks_by_table + ) + update = _collect_post_update_commands(base_mapper, uowtransaction, - table, states_to_update, + table, update, post_update_cols) _emit_post_update_statements(base_mapper, uowtransaction, @@ -139,8 +149,15 @@ def delete_obj(base_mapper, states, uowtransaction): if table not in mapper._pks_by_table: continue + delete = ( + (state, state_dict, sub_mapper, connection) + for state, state_dict, sub_mapper, has_identity, connection + in states_to_delete if table in sub_mapper._pks_by_table + and has_identity + ) + delete = _collect_delete_commands(base_mapper, uowtransaction, - table, states_to_delete) + table, delete) _emit_delete_statements(base_mapper, uowtransaction, cached_connections, mapper, table, delete) @@ -248,8 +265,7 @@ def _collect_insert_commands(table, states_to_insert): """ for state, state_dict, mapper, connection in states_to_insert: - if table not in mapper._pks_by_table: - continue + # assert table in mapper._pks_by_table params = {} value_params = {} @@ -309,8 +325,8 @@ def _collect_update_commands(uowtransaction, table, states_to_update): """ for state, state_dict, mapper, connection, row_switch in states_to_update: - if table not in mapper._pks_by_table: - continue + + # assert table in mapper._pks_by_table pks = mapper._pks_by_table[table] @@ -394,8 +410,9 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, """ for state, state_dict, mapper, connection in states_to_update: - if table not in mapper._pks_by_table: - continue + + # assert table in mapper._pks_by_table + pks = mapper._pks_by_table[table] params = {} hasdata = False @@ -425,10 +442,9 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, """Identify values to use in DELETE statements for a list of states to be deleted.""" - for state, state_dict, mapper, has_identity, connection \ - in states_to_delete: - if not has_identity or table not in mapper._pks_by_table: - continue + for state, state_dict, mapper, connection in states_to_delete: + + # assert table in mapper._pks_by_table params = {} for col in mapper._pks_by_table[table]: -- cgit v1.2.1 From 399c03939768d4c8afb29ca1e091b046ea4fc88f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 18 Aug 2014 17:12:06 -0400 Subject: - optimize collection of cols we insert as none --- lib/sqlalchemy/orm/mapper.py | 18 +++++++++--------- lib/sqlalchemy/orm/persistence.py | 10 ++-------- 2 files changed, 11 insertions(+), 17 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 14dc5d7f8..89c092b58 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1894,27 +1894,27 @@ class Mapper(InspectionAttr): """ @_memoized_configured_property - def _propkey_to_col(self): + def _insert_cols_as_none(self): return dict( ( table, - dict( - (self._columntoproperty[col].key, col) - for col in columns - ) + frozenset( + col.key for col in columns + if not col.primary_key and + not col.server_default and not col.default) ) for table, columns in self._cols_by_table.items() ) @_memoized_configured_property - def _col_to_propkey(self): + def _propkey_to_col(self): return dict( ( table, - [ - (col, self._columntoproperty[col].key) + dict( + (self._columntoproperty[col].key, col) for col in columns - ] + ) ) for table, columns in self._cols_by_table.items() ) diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index c949e4776..e36f87991 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -282,14 +282,8 @@ def _collect_insert_commands(table, states_to_insert): else: params[col.key] = value - for colkey in ( - set( - col.key for col in - mapper._cols_by_table[table] - if not col.primary_key and - not col.server_default and not col.default - ).difference(params).difference(value_params) - ): + for colkey in mapper._insert_cols_as_none[table].\ + difference(params).difference(value_params): params[colkey] = None has_all_pks = mapper._pk_keys_by_table[table].issubset(params) -- cgit v1.2.1 From 28103e9a865860a46037ca82e634827f2329deb0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 19 Aug 2014 16:14:57 -0400 Subject: - simplify PK logic in update for row switch --- lib/sqlalchemy/orm/mapper.py | 3 +++ lib/sqlalchemy/orm/persistence.py | 22 ++++++---------------- 2 files changed, 9 insertions(+), 16 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 89c092b58..f22cac329 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2364,6 +2364,9 @@ class Mapper(InspectionAttr): @_memoized_configured_property def _primary_key_props(self): + # TODO: this should really be called "identity key props", + # as it does not necessarily include primary key columns within + # individual tables return [self._columntoproperty[col] for col in self.primary_key] def _get_state_attr_by_column( diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index e36f87991..37b696d0f 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -362,29 +362,19 @@ def _collect_update_commands(uowtransaction, table, states_to_update): history = state.manager[propkey].impl.get_history( state, state_dict, attributes.PASSIVE_OFF) - if row_switch and not history.deleted and history.added: - # row switch present. convert a row that thought - # it would be an INSERT into an UPDATE, by removing - # the PK value from the SET clause and instead putting - # it in the WHERE clause. - del params[col.key] - pk_params[col._label] = history.added[0] - elif history.added: - # we're updating the PK value. - assert history.deleted, ( - "New PK value without an old one not " - "possible for an UPDATE") - # check if an UPDATE of the PK value - # has already occurred as a result of ON UPDATE CASCADE. - # If so, use the new value to locate the row. - if ("pk_cascaded", state, col) in uowtransaction.attributes: + if history.added: + if not history.deleted or \ + ("pk_cascaded", state, col) in uowtransaction.attributes: pk_params[col._label] = history.added[0] + params.pop(col.key, None) else: # else, use the old value to locate the row pk_params[col._label] = history.deleted[0] + params[col.key] = history.added[0] else: pk_params[col._label] = history.unchanged[0] + if params or value_params: if None in pk_params.values(): raise orm_exc.FlushError( -- cgit v1.2.1 From 92b0ad0fef0b9ee3d54767cf17e2baf1fd1546da Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 20 Aug 2014 12:01:20 -0400 Subject: - Fixed bug in connection pool logging where the "connection checked out" debug logging message would not emit if the logging were set up using ``logging.setLevel()``, rather than using the ``echo_pool`` flag. Tests to assert this logging have been added. This is a regression that was introduced in 0.9.0. fixes #3168 --- lib/sqlalchemy/pool.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index d26bbf32c..89cddfc31 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -443,16 +443,17 @@ class _ConnectionRecord(object): except: rec.checkin() raise - fairy = _ConnectionFairy(dbapi_connection, rec) + echo = pool._should_log_debug() + fairy = _ConnectionFairy(dbapi_connection, rec, echo) rec.fairy_ref = weakref.ref( fairy, lambda ref: _finalize_fairy and _finalize_fairy( dbapi_connection, - rec, pool, ref, pool._echo) + rec, pool, ref, echo) ) _refs.add(rec) - if pool._echo: + if echo: pool.logger.debug("Connection %r checked out from pool", dbapi_connection) return fairy @@ -560,9 +561,10 @@ def _finalize_fairy(connection, connection_record, connection) try: - fairy = fairy or _ConnectionFairy(connection, connection_record) + fairy = fairy or _ConnectionFairy( + connection, connection_record, echo) assert fairy.connection is connection - fairy._reset(pool, echo) + fairy._reset(pool) # Immediately close detached instances if not connection_record: @@ -603,9 +605,10 @@ class _ConnectionFairy(object): """ - def __init__(self, dbapi_connection, connection_record): + def __init__(self, dbapi_connection, connection_record, echo): self.connection = dbapi_connection self._connection_record = connection_record + self._echo = echo connection = None """A reference to the actual DBAPI connection being tracked.""" @@ -642,7 +645,6 @@ class _ConnectionFairy(object): fairy._pool = pool fairy._counter = 0 - fairy._echo = pool._should_log_debug() if threadconns is not None: threadconns.current = weakref.ref(fairy) @@ -684,11 +686,11 @@ class _ConnectionFairy(object): _close = _checkin - def _reset(self, pool, echo): + def _reset(self, pool): if pool.dispatch.reset: pool.dispatch.reset(self, self._connection_record) if pool._reset_on_return is reset_rollback: - if echo: + if self._echo: pool.logger.debug("Connection %s rollback-on-return%s", self.connection, ", via agent" @@ -698,7 +700,7 @@ class _ConnectionFairy(object): else: pool._dialect.do_rollback(self) elif pool._reset_on_return is reset_commit: - if echo: + if self._echo: pool.logger.debug("Connection %s commit-on-return%s", self.connection, ", via agent" -- cgit v1.2.1 From 85e75ebcee15f216ace71628f1e491e36663d5c8 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 20 Aug 2014 14:24:45 -0400 Subject: - factor out determination of current version id out of _collect_update_commands and _collect_delete_commands --- lib/sqlalchemy/orm/persistence.py | 110 +++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 55 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 37b696d0f..511a9cef0 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -45,38 +45,26 @@ def save_obj(base_mapper, states, uowtransaction, single=False): cached_connections = _cached_connection_dict(base_mapper) for (state, dict_, mapper, connection, - has_identity, row_switch) in _organize_states_for_save( + has_identity, + row_switch, update_version_id) in _organize_states_for_save( base_mapper, states, uowtransaction ): if has_identity or row_switch: states_to_update.append( - (state, dict_, mapper, connection, - has_identity, row_switch) + (state, dict_, mapper, connection, update_version_id) ) else: states_to_insert.append( - (state, dict_, mapper, connection, - has_identity, row_switch) + (state, dict_, mapper, connection) ) for table, mapper in base_mapper._sorted_tables.items(): if table not in mapper._pks_by_table: continue - insert = ( - (state, state_dict, sub_mapper, connection) - for state, state_dict, sub_mapper, connection, has_identity, - row_switch in states_to_insert - if table in sub_mapper._pks_by_table - ) - insert = _collect_insert_commands(table, insert) + insert = _collect_insert_commands(table, states_to_insert) - update = ( - (state, state_dict, sub_mapper, connection, row_switch) - for state, state_dict, sub_mapper, connection, has_identity, - row_switch in states_to_update - if table in sub_mapper._pks_by_table - ) - update = _collect_update_commands(uowtransaction, table, update) + update = _collect_update_commands( + uowtransaction, table, states_to_update) _emit_update_statements(base_mapper, uowtransaction, cached_connections, @@ -89,9 +77,16 @@ def save_obj(base_mapper, states, uowtransaction, single=False): _finalize_insert_update_commands( base_mapper, uowtransaction, ( - (state, state_dict, mapper, connection, has_identity) - for state, state_dict, mapper, connection, has_identity, - row_switch in states_to_insert + states_to_update + (state, state_dict, mapper, connection, False) + for state, state_dict, mapper, connection in states_to_insert + ) + ) + _finalize_insert_update_commands( + base_mapper, uowtransaction, + ( + (state, state_dict, mapper, connection, True) + for state, state_dict, mapper, connection, + update_version_id in states_to_update ) ) @@ -149,21 +144,14 @@ def delete_obj(base_mapper, states, uowtransaction): if table not in mapper._pks_by_table: continue - delete = ( - (state, state_dict, sub_mapper, connection) - for state, state_dict, sub_mapper, has_identity, connection - in states_to_delete if table in sub_mapper._pks_by_table - and has_identity - ) - delete = _collect_delete_commands(base_mapper, uowtransaction, - table, delete) + table, states_to_delete) _emit_delete_statements(base_mapper, uowtransaction, cached_connections, mapper, table, delete) - for state, state_dict, mapper, has_identity, connection \ - in states_to_delete: + for state, state_dict, mapper, connection, \ + update_version_id in states_to_delete: mapper.dispatch.after_delete(mapper, connection, state) @@ -187,7 +175,7 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): instance_key = state.key or mapper._identity_key_from_state(state) - row_switch = None + row_switch = update_version_id = None # call before_XXX extensions if not has_identity: @@ -224,8 +212,14 @@ def _organize_states_for_save(base_mapper, states, uowtransaction): uowtransaction.remove_state_actions(existing) row_switch = existing + if (has_identity or row_switch) and mapper.version_id_col is not None: + update_version_id = mapper._get_committed_state_attr_by_column( + row_switch if row_switch else state, + row_switch.dict if row_switch else dict_, + mapper.version_id_col) + yield (state, dict_, mapper, connection, - has_identity, row_switch) + has_identity, row_switch, update_version_id) def _organize_states_for_post_update(base_mapper, states, @@ -255,7 +249,16 @@ def _organize_states_for_delete(base_mapper, states, uowtransaction): mapper.dispatch.before_delete(mapper, connection, state) - yield state, dict_, mapper, bool(state.key), connection + if mapper.version_id_col is not None: + update_version_id = \ + mapper._get_committed_state_attr_by_column( + state, dict_, + mapper.version_id_col) + else: + update_version_id = None + + yield ( + state, dict_, mapper, connection, update_version_id) def _collect_insert_commands(table, states_to_insert): @@ -264,8 +267,8 @@ def _collect_insert_commands(table, states_to_insert): """ for state, state_dict, mapper, connection in states_to_insert: - - # assert table in mapper._pks_by_table + if table not in mapper._pks_by_table: + continue params = {} value_params = {} @@ -318,9 +321,11 @@ def _collect_update_commands(uowtransaction, table, states_to_update): """ - for state, state_dict, mapper, connection, row_switch in states_to_update: + for state, state_dict, mapper, connection, \ + update_version_id in states_to_update: - # assert table in mapper._pks_by_table + if table not in mapper._pks_by_table: + continue pks = mapper._pks_by_table[table] @@ -340,17 +345,13 @@ def _collect_update_commands(uowtransaction, table, states_to_update): else: params[col.key] = value - if mapper.version_id_col is not None: + if update_version_id is not None: col = mapper.version_id_col - params[col._label] = \ - mapper._get_committed_state_attr_by_column( - row_switch if row_switch else state, - row_switch.dict if row_switch else state_dict, - col) + params[col._label] = update_version_id if col.key not in params and \ mapper.version_id_generator is not False: - val = mapper.version_id_generator(params[col._label]) + val = mapper.version_id_generator(update_version_id) params[col.key] = val if not (params or value_params): @@ -364,7 +365,8 @@ def _collect_update_commands(uowtransaction, table, states_to_update): if history.added: if not history.deleted or \ - ("pk_cascaded", state, col) in uowtransaction.attributes: + ("pk_cascaded", state, col) in \ + uowtransaction.attributes: pk_params[col._label] = history.added[0] params.pop(col.key, None) else: @@ -374,7 +376,6 @@ def _collect_update_commands(uowtransaction, table, states_to_update): else: pk_params[col._label] = history.unchanged[0] - if params or value_params: if None in pk_params.values(): raise orm_exc.FlushError( @@ -426,9 +427,11 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, """Identify values to use in DELETE statements for a list of states to be deleted.""" - for state, state_dict, mapper, connection in states_to_delete: + for state, state_dict, mapper, connection, \ + update_version_id in states_to_delete: - # assert table in mapper._pks_by_table + if table not in mapper._pks_by_table: + continue params = {} for col in mapper._pks_by_table[table]: @@ -442,12 +445,9 @@ def _collect_delete_commands(base_mapper, uowtransaction, table, "using NULL for primary " "key value") - if mapper.version_id_col is not None and \ + if update_version_id is not None and \ table.c.contains_column(mapper.version_id_col): - params[mapper.version_id_col.key] = \ - mapper._get_committed_state_attr_by_column( - state, state_dict, - mapper.version_id_col) + params[mapper.version_id_col.key] = update_version_id yield params, connection -- cgit v1.2.1 From 71ca494f518658676b532afaf84a4cc93025dbbb Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 20 Aug 2014 20:14:20 -0400 Subject: - The INSERT...FROM SELECT construct now implies ``inline=True`` on :class:`.Insert`. This helps to fix a bug where an INSERT...FROM SELECT construct would inadvertently be compiled as "implicit returning" on supporting backends, which would cause breakage in the case of an INSERT that inserts zero rows (as implicit returning expects a row), as well as arbitrary return data in the case of an INSERT that inserts multiple rows (e.g. only the first row of many). A similar change is also applied to an INSERT..VALUES with multiple parameter sets; implicit RETURNING will no longer emit for this statement either. As both of these constructs deal with varible numbers of rows, the :attr:`.ResultProxy.inserted_primary_key` accessor does not apply. Previously, there was a documentation note that one may prefer ``inline=True`` with INSERT..FROM SELECT as some databases don't support returning and therefore can't do "implicit" returning, but there's no reason an INSERT...FROM SELECT needs implicit returning in any case. Regular explicit :meth:`.Insert.returning` should be used to return variable numbers of result rows if inserted data is needed. fixes #3169 --- lib/sqlalchemy/sql/compiler.py | 4 +++- lib/sqlalchemy/sql/dml.py | 34 +++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index e45510aa4..fac4980b0 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1981,11 +1981,13 @@ class SQLCompiler(Compiled): need_pks = self.isinsert and \ not self.inline and \ - not stmt._returning + not stmt._returning and \ + not stmt._has_multi_parameters implicit_returning = need_pks and \ self.dialect.implicit_returning and \ stmt.table.implicit_returning + if self.isinsert: implicit_return_defaults = (implicit_returning and stmt._return_defaults) diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index f7e033d85..72dd92c99 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -269,6 +269,13 @@ class ValuesBase(UpdateBase): .. versionadded:: 0.8 Support for multiple-VALUES INSERT statements. + .. versionchanged:: 1.0.0 an INSERT that uses a multiple-VALUES + clause, even a list of length one, + implies that the :paramref:`.Insert.inline` flag is set to + True, indicating that the statement will not attempt to fetch + the "last inserted primary key" or other defaults. The statement + deals with an arbitrary number of rows, so the + :attr:`.ResultProxy.inserted_primary_key` accessor does not apply. .. seealso:: @@ -434,8 +441,13 @@ class Insert(ValuesBase): dynamically render the VALUES clause at execution time based on the parameters passed to :meth:`.Connection.execute`. - :param inline: if True, SQL defaults will be compiled 'inline' into - the statement and not pre-executed. + :param inline: if True, no attempt will be made to retrieve the + SQL-generated default values to be provided within the statement; + in particular, + this allows SQL expressions to be rendered 'inline' within the + statement without the need to pre-execute them beforehand; for + backends that support "returning", this turns off the "implicit + returning" feature for the statement. If both `values` and compile-time bind parameters are present, the compile-time bind parameters override the information specified @@ -495,17 +507,12 @@ class Insert(ValuesBase): would normally raise an exception if these column lists don't correspond. - .. note:: - - Depending on backend, it may be necessary for the :class:`.Insert` - statement to be constructed using the ``inline=True`` flag; this - flag will prevent the implicit usage of ``RETURNING`` when the - ``INSERT`` statement is rendered, which isn't supported on a - backend such as Oracle in conjunction with an ``INSERT..SELECT`` - combination:: - - sel = select([table1.c.a, table1.c.b]).where(table1.c.c > 5) - ins = table2.insert(inline=True).from_select(['a', 'b'], sel) + .. versionchanged:: 1.0.0 an INSERT that uses FROM SELECT + implies that the :paramref:`.Insert.inline` flag is set to + True, indicating that the statement will not attempt to fetch + the "last inserted primary key" or other defaults. The statement + deals with an arbitrary number of rows, so the + :attr:`.ResultProxy.inserted_primary_key` accessor does not apply. .. note:: @@ -525,6 +532,7 @@ class Insert(ValuesBase): self._process_colparams(dict((n, Null()) for n in names)) self.select_names = names + self.inline = True self.select = _interpret_as_select(select) def _copy_internals(self, clone=_clone, **kw): -- cgit v1.2.1 From 374173e89d4e21a75bfabd8a655d17c247b6f1fc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 21 Aug 2014 10:29:21 -0400 Subject: - fix link --- lib/sqlalchemy/sql/dml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 72dd92c99..06c50981d 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -508,7 +508,7 @@ class Insert(ValuesBase): correspond. .. versionchanged:: 1.0.0 an INSERT that uses FROM SELECT - implies that the :paramref:`.Insert.inline` flag is set to + implies that the :paramref:`.insert.inline` flag is set to True, indicating that the statement will not attempt to fetch the "last inserted primary key" or other defaults. The statement deals with an arbitrary number of rows, so the -- cgit v1.2.1 From 7c4d0a4d6611fd891fb7afda6db277e9fbf83e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20=C3=9E=C3=B3r=20Briem?= Date: Thu, 31 Jul 2014 23:23:56 +0000 Subject: Fix copy-paste error in Delete doc --- lib/sqlalchemy/sql/dml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 06c50981d..1934d0776 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -736,10 +736,10 @@ class Delete(UpdateBase): :meth:`~.TableClause.delete` method on :class:`~.schema.Table`. - :param table: The table to be updated. + :param table: The table to delete rows from. :param whereclause: A :class:`.ClauseElement` describing the ``WHERE`` - condition of the ``UPDATE`` statement. Note that the + condition of the ``DELETE`` statement. Note that the :meth:`~Delete.where()` generative method may be used instead. .. seealso:: -- cgit v1.2.1 From a12fcd1487f6ae210486fa4a015d9ea71e3bb7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20=C3=9E=C3=B3r=20Briem?= Date: Thu, 31 Jul 2014 23:26:18 +0000 Subject: Fix doc typo 'conjunection' --- lib/sqlalchemy/pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/pool.py b/lib/sqlalchemy/pool.py index 89cddfc31..bc9affe4a 100644 --- a/lib/sqlalchemy/pool.py +++ b/lib/sqlalchemy/pool.py @@ -305,7 +305,7 @@ class Pool(log.Identified): """Return a new :class:`.Pool`, of the same class as this one and configured with identical creation arguments. - This method is used in conjunection with :meth:`dispose` + This method is used in conjunction with :meth:`dispose` to close out an entire :class:`.Pool` and create a new one in its place. -- cgit v1.2.1 From b490534657229cbc44f1f5735a39539ceaf776a3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 23 Aug 2014 15:21:16 -0400 Subject: - pep8 formatting for pg table opts feature, tests - add support for PG INHERITS - fix mis-named tests - changelog fixes #2051 --- lib/sqlalchemy/dialects/postgresql/base.py | 83 +++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 24 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 34932520f..39de0cf92 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -417,22 +417,42 @@ of :class:`.PGInspector`, which offers additional methods:: .. autoclass:: PGInspector :members: -PostgreSQL specific table options ---------------------------------- +.. postgresql_table_options: + +PostgreSQL Table Options +------------------------- + +Several options for CREATE TABLE are supported directly by the PostgreSQL +dialect in conjunction with the :class:`.Table` construct: + +* ``TABLESPACE``:: + + Table("some_table", metadata, ..., postgresql_tablespace='some_tablespace') + +* ``ON COMMIT``:: + + Table("some_table", metadata, ..., postgresql_on_commit='PRESERVE ROWS') -PostgreSQL provides several CREATE TABLE specific options allowing to -specify how table data are stored. The following options are currently -supported: ``TABLESPACE``, ``ON COMMIT``, ``WITH OIDS``. +* ``WITH OIDS``:: -``postgresql_tablespace`` is probably the more common and allows to specify -where in the filesystem the data files for the table will be created (see -http://www.postgresql.org/docs/9.3/static/manage-ag-tablespaces.html) + Table("some_table", metadata, ..., postgresql_with_oids=True) + +* ``WITHOUT OIDS``:: + + Table("some_table", metadata, ..., postgresql_with_oids=False) + +* ``INHERITS``:: + + Table("some_table", metadata, ..., postgresql_inherits="some_supertable") + + Table("some_table", metadata, ..., postgresql_inherits=("t1", "t2", ...)) + +.. versionadded:: 1.0.0 .. seealso:: `Postgresql CREATE TABLE options - `_ - - on the PostgreSQL website + `_ """ from collections import defaultdict @@ -1466,19 +1486,33 @@ class PGDDLCompiler(compiler.DDLCompiler): def post_create_table(self, table): table_opts = [] - if table.dialect_options['postgresql']['with_oids'] is not None: - if table.dialect_options['postgresql']['with_oids']: - table_opts.append('WITH OIDS') - else: - table_opts.append('WITHOUT OIDS') - if table.dialect_options['postgresql']['on_commit']: - on_commit_options = table.dialect_options['postgresql']['on_commit'].replace("_", " ").upper() - table_opts.append('ON COMMIT %s' % on_commit_options) - if table.dialect_options['postgresql']['tablespace']: - tablespace_name = table.dialect_options['postgresql']['tablespace'] - table_opts.append('TABLESPACE %s' % self.preparer.quote(tablespace_name)) + pg_opts = table.dialect_options['postgresql'] + + inherits = pg_opts.get('inherits') + if inherits is not None: + if not isinstance(inherits, (list, tuple)): + inherits = (inherits, ) + table_opts.append( + '\n INHERITS ( ' + + ', '.join(self.preparer.quote(name) for name in inherits) + + ' )') + + if pg_opts['with_oids'] is True: + table_opts.append('\n WITH OIDS') + elif pg_opts['with_oids'] is False: + table_opts.append('\n WITHOUT OIDS') + + if pg_opts['on_commit']: + on_commit_options = pg_opts['on_commit'].replace("_", " ").upper() + table_opts.append('\n ON COMMIT %s' % on_commit_options) + + if pg_opts['tablespace']: + tablespace_name = pg_opts['tablespace'] + table_opts.append( + '\n TABLESPACE %s' % self.preparer.quote(tablespace_name) + ) - return ' '.join(table_opts) + return ''.join(table_opts) class PGTypeCompiler(compiler.GenericTypeCompiler): @@ -1741,8 +1775,9 @@ class PGDialect(default.DefaultDialect): (schema.Table, { "ignore_search_path": False, "tablespace": None, - "with_oids" : None, - "on_commit" : None, + "with_oids": None, + "on_commit": None, + "inherits": None }) ] -- cgit v1.2.1 From be2351481fdb83d1ed02a717ecc7741a19c73f62 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 25 Aug 2014 17:00:21 -0400 Subject: - The "resurrect" ORM event has been removed. This event hook had no purpose since the old "mutable attribute" system was removed in 0.8. fixes #3171 --- lib/sqlalchemy/orm/events.py | 12 ------------ lib/sqlalchemy/orm/mapper.py | 11 ----------- 2 files changed, 23 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index aa99673ba..8edaa2744 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -293,18 +293,6 @@ class InstanceEvents(event.Events): """ - def resurrect(self, target): - """Receive an object instance as it is 'resurrected' from - garbage collection, which occurs when a "dirty" state falls - out of scope. - - :param target: the mapped instance. If - the event is configured with ``raw=True``, this will - instead be the :class:`.InstanceState` state-management - object associated with the instance. - - """ - def pickle(self, target, state_dict): """Receive an object instance when its associated state is being pickled. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index f22cac329..aab28ee0c 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1127,7 +1127,6 @@ class Mapper(InspectionAttr): event.listen(manager, 'first_init', _event_on_first_init, raw=True) event.listen(manager, 'init', _event_on_init, raw=True) - event.listen(manager, 'resurrect', _event_on_resurrect, raw=True) for key, method in util.iterate_attributes(self.class_): if isinstance(method, types.FunctionType): @@ -2762,16 +2761,6 @@ def _event_on_init(state, args, kwargs): instrumenting_mapper._set_polymorphic_identity(state) -def _event_on_resurrect(state): - # re-populate the primary key elements - # of the dict based on the mapping. - instrumenting_mapper = state.manager.info.get(_INSTRUMENTOR) - if instrumenting_mapper: - for col, val in zip(instrumenting_mapper.primary_key, state.key[1]): - instrumenting_mapper._set_state_attr_by_column( - state, state.dict, col, val) - - class _ColumnMapping(dict): """Error reporting helper for mapper._columntoproperty.""" -- cgit v1.2.1 From a16ee423e4528bd7a6ba6375cccd88b7450c58d3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 25 Aug 2014 17:06:28 -0400 Subject: - mention that FOUND_ROWS is hardcoded; fixes #3146 --- lib/sqlalchemy/dialects/mysql/base.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 374960765..012d178e7 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -190,15 +190,13 @@ SQLAlchemy standardizes the DBAPI ``cursor.rowcount`` attribute to be the usual definition of "number of rows matched by an UPDATE or DELETE" statement. This is in contradiction to the default setting on most MySQL DBAPI drivers, which is "number of rows actually modified/deleted". For this reason, the -SQLAlchemy MySQL dialects always set the ``constants.CLIENT.FOUND_ROWS`` flag, -or whatever is equivalent for the DBAPI in use, on connect, unless the flag -value is overridden using DBAPI-specific options -(such as ``client_flag`` for the MySQL-Python driver, ``found_rows`` for the -OurSQL driver). +SQLAlchemy MySQL dialects always add the ``constants.CLIENT.FOUND_ROWS`` +flag, or whatever is equivalent for the target dialect, upon connection. +This setting is currently hardcoded. -See also: +.. seealso:: -:attr:`.ResultProxy.rowcount` + :attr:`.ResultProxy.rowcount` CAST Support -- cgit v1.2.1