Pydantic ORM — overview¶
cypher_validator ships a Pydantic-based graph ORM that lets you declare your schema as
Python classes and then build, validate, and execute Cypher queries against Neo4j
type-safely. The ORM layer is pure Python — it sits on top of the Rust validator and
generator, not under them.
The design goals:
- Familiar — looks like SQLAlchemy / Django ORM if you squint.
- Schema-driven — your declared models are the schema, and they feed straight into
the Rust
CypherValidator. - Parameterised by default — every value path emits
$paramplaceholders. - Agent-friendly — every query exposes
(cypher, params)so an LLM can inspect or modify it before execution.
How the pieces fit together¶
Pydantic NodeModel / RelationshipModel
│
▼
┌───────────────────────────────────────────┐
│ GraphSchema │
│ (collects models, computes shape) │
└───────────────────────────────────────────┘
│ │
to_dict() │ │ to_cypher_schema()
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ AgentTools spec │ │ Rust Schema │
│ (OpenAI / Anth) │ │ (HashSet props) │
└─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ CypherValidator │ ← validates every
│ (Rust core) │ generated query
└─────────────────┘
│
┌──────────────────────────────┬────────────────┴──────────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐
│ Query │ │ Repository │ │ GraphSession │
│ (builder) │ │ (CRUD per model)│ │ (execute, hydrate) │
└──────────────┘ └─────────────────┘ └────────────────────┘
│ build() │
▼ ▼
(cypher, params) → db.execute → records → Model.from_records(...) → Pydantic instances
Component cheat-sheet¶
| Component | Purpose |
|---|---|
NodeModel |
Base class for node types. Subclass and add fields. |
RelationshipModel |
Base class for relationships. Set __source__, __target__, __rel_type__. |
Query |
Fluent Cypher builder — .match().where().return_().build(). |
Cond |
Single condition Cond(left, op, right). Inlines scalar literals. |
CondGroup |
Group of conditions joined by AND / OR. |
RawExpr |
Escape hatch — inject raw Cypher text. |
GraphSchema |
Bridge from Pydantic models to the Rust Schema. |
AgentTools |
OpenAI / Anthropic function-call tool specs for an LLM agent. |
ExtendedAgentTools |
Adds search_nodes, find_neighbors, find_path, etc. |
QueryPlan |
Multi-step plan with dependencies; topologically sorted into waves. |
QueryStep |
One step in a QueryPlan. |
QueryResult |
Structured wrapper around execution output. |
Traversal |
Pre-built patterns: neighbors, shortest_path, degree, path_exists, … |
BulkOps |
UNWIND-based bulk create / merge / delete. |
SchemaDDL |
Auto-generate CREATE CONSTRAINT / CREATE INDEX. |
GraphSession |
Sync session over a Neo4jDatabase. Hydrates results into models. |
AsyncGraphSession |
Async sibling — async with, await session.query(...). |
PropExpr |
Typed property reference: p.name == "Alice" → Cond. |
NodeRef |
Typed variable reference for nodes. |
RelRef |
Typed variable reference for relationships. |
QueryHistory |
LRU history for agent conversation context. |
SchemaDiff |
Compare two schemas; emit migration DDL. |
CypherFn / fn |
Type-safe wrappers for built-in functions (count, avg, coalesce, …). |
PathBuilder |
Multi-hop path construction: PathBuilder(Person).rel(ActedIn).to(Movie). |
Repository |
Typed CRUD over a single model. |
schema_to_pipeline_kwargs |
Hand off a GraphSchema to LLMNLToCypher. |
A complete example¶
from cypher_validator import (
NodeModel, RelationshipModel,
Query, Cond, GraphSchema,
CypherValidator, GraphSession, Neo4jDatabase,
Repository,
)
# 1) Declare schema as Python classes
class Person(NodeModel):
__label__ = "Person"
name: str
age: int = 0
class Movie(NodeModel):
__label__ = "Movie"
title: str
year: int
class ActedIn(RelationshipModel):
__source__ = Person
__target__ = Movie
__rel_type__ = "ACTED_IN"
roles: list[str] = []
# 2) Bridge to the Rust validator
schema = GraphSchema.from_models([Person, Movie, ActedIn])
validator = CypherValidator(schema.to_cypher_schema())
# 3) Build a query
q = (Query()
.match(Person, "p")
.where(Cond("p.age", ">=", 18)) # → p.age >= 18 (literal inlined)
.return_("p.name AS name", "p.age AS age"))
cypher, params = q.build()
result = q.validate(schema) # validates against the Rust validator
assert result.is_valid
# 4) Execute via a GraphSession
db = Neo4jDatabase("bolt://localhost:7687", "neo4j", "password")
with GraphSession(db, schema) as session:
rows = session.execute(cypher, params)
# 5) Or use a per-model Repository for CRUD
repo = Repository(Person, db)
alice = repo.find_one(name="Alice")
all_people = repo.find_all(limit=100)
Integration with the LLM pipeline¶
schema_to_pipeline_kwargs(graph_schema) converts a GraphSchema into the kwargs
LLMNLToCypher expects:
from cypher_validator import LLMNLToCypher, schema_to_pipeline_kwargs
pipe = LLMNLToCypher.from_openai(
model="gpt-4o",
api_key="...",
**schema_to_pipeline_kwargs(graph_schema),
)
cypher = pipe("List actors over 60 who appeared in films from the 90s.", mode="match")
This is the recommended way to share a single source-of-truth schema between your Pydantic models, the validator, and the LLM pipeline.
Where to go from here¶
- Define your schema → Models
- Build queries → Query builder
- Per-model CRUD → Repository
- Execute against Neo4j → Sessions
- Batch operations → Bulk ops
- Graph patterns → Traversal
- Constraints & indexes → DDL & migrations
- Build an LLM agent → Agent tools
- Gotchas → API caveats