--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 10279
+
+ Adjusted the ORM's interpretation of UPDATE/DELETE targets to not interfere
+ with the target table passed to the statement, such as for
+ :class:`_orm.aliased` constructs. Cases like ORM session synchonize using
+ "SELECT" statements such as with MySQL/ MariaDB will still have issues with
+ UPDATE/DELETE of this form so it's best to disable synchonize_session when
+ using DML statements of this type.
self._resolved_values = dict(self._resolved_values)
new_stmt = statement._clone()
- new_stmt.table = mapper.local_table
# note if the statement has _multi_values, these
# are passed through to the new statement, which will then raise
# over and over again. so perhaps if it could be RETURNING just
# the elements that were based on a SQL expression and not
# a constant. For now it doesn't quite seem worth it
- new_stmt = new_stmt.return_defaults(
- *(list(mapper.local_table.primary_key))
- )
+ new_stmt = new_stmt.return_defaults(*new_stmt.table.primary_key)
if toplevel:
new_stmt = self._setup_orm_returning(
)
new_stmt = statement._clone()
- new_stmt.table = mapper.local_table
new_crit = cls._adjust_for_extra_criteria(
self.global_attributes, mapper
compiler_implicit_returning = compiler.implicit_returning
+ # TODO - see TODO(return_defaults_columns) below
+ # cols_in_params = set()
+
for c in cols:
# scan through every column in the target table
kw,
)
+ # TODO - see TODO(return_defaults_columns) below
+ # cols_in_params.add(c)
+
elif isinsert:
# no parameter is present and it's an insert.
if c in remaining_supplemental
)
+ # TODO(return_defaults_columns): there can still be more columns in
+ # _return_defaults_columns in the case that they are from something like an
+ # aliased of the table. we can add them here, however this breaks other ORM
+ # things. so this is for another day. see
+ # test/orm/dml/test_update_delete_where.py -> test_update_from_alias
+
+ # if stmt._return_defaults_columns:
+ # compiler_implicit_returning.extend(
+ # set(stmt._return_defaults_columns)
+ # .difference(compiler_implicit_returning)
+ # .difference(cols_in_params)
+ # )
+
return (use_insertmanyvalues, use_sentinel_columns)
asyncmy_fallback = mysql+asyncmy://scott:tiger@127.0.0.1:3306/test?charset=utf8mb4&async_fallback=true
mariadb = mariadb+mysqldb://scott:tiger@127.0.0.1:3306/test
mariadb_connector = mariadb+mariadbconnector://scott:tiger@127.0.0.1:3306/test
-mssql = mssql+pyodbc://scott:tiger^5HHH@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server
+mssql = mssql+pyodbc://scott:tiger^5HHH@mssql2017:1433/test?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes
pymssql = mssql+pymssql://scott:tiger^5HHH@mssql2017:1433/test
docker_mssql = mssql+pyodbc://scott:tiger^5HHH@127.0.0.1:1433/test?driver=ODBC+Driver+17+for+SQL+Server
oracle = oracle+cx_oracle://scott:tiger@oracle18c/xe
from sqlalchemy import text
from sqlalchemy import update
from sqlalchemy import values
+from sqlalchemy.orm import aliased
from sqlalchemy.orm import backref
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy.orm import immediateload
sess = fixture_session()
john, jack, jill, jane = sess.query(User).order_by(User.id).all()
+
sess.query(User).filter(User.age > 29).update(
{"age": User.age - 10}, synchronize_session="evaluate"
)
properties={"user": relationship(User, backref="documents")},
)
+ @testing.requires.update_from_using_alias
+ @testing.combinations(
+ False,
+ ("fetch", testing.requires.update_returning),
+ ("auto", testing.requires.update_returning),
+ argnames="synchronize_session",
+ )
+ def test_update_from_alias(self, synchronize_session):
+ Document = self.classes.Document
+ s = fixture_session()
+
+ d1 = aliased(Document)
+
+ with self.sql_execution_asserter() as asserter:
+ s.execute(
+ update(d1).where(d1.title == "baz").values(flag=True),
+ execution_options={"synchronize_session": synchronize_session},
+ )
+
+ if True:
+ # TODO: see note in crud.py line 770. RETURNING should be here
+ # if synchronize_session="fetch" however there are more issues
+ # with this.
+ # if synchronize_session is False:
+ asserter.assert_(
+ CompiledSQL(
+ "UPDATE documents AS documents_1 SET flag=:flag "
+ "WHERE documents_1.title = :title_1",
+ [{"flag": True, "title_1": "baz"}],
+ )
+ )
+ else:
+ asserter.assert_(
+ CompiledSQL(
+ "UPDATE documents AS documents_1 SET flag=:flag "
+ "WHERE documents_1.title = :title_1 "
+ "RETURNING documents_1.id",
+ [{"flag": True, "title_1": "baz"}],
+ )
+ )
+
+ @testing.requires.delete_using_alias
+ @testing.combinations(
+ False,
+ ("fetch", testing.requires.delete_returning),
+ ("auto", testing.requires.delete_returning),
+ argnames="synchronize_session",
+ )
+ def test_delete_using_alias(self, synchronize_session):
+ Document = self.classes.Document
+ s = fixture_session()
+
+ d1 = aliased(Document)
+
+ with self.sql_execution_asserter() as asserter:
+ s.execute(
+ delete(d1).where(d1.title == "baz"),
+ execution_options={"synchronize_session": synchronize_session},
+ )
+
+ if synchronize_session is False:
+ asserter.assert_(
+ CompiledSQL(
+ "DELETE FROM documents AS documents_1 "
+ "WHERE documents_1.title = :title_1",
+ [{"title_1": "baz"}],
+ )
+ )
+ else:
+ asserter.assert_(
+ CompiledSQL(
+ "DELETE FROM documents AS documents_1 "
+ "WHERE documents_1.title = :title_1 "
+ "RETURNING documents_1.id",
+ [{"title_1": "baz"}],
+ )
+ )
+
@testing.requires.update_from
def test_update_from_joined_subq_test(self):
Document = self.classes.Document
class DMLTest(QueryTest, AssertsCompiledSQL):
- __dialect__ = "default"
+ __dialect__ = "default_enhanced"
@testing.variation("stmt_type", ["update", "delete"])
def test_dml_ctes(self, stmt_type: testing.Variation):
else:
stmt_type.fail()
+ @testing.variation("stmt_type", ["core", "orm"])
+ def test_aliased_update(self, stmt_type: testing.Variation):
+ """test #10279"""
+ if stmt_type.orm:
+ User = self.classes.User
+ u1 = aliased(User)
+ stmt = update(u1).where(u1.name == "xyz").values(name="newname")
+ elif stmt_type.core:
+ user_table = self.tables.users
+ u1 = user_table.alias()
+ stmt = update(u1).where(u1.c.name == "xyz").values(name="newname")
+ else:
+ stmt_type.fail()
+
+ self.assert_compile(
+ stmt,
+ "UPDATE users AS users_1 SET name=:name "
+ "WHERE users_1.name = :name_1",
+ )
+
+ @testing.variation("stmt_type", ["core", "orm"])
+ def test_aliased_delete(self, stmt_type: testing.Variation):
+ """test #10279"""
+ if stmt_type.orm:
+ User = self.classes.User
+ u1 = aliased(User)
+ stmt = delete(u1).where(u1.name == "xyz")
+ elif stmt_type.core:
+ user_table = self.tables.users
+ u1 = user_table.alias()
+ stmt = delete(u1).where(u1.c.name == "xyz")
+ else:
+ stmt_type.fail()
+
+ self.assert_compile(
+ stmt,
+ "DELETE FROM users AS users_1 " "WHERE users_1.name = :name_1",
+ )
+
@testing.variation("stmt_type", ["core", "orm"])
def test_add_cte(self, stmt_type: testing.Variation):
"""test #10167"""
"Backend does not support UPDATE..FROM",
)
+ @property
+ def update_from_using_alias(self):
+ """Target must support UPDATE..FROM syntax against an alias"""
+
+ return skip_if(
+ ["oracle", "sqlite<3.33.0", "mssql"],
+ "Backend does not support UPDATE..FROM with an alias",
+ )
+
@property
def delete_using(self):
"""Target must support DELETE FROM..FROM or DELETE..USING syntax"""
"Backend does not support DELETE..USING or equivalent",
)
+ @property
+ def delete_using_alias(self):
+ """Target must support DELETE FROM against an alias"""
+ return only_on(
+ ["postgresql", "sqlite"],
+ "Backend does not support DELETE..USING/FROM with an alias",
+ )
+
@property
def update_where_target_in_subquery(self):
"""Target must support UPDATE (or DELETE) where the same table is