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. Recently, we made a bold decision: we are bringing back the beloved E:: fluent expression chain to Rust.
But this isn't a simple rollback. We've redesigned it from the ground up with zero-cost reference chaining and introduced a revolutionary structured panic mechanism that turns runtime errors into self-healing instructions for AI agents.
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 zero-cost, it destroyed the developer experience. As we often say: "Fluent expressions are mental candy for humans." Writing deeply nested closures just to read a nested property felt punishing.
Bringing Back E:: (The Right Way)
We realized we could have our cake and eat it too. By carefully crafting lifetime-bound wrapper structs in the code generator, we brought back the E:: syntax without any of the .clone() overhead.
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())
================================================================================
The Magic of <broken>
The most crucial addition here is the original_expr_with_broken_point field. By injecting a visual <broken> marker exactly where the chain failed, we provide immense context.
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
By combining zero-cost reference wrappers with highly structured, AI-readable panics, we've achieved the holy grail of framework design:
- Human Ergonomics: Developers get the "mental candy" of fluent
.get_foo().get_bar()chaining. - Performance: Zero allocations, purely reference-driven.
- AI Auto-Healing: When things break, the framework hands the AI the exact instructions needed to rewrite its query and heal the code automatically.
In the AI era, an error is no longer a dead end—it's just a prompt for the next self-correction.
