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 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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
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__)
|
||||||
|
|
Reference in New Issue