diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-06-21 12:21:21 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-06-23 10:41:39 -0400 |
| commit | 62be25cdfaab377319602a1852a1fddcbf6acd45 (patch) | |
| tree | 803838d317de872ba941264f8ae64e2d4dadc9ae /test/ext | |
| parent | 56e817bb0ef4eaca189b42b930a6e99ee4ed0671 (diff) | |
| download | sqlalchemy-62be25cdfaab377319602a1852a1fddcbf6acd45.tar.gz | |
Propose using RETURNING for bulk updates, deletes
This patch makes several improvements in the area of
bulk updates and deletes as well as the new session mechanics.
RETURNING is now used for an UPDATE or DELETE statement
emitted for a diaelct that supports "full returning"
in order to satisfy the "fetch" strategy; this currently
includes PostgreSQL and SQL Server. The Oracle dialect
does not support RETURNING for more than one row,
so a new dialect capability "full_returning" is added
in addition to the existing "implicit_returning", indicating
this dialect supports RETURNING for zero or more rows,
not just a single identity row.
The "fetch" strategy will gracefully degrade to
the previous SELECT mechanics for dialects that do not
support RETURNING.
Additionally, the "fetch" strategy will attempt to use
evaluation for the VALUES that were UPDATEd, rather
than just expiring the updated attributes. Values should
be evalutable in all cases where the value is not
a SQL expression.
The new approach also incurs some changes in the
session.execute mechanics, where do_orm_execute() event
handlers can now be chained to each return results;
this is in turn used by the handler to detect on a
per-bind basis if the fetch strategy needs to
do a SELECT or if it can do RETURNING. A test suite is
added to test_horizontal_shard that breaks up a single
UPDATE or DELETE operation among multiple backends
where some are SQLite and don't support RETURNING and
others are PostgreSQL and do.
The session event mechanics are corrected
in terms of the "orm pre execute" hook, which now
receives a flag "is_reentrant" so that the two
ORM implementations for this can skip on their work
if they are being called inside of ORMExecuteState.invoke(),
where previously bulk update/delete were calling its
SELECT a second time.
In order for "fetch" to get the correct identity when
called as pre-execute, it also requests the identity_token
for each mapped instance which is now added as an optional
capability of a SELECT for ORM columns. the identity_token
that's placed by horizontal_sharding is now made available
within each result row, so that even when fetching a
merged result of plain rows we can tell which row belongs
to which identity token.
The evaluator that takes place within the ORM bulk update and delete for
synchronize_session="evaluate" now supports the IN and NOT IN operators.
Tuple IN is also supported.
Fixes: #1653
Change-Id: I2292b56ae004b997cef0ba4d3fc350ae1dd5efc1
Diffstat (limited to 'test/ext')
| -rw-r--r-- | test/ext/test_horizontal_shard.py | 62 |
1 files changed, 52 insertions, 10 deletions
diff --git a/test/ext/test_horizontal_shard.py b/test/ext/test_horizontal_shard.py index c0029fbb6..9855cd5ab 100644 --- a/test/ext/test_horizontal_shard.py +++ b/test/ext/test_horizontal_shard.py @@ -35,8 +35,6 @@ from sqlalchemy.testing import provision from sqlalchemy.testing.engines import testing_engine from sqlalchemy.testing.engines import testing_reaper -# TODO: ShardTest can be turned into a base for further subclasses - class ShardTest(object): __skip_if__ = (lambda: util.win32,) @@ -47,9 +45,9 @@ class ShardTest(object): def setUp(self): global db1, db2, db3, db4, weather_locations, weather_reports - db1, db2, db3, db4 = self._init_dbs() + db1, db2, db3, db4 = self._dbs = self._init_dbs() - meta = MetaData() + meta = self.metadata = MetaData() ids = Table("ids", meta, Column("nextid", Integer, nullable=False)) def id_generator(ctx): @@ -578,9 +576,11 @@ class ShardTest(object): temps = sess.execute(future_select(Report)).scalars().all() eq_(set(t.temperature for t in temps), {80.0, 75.0, 85.0}) + # MARKMARK + # omitting the criteria so that the UPDATE affects three out of + # four shards sess.execute( update(Report) - .filter(Report.temperature >= 80) .values({"temperature": Report.temperature + 6},) .execution_options(synchronize_session="fetch") ) @@ -590,11 +590,11 @@ class ShardTest(object): row.temperature for row in sess.execute(future_select(Report.temperature)) ), - {86.0, 75.0, 91.0}, + {86.0, 81.0, 91.0}, ) # test synchronize session as well - eq_(set(t.temperature for t in temps), {86.0, 75.0, 91.0}) + eq_(set(t.temperature for t in temps), {86.0, 81.0, 91.0}) def test_bulk_delete_future_synchronize_evaluate(self): sess = self._fixture_data() @@ -711,9 +711,8 @@ class TableNameConventionShardTest(ShardTest, fixtures.TestBase): This used to be called "AttachedFileShardTest" but I didn't see any ATTACH going on. - The approach taken by this test is awkward and I wouldn't recommend using - this pattern in a real situation. I'm not sure of the history of this test - but it likely predates when we knew how to use real ATTACH in SQLite. + A more modern approach here would be to use the schema_translate_map + option. """ @@ -742,6 +741,49 @@ class TableNameConventionShardTest(ShardTest, fixtures.TestBase): return db1, db2, db3, db4 +class MultipleDialectShardTest(ShardTest, fixtures.TestBase): + __only_on__ = "postgresql" + + schema = "changeme" + + def _init_dbs(self): + e1 = testing_engine("sqlite://") + with e1.connect() as conn: + for i in [1, 3]: + conn.exec_driver_sql( + 'ATTACH DATABASE "shard%s_%s.db" AS shard%s' + % (i, provision.FOLLOWER_IDENT, i) + ) + + e2 = testing_engine() + with e2.connect() as conn: + for i in [2, 4]: + conn.exec_driver_sql( + "CREATE SCHEMA IF NOT EXISTS shard%s" % (i,) + ) + + db1 = e1.execution_options(schema_translate_map={"changeme": "shard1"}) + db2 = e2.execution_options(schema_translate_map={"changeme": "shard2"}) + db3 = e1.execution_options(schema_translate_map={"changeme": "shard3"}) + db4 = e2.execution_options(schema_translate_map={"changeme": "shard4"}) + + self.sqlite_engine = e1 + self.postgresql_engine = e2 + return db1, db2, db3, db4 + + def teardown(self): + clear_mappers() + + self.sqlite_engine.connect().invalidate() + for i in [1, 3]: + os.remove("shard%d_%s.db" % (i, provision.FOLLOWER_IDENT)) + + with self.postgresql_engine.connect() as conn: + self.metadata.drop_all(conn) + for i in [2, 4]: + conn.exec_driver_sql("DROP SCHEMA shard%s CASCADE" % (i,)) + + class SelectinloadRegressionTest(fixtures.DeclarativeMappedTest): """test #4175 """ |
