Skip to content

Commit

Permalink
Make entity and value metadata use class_name as unique key
Browse files Browse the repository at this point in the history
class_name is required to uniquely identify entities and parameter values.

Re #318,#328
  • Loading branch information
soininen committed Jan 5, 2024
1 parent 3f785a0 commit 4f56747
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 23 deletions.
7 changes: 2 additions & 5 deletions spinedb_api/db_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,8 @@ def __init__(
self._filter_configs = filter_configs if apply_filters else None
try:
self.sa_url = make_url(db_url)
except ArgumentError as err:
raise SpineDBAPIError(
f"Could not parse the given URL. "
f"Please check that it is valid."
)
except ArgumentError:
raise SpineDBAPIError("Could not parse the given URL. Please check that it is valid.")
self.username = username if username else "anon"
self.codename = self._make_codename(codename)
self._memory = memory
Expand Down
4 changes: 2 additions & 2 deletions spinedb_api/import_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ def _get_metadata_for_import(db_map, data):


def _get_entity_metadata_for_import(db_map, data):
key = ("entity_class_name", "entity_byname", "metadata_name", "metadata_value")
key = ("class_name", "entity_byname", "metadata_name", "metadata_value")
for class_name, entity_byname, metadata in data:
if isinstance(entity_byname, str):
entity_byname = (entity_byname,)
Expand All @@ -652,7 +652,7 @@ def _get_entity_metadata_for_import(db_map, data):

def _get_parameter_value_metadata_for_import(db_map, data):
key = (
"entity_class_name",
"class_name",
"entity_byname",
"parameter_definition_name",
"metadata_name",
Expand Down
47 changes: 32 additions & 15 deletions spinedb_api/mapped_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def item_factory(item_type):
}.get(item_type, MappedItemBase)


_ENTITY_BYNAME_VALUE = 'A tuple with the entity name as single element if the entity is zero-dimensional, or the element names if the entity is multi-dimensional.'


class CommitItem(MappedItemBase):
fields = {
'comment': {'type': str, 'value': 'A comment describing the commit.'},
Expand Down Expand Up @@ -282,8 +285,7 @@ class EntityAlternativeItem(MappedItemBase):
'entity_class_name': {'type': str, 'value': 'The entity class name.'},
'entity_byname': {
'type': tuple,
'value': 'A tuple with the entity name as single element if the entity is zero-dimensional, '
'or the element names if it is multi-dimensional.',
'value': _ENTITY_BYNAME_VALUE,
},
'alternative_name': {'type': str, 'value': 'The alternative name.'},
'active': {
Expand Down Expand Up @@ -504,8 +506,7 @@ class ParameterValueItem(ParameterItemBase):
'parameter_definition_name': {'type': str, 'value': 'The parameter name.'},
'entity_byname': {
'type': tuple,
'value': 'A tuple with the entity name as single element if the entity is zero-dimensional, '
'or the element names if the entity is multi-dimensional.',
'value': _ENTITY_BYNAME_VALUE,
},
'value': {'type': bytes, 'value': 'The value.'},
'type': {'type': str, 'value': 'The value type.', 'optional': True},
Expand Down Expand Up @@ -676,60 +677,76 @@ class MetadataItem(MappedItemBase):

class EntityMetadataItem(MappedItemBase):
fields = {
'entity_name': {'type': str, 'value': 'The entity name.'},
'class_name': {'type': str, 'value': 'The entity class name.'},
'entity_byname': {'type': tuple, 'value': _ENTITY_BYNAME_VALUE},
'metadata_name': {'type': str, 'value': 'The metadata entry name.'},
'metadata_value': {'type': str, 'value': 'The metadata entry value.'},
}
_unique_keys = (("entity_name", "metadata_name", "metadata_value"),)
_references = {"entity_id": ("entity", "id"), "metadata_id": ("metadata", "id")}
_unique_keys = (("class_name", "entity_byname", "metadata_name", "metadata_value"),)
_references = {
"entity_id": ("entity", "id"),
"metadata_id": ("metadata", "id"),
}
_external_fields = {
"entity_name": ("entity_id", "name"),
"class_name": ("entity_id", "class_name"),
"entity_byname": ("entity_id", "byname"),
"metadata_name": ("metadata_id", "name"),
"metadata_value": ("metadata_id", "value"),
}
_alt_references = {
("entity_class_name", "entity_byname"): ("entity", ("class_name", "byname")),
(
"class_name",
"entity_byname",
): ("entity", ("class_name", "byname")),
("metadata_name", "metadata_value"): ("metadata", ("name", "value")),
}
_internal_fields = {
"entity_id": (("entity_class_name", "entity_byname"), "id"),
"entity_id": (("class_name", "entity_byname"), "id"),
"metadata_id": (("metadata_name", "metadata_value"), "id"),
}


class ParameterValueMetadataItem(MappedItemBase):
fields = {
'class_name': {'type': str, 'value': 'The entity class name.'},
'parameter_definition_name': {'type': str, 'value': 'The parameter name.'},
'entity_byname': {
'type': tuple,
'value': 'A tuple with the entity name as single element if the entity is zero-dimensional, '
'or the element names if it is multi-dimensional.',
'value': _ENTITY_BYNAME_VALUE,
},
'alternative_name': {'type': str, 'value': 'The alternative name.'},
'metadata_name': {'type': str, 'value': 'The metadata entry name.'},
'metadata_value': {'type': str, 'value': 'The metadata entry value.'},
}
_unique_keys = (
("parameter_definition_name", "entity_byname", "alternative_name", "metadata_name", "metadata_value"),
(
"class_name",
"parameter_definition_name",
"entity_byname",
"alternative_name",
"metadata_name",
"metadata_value",
),
)
_references = {"parameter_value_id": ("parameter_value", "id"), "metadata_id": ("metadata", "id")}
_external_fields = {
"class_name": ("parameter_value_id", "entity_class_name"),
"parameter_definition_name": ("parameter_value_id", "parameter_definition_name"),
"entity_byname": ("parameter_value_id", "entity_byname"),
"alternative_name": ("parameter_value_id", "alternative_name"),
"metadata_name": ("metadata_id", "name"),
"metadata_value": ("metadata_id", "value"),
}
_alt_references = {
("entity_class_name", "parameter_definition_name", "entity_byname", "alternative_name"): (
("class_name", "parameter_definition_name", "entity_byname", "alternative_name"): (
"parameter_value",
("entity_class_name", "parameter_definition_name", "entity_byname", "alternative_name"),
),
("metadata_name", "metadata_value"): ("metadata", ("name", "value")),
}
_internal_fields = {
"parameter_value_id": (
("entity_class_name", "parameter_definition_name", "entity_byname", "alternative_name"),
("class_name", "parameter_definition_name", "entity_byname", "alternative_name"),
"id",
),
"metadata_id": (("metadata_name", "metadata_value"), "id"),
Expand Down
173 changes: 172 additions & 1 deletion tests/test_DatabaseMapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,169 @@ def test_updating_entity_name_updates_the_name_in_parameter_value_too(self):
)
self.assertIsNotNone(color)

def test_update_entity_metadata_by_changing_its_entity(self):
with DatabaseMapping("sqlite://", create=True) as db_map:
entity_class, _ = db_map.add_entity_class_item(name="my_class")
db_map.add_entity_item(name="entity_1", class_name="my_class")
entity_2, _ = db_map.add_entity_item(name="entity_2", class_name="my_class")
metadata_value = '{"sources": [], "contributors": []}'
metadata, _ = db_map.add_metadata_item(name="my_metadata", value=metadata_value)
entity_metadata, error = db_map.add_entity_metadata_item(
metadata_name="my_metadata",
metadata_value=metadata_value,
class_name="my_class",
entity_byname=("entity_1",),
)
self.assertIsNone(error)
entity_metadata.update(entity_byname=("entity_2",))
self.assertEqual(
entity_metadata._extended(),
{
"class_name": "my_class",
"entity_byname": ("entity_2",),
"entity_id": entity_2["id"],
"id": entity_metadata["id"],
"metadata_id": metadata["id"],
"metadata_name": "my_metadata",
"metadata_value": metadata_value,
},
)
db_map.commit_session("Add initial data.")
entity_sq = (
db_map.query(
db_map.entity_sq.c.id.label("entity_id"),
db_map.entity_class_sq.c.name.label("class_name"),
db_map.entity_sq.c.name.label("entity_name"),
)
.join(db_map.entity_class_sq, db_map.entity_class_sq.c.id == db_map.entity_sq.c.class_id)
.subquery()
)
metadata_records = (
db_map.query(
db_map.entity_metadata_sq.c.id,
entity_sq.c.class_name,
entity_sq.c.entity_name,
db_map.metadata_sq.c.name.label("metadata_name"),
db_map.metadata_sq.c.value.label("metadata_value"),
)
.join(entity_sq, entity_sq.c.entity_id == db_map.entity_metadata_sq.c.entity_id)
.join(db_map.metadata_sq, db_map.metadata_sq.c.id == db_map.entity_metadata_sq.c.metadata_id)
.all()
)
self.assertEqual(len(metadata_records), 1)
self.assertEqual(
dict(**metadata_records[0]),
{
"id": 1,
"class_name": "my_class",
"entity_name": "entity_2",
"metadata_name": "my_metadata",
"metadata_value": metadata_value,
},
)

def test_update_parameter_value_metadata_by_changing_its_parameter(self):
with DatabaseMapping("sqlite://", create=True) as db_map:
entity_class, _ = db_map.add_entity_class_item(name="my_class")
_, error = db_map.add_parameter_definition_item(name="x", entity_class_name="my_class")
self.assertIsNone(error)
db_map.add_parameter_definition_item(name="y", entity_class_name="my_class")
entity, _ = db_map.add_entity_item(name="my_entity", class_name="my_class")
value, value_type = to_database(2.3)
_, error = db_map.add_parameter_value_item(
entity_class_name="my_class",
entity_byname=("my_entity",),
parameter_definition_name="x",
alternative_name="Base",
value=value,
type=value_type,
)
self.assertIsNone(error)
value, value_type = to_database(-2.3)
y, error = db_map.add_parameter_value_item(
entity_class_name="my_class",
entity_byname=("my_entity",),
parameter_definition_name="y",
alternative_name="Base",
value=value,
type=value_type,
)
self.assertIsNone(error)
metadata_value = '{"sources": [], "contributors": []}'
metadata, error = db_map.add_metadata_item(name="my_metadata", value=metadata_value)
self.assertIsNone(error)
value_metadata, error = db_map.add_parameter_value_metadata_item(
metadata_name="my_metadata",
metadata_value=metadata_value,
class_name="my_class",
entity_byname=("my_entity",),
parameter_definition_name="x",
alternative_name="Base",
)
self.assertIsNone(error)
value_metadata.update(parameter_definition_name="y")
self.assertEqual(
value_metadata._extended(),
{
"class_name": "my_class",
"entity_byname": ("my_entity",),
"alternative_name": "Base",
"parameter_definition_name": "y",
"parameter_value_id": y["id"],
"id": value_metadata["id"],
"metadata_id": metadata["id"],
"metadata_name": "my_metadata",
"metadata_value": metadata_value,
},
)
db_map.commit_session("Add initial data.")
parameter_sq = (
db_map.query(
db_map.parameter_value_sq.c.id.label("value_id"),
db_map.entity_class_sq.c.name.label("class_name"),
db_map.entity_sq.c.name.label("entity_name"),
db_map.parameter_definition_sq.c.name.label("parameter_definition_name"),
db_map.alternative_sq.c.name.label("alternative_name"),
)
.join(
db_map.entity_class_sq, db_map.entity_class_sq.c.id == db_map.parameter_value_sq.c.entity_class_id
)
.join(db_map.entity_sq, db_map.entity_sq.c.id == db_map.parameter_value_sq.c.entity_id)
.join(
db_map.parameter_definition_sq,
db_map.parameter_definition_sq.c.id == db_map.parameter_value_sq.c.parameter_definition_id,
)
.join(db_map.alternative_sq, db_map.alternative_sq.c.id == db_map.parameter_value_sq.c.alternative_id)
.subquery("parameter_sq")
)
metadata_records = (
db_map.query(
db_map.parameter_value_metadata_sq.c.id,
parameter_sq.c.class_name,
parameter_sq.c.entity_name,
parameter_sq.c.parameter_definition_name,
parameter_sq.c.alternative_name,
db_map.metadata_sq.c.name.label("metadata_name"),
db_map.metadata_sq.c.value.label("metadata_value"),
)
.join(parameter_sq, parameter_sq.c.value_id == db_map.parameter_value_metadata_sq.c.parameter_value_id)
.join(db_map.metadata_sq, db_map.metadata_sq.c.id == db_map.parameter_value_metadata_sq.c.metadata_id)
.all()
)
self.assertEqual(len(metadata_records), 1)
self.assertEqual(
dict(**metadata_records[0]),
{
"id": 1,
"class_name": "my_class",
"entity_name": "my_entity",
"parameter_definition_name": "y",
"alternative_name": "Base",
"metadata_name": "my_metadata",
"metadata_value": metadata_value,
},
)

def test_fetch_more(self):
with DatabaseMapping("sqlite://", create=True) as db_map:
alternatives = db_map.fetch_more("alternative")
Expand Down Expand Up @@ -2092,6 +2255,10 @@ def setUp(self):
def tearDown(self):
self._db_map.close()

def _assert_import(self, result):
error = result[1]
self.assertEqual(error, [])

def test_remove_object_class(self):
"""Test adding and removing an object class and committing"""
items, _ = self._db_map.add_object_classes({"name": "oc1", "id": 1}, {"name": "oc2", "id": 2})
Expand Down Expand Up @@ -2332,7 +2499,11 @@ def test_cascade_remove_entity_metadata_leaves_metadata_used_by_value_intact(sel
self._db_map, (("my_class", "my_object", "my_parameter", 99.0),)
)
import_functions.import_metadata(self._db_map, ('{"title": "My metadata."}',))
import_functions.import_object_metadata(self._db_map, (("my_class", "my_object", '{"title": "My metadata."}'),))
self._assert_import(
import_functions.import_object_metadata(
self._db_map, (("my_class", "my_object", '{"title": "My metadata."}'),)
)
)
import_functions.import_object_parameter_value_metadata(
self._db_map, (("my_class", "my_object", "my_parameter", '{"title": "My metadata."}'),)
)
Expand Down

0 comments on commit 4f56747

Please sign in to comment.