Change name Edge for Path and use py interface

This commit is contained in:
Xavier Bustamante Talavera 2018-08-27 16:32:45 +02:00
parent 9dbe76670c
commit 08a250f162
5 changed files with 89 additions and 34 deletions

View File

@ -2,12 +2,14 @@ from typing import Dict, List, Set
from colour import Color from colour import Color
from sqlalchemy import Column, Integer from sqlalchemy import Column, Integer
from sqlalchemy.orm import relationship
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
RamFormat, RamInterface RamFormat, RamInterface
from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \
EventWithOneDevice EventWithOneDevice
from ereuse_devicehub.resources.image.models import ImageList 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.models import Thing
from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.tag import Tag
@ -24,6 +26,7 @@ class Device(Thing):
height = ... # type: Column height = ... # type: Column
depth = ... # type: Column depth = ... # type: Column
color = ... # type: Column color = ... # type: Column
parents = ... # type: relationship
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
@ -44,6 +47,7 @@ class Device(Thing):
self.events_one = ... # type: Set[EventWithOneDevice] self.events_one = ... # type: Set[EventWithOneDevice]
self.images = ... # type: ImageList self.images = ... # type: ImageList
self.tags = ... # type: Set[Tag] self.tags = ... # type: Set[Tag]
self.parents = ... # type: Set[Lot]
class DisplayMixin: class DisplayMixin:

View File

@ -24,8 +24,8 @@ BEGIN
raise exception 'Cannot create edge: the parent is the same as the child.'; raise exception 'Cannot create edge: the parent is the same as the child.';
end if; end if;
if exists( if EXISTS (
select 1 from edge where edge.path ~ CAST('*.' || child || '.*.' || parent || '.*' as lquery) SELECT 1 FROM path where path.path ~ CAST('*.' || child || '.*.' || parent || '.*' as lquery)
) )
then then
raise exception 'Cannot create edge: child already contains parent.'; raise exception 'Cannot create edge: child already contains parent.';
@ -36,16 +36,14 @@ BEGIN
-- to all the leafs. -- to all the leafs.
-- We do the cartesian product from all the paths of the parent subgraph that end in the parent -- 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. -- 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 || insert into path (lot_id, path) (
subpath(edge.path, index(edge.path, text2ltree(child))) select distinct lot_id, fp.path || subpath(path.path, index(path.path, text2ltree(child)))
from edge, from path, (select path.path from path where path.path ~ CAST('*.' || parent AS lquery)) as fp
(select path where path.path ~ CAST('*.' || child || '.*' AS lquery)
from edge );
where path ~ CAST('*.' || parent AS lquery)) as fp
where edge.path ~ CAST('*.' || child || '.*' AS lquery));
-- Cleanup: old paths that start with the child (that where used above in the cartesian product) -- 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. -- 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 END
$$ $$
LANGUAGE plpgsql; LANGUAGE plpgsql;
@ -70,11 +68,11 @@ BEGIN
-- this part of the path we will have duplicate paths. -- this part of the path we will have duplicate paths.
-- don't check uniqueness for path key until we delete duplicates -- 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 -- remove everything above the child lot_id in the path
-- this creates duplicates on path and lot_id -- this creates duplicates on path and lot_id
update edge update path
set path = subpath(path, index(path, text2ltree(child))) set path = subpath(path, index(path, text2ltree(child)))
where path ~ CAST('*.' || parent || '.' || child || '.*' AS lquery); where path ~ CAST('*.' || parent || '.' || child || '.*' AS lquery);
@ -82,13 +80,14 @@ BEGIN
-- we need an id field exclusively for this operation -- we need an id field exclusively for this operation
-- from https://wiki.postgresql.org/wiki/Deleting_duplicates -- from https://wiki.postgresql.org/wiki/Deleting_duplicates
DELETE DELETE
FROM edge FROM path
WHERE id IN (SELECT id 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); WHERE t.rnum > 1);
-- re-activate uniqueness check and perform check -- 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 -- After the update the one of the paths of the child will be
-- containing only the child. -- containing only the child.
@ -96,10 +95,10 @@ BEGIN
-- In case the child has more than one parent, remove this path -- 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 -- (note that we want it to remove it too from descendants of this
-- child, ex. 'child_id'.'desc1') -- 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 IF number > 1
THEN THEN
delete from edge where path <@ text2ltree(child); delete from path where path <@ text2ltree(child);
end if; end if;
END END

View File

@ -7,12 +7,12 @@ from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import expression from sqlalchemy.sql import expression
from sqlalchemy_utils import LtreeType from sqlalchemy_utils import LtreeType
from sqlalchemy_utils.types.ltree import LQUERY from sqlalchemy_utils.types.ltree import LQUERY
from teal.db import UUIDLtree
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.models import STR_SIZE, Thing from ereuse_devicehub.resources.models import STR_SIZE, Thing
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from teal.db import UUIDLtree
class Lot(Thing): class Lot(Thing):
@ -34,24 +34,24 @@ class Lot(Thing):
:param closed: :param closed:
""" """
super().__init__(id=uuid.uuid4(), name=name, closed=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'): def add_child(self, child: 'Lot'):
"""Adds a child to this 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(self) # todo is this useful?
db.session.refresh(child) db.session.refresh(child)
def remove_child(self, child: 'Lot'): def remove_child(self, child: 'Lot'):
Edge.delete(self.id, child.id) Path.delete(self.id, child.id)
def __contains__(self, child: 'Lot'): def __contains__(self, child: 'Lot'):
return Edge.has_lot(self.id, child.id) return Path.has_lot(self.id, child.id)
@classmethod @classmethod
def roots(cls): def roots(cls):
"""Gets the lots that are not under any other lot.""" """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: def __repr__(self) -> str:
return '<Lot {0.name} devices={0.devices!r}>'.format(self) 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), id = db.Column(db.UUID(as_uuid=True),
primary_key=True, primary_key=True,
server_default=db.text('gen_random_uuid()')) server_default=db.text('gen_random_uuid()'))
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False) lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False)
lot = db.relationship(Lot, 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) primaryjoin=Lot.id == lot_id)
path = db.Column(LtreeType, nullable=False) path = db.Column(LtreeType, nullable=False)
created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP')) created = db.Column(db.TIMESTAMP(timezone=True), server_default=db.text('CURRENT_TIMESTAMP'))
@ -86,7 +86,8 @@ class Edge(db.Model):
""" """
__table_args__ = ( __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_gist', path, postgresql_using='gist'),
db.Index('path_btree', path, postgresql_using='btree') db.Index('path_btree', path, postgresql_using='btree')
) )
@ -95,7 +96,7 @@ class Edge(db.Model):
super().__init__(lot=lot) super().__init__(lot=lot)
self.path = UUIDLtree(lot.id) self.path = UUIDLtree(lot.id)
def children(self) -> Set['Edge']: def children(self) -> Set['Path']:
"""Get the children edges.""" """Get the children edges."""
# todo is it useful? test it when first usage # todo is it useful? test it when first usage
# From https://stackoverflow.com/a/41158890 # From https://stackoverflow.com/a/41158890
@ -118,6 +119,6 @@ class Edge(db.Model):
@classmethod @classmethod
def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool: def has_lot(cls, parent_id: uuid.UUID, child_id: uuid.UUID) -> bool:
return bool(db.session.execute( 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('-', '_')) str(parent_id).replace('-', '_'), str(child_id).replace('-', '_'))
).first()) ).first())

View 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

View File

@ -52,13 +52,13 @@ def test_add_edge():
parent.add_child(child) parent.add_child(child)
assert child in parent assert child in parent
assert len(child.edges) == 1 assert len(child.paths) == 1
assert len(parent.edges) == 1 assert len(parent.paths) == 1
parent.remove_child(child) parent.remove_child(child)
assert child not in parent assert child not in parent
assert len(child.edges) == 1 assert len(child.paths) == 1
assert len(parent.edges) == 1 assert len(parent.paths) == 1
grandparent = Lot('grandparent') grandparent = Lot('grandparent')
db.session.add(grandparent) db.session.add(grandparent)
@ -115,8 +115,8 @@ def test_lot_multiple_parents():
parent.remove_child(child) parent.remove_child(child)
assert child not in parent assert child not in parent
assert len(child.edges) == 1 assert len(child.paths) == 1
assert len(parent.edges) == 1 assert len(parent.paths) == 1
@pytest.mark.usefixtures(conftest.auth_app_context.__name__) @pytest.mark.usefixtures(conftest.auth_app_context.__name__)