A metric object shaped like a Custom SQL ad-hoc metric but missing
`expressionType` is silently rewritten into a bare string by
`QueryObject._set_metrics`, because `is_adhoc_metric` classifies a metric
solely by the presence of that one key. The string used is the metric's
`label`, which for a Custom SQL metric is the SQL text itself, so metric
resolution then reports `Metric '<sql text>' does not exist`.
`ChartDataAdhocMetricSchema` declares `expressionType` as required, but
`ChartDataQueryObjectSchema.metrics` is a list of `fields.Raw`, so that
contract is never enforced at the API boundary.
Adds a regression guard that reproduces the coercion through the real
request-deserialization path and the resulting error through the real query
builder, plus two tests fencing a fix from over-correcting: the legacy
`{"label": ...}` saved-metric reference must keep collapsing to its name, and
a well-formed Custom SQL metric must stay an ad-hoc definition.
The first test is red until the coercion is narrowed; the fix is not applied
here. RCA.md records the analysis, including the unresolved question of what
omits `expressionType` from the request in the first place.
Co-Authored-By: Claude <noreply@anthropic.com>