--- /dev/null
+.. change::
+ :tags: bug, mssql, orm
+ :tickets: 4062
+
+ Added a new class of "rowcount support" for dialects that is specific to
+ when "RETURNING", which on SQL Server looks like "OUTPUT inserted", is in
+ use, as the PyODBC backend isn't able to give us rowcount on an UPDATE or
+ DELETE statement when OUTPUT is in effect. This primarily affects the ORM
+ when a flush is updating a row that contains server-calcluated values,
+ raising an error if the backend does not return the expected row count.
+ PyODBC now states that it supports rowcount except if OUTPUT.inserted is
+ present, which is taken into account by the ORM during a flush as to
+ whether it will look for a rowcount.
class PyODBCConnector(Connector):
driver = 'pyodbc'
+ supports_sane_rowcount_returning = False
supports_sane_multi_rowcount = False
if util.py2k:
def dialect_description(self):
return self.name + "+" + self.driver
+ @property
+ def supports_sane_rowcount_returning(self):
+ return self.supports_sane_rowcount
+
@classmethod
def get_pool_class(cls, url):
return getattr(cls, 'poolclass', pool.QueuePool)
records = list(records)
statement = cached_stmt
-
- # TODO: would be super-nice to not have to determine this boolean
- # inside the loop here, in the 99.9999% of the time there's only
- # one connection in use
- assert_singlerow = connection.dialect.supports_sane_rowcount
- assert_multirow = assert_singlerow and \
- connection.dialect.supports_sane_multi_rowcount
- allow_multirow = has_all_defaults and not needs_version_id
+ return_defaults = False
if not has_all_pks:
statement = statement.return_defaults()
+ return_defaults = True
elif bookkeeping and not has_all_defaults and \
mapper.base_mapper.eager_defaults:
statement = statement.return_defaults()
+ return_defaults = True
elif mapper.version_id_col is not None:
statement = statement.return_defaults(mapper.version_id_col)
+ return_defaults = True
+
+ assert_singlerow = (
+ connection.dialect.supports_sane_rowcount
+ if not return_defaults
+ else connection.dialect.supports_sane_rowcount_returning
+ )
+
+ assert_multirow = assert_singlerow and \
+ connection.dialect.supports_sane_multi_rowcount
+ allow_multirow = has_all_defaults and not needs_version_id
if hasvalue:
for state, state_dict, params, mapper, \
c.context.compiled_parameters[0],
value_params)
rows += c.rowcount
- check_rowcount = True
+ check_rowcount = assert_singlerow
else:
if not allow_multirow:
check_rowcount = assert_singlerow
return exclusions.open()
+
+ @property
+ def sane_rowcount(self):
+ return exclusions.skip_if(
+ lambda config: not config.db.dialect.supports_sane_rowcount,
+ "driver doesn't support 'sane' rowcount"
+ )
+
+ @property
+ def sane_multi_rowcount(self):
+ return exclusions.fails_if(
+ lambda config: not config.db.dialect.supports_sane_multi_rowcount,
+ "driver %(driver)s %(doesnt_support)s 'sane' multi row count"
+ )
+
+ @property
+ def sane_rowcount_w_returning(self):
+ return exclusions.fails_if(
+ lambda config:
+ not config.db.dialect.supports_sane_rowcount_returning,
+ "driver doesn't support 'sane' rowcount when returning is on"
+ )
+
@property
def empty_inserts(self):
"""target platform supports INSERT with no values, i.e.
exclude('mysql', '<', (4, 1, 1), 'no unicode connection support'),
])
- @property
- def sane_rowcount(self):
- return skip_if(
- lambda config: not config.db.dialect.supports_sane_rowcount,
- "driver doesn't support 'sane' rowcount"
- )
-
@property
def emulated_lastrowid(self):
""""target dialect retrieves cursor.lastrowid or an equivalent
'sqlite+pysqlite',
'sqlite+pysqlcipher')
- @property
- def sane_multi_rowcount(self):
- return fails_if(
- lambda config: not config.db.dialect.supports_sane_multi_rowcount,
- "driver %(driver)s %(doesnt_support)s 'sane' multi row count"
- )
-
@property
def nullsordering(self):
"""Target backends that support nulls ordering."""
r = employees_table.update(department == 'C').execute(department='C')
assert r.rowcount == 3
+ @testing.requires.sane_rowcount_w_returning
+ def test_update_rowcount_return_defaults(self):
+ department = employees_table.c.department
+ stmt = employees_table.update(department == 'C').values(
+ name=employees_table.c.department + 'Z').return_defaults()
+
+ r = stmt.execute()
+ assert r.rowcount == 3
+
def test_raw_sql_rowcount(self):
# test issue #3622, make sure eager rowcount is called for text
with testing.db.connect() as conn:
eq_(
r.rowcount, 2
)
+