Skip to content

exists() and save() raise TypeError when an existence-query property holds an unresolved link (KGProxy) #145

Description

@apdavison

Summary

For an object whose id is missing or is not a full URI, exists() raises TypeError if one of the class's existence_query_properties holds a KGProxy. save() calls exists(), so saving the object fails too, and so does Collection.upload():

from fairgraph.kgproxy import KGProxy
import fairgraph.openminds.core as omcore

funding = omcore.Funding(
    funder=KGProxy(omcore.Organization, "https://kg.ebrains.eu/api/instances/2cd6bfcd-6e3b-4b53-8b18-5a1eb6dc6f64")
)
funding.exists(client)
# TypeError: Filter specifications should be a single-level dict, without nesting
funding.save(client, space="myspace")
# TypeError: Filter specifications should be a single-level dict, without nesting

The same link given as a resolved object works:

funding = omcore.Funding(funder=omcore.Organization(id="https://kg.ebrains.eu/api/instances/2cd6bfcd-...", name="Acme"))
funding._build_existence_query()
# {'funder': 'https://kg.ebrains.eu/api/instances/2cd6bfcd-...'}
funding.exists(client)  # runs the query as expected

Cause

_build_existence_query() (fairgraph/node.py:465) only takes the id of a linked value when the value is a KGNode (node.py:485). KGProxy is not a KGNode (its bases are Releasable, Resolvable and openMINDS Link), so it falls through to the final branch, where value_to_jsonld() (node.py:496) serializes it as a nested dict:

{'funder': {'@id': 'https://kg.ebrains.eu/api/instances/2cd6bfcd-...'}}

exists() passes this to generate_minimal_query(), and expand_filter() rejects the nested dict (fairgraph/utility.py:280). The TypeError isn't caught in exists() (only CannotBuildExistenceQuery is), so it propagates to save() and Collection.upload().

When it happens

A KGProxy ends up in an existence-query property of an object whose id is missing or is not a full URI in several common situations:

  • Reusing a link from a fetched object. Links on objects retrieved with from_id(), from_uri() or list() are KGProxy objects unless they were followed with follow_links. For example, building a new Comment whose about and commenter are taken from an existing Comment fails.
  • Creating a proxy directly, to link to an existing KG instance without fetching it.
  • Collection.load() followed by Collection.upload(). When a JSON-LD node links to an instance that is already in the KG (not in the collection), deserialization turns the link into a KGProxy (node.py:393). If the node's own @id is not a full URI (e.g. a blank-node id such as _:funding1), upload() then saves the node and raises. Links between nodes within the collection are resolved to nodes by Collection._resolve_links() in openMINDS-Python (openminds/collection.py) and are not affected.

Objects whose id is a full URI (starting with http) are not affected, because exists() then looks them up by id and never builds an existence query (kgobject.py:588). Objects with any other id, such as a blank-node id, do build one.

There is also a silent side effect when such an object is created without error (e.g. one with a full-URI id that is not yet in the KG): after creating the instance, save() builds the existence query again to populate the save cache (kgobject.py:926). Here the nested dict does not raise, because generate_cache_key() converts dict values to strings, so the cache key is based on str({'@id': ...}) instead of the id itself.

A multi-valued link property that holds a list (of proxies or of objects) is not affected in the same way: _build_existence_query() raises CannotBuildExistenceQuery for any list, exists() returns False, and a new instance is created. That is a separate limitation.

Affected classes

Classes with a linked-object property in existence_query_properties, where the property holds a single KGProxy:

Class Link properties in existence query v4 v5
Accessibility channel, eligibility, form, process
AccountInformation service
AnatomicalAtlas digital_identifier
BrainAtlas digital_identifier
Chapter is_part_of
ChemicalMixture type
Comment about, commenter
CommonCoordinateSpaceVersion accessibility, anatomical_axes_orientation, full_documentation, native_unit
Electrode, ElectrodeArray, MRIScanner, Pipette, SlicingDevice type
MRICoil mounting_type, type
HardwareProduct type
Environment hardware
FileArchive format
FileBundle is_part_of
Funding funder
GridImage, GridImageSequence, GridImageStack, GridVolume, GridVolumeSequence data_location
LivePaperResourceItem, LivePaperSection is_part_of
Organization country_of_formation, type
ProductSource provider
PublicationIssue, PublicationVolume is_part_of
Recording data_location, recorded_with
RegularTimeSeries data_location
ServiceDeployment service
ServiceLink data_location (v4 and v5), service (v4)
SoftwareAgent software
Strain genetic_strain_type, species
UsageAgreement jurisdiction, template

(Multi-valued link properties such as contributions, has_parts or stages are omitted; see above.)

Why no test catches it

test_build_existence_query in test/test_base.py uses plain values only, and no test calls exists() or save() on an object without an id that holds a KGProxy in an existence-query property.

Suggested fix

In _build_existence_query(), handle a KGProxy like a KGNode that has an id:

from .kgproxy import KGProxy  # already imported in node.py

...
if isinstance(value, KGProxy):
    query[query_property_name] = value.id
elif isinstance(value, KGNode):
    ...

Checked against the mock client by substituting a KGNode with the same id: the generated filter is correct both for a proxy with a full URI (a CONTAINS filter on the link's @id) and for one with a bare UUID (a CONTAINS filter on http://schema.org/identifier).

Tests:

  • _build_existence_query() returns the proxy's id for a KGProxy value.
  • exists() on a new object with a KGProxy in an existence-query property (e.g. Funding) runs the existence query, against the mock client with the query stubbed, instead of raising.
  • Collection.upload() of a JSON-LD node that links to an existing KG instance through an existence-query property.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions