The previous version of this fix left a malformed ad-hoc metric (one that
carries ad-hoc-only keys like sqlExpression/aggregate/column but is missing
expressionType) as an untouched dict, expecting later validation to catch it.
In practice nothing downstream recognizes that shape either, so it fell
through to a bare ValueError instead of a QueryObjectValidationError -
an uncontrolled error rather than a clean, user-facing one.
QueryObject._set_metrics now raises QueryObjectValidationError directly for
this shape. The chart-data API endpoints already catch
QueryObjectValidationError around query-context deserialization and return a
400, so the error surfaces as a controlled response instead of propagating
uncaught.
The guard test is updated to assert the raise (with a message naming the
missing field) instead of asserting on a returned value, since the malformed
metric is now rejected at deserialization time rather than passed through.