Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions store/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#


import uuid
import atexit

from sqlitedict import SqliteDict
Expand Down Expand Up @@ -54,6 +55,13 @@ def __init__(
self.file_name, tablename=attribute_function_space, autocommit=True
)

# dependency registry (persistent)
self._registry_key = "__dependency_registry__"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of this string?


if self._registry_key not in self.sqlite_dict:
self.sqlite_dict[self._registry_key] = {}
self.sqlite_dict.commit()

# register to be called at exit:
atexit.register(self.close)

Expand All @@ -76,27 +84,72 @@ def register(self, af: AttributeFunction):
(i.e. persisted).
@param af: The AttributeFunction instance to register.
"""
uuid_str: str = str(af.uuid)

self.sqlite_dict[af.uuid] = af
self.sqlite_dict[uuid_str] = af
self.sqlite_dict.commit()
self.attribute_function_buffer[af.uuid] = af

def load(self, afid: int) -> None:
"""Load an afid from the persistent store into the buffer.
@param afid: The ID of the item to load.
"""

try:
af: AttributeFunction = self.sqlite_dict[afid]
if self.add_reference_to_store_on_read:
af.__dict__["store"] = self

self.attribute_function_buffer[afid] = af
except KeyError as e:
raise KeyError(f"ID '{afid}' not found in the store.") from e

def _get_registry(self) -> dict[str, list[str]]:
return self.sqlite_dict.get(self._registry_key, {})

def register_dependency(self, parent_uuid: uuid.UUID, child_uuid: uuid.UUID):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the idea here is to have a separte dependency registry, i.e. all subscriptions are additionally subscribed here, correct?
Is this supposed to be redundant to the data keptin the AFs? Why not store it with the AFs directly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the idea is to maintain a separate persistent dependency registry that mirrors the subscription relationships.

The reason for not storing dependencies directly within the AttributeFunctions is to keep the dependency mechanism at the Store level and independent of the in-memory state of AFs. This ensures that:

Dependencies persist even when AFs are not loaded in memory
Notifications can be triggered correctly after reloading the store
The internal structure of AttributeFunction does not need to be modified for persistence concerns

So the registry is not meant to be redundant, but rather a persistence layer that enables subscriptions to work transparently across sessions.

"""
Register a persistent dependency between two AttributeFunctions.

@param parent_uuid: The UUID of the AF being observed.
@param child_uuid: The UUID of the AF that depends on the parent.
"""
registry: dict[str, list[str]] = self._get_registry()

p_uuid_str: str = str(parent_uuid)
c_uuid_str: str = str(child_uuid)

if p_uuid_str not in registry:
registry[p_uuid_str] = []

if c_uuid_str not in registry[p_uuid_str]:
registry[p_uuid_str].append(c_uuid_str)

self.sqlite_dict[self._registry_key] = registry
self.sqlite_dict.commit()

def _notify(self, parent_uuid: uuid.UUID):
registry = self._get_registry()
p_uuid_str = str(parent_uuid)

if p_uuid_str not in registry:
return

parent_af: AttributeFunction = self.get(parent_uuid)

dependent_id: str
for dependent_id in registry[p_uuid_str]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typehint missing

try:
dependent_af: AttributeFunction = self.get(dependent_id)
if dependent_af and hasattr(dependent_af, "update"):
dependent_af.update(other=parent_af)
dependent_af.was_updated_in_test = True
self.put(dependent_af)
except KeyError:
continue

def __len__(self) -> int:
"""Return the number of items in the store.
@return: The number of items in the store.
"""
return len(self.sqlite_dict)
size = len(self.sqlite_dict)

if self._registry_key in self.sqlite_dict:
size -= 1

return size


57 changes: 57 additions & 0 deletions tests/store/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,28 @@
#

import pickle
import uuid

from fdm.API import AttributeFunction, AttributeFunctionSentinel
from fdm.attribute_functions import TF
from fql.util import Item
from store.store import Store

WAS_UPDATED = False

def global_update_mock(self, other=None, *args, **kwargs):
"""
Picklable global mock that accepts the 'other' argument.
This function is required because the Store triggers update() internally
when notifying dependent AttributeFunctions. The test itself cannot directly
observe this call, so this mock sets the global flag WAS_UPDATED to True
when invoked.

The function is defined at module level to ensure it is picklable, as
AttributeFunctions may be serialized when stored.
"""
global WAS_UPDATED
WAS_UPDATED = True

def test_pickle_Item(tmp_path):

Expand Down Expand Up @@ -215,3 +231,44 @@ def test_store_get_put_with_sentinel_replacement(tmp_path):
assert type(outer_tuple.observers[0]) == TF

store_read.close()

def test_store_dependency_notification(tmp_path):
"""
Test that updates propagate through the Store via subscriptions,
using the persistent dependency mechanism transparently.
This test covers:
1. Creating AttributeFunctions (AFs) with dependencies
2. Verifying that updating a parent AF triggers the child's update method
3. Ensuring that update propagation works across store persistence
"""
global WAS_UPDATED
WAS_UPDATED = False

TF.update = global_update_mock

file_name = str(tmp_path / "test_dependency.sqlite")
store = Store(file_name=file_name)

parent_af = TF({"value": 1}, store=store)
child_af = TF({"value": 2}, store=store)

store.register_dependency(parent_af.uuid, child_af.uuid)

store.put(child_af)
store.put(parent_af)

assert WAS_UPDATED is True

# verify registry persisted correctly
registry = store._get_registry()
parent_uuid_str = str(parent_af.uuid)
assert parent_uuid_str in registry
assert str(child_af.uuid) in registry[parent_uuid_str]

store.close()
store = Store(file_name=file_name)

# Check that the data is STILL there after re-opening
new_registry = store._get_registry()
assert str(parent_af.uuid) in new_registry