Skip to content

Commit

Permalink
Nested JOINs (#9)
Browse files Browse the repository at this point in the history
* Made ORMJoins be able to join with other ORMJoins recursively

* Bump version: 0.5.1 → 0.6.0

* Tweaks
  • Loading branch information
flipbit03 authored Dec 20, 2022
1 parent e94e2f3 commit 8fc884b
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 120 deletions.
4 changes: 2 additions & 2 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
current_version = 0.5.1
current_version = 0.6.0
commit = True
tag = True
tag = False

[bumpversion:file:pyproject.toml]
search = version = "{current_version}"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool]
[tool.poetry]
name = "sqlalchemy-easy-softdelete"
version = "0.5.1"
version = "0.6.0"
homepage = "https://github.com/flipbit03/sqlalchemy-easy-softdelete"
description = "Easily add soft-deletion to your SQLAlchemy Models."
authors = ["Cadu <cadu.coelho@gmail.com>"]
Expand Down
2 changes: 1 addition & 1 deletion sqlalchemy_easy_softdelete/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

__author__ = """Cadu"""
__email__ = 'cadu.coelho@gmail.com'
__version__ = '0.5.1'
__version__ = '0.6.0'
23 changes: 19 additions & 4 deletions sqlalchemy_easy_softdelete/handler/rewriter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,31 @@ def rewrite_element(self, subquery: Subquery) -> Subquery:

raise NotImplementedError(f"Unsupported object \"{(type(subquery.element))}\" in subquery.element")

def rewrite_from_orm_join(self, stmt: Select, join_obj: Union[_ORMJoin, Join]) -> Select:
# Recursive cases (multiple joins)
if isinstance(join_obj.left, _ORMJoin) or isinstance(join_obj.left, Join):
stmt = self.rewrite_from_orm_join(stmt, join_obj.left)

if isinstance(join_obj.right, _ORMJoin) or isinstance(join_obj.right, Join):
stmt = self.rewrite_from_orm_join(stmt, join_obj.right)

# Normal cases - Tables
if isinstance(join_obj.left, Table):
stmt = self.rewrite_from_table(stmt, join_obj.left)

if isinstance(join_obj.right, Table):
stmt = self.rewrite_from_table(stmt, join_obj.right)

return stmt

def analyze_from(self, stmt: Select, from_obj):
"""Analyze the FROMS of a Select to determine possible soft-delete rewritable tables."""
if isinstance(from_obj, Table):
return self.rewrite_from_table(stmt, from_obj)

if isinstance(from_obj, _ORMJoin) or isinstance(from_obj, Join):
# _ORMJOIN/Join contains information about two tables: 'left' and 'right'. Check both.
left_adapted_stmt = self.rewrite_from_table(stmt, from_obj.left)
right_adapted_stmt = self.rewrite_from_table(left_adapted_stmt, from_obj.right)
return right_adapted_stmt
# _ORMJOIN/Join contains information about two things: 'left' and 'right'. Check both.
return self.rewrite_from_orm_join(stmt, from_obj)

if isinstance(from_obj, Subquery):
self.rewrite_element(from_obj)
Expand Down
8 changes: 6 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@

from sqlalchemy_easy_softdelete.handler.rewriter import SoftDeleteQueryRewriter
from tests.model import TestModelBase
from tests.seed_data import generate_parent_child_object_hierarchy, generate_table_with_inheritance_obj
from tests.seed_data import generate_table_with_inheritance_obj
from tests.seed_data.parent_child_childchild import generate_parent_child_object_hierarchy

test_db_url = os.environ.get("TEST_CONNECTION_STRING", "sqlite://")
env_connection_string = os.environ.get("TEST_CONNECTION_STRING", None)

test_db_url = env_connection_string or "sqlite://"


@pytest.fixture
def db_engine() -> Engine:
print(f"connection_string={test_db_url}")
return create_engine(test_db_url)


Expand Down
13 changes: 12 additions & 1 deletion tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class SoftDeleteMixin(generate_soft_delete_mixin_class()):


class SDSimpleTable(TestModelBase, SoftDeleteMixin):

int_field = Column(Integer)

def __repr__(self):
Expand All @@ -43,12 +42,24 @@ class SDChild(TestModelBase, SoftDeleteMixin):
parent_id = Column(Integer, ForeignKey(f'{SDParent.__tablename__}.id'), nullable=False)
parent: SDParent = relationship('SDParent', back_populates="children")

child_children: 'List[SDChildChild]' = relationship('SDChildChild')

def __repr__(self):
pid = f"(parent_id={self.parent_id})"
left = f"{self.__class__.__name__} id={self.id} deleted={bool(self.deleted_at)}"
return f"<{left:30} {pid:>15}>"


class SDChildChild(TestModelBase, SoftDeleteMixin):
child_id = Column(Integer, ForeignKey(f'{SDChild.__tablename__}.id'), nullable=False)
child: SDChild = relationship('SDChild', back_populates="child_children")

def __repr__(self):
pid = f"(child_id={self.child_id})"
left = f"{self.__class__.__name__} id={self.id} deleted={bool(self.deleted_at)}"
return f"<{left:30} {pid:>15}>"


class SDBaseRequest(
TestModelBase,
SoftDeleteMixin,
Expand Down
46 changes: 0 additions & 46 deletions tests/seed_data.py

This file was deleted.

12 changes: 12 additions & 0 deletions tests/seed_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from datetime import datetime

from sqlalchemy.orm import Session

from tests.model import SDDerivedRequest


def generate_table_with_inheritance_obj(s: Session, obj_id: int, deleted: bool = False):
deleted_at = datetime.utcnow() if deleted else None
new_parent = SDDerivedRequest(id=obj_id, deleted_at=deleted_at)
s.add(new_parent)
s.commit()
54 changes: 54 additions & 0 deletions tests/seed_data/parent_child_childchild.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import datetime
import random

from sqlalchemy.orm import Session

from tests.model import SDChild, SDChildChild, SDParent

TEST_EPOCH = datetime.datetime(year=1985, month=8, day=4)


def pseudorandom_date(max_days: int = 3650) -> datetime:
return TEST_EPOCH + datetime.timedelta(days=random.randint(0, max_days))


def generate_parent_child_object_hierarchy(
s: Session, parent_id: int, min_children: int = 1, max_children: int = 5, parent_deleted: bool = False
):
# Fix a seed in the RNG for deterministic outputs
random.seed(parent_id)

# Generate the Parent
new_parent = SDParent(id=parent_id)
new_parent.deleted_at = pseudorandom_date() if parent_deleted else None
s.add(new_parent)
s.flush()

active_children = random.randint(min_children, max_children)
deleted_children = random.randint(min_children, max_children)

children = [False] * active_children + [True] * deleted_children

# Create Children (SDChild)
for child_no, child_deleted in enumerate(children):
new_child_id = parent_id * 100 + child_no
new_child = SDChild(id=new_child_id, parent=new_parent)
new_child.deleted_at = pseudorandom_date() if child_deleted else None
s.add(new_child)
s.flush()

# Create Child's Children (SDChildChild)
active_child_children = random.randint(min_children, max_children)
deleted_child_children = random.randint(min_children, max_children)

child_children = [False] * active_child_children + [True] * deleted_child_children

for child_children_no, child_children_deleted in enumerate(child_children):
sdchild_child_id = new_child_id * 100 + child_children_no
new_child_child = SDChildChild(id=sdchild_child_id, child=new_child)
child_child_deleted = pseudorandom_date() if child_children_deleted else None
new_child_child.deleted_at = child_child_deleted
s.add(new_child_child)
s.flush()

s.commit()
90 changes: 47 additions & 43 deletions tests/snapshots/snap_test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from snapshottest import GenericRepr, Snapshot


snapshots = Snapshot()

snapshots['test_ensure_aggregate_from_multiple_table_deletion_works_active_object_count 1'] = '''SELECT count(*) AS count_1
Expand Down Expand Up @@ -32,16 +33,16 @@
WHERE sdchild.deleted_at IS NULL'''

snapshots['test_query_single_table 2'] = [
GenericRepr('<SDChild id=1000000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1001000 deleted=False (parent_id=1001)>'),
GenericRepr('<SDChild id=1002000 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002001 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002002 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002003 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002004 deleted=False (parent_id=1002)>')
GenericRepr('<SDChild id=100000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100100 deleted=False (parent_id=1001)>'),
GenericRepr('<SDChild id=100200 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100201 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100202 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100203 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100204 deleted=False (parent_id=1002)>')
]

snapshots['test_query_union_sdchild 1'] = '''SELECT anon_1.sdchild_id, anon_1.sdchild_deleted_at, anon_1.sdchild_parent_id
Expand All @@ -52,30 +53,34 @@
WHERE sdchild.deleted_at IS NULL) AS anon_1'''

snapshots['test_query_union_sdchild 2'] = [
GenericRepr('<SDChild id=1000000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1001000 deleted=False (parent_id=1001)>'),
GenericRepr('<SDChild id=1002000 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002001 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002002 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002003 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002004 deleted=False (parent_id=1002)>')
GenericRepr('<SDChild id=100000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100100 deleted=False (parent_id=1001)>'),
GenericRepr('<SDChild id=100200 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100201 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100202 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100203 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100204 deleted=False (parent_id=1002)>')
]

snapshots['test_query_with_join 1'] = '''SELECT sdchild.id, sdchild.deleted_at, sdchild.parent_id
FROM sdchild JOIN sdparent ON sdparent.id = sdchild.parent_id
WHERE sdchild.deleted_at IS NULL AND sdparent.deleted_at IS NULL'''

snapshots['test_query_with_join 2'] = [
GenericRepr('<SDChild id=1000000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1001000 deleted=False (parent_id=1001)>')
GenericRepr('<SDChild id=100000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100100 deleted=False (parent_id=1001)>')
]

snapshots['test_query_with_more_than_one_join 1'] = '''SELECT sdparent.id, sdparent.deleted_at
FROM sdparent JOIN sdchild ON sdparent.id = sdchild.parent_id JOIN sdchildchild ON sdchild.id = sdchildchild.child_id
WHERE sdparent.id > :id_1 AND sdparent.deleted_at IS NULL AND sdchild.deleted_at IS NULL AND sdchildchild.deleted_at IS NULL'''

snapshots['test_query_with_table_clause_as_table 1'] = '''SELECT id
FROM sdderivedrequest'''

Expand All @@ -89,22 +94,21 @@
FROM sdchild) AS anon_1'''

snapshots['test_query_with_union_but_union_softdelete_disabled 2'] = [
GenericRepr('<SDChild id=1000000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=1000004 deleted=True (parent_id=1000)>'),
GenericRepr('<SDChild id=1001000 deleted=False (parent_id=1001)>'),
GenericRepr('<SDChild id=1001001 deleted=True (parent_id=1001)>'),
GenericRepr('<SDChild id=1001002 deleted=True (parent_id=1001)>'),
GenericRepr('<SDChild id=1002000 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002001 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002002 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002003 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002004 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=1002005 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=1002006 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=1002007 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=1002008 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=1002009 deleted=True (parent_id=1002)>')
GenericRepr('<SDChild id=100000 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100001 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100002 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100003 deleted=False (parent_id=1000)>'),
GenericRepr('<SDChild id=100004 deleted=True (parent_id=1000)>'),
GenericRepr('<SDChild id=100100 deleted=False (parent_id=1001)>'),
GenericRepr('<SDChild id=100101 deleted=True (parent_id=1001)>'),
GenericRepr('<SDChild id=100102 deleted=True (parent_id=1001)>'),
GenericRepr('<SDChild id=100200 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100201 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100202 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100203 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100204 deleted=False (parent_id=1002)>'),
GenericRepr('<SDChild id=100205 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=100206 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=100207 deleted=True (parent_id=1002)>'),
GenericRepr('<SDChild id=100208 deleted=True (parent_id=1002)>')
]
Loading

0 comments on commit 8fc884b

Please sign in to comment.