From 88028954576a8d7e772160d74b14da62c0b4fd43 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 26 Nov 2025 01:36:57 -0500 Subject: [PATCH] stop using MyISAM; more oracle struggles getting some fails on mariadb12 and likely 11 which appear to be related to calling in MyISAM, which is not used in modern mysql/mariadb. see if we can just remove this whole thing and rely on default engines for mariadb/mysql. this change also removes the "ignore errors" part of the run deletes for the TablesTest fixture, which was resulting in compound failures, and apparently a lot of tests were relying on it skipping nonexistent tables. rather than check for that we should just improve the tests and probably increase use of pytest style fixtures overall. this change also identifies and fixes that memusage_w_backend tests were running for all backends with a tag like py314_mysql_backendonly; the memusage tests should basically never be run as part of the whole suite since they are entirely unreliable within a full scale test run. dialect suite tests are also further broken out into those where every driver should be exercised (i.e. __backend__, for tests that test datatypes going out and coming back from the database as well as identity/autoincrement kinds of tests) vs. those where only one driver per backend is needed (i.e. __sparse_driver_backend__, for tests like reflection, DDL, CTEs, etc.). we are also trying to get a --low-connections option that actually works. changed this so that the testing reaper aggressively disposes the "global" engines (one per backend / driver) after test classes are done and before any testing_engine() call. This definitely works, however some monitoring with PG shows the number of connections still has brief bursts for some reason. it should be much more effective than before though as oracle 23/26 really does not handle more than a few connections. this change reverts oracle to oracle18c for now in setup.cfg; further work will be needed to determine if oracle23c can be run with this test suite Change-Id: Id87d0ea15155c452615a7edeb9d434c8e55151e7 --- lib/sqlalchemy/dialects/mysql/provision.py | 10 + lib/sqlalchemy/dialects/oracle/provision.py | 3 +- lib/sqlalchemy/testing/engines.py | 18 ++ lib/sqlalchemy/testing/fixtures/sql.py | 31 +-- lib/sqlalchemy/testing/plugin/plugin_base.py | 2 +- lib/sqlalchemy/testing/provision.py | 37 ++++ lib/sqlalchemy/testing/requirements.py | 10 +- lib/sqlalchemy/testing/schema.py | 30 +-- lib/sqlalchemy/testing/suite/test_cte.py | 2 +- lib/sqlalchemy/testing/suite/test_ddl.py | 4 +- .../testing/suite/test_reflection.py | 24 ++- lib/sqlalchemy/testing/suite/test_select.py | 24 +-- lib/sqlalchemy/testing/suite/test_sequence.py | 8 +- .../testing/suite/test_table_via_select.py | 2 +- .../testing/suite/test_update_delete.py | 2 +- noxfile.py | 10 +- setup.cfg | 8 +- test/orm/inheritance/test_basic.py | 190 +++++++++--------- test/orm/inheritance/test_relationship.py | 4 +- test/orm/test_versioning.py | 2 + 20 files changed, 227 insertions(+), 194 deletions(-) diff --git a/lib/sqlalchemy/dialects/mysql/provision.py b/lib/sqlalchemy/dialects/mysql/provision.py index 242adbf82f..4559dfc77a 100644 --- a/lib/sqlalchemy/dialects/mysql/provision.py +++ b/lib/sqlalchemy/dialects/mysql/provision.py @@ -12,6 +12,7 @@ from ... import exc from ...testing.provision import allow_stale_update_impl from ...testing.provision import configure_follower from ...testing.provision import create_db +from ...testing.provision import delete_from_all_tables from ...testing.provision import drop_db from ...testing.provision import generate_driver_url from ...testing.provision import temp_table_keyword_args @@ -117,6 +118,15 @@ def _upsert( return stmt +@delete_from_all_tables.for_db("mysql", "mariadb") +def _delete_from_all_tables(cfg, connection, metadata): + connection.exec_driver_sql("SET foreign_key_checks = 0") + try: + delete_from_all_tables.call_original(cfg, connection, metadata) + finally: + connection.exec_driver_sql("SET foreign_key_checks = 1") + + @allow_stale_update_impl.for_db("mariadb") def _allow_stale_update_impl(cfg): @contextlib.contextmanager diff --git a/lib/sqlalchemy/dialects/oracle/provision.py b/lib/sqlalchemy/dialects/oracle/provision.py index 3587de9d01..76d13c53bf 100644 --- a/lib/sqlalchemy/dialects/oracle/provision.py +++ b/lib/sqlalchemy/dialects/oracle/provision.py @@ -106,10 +106,11 @@ def _ora_stop_test_class_outside_fixtures(config, db, cls): # clear statement cache on all connections that were used # https://github.com/oracle/python-cx_Oracle/issues/519 + all_dbapis = {cfg.db.dialect.dbapi for cfg in config.Config.all_configs()} for cx_oracle_conn in _all_conns: try: sc = cx_oracle_conn.stmtcachesize - except db.dialect.dbapi.InterfaceError: + except tuple(dbapi.InterfaceError for dbapi in all_dbapis): # connection closed pass else: diff --git a/lib/sqlalchemy/testing/engines.py b/lib/sqlalchemy/testing/engines.py index fdb6d18b13..2dfa07222f 100644 --- a/lib/sqlalchemy/testing/engines.py +++ b/lib/sqlalchemy/testing/engines.py @@ -115,8 +115,17 @@ class ConnectionKiller: await_(rec.dispose()) else: rec.dispose() + eng.clear() + def _dispose_testing_engines(self, scope): + eng = self.testing_engines[scope] + for rec in list(eng): + if hasattr(rec, "sync_engine"): + await_(rec.dispose()) + else: + rec.dispose() + def after_test(self): self._drop_testing_engines("function") @@ -155,6 +164,10 @@ class ConnectionKiller: assert ( False ), "%d connection recs not cleared after test suite" % (ln) + if config.options and config.options.low_connections: + # for suites running with --low-connections, dispose the "global" + # engines to disconnect everything before making a testing engine + self._dispose_testing_engines("global") def final_cleanup(self): self.checkin_all() @@ -309,6 +322,7 @@ def testing_engine( share_pool=False, _sqlite_savepoint=False, ): + if asyncio: assert not _sqlite_savepoint from sqlalchemy.ext.asyncio import ( @@ -361,6 +375,10 @@ def testing_engine( engine.pool._transfer_from(config.db.pool) elif share_pool: engine.pool = config.db.pool + elif config.options and config.options.low_connections: + # for suites running with --low-connections, dispose the "global" + # engines to disconnect everything before making a testing engine + testing_reaper._dispose_testing_engines("global") if scope == "global": if asyncio: diff --git a/lib/sqlalchemy/testing/fixtures/sql.py b/lib/sqlalchemy/testing/fixtures/sql.py index dc7add481e..ae18e69f58 100644 --- a/lib/sqlalchemy/testing/fixtures/sql.py +++ b/lib/sqlalchemy/testing/fixtures/sql.py @@ -10,12 +10,12 @@ from __future__ import annotations import itertools import random import re -import sys import sqlalchemy as sa from .base import TestBase from .. import config from .. import mock +from .. import provision from ..assertions import eq_ from ..assertions import expect_deprecated from ..assertions import ne_ @@ -134,37 +134,16 @@ class TablesTest(TestBase): elif self.run_create_tables == "each": drop_all_tables_from_metadata(self._tables_metadata, self.bind) - savepoints = getattr(config.requirements, "savepoints", False) - if savepoints: - savepoints = savepoints.enabled - # no need to run deletes if tables are recreated on setup if ( self.run_define_tables != "each" - and self.run_create_tables != "each" + and self.run_create_tables == "once" and self.run_deletes == "each" ): with self.bind.begin() as conn: - for table in reversed( - [ - t - for (t, fks) in sort_tables_and_constraints( - self._tables_metadata.tables.values() - ) - if t is not None - ] - ): - try: - if savepoints: - with conn.begin_nested(): - conn.execute(table.delete()) - else: - conn.execute(table.delete()) - except sa.exc.DBAPIError as ex: - print( - ("Error emptying table %s: %r" % (table, ex)), - file=sys.stderr, - ) + provision.delete_from_all_tables( + config, conn, self._tables_metadata + ) @classmethod def _teardown_once_metadata_bind(cls): diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py index 4f35b10294..fb534d5e1a 100644 --- a/lib/sqlalchemy/testing/plugin/plugin_base.py +++ b/lib/sqlalchemy/testing/plugin/plugin_base.py @@ -588,8 +588,8 @@ def stop_test_class(cls): def stop_test_class_outside_fixtures(cls): - engines.testing_reaper.stop_test_class_outside_fixtures() provision.stop_test_class_outside_fixtures(config, config.db, cls) + engines.testing_reaper.stop_test_class_outside_fixtures() try: if not options.low_connections: assertions.global_cleanup_assertions() diff --git a/lib/sqlalchemy/testing/provision.py b/lib/sqlalchemy/testing/provision.py index 17992a0ff8..ebf7f63ca5 100644 --- a/lib/sqlalchemy/testing/provision.py +++ b/lib/sqlalchemy/testing/provision.py @@ -18,6 +18,7 @@ from . import util from .. import exc from .. import inspect from ..engine import url as sa_url +from ..schema import sort_tables_and_constraints from ..sql import ddl from ..sql import schema from ..util import decorator @@ -51,6 +52,9 @@ class register: return decorate + def call_original(self, cfg, *arg, **kw): + return self.fns["*"](cfg, *arg, **kw) + def __call__(self, cfg, *arg, **kw): if isinstance(cfg, str): url = sa_url.make_url(cfg) @@ -521,3 +525,36 @@ def allow_stale_updates(fn, *arg, **kw): """ with allow_stale_update_impl(config._current): return fn(*arg, **kw) + + +@register.init +def delete_from_all_tables(cfg, connection, metadata): + """an absolutely foolproof delete from all tables routine. + + dialects should override this to add special instructions like + disable constraints etc. + + """ + savepoints = getattr(cfg.requirements, "savepoints", False) + if savepoints: + savepoints = savepoints.enabled + + inspector = inspect(connection) + + for table in reversed( + [ + t + for (t, fks) in sort_tables_and_constraints( + metadata.tables.values() + ) + if t is not None + # remember that inspector.get_table_names() is cached, + # so this emits SQL once per unique schema name + and t.name in inspector.get_table_names(schema=t.schema) + ] + ): + if savepoints: + with connection.begin_nested(): + connection.execute(table.delete()) + else: + connection.execute(table.delete()) diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 97281be19c..e17c4aab67 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -1553,14 +1553,12 @@ class SuiteRequirements(Requirements): def ad_hoc_engines(self): """Test environment must allow ad-hoc engine/connection creation. - DBs that scale poorly for many connections, even when closed, i.e. - Oracle, may use the "--low-connections" option which flags this - requirement as not present. + This is now a no-op since we reconfigured ``options.low_connections`` + to cause the ``testing_engine()`` to close off other open connections + when its invoked. """ - return exclusions.skip_if( - lambda config: config.options.low_connections - ) + return exclusions.open() @property def no_windows(self): diff --git a/lib/sqlalchemy/testing/schema.py b/lib/sqlalchemy/testing/schema.py index 0dd7de2029..516af2f367 100644 --- a/lib/sqlalchemy/testing/schema.py +++ b/lib/sqlalchemy/testing/schema.py @@ -26,37 +26,11 @@ table_options = {} def Table(*args, **kw) -> schema.Table: """A schema.Table wrapper/hook for dialect-specific tweaks.""" - test_opts = {k: kw.pop(k) for k in list(kw) if k.startswith("test_")} + # pop out local options; these are not used at the moment + _ = {k: kw.pop(k) for k in list(kw) if k.startswith("test_")} kw.update(table_options) - if exclusions.against(config._current, "mysql"): - if ( - "mysql_engine" not in kw - and "mysql_type" not in kw - and "autoload_with" not in kw - ): - if "test_needs_fk" in test_opts or "test_needs_acid" in test_opts: - kw["mysql_engine"] = "InnoDB" - else: - # there are in fact test fixtures that rely upon MyISAM, - # due to MySQL / MariaDB having poor FK behavior under innodb, - # such as a self-referential table can't be deleted from at - # once without attending to per-row dependencies. We'd need to - # add special steps to some fixtures if we want to not - # explicitly state MyISAM here - kw["mysql_engine"] = "MyISAM" - elif exclusions.against(config._current, "mariadb"): - if ( - "mariadb_engine" not in kw - and "mariadb_type" not in kw - and "autoload_with" not in kw - ): - if "test_needs_fk" in test_opts or "test_needs_acid" in test_opts: - kw["mariadb_engine"] = "InnoDB" - else: - kw["mariadb_engine"] = "MyISAM" - return schema.Table(*args, **kw) diff --git a/lib/sqlalchemy/testing/suite/test_cte.py b/lib/sqlalchemy/testing/suite/test_cte.py index e6e852bee5..ca9a2e50e5 100644 --- a/lib/sqlalchemy/testing/suite/test_cte.py +++ b/lib/sqlalchemy/testing/suite/test_cte.py @@ -20,7 +20,7 @@ from ... import values class CTETest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("ctes",) run_inserts = "each" diff --git a/lib/sqlalchemy/testing/suite/test_ddl.py b/lib/sqlalchemy/testing/suite/test_ddl.py index e729c338f7..00b943d0d3 100644 --- a/lib/sqlalchemy/testing/suite/test_ddl.py +++ b/lib/sqlalchemy/testing/suite/test_ddl.py @@ -29,7 +29,7 @@ from ... import UniqueConstraint class TableDDLTest(fixtures.TestBase): - __backend__ = True + __sparse_driver_backend__ = True def _simple_fixture(self, schema=None): return Table( @@ -233,7 +233,7 @@ class LongNameBlowoutTest(fixtures.TestBase): """ - __backend__ = True + __sparse_driver_backend__ = True def fk(self, metadata, connection): convention = { diff --git a/lib/sqlalchemy/testing/suite/test_reflection.py b/lib/sqlalchemy/testing/suite/test_reflection.py index bbbaa20246..d324d2ae91 100644 --- a/lib/sqlalchemy/testing/suite/test_reflection.py +++ b/lib/sqlalchemy/testing/suite/test_reflection.py @@ -73,7 +73,9 @@ class OneConnectionTablesTest(fixtures.TablesTest): class HasTableTest(OneConnectionTablesTest): - __backend__ = True + __sparse_driver_backend__ = True + + run_deletes = None @classmethod def define_tables(cls, metadata): @@ -222,7 +224,7 @@ class HasTableTest(OneConnectionTablesTest): class HasIndexTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("index_reflection",) @classmethod @@ -300,7 +302,7 @@ class HasIndexTest(fixtures.TablesTest): class BizarroCharacterTest(fixtures.TestBase): - __backend__ = True + __sparse_driver_backend__ = True def column_names(): return testing.combinations( @@ -403,7 +405,7 @@ class BizarroCharacterTest(fixtures.TestBase): class TempTableElementsTest(fixtures.TestBase): - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("temp_table_reflection",) @@ -441,7 +443,7 @@ class TempTableElementsTest(fixtures.TestBase): class QuotedNameArgumentTest(fixtures.TablesTest): run_create_tables = "once" - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -623,7 +625,7 @@ def _multi_combination(fn): class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest): run_inserts = run_deletes = None - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -2656,7 +2658,7 @@ class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest): class TableNoColumnsTest(fixtures.TestBase): __requires__ = ("reflect_tables_no_columns",) - __backend__ = True + __sparse_driver_backend__ = True @testing.fixture def table_no_columns(self, connection, metadata): @@ -2708,7 +2710,7 @@ class TableNoColumnsTest(fixtures.TestBase): class ComponentReflectionTestExtra(ComparesIndexes, fixtures.TestBase): - __backend__ = True + __sparse_driver_backend__ = True @testing.fixture(params=[True, False]) def use_schema_fixture(self, request): @@ -3211,7 +3213,7 @@ class ComponentReflectionTestExtra(ComparesIndexes, fixtures.TestBase): class NormalizedNameTest(fixtures.TablesTest): __requires__ = ("denormalized_names",) - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -3352,7 +3354,7 @@ class ComputedReflectionTest(fixtures.ComputedReflectionFixtureTest): class IdentityReflectionTest(fixtures.TablesTest): run_inserts = run_deletes = None - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("identity_columns", "table_reflection") @classmethod @@ -3491,7 +3493,7 @@ class IdentityReflectionTest(fixtures.TablesTest): class CompositeKeyReflectionTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): diff --git a/lib/sqlalchemy/testing/suite/test_select.py b/lib/sqlalchemy/testing/suite/test_select.py index 3e60e546d9..9cc6640c3a 100644 --- a/lib/sqlalchemy/testing/suite/test_select.py +++ b/lib/sqlalchemy/testing/suite/test_select.py @@ -53,7 +53,7 @@ from ...exc import ProgrammingError class CollateTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -99,7 +99,7 @@ class OrderByLabelTest(fixtures.TablesTest): """ - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -170,7 +170,7 @@ class OrderByLabelTest(fixtures.TablesTest): class ValuesExpressionTest(fixtures.TestBase): __requires__ = ("table_value_constructor",) - __backend__ = True + __sparse_driver_backend__ = True def test_tuples(self, connection): value_expr = values( @@ -643,7 +643,7 @@ class FetchLimitOffsetTest(fixtures.TablesTest): class SameNamedSchemaTableTest(fixtures.TablesTest): """tests for #7471""" - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("schemas",) @@ -740,7 +740,7 @@ class SameNamedSchemaTableTest(fixtures.TablesTest): class JoinTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True def _assert_result(self, select, result, params=None): with config.db.connect() as conn: @@ -840,7 +840,7 @@ class JoinTest(fixtures.TablesTest): class CompoundSelectTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -1501,7 +1501,7 @@ class ExpandingBoundInTest(fixtures.TablesTest): class LikeFunctionsTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True run_inserts = "once" run_deletes = None @@ -1646,7 +1646,7 @@ class LikeFunctionsTest(fixtures.TablesTest): class ComputedColumnTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("computed_columns",) @classmethod @@ -1794,7 +1794,7 @@ class IdentityAutoincrementTest(fixtures.TablesTest): class ExistsTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -1841,7 +1841,7 @@ class ExistsTest(fixtures.TablesTest): class DistinctOnTest(AssertsCompiledSQL, fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True @testing.fails_if(testing.requires.supports_distinct_on) def test_distinct_on(self): @@ -1856,7 +1856,7 @@ class DistinctOnTest(AssertsCompiledSQL, fixtures.TablesTest): class IsOrIsNotDistinctFromTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True __requires__ = ("supports_is_distinct_from",) @classmethod @@ -1911,7 +1911,7 @@ class IsOrIsNotDistinctFromTest(fixtures.TablesTest): class WindowFunctionTest(fixtures.TablesTest): __requires__ = ("window_functions",) - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): diff --git a/lib/sqlalchemy/testing/suite/test_sequence.py b/lib/sqlalchemy/testing/suite/test_sequence.py index f0e6575370..7800843043 100644 --- a/lib/sqlalchemy/testing/suite/test_sequence.py +++ b/lib/sqlalchemy/testing/suite/test_sequence.py @@ -24,7 +24,7 @@ from ... import testing class SequenceTest(fixtures.TablesTest): __requires__ = ("sequences",) - __backend__ = True + __sparse_driver_backend__ = True run_create_tables = "each" @@ -163,7 +163,7 @@ class SequenceTest(fixtures.TablesTest): class SequenceCompilerTest(testing.AssertsCompiledSQL, fixtures.TestBase): __requires__ = ("sequences",) - __backend__ = True + __sparse_driver_backend__ = True def test_literal_binds_inline_compile(self, connection): table = Table( @@ -192,7 +192,7 @@ class HasSequenceTest(fixtures.TablesTest): run_deletes = None __requires__ = ("sequences",) - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): @@ -308,7 +308,7 @@ class HasSequenceTest(fixtures.TablesTest): class HasSequenceTestEmpty(fixtures.TestBase): __requires__ = ("sequences",) - __backend__ = True + __sparse_driver_backend__ = True def test_get_sequence_names_no_sequence(self, connection): eq_( diff --git a/lib/sqlalchemy/testing/suite/test_table_via_select.py b/lib/sqlalchemy/testing/suite/test_table_via_select.py index 06b652ef7b..86e8841c24 100644 --- a/lib/sqlalchemy/testing/suite/test_table_via_select.py +++ b/lib/sqlalchemy/testing/suite/test_table_via_select.py @@ -28,7 +28,7 @@ from ...testing import config class TableViaSelectTest(fixtures.TablesTest): - __backend__ = True + __sparse_driver_backend__ = True @classmethod def temp_table_name(cls): diff --git a/lib/sqlalchemy/testing/suite/test_update_delete.py b/lib/sqlalchemy/testing/suite/test_update_delete.py index 85a8d39339..50f0ca9fca 100644 --- a/lib/sqlalchemy/testing/suite/test_update_delete.py +++ b/lib/sqlalchemy/testing/suite/test_update_delete.py @@ -18,7 +18,7 @@ from ... import testing class SimpleUpdateDeleteTest(fixtures.TablesTest): run_deletes = "each" __requires__ = ("sane_rowcount",) - __backend__ = True + __sparse_driver_backend__ = True @classmethod def define_tables(cls, metadata): diff --git a/noxfile.py b/noxfile.py index ed388b49d6..a7309a6c77 100644 --- a/noxfile.py +++ b/noxfile.py @@ -239,8 +239,9 @@ def _tests( elif backendonly: # with "-m backendonly", only tests with the backend pytest mark # (or pytestplugin equivalent, like __backend__) will be selected - # by pytest - includes_excludes["m"].append("backend") + # by pytest. + # memory intensive is deselected to prevent these from running + includes_excludes["m"].extend(["backend", "not memory_intensive"]) else: includes_excludes["m"].append("not memory_intensive") @@ -294,10 +295,13 @@ def _tests( coverage=coverage, ) + if database in ["oracle", "mssql"]: + cmd.extend(["--low-connections"]) + if database in ["oracle", "mssql", "sqlite_file"]: # use equals sign so that we avoid # https://github.com/pytest-dev/pytest/issues/13913 - cmd.extend(["--write-idents=db_idents.txt", "--low-connections"]) + cmd.extend(["--write-idents=db_idents.txt"]) cmd.extend(posargs) diff --git a/setup.cfg b/setup.cfg index a143065685..4bbe7d0aab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,8 +47,8 @@ mssql = mssql+pyodbc://scott:tiger^5HHH@mssql2022:1433/test?driver=ODBC+Driver+1 mssql_async = mssql+aioodbc://scott:tiger^5HHH@mssql2022:1433/test?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Encrypt=Optional pymssql = mssql+pymssql://scott:tiger^5HHH@mssql2022:1433/test docker_mssql = mssql+pyodbc://scott:tiger^5HHH@127.0.0.1:1433/test?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Encrypt=Optional -oracle = oracle+cx_oracle://scott:tiger@oracle23c/freepdb1 -cxoracle = oracle+cx_oracle://scott:tiger@oracle23c/freepdb1 -oracledb = oracle+oracledb://scott:tiger@oracle23c/freepdb1 -oracledb_async = oracle+oracledb_async://scott:tiger@oracle23c/freepdb1 +oracle = oracle+cx_oracle://scott:tiger@oracle18c/xe +cxoracle = oracle+cx_oracle://scott:tiger@oracle18c/xe +oracledb = oracle+oracledb://scott:tiger@oracle18c/xe +oracledb_async = oracle+oracledb_async://scott:tiger@oracle18c/xe docker_oracle = oracle+cx_oracle://scott:tiger@127.0.0.1:1521/?service_name=FREEPDB1 diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py index d5455b2c9d..c7ac877e0b 100644 --- a/test/orm/inheritance/test_basic.py +++ b/test/orm/inheritance/test_basic.py @@ -40,6 +40,7 @@ from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ from sqlalchemy.testing import mock +from sqlalchemy.testing import provision from sqlalchemy.testing.assertions import assert_warns_message from sqlalchemy.testing.assertsql import AllOf from sqlalchemy.testing.assertsql import CompiledSQL @@ -2100,6 +2101,7 @@ class VersioningTest(fixtures.MappedTest): ) @testing.requires.sane_rowcount + @provision.allow_stale_updates def test_save_update(self): subtable, base, stuff = ( self.tables.subtable, @@ -2553,98 +2555,6 @@ class OverrideColKeyTest(fixtures.MappedTest): # PK col assert s2.id == s2.base_id != 15 - def test_subclass_renames_superclass_col_single_inh(self, decl_base): - """tested as part of #8705. - - The step where we configure columns mapped to specific keys must - take place even if the given column is already in _columntoproperty, - as would be the case if the superclass maps that column already. - - """ - - class A(decl_base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - a_data = Column(String) - - class B(A): - b_data = column_property(A.__table__.c.a_data) - - is_(A.a_data.property.columns[0], A.__table__.c.a_data) - is_(B.a_data.property.columns[0], A.__table__.c.a_data) - is_(B.b_data.property.columns[0], A.__table__.c.a_data) - - def test_subsubclass_groups_super_cols(self, decl_base): - """tested for #9220, which is a regression caused by #8705.""" - - class BaseClass(decl_base): - __tablename__ = "basetable" - - id = Column(Integer, primary_key=True) - name = Column(String(50)) - type = Column(String(20)) - - __mapper_args__ = { - "polymorphic_on": type, - "polymorphic_identity": "base", - } - - class SubClass(BaseClass): - __tablename__ = "subtable" - - id = column_property( - Column(Integer, primary_key=True), BaseClass.id - ) - base_id = Column(Integer, ForeignKey("basetable.id")) - subdata1 = Column(String(50)) - - __mapper_args__ = {"polymorphic_identity": "sub"} - - class SubSubClass(SubClass): - __tablename__ = "subsubtable" - - id = column_property( - Column(Integer, ForeignKey("subtable.id"), primary_key=True), - SubClass.id, - BaseClass.id, - ) - subdata2 = Column(String(50)) - - __mapper_args__ = {"polymorphic_identity": "subsub"} - - is_(SubSubClass.id.property.columns[0], SubSubClass.__table__.c.id) - is_( - SubSubClass.id.property.columns[1]._deannotate(), - SubClass.__table__.c.id, - ) - is_( - SubSubClass.id.property.columns[2]._deannotate(), - BaseClass.__table__.c.id, - ) - - def test_column_setup_sanity_check(self, decl_base): - class A(decl_base): - __tablename__ = "a" - - id = Column(Integer, primary_key=True) - a_data = Column(String) - - class B(A): - __tablename__ = "b" - id = Column(Integer, ForeignKey("a.id"), primary_key=True) - b_data = Column(String) - - is_(A.id.property.parent, inspect(A)) - # overlapping cols get a new prop on the subclass, with cols merged - is_(B.id.property.parent, inspect(B)) - eq_(B.id.property.columns, [B.__table__.c.id, A.__table__.c.id]) - - # totally independent cols remain w/ parent on the originating - # mapper - is_(B.a_data.property.parent, inspect(A)) - is_(B.b_data.property.parent, inspect(B)) - def test_override_implicit(self): # this is originally [ticket:1111]. # the pattern here is now disallowed by [ticket:1892] @@ -2791,6 +2701,102 @@ class OverrideColKeyTest(fixtures.MappedTest): assert sess.get(Sub, s1.base_id).data == "this is base" +class OverrideColKeyTestDeclarative(fixtures.TestBase): + """test overriding of column attributes.""" + + def test_subclass_renames_superclass_col_single_inh(self, decl_base): + """tested as part of #8705. + + The step where we configure columns mapped to specific keys must + take place even if the given column is already in _columntoproperty, + as would be the case if the superclass maps that column already. + + """ + + class A(decl_base): + __tablename__ = "a" + + id = Column(Integer, primary_key=True) + a_data = Column(String) + + class B(A): + b_data = column_property(A.__table__.c.a_data) + + is_(A.a_data.property.columns[0], A.__table__.c.a_data) + is_(B.a_data.property.columns[0], A.__table__.c.a_data) + is_(B.b_data.property.columns[0], A.__table__.c.a_data) + + def test_subsubclass_groups_super_cols(self, decl_base): + """tested for #9220, which is a regression caused by #8705.""" + + class BaseClass(decl_base): + __tablename__ = "basetable" + + id = Column(Integer, primary_key=True) + name = Column(String(50)) + type = Column(String(20)) + + __mapper_args__ = { + "polymorphic_on": type, + "polymorphic_identity": "base", + } + + class SubClass(BaseClass): + __tablename__ = "subtable" + + id = column_property( + Column(Integer, primary_key=True), BaseClass.id + ) + base_id = Column(Integer, ForeignKey("basetable.id")) + subdata1 = Column(String(50)) + + __mapper_args__ = {"polymorphic_identity": "sub"} + + class SubSubClass(SubClass): + __tablename__ = "subsubtable" + + id = column_property( + Column(Integer, ForeignKey("subtable.id"), primary_key=True), + SubClass.id, + BaseClass.id, + ) + subdata2 = Column(String(50)) + + __mapper_args__ = {"polymorphic_identity": "subsub"} + + is_(SubSubClass.id.property.columns[0], SubSubClass.__table__.c.id) + is_( + SubSubClass.id.property.columns[1]._deannotate(), + SubClass.__table__.c.id, + ) + is_( + SubSubClass.id.property.columns[2]._deannotate(), + BaseClass.__table__.c.id, + ) + + def test_column_setup_sanity_check(self, decl_base): + class A(decl_base): + __tablename__ = "a" + + id = Column(Integer, primary_key=True) + a_data = Column(String) + + class B(A): + __tablename__ = "b" + id = Column(Integer, ForeignKey("a.id"), primary_key=True) + b_data = Column(String) + + is_(A.id.property.parent, inspect(A)) + # overlapping cols get a new prop on the subclass, with cols merged + is_(B.id.property.parent, inspect(B)) + eq_(B.id.property.columns, [B.__table__.c.id, A.__table__.c.id]) + + # totally independent cols remain w/ parent on the originating + # mapper + is_(B.a_data.property.parent, inspect(A)) + is_(B.b_data.property.parent, inspect(B)) + + class OptimizedLoadTest(fixtures.MappedTest): """tests for the "optimized load" routine.""" diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py index 82ec9ebeea..f97e9248d4 100644 --- a/test/orm/inheritance/test_relationship.py +++ b/test/orm/inheritance/test_relationship.py @@ -438,7 +438,9 @@ class SelfReferentialJ2JSelfTest(fixtures.MappedTest): primary_key=True, ), Column( - "reports_to_id", Integer, ForeignKey("engineers.person_id") + "reports_to_id", + Integer, + ForeignKey("engineers.person_id"), ), ) diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index 9a06de72dd..3df66c03d4 100644 --- a/test/orm/test_versioning.py +++ b/test/orm/test_versioning.py @@ -1112,6 +1112,7 @@ class AlternateGeneratorTest(fixtures.MappedTest): session.commit() @testing.requires.sane_rowcount + @provision.allow_stale_updates def test_child_row_switch_two(self): P = self.classes.P @@ -1675,6 +1676,7 @@ class ServerVersioningTest(fixtures.MappedTest): self.assert_sql_execution(testing.db, sess.flush, *statements) @testing.requires.independent_connections + @provision.allow_stale_updates def test_concurrent_mod_err_expire_on_commit(self): sess = self._fixture() -- 2.47.3