01.10.2025 • 2 min read

Vellum Part 1: Typing MongoDB at the Speed of Thought

The Problem

Traditional MongoDB queries rely on string-based field names, creating maintainability challenges. Strings. Everywhere. No autocomplete. No refactoring support. A single typo like "pirce" instead of "price" only surfaces at runtime.

# The old way — fragile
await collection.find({"pirce": {"$lt": 10}})  # typo only caught at runtime

The Vision

The goal was simple Pythonic query syntax:

products = await repo.find(Product.category == "electronics", Product.price < 10)

No operator strings. No dictionaries. Python operators directly on model fields — with full IDE autocomplete and compile-time validation.


Implementation

The FieldRef Class

Overloading comparison operators (<, >, ==, etc.) on a FieldRef class returns query expression objects rather than booleans. Product.price < 10 generates {"price": {"$lt": 10}} — not False.

Metaclass Integration

A custom metaclass intercepts attribute access on the model class. When you access Product.price, the metaclass checks if price exists in the model fields and returns a FieldRef instance instead of the raw value.

Expression Composition

Logical operators combine expressions naturally:

# AND
(Product.price < 10) & (Product.category == "electronics")

# OR
(Product.category == "food") | (Product.category == "drink")

# NOT
~(Product.status == "archived")

Extended Operators

Beyond comparisons, method calls cover MongoDB-specific operations:

Product.tags.in_(["mobile", "backend"])
Product.name.regex("^go", "i")
Product.metadata.exists()
Product.reviews.elem_match(Review.rating > 4)

Aggregation Pipeline Support

A resolve_agg_refs() function recursively converts FieldRef objects to MongoDB field references (e.g., "$total"), enabling type-safe aggregation pipeline construction without writing "$fieldname" strings manually.


Key Benefits

  • Compile-time field validation — typos caught before the code runs
  • IDE autocomplete — full intellisense on model fields
  • Safe refactoring — rename a field, the query breaks visibly
  • Composable logic — build queries incrementally as Python objects
  • Gradual adoption — works alongside existing raw dictionary queries

Next: Part 2 — Architecture Decisions That Almost Broke Us