From 8ac917aa6498ca995a0295a716525210136156fc Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Mon, 21 Mar 2016 12:53:01 -0400 Subject: [PATCH 01/90] Failing tests for entities living in other schema --- tests/builders/test_table_builder.py | 29 +++++ .../test_many_to_many_relations.py | 109 +++++++++++++++++- tests/test_transaction.py | 29 +++++ 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/tests/builders/test_table_builder.py b/tests/builders/test_table_builder.py index 16323d02..a2255c83 100644 --- a/tests/builders/test_table_builder.py +++ b/tests/builders/test_table_builder.py @@ -3,6 +3,7 @@ import sqlalchemy as sa from sqlalchemy_continuum import version_class from tests import TestCase +from pytest import mark class TestTableBuilder(TestCase): @@ -69,3 +70,31 @@ class Article(self.Model): def test_takes_out_onupdate_triggers(self): table = version_class(self.Article).__table__ assert table.c.last_update.onupdate is None + +@mark.skipif("os.environ.get('DB') == 'sqlite'") +class TestTableBuilderInOtherSchema(TestCase): + def create_models(self): + class Article(self.Model): + __tablename__ = 'article' + __versioned__ = copy(self.options) + __table_args__ = {'schema': 'other'} + + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + last_update = sa.Column( + sa.DateTime, + default=datetime.utcnow, + onupdate=datetime.utcnow, + nullable=False + ) + self.Article = Article + + def create_tables(self): + self.connection.execute('DROP SCHEMA IF EXISTS other') + self.connection.execute('CREATE SCHEMA other') + TestCase.create_tables(self) + + def test_created_tables_retain_schema(self): + table = version_class(self.Article).__table__ + assert table.schema is not None + assert table.schema == self.Article.__table__.schema + diff --git a/tests/relationships/test_many_to_many_relations.py b/tests/relationships/test_many_to_many_relations.py index 256178ee..2aaa1196 100644 --- a/tests/relationships/test_many_to_many_relations.py +++ b/tests/relationships/test_many_to_many_relations.py @@ -1,4 +1,5 @@ import pytest +from pytest import mark import sqlalchemy as sa from sqlalchemy_continuum import versioning_manager @@ -339,4 +340,110 @@ def test_multiple_inserts_over_multiple_transactions(self): assert reference2.versions[0] in article.versions[2].references assert len(reference1.versions[2].cited_by) == 1 - assert article.versions[2] in reference1.versions[2].cited_by \ No newline at end of file + assert article.versions[2] in reference1.versions[2].cited_by + + +@mark.skipif("os.environ.get('DB') == 'sqlite'") +class TestManyToManySelfReferentialInOtherSchema(TestManyToManySelfReferential): + def create_models(self): + class Article(self.Model): + __tablename__ = 'article' + __versioned__ = {} + __table_args__ = {'schema': 'other'} + + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + + article_references = sa.Table( + 'article_references', + self.Model.metadata, + sa.Column( + 'referring_id', + sa.Integer, + sa.ForeignKey('other.article.id'), + primary_key=True, + ), + sa.Column( + 'referred_id', + sa.Integer, + sa.ForeignKey('other.article.id'), + primary_key=True + ), + schema='other' + ) + + Article.references = sa.orm.relationship( + Article, + secondary=article_references, + primaryjoin=Article.id == article_references.c.referring_id, + secondaryjoin=Article.id == article_references.c.referred_id, + backref='cited_by' + ) + + self.Article = Article + self.referenced_articles_table = article_references + + def create_tables(self): + self.connection.execute('DROP SCHEMA IF EXISTS other') + self.connection.execute('CREATE SCHEMA other') + TestManyToManySelfReferential.create_tables(self) + + +@mark.skipif("os.environ.get('DB') == 'sqlite'") +class ManyToManyRelationshipsInOtherSchemaTestCase(ManyToManyRelationshipsTestCase): + def create_models(self): + class Article(self.Model): + __tablename__ = 'article' + __versioned__ = { + 'base_classes': (self.Model, ) + } + __table_args__ = {'schema': 'other'} + + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + + article_tag = sa.Table( + 'article_tag', + self.Model.metadata, + sa.Column( + 'article_id', + sa.Integer, + sa.ForeignKey('other.article.id'), + primary_key=True, + ), + sa.Column( + 'tag_id', + sa.Integer, + sa.ForeignKey('other.tag.id'), + primary_key=True + ), + schema='other' + ) + + class Tag(self.Model): + __tablename__ = 'tag' + __versioned__ = { + 'base_classes': (self.Model, ) + } + __table_args__ = {'schema': 'other'} + + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + + Tag.articles = sa.orm.relationship( + Article, + secondary=article_tag, + backref='tags' + ) + + self.Article = Article + self.Tag = Tag + + + def create_tables(self): + self.connection.execute('DROP SCHEMA IF EXISTS other') + self.connection.execute('CREATE SCHEMA other') + ManyToManyRelationshipsTestCase.create_tables(self) + +create_test_cases(ManyToManyRelationshipsInOtherSchemaTestCase) + diff --git a/tests/test_transaction.py b/tests/test_transaction.py index f647130d..9d9e8f1b 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -1,6 +1,7 @@ import sqlalchemy as sa from sqlalchemy_continuum import versioning_manager from tests import TestCase +from pytest import mark class TestTransaction(TestCase): @@ -56,3 +57,31 @@ class User(self.Model): def test_copies_primary_key_type_from_user_class(self): attr = versioning_manager.transaction_cls.user_id assert isinstance(attr.property.columns[0].type, sa.Unicode) + + +@mark.skipif("os.environ.get('DB') == 'sqlite'") +class TestAssigningUserClassInOtherSchema(TestCase): + user_cls = 'User' + + def create_models(self): + class User(self.Model): + __tablename__ = 'user' + __versioned__ = { + 'base_classes': (self.Model,) + } + __table_args__ = {'schema': 'other'} + + id = sa.Column(sa.Unicode(255), primary_key=True) + name = sa.Column(sa.Unicode(255), nullable=False) + + self.User = User + + def create_tables(self): + self.connection.execute('DROP SCHEMA IF EXISTS other') + self.connection.execute('CREATE SCHEMA other') + TestCase.create_tables(self) + + def test_can_build_transaction_model(self): + # If create_models didn't crash this should be good + pass + From e2c7c5e3c4ba7b0b19502c543d26c27759cc4a18 Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Fri, 26 Feb 2016 23:51:16 -0500 Subject: [PATCH 02/90] transaction uses pkey column for user_id foreign key --- sqlalchemy_continuum/transaction.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index ce3b3de1..3a344c9b 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -147,9 +147,7 @@ class Transaction( user_id = sa.Column( sa.inspect(user_cls).primary_key[0].type, - sa.ForeignKey( - '%s.%s' % (user_cls.__tablename__, sa.inspect(user_cls).primary_key[0].name) - ), + sa.ForeignKey(sa.inspect(user_cls).primary_key[0]), index=True ) From 4f5ed91fcbf1fbb508d8898b62ded6e88953ddd4 Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Tue, 1 Mar 2016 10:26:38 -0500 Subject: [PATCH 03/90] table_builder copies schema of versioned table --- sqlalchemy_continuum/table_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlalchemy_continuum/table_builder.py b/sqlalchemy_continuum/table_builder.py index 8ac0de8f..2bfa06a9 100644 --- a/sqlalchemy_continuum/table_builder.py +++ b/sqlalchemy_continuum/table_builder.py @@ -150,5 +150,6 @@ def __call__(self, extends=None): extends.name if extends is not None else self.table_name, self.parent_table.metadata, *columns, + schema=self.parent_table.schema, extend_existing=extends is not None ) From 4b656c689028f93ee35dccbcfb32227e230f3440 Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Fri, 18 Mar 2016 11:23:17 -0400 Subject: [PATCH 04/90] Fix many-to-many versioning in separate schemas --- sqlalchemy_continuum/manager.py | 3 ++- sqlalchemy_continuum/relationship_builder.py | 4 +++- sqlalchemy_continuum/utils.py | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_continuum/manager.py b/sqlalchemy_continuum/manager.py index 779585be..ad10f410 100644 --- a/sqlalchemy_continuum/manager.py +++ b/sqlalchemy_continuum/manager.py @@ -400,7 +400,8 @@ def track_association_operations( if op is not None: table_name = statement.split(' ')[2] table_names = [ - table.name for table in self.association_tables + table.name if not table.schema else table.schema + '.' + table.name + for table in self.association_tables ] if table_name in table_names: if executemany: diff --git a/sqlalchemy_continuum/relationship_builder.py b/sqlalchemy_continuum/relationship_builder.py index 701013f4..393195c5 100644 --- a/sqlalchemy_continuum/relationship_builder.py +++ b/sqlalchemy_continuum/relationship_builder.py @@ -316,7 +316,9 @@ def build_association_version_tables(self): column.table ) metadata = column.table.metadata - if metadata.schema: + if builder.parent_table.schema: + table_name = builder.parent_table.schema + '.' + builder.table_name + elif metadata.schema: table_name = metadata.schema + '.' + builder.table_name else: table_name = builder.table_name diff --git a/sqlalchemy_continuum/utils.py b/sqlalchemy_continuum/utils.py index 372cc317..7db6d1db 100644 --- a/sqlalchemy_continuum/utils.py +++ b/sqlalchemy_continuum/utils.py @@ -133,7 +133,11 @@ def version_table(table): :param table: SQLAlchemy Table object """ - if table.metadata.schema: + if table.schema: + return table.metadata.tables[ + table.schema + '.' + table.name + '_version' + ] + elif table.metadata.schema: return table.metadata.tables[ table.metadata.schema + '.' + table.name + '_version' ] From 95a7a6584cedfe28c5eb2f8dda5eeddaeda78cd6 Mon Sep 17 00:00:00 2001 From: Tomek Paczkowski Date: Mon, 20 Jun 2016 16:06:26 +0100 Subject: [PATCH 05/90] Fixed `changeset` method for multi-flush transactions --- sqlalchemy_continuum/version.py | 4 +--- tests/test_changeset.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sqlalchemy_continuum/version.py b/sqlalchemy_continuum/version.py index 5c3c1ed2..d71e745d 100644 --- a/sqlalchemy_continuum/version.py +++ b/sqlalchemy_continuum/version.py @@ -1,4 +1,5 @@ import sqlalchemy as sa + from .reverter import Reverter from .utils import get_versioning_manager, is_internal_column, parent_class @@ -49,9 +50,6 @@ def changeset(self): and second list value as the new value. """ previous_version = self.previous - if not previous_version and self.operation_type != 0: - return {} - data = {} for key in sa.inspect(self.__class__).columns.keys(): diff --git a/tests/test_changeset.py b/tests/test_changeset.py index e13b1712..657c9d89 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -55,7 +55,11 @@ def test_changeset_for_history_that_does_not_have_first_insert(self): ''' % (self.transaction_column_name, tx_log.id) ) - assert self.session.query(self.ArticleVersion).first().changeset == {} + assert self.session.query(self.ArticleVersion).first().changeset == { + 'content': [None, 'some content'], + 'id': [None, 1], + 'name': [None, 'something'] + } class TestChangeSetWithValidityStrategy(ChangeSetTestCase): @@ -71,7 +75,7 @@ def create_models(self): class Article(self.Model): __tablename__ = 'article' __versioned__ = { - 'base_classes': (self.Model, ) + 'base_classes': (self.Model,) } id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) @@ -82,7 +86,7 @@ class Article(self.Model): class Tag(self.Model): __tablename__ = 'tag' __versioned__ = { - 'base_classes': (self.Model, ) + 'base_classes': (self.Model,) } id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) @@ -92,8 +96,8 @@ class Tag(self.Model): Article.tag_count = sa.orm.column_property( sa.select([sa.func.count(Tag.id)]) - .where(Tag.article_id == Article.id) - .correlate_except(Tag) + .where(Tag.article_id == Article.id) + .correlate_except(Tag) ) self.Article = Article From b8fa57585e9ae670444a19daf0f7a4f43ba4149b Mon Sep 17 00:00:00 2001 From: Forrest Pruitt Date: Thu, 24 Aug 2017 13:44:39 -0400 Subject: [PATCH 06/90] little grammar fix --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index e6002b87..b420d2bc 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -5,7 +5,7 @@ Introduction Why? ^^^^ -SQLAlchemy already has versioning extension. This extension however is very limited. It does not support versioning entire transactions. +SQLAlchemy already has a versioning extension. This extension however is very limited. It does not support versioning entire transactions. Hibernate for Java has Envers, which had nice features but lacks a nice API. Ruby on Rails has papertrail_, which has very nice API but lacks the efficiency and feature set of Envers. From 48f24918e2f55ad36de90708ec9d03246d7ca808 Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Mon, 4 Sep 2017 12:15:13 +0000 Subject: [PATCH 07/90] improve memory usage on large tables --- sqlalchemy_continuum/utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sqlalchemy_continuum/utils.py b/sqlalchemy_continuum/utils.py index 372cc317..3733c7d8 100644 --- a/sqlalchemy_continuum/utils.py +++ b/sqlalchemy_continuum/utils.py @@ -215,7 +215,7 @@ def versioned_relationships(obj, versioned_column_keys): yield prop -def vacuum(session, model): +def vacuum(session, model, yield_per=1000): """ When making structural changes to version tables (for example dropping columns) there are sometimes situations where some old version records @@ -236,6 +236,7 @@ def vacuum(session, model): :param session: SQLAlchemy session object :param model: SQLAlchemy declarative model class + :param yield_per: how many rows to process at a time """ version_cls = version_class(model) versions = defaultdict(list) @@ -243,15 +244,18 @@ def vacuum(session, model): query = ( session.query(version_cls) .order_by(option(version_cls, 'transaction_column_name')) - ) + ).yield_per(yield_per) + + primary_key_col = sa.inspection.inspect(model).primary_key[0].name for version in query: - if versions[version.id]: - prev_version = versions[version.id][-1] + version_id = getattr(version, primary_key_col) + if versions[version_id]: + prev_version = versions[version_id][-1] if naturally_equivalent(prev_version, version): session.delete(version) else: - versions[version.id].append(version) + versions[version_id].append(version) def is_internal_column(model, column_name): From 9cf6ebeaa0e6fe5abd1a1f37e482a11173c69b51 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Thu, 12 Oct 2017 13:57:14 +0300 Subject: [PATCH 08/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c9db766a..6faeb620 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.2 (2017-10-12) +^^^^^^^^^^^^^^^^^^ + +- Fixed multiple schema handling (#132, courtesy of vault) + + 1.3.1 (2017-06-28) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index bc32d298..2d6cec14 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.1' +__version__ = '1.3.2' versioning_manager = VersioningManager() From 486b6c800fb45fd98c51cb6f7323c10df3664593 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 5 Nov 2017 18:07:12 +0200 Subject: [PATCH 09/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6faeb620..5128b4e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.3 (2017-11-05) +^^^^^^^^^^^^^^^^^^ + +- Fixed changeset when updating object in same transaction as inserting it (#141, courtesy of oinopion) + + 1.3.2 (2017-10-12) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 2d6cec14..44cabfb5 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.2' +__version__ = '1.3.3' versioning_manager = VersioningManager() From 8399829b263d63ba5a2db663c601913f36bdf2d9 Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Mon, 4 Sep 2017 00:14:17 +0000 Subject: [PATCH 10/90] don't include excluded properties --- sqlalchemy_continuum/relationship_builder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_continuum/relationship_builder.py b/sqlalchemy_continuum/relationship_builder.py index 7cf62814..f3dc368d 100644 --- a/sqlalchemy_continuum/relationship_builder.py +++ b/sqlalchemy_continuum/relationship_builder.py @@ -346,7 +346,10 @@ def __call__(self): except ClassNotVersioned: self.remote_cls = self.property.mapper.class_ - if self.property.secondary is not None and not self.property.viewonly: + if (self.property.secondary is not None and + not self.property.viewonly and + not self.manager.is_excluded_property( + self.model, self.property.key)): self.build_association_version_tables() # store remote cls to association table column pairs From 4e222f203760744b3a2c98d6d6a3dbe843cbc1af Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Mon, 4 Sep 2017 12:00:10 +0000 Subject: [PATCH 11/90] add mypy & .cache to gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index a015a2d6..fe795a50 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,9 @@ nosetests.xml .mr.developer.cfg .project .pydevproject + +# mypy +.mypy_cache/ + +# Unit test / coverage reports +.cache From d540483319a74c0d687ae05131032bfc674443c9 Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Wed, 11 Oct 2017 14:30:51 +0000 Subject: [PATCH 12/90] add test for excluded relationship --- tests/test_column_inclusion_and_exclusion.py | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_column_inclusion_and_exclusion.py b/tests/test_column_inclusion_and_exclusion.py index e8530d2d..e916b383 100644 --- a/tests/test_column_inclusion_and_exclusion.py +++ b/tests/test_column_inclusion_and_exclusion.py @@ -53,3 +53,52 @@ class TextItem(self.Model): content = sa.Column('_content', sa.UnicodeText) self.TextItem = TextItem + + +class TestColumnExclusionWithRelationship(TestCase): + def create_models(self): + + class Word(self.Model): + __tablename__ = 'word' + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + word = sa.Column(sa.Unicode(255)) + + class TextItemWord(self.Model): + __tablename__ = 'text_item_word' + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + text_item_id = sa.Column(sa.Integer, sa.ForeignKey('text_item.id'), nullable=False) + word_id = sa.Column(sa.Integer, sa.ForeignKey('word.id'), nullable=False) + + class TextItem(self.Model): + __tablename__ = 'text_item' + __versioned__ = { + 'exclude': ['content'] + } + + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + content = sa.orm.relationship(Word, secondary='text_item_word') + + self.TextItem = TextItem + self.Word = Word + + def test_excluded_columns_not_included_in_version_class(self): + cls = version_class(self.TextItem) + manager = cls._sa_class_manager + assert 'content' not in manager.keys() + + def test_versioning_with_column_exclusion(self): + item = self.TextItem(name=u'Some textitem', + content=[self.Word(word=u'bird')]) + self.session.add(item) + self.session.commit() + + assert item.versions[0].name == u'Some textitem' + + def test_does_not_create_record_if_only_excluded_column_updated(self): + item = self.TextItem(name=u'Some textitem') + self.session.add(item) + self.session.commit() + item.content.append(self.Word(word=u'Some content')) + self.session.commit() + assert item.versions.count() == 1 From a9ac2fb28bbd086bb4bc1fe71798f6e6bb14635c Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Wed, 8 Nov 2017 22:59:51 +0000 Subject: [PATCH 13/90] add python 3.6, remove 3.3 to travis --- .travis.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6ee80dba..d5187594 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,9 @@ before_script: language: python python: - 2.7 - - 3.3 - 3.4 - 3.5 + - 3.6 install: - pip install -e ".[test]" script: diff --git a/setup.py b/setup.py index 439b821d..6dbcf5a6 100644 --- a/setup.py +++ b/setup.py @@ -77,9 +77,9 @@ def get_version(): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] From 9027b5dedae5fa1da65678a86275dbd38b787be7 Mon Sep 17 00:00:00 2001 From: tsantanaDH <36960083+tsantanaDH@users.noreply.github.com> Date: Mon, 5 Mar 2018 11:21:05 +0100 Subject: [PATCH 14/90] Update README.rst As stated on #101, quickstart example doesn't work as is. I've adapted the snippet to let it work without problem if someone copy and paste it. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b3dec67c..b6a2bda0 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ In order to make your models versioned you need two things: from sqlalchemy_continuum import make_versioned - make_versioned() + make_versioned(user_cls=None) class Article(Base): From 7181ffa83b407b6784642465ec7e8298278bb622 Mon Sep 17 00:00:00 2001 From: tsantanaDH <36960083+tsantanaDH@users.noreply.github.com> Date: Mon, 5 Mar 2018 11:22:41 +0100 Subject: [PATCH 15/90] Update intro.rst As stated on #101, quickstart example doesn't work as is. I've adapted the snippet to let it work without problem if someone copy and paste it. --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index b420d2bc..aca33422 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -54,7 +54,7 @@ In order to make your models versioned you need two things: from sqlalchemy_continuum import make_versioned - make_versioned() + make_versioned(user_cls=None) class Article(Base): From c4aad39608c3dd98b7f0091012656a5a657166ab Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Wed, 7 Mar 2018 20:19:51 +0200 Subject: [PATCH 16/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5128b4e4..1031c2f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.4 (2018-03-07) +^^^^^^^^^^^^^^^^^^ + +- Exclude many-to-many properties from versioning if they are added in exclude parameter (#169, courtesy of fuhrysteve) + + 1.3.3 (2017-11-05) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 44cabfb5..2bbc5015 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.3' +__version__ = '1.3.4' versioning_manager = VersioningManager() From f98469d3a0b9b7682c25b292fbdc30a40a5d1a17 Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Wed, 16 May 2018 16:47:17 +0200 Subject: [PATCH 17/90] Adding a unittest, reproducing #166 --- .../test_association_table_relations.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/relationships/test_association_table_relations.py diff --git a/tests/relationships/test_association_table_relations.py b/tests/relationships/test_association_table_relations.py new file mode 100644 index 00000000..81ec3739 --- /dev/null +++ b/tests/relationships/test_association_table_relations.py @@ -0,0 +1,61 @@ +import sqlalchemy as sa +from sqlalchemy import PrimaryKeyConstraint +from sqlalchemy.orm import relationship +from tests import TestCase, create_test_cases + + +class AssociationTableRelationshipsTestCase(TestCase): + def create_models(self): + super(AssociationTableRelationshipsTestCase, self).create_models() + + class PublishedArticle(self.Model): + __tablename__ = 'published_article' + __table_args__ = ( + PrimaryKeyConstraint("article_id", "author_id"), + {'useexisting': True} + ) + + article_id = sa.Column(sa.Integer, sa.ForeignKey('article.id')) + author_id = sa.Column(sa.Integer, sa.ForeignKey('author.id')) + author = relationship('Author') + article = relationship('Article') + + self.PublishedArticle = PublishedArticle + + published_articles_table = sa.Table(PublishedArticle.__tablename__, + PublishedArticle.metadata, + extend_existing=True) + + class Author(self.Model): + __tablename__ = 'author' + __versioned__ = { + 'base_classes': (self.Model, ) + } + + id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255)) + articles = relationship('Article', secondary=published_articles_table) + + self.Author = Author + + def test_version_relations(self): + article = self.Article() + name = u'Some article' + article.name = name + article.content = u'Some content' + self.session.add(article) + self.session.commit() + assert article.versions[0].name == name + + au = self.Author(name=u'Some author') + self.session.add(au) + self.session.commit() + + pa = self.PublishedArticle(article_id=article.id, author_id=au.id) + self.session.add(pa) + + self.session.commit() + + + +create_test_cases(AssociationTableRelationshipsTestCase) From acef01be8158f2869e25bae2729eb9396b92d1bc Mon Sep 17 00:00:00 2001 From: Fernando Cezar Date: Tue, 15 Aug 2017 15:12:29 +0200 Subject: [PATCH 18/90] track cloned connections Hotfix/track cloned connections (#2) Hotfix/track cloned connections (#3) change iterations use only ConnectionFairy as indexes create new dict entry for clones create entry using old connection create entry using session revert to event listening strategy remove cloned connections on rollback treat None case when cleaning connections Hotfix/track cloned connections (#2) Hotfix/track cloned connections (#3) change iterations use only ConnectionFairy as indexes create new dict entry for clones create entry using old connection create entry using session revert to event listening strategy remove cloned connections on rollback treat None case when cleaning connections --- sqlalchemy_continuum/__init__.py | 12 ++++++++++ sqlalchemy_continuum/manager.py | 38 ++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 5a46ced3..be6360a0 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -70,6 +70,12 @@ def make_versioned( manager.track_association_operations ) + sa.event.listen( + sa.engine.Engine, + 'set_connection_execution_options', + manager.track_cloned_connections + ) + def remove_versioning( mapper=sa.orm.mapper, @@ -96,3 +102,9 @@ def remove_versioning( 'before_cursor_execute', manager.track_association_operations ) + + sa.event.remove( + sa.engine.Engine, + 'set_connection_execution_options', + manager.track_cloned_connections + ) diff --git a/sqlalchemy_continuum/manager.py b/sqlalchemy_continuum/manager.py index 779585be..a4ec335e 100644 --- a/sqlalchemy_continuum/manager.py +++ b/sqlalchemy_continuum/manager.py @@ -24,7 +24,15 @@ def wrapper(self, mapper, connection, target): try: uow = self.units_of_work[conn] except KeyError: - uow = self.units_of_work[conn.engine] + try: + uow = self.units_of_work[conn.engine] + except KeyError: + for connection in self.units_of_work.keys(): + if connection.connection is conn.connection: + uow = self.unit_of_work(session) + break # The ConnectionFairy is the same, this connection is a clone + else: + raise KeyError return func(self, uow, target) return wrapper @@ -357,11 +365,20 @@ def clear(self, session): if session.transaction.nested: return conn = self.session_connection_map.pop(session, None) + if conn is None: + return + if conn in self.units_of_work: uow = self.units_of_work[conn] uow.reset(session) del self.units_of_work[conn] + for connection in dict(self.units_of_work).keys(): + if conn.connection is connection.connection: + uow = self.units_of_work[connection] + uow.reset(session) + del self.units_of_work[connection] + def append_association_operation(self, conn, table_name, params, op): """ Append history association operation to pending_statements list. @@ -375,9 +392,26 @@ def append_association_operation(self, conn, table_name, params, op): try: uow = self.units_of_work[conn] except KeyError: - uow = self.units_of_work[conn.engine] + try: + uow = self.units_of_work[conn.engine] + except KeyError: + for connection in self.units_of_work.keys(): + if connection.connection is conn.connection: + uow = self.unit_of_work(conn.session) + break # The ConnectionFairy is the same, this connection is a clone + else: + raise KeyError uow.pending_statements.append(stmt) + def track_cloned_connections(self, c, opt): + """ + Track cloned connections from association tables. + """ + if c not in self.units_of_work.keys(): + for connection, uow in dict(self.units_of_work).items(): + if connection.connection is c.connection: # ConnectionFairy is the same - this is a clone + self.units_of_work[c] = uow + def track_association_operations( self, conn, cursor, statement, parameters, context, executemany ): From d4ed900347b6accd6ea9b018ec5e985358be0fe6 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 3 Jun 2018 14:25:28 +0300 Subject: [PATCH 19/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1031c2f9..a1fa3a33 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.5 (2018-06-03) +^^^^^^^^^^^^^^^^^^ + +- Track cloned connections (#167, courtesy of netcriptus) + + 1.3.4 (2018-03-07) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index be2a580f..51806ee7 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.4' +__version__ = '1.3.5' versioning_manager = VersioningManager() From 79b98764bb2931220501509b268c64e123be2412 Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Thu, 7 Jun 2018 18:49:20 +0000 Subject: [PATCH 20/90] Reraise original exception instead of a new one Raising a new KeyError means you lose the original stacktrace, which is going to be more useful since it'll contain more information about the relevant context for this error. --- sqlalchemy_continuum/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/manager.py b/sqlalchemy_continuum/manager.py index 766780e9..641e9521 100644 --- a/sqlalchemy_continuum/manager.py +++ b/sqlalchemy_continuum/manager.py @@ -32,7 +32,7 @@ def wrapper(self, mapper, connection, target): uow = self.unit_of_work(session) break # The ConnectionFairy is the same, this connection is a clone else: - raise KeyError + raise return func(self, uow, target) return wrapper @@ -400,7 +400,7 @@ def append_association_operation(self, conn, table_name, params, op): uow = self.unit_of_work(conn.session) break # The ConnectionFairy is the same, this connection is a clone else: - raise KeyError + raise uow.pending_statements.append(stmt) def track_cloned_connections(self, c, opt): From 8496cdd2357439afb12de405ea66c5d65e10d98c Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Thu, 26 Jul 2018 15:37:49 -0400 Subject: [PATCH 21/90] Test for leaked connections on external rollback --- tests/test_sessions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index f5780f89..6a1fbfb0 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -52,3 +52,19 @@ class TestUnitOfWork(TestCase): def test_with_session_arg(self): uow = versioning_manager.unit_of_work(self.session) assert isinstance(uow, UnitOfWork) + + +class TestExternalTransactionSession(TestCase): + + def test_session_with_external_transaction(self): + conn = self.engine.connect() + t = conn.begin() + session = Session(bind=conn) + + article = self.Article(name=u'My Session Article') + session.add(article) + session.flush() + + session.close() + t.rollback() + conn.close() From 3a5b967cad6087fdb95c66bf625cbb24d79299b1 Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Thu, 26 Jul 2018 16:07:50 -0400 Subject: [PATCH 22/90] Check connection.closed before connection.connection --- sqlalchemy_continuum/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sqlalchemy_continuum/manager.py b/sqlalchemy_continuum/manager.py index 641e9521..f8c11f4f 100644 --- a/sqlalchemy_continuum/manager.py +++ b/sqlalchemy_continuum/manager.py @@ -28,7 +28,7 @@ def wrapper(self, mapper, connection, target): uow = self.units_of_work[conn.engine] except KeyError: for connection in self.units_of_work.keys(): - if connection.connection is conn.connection: + if not connection.closed and connection.connection is conn.connection: uow = self.unit_of_work(session) break # The ConnectionFairy is the same, this connection is a clone else: @@ -374,7 +374,7 @@ def clear(self, session): del self.units_of_work[conn] for connection in dict(self.units_of_work).keys(): - if conn.connection is connection.connection: + if connection.closed or conn.connection is connection.connection: uow = self.units_of_work[connection] uow.reset(session) del self.units_of_work[connection] @@ -396,7 +396,7 @@ def append_association_operation(self, conn, table_name, params, op): uow = self.units_of_work[conn.engine] except KeyError: for connection in self.units_of_work.keys(): - if connection.connection is conn.connection: + if not connection.closed and connection.connection is conn.connection: uow = self.unit_of_work(conn.session) break # The ConnectionFairy is the same, this connection is a clone else: @@ -409,7 +409,7 @@ def track_cloned_connections(self, c, opt): """ if c not in self.units_of_work.keys(): for connection, uow in dict(self.units_of_work).items(): - if connection.connection is c.connection: # ConnectionFairy is the same - this is a clone + if not connection.closed and connection.connection is c.connection: # ConnectionFairy is the same - this is a clone self.units_of_work[c] = uow def track_association_operations( From 4ded9b31bf310a1eadf055e0e7f1ce4fa213820a Mon Sep 17 00:00:00 2001 From: Michael Abed Date: Thu, 26 Jul 2018 16:09:19 -0400 Subject: [PATCH 23/90] Invalidate units_of_work on Engine rollback event --- sqlalchemy_continuum/__init__.py | 12 ++++++++++++ sqlalchemy_continuum/manager.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 51806ee7..60727e74 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -70,6 +70,12 @@ def make_versioned( manager.track_association_operations ) + sa.event.listen( + sa.engine.Engine, + 'rollback', + manager.clear_connection + ) + sa.event.listen( sa.engine.Engine, 'set_connection_execution_options', @@ -103,6 +109,12 @@ def remove_versioning( manager.track_association_operations ) + sa.event.remove( + sa.engine.Engine, + 'rollback', + manager.clear_connection + ) + sa.event.remove( sa.engine.Engine, 'set_connection_execution_options', diff --git a/sqlalchemy_continuum/manager.py b/sqlalchemy_continuum/manager.py index f8c11f4f..0e945d53 100644 --- a/sqlalchemy_continuum/manager.py +++ b/sqlalchemy_continuum/manager.py @@ -379,6 +379,25 @@ def clear(self, session): uow.reset(session) del self.units_of_work[connection] + def clear_connection(self, conn): + if conn in self.units_of_work: + uow = self.units_of_work[conn] + uow.reset() + del self.units_of_work[conn] + + + for session, connection in dict(self.session_connection_map).items(): + if connection is conn: + del self.session_connection_map[session] + + + for connection in dict(self.units_of_work).keys(): + if connection.closed or conn.connection is connection.connection: + uow = self.units_of_work[connection] + uow.reset() + del self.units_of_work[connection] + + def append_association_operation(self, conn, table_name, params, op): """ Append history association operation to pending_statements list. From cedad54f1092bbb940a837ed446640bdb2ff8101 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Mon, 30 Jul 2018 15:48:20 +0300 Subject: [PATCH 24/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a1fa3a33..1d98e6dd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.6 (2018-07-30) +^^^^^^^^^^^^^^^^^^ + +- Fixed ResourceClosedErrors from connections leaking when using an external transaction (#196, courtesy of vault) + + 1.3.5 (2018-06-03) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 60727e74..8e212af1 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.5' +__version__ = '1.3.6' versioning_manager = VersioningManager() From c7d4580b9146a374576439319e59b5efcef19bd8 Mon Sep 17 00:00:00 2001 From: Ken Celenza Date: Sat, 29 Sep 2018 12:14:24 -0400 Subject: [PATCH 25/90] Working example in Readme I for one, love when there is a working example in the readme. --- README.rst | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.rst b/README.rst index b6a2bda0..ee9b1c4f 100644 --- a/README.rst +++ b/README.rst @@ -74,6 +74,39 @@ In order to make your models versioned you need two things: article.name # u'Some article' +For completeness, below is a working example. + +.. code-block:: python + + from sqlalchemy_continuum import make_versioned + from sqlalchemy import Column, Integer, Unicode, UnicodeText, create_engine + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.orm import create_session, configure_mappers + + make_versioned(user_cls=None) + + Base = declarative_base() + class Article(Base): + __versioned__ = {} + __tablename__ = 'article' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Unicode(255)) + content = Column(UnicodeText) + + configure_mappers() + engine = create_engine('sqlite://') + Base.metadata.create_all(engine) + session = create_session(bind=engine, autocommit=False) + + article = Article(name=u'Some article', content=u'Some content') + session.add(article) + session.commit() + article.versions[0].name + article.name = u'Updated name' + session.commit() + article.versions[1].name + article.versions[0].revert() + article.name Resources --------- From 472513f06d175b2202ee8f542b508f476a6530ec Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 17 Nov 2018 17:58:22 +0000 Subject: [PATCH 26/90] Assume Python 3 and write string without u prefix Python 3 is well established. We can write example string literals without the u prefix. --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b6a2bda0..d4a5a49c 100644 --- a/README.rst +++ b/README.rst @@ -53,26 +53,26 @@ In order to make your models versioned you need two things: content = sa.Column(sa.UnicodeText) - article = Article(name=u'Some article', content=u'Some content') + article = Article(name='Some article', content='Some content') session.add(article) session.commit() # article has now one version stored in database article.versions[0].name - # u'Some article' + # 'Some article' - article.name = u'Updated name' + article.name = 'Updated name' session.commit() article.versions[1].name - # u'Updated name' + # 'Updated name' # lets revert back to first version article.versions[0].revert() article.name - # u'Some article' + # 'Some article' Resources From 1291a9ad8d542a700075158ef037cc6899dee3e6 Mon Sep 17 00:00:00 2001 From: Lyndsy Simon Date: Tue, 8 Jan 2019 12:58:33 -0600 Subject: [PATCH 27/90] Fix typo in docs --- docs/plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index 5f88b6fe..09c1e261 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -7,7 +7,7 @@ Using plugins :: - from sqlalchemy.continuum.plugins import PropertyModTrackerPlugin + from sqlalchemy_continuum.plugins import PropertyModTrackerPlugin versioning_manager.plugins.append(PropertyModTrackerPlugin()) From fb6b1e970e219cdd0618879fa39b4ece58ec2157 Mon Sep 17 00:00:00 2001 From: Lyndsy Simon Date: Fri, 11 Jan 2019 15:43:51 -0600 Subject: [PATCH 28/90] Fix trigger creation during alembic migrations --- sqlalchemy_continuum/transaction.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index 3a344c9b..72959561 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -1,4 +1,5 @@ from datetime import datetime +from functools import partial try: from collections import OrderedDict @@ -73,10 +74,25 @@ def changed_entities(self): """ -def create_triggers(cls): +def _after_create(cls, ddl): + """Execute DDL after a table is created + + This is required for compatibility with alembic, as the `after_create` event + does not fire during alembic migrations""" + def listener(tablename, ddl, table, bind, **kw): + if table.name == tablename: + ddl(table, bind, **kw) + sa.event.listen( - cls.__table__, - 'after_create', + sa.Table, + 'after_create', + partial(listener, cls.__table__.name, ddl) + ) + + +def create_triggers(cls): + _after_create( + cls, sa.schema.DDL( procedure_sql.format( temporary_transaction_sql=CreateTemporaryTransactionTableSQL(), @@ -88,9 +104,8 @@ def create_triggers(cls): ) ) ) - sa.event.listen( - cls.__table__, - 'after_create', + _after_create( + cls, sa.schema.DDL(str(TransactionTriggerSQL(cls))) ) sa.event.listen( From 77c17195c1288fbef8de911df12fa105890f2cf8 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 13 Jan 2019 16:09:38 +0200 Subject: [PATCH 29/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1d98e6dd..9d70761b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.7 (2019-01-13) +^^^^^^^^^^^^^^^^^^ + +Fix trigger creation during alembic migrations (#209, courtesy of lyndsysimon) + + 1.3.6 (2018-07-30) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 8e212af1..87df7772 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.6' +__version__ = '1.3.7' versioning_manager = VersioningManager() From 007a0cc2ac87cabf788a11ffa6d32519c7677149 Mon Sep 17 00:00:00 2001 From: Paulo R Date: Tue, 26 Feb 2019 09:26:40 +0200 Subject: [PATCH 30/90] Removed non-table columns from the restore operation When a table contains a *sa.orm.column_property* the restore operation crashes if the column is not on the ignore list. This change filters out those invalid columns (non-table) to remove the need for this declaration. --- sqlalchemy_continuum/utils.py | 6 +++++- tests/__init__.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/utils.py b/sqlalchemy_continuum/utils.py index c622760e..080f3124 100644 --- a/sqlalchemy_continuum/utils.py +++ b/sqlalchemy_continuum/utils.py @@ -202,7 +202,11 @@ def versioned_column_properties(obj_or_class): cls = obj_or_class if isclass(obj_or_class) else obj_or_class.__class__ mapper = sa.inspect(cls) - for key in mapper.columns.keys(): + for key, column in mapper.columns.items(): + # Ignores non table columns + if not isinstance(column, sa.Column): + continue + if not manager.is_excluded_property(obj_or_class, key): yield getattr(mapper.attrs, key) diff --git a/tests/__init__.py b/tests/__init__.py index 310e9bb6..3a96eb0f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,4 @@ + from copy import copy import inspect import itertools as it @@ -6,7 +7,7 @@ import sqlalchemy as sa from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, column_property from sqlalchemy_continuum import ( ClassNotVersioned, version_class, @@ -148,6 +149,9 @@ class Article(self.Model): content = sa.Column(sa.UnicodeText) description = sa.Column(sa.UnicodeText) + # Dynamic column cotaining all text content data + fulltext_content = column_property(name + content + description) + class Tag(self.Model): __tablename__ = 'tag' __versioned__ = copy(self.options) From 0cb3e6b1b5ae500b9072328583cc70fe061c9f8b Mon Sep 17 00:00:00 2001 From: Paulo R Date: Tue, 26 Feb 2019 10:58:00 +0200 Subject: [PATCH 31/90] Added is_table_column function Added extra missing validation. --- sqlalchemy_continuum/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/utils.py b/sqlalchemy_continuum/utils.py index 080f3124..efca7b6f 100644 --- a/sqlalchemy_continuum/utils.py +++ b/sqlalchemy_continuum/utils.py @@ -204,7 +204,7 @@ def versioned_column_properties(obj_or_class): mapper = sa.inspect(cls) for key, column in mapper.columns.items(): # Ignores non table columns - if not isinstance(column, sa.Column): + if not is_table_column(column): continue if not manager.is_excluded_property(obj_or_class, key): @@ -266,6 +266,16 @@ def vacuum(session, model, yield_per=1000): versions[version_id].append(version) +def is_table_column(column): + """ + Return wheter of not give field is a column over the database table. + + :param column: SQLAclhemy model field. + :rtype: bool + """ + return isinstance(column, sa.Column) + + def is_internal_column(model, column_name): """ Return whether or not given column of given SQLAlchemy declarative classs @@ -410,7 +420,10 @@ def changeset(obj): data = {} session = sa.orm.object_session(obj) if session and obj in session.deleted: - for column in sa.inspect(obj.__class__).columns.values(): + columns = [c for c in sa.inspect(obj.__class__).columns.values() + if is_table_column(c)] + + for column in columns: if not column.primary_key: value = getattr(obj, column.key) if value is not None: From f549c4004223997e4ccd4ea6640b6e1fdf8e3450 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Wed, 27 Feb 2019 17:09:27 +0200 Subject: [PATCH 32/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9d70761b..7a276f85 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.8 (2019-02-27) +^^^^^^^^^^^^^^^^^^ + +- Fixed revert to ignore non-columns (#197, courtesy of mauler) + + 1.3.7 (2019-01-13) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 87df7772..bb0c3bc5 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.7' +__version__ = '1.3.8' versioning_manager = VersioningManager() From 0cd9584fcd539a5d6417aa168c41eb67a4d7f297 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Mon, 18 Mar 2019 14:57:11 +0200 Subject: [PATCH 33/90] Add SA 1.3 support --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- sqlalchemy_continuum/relationship_builder.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7a276f85..9a94f54e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.9 (2019-03-18) +^^^^^^^^^^^^^^^^^^ + +- Added SA 1.3 support + + 1.3.8 (2019-02-27) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index bb0c3bc5..4de143f4 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.8' +__version__ = '1.3.9' versioning_manager = VersioningManager() diff --git a/sqlalchemy_continuum/relationship_builder.py b/sqlalchemy_continuum/relationship_builder.py index f3dc368d..73a2ef78 100644 --- a/sqlalchemy_continuum/relationship_builder.py +++ b/sqlalchemy_continuum/relationship_builder.py @@ -355,7 +355,7 @@ def __call__(self): # store remote cls to association table column pairs self.remote_to_association_column_pairs = [] for column_pair in self.property.local_remote_pairs: - if column_pair[0] in self.property.table.c.values(): + if column_pair[0] in self.property.target.c.values(): self.remote_to_association_column_pairs.append(column_pair) setattr( From b49936ae87673633d924c702e458a97abcfa742f Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Wed, 27 Feb 2019 17:22:58 +0200 Subject: [PATCH 34/90] Revert fb6b1e970e --- sqlalchemy_continuum/transaction.py | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index 72959561..d1b85534 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -74,25 +74,10 @@ def changed_entities(self): """ -def _after_create(cls, ddl): - """Execute DDL after a table is created - - This is required for compatibility with alembic, as the `after_create` event - does not fire during alembic migrations""" - def listener(tablename, ddl, table, bind, **kw): - if table.name == tablename: - ddl(table, bind, **kw) - - sa.event.listen( - sa.Table, - 'after_create', - partial(listener, cls.__table__.name, ddl) - ) - - def create_triggers(cls): - _after_create( - cls, + sa.event.listen( + cls.__table__, + 'after_create', sa.schema.DDL( procedure_sql.format( temporary_transaction_sql=CreateTemporaryTransactionTableSQL(), @@ -104,8 +89,9 @@ def create_triggers(cls): ) ) ) - _after_create( - cls, + sa.event.listen( + cls.__table__, + 'after_create', sa.schema.DDL(str(TransactionTriggerSQL(cls))) ) sa.event.listen( From 239a3418e3bc2f66df0d2ae040ce231f3bf59de5 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Tue, 19 Mar 2019 15:10:56 +0200 Subject: [PATCH 35/90] Update changes --- CHANGES.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9a94f54e..958eb9a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,10 +4,11 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. -1.3.9 (2019-03-18) +1.3.9 (2019-03-19) ^^^^^^^^^^^^^^^^^^ - Added SA 1.3 support +- Reverted trigger creation from 1.3.7 1.3.8 (2019-02-27) @@ -19,7 +20,7 @@ Here you can see the full list of changes between each SQLAlchemy-Continuum rele 1.3.7 (2019-01-13) ^^^^^^^^^^^^^^^^^^ -Fix trigger creation during alembic migrations (#209, courtesy of lyndsysimon) +- Fix trigger creation during alembic migrations (#209, courtesy of lyndsysimon) 1.3.6 (2018-07-30) From fcb182901e6fae1eb23b96ec9a633e0d22213b51 Mon Sep 17 00:00:00 2001 From: yetem Date: Wed, 7 Aug 2019 23:48:03 +0200 Subject: [PATCH 36/90] Issue #226 --- sqlalchemy_continuum/plugins/activity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlalchemy_continuum/plugins/activity.py b/sqlalchemy_continuum/plugins/activity.py index 8905079a..e6886de8 100644 --- a/sqlalchemy_continuum/plugins/activity.py +++ b/sqlalchemy_continuum/plugins/activity.py @@ -314,6 +314,8 @@ def target_version_type(cls): class ActivityPlugin(Plugin): + activity_cls = None + def after_build_models(self, manager): self.activity_cls = ActivityFactory()(manager) manager.activity_cls = self.activity_cls From 591f9f010c9f219e89839f688dbbe2550c2d41c6 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Tue, 16 Oct 2018 12:34:23 +0200 Subject: [PATCH 37/90] test against python3.7 --- .travis.yml | 4 ++++ setup.py | 1 + tox.ini | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d5187594..174b7c0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ addons: postgresql: 9.3 +dist: xenial +sudo: true + env: - DB=mysql - DB=postgres @@ -18,6 +21,7 @@ python: - 3.4 - 3.5 - 3.6 + - 3.7 install: - pip install -e ".[test]" script: diff --git a/setup.py b/setup.py index 6dbcf5a6..b6543282 100644 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ def get_version(): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] diff --git a/tox.ini b/tox.ini index 0f2033d4..2d9b2fd2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py33, py34, py35 +envlist = py27, py33, py34, py35, py36, py37 [testenv] commands = pip install -e ".[test]" From fe8b0402406942393ba70fc1cd24e04e316707ea Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Mon, 9 Sep 2019 23:00:56 +0200 Subject: [PATCH 38/90] add mysql to travis explicitly --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 174b7c0d..cff4efef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ addons: postgresql: 9.3 +services: + - mysql + dist: xenial sudo: true From 06ce8bc0ad529f65bf5183a8b05c14d4c9554184 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Mon, 9 Sep 2019 23:04:26 +0200 Subject: [PATCH 39/90] added postgresql as service to travis.yml --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index cff4efef..035bf151 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ -addons: - postgresql: 9.3 - services: - mysql + - postgresql dist: xenial sudo: true From 8902be723544bb90c6a54f2f62fb8a7dd7048889 Mon Sep 17 00:00:00 2001 From: Nikolay Shebanov Date: Thu, 7 Nov 2019 18:30:24 +0100 Subject: [PATCH 40/90] Add a link to the SQLAlchemy history_meta example I stumbled across this mention of an SQLAlchemy versioning extension a few times, and googling it up every time is embarassing. Did I get it right this time, after all? --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index aca33422..11b12306 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -5,7 +5,7 @@ Introduction Why? ^^^^ -SQLAlchemy already has a versioning extension. This extension however is very limited. It does not support versioning entire transactions. +SQLAlchemy [already has a versioning extension](https://docs.sqlalchemy.org/en/13/orm/examples.html#module-examples.versioned_history). This extension however is very limited. It does not support versioning entire transactions. Hibernate for Java has Envers, which had nice features but lacks a nice API. Ruby on Rails has papertrail_, which has very nice API but lacks the efficiency and feature set of Envers. From 5ab6fbb26820d4c1ea4629b88dba8b962c4a63fe Mon Sep 17 00:00:00 2001 From: Nikolay Shebanov Date: Fri, 8 Nov 2019 10:02:36 +0100 Subject: [PATCH 41/90] Fix a link formatting from markdown to rst The previous commit was added through the github editor without looking at the preview. --- docs/intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro.rst b/docs/intro.rst index 11b12306..8b276994 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -5,7 +5,7 @@ Introduction Why? ^^^^ -SQLAlchemy [already has a versioning extension](https://docs.sqlalchemy.org/en/13/orm/examples.html#module-examples.versioned_history). This extension however is very limited. It does not support versioning entire transactions. +SQLAlchemy `already has a versioning extension `_. This extension however is very limited. It does not support versioning entire transactions. Hibernate for Java has Envers, which had nice features but lacks a nice API. Ruby on Rails has papertrail_, which has very nice API but lacks the efficiency and feature set of Envers. From 36cd474617c1a5c0ad4b819873bc1daa2bfdf3bb Mon Sep 17 00:00:00 2001 From: Jakub Kuszneruk Date: Thu, 12 Dec 2019 16:58:27 +0100 Subject: [PATCH 42/90] #128 Update docs regarding `alembic-migrations` --- docs/alembic.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/alembic.rst b/docs/alembic.rst index 562ebb34..d46a4700 100644 --- a/docs/alembic.rst +++ b/docs/alembic.rst @@ -1,6 +1,11 @@ Alembic migrations ================== -Each time you make changes to database structure you should also change the associated history tables. When you make changes to your models SQLAlchemy-Continuum automatically alters the history model definitions, hence you can use `alembic revision --autogenerate` just like before. You just need to make sure `make_versioned` function gets called before alembic gathers all your models. +Each time you make changes to database structure you should also change the associated history tables. When you make changes to your models SQLAlchemy-Continuum automatically alters the history model definitions, hence you can use `alembic revision --autogenerate` just like before. You just need to make sure `make_versioned` function gets called before alembic gathers all your models and `configure_mappers` is called afterwards. Pay close attention when dropping or moving data from parent tables and reflecting these changes to history tables. + +Troubleshooting +############### + +If alembic didn't detect any changes or generates reversed migration (tries to remove `*_version` tables from database instead of creating), make sure that `configure_mappers` was called by alembic command. From 2c4d79524575fb386f2d0089d66510a26aa5767c Mon Sep 17 00:00:00 2001 From: Andrew Dickinson Date: Fri, 17 Apr 2020 13:51:05 -0400 Subject: [PATCH 43/90] Add test to expose bug on unrelated change with m2m --- .../test_many_to_many_relations.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/relationships/test_many_to_many_relations.py b/tests/relationships/test_many_to_many_relations.py index 2aaa1196..5398fe64 100644 --- a/tests/relationships/test_many_to_many_relations.py +++ b/tests/relationships/test_many_to_many_relations.py @@ -70,6 +70,33 @@ def test_single_insert(self): self.session.commit() assert len(article.versions[0].tags) == 1 + def test_unrelated_change(self): + tag1 = self.Tag(name=u'some tag') + tag2 = self.Tag(name=u'some tag2') + + self.session.add(tag1) + self.session.add(tag2) + self.session.commit() + + article1 = self.Article(name="Some article", ) + article1.name = u'Some article' + article1.tags.append(tag1) + + self.session.add(article1) + self.session.commit() + + article2 = self.Article() + article2.name = u'Some article2' + article2.tags.append(tag1) + + self.session.add(article2) + self.session.commit() + + article1.name = u'Some other name' + self.session.commit() + + assert len(article1.versions[1].tags) == 1 + def test_multi_insert(self): article = self.Article() article.name = u'Some article' From f7e38b1fbaabd5797458124a33f0c4152456d8f9 Mon Sep 17 00:00:00 2001 From: Andrew Dickinson Date: Fri, 17 Apr 2020 13:52:26 -0400 Subject: [PATCH 44/90] Add fix to m2m unrelated change bug --- sqlalchemy_continuum/relationship_builder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sqlalchemy_continuum/relationship_builder.py b/sqlalchemy_continuum/relationship_builder.py index 73a2ef78..e4888a24 100644 --- a/sqlalchemy_continuum/relationship_builder.py +++ b/sqlalchemy_continuum/relationship_builder.py @@ -249,6 +249,7 @@ def association_subquery(self, obj): FROM article_tag_version as article_tag_version2 WHERE article_tag_version2.tag_id = article_tag_version.tag_id AND article_tag_version2.tx_id <=5 + AND article_tag_version2.article_id = 3 GROUP BY article_tag_version2.tag_id HAVING MAX(article_tag_version2.tx_id) = @@ -260,6 +261,8 @@ def association_subquery(self, obj): """ tx_column = option(obj, 'transaction_column_name') + join_column = self.property.primaryjoin.right.name + object_join_column = self.property.primaryjoin.left.name reflector = VersionExpressionReflector(obj, self.property) association_table_alias = self.association_version_table.alias() @@ -276,6 +279,7 @@ def association_subquery(self, obj): sa.and_( association_table_alias.c[tx_column] <= getattr(obj, tx_column), + association_table_alias.c[join_column] == getattr(obj, object_join_column), *[association_col == self.association_version_table.c[association_col.name] for association_col From 8c0c3065de52be188648f43cb12c093db24f7136 Mon Sep 17 00:00:00 2001 From: vhermecz Date: Tue, 28 Apr 2020 15:49:27 +0200 Subject: [PATCH 45/90] Fix order of old-new values in changeset example for create --- docs/version_objects.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/version_objects.rst b/docs/version_objects.rst index ecbb71ce..54f0b774 100644 --- a/docs/version_objects.rst +++ b/docs/version_objects.rst @@ -102,7 +102,7 @@ you can easily check the changeset of given object in current transaction. article = Article(name=u'Some article') changeset(article) - # {'name': [u'Some article', None]} + # {'name': [None, u'Some article']} Version relationships From 197470f6cb1db6a3c107261b95331e84005ed6bf Mon Sep 17 00:00:00 2001 From: Nikolay Shebanov Date: Sat, 2 May 2020 18:10:44 +0200 Subject: [PATCH 46/90] Fix the login/logout helpers in the FlaskPlugin tests Tests were failing since the FlaskLogin extension has prefixed internal variable names with an underscore: https://github.com/maxcountryman/flask-login/pull/470 --- tests/plugins/test_flask.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/plugins/test_flask.py b/tests/plugins/test_flask.py index b81d6a65..7f962c99 100644 --- a/tests/plugins/test_flask.py +++ b/tests/plugins/test_flask.py @@ -66,12 +66,12 @@ def login(self, user): :returns: the logged in user """ with self.client.session_transaction() as s: - s['user_id'] = user.id + s['_user_id'] = user.id return user def logout(self, user=None): with self.client.session_transaction() as s: - s['user_id'] = None + s['_user_id'] = None def create_models(self): TestCase.create_models(self) @@ -281,5 +281,3 @@ def test_create_transaction_with_scoped_session(self): uow = versioning_manager.unit_of_work(self.db.session) transaction = uow.create_transaction(self.db.session) assert transaction.id - - From f2ee5b63b4c2b5ba0db699384eff485a57044ea6 Mon Sep 17 00:00:00 2001 From: Nikolay Shebanov Date: Wed, 6 May 2020 21:28:18 +0200 Subject: [PATCH 47/90] Fix #245: create column aliases in the version model --- sqlalchemy_continuum/builder.py | 42 ++++++++++++++++--- sqlalchemy_continuum/model_builder.py | 1 + .../test_single_table_inheritance.py | 8 ++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/sqlalchemy_continuum/builder.py b/sqlalchemy_continuum/builder.py index 47bbef35..8a2f219c 100644 --- a/sqlalchemy_continuum/builder.py +++ b/sqlalchemy_continuum/builder.py @@ -144,14 +144,16 @@ def build_transaction_class(self): def configure_versioned_classes(self): """ Configures all versioned classes that were collected during - instrumentation process. The configuration has 4 steps: + instrumentation process. The configuration has 6 steps: 1. Build tables for version models. 2. Build the actual version model declarative classes. 3. Build relationships between these models. 4. Empty pending_classes list so that consecutive mapper configuration does not create multiple version classes - 5. Assign all versioned attributes to use active history. + 5. Build aliases for columns. + 6. Assign all versioned attributes to use active history. + """ if not self.manager.options['versioning']: return @@ -168,11 +170,39 @@ def configure_versioned_classes(self): # Create copy of all pending versioned classes so that we can inspect # them later when creating relationships. - pending_copy = copy(self.manager.pending_classes) + pending_classes_copies = copy(self.manager.pending_classes) self.manager.pending_classes = [] - self.build_relationships(pending_copy) + self.build_relationships(pending_classes_copies) + self.enable_active_history(pending_classes_copies) + self.create_column_aliases(pending_classes_copies) - for cls in pending_copy: - # set the "active_history" flag + def enable_active_history(self, version_classes): + """ + Assign all versioned attributes to use active history. + """ + for cls in version_classes: for prop in sa.inspect(cls).iterate_properties: getattr(cls, prop.key).impl.active_history = True + + def create_column_aliases(self, version_classes): + """ + Create aliases for the columns from the original model. + + This, for example, imitates the behavior of @declared_attr columns. + """ + for cls in version_classes: + model_mapper = sa.inspect(cls) + version_class = self.manager.version_class_map.get(cls) + if not version_class: + continue + + version_class_mapper = sa.inspect(version_class) + + for key, column in model_mapper.columns.items(): + if key != column.key: + version_class_column = version_class.__table__.c.get(column.key) + + if version_class_column is None: + continue + + version_class_mapper.add_property(key, sa.orm.column_property(version_class_column)) diff --git a/sqlalchemy_continuum/model_builder.py b/sqlalchemy_continuum/model_builder.py index 2be6e63b..1d29d68c 100644 --- a/sqlalchemy_continuum/model_builder.py +++ b/sqlalchemy_continuum/model_builder.py @@ -261,6 +261,7 @@ def mapper_args(cls): name = '%sVersion' % (self.model.__name__,) return type(name, self.base_classes(), args) + def __call__(self, table, tx_class): """ Build history model and relationships to parent model, transaction diff --git a/tests/inheritance/test_single_table_inheritance.py b/tests/inheritance/test_single_table_inheritance.py index 9b723c15..9a259a54 100644 --- a/tests/inheritance/test_single_table_inheritance.py +++ b/tests/inheritance/test_single_table_inheritance.py @@ -1,4 +1,5 @@ import sqlalchemy as sa +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy_continuum import versioning_manager, version_class from tests import TestCase, create_test_cases @@ -25,6 +26,10 @@ class Article(TextItem): __mapper_args__ = {'polymorphic_identity': u'article'} name = sa.Column(sa.Unicode(255)) + @sa.ext.declarative.declared_attr + def status(cls): + return sa.Column("_status", sa.Unicode(255)) + class BlogPost(TextItem): __mapper_args__ = {'polymorphic_identity': u'blog_post'} title = sa.Column(sa.Unicode(255)) @@ -79,5 +84,8 @@ def test_transaction_changed_entities(self): assert transaction.entity_names == [u'Article'] assert transaction.changed_entities + def test_declared_attr_inheritance(self): + assert self.ArticleVersion.status + create_test_cases(SingleTableInheritanceTestCase) From c2df54079a8ef1426bf2136b589a37b24d0075ce Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Mar 2020 15:06:39 -0500 Subject: [PATCH 48/90] Add explicit "pseudo-backref" relationships for version/parent refs #239 --- sqlalchemy_continuum/model_builder.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_continuum/model_builder.py b/sqlalchemy_continuum/model_builder.py index 2be6e63b..03be2c53 100644 --- a/sqlalchemy_continuum/model_builder.py +++ b/sqlalchemy_continuum/model_builder.py @@ -107,6 +107,7 @@ class represents). """ conditions = [] foreign_keys = [] + model_keys = [] for key, column in sa.inspect(self.model).columns.items(): if column.primary_key: conditions.append( @@ -117,6 +118,9 @@ class represents). foreign_keys.append( getattr(self.version_class, key) ) + model_keys.append( + getattr(self.model, key) + ) # We need to check if versions relation was already set for parent # class. @@ -130,11 +134,18 @@ class represents). option(self.model, 'transaction_column_name') ), lazy='dynamic', - backref=sa.orm.backref( - 'version_parent' - ), viewonly=True ) + # We must explicitly declare this relationship, instead of + # specifying as a backref to the one above, since they are + # viewonly=True and SQLAlchemy will warn if using backref. + self.version_class.version_parent = sa.orm.relationship( + self.model, + primaryjoin=sa.and_(*conditions), + foreign_keys=model_keys, + viewonly=True, + uselist=False, + ) def build_transaction_relationship(self, tx_class): """ From 645d0197e1fcfef0778f6f336938cb9d01dd36d2 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 10 May 2020 19:35:39 +0300 Subject: [PATCH 49/90] Bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 958eb9a0..d06eec9c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.10 (2020-05-10) +^^^^^^^^^^^^^^^^^^^ + +- Added explicit "pseudo-backref" relationships for version/parent (#240, courtesy of lgedgar) + + 1.3.9 (2019-03-19) ^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 4de143f4..171f3206 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.9' +__version__ = '1.3.10' versioning_manager = VersioningManager() From 54af8b28356af350135886c9cfae9fbe7749d2d3 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 10 May 2020 19:49:35 +0300 Subject: [PATCH 50/90] Update changes --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index d06eec9c..5b7735e0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Here you can see the full list of changes between each SQLAlchemy-Continuum rele ^^^^^^^^^^^^^^^^^^^ - Added explicit "pseudo-backref" relationships for version/parent (#240, courtesy of lgedgar) +- Fixed m2m Bug when an unrelated change is made to a model (#242, courtesy of Andrew-Dickinson) 1.3.9 (2019-03-19) From 8a9b357ddfb4356f3232e2c3bed7fb5ac2ed7146 Mon Sep 17 00:00:00 2001 From: Konsta Vesterinen Date: Sun, 24 May 2020 09:53:16 +0300 Subject: [PATCH 51/90] bump version --- CHANGES.rst | 6 ++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5b7735e0..0f8cc598 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.11 (2020-05-24) +^^^^^^^^^^^^^^^^^^^ + +- Made ModelBuilder create column aliases in version models (#246, courtesy of killthekitten) + + 1.3.10 (2020-05-10) ^^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 171f3206..96fc114a 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.10' +__version__ = '1.3.11' versioning_manager = VersioningManager() From aa4236523e515785803ddf13b04a4a8e353f2a1f Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sat, 1 Jan 2022 22:16:57 +0000 Subject: [PATCH 52/90] Fix class registry lookup for SQLAlchemy >= 1.4 _decl_class_registry was moved by 450f5c0d. This should be replaced by a public interface once one is added. --- sqlalchemy_continuum/factory.py | 6 +++++- sqlalchemy_continuum/transaction.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/factory.py b/sqlalchemy_continuum/factory.py index 5e36dc81..9951f67a 100644 --- a/sqlalchemy_continuum/factory.py +++ b/sqlalchemy_continuum/factory.py @@ -6,7 +6,11 @@ def __call__(self, manager): Create model class but only if it doesn't already exist in declarative model registry. """ - registry = manager.declarative_base._decl_class_registry + Base = manager.declarative_base + try: + registry = Base.registry._class_registry + except AttributeError: # SQLAlchemy < 1.4 + registry = Base._decl_class_registry if self.model_name not in registry: return self.create_class(manager) return registry[self.model_name] diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index d1b85534..870407dc 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -132,7 +132,11 @@ class Transaction( if manager.user_cls: user_cls = manager.user_cls - registry = manager.declarative_base._decl_class_registry + Base = manager.declarative_base + try: + registry = Base.registry._class_registry + except AttributeError: # SQLAlchemy < 1.4 + registry = Base._decl_class_registry if isinstance(user_cls, six.string_types): try: From f12a7c3f6c856774c7725bc9962db6835bbf9560 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sat, 1 Jan 2022 22:18:46 +0000 Subject: [PATCH 53/90] Prevent re-entry of after_configured Since 5ec5b0a6, SQLAlchemy re-triggers after_configured while we're trying to add version classes. Prevent this handler from being re-entered, but don't make it one-shot. --- sqlalchemy_continuum/builder.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sqlalchemy_continuum/builder.py b/sqlalchemy_continuum/builder.py index 8a2f219c..53134029 100644 --- a/sqlalchemy_continuum/builder.py +++ b/sqlalchemy_continuum/builder.py @@ -1,5 +1,6 @@ from copy import copy from inspect import getmro +from functools import wraps import sqlalchemy as sa from sqlalchemy_utils.functions import get_declarative_base @@ -10,6 +11,18 @@ from .table_builder import TableBuilder +def prevent_reentry(handler): + in_handler = False + @wraps(handler) + def check_reentry(*args, **kwargs): + nonlocal in_handler + if in_handler: + return + in_handler = True + handler(*args, **kwargs) + in_handler = False + return check_reentry + class Builder(object): def build_triggers(self): """ @@ -141,6 +154,7 @@ def build_transaction_class(self): self.manager.create_transaction_model() self.manager.plugins.after_build_tx_class(self.manager) + @prevent_reentry def configure_versioned_classes(self): """ Configures all versioned classes that were collected during From 693cbcafa894d53598038f7e7f5c8e51c999b414 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sat, 1 Jan 2022 22:21:46 +0000 Subject: [PATCH 54/90] Only pass classes to aliased() 4ecd352a adds a reference to .entity in AliasedInsp, which previously would accept an object and alias its class. --- sqlalchemy_continuum/fetcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/fetcher.py b/sqlalchemy_continuum/fetcher.py index 1ac1a175..0f262b6f 100644 --- a/sqlalchemy_continuum/fetcher.py +++ b/sqlalchemy_continuum/fetcher.py @@ -59,7 +59,7 @@ def _transaction_id_subquery(self, obj, next_or_prev='next', alias=None): func = sa.func.max if alias is None: - alias = sa.orm.aliased(obj) + alias = sa.orm.aliased(obj.__class__) table = alias.__table__ if hasattr(alias, 'c'): attrs = alias.c @@ -117,7 +117,7 @@ def _index_query(self, obj): Returns the query needed for fetching the index of this record relative to version history. """ - alias = sa.orm.aliased(obj) + alias = sa.orm.aliased(obj.__class__) subquery = ( sa.select([sa.func.count('1')], from_obj=[alias.__table__]) From 1d1251b5abb971fa13093a9a4942abcd7ec207f7 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sat, 1 Jan 2022 22:26:28 +0000 Subject: [PATCH 55/90] Fix deprecated useexisting useexisting was removed in a9b068ae --- tests/relationships/test_association_table_relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/relationships/test_association_table_relations.py b/tests/relationships/test_association_table_relations.py index 81ec3739..b9d515c9 100644 --- a/tests/relationships/test_association_table_relations.py +++ b/tests/relationships/test_association_table_relations.py @@ -12,7 +12,7 @@ class PublishedArticle(self.Model): __tablename__ = 'published_article' __table_args__ = ( PrimaryKeyConstraint("article_id", "author_id"), - {'useexisting': True} + {'keep_existing': True} ) article_id = sa.Column(sa.Integer, sa.ForeignKey('article.id')) From 5e3edc9bcdf46a064b27ea4cb6d57643ac6bc12f Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sun, 16 Jan 2022 03:47:44 +0000 Subject: [PATCH 56/90] Skip tests for deprecated order_by on sqlalchemy>=1.4 --- tests/test_mapper_args.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_mapper_args.py b/tests/test_mapper_args.py index 356f85f1..bb14ffb5 100644 --- a/tests/test_mapper_args.py +++ b/tests/test_mapper_args.py @@ -1,3 +1,6 @@ +from pytest import mark +from packaging import version + import sqlalchemy as sa from sqlalchemy_continuum import version_class from tests import TestCase @@ -29,6 +32,7 @@ def test_supports_column_prefix(self): assert self.TextItem._id +@mark.skipif("version.parse(sa.__version__) >= version.parse('1.4')") class TestOrderByWithStringArg(TestCase): def create_models(self): class TextItem(self.Model): @@ -55,6 +59,7 @@ def test_reflects_order_by(self): assert self.TextItemVersion.__mapper_args__['order_by'] == 'id' +@mark.skipif("version.parse(sa.__version__) >= version.parse('1.4')") class TestOrderByWithInstrumentedAttribute(TestCase): def create_models(self): class TextItem(self.Model): From 3f5f95fbf6897772c881b3665be6143f079e534f Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sun, 16 Jan 2022 03:48:26 +0000 Subject: [PATCH 57/90] An entity with an empty polymorphic_identity can't be loaded --- tests/inheritance/test_single_table_inheritance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/inheritance/test_single_table_inheritance.py b/tests/inheritance/test_single_table_inheritance.py index 9a259a54..73295bea 100644 --- a/tests/inheritance/test_single_table_inheritance.py +++ b/tests/inheritance/test_single_table_inheritance.py @@ -19,6 +19,7 @@ class TextItem(self.Model): __mapper_args__ = { 'polymorphic_on': discriminator, + 'polymorphic_identity': u'base', 'with_polymorphic': '*' } From 816fe4582595cf27e5fdfdda181f79a4a59b4877 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sun, 16 Jan 2022 03:51:39 +0000 Subject: [PATCH 58/90] Add GitHub test action --- .github/workflows/main.yml | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..0733e190 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,42 @@ +name: "Test" + +on: + push: + paths-ignore: + - "docs/**" + pull_request: + paths-ignore: + - "docs/**" + schedule: + - cron: '40 1 * * 3' + +jobs: + test: + name: test-python${{ matrix.python-version }}-sa${{ matrix.sqlalchemy-version }}-${{ matrix.db-engine }} + strategy: + matrix: + python-version: +# - "2.7" +# - "3.4" +# - "3.5" +# - "3.6" +# - "3.7" + - "3.8" +# - "3.9" +# - "3.10" +# - "pypy-3.7" + sqlalchemy-version: + - "<1.4" + - ">=1.4" + db-engine: + - "sqlite" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install sqlalchemy + run: pip3 install 'sqlalchemy${{ matrix.sqlalchemy-version }}' + - name: Build + run: pip3 install -e '.[test]' + - name: Run tests + run: DB=${{ matrix.db-engine }} pytest + From 3f69373f0bd82da4822d888a4cf82ec2b73a5dfe Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sun, 16 Jan 2022 22:33:34 +0000 Subject: [PATCH 59/90] Avoid sqlalchemy-i18n==1.1.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b6543282..8988e22c 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def get_version(): 'flask-login': ['Flask-Login>=0.2.9'], 'flask-sqlalchemy': ['Flask-SQLAlchemy>=1.0'], 'flexmock': ['flexmock>=0.9.7'], - 'i18n': ['SQLAlchemy-i18n>=0.8.4'], + 'i18n': ['SQLAlchemy-i18n>=0.8.4,!=1.1.0'], } From ac5d1803b4e2ca7bfa375bd5a53949640d8387b0 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Mon, 17 Jan 2022 00:37:30 +0000 Subject: [PATCH 60/90] Bump PyMySQL in test to support UTF-8 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8988e22c..ad86ab4e 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def get_version(): 'pytest>=2.3.5', 'flexmock>=0.9.7', 'psycopg2>=2.4.6', - 'PyMySQL==0.6.1', + 'PyMySQL>=0.8.0', 'six>=1.4.0' ], 'anyjson': ['anyjson>=0.3.3'], From e03481ca59dfc5a747877b575eabc5f0bc356d79 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Mon, 17 Jan 2022 00:37:44 +0000 Subject: [PATCH 61/90] Add DB engines to GitHub action --- .github/workflows/main.yml | 35 +++++++++++++++++++++++++++++++++-- benchmark.py | 2 +- tests/__init__.py | 4 ++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0733e190..b94b4507 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ on: schedule: - cron: '40 1 * * 3' + jobs: test: name: test-python${{ matrix.python-version }}-sa${{ matrix.sqlalchemy-version }}-${{ matrix.db-engine }} @@ -29,8 +30,36 @@ jobs: - "<1.4" - ">=1.4" db-engine: - - "sqlite" + - sqlite + - postgres + - postgres-native + - mysql runs-on: ubuntu-latest + services: + mysql: + image: mysql + ports: + - 3306:3306 + env: + MYSQL_DATABASE: sqlalchemy_continuum_test + MYSQL_ALLOW_EMPTY_PASSWORD: yes + options: >- + --health-cmd "mysqladmin ping" + --health-interval 5s + --health-timeout 2s + --health-retries 3 + postgres: + image: postgres + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: sqlalchemy_continuum_test + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 2s + --health-retries 3 steps: - uses: actions/checkout@v1 - name: Install sqlalchemy @@ -38,5 +67,7 @@ jobs: - name: Build run: pip3 install -e '.[test]' - name: Run tests - run: DB=${{ matrix.db-engine }} pytest + run: pytest + env: + DB: ${{ matrix.db-engine }} diff --git a/benchmark.py b/benchmark.py index 9ee0250f..f4b11aab 100644 --- a/benchmark.py +++ b/benchmark.py @@ -50,7 +50,7 @@ def test_versioning( make_versioned(options=options) - dns = 'postgres://postgres@localhost/sqlalchemy_continuum_test' + dns = 'postgresql://postgres:postgres@localhost/sqlalchemy_continuum_test' versioning_manager.plugins = plugins versioning_manager.transaction_cls = transaction_cls versioning_manager.user_cls = user_cls diff --git a/tests/__init__.py b/tests/__init__.py index 3a96eb0f..b744ad17 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -42,9 +42,9 @@ def log_sql( def get_dns_from_driver(driver): if driver == 'postgres': - return 'postgres://postgres@localhost/sqlalchemy_continuum_test' + return 'postgresql://postgres:postgres@localhost/sqlalchemy_continuum_test' elif driver == 'mysql': - return 'mysql+pymysql://travis@localhost/sqlalchemy_continuum_test' + return 'mysql+pymysql://root@localhost/sqlalchemy_continuum_test' elif driver == 'sqlite': return 'sqlite:///:memory:' else: From 3da1c1348b7a169dd738057c045e278125438f1d Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Tue, 18 Jan 2022 23:33:08 +0000 Subject: [PATCH 62/90] Update build badge --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5636fef2..367a16a7 100644 --- a/README.rst +++ b/README.rst @@ -119,8 +119,8 @@ Resources .. image:: http://i.imgur.com/UFaRx.gif -.. |Build Status| image:: https://travis-ci.org/kvesteri/sqlalchemy-continuum.png?branch=master - :target: https://travis-ci.org/kvesteri/sqlalchemy-continuum +.. |Build Status| image:: https://github.com/kvesteri/sqlalchemy-continuum/workflows/Test/badge.svg + :target: https://github.com/kvesteri/sqlalchemy-continuum/actions?query=workflow%3ATest .. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Continuum.png :target: https://pypi.python.org/pypi/SQLAlchemy-Continuum/ .. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Continuum.png From 85f09a4b3b23240f47c5c848b1e6c4b80b3accdd Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Wed, 19 Jan 2022 00:37:53 +0000 Subject: [PATCH 63/90] Bump version --- CHANGES.rst | 5 +++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0f8cc598..39a33ef8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,11 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.12 (2022-01-18) +^^^^^^^^^^^^^^^^^^^ + +- Support SA 1.4 + 1.3.11 (2020-05-24) ^^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 96fc114a..8af744c9 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.11' +__version__ = '1.3.12' versioning_manager = VersioningManager() From 465169839fef1bcb888b406a179fa6b6463b4101 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Wed, 26 Jan 2022 18:33:21 +0000 Subject: [PATCH 64/90] Correct wording of LICENSE (fixes #224) --- LICENSE | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index d604ce84..cccc1d8f 100644 --- a/LICENSE +++ b/LICENSE @@ -12,8 +12,9 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* The names of the contributors may not be used to endorse or promote products - derived from this software without specific prior written permission. +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED From 2e53b5f927c7b601c1dab02b2c91c4dbb3b9ed15 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Wed, 26 Jan 2022 18:36:38 +0000 Subject: [PATCH 65/90] Remove unused anyjson (fixes #261) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ad86ab4e..276e74f1 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ def get_version(): 'PyMySQL>=0.8.0', 'six>=1.4.0' ], - 'anyjson': ['anyjson>=0.3.3'], 'flask': ['Flask>=0.9'], 'flask-login': ['Flask-Login>=0.2.9'], 'flask-sqlalchemy': ['Flask-SQLAlchemy>=1.0'], From 86295911f77eb65df10f6e18861b4f31a28a5937 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Sun, 6 Feb 2022 20:31:04 -0800 Subject: [PATCH 66/90] add test that reproduces issue 85 "TransactionBase.changed_entities fails without TransactionChanges plugin" and fix that has Transaction.entity_names raise a NoChangesColumn instead of AttributeError. --- sqlalchemy_continuum/transaction.py | 18 +++++++++++++++--- tests/test_transaction.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index 870407dc..144579aa 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -23,6 +23,10 @@ def compile_big_integer(element, compiler, **kw): return 'INTEGER' +class NoChangesColumn(Exception): + pass + + class TransactionBase(object): issued_at = sa.Column(sa.DateTime, default=datetime.utcnow) @@ -30,8 +34,13 @@ class TransactionBase(object): def entity_names(self): """ Return a list of entity names that changed during this transaction. + Raises a NoChangesColumn exception if the 'changes' column does + not exist, most likely because TransactionChangesPlugin is not enabled. """ - return [changes.entity_name for changes in self.changes] + if 'changes' in self.__table__.columns: + return [changes.entity_name for changes in self.changes] + else: + raise NoChangesColumn() @property def changed_entities(self): @@ -48,8 +57,11 @@ def changed_entities(self): session = sa.orm.object_session(self) for class_, version_class in tuples: - if class_.__name__ not in self.entity_names: - continue + try: + if class_.__name__ not in self.entity_names: + continue + except NoChangesColumn: + pass tx_column = manager.option(class_, 'transaction_column_name') diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 9d9e8f1b..2b3e7b56 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -2,6 +2,8 @@ from sqlalchemy_continuum import versioning_manager from tests import TestCase from pytest import mark +from sqlalchemy_continuum.plugins import TransactionMetaPlugin + class TestTransaction(TestCase): @@ -38,6 +40,19 @@ def test_repr(self): repr(transaction) ) + def test_changed_entities(self): + article_v0 = self.article.versions[0] + transaction = article_v0.transaction + assert transaction.changed_entities == { + self.ArticleVersion: [article_v0], + self.TagVersion: [self.article.tags[0].versions[0]], + } + + +# Check that the tests pass without TransactionChangesPlugin +class TestTransactionWithoutChangesPlugin(TestTransaction): + plugins = [TransactionMetaPlugin()] + class TestAssigningUserClass(TestCase): user_cls = 'User' From 6bfcf3dc7608074e56848ea242a31bb92bfa0a7d Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 7 Feb 2022 06:49:50 -0800 Subject: [PATCH 67/90] use hasattr instead of __table__.columns, fixes tests/plugins/test_transaction_changes.py and tests/inheritance/test_single_table_inheritance.py --- sqlalchemy_continuum/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index 144579aa..f3e26713 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -37,7 +37,7 @@ def entity_names(self): Raises a NoChangesColumn exception if the 'changes' column does not exist, most likely because TransactionChangesPlugin is not enabled. """ - if 'changes' in self.__table__.columns: + if hasattr(self, 'changes'): return [changes.entity_name for changes in self.changes] else: raise NoChangesColumn() From ec08cf4aa1d598a4a1ab3a695a4d91296880201a Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Tue, 8 Feb 2022 13:31:16 -0800 Subject: [PATCH 68/90] fix for unit_of_work.py:263: SAWarning: implicitly coercing SELECT object to scalar subquery Tested by running `DB=sqlite py.test tests`. With master: 653 passed, 99 skipped, 4909 warnings in 65.90s (0:01:05) With this change: 653 passed, 99 skipped, 4695 warnings in 64.93s (0:01:04) --- sqlalchemy_continuum/unit_of_work.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlalchemy_continuum/unit_of_work.py b/sqlalchemy_continuum/unit_of_work.py index 5f91b13d..51d74df6 100644 --- a/sqlalchemy_continuum/unit_of_work.py +++ b/sqlalchemy_continuum/unit_of_work.py @@ -252,7 +252,7 @@ def update_version_validity(self, parent, version_obj): parent, version_obj, alias=sa.orm.aliased(class_.__table__) - ) + ).scalar_subquery() query = ( session.query(class_.__table__) .filter( From f96c9a1f3aa23c0d4b1e7ae622c6913c16747142 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Tue, 8 Feb 2022 13:55:23 -0800 Subject: [PATCH 69/90] fallback to as_scalar to continue working in SQLAlchemy < 1.4 --- sqlalchemy_continuum/unit_of_work.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sqlalchemy_continuum/unit_of_work.py b/sqlalchemy_continuum/unit_of_work.py index 51d74df6..50ab768b 100644 --- a/sqlalchemy_continuum/unit_of_work.py +++ b/sqlalchemy_continuum/unit_of_work.py @@ -252,7 +252,12 @@ def update_version_validity(self, parent, version_obj): parent, version_obj, alias=sa.orm.aliased(class_.__table__) - ).scalar_subquery() + ) + try: + subquery = subquery.scalar_subquery() + except AttributeError: # SQLAlchemy < 1.4 + subquery = subquery.as_scalar() + query = ( session.query(class_.__table__) .filter( From 7eda52765ee8bb1c1c5ff193fc3a7ee032e5323f Mon Sep 17 00:00:00 2001 From: Jiri Kuncar Date: Tue, 8 Nov 2016 13:19:14 +0100 Subject: [PATCH 70/90] FlaskPlugin: use `get_id()` instead of `id` attr Following Flask-Login documentation we should use current_user.get_id() instead of current_user.id. (closes #149) https://flask-login.readthedocs.io/en/latest/#your-user-class Signed-off-by: Jiri Kuncar --- sqlalchemy_continuum/plugins/flask.py | 2 +- tests/plugins/test_flask.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_continuum/plugins/flask.py b/sqlalchemy_continuum/plugins/flask.py index c7b14254..d18a9b8b 100644 --- a/sqlalchemy_continuum/plugins/flask.py +++ b/sqlalchemy_continuum/plugins/flask.py @@ -36,7 +36,7 @@ def fetch_current_user_id(): if _app_ctx_stack.top is None or _request_ctx_stack.top is None: return try: - return current_user.id + return current_user.get_id() except AttributeError: return diff --git a/tests/plugins/test_flask.py b/tests/plugins/test_flask.py index 7f962c99..24e8d960 100644 --- a/tests/plugins/test_flask.py +++ b/tests/plugins/test_flask.py @@ -1,7 +1,7 @@ import os from flask import Flask, url_for -from flask_login import LoginManager +from flask_login import LoginManager, UserMixin from flask_sqlalchemy import SQLAlchemy, _SessionSignalEvents from flexmock import flexmock @@ -76,7 +76,7 @@ def logout(self, user=None): def create_models(self): TestCase.create_models(self) - class User(self.Model): + class User(self.Model, UserMixin): __tablename__ = 'user' __versioned__ = { 'base_classes': (self.Model, ) From 66b99eb5464c47ad02ff1d65af8ef548e3437b08 Mon Sep 17 00:00:00 2001 From: jstolarski Date: Mon, 28 Jan 2019 09:59:10 +0100 Subject: [PATCH 71/90] t Test that recreates bug. --- tests/plugins/test_activity.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/plugins/test_activity.py b/tests/plugins/test_activity.py index 812eb542..903e9e7b 100644 --- a/tests/plugins/test_activity.py +++ b/tests/plugins/test_activity.py @@ -36,6 +36,34 @@ def create_activity(self, object=None, target=None): return activity +class TestActivityNotId(ActivityTestCase): + + def create_models(self): + TestCase.create_models(self) + + class NotIdModel(self.Model): + __tablename__ = 'not_id' + __versioned__ = { + 'base_classes': (self.Model, ) + } + + pk = sa.Column(sa.Integer, autoincrement=True, primary_key=True) + name = sa.Column(sa.Unicode(255), nullable=False) + self.NotIdModel = NotIdModel + + def test_create_activity_with_pk(self): + not_id_model = self.NotIdModel(name="abc") + self.session.add(not_id_model) + self.session.commit() + self.create_activity(not_id_model) + self.session.commit() + activity = self.session.query(versioning_manager.activity_cls).first() + assert activity + assert activity.transaction_id + assert activity.object == not_id_model + assert activity.object_version == not_id_model.versions[-1] + + class TestActivity(ActivityTestCase): def test_creates_activity_class(self): assert versioning_manager.activity_cls.__name__ == 'Activity' From 46886b049463cad3ed0f35c04ee3e52530225d01 Mon Sep 17 00:00:00 2001 From: jstolarski Date: Mon, 28 Jan 2019 10:12:06 +0100 Subject: [PATCH 72/90] B Activity plugin works with PKs named other than id. closes #210 --- sqlalchemy_continuum/plugins/activity.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/plugins/activity.py b/sqlalchemy_continuum/plugins/activity.py index e6886de8..10b85d3f 100644 --- a/sqlalchemy_continuum/plugins/activity.py +++ b/sqlalchemy_continuum/plugins/activity.py @@ -191,6 +191,7 @@ import sqlalchemy as sa from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.inspection import inspect from sqlalchemy_utils import JSONType, generic_relationship from .base import Plugin @@ -254,11 +255,13 @@ def _calculate_tx_id(self, obj): if object_version: return object_version.transaction_id - version_cls = version_class(obj.__class__) + model = obj.__class__ + version_cls = version_class(model) + primary_key = inspect(model).primary_key[0].name return session.query( sa.func.max(version_cls.transaction_id) ).filter( - version_cls.id == obj.id + getattr(version_cls, primary_key) == getattr(obj, primary_key) ).scalar() def calculate_object_tx_id(self): From f3353a734cfaf5180e508658dd1562cd3d9aff5a Mon Sep 17 00:00:00 2001 From: jstolarski Date: Mon, 28 Jan 2019 10:23:51 +0100 Subject: [PATCH 73/90] t Test adjusted for python 2.7 --- tests/plugins/test_activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/test_activity.py b/tests/plugins/test_activity.py index 903e9e7b..4d0ab532 100644 --- a/tests/plugins/test_activity.py +++ b/tests/plugins/test_activity.py @@ -52,7 +52,7 @@ class NotIdModel(self.Model): self.NotIdModel = NotIdModel def test_create_activity_with_pk(self): - not_id_model = self.NotIdModel(name="abc") + not_id_model = self.NotIdModel(name=u'Some model without id PK') self.session.add(not_id_model) self.session.commit() self.create_activity(not_id_model) From 745dc9e9a03f414c9d76394e66e0db08ebe32cff Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Fri, 11 Feb 2022 02:01:55 -0800 Subject: [PATCH 74/90] NoChangesColumn -> NoChangesAttribute --- sqlalchemy_continuum/transaction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index f3e26713..dc2bcda4 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -23,7 +23,7 @@ def compile_big_integer(element, compiler, **kw): return 'INTEGER' -class NoChangesColumn(Exception): +class NoChangesAttribute(Exception): pass @@ -34,13 +34,13 @@ class TransactionBase(object): def entity_names(self): """ Return a list of entity names that changed during this transaction. - Raises a NoChangesColumn exception if the 'changes' column does + Raises a NoChangesAttribute exception if the 'changes' column does not exist, most likely because TransactionChangesPlugin is not enabled. """ if hasattr(self, 'changes'): return [changes.entity_name for changes in self.changes] else: - raise NoChangesColumn() + raise NoChangesAttribute() @property def changed_entities(self): @@ -60,7 +60,7 @@ def changed_entities(self): try: if class_.__name__ not in self.entity_names: continue - except NoChangesColumn: + except NoChangesAttribute: pass tx_column = manager.option(class_, 'transaction_column_name') From 92eeb1edd8d1ffc9463d0123296dc2d8720cdb3a Mon Sep 17 00:00:00 2001 From: Oleksandr <1374878+nanvel@users.noreply.github.com> Date: Mon, 18 Apr 2022 21:45:50 +0700 Subject: [PATCH 75/90] Allow to disable mod plugin in sync_triggers (#273) Pass sync_triggers kwargs through to create_triggers Allows use of native versioning without mod tracking --- docs/native_versioning.rst | 6 ++++++ sqlalchemy_continuum/dialects/postgresql.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/native_versioning.rst b/docs/native_versioning.rst index 60e215fe..4bc63c96 100644 --- a/docs/native_versioning.rst +++ b/docs/native_versioning.rst @@ -30,3 +30,9 @@ When making schema migrations (for example adding new columns to version tables) sync_trigger(conn, 'article_version') + +If you don't use `PropertyModTrackerPlugin`, then you have to disable it: + +:: + + sync_trigger(conn, 'article_version', use_property_mod_tracking=False) diff --git a/sqlalchemy_continuum/dialects/postgresql.py b/sqlalchemy_continuum/dialects/postgresql.py index f24d9077..c69a64be 100644 --- a/sqlalchemy_continuum/dialects/postgresql.py +++ b/sqlalchemy_continuum/dialects/postgresql.py @@ -456,7 +456,7 @@ def create_versioning_trigger_listeners(manager, cls): ) -def sync_trigger(conn, table_name): +def sync_trigger(conn, table_name, **kwargs): """ Synchronizes versioning trigger for given table with given connection. @@ -468,6 +468,7 @@ def sync_trigger(conn, table_name): :param conn: SQLAlchemy connection object :param table_name: Name of the table to synchronize versioning trigger for + :params **kwargs: kwargs to pass to create_trigger .. versionadded: 1.1.0 """ @@ -489,7 +490,7 @@ def sync_trigger(conn, table_name): set(c.name for c in version_table.c if not c.name.endswith('_mod')) ) drop_trigger(conn, parent_table.name) - create_trigger(conn, table=parent_table, excluded_columns=excluded_columns) + create_trigger(conn, table=parent_table, excluded_columns=excluded_columns, **kwargs) def create_trigger( From 8552a0798c8d89fbd72275b353d737db423cb46e Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sun, 28 Aug 2022 15:44:49 +0100 Subject: [PATCH 76/90] Fix for flask-login 0.6.2 Previously we seem to have relied on the login state not being preserved across contexts, but it's now moved to `g`. Using login_user instead of grovelling inside flask-login internals does the trick. --- tests/plugins/test_flask.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/tests/plugins/test_flask.py b/tests/plugins/test_flask.py index 24e8d960..379496d8 100644 --- a/tests/plugins/test_flask.py +++ b/tests/plugins/test_flask.py @@ -1,7 +1,7 @@ import os from flask import Flask, url_for -from flask_login import LoginManager, UserMixin +from flask_login import LoginManager, UserMixin, login_user from flask_sqlalchemy import SQLAlchemy, _SessionSignalEvents from flexmock import flexmock @@ -59,20 +59,6 @@ def teardown_method(self, method): self.client = None self.app = None - def login(self, user): - """ - Log in the user returned by :meth:`create_user`. - - :returns: the logged in user - """ - with self.client.session_transaction() as s: - s['_user_id'] = user.id - return user - - def logout(self, user=None): - with self.client.session_transaction() as s: - s['_user_id'] = None - def create_models(self): TestCase.create_models(self) @@ -114,7 +100,7 @@ def test_versioning_inside_request(self): user = self.User(name=u'Rambo') self.session.add(user) self.session.commit() - self.login(user) + login_user(user) self.client.get(url_for('.test_simple_flush')) article = self.session.query(self.Article).first() @@ -125,7 +111,7 @@ def test_raw_sql_and_flush(self): user = self.User(name=u'Rambo') self.session.add(user) self.session.commit() - self.login(user) + login_user(user) self.client.get(url_for('.test_raw_sql_and_flush')) assert ( self.session.query(versioning_manager.transaction_cls).count() == 2 From 7430f15e62cb22d92580208b22b7b7b93d47610e Mon Sep 17 00:00:00 2001 From: Ed Hazledine Date: Sun, 24 Jul 2022 19:46:46 +0100 Subject: [PATCH 77/90] Move to close_all_sessions --- benchmark.py | 4 ++-- tests/__init__.py | 4 ++-- tests/plugins/test_flask.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/benchmark.py b/benchmark.py index f4b11aab..55c6e41c 100644 --- a/benchmark.py +++ b/benchmark.py @@ -6,7 +6,7 @@ import sqlalchemy as sa from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, close_all_sessions from sqlalchemy_continuum import ( make_versioned, versioning_manager, @@ -106,7 +106,7 @@ class Tag(Model): remove_versioning() versioning_manager.reset() - session.close_all() + close_all_sessions() session.expunge_all() Model.metadata.drop_all(connection) engine.dispose() diff --git a/tests/__init__.py b/tests/__init__.py index b744ad17..dc9dc878 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,7 +7,7 @@ import sqlalchemy as sa from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, column_property +from sqlalchemy.orm import sessionmaker, column_property, close_all_sessions from sqlalchemy_continuum import ( ClassNotVersioned, version_class, @@ -130,7 +130,7 @@ def teardown_method(self, method): QueryPool.queries = [] versioning_manager.reset() - self.session.close_all() + close_all_sessions() self.session.expunge_all() self.drop_tables() self.engine.dispose() diff --git a/tests/plugins/test_flask.py b/tests/plugins/test_flask.py index 379496d8..0db414d0 100644 --- a/tests/plugins/test_flask.py +++ b/tests/plugins/test_flask.py @@ -6,6 +6,7 @@ from flexmock import flexmock import sqlalchemy as sa +from sqlalchemy.orm import close_all_sessions from sqlalchemy_continuum import ( make_versioned, remove_versioning, versioning_manager ) @@ -234,7 +235,7 @@ def teardown_method(self, method): remove_versioning() self.db.session.remove() self.db.drop_all() - self.db.session.close_all() + close_all_sessions() self.db.engine.dispose() self.context.pop() self.context = None From 2e0768aaf988bac0fc42ea9d7baf58c9abde4ef9 Mon Sep 17 00:00:00 2001 From: Ed Hazledine Date: Sat, 30 Jul 2022 11:40:57 +0100 Subject: [PATCH 78/90] Update to column._copy for sqlalchemy1.4 --- sqlalchemy_continuum/table_builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sqlalchemy_continuum/table_builder.py b/sqlalchemy_continuum/table_builder.py index 2bfa06a9..600d1e02 100644 --- a/sqlalchemy_continuum/table_builder.py +++ b/sqlalchemy_continuum/table_builder.py @@ -20,12 +20,9 @@ def reflect_column(self, column): :param column: SQLAlchemy Column object of parent table """ - # Make a copy of the column so that it does not point to wrong - # table. - column_copy = column.copy() - # Remove unique constraints + # Make a copy of the column so that it does not point to wrong table. + column_copy = column._copy() if hasattr(column, '_copy') else column.copy() column_copy.unique = False - # Remove onupdate triggers column_copy.onupdate = None if column_copy.autoincrement: column_copy.autoincrement = False From 38cef849e01326313874e7a9172b5c52c2ef3c2d Mon Sep 17 00:00:00 2001 From: AbdealiJK Date: Wed, 24 Aug 2022 08:17:58 +0530 Subject: [PATCH 79/90] fix(289): Avoid modifying params when in assoc handling When handling assoc tables, we use the existing properties and use them for another query for the version tables. But if we modify the existing dict - in some dialects, that causes the original query to be modified (For example oracle with cx-oracle library) Avoid mutating the properties to avoid such issues. --- sqlalchemy_continuum/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/manager.py b/sqlalchemy_continuum/manager.py index 0e945d53..7bb5f731 100644 --- a/sqlalchemy_continuum/manager.py +++ b/sqlalchemy_continuum/manager.py @@ -402,11 +402,10 @@ def append_association_operation(self, conn, table_name, params, op): """ Append history association operation to pending_statements list. """ - params['operation_type'] = op stmt = ( self.metadata.tables[self.options['table_name'] % table_name] .insert() - .values(params) + .values({**params, 'operation_type': op}) ) try: uow = self.units_of_work[conn] From 0c5b64eb492e0ae3e4db90a0541a6d42867ccb4b Mon Sep 17 00:00:00 2001 From: indivar Date: Wed, 31 Aug 2022 19:56:36 +0530 Subject: [PATCH 80/90] fix for test_association_table_relations.py, relationship warning SQLA>1.4 --- tests/relationships/test_association_table_relations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/relationships/test_association_table_relations.py b/tests/relationships/test_association_table_relations.py index b9d515c9..a447b61e 100644 --- a/tests/relationships/test_association_table_relations.py +++ b/tests/relationships/test_association_table_relations.py @@ -2,6 +2,7 @@ from sqlalchemy import PrimaryKeyConstraint from sqlalchemy.orm import relationship from tests import TestCase, create_test_cases +from packaging import version as py_pkg_version class AssociationTableRelationshipsTestCase(TestCase): @@ -17,8 +18,11 @@ class PublishedArticle(self.Model): article_id = sa.Column(sa.Integer, sa.ForeignKey('article.id')) author_id = sa.Column(sa.Integer, sa.ForeignKey('author.id')) - author = relationship('Author') - article = relationship('Article') + relationship_kwargs = {} + if py_pkg_version.parse(sa.__version__) >= py_pkg_version.parse('1.4.0'): + relationship_kwargs.update({'overlaps': 'articles'}) + author = relationship('Author', **relationship_kwargs) + article = relationship('Article', **relationship_kwargs) self.PublishedArticle = PublishedArticle From 5fb48b87e25a1029527409084e79e3f61e8e9e64 Mon Sep 17 00:00:00 2001 From: indivar Date: Fri, 2 Sep 2022 21:28:44 +0530 Subject: [PATCH 81/90] fix for test_custom_condition_relations.py, SQLA>1.4 relationship warning --- tests/relationships/test_custom_condition_relations.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/relationships/test_custom_condition_relations.py b/tests/relationships/test_custom_condition_relations.py index b888e424..08c89955 100644 --- a/tests/relationships/test_custom_condition_relations.py +++ b/tests/relationships/test_custom_condition_relations.py @@ -1,6 +1,6 @@ import sqlalchemy as sa from tests import TestCase, create_test_cases - +from packaging import version as py_pkg_version class CustomConditionRelationsTestCase(TestCase): def create_models(self): @@ -26,12 +26,19 @@ class Tag(self.Model): article_id = sa.Column(sa.Integer, sa.ForeignKey(Article.id)) category = sa.Column(sa.Unicode(20)) + if py_pkg_version.parse(sa.__version__) < py_pkg_version.parse('1.4.0'): + primary_key_overlaps = {} + secondary_key_overlaps = {} + else: + primary_key_overlaps = {'overlaps': 'secondary_tags, Article'} + secondary_key_overlaps = {'overlaps': 'primary_tags, Article'} Article.primary_tags = sa.orm.relationship( Tag, primaryjoin=sa.and_( Tag.article_id == Article.id, Tag.category == u'primary' ), + **primary_key_overlaps ) Article.secondary_tags = sa.orm.relationship( @@ -40,6 +47,7 @@ class Tag(self.Model): Tag.article_id == Article.id, Tag.category == u'secondary' ), + **secondary_key_overlaps ) self.Article = Article From 206a16d1a595c61e687e21a552eced3aac08866e Mon Sep 17 00:00:00 2001 From: indivar Date: Mon, 15 Aug 2022 13:25:08 +0530 Subject: [PATCH 82/90] fix for relationship_builder.py:51 SAWarning: implicitly coercing SELECT object to scalar subquery --- sqlalchemy_continuum/relationship_builder.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sqlalchemy_continuum/relationship_builder.py b/sqlalchemy_continuum/relationship_builder.py index e4888a24..19deb58d 100644 --- a/sqlalchemy_continuum/relationship_builder.py +++ b/sqlalchemy_continuum/relationship_builder.py @@ -47,9 +47,7 @@ def one_to_many_subquery(self, obj): def many_to_one_subquery(self, obj): tx_column = option(obj, 'transaction_column_name') reflector = VersionExpressionReflector(obj, self.property) - - return getattr(self.remote_cls, tx_column) == ( - sa.select( + subquery = sa.select( [sa.func.max(getattr(self.remote_cls, tx_column))] ).where( sa.and_( @@ -58,7 +56,11 @@ def many_to_one_subquery(self, obj): reflector(self.property.primaryjoin) ) ) - ) + try: + subquery = subquery.scalar_subquery() + except AttributeError: # SQLAlchemy < 1.4 + subquery = subquery.as_scalar() + return getattr(self.remote_cls, tx_column) == subquery def query(self, obj): session = sa.orm.object_session(obj) From 7d4876caed8931b80db36b43a8342ed69199e962 Mon Sep 17 00:00:00 2001 From: indiVar0508 Date: Sat, 3 Sep 2022 04:16:20 +0530 Subject: [PATCH 83/90] Fix SAWarning for scalar_subquery warning SQLA<1.4 (#300) * fixes for scalar_subquery and subquery warnings in SQLA>=1.4 Co-authored-by: Mark Steward --- sqlalchemy_continuum/fetcher.py | 6 ++++-- sqlalchemy_continuum/relationship_builder.py | 15 ++++++++------- sqlalchemy_continuum/schema.py | 4 ++++ sqlalchemy_continuum/unit_of_work.py | 2 +- tests/test_changeset.py | 15 +++++++++------ 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/sqlalchemy_continuum/fetcher.py b/sqlalchemy_continuum/fetcher.py index 0f262b6f..a2da2fb9 100644 --- a/sqlalchemy_continuum/fetcher.py +++ b/sqlalchemy_continuum/fetcher.py @@ -90,11 +90,13 @@ def _transaction_id_subquery(self, obj, next_or_prev='next', alias=None): ) .correlate(table) ) - return query + try: + return query.scalar_subquery() + except AttributeError: # SQLAlchemy < 1.4 + return query.as_scalar() def _next_prev_query(self, obj, next_or_prev='next'): session = sa.orm.object_session(obj) - return ( session.query(obj.__class__) .filter( diff --git a/sqlalchemy_continuum/relationship_builder.py b/sqlalchemy_continuum/relationship_builder.py index 19deb58d..032e09b1 100644 --- a/sqlalchemy_continuum/relationship_builder.py +++ b/sqlalchemy_continuum/relationship_builder.py @@ -48,18 +48,19 @@ def many_to_one_subquery(self, obj): tx_column = option(obj, 'transaction_column_name') reflector = VersionExpressionReflector(obj, self.property) subquery = sa.select( - [sa.func.max(getattr(self.remote_cls, tx_column))] - ).where( - sa.and_( - getattr(self.remote_cls, tx_column) <= - getattr(obj, tx_column), - reflector(self.property.primaryjoin) - ) + [sa.func.max(getattr(self.remote_cls, tx_column))] + ).where( + sa.and_( + getattr(self.remote_cls, tx_column) <= + getattr(obj, tx_column), + reflector(self.property.primaryjoin) ) + ) try: subquery = subquery.scalar_subquery() except AttributeError: # SQLAlchemy < 1.4 subquery = subquery.as_scalar() + return getattr(self.remote_cls, tx_column) == subquery def query(self, obj): diff --git a/sqlalchemy_continuum/schema.py b/sqlalchemy_continuum/schema.py index 659df1b6..83728ef8 100644 --- a/sqlalchemy_continuum/schema.py +++ b/sqlalchemy_continuum/schema.py @@ -25,6 +25,10 @@ def get_end_tx_column_query( ] ) ) + try: + tx_criterion = tx_criterion.scalar_subquery() + except AttributeError: # SQLAlchemy < 1.4 + tx_criterion = tx_criterion.as_scalar() return sa.select( columns=[ getattr(v1.c, column) diff --git a/sqlalchemy_continuum/unit_of_work.py b/sqlalchemy_continuum/unit_of_work.py index 50ab768b..22596b24 100644 --- a/sqlalchemy_continuum/unit_of_work.py +++ b/sqlalchemy_continuum/unit_of_work.py @@ -226,7 +226,7 @@ def version_validity_subquery(self, parent, version_obj, alias=None): return sa.select( [sa.text('max_1')], from_obj=[ - sa.sql.expression.alias(subquery, name='subquery') + sa.sql.expression.alias(subquery.subquery() if hasattr(subquery, 'subquery') else subquery, name='subquery') ] ) return subquery diff --git a/tests/test_changeset.py b/tests/test_changeset.py index 757ff3b7..f72ccb88 100644 --- a/tests/test_changeset.py +++ b/tests/test_changeset.py @@ -93,12 +93,15 @@ class Tag(self.Model): name = sa.Column(sa.Unicode(255)) article_id = sa.Column(sa.Integer, sa.ForeignKey(Article.id)) article = sa.orm.relationship(Article, backref='tags') - - Article.tag_count = sa.orm.column_property( - sa.select([sa.func.count(Tag.id)]) - .where(Tag.article_id == Article.id) - .correlate_except(Tag) - ) + + subquery = (sa.select([sa.func.count(Tag.id)]) + .where(Tag.article_id == Article.id) + .correlate_except(Tag)) + try: + subquery = subquery.scalar_subquery() + except AttributeError: # SQLAlchemy < 1.4 + subquery = subquery.as_scalar() + Article.tag_count = sa.orm.column_property(subquery) self.Article = Article self.Tag = Tag From fb5f710e07cf1a8ed81c64e8365da7e5c2ea32c7 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Sat, 3 Sep 2022 16:29:04 +0100 Subject: [PATCH 84/90] Another scalar_subquery --- sqlalchemy_continuum/fetcher.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sqlalchemy_continuum/fetcher.py b/sqlalchemy_continuum/fetcher.py index a2da2fb9..a1f2684d 100644 --- a/sqlalchemy_continuum/fetcher.py +++ b/sqlalchemy_continuum/fetcher.py @@ -97,6 +97,15 @@ def _transaction_id_subquery(self, obj, next_or_prev='next', alias=None): def _next_prev_query(self, obj, next_or_prev='next'): session = sa.orm.object_session(obj) + + subquery = self._transaction_id_subquery( + obj, next_or_prev=next_or_prev + ) + try: + subquery = subquery.scalar_subquery() + except AttributeError: # SQLAlchemy < 1.4 + subquery = subquery.as_scalar() + return ( session.query(obj.__class__) .filter( @@ -104,11 +113,7 @@ def _next_prev_query(self, obj, next_or_prev='next'): getattr( obj.__class__, tx_column_name(obj) - ) - == - self._transaction_id_subquery( - obj, next_or_prev=next_or_prev - ), + ) == subquery, *parent_criteria(obj) ) ) From d8c1931a613ae5992344efef839fca85a4b7563d Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Wed, 7 Sep 2022 00:30:14 +0100 Subject: [PATCH 85/90] Bump version --- CHANGES.rst | 10 ++++++++++ sqlalchemy_continuum/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 39a33ef8..41117049 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,16 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. +1.3.13 (2022-01-18) +^^^^^^^^^^^^^^^^^^^ + +- Fixes for Flask 2.2 and Flask-Login 0.6.2 (#288, thanks to AbdealiJK) +- Allow changed_entities to work without TransactionChanges plugin (#268, thanks to TomGoBravo) +- Fix Activity plugin for non-composite primary keys not named id (#210, thanks to dryobates) +- Allow sync_trigger to pass arguments through to create_trigger (#273, thanks to nanvel) +- Fix association tables on Oracle (#291, thanks to AbdealiJK) +- Fix some deprecation warnings in SA 1.4 (#269, #277, #279, #300, #302, thanks to TomGoBravo, edhaz, and indiVar0508) + 1.3.12 (2022-01-18) ^^^^^^^^^^^^^^^^^^^ diff --git a/sqlalchemy_continuum/__init__.py b/sqlalchemy_continuum/__init__.py index 8af744c9..61081473 100644 --- a/sqlalchemy_continuum/__init__.py +++ b/sqlalchemy_continuum/__init__.py @@ -18,7 +18,7 @@ ) -__version__ = '1.3.12' +__version__ = '1.3.13' versioning_manager = VersioningManager() From 12c2980dfbece0315962a613d0966a002bcd3d26 Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Wed, 7 Sep 2022 00:31:45 +0100 Subject: [PATCH 86/90] Fix date --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 41117049..8499b4f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,7 @@ Changelog Here you can see the full list of changes between each SQLAlchemy-Continuum release. -1.3.13 (2022-01-18) +1.3.13 (2022-09-07) ^^^^^^^^^^^^^^^^^^^ - Fixes for Flask 2.2 and Flask-Login 0.6.2 (#288, thanks to AbdealiJK) From f5a3f8062c506c1330971628bc00704f842c429f Mon Sep 17 00:00:00 2001 From: Mark Steward Date: Fri, 21 Oct 2022 21:49:00 +0100 Subject: [PATCH 87/90] Pin flask-sqlalchemy for now --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 276e74f1..a0dfb651 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def get_version(): ], 'flask': ['Flask>=0.9'], 'flask-login': ['Flask-Login>=0.2.9'], - 'flask-sqlalchemy': ['Flask-SQLAlchemy>=1.0'], + 'flask-sqlalchemy': ['Flask-SQLAlchemy>=1.0,<3.0.0'], 'flexmock': ['flexmock>=0.9.7'], 'i18n': ['SQLAlchemy-i18n>=0.8.4,!=1.1.0'], } From 9a71f46419aafa21694760627f2202543093dd6d Mon Sep 17 00:00:00 2001 From: Emily Old Date: Wed, 11 Nov 2020 16:06:58 -0500 Subject: [PATCH 88/90] more specific model and table names --- sqlalchemy_continuum/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index dc2bcda4..64aa2eca 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -116,7 +116,7 @@ def create_triggers(cls): class TransactionFactory(ModelFactory): - model_name = 'Transaction' + model_name = 'VersionTransaction' def __init__(self, remote_addr=True): self.remote_addr = remote_addr @@ -129,7 +129,7 @@ class Transaction( manager.declarative_base, TransactionBase ): - __tablename__ = 'transaction' + __tablename__ = 'version_transaction' __versioning_manager__ = manager id = sa.Column( From d368c6f324c481017ada9d4136228c595e150b80 Mon Sep 17 00:00:00 2001 From: Emily Old Date: Wed, 11 Nov 2020 16:15:16 -0500 Subject: [PATCH 89/90] update actual class name --- sqlalchemy_continuum/transaction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index 64aa2eca..19830059 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -125,7 +125,7 @@ def create_class(self, manager): """ Create Transaction class. """ - class Transaction( + class VersionTransaction( manager.declarative_base, TransactionBase ): @@ -190,5 +190,5 @@ def __repr__(self): ) if manager.options['native_versioning']: - create_triggers(Transaction) - return Transaction + create_triggers(VersionTransaction) + return VersionTransaction From 8f5257270e8d6b34d8688b80bf6c17d4f8ab6826 Mon Sep 17 00:00:00 2001 From: Emily Old Date: Wed, 11 Nov 2020 16:19:45 -0500 Subject: [PATCH 90/90] update foreign key sequence name --- sqlalchemy_continuum/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_continuum/transaction.py b/sqlalchemy_continuum/transaction.py index 19830059..68f0fd76 100644 --- a/sqlalchemy_continuum/transaction.py +++ b/sqlalchemy_continuum/transaction.py @@ -134,7 +134,7 @@ class VersionTransaction( id = sa.Column( sa.types.BigInteger, - sa.schema.Sequence('transaction_id_seq'), + sa.schema.Sequence('version_transaction_id_seq'), primary_key=True, autoincrement=True ) @@ -177,7 +177,7 @@ def __repr__(self): for field in fields if hasattr(self, field) ) - return '' % ', '.join( + return '' % ', '.join( ( '%s=%r' % (field, value) if not isinstance(value, six.integer_types)