MDF Enforced Validator and Loader

Logo

Documentation for MEVAL.

Loader Usage

Loader (meval/loader.py) handles ingestion of TSV data files into a Neo4j/Memgraph graph database, using MDF model metadata (via ModelParser) to interpret node types, property types, and relationships. It reads files in chunks, upserts nodes and relationships, manages indexes, and provides utilities for graph maintenance such as deleting subgraphs and finding orphan nodes.

Contents

Instantiation

from neo4j import GraphDatabase
from meval.loader import Loader

driver = GraphDatabase.driver(uri, auth=(user, password))
loader = Loader(driver)

# close the underlying driver when the loading is done
loader.close()

Loading nodes and relationships

upsert_file_records

Upserts all node records from a TSV file into the database, in chunks (default chunk_size=3000). For each chunk, node properties are cleaned (whitespace trimmed, missing values dropped, list-type/number-type properties converted per the MDF model) and merged into the graph using the id_field (default "guid") as the match key. Returns a combined summary dict of Neo4j counters (e.g. nodes_created, properties_set) across all chunks.

summary = loader.upsert_file_records(
    file_path="participant.tsv",
    model_parser=model_parser, # a ModelParser instance
    subgraph_col="subgraph",   # optional column identifying the study/subgraph
    id_field="guid",
    chunk_size=3000,
)

upsert_file_relationships

Upserts relationships declared by <parent_node_type>.<parent_key_prop> columns in a TSV file. Both the source and destination nodes must already exist (i.e. run after upsert_file_records for all related files). It also tracks which guid values have already been processed (via processed_rel_dict) within a loading job so that if the same guid reappears, previously established relationships for the old row are removed and replaced with the new one.

processed_rel_dict = {}
summary, processed_rel_dict = loader.upsert_file_relationships(
    file_path="sample.tsv",
    model_parser=model_parser,
    processed_rel_dict=processed_rel_dict,
    id_field="guid",
    chunk_size=3000,
)

Rows with unmatched source/destination nodes are skipped (not upserted) and logged as missing_src/missing_dst/missing_both.

Index management

created = loader.create_index(model_parser, id_field="guid")
loader.drop_index(created)
loader.drop_all_indexes()

Deleting records from database

loader.wipe_rooted_subgraph( # This can be used to delete a study or a program
    root_node_label="study",
    root_node_prop="study_id",
    subgraph_value="phs002790",
)

loader.delete_nodes_by_prop_value(
    identifier_list=["uuid1", "uuid2"],
    property_name="guid",
)

Finding orphan / floating nodes

find_nodes_without_path_to_root(root_node_label) returns a generator of nodes that have no outgoing path to any node labeled root_node_label (e.g. study/program). These are orphan nodes disconnected from the root, or nodes whose path to the root is broken. Each yielded item includes db_internal_id, type, and properties.

for orphan in loader.find_nodes_without_path_to_root(root_node_label="study"):
    print(orphan["type"], orphan["properties"])

Traversal / graph inspection utilities

These helpers support inspecting a subgraph before performing a deletion, typically to identify all descendants of an β€œintermediate root” node (e.g. a participant) within a larger rooted subgraph (e.g. a study):

# This example returns all descendants of a data node in the graph database
descendants = loader.find_intermediate_anchored_descendants(
    root_node_label="study",
    root_node_prop="study_id",
    root_node_prop_value="phs002790",
    intermediate_root_node_label="participant",
    intermediate_root_node_prop="guid",
    intermediate_root_node_prop_value="participant-uuid",
    model_parser=model_parser,
)

Notes