Skip to content
Snippets Groups Projects
Commit 1a294d69 authored by Henrik tom Wörden's avatar Henrik tom Wörden Committed by Florian Spreckelsen
Browse files

FIX: Treat dependencies inside containers when split for deletion

parent a9456cad
No related branches found
No related tags found
2 merge requests!33MAINT: change arguments of create_user,!14F delete container
...@@ -45,6 +45,8 @@ def LIST(datatype): ...@@ -45,6 +45,8 @@ def LIST(datatype):
def get_list_datatype(datatype): def get_list_datatype(datatype):
""" returns the datatype of the elements in the list """ """ returns the datatype of the elements in the list """
if not isinstance(datatype, str):
return None
match = re.match("LIST(<|&lt;)(?P<datatype>.*)(>|&gt;)", datatype) match = re.match("LIST(<|&lt;)(?P<datatype>.*)(>|&gt;)", datatype)
if match is not None: if match is not None:
......
...@@ -40,26 +40,23 @@ from sys import hexversion ...@@ -40,26 +40,23 @@ from sys import hexversion
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from warnings import warn from warnings import warn
from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT) from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, INTEGER, TEXT,
from caosdb.common.versioning import Version is_list_datatype, is_reference)
from caosdb.common.state import State from caosdb.common.state import State
from caosdb.common.utils import uuid, xml2str from caosdb.common.utils import uuid, xml2str
from caosdb.common.versioning import Version
from caosdb.configuration import get_config from caosdb.configuration import get_config
from caosdb.connection.connection import get_connection from caosdb.connection.connection import get_connection
from caosdb.connection.encode import MultipartParam, multipart_encode from caosdb.connection.encode import MultipartParam, multipart_encode
from caosdb.exceptions import (AmbiguousEntityError, from caosdb.exceptions import (AmbiguousEntityError, AuthorizationError,
AuthorizationError, CaosDBConnectionError, CaosDBException,
CaosDBException, CaosDBConnectionError, ConsistencyError, EmptyUniqueQueryError,
ConsistencyError,
EmptyUniqueQueryError,
EntityDoesNotExistError, EntityError, EntityDoesNotExistError, EntityError,
EntityHasNoDatatypeError, EntityHasNoDatatypeError, HTTPURITooLongError,
MismatchingEntitiesError, MismatchingEntitiesError, QueryNotUniqueError,
QueryNotUniqueError, TransactionError, TransactionError, UniqueNamesError,
UniqueNamesError,
UnqualifiedParentsError, UnqualifiedParentsError,
UnqualifiedPropertiesError, UnqualifiedPropertiesError)
HTTPURITooLongError)
from lxml import etree from lxml import etree
_ENTITY_URI_SEGMENT = "Entity" _ENTITY_URI_SEGMENT = "Entity"
...@@ -580,6 +577,7 @@ class Entity(object): ...@@ -580,6 +577,7 @@ class Entity(object):
entity, or name. entity, or name.
""" """
if isinstance(key, int): if isinstance(key, int):
for p in self.parents: for p in self.parents:
if p.id is not None and int(p.id) == int(key): if p.id is not None and int(p.id) == int(key):
...@@ -588,14 +586,17 @@ class Entity(object): ...@@ -588,14 +586,17 @@ class Entity(object):
if key.id is not None: if key.id is not None:
# first try by id # first try by id
found = self.get_parent(int(key.id)) found = self.get_parent(int(key.id))
if found is not None: if found is not None:
return found return found
# otherwise by name # otherwise by name
return self.get_parent(key.name) return self.get_parent(key.name)
else: else:
for p in self.parents: for p in self.parents:
if (p.name is not None if (p.name is not None
and str(p.name).lower() == str(key).lower()): and str(p.name).lower() == str(key).lower()):
return p return p
return None return None
...@@ -680,6 +681,7 @@ class Entity(object): ...@@ -680,6 +681,7 @@ class Entity(object):
special_selector = None special_selector = None
# iterating through the entity tree according to the selector # iterating through the entity tree according to the selector
for subselector in selector: for subselector in selector:
# selector does not match the structure, we cannot get a # selector does not match the structure, we cannot get a
# property of non-entity # property of non-entity
...@@ -691,11 +693,13 @@ class Entity(object): ...@@ -691,11 +693,13 @@ class Entity(object):
# selector does not match the structure, we did not get a # selector does not match the structure, we did not get a
# property # property
if prop is None: if prop is None:
return None return None
# if the property is a reference, we are interested in the # if the property is a reference, we are interested in the
# corresponding entities attributes # corresponding entities attributes
if isinstance(prop.value, Entity): if isinstance(prop.value, Entity):
ref = prop.value ref = prop.value
...@@ -704,6 +708,7 @@ class Entity(object): ...@@ -704,6 +708,7 @@ class Entity(object):
ref = prop ref = prop
# if we saved a special selector before, apply it # if we saved a special selector before, apply it
if special_selector is None: if special_selector is None:
return prop.value return prop.value
else: else:
...@@ -835,6 +840,7 @@ class Entity(object): ...@@ -835,6 +840,7 @@ class Entity(object):
assert isinstance(xml, etree._Element) assert isinstance(xml, etree._Element)
# unwrap wrapped entity # unwrap wrapped entity
if self._wrapped_entity is not None: if self._wrapped_entity is not None:
xml = self._wrapped_entity.to_xml(xml, add_properties) xml = self._wrapped_entity.to_xml(xml, add_properties)
...@@ -1074,6 +1080,7 @@ class Entity(object): ...@@ -1074,6 +1080,7 @@ class Entity(object):
if len(c) == 1: if len(c) == 1:
c[0].messages.extend(c.messages) c[0].messages.extend(c.messages)
return c[0] return c[0]
raise QueryNotUniqueError("This retrieval was not unique!!!") raise QueryNotUniqueError("This retrieval was not unique!!!")
...@@ -2145,6 +2152,7 @@ class _Messages(dict): ...@@ -2145,6 +2152,7 @@ class _Messages(dict):
else: else:
raise TypeError( raise TypeError(
"('description', 'body'), ('body'), or 'body' expected.") "('description', 'body'), ('body'), or 'body' expected.")
if isinstance(value, Message): if isinstance(value, Message):
body = value.body body = value.body
description = value.description description = value.description
...@@ -2314,6 +2322,7 @@ def _deletion_sync(e_local, e_remote): ...@@ -2314,6 +2322,7 @@ def _deletion_sync(e_local, e_remote):
except KeyError: except KeyError:
# deletion info wasn't there # deletion info wasn't there
e_local.messages = e_remote.messages e_local.messages = e_remote.messages
return return
_basic_sync(e_local, e_remote) _basic_sync(e_local, e_remote)
...@@ -2506,6 +2515,7 @@ class Container(list): ...@@ -2506,6 +2515,7 @@ class Container(list):
tmpid = 0 tmpid = 0
# users might already have specified some tmpids. -> look for smallest. # users might already have specified some tmpids. -> look for smallest.
for e in self: for e in self:
tmpid = min(tmpid, Container._get_smallest_tmpid(e)) tmpid = min(tmpid, Container._get_smallest_tmpid(e))
tmpid -= 1 tmpid -= 1
...@@ -2725,6 +2735,7 @@ class Container(list): ...@@ -2725,6 +2735,7 @@ class Container(list):
used_remote_entities = [] used_remote_entities = []
# match by cuid # match by cuid
for local_entity in self: for local_entity in self:
sync_dict[local_entity] = None sync_dict[local_entity] = None
...@@ -2753,6 +2764,7 @@ class Container(list): ...@@ -2753,6 +2764,7 @@ class Container(list):
raise MismatchingEntitiesError(msg) raise MismatchingEntitiesError(msg)
# match by id # match by id
for local_entity in self: for local_entity in self:
if sync_dict[local_entity] is None and local_entity.id is not None: if sync_dict[local_entity] is None and local_entity.id is not None:
sync_remote_entities = [] sync_remote_entities = []
...@@ -2777,6 +2789,7 @@ class Container(list): ...@@ -2777,6 +2789,7 @@ class Container(list):
raise MismatchingEntitiesError(msg) raise MismatchingEntitiesError(msg)
# match by path # match by path
for local_entity in self: for local_entity in self:
if (sync_dict[local_entity] is None if (sync_dict[local_entity] is None
and local_entity.path is not None): and local_entity.path is not None):
...@@ -2806,6 +2819,7 @@ class Container(list): ...@@ -2806,6 +2819,7 @@ class Container(list):
raise MismatchingEntitiesError(msg) raise MismatchingEntitiesError(msg)
# match by name # match by name
for local_entity in self: for local_entity in self:
if (sync_dict[local_entity] is None if (sync_dict[local_entity] is None
and local_entity.name is not None): and local_entity.name is not None):
...@@ -2855,7 +2869,60 @@ class Container(list): ...@@ -2855,7 +2869,60 @@ class Container(list):
return sync_dict return sync_dict
def delete(self, raise_exception_on_error=True, flags=None): def _test_dependencies_in_container(self, container):
"""This function returns those elements of a given container that are a dependency of another element of the same container.
Args:
container (Container): a caosdb container
Returns:
[set]: a set of unique elements that are a dependency of another element of `container`
"""
item_id = set()
is_parent = set()
is_property = set()
is_being_referenced = set()
dependent_parents = set()
dependent_properties = set()
dependent_references = set()
dependencies = set()
for container_item in container:
item_id.add(container_item.id)
for parents in container_item.get_parents():
is_parent.add(parents.id)
for references in container_item.get_properties():
if is_reference(references.datatype):
# add only if it is a reference, not a property
if isinstance(references.value, int):
is_being_referenced.add(references.value)
elif is_list_datatype(references.datatype):
for list_item in references.value:
if isinstance(list_item, int):
is_being_referenced.add(list_item)
else:
is_being_referenced.add(list_item.id)
else:
try:
is_being_referenced.add(references.value.id)
except:
pass
if hasattr(references, 'id'):
is_property.add(references.id)
dependent_parents = item_id.intersection(is_parent)
dependent_properties = item_id.intersection(is_property)
dependent_references = item_id.intersection(is_being_referenced)
dependencies = dependent_parents.union(dependent_references)
dependencies = dependencies.union(dependent_properties)
return dependencies
def delete(self, raise_exception_on_error=True, flags=None, chunk_size=100):
"""Delete all entities in this container. """Delete all entities in this container.
Entities are identified via their id if present and via their Entities are identified via their id if present and via their
...@@ -2866,16 +2933,43 @@ class Container(list): ...@@ -2866,16 +2933,43 @@ class Container(list):
this happens, none of them will be deleted. It occurs an error this happens, none of them will be deleted. It occurs an error
instead. instead.
""" """
chunk_size = 100
item_count = len(self) item_count = len(self)
# Split Container in 'chunk_size'-sized containers (if necessary) to avoid error 414 Request-URI Too Long # Split Container in 'chunk_size'-sized containers (if necessary) to avoid error 414 Request-URI Too Long
if item_count > chunk_size: if item_count > chunk_size:
dependencies = self._test_dependencies_in_container(self)
'''
If there are as many dependencies as entities in the container and it is larger than chunk_size it cannot be split and deleted.
This case cannot be handled at the moment.
'''
if len(dependencies) == item_count:
if raise_exception_on_error:
te = TransactionError(
msg="The container is too large and with too many dependencies within to be deleted.",
container=self)
raise te
return self
dependencies_delete = Container() # items which have to be deleted later because of dependencies.
for i in range(0, int(item_count/chunk_size)+1): for i in range(0, int(item_count/chunk_size)+1):
chunk = Container() chunk = Container()
for j in range(i*chunk_size, min(item_count, (i+1)*chunk_size)): for j in range(i*chunk_size, min(item_count, (i+1)*chunk_size)):
if len(dependencies):
if self[j].id in dependencies:
dependencies_delete.append(self[j])
else:
chunk.append(self[j])
else:
chunk.append(self[j]) chunk.append(self[j])
if len(chunk): if len(chunk):
chunk.delete() chunk.delete()
dependencies_delete.delete()
return self return self
if len(self) == 0: if len(self) == 0:
...@@ -3513,6 +3607,7 @@ class ACL(): ...@@ -3513,6 +3607,7 @@ class ACL():
result._priority_grants.update(self._priority_grants) result._priority_grants.update(self._priority_grants)
result._priority_denials.update(other._priority_denials) result._priority_denials.update(other._priority_denials)
result._priority_denials.update(self._priority_denials) result._priority_denials.update(self._priority_denials)
return result return result
def __eq__(self, other): def __eq__(self, other):
...@@ -3803,6 +3898,7 @@ class Query(): ...@@ -3803,6 +3898,7 @@ class Query():
connection = get_connection() connection = get_connection()
flags = self.flags flags = self.flags
if cache is False: if cache is False:
flags["cache"] = "false" flags["cache"] = "false"
query_dict = dict(flags) query_dict = dict(flags)
...@@ -3829,9 +3925,11 @@ class Query(): ...@@ -3829,9 +3925,11 @@ class Query():
if len(cresp) > 1 and raise_exception_on_error: if len(cresp) > 1 and raise_exception_on_error:
raise QueryNotUniqueError( raise QueryNotUniqueError(
"Query '{}' wasn't unique.".format(self.q)) "Query '{}' wasn't unique.".format(self.q))
if len(cresp) == 0 and raise_exception_on_error: if len(cresp) == 0 and raise_exception_on_error:
raise EmptyUniqueQueryError( raise EmptyUniqueQueryError(
"Query '{}' found no results.".format(self.q)) "Query '{}' found no results.".format(self.q))
if len(cresp) == 1: if len(cresp) == 1:
r = cresp[0] r = cresp[0]
r.messages.extend(cresp.messages) r.messages.extend(cresp.messages)
...@@ -3938,6 +4036,7 @@ class Info(): ...@@ -3938,6 +4036,7 @@ class Info():
for e in xml: for e in xml:
m = _parse_single_xml_element(e) m = _parse_single_xml_element(e)
if isinstance(m, UserInfo): if isinstance(m, UserInfo):
self.user_info = m self.user_info = m
else: else:
...@@ -4097,13 +4196,16 @@ def _evaluate_and_add_error(parent_error, ent): ...@@ -4097,13 +4196,16 @@ def _evaluate_and_add_error(parent_error, ent):
Parent error with new exception(s) attached to it. Parent error with new exception(s) attached to it.
""" """
if isinstance(ent, (Entity, QueryTemplate)): if isinstance(ent, (Entity, QueryTemplate)):
# Check all error messages # Check all error messages
found114 = False found114 = False
found116 = False found116 = False
for err in ent.get_errors(): for err in ent.get_errors():
# Evaluate specific EntityErrors depending on the error # Evaluate specific EntityErrors depending on the error
# code # code
if err.code is not None: if err.code is not None:
if int(err.code) == 101: # ent doesn't exist if int(err.code) == 101: # ent doesn't exist
new_exc = EntityDoesNotExistError(entity=ent, new_exc = EntityDoesNotExistError(entity=ent,
...@@ -4120,6 +4222,7 @@ def _evaluate_and_add_error(parent_error, ent): ...@@ -4120,6 +4222,7 @@ def _evaluate_and_add_error(parent_error, ent):
found114 = True found114 = True
new_exc = UnqualifiedPropertiesError(entity=ent, new_exc = UnqualifiedPropertiesError(entity=ent,
error=err) error=err)
for prop in ent.get_properties(): for prop in ent.get_properties():
new_exc = _evaluate_and_add_error(new_exc, new_exc = _evaluate_and_add_error(new_exc,
prop) prop)
...@@ -4127,6 +4230,7 @@ def _evaluate_and_add_error(parent_error, ent): ...@@ -4127,6 +4230,7 @@ def _evaluate_and_add_error(parent_error, ent):
found116 = True found116 = True
new_exc = UnqualifiedParentsError(entity=ent, new_exc = UnqualifiedParentsError(entity=ent,
error=err) error=err)
for par in ent.get_parents(): for par in ent.get_parents():
new_exc = _evaluate_and_add_error(new_exc, new_exc = _evaluate_and_add_error(new_exc,
par) par)
...@@ -4137,21 +4241,28 @@ def _evaluate_and_add_error(parent_error, ent): ...@@ -4137,21 +4241,28 @@ def _evaluate_and_add_error(parent_error, ent):
parent_error.add_error(new_exc) parent_error.add_error(new_exc)
# Check for possible errors in parents and properties that # Check for possible errors in parents and properties that
# weren't detected up to here # weren't detected up to here
if not found114: if not found114:
dummy_err = EntityError(entity=ent) dummy_err = EntityError(entity=ent)
for prop in ent.get_properties(): for prop in ent.get_properties():
dummy_err = _evaluate_and_add_error(dummy_err, prop) dummy_err = _evaluate_and_add_error(dummy_err, prop)
if dummy_err.errors: if dummy_err.errors:
parent_error.add_error(dummy_err) parent_error.add_error(dummy_err)
if not found116: if not found116:
dummy_err = EntityError(entity=ent) dummy_err = EntityError(entity=ent)
for par in ent.get_parents(): for par in ent.get_parents():
dummy_err = _evaluate_and_add_error(dummy_err, par) dummy_err = _evaluate_and_add_error(dummy_err, par)
if dummy_err.errors: if dummy_err.errors:
parent_error.add_error(dummy_err) parent_error.add_error(dummy_err)
elif isinstance(ent, Container): elif isinstance(ent, Container):
parent_error.container = ent parent_error.container = ent
if ent.get_errors() is not None: if ent.get_errors() is not None:
parent_error.code = ent.get_errors()[0].code parent_error.code = ent.get_errors()[0].code
# In the highly unusual case of more than one error # In the highly unusual case of more than one error
...@@ -4159,6 +4270,7 @@ def _evaluate_and_add_error(parent_error, ent): ...@@ -4159,6 +4270,7 @@ def _evaluate_and_add_error(parent_error, ent):
parent_error.msg = '\n'.join( parent_error.msg = '\n'.join(
[x.description for x in ent.get_errors()]) [x.description for x in ent.get_errors()])
# Go through all container elements and add them: # Go through all container elements and add them:
for elt in ent: for elt in ent:
parent_error = _evaluate_and_add_error(parent_error, elt) parent_error = _evaluate_and_add_error(parent_error, elt)
...@@ -4184,10 +4296,12 @@ def raise_errors(arg0): ...@@ -4184,10 +4296,12 @@ def raise_errors(arg0):
transaction_error = _evaluate_and_add_error(TransactionError(), transaction_error = _evaluate_and_add_error(TransactionError(),
arg0) arg0)
# Raise if any error was found # Raise if any error was found
if len(transaction_error.all_errors) > 0: if len(transaction_error.all_errors) > 0:
raise transaction_error raise transaction_error
# Cover the special case of an empty container with error # Cover the special case of an empty container with error
# message(s) (e.g. query syntax error) # message(s) (e.g. query syntax error)
if (transaction_error.container is not None and if (transaction_error.container is not None and
transaction_error.container.has_errors()): transaction_error.container.has_errors()):
raise transaction_error raise transaction_error
......
...@@ -25,28 +25,28 @@ ...@@ -25,28 +25,28 @@
"""Tests for the Container class.""" """Tests for the Container class."""
from __future__ import absolute_import from __future__ import absolute_import
import caosdb as c import caosdb as db
def test_get_property_values(): def test_get_property_values():
rt_house = c.RecordType("House") rt_house = db.RecordType("House")
rt_window = c.RecordType("Window") rt_window = db.RecordType("Window")
rt_owner = c.RecordType("Owner") rt_owner = db.RecordType("Owner")
p_height = c.Property("Height", datatype=c.DOUBLE) p_height = db.Property("Height", datatype=db.DOUBLE)
window = c.Record().add_parent(rt_window) window = db.Record().add_parent(rt_window)
window.id = 1001 window.id = 1001
window.add_property(p_height, 20.5, unit="m") window.add_property(p_height, 20.5, unit="m")
owner = c.Record("The Queen").add_parent(rt_owner) owner = db.Record("The Queen").add_parent(rt_owner)
house = c.Record("Buckingham Palace") house = db.Record("Buckingham Palace")
house.add_parent(rt_house) house.add_parent(rt_house)
house.add_property(rt_owner, owner) house.add_property(rt_owner, owner)
house.add_property(rt_window, window) house.add_property(rt_window, window)
house.add_property(p_height, 40.2, unit="ft") house.add_property(p_height, 40.2, unit="ft")
container = c.Container() container = db.Container()
container.extend([ container.extend([
house, house,
owner owner
...@@ -77,3 +77,68 @@ def test_get_property_values(): ...@@ -77,3 +77,68 @@ def test_get_property_values():
assert container.get_property_values("non-existing") == [(None,), (None,)] assert container.get_property_values("non-existing") == [(None,), (None,)]
assert container.get_property_values("name") == [(house.name,), assert container.get_property_values("name") == [(house.name,),
(owner.name,)] (owner.name,)]
def test_container_dependencies_for_deletion():
not_included_rt = 1000
rt = db.RecordType("Just a RecordType")
rt.id = 1001
rt_record_with_parent = db.RecordType("Records with parent")
rt_record_with_parent.id = 1005
property_which_is_not_a_record = db.Property(
"Normal Property", datatype=db.DOUBLE, value=1006)
property_which_is_not_a_record.id = 1006
property_which_shall_be_deleted = db.Property(
"Normal Property 2", datatype=db.DOUBLE, value=1006)
property_which_shall_be_deleted .id = 1007
record_without_dependencies = db.Record().add_parent(not_included_rt)
record_without_dependencies.id = 2003
record_referenced = db.Record().add_parent(not_included_rt)
record_referenced.id = 2002
record_with_dependencies = db.Record().add_parent(not_included_rt)
record_with_dependencies.id = 2004
record_with_dependencies.add_property(not_included_rt, record_referenced)
record_with_parent = db.Record().add_parent(rt_record_with_parent)
record_with_parent.id = 2005
record_with_property_which_is_not_a_record = db.Record(
).add_parent(not_included_rt)
record_with_property_which_is_not_a_record.id = 2006
record_with_property_which_is_not_a_record.add_property(
property_which_is_not_a_record)
record_with_property_which_is_not_a_record.add_property(
property_which_shall_be_deleted)
container = db.Container()
container.extend([
rt,
rt_record_with_parent, # 1005, dependency
record_without_dependencies,
property_which_shall_be_deleted, # 1007, dependency
record_referenced, # 2002, dependency
record_with_dependencies,
record_with_parent,
record_with_property_which_is_not_a_record
])
assert (db.Container()._test_dependencies_in_container(container)
== {2002, 1005, 1007})
def test_container_dependencies_for_deletion_with_lists():
not_included_rt = 1000
record_referenced = db.Record().add_parent(not_included_rt)
record_referenced.id = 2001
record_with_list = db.Record().add_parent(not_included_rt)
record_with_list.id = 2002
record_with_list.add_property(not_included_rt, datatype=db.LIST(
not_included_rt), value=[record_referenced, 2003, 2004, 2005, 2006])
container = db.Container()
container.extend([record_with_list, record_referenced])
assert db.Container()._test_dependencies_in_container(container) == {2001}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment