diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index fbdaeea0..3efe9e6c 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -2,12 +2,14 @@ from typing import Dict, List, Set from colour import Color from sqlalchemy import Column, Integer +from sqlalchemy.orm import relationship from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ RamFormat, RamInterface from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ EventWithOneDevice from ereuse_devicehub.resources.image.models import ImageList +from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.tag import Tag @@ -24,6 +26,7 @@ class Device(Thing): height = ... # type: Column depth = ... # type: Column color = ... # type: Column + parents = ... # type: relationship def __init__(self, **kwargs) -> None: super().__init__(**kwargs) @@ -44,6 +47,7 @@ class Device(Thing): self.events_one = ... # type: Set[EventWithOneDevice] self.images = ... # type: ImageList self.tags = ... # type: Set[Tag] + self.parents = ... # type: Set[Lot] class DisplayMixin: diff --git a/ereuse_devicehub/resources/lot/dag.sql b/ereuse_devicehub/resources/lot/dag.sql index 3807bde5..3f4b4840 100644 --- a/ereuse_devicehub/resources/lot/dag.sql +++ b/ereuse_devicehub/resources/lot/dag.sql @@ -24,8 +24,8 @@ BEGIN raise exception 'Cannot create edge: the parent is the same as the child.'; end if; - if exists( - select 1 from edge where edge.path ~ CAST('*.' || child || '.*.' || parent || '.*' as lquery) + if EXISTS ( + SELECT 1 FROM path where path.path ~ CAST('*.' || child || '.*.' || parent || '.*' as lquery) ) then raise exception 'Cannot create edge: child already contains parent.'; @@ -36,16 +36,14 @@ BEGIN -- to all the leafs. -- We do the cartesian product from all the paths of the parent subgraph that end in the parent -- WITH all the paths that start from the child that end to its leafs. - insert into edge (lot_id, path) (select distinct lot_id, fp.path || - subpath(edge.path, index(edge.path, text2ltree(child))) - from edge, - (select path - from edge - where path ~ CAST('*.' || parent AS lquery)) as fp - where edge.path ~ CAST('*.' || child || '.*' AS lquery)); + insert into path (lot_id, path) ( + select distinct lot_id, fp.path || subpath(path.path, index(path.path, text2ltree(child))) + from path, (select path.path from path where path.path ~ CAST('*.' || parent AS lquery)) as fp + where path.path ~ CAST('*.' || child || '.*' AS lquery) + ); -- Cleanup: old paths that start with the child (that where used above in the cartesian product) -- have became a subset of the result of the cartesian product, thus being redundant. - delete from edge where edge.path ~ CAST(child || '.*' AS lquery); + delete from path where path.path ~ CAST(child || '.*' AS lquery); END $$ LANGUAGE plpgsql; @@ -70,11 +68,11 @@ BEGIN -- this part of the path we will have duplicate paths. -- don't check uniqueness for path key until we delete duplicates - SET CONSTRAINTS edge_path_unique DEFERRED; + SET CONSTRAINTS path_unique DEFERRED; -- remove everything above the child lot_id in the path -- this creates duplicates on path and lot_id - update edge + update path set path = subpath(path, index(path, text2ltree(child))) where path ~ CAST('*.' || parent || '.' || child || '.*' AS lquery); @@ -82,13 +80,14 @@ BEGIN -- we need an id field exclusively for this operation -- from https://wiki.postgresql.org/wiki/Deleting_duplicates DELETE - FROM edge + FROM path WHERE id IN (SELECT id - FROM (SELECT id, ROW_NUMBER() OVER (partition BY lot_id, path) AS rnum FROM edge) t + FROM (SELECT id, ROW_NUMBER() OVER (partition BY lot_id, path) AS rnum FROM path) t WHERE t.rnum > 1); -- re-activate uniqueness check and perform check - SET CONSTRAINTS edge_path_unique IMMEDIATE; + -- todo we should put this in a kind-of finally clause + SET CONSTRAINTS path_unique IMMEDIATE; -- After the update the one of the paths of the child will be -- containing only the child. @@ -96,10 +95,10 @@ BEGIN -- In case the child has more than one parent, remove this path -- (note that we want it to remove it too from descendants of this -- child, ex. 'child_id'.'desc1') - select COUNT(1) into number from edge where lot_id = child_id; + select COUNT(1) into number from path where lot_id = child_id; IF number > 1 THEN - delete from edge where path <@ text2ltree(child); + delete from path where path <@ text2ltree(child); end if; END diff --git a/ereuse_devicehub/resources/lot/models.py b/ereuse_devicehub/resources/lot/models.py index a264183c..b11ac785 100644 --- a/ereuse_devicehub/resources/lot/models.py +++ b/ereuse_devicehub/resources/lot/models.py @@ -7,12 +7,12 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import expression from sqlalchemy_utils import LtreeType from sqlalchemy_utils.types.ltree import LQUERY +from teal.db import UUIDLtree from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.models import STR_SIZE, Thing from ereuse_devicehub.resources.user.models import User -from teal.db import UUIDLtree class Lot(Thing): @@ -34,24 +34,24 @@ class Lot(Thing): :param closed: """ super().__init__(id=uuid.uuid4(), name=name, closed=closed) - Edge(self) # Lots have always one edge per default. + Path(self) # Lots have always one edge per default. def add_child(self, child: 'Lot'): """Adds a child to this lot.""" - Edge.add(self.id, child.id) + Path.add(self.id, child.id) db.session.refresh(self) # todo is this useful? db.session.refresh(child) def remove_child(self, child: 'Lot'): - Edge.delete(self.id, child.id) + Path.delete(self.id, child.id) def __contains__(self, child: 'Lot'): - return Edge.has_lot(self.id, child.id) + return Path.has_lot(self.id, child.id) @classmethod def roots(cls): """Gets the lots that are not under any other lot.""" - return set(cls.query.join(cls.edges).filter(db.func.nlevel(Edge.path) == 1).all()) + return set(cls.query.join(cls.paths).filter(db.func.nlevel(Path.path) == 1).all()) def __repr__(self) -> str: return ''.format(self) @@ -71,13 +71,13 @@ class LotDevice(db.Model): """ -class Edge(db.Model): +class Path(db.Model): id = db.Column(db.UUID(as_uuid=True), primary_key=True, server_default=db.text('gen_random_uuid()')) lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) lot = db.relationship(Lot, - backref=db.backref('edges', lazy=True, collection_class=set), + backref=db.backref('paths', lazy=True, collection_class=set), primaryjoin=Lot.id == lot_id) path = db.Column(LtreeType, nullable=False) created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP')) @@ -86,7 +86,8 @@ class Edge(db.Model): """ __table_args__ = ( - db.UniqueConstraint(path, name='edge_path_unique', deferrable=True, initially='immediate'), + # dag.delete_edge needs to disable internally/temporarily the unique constraint + db.UniqueConstraint(path, name='path_unique', deferrable=True, initially='immediate'), db.Index('path_gist', path, postgresql_using='gist'), db.Index('path_btree', path, postgresql_using='btree') ) @@ -95,7 +96,7 @@ class Edge(db.Model): super().__init__(lot=lot) self.path = UUIDLtree(lot.id) - def children(self) -> Set['Edge']: + def children(self) -> Set['Path']: """Get the children edges.""" # todo is it useful? test it when first usage # From https://stackoverflow.com/a/41158890 @@ -118,6 +119,6 @@ class Edge(db.Model): @classmethod def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool: return bool(db.session.execute( - "SELECT 1 from edge where path ~ '*.{}.*.{}.*'".format( + "SELECT 1 from path where path ~ '*.{}.*.{}.*'".format( str(parent_id).replace('-', '_'), str(child_id).replace('-', '_')) ).first()) diff --git a/ereuse_devicehub/resources/lot/models.pyi b/ereuse_devicehub/resources/lot/models.pyi new file mode 100644 index 00000000..b8f60afe --- /dev/null +++ b/ereuse_devicehub/resources/lot/models.pyi @@ -0,0 +1,51 @@ +from datetime import datetime +from typing import Set +from uuid import UUID + +from sqlalchemy import Column +from sqlalchemy.orm import relationship +from sqlalchemy_utils import Ltree + +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.models import Thing + + +class Lot(Thing): + id = ... # type: Column + name = ... # type: Column + closed = ... # type: Column + devices = ... # type: relationship + paths = ... # type: relationship + + def __init__(self, name: str, closed: bool = closed.default.arg) -> None: + super().__init__() + self.id = ... # type: UUID + self.name = ... # type: str + self.closed = ... # type: bool + self.devices = ... # type: Set[Device] + self.paths = ... # type: Set[Path] + + def add_child(self, child: 'Lot'): + pass + + def remove_child(self, child: 'Lot'): + pass + + @classmethod + def roots(cls): + pass + + +class Path: + id = ... # type: Column + lot_id = ... # type: Column + lot = ... # type: relationship + path = ... # type: Column + created = ... # type: Column + + def __init__(self, lot: Lot) -> None: + super().__init__() + self.id = ... # type: UUID + self.lot = ... # type: Lot + self.path = ... # type: Ltree + self.created = ... # type: datetime diff --git a/tests/test_lot.py b/tests/test_lot.py index 97edb752..1fa50a93 100644 --- a/tests/test_lot.py +++ b/tests/test_lot.py @@ -52,13 +52,13 @@ def test_add_edge(): parent.add_child(child) assert child in parent - assert len(child.edges) == 1 - assert len(parent.edges) == 1 + assert len(child.paths) == 1 + assert len(parent.paths) == 1 parent.remove_child(child) assert child not in parent - assert len(child.edges) == 1 - assert len(parent.edges) == 1 + assert len(child.paths) == 1 + assert len(parent.paths) == 1 grandparent = Lot('grandparent') db.session.add(grandparent) @@ -115,8 +115,8 @@ def test_lot_multiple_parents(): parent.remove_child(child) assert child not in parent - assert len(child.edges) == 1 - assert len(parent.edges) == 1 + assert len(child.paths) == 1 + assert len(parent.paths) == 1 @pytest.mark.usefixtures(conftest.auth_app_context.__name__)