From 2d545de5bf8ba484f2d0bcb4a42b25bf2863e523 Mon Sep 17 00:00:00 2001 From: Parnassius Date: Sat, 11 Dec 2021 01:37:10 +0100 Subject: [PATCH 1/4] SQLAlchemy 1.4 support --- pokedex/db/multilang.py | 43 +++++++++++++++++++++++++++++++---------- setup.py | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py index fcfcd47b3..6d2f24252 100644 --- a/pokedex/db/multilang.py +++ b/pokedex/db/multilang.py @@ -10,6 +10,18 @@ from pokedex.db import markdown +# Decide which method to use for the default value of the parameter _default_language_id +_MULTILANG_SESSION_USE_EVENT = False +try: + from sqlalchemy.orm import SessionEvents +except ImportError: + pass +else: + if hasattr(SessionEvents, 'do_orm_execute'): + # SQLAlchemy 1.4+ + from sqlalchemy import event + _MULTILANG_SESSION_USE_EVENT = True + class LocalAssociationProxy(AssociationProxy, ColumnOperators): """An association proxy for names in the default language @@ -168,7 +180,7 @@ class Foo(Base): ... primaryjoin=and_( Translations.foreign_id == foreign_class.id, Translations.local_language_id == bindparam('_default_language_id', - value='dummy', type_=Integer, required=True), + value='dummy', type_=Integer), ), foreign_keys=[Translations.foreign_id, Translations.local_language_id], uselist=False, @@ -206,14 +218,16 @@ def creator(language, value): # Done return Translations -class MultilangQuery(Query): - def _execute_and_instances(self, *args, **kwargs): - # Set _default_language_id param if it hasn't been set by the time the query is executed. - # XXX This is really hacky and we should figure out a cleaner method. - if '_default_language_id' not in self._params or self._params['_default_language_id'] == 'dummy': - self._params = self._params.copy() - self._params['_default_language_id'] = self.session.default_language_id - return super(MultilangQuery, self)._execute_and_instances(*args, **kwargs) +if not _MULTILANG_SESSION_USE_EVENT: + # SQLAlchemy 1.4 no longer supports Query._execute_and_instances + class MultilangQuery(Query): + def _execute_and_instances(self, *args, **kwargs): + # Set _default_language_id param if it hasn't been set by the time the query is executed. + # XXX This is really hacky and we should figure out a cleaner method. + if '_default_language_id' not in self._params or self._params['_default_language_id'] == 'dummy': + self._params = self._params.copy() + self._params['_default_language_id'] = self.session.default_language_id + return super(MultilangQuery, self)._execute_and_instances(*args, **kwargs) class MultilangSession(Session): """A tiny Session subclass that adds support for a default language. @@ -232,10 +246,19 @@ def __init__(self, *args, **kwargs): self.markdown_extension = markdown_extension_class(self) - kwargs.setdefault('query_cls', MultilangQuery) + if not _MULTILANG_SESSION_USE_EVENT: + kwargs.setdefault('query_cls', MultilangQuery) super(MultilangSession, self).__init__(*args, **kwargs) +if _MULTILANG_SESSION_USE_EVENT: + @event.listens_for(MultilangSession, 'do_orm_execute') + def receive_do_orm_execute(state): + # Set _default_language_id param if it hasn't been set by the time the query is executed. + # The same hack as above, but for SQLAlchemy 1.4+ + if state.is_select and state.parameters.get('_default_language_id', 'dummy') == 'dummy': + return state.invoke_statement(params={'_default_language_id': state.session.default_language_id}) + class MultilangScopedSession(ScopedSession): """Dispatches language selection to the attached Session.""" diff --git a/setup.py b/setup.py index 16ac66753..cb109367b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ 'pokedex': ['data/csv/*.csv'] }, install_requires=[ - 'SQLAlchemy>=1.0,<1.4', + 'SQLAlchemy>=1.0,<2.0', 'whoosh>=2.5,<2.7', 'markdown>=2.4.1,<=2.6.11', 'construct==2.5.3', From 62c8ca421a3ebe4d71c1a37e1c5b812e647707b7 Mon Sep 17 00:00:00 2001 From: Parnassius Date: Sat, 11 Dec 2021 14:34:13 +0100 Subject: [PATCH 2/4] Replace a couple of double relationships with a single one with `backref` --- pokedex/db/tables.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py index c37f22df7..e2f7a20e7 100644 --- a/pokedex/db/tables.py +++ b/pokedex/db/tables.py @@ -2475,7 +2475,8 @@ class VersionGroupRegion(TableBase): Generation.versions = relationship(Version, secondary=VersionGroup.__table__, innerjoin=True) -Generation.main_region = relationship(Region, innerjoin=True) +Generation.main_region = relationship(Region, innerjoin=True, + backref=backref('generation', uselist=False)) GrowthRate.max_experience_obj = relationship(Experience, @@ -2497,14 +2498,13 @@ class VersionGroupRegion(TableBase): Item.fling_effect = relationship(ItemFlingEffect, backref='items') Item.machines = relationship(Machine, - order_by=Machine.version_group_id.asc()) + order_by=Machine.version_group_id.asc(), + backref='item') Item.category = relationship(ItemCategory, innerjoin=True, backref=backref('items', order_by=Item.identifier.asc())) Item.pocket = association_proxy('category', 'pocket') -ItemCategory.pocket = relationship(ItemPocket, innerjoin=True) - ItemFlavorText.version_group = relationship(VersionGroup, innerjoin=True, lazy='joined') ItemFlavorText.language = relationship(Language, @@ -2518,7 +2518,8 @@ class VersionGroupRegion(TableBase): ItemPocket.categories = relationship(ItemCategory, innerjoin=True, - order_by=ItemCategory.identifier.asc()) + order_by=ItemCategory.identifier.asc(), + backref=backref('pocket', innerjoin=True)) Location.region = relationship(Region, @@ -2542,11 +2543,6 @@ class VersionGroupRegion(TableBase): innerjoin=True, lazy='joined') -Machine.item = relationship(Item) -Machine.version_group = relationship(VersionGroup, - innerjoin=True, lazy='joined') - - Move.changelog = relationship(MoveChangelog, order_by=MoveChangelog.changed_in_version_group_id.desc(), backref=backref('move', innerjoin=True, lazy='joined')) @@ -2886,7 +2882,6 @@ class VersionGroupRegion(TableBase): PokemonSpeciesFlavorText.version = relationship(Version, innerjoin=True, lazy='joined') PokemonSpeciesFlavorText.language = relationship(Language, innerjoin=True, lazy='joined') -Region.generation = relationship(Generation, uselist=False) Region.version_group_regions = relationship(VersionGroupRegion, order_by=VersionGroupRegion.version_group_id.asc(), backref='region') @@ -2950,7 +2945,8 @@ class VersionGroupRegion(TableBase): backref="version_groups") VersionGroup.machines = relationship(Machine, innerjoin=True, - order_by=Machine.machine_number) + order_by=Machine.machine_number, + backref=backref('version_group', innerjoin=True, lazy='joined')) VersionGroupPokemonMoveMethod.version_group = relationship(VersionGroup, From 716ad75dbc13bb8559d65c808a61c5e5c9afbbd9 Mon Sep 17 00:00:00 2001 From: Parnassius Date: Sat, 11 Dec 2021 16:18:11 +0100 Subject: [PATCH 3/4] Use `viewonly=True` to suppress relationship warnings in SQLA 1.4 --- pokedex/db/multilang.py | 1 + pokedex/db/tables.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py index 6d2f24252..211df50a7 100644 --- a/pokedex/db/multilang.py +++ b/pokedex/db/multilang.py @@ -185,6 +185,7 @@ class Foo(Base): ... foreign_keys=[Translations.foreign_id, Translations.local_language_id], uselist=False, lazy=relation_lazy, + viewonly=True, )) # Add per-column proxies to the original class diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py index e2f7a20e7..e30d54b34 100644 --- a/pokedex/db/tables.py +++ b/pokedex/db/tables.py @@ -30,6 +30,7 @@ import six from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, UniqueConstraint +from sqlalchemy import __version__ as sqla_version from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property @@ -39,6 +40,12 @@ from pokedex.db import markdown, multilang +relationship = partial(relationship, viewonly=True) +if (1, 3, 17) <= tuple(int(x) for x in sqla_version.split(".")) < (1, 4): + # `sync_backref` was introduced in 1.3.17 + # Since 1.4 it defaults to False if `viewonly` is True + relationship = partial(relationship, sync_backref=False) + class TableSuperclass(object): """Superclass for declarative tables, to give them some generic niceties like stringification. From 678f9f8e9438c5d0feafbcc98d3f9f30242407cf Mon Sep 17 00:00:00 2001 From: Parnassius Date: Sat, 11 Dec 2021 16:37:21 +0100 Subject: [PATCH 4/4] Suppress a warning about an intentional cartesian product --- pokedex/db/load.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pokedex/db/load.py b/pokedex/db/load.py index a13cc3f2c..079696fed 100644 --- a/pokedex/db/load.py +++ b/pokedex/db/load.py @@ -17,7 +17,7 @@ from pokedex.db.dependencies import find_dependent_tables from pokedex.db.oracle import rewrite_long_table_names -from sqlalchemy import and_ +from sqlalchemy import and_, true from sqlalchemy.sql import exists @@ -381,6 +381,7 @@ def insert_and_commit(): session.query(VGPMM).delete() q = session.query(t.VersionGroup.id, t.PokemonMoveMethod.id) + q = q.filter(true()) # Suppress cartesian product warning q = q.filter(exists().where(and_( t.PokemonMove.pokemon_move_method_id == t.PokemonMoveMethod.id, t.PokemonMove.version_group_id == t.VersionGroup.id)))