Change name Edge for Path and use py interface
This commit is contained in:
parent
9dbe76670c
commit
08a250f162
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 '<Lot {0.name} devices={0.devices!r}>'.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())
|
||||
|
|
51
ereuse_devicehub/resources/lot/models.pyi
Normal file
51
ereuse_devicehub/resources/lot/models.pyi
Normal file
|
@ -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
|
|
@ -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__)
|
||||
|
|
Reference in a new issue