summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-03-28 16:32:11 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2014-03-28 16:32:11 -0400
commitc01558ae7f4af08acc523786e107ea5e2e214184 (patch)
treea235ab825660b00c674f496354b364f58f8b464a /lib/sqlalchemy
parent9cdbed37f8c420db0b42fb959813d079622c3f3a (diff)
downloadsqlalchemy-c01558ae7f4af08acc523786e107ea5e2e214184.tar.gz
- Fixed ORM bug where changing the primary key of an object, then marking
it for DELETE would fail to target the correct row for DELETE. Then to compound matters, basic "number of rows matched" checks were not being performed. Both issues are fixed, however note that the "rows matched" check requires so-called "sane multi-row count" functionality; the DBAPI's executemany() method must count up the rows matched by individual statements and SQLAlchemy's dialect must mark this feature as supported, currently applies to some mysql dialects, psycopg2, sqlite only. fixes #3006 - Enabled "sane multi-row count" checking for the psycopg2 DBAPI, as this seems to be supported as of psycopg2 2.0.9.
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/dialects/postgresql/psycopg2.py5
-rw-r--r--lib/sqlalchemy/engine/default.py2
-rw-r--r--lib/sqlalchemy/ext/orderinglist.py23
-rw-r--r--lib/sqlalchemy/orm/persistence.py26
-rw-r--r--lib/sqlalchemy/testing/exclusions.py3
-rw-r--r--lib/sqlalchemy/testing/requirements.py6
6 files changed, 51 insertions, 14 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py
index 099ddf03d..ac1770625 100644
--- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py
+++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py
@@ -347,7 +347,7 @@ class PGDialect_psycopg2(PGDialect):
supports_unicode_statements = False
default_paramstyle = 'pyformat'
- supports_sane_multi_rowcount = False
+ supports_sane_multi_rowcount = False # set to true based on psycopg2 version
execution_ctx_cls = PGExecutionContext_psycopg2
statement_compiler = PGCompiler_psycopg2
preparer = PGIdentifierPreparer_psycopg2
@@ -393,6 +393,9 @@ class PGDialect_psycopg2(PGDialect):
is not None
self._has_native_json = self.psycopg2_version >= (2, 5)
+ # http://initd.org/psycopg/docs/news.html#what-s-new-in-psycopg-2-0-9
+ self.supports_sane_multi_rowcount = self.psycopg2_version >= (2, 0, 9)
+
@classmethod
def dbapi(cls):
import psycopg2
diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py
index 0fd41105c..368a08ec2 100644
--- a/lib/sqlalchemy/engine/default.py
+++ b/lib/sqlalchemy/engine/default.py
@@ -514,6 +514,8 @@ class DefaultExecutionContext(interfaces.ExecutionContext):
self.compiled = compiled
if not compiled.can_execute:
+ import pdb
+ pdb.set_trace()
raise exc.ArgumentError("Not an executable clause")
self.execution_options = compiled.statement._execution_options
diff --git a/lib/sqlalchemy/ext/orderinglist.py b/lib/sqlalchemy/ext/orderinglist.py
index 9310c6071..16bab6b29 100644
--- a/lib/sqlalchemy/ext/orderinglist.py
+++ b/lib/sqlalchemy/ext/orderinglist.py
@@ -91,8 +91,27 @@ attribute, so that the ordering is correct when first loaded.
.. warning::
:class:`.OrderingList` only provides limited functionality when a primary
- key column or unique column is the target of the sort. Since changing the
- order of entries often means that two rows must trade values, this is not
+ key column or unique column is the target of the sort. The two operations
+ that are unsupported or are problematic are:
+
+ * two entries must trade values. This is not supported directly in the
+ case of a primary key or unique constraint because it means at least
+ one row would need to be temporarily removed first, or changed to
+ a third, neutral value while the switch occurs.
+
+ * an entry must be deleted in order to make room for a new entry. SQLAlchemy's
+ unit of work performs all INSERTs before DELETEs within a single flush
+ by default. A future feature will allow this to be configurable for
+ specific sets of columns on mappings.
+
+ Additional issues when using primary keys as ordering keys are that UPDATE
+ or DELETE statements on target rows may fail to fire correctly as orderinglist
+ may change their primary key value
+
+
+ Since changing the
+ order of entries often means that either rows must trade values,
+ or rows must be deleted to make way for new inserts, this is not
possible when the value is constrained by a primary key or unique
constraint, since one of the rows would temporarily have to point to a
third available value so that the other row could take its old
diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
index 35631988b..b4c9c052b 100644
--- a/lib/sqlalchemy/orm/persistence.py
+++ b/lib/sqlalchemy/orm/persistence.py
@@ -450,7 +450,7 @@ def _collect_delete_commands(base_mapper, uowtransaction, table,
for col in mapper._pks_by_table[table]:
params[col.key] = \
value = \
- mapper._get_state_attr_by_column(
+ mapper._get_committed_state_attr_by_column(
state, state_dict, col)
if value is None:
raise orm_exc.FlushError(
@@ -679,21 +679,19 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections,
connection = cached_connections[connection]
- if need_version_id:
- # TODO: need test coverage for this [ticket:1761]
+ expected = len(del_objects)
+ rows_matched = -1
+ if connection.dialect.supports_sane_multi_rowcount:
+ c = connection.execute(statement, del_objects)
+ rows_matched = c.rowcount
+ elif need_version_id:
if connection.dialect.supports_sane_rowcount:
- rows = 0
+ rows_matched = 0
# execute deletes individually so that versioned
# rows can be verified
for params in del_objects:
c = connection.execute(statement, params)
- rows += c.rowcount
- if rows != len(del_objects):
- raise orm_exc.StaleDataError(
- "DELETE statement on table '%s' expected to "
- "delete %d row(s); %d were matched." %
- (table.description, len(del_objects), c.rowcount)
- )
+ rows_matched += c.rowcount
else:
util.warn(
"Dialect %s does not support deleted rowcount "
@@ -704,6 +702,12 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections,
else:
connection.execute(statement, del_objects)
+ if rows_matched > -1 and expected != rows_matched:
+ raise orm_exc.StaleDataError(
+ "DELETE statement on table '%s' expected to "
+ "delete %d row(s); %d were matched." %
+ (table.description, expected, rows_matched)
+ )
def _finalize_insert_update_commands(base_mapper, uowtransaction,
states_to_insert, states_to_update):
diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py
index 00bb69cbc..00ca28428 100644
--- a/lib/sqlalchemy/testing/exclusions.py
+++ b/lib/sqlalchemy/testing/exclusions.py
@@ -74,6 +74,9 @@ class skip_if(object):
self._fails_on = skip_if(other, reason)
return self
+ def fails_on_everything_except(self, *dbs):
+ self._fails_on = skip_if(fails_on_everything_except(*dbs))
+ return self
class fails_if(skip_if):
def __call__(self, fn):
diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py
index 76e48f8c8..07b5697e2 100644
--- a/lib/sqlalchemy/testing/requirements.py
+++ b/lib/sqlalchemy/testing/requirements.py
@@ -48,6 +48,12 @@ class SuiteRequirements(Requirements):
return exclusions.open()
@property
+ def non_updating_cascade(self):
+ """target database must *not* support ON UPDATE..CASCADE behavior in
+ foreign keys."""
+ return exclusions.closed()
+
+ @property
def deferrable_fks(self):
return exclusions.closed()