The Return of E Expressions: Fluent Chaining and Structured Panics for AI Auto-Healing
In the evolution of TeaQL, we've constantly navigated the tension between developer ergonomics and idiomatic Rust. We recently brought the E:: fluent expression chain back to Rust.
This is not a simple rollback. The new design uses reference chaining instead of cloning entity graphs and emits structured diagnostics when a relation was not loaded.
The Ping-Pong of API Design
Early in our Rust port, we attempted to replicate Java's E:: expression wrappers. However, to satisfy the borrow checker without complex lifetime annotations, we ended up requiring .clone() on entire entity graphs. This was unacceptable in Rust.
Our first reaction was to swing the pendulum the other way. We introduced eval_xxx() methods and the EvalResult enum, forcing developers to use combinators like .and_then() to safely traverse graphs:
let result = user.eval_platform()
.and_then("platform", |p| p.eval_company().and_then("company", |c| c.eval_name()));
While this was memory-safe and avoided cloning, deeply nested closures made common relation traversal harder to read.
Bringing Back E:: with References
By generating lifetime-bound wrapper structs, we restored the E:: syntax without cloning the traversed entity graph.
You can now write fluent chains in multiple ways depending on your error-handling preference:
// 1. Strict evaluation (Panics with structured AI diagnostic if missing)
let name = E::user(&user).get_platform().get_company().get_name().unwrap();
// 2. Safe Optional evaluation (Returns None if logically missing or naturally Null)
if let Some(name) = E::user(&user).get_platform().get_company().get_name().eval() {
println!("Found name: {}", name);
}
// 3. Fluent fallback (Provides a default if missing or Null)
let name = E::user(&user)
.get_platform()
.get_company()
.get_name()
.or_else("Unknown Company".to_string());
Under the hood, all of these are zero-cost abstractions that simply pass references along the chain until the final terminator (unwrap, eval, or or_else) is called.
Strict Panics: Rust's Bottom Line
But what happens if you try to traverse a relation that wasn't loaded from the database? In Java, you might get a silent null or a late NullPointerException. In our previous eval_xxx iteration, you got a safe EvalResult::NotLoaded enum.
With the return of E::, we decided to embrace a core Rust philosophy: Fail fast and fail loudly.
If you access an unloaded relation, the expression evaluates to a panic. But this is not your typical panic. It is a Structured Logic Bug Panic.
Designing for AI: The Structured Panic
When building AI-native frameworks, errors shouldn't just halt execution; they should provide the exact recipe to fix the bug.
When an E:: expression encounters an unloaded relation, it triggers a highly structured diagnostic panic that looks like this:
================================================================================
☕ TeaQL Logic Bug Detected ☕
Severity: FATAL - System halted to prevent undefined business logic.
[Human Message]
You attempted to access a relation that was not loaded in the initial query.
Root Entity: User(id=42)
Attempted Path: platform.company.name
[Diagnostic Context]
original_expr_with_broken_point: E::user(id=42).get_platform().get_company()<broken>.get_name()
missing_preload: select_company()
suggested_fix: .select_platform_with(Q::platforms().select_company())
================================================================================
Locating the Broken Point
The original_expr_with_broken_point field inserts a visual <broken> marker where the chain failed.
When an AI agent (like our coding assistants) runs a test and hits this panic, it doesn't need to guess where the data is missing. It reads the structured payload, spots the <broken> marker, and immediately knows that the company relation needs to be loaded. It even gets the exact suggested_fix to append to its query builder.
Conclusion
Combining reference wrappers with structured, machine-readable diagnostics gives the API three useful properties:
- Readable traversal: Developers can use fluent
.get_foo().get_bar()chaining. - No graph cloning: Traversal is reference-driven.
- Actionable failures: When a relation is unloaded, the diagnostic identifies the broken path and suggests the preload to review.
The diagnostic does not guarantee an automatic fix, but it gives developers and coding agents a precise starting point for the next correction.
