aboutsummaryrefslogtreecommitdiff
path: root/ihatemoney/patch_sqlalchemy_continuum.py
diff options
context:
space:
mode:
authorAndrew Dickinson <Andrew-Dickinson@users.noreply.github.com>2020-04-20 09:30:27 -0400
committerGitHub <noreply@github.com>2020-04-20 15:30:27 +0200
commit026a0722357d74b143ed2d974ad2d871a56041b3 (patch)
tree2f23323f01e5ec1dec07ef1032acc407cba38879 /ihatemoney/patch_sqlalchemy_continuum.py
parent91ef80ebb712b06b6c48336beeb7f60219b0f062 (diff)
downloadihatemoney-mirror-026a0722357d74b143ed2d974ad2d871a56041b3.zip
ihatemoney-mirror-026a0722357d74b143ed2d974ad2d871a56041b3.tar.gz
ihatemoney-mirror-026a0722357d74b143ed2d974ad2d871a56041b3.tar.bz2
Add Project History Page (#553)
Co-Authored-By: Glandos <bugs-github@antipoul.fr> All project activity can be tracked, using SQLAlchemy-continuum. IP addresses can optionally be recorded.
Diffstat (limited to 'ihatemoney/patch_sqlalchemy_continuum.py')
-rw-r--r--ihatemoney/patch_sqlalchemy_continuum.py138
1 files changed, 138 insertions, 0 deletions
diff --git a/ihatemoney/patch_sqlalchemy_continuum.py b/ihatemoney/patch_sqlalchemy_continuum.py
new file mode 100644
index 0000000..e0680c6
--- /dev/null
+++ b/ihatemoney/patch_sqlalchemy_continuum.py
@@ -0,0 +1,138 @@
+"""
+A temporary work-around to patch SQLAlchemy-continuum per:
+https://github.com/kvesteri/sqlalchemy-continuum/pull/242
+
+Source code reproduced under their license:
+
+ Copyright (c) 2012, Konsta Vesterinen
+
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ 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.
+
+ 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
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import sqlalchemy as sa
+from sqlalchemy_continuum import Operation
+from sqlalchemy_continuum.builder import Builder
+from sqlalchemy_continuum.expression_reflector import VersionExpressionReflector
+from sqlalchemy_continuum.relationship_builder import RelationshipBuilder
+from sqlalchemy_continuum.utils import option, adapt_columns
+
+
+class PatchedRelationShipBuilder(RelationshipBuilder):
+ def association_subquery(self, obj):
+ """
+ Returns an EXISTS clause that checks if an association exists for given
+ SQLAlchemy declarative object. This query is used by
+ many_to_many_criteria method.
+
+ Example query:
+
+ .. code-block:: sql
+
+ EXISTS (
+ SELECT 1
+ FROM article_tag_version
+ WHERE article_id = 3
+ AND tag_id = tags_version.id
+ AND operation_type != 2
+ AND EXISTS (
+ SELECT 1
+ 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) =
+ article_tag_version.tx_id
+ )
+ )
+
+ :param obj: SQLAlchemy declarative object
+ """
+
+ 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()
+ association_cols = [
+ association_table_alias.c[association_col.name]
+ for _, association_col in self.remote_to_association_column_pairs
+ ]
+
+ association_exists = sa.exists(
+ sa.select([1])
+ .where(
+ 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 in association_cols
+ ]
+ )
+ )
+ .group_by(*association_cols)
+ .having(
+ sa.func.max(association_table_alias.c[tx_column])
+ == self.association_version_table.c[tx_column]
+ )
+ .correlate(self.association_version_table)
+ )
+ return sa.exists(
+ sa.select([1])
+ .where(
+ sa.and_(
+ reflector(self.property.primaryjoin),
+ association_exists,
+ self.association_version_table.c.operation_type != Operation.DELETE,
+ adapt_columns(self.property.secondaryjoin),
+ )
+ )
+ .correlate(self.local_cls, self.remote_cls)
+ )
+
+
+class PatchedBuilder(Builder):
+ def build_relationships(self, version_classes):
+ """
+ Builds relationships for all version classes.
+
+ :param version_classes: list of generated version classes
+ """
+ for cls in version_classes:
+ if not self.manager.option(cls, "versioning"):
+ continue
+
+ for prop in sa.inspect(cls).iterate_properties:
+ if prop.key == "versions":
+ continue
+ builder = PatchedRelationShipBuilder(self.manager, cls, prop)
+ builder()