Create-only Cypher for a bulk load, and the counts that verify it.
In a bulk load every row is new, so nodes and relationships are created with
no MERGE: nothing is looked for but a relationship's two endpoints. Rows
are $rows: node property maps, or [source keys, target keys, properties].
A null property is not stored, as an upsert's += removes it.
Attributes
Run = Callable[[str], list[dict[str, Any]]]
module-attribute
Classes
Functions:
bulk_counts(run)
Nodes per label and relationships per (source label, type, target label).
A relationship type with one source and one target label is counted from
per-label counts, which Neo4j answers from its count store; a type with
several is counted by a scan of that type alone.
Source code in graflo/db/cypher/bulk.py
| def bulk_counts(run: Run) -> BulkCounts:
"""Nodes per label and relationships per (source label, type, target label).
A relationship type with one source and one target label is counted from
per-label counts, which Neo4j answers from its count store; a type with
several is counted by a scan of that type alone.
"""
labels = [r["label"] for r in run("CALL db.labels() YIELD label RETURN label")]
vertices = {
label: int(_single(run(f"MATCH (n:{cypher_name(label)}) RETURN count(n) AS c")))
for label in labels
}
types = [
r["relationshipType"]
for r in run(
"CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType"
)
]
edges: dict[StoredEdgeId, int] = {}
for rel in types:
r = cypher_name(rel)
sources = {
label: int(
_single(
run(
f"MATCH (:{cypher_name(label)})-[r:{r}]->() RETURN count(r) AS c"
)
)
)
for label in labels
}
targets = {
label: int(
_single(
run(
f"MATCH ()-[r:{r}]->(:{cypher_name(label)}) RETURN count(r) AS c"
)
)
)
for label in labels
}
sources = {k: v for k, v in sources.items() if v}
targets = {k: v for k, v in targets.items() if v}
if len(sources) == 1 and len(targets) == 1:
((source, count),) = sources.items()
(target,) = targets
edges[(source, rel, target)] = count
continue
for row in run(
f"MATCH (s)-[r:{r}]->(t) "
"RETURN head(labels(s)) AS s, head(labels(t)) AS t, count(r) AS c"
):
edges[(row["s"], rel, row["t"])] = int(row["c"])
return BulkCounts(vertices={k: v for k, v in vertices.items() if v}, edges=edges)
|
create_nodes_query(label)
Create one label node per row.
Source code in graflo/db/cypher/bulk.py
| def create_nodes_query(label: str) -> str:
"""Create one ``label`` node per row."""
return f"UNWIND $rows AS row CREATE (n:{cypher_name(label)}) SET n = row"
|
create_relationships_query(source_label, target_label, relation, match_keys_source, match_keys_target)
Create one relationship per row between endpoints found by their keys.
A row whose endpoint is missing creates nothing; the load's count check
reports it.
Source code in graflo/db/cypher/bulk.py
| def create_relationships_query(
source_label: str,
target_label: str,
relation: str,
match_keys_source: Sequence[str],
match_keys_target: Sequence[str],
) -> str:
"""Create one relationship per row between endpoints found by their keys.
A row whose endpoint is missing creates nothing; the load's count check
reports it.
"""
return (
"UNWIND $rows AS row "
f"MATCH (source:{cypher_name(source_label)} "
f"{{{_key_map(match_keys_source, 0)}}}) "
f"MATCH (target:{cypher_name(target_label)} "
f"{{{_key_map(match_keys_target, 1)}}}) "
f"CREATE (source)-[r:{cypher_name(relation)}]->(target) SET r = row[2]"
)
|
scanned_counts(run)
Nodes per label and relationships per (source label, type, target label), by a scan.
For a server without db.labels() or a count store; one pass over the
nodes and one over the relationships.
Source code in graflo/db/cypher/bulk.py
| def scanned_counts(run: Run) -> BulkCounts:
"""Nodes per label and relationships per (source label, type, target label), by a scan.
For a server without ``db.labels()`` or a count store; one pass over the
nodes and one over the relationships.
"""
vertices = {
row["label"]: int(row["c"])
for row in run("MATCH (n) RETURN head(labels(n)) AS label, count(n) AS c")
}
edges: dict[StoredEdgeId, int] = {
(row["s"], row["r"], row["t"]): int(row["c"])
for row in run(
"MATCH (s)-[r]->(t) RETURN head(labels(s)) AS s, type(r) AS r, "
"head(labels(t)) AS t, count(r) AS c"
)
}
return BulkCounts(vertices=vertices, edges=edges)
|