Rust Option Is Not Enough for Partially Loaded Entities
Rust's Option<T> is one of the language's best tools. It forces absence into
the type system and eliminates an entire class of null-pointer failures.
But an ORM or data runtime has another state that Option<T> cannot express on
its own: the field was not loaded.
If both SQL NULL and an unselected column become None, business logic cannot
tell a legitimate absence from an incomplete query. TeaQL Rust models the
missing state explicitly with EvalResult::Value, EvalResult::Null, and
EvalResult::NotLoaded.
Absence and Ignorance Are Different
Suppose an order may legitimately have no cancellation reason. This is a normal nullable value:
match order.eval_cancellation_reason() {
EvalResult::Null => println!("The order was not cancelled"),
EvalResult::Value(reason) => println!("Cancelled: {reason}"),
EvalResult::NotLoaded { .. } => unreachable!("query contract violation"),
}
Now suppose the query selected only the order ID. Returning None for the same
access would be false information: the runtime does not know whether a reason
exists.
TeaQL preserves that distinction until evaluation.
| State | What the runtime knows | Strict E behavior |
|---|---|---|
Value(value) | Selected and present | returns the value |
Null | Selected and absent | evaluates to None |
NotLoaded | Not selected | structured panic at the boundary |
Fluent E Expressions Without Cloning the Graph
The generated E API uses reference-bound wrappers, so a deep traversal does not clone the entity graph:
let merchant_name = E::merchant(&merchant)
.get_name()
.eval();
The result is Some(name) for Value and None for a loaded Null. If a required
relation or field is NotLoaded, the terminating operation panics with a
structured logic-bug diagnostic.
For a fallback that applies only to Null, the API is deliberately explicit:
let label = E::merchant(&merchant)
.get_name()
.or_if_null("Anonymous".to_string());
or_if_null, or_else_if_null, and or_default_if_null do not catch
NotLoaded. A default value must never turn an incomplete query into a plausible
business result.
Why Panic Is Appropriate Here
This is not an operational failure such as a timeout or unavailable database. It is a programming error: code accessed data outside the declared query contract.
At an application boundary, silently converting that error to None is more
dangerous than stopping the test. A strict panic makes the defect deterministic
and places it next to the expression that made the invalid assumption.
The diagnostic identifies:
- the root entity and identity;
- the complete attempted access path;
- the field or relation where traversal stopped;
- the missing preload;
- a query-builder correction to review.
This is especially useful in generated APIs because the repair can refer to real generated selection methods instead of asking a coding agent to invent one.
The Test Becomes a Query-Completeness Check
A conventional test often verifies only the final value. A TeaQL E-expression test also verifies that the value was obtained from data the query explicitly requested.
#[test]
fn display_name_requires_the_selected_field() {
let expression = ValueExpression::<'static, String>::new(
EvalResult::NotLoaded {
failed_node: "name".to_owned(),
attempted_path: "name".to_owned(),
},
std::sync::Arc::new("Merchant(id=42)".to_owned()),
);
let result = std::panic::catch_unwind(|| {
expression.or_if_null("Anonymous".to_string())
});
assert!(result.is_err());
}
After the request adds the generated display-name selection, the same test becomes evidence that both the query and the expression agree about their data contract.
Why AI Can Repair This Reliably
An AI agent is good at local transformations when the framework supplies exact evidence. It is much less reliable when a late failure could have originated in dozens of queries.
NotLoaded narrows the task:
root: Merchant(id=42)
access path: name
break point: name
suggested preload: select_name(...)
The agent can inspect the generated request type, add the corresponding selection, compile, and rerun the test. That is a bounded repair loop rather than an open-ended debugging exercise.
Cross-Language Evidence
Rust established this semantic baseline, and the same contract is now generated for Java, Go, Python, .NET, and TypeScript. The cross-language regression suite generated and compiled the language targets and finished with 109 tests, zero failures, and zero errors. Separate Java and .NET database tests verified that a selected SQL NULL remains Null rather than becoming NotLoaded.
The key lesson is broader than TeaQL: a type-safe application should model not only whether a value exists, but whether the program had enough data to ask the question in the first place.
