Filed unassigned, unlabelled and ungraded — domain:*, type and priority are the triage seat's. Filed by the domain:engine execution PM seat as the follow-up ruled at #16322 (comment 5594885075), which is what comment 5570949379 on that card had already asked for: "I would rather see it split than smuggled in."
The gap
POST /api/v1/analytics/dataset/query accepts its request body and hands selection onward without parsing it against any schema at the route. Its two siblings do not: /analytics/query and /analytics/sql lift a malformed selection to a 400 at the door.
⇒ a malformed member of selection — timeDimensions is the one that was measured, but it is not special — travels into dataset-executor and is answered by whatever the face behind it happens to do with it.
What is and is not already closed
⚠️ The dateRange half specifically is now closed, but BEHIND the route, not at it. PR #17015 (card #16322) gave the analytics faces a single shared refusal: an unrecognised dateRange string is refused with ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400 by driver-memory's cube face and by both service-analytics strategies, and a cross-face conformance fixture pins it.
Measured before that change, on the built artefacts: the route accepted the literal string not a range at all all the way to the executor, where both SQL strategies lowered it to a point window bound with the literal name (created_at >= $1 AND created_at <= $2, params ['not a range at all', 'not a range at all']) — identically to a valid preset — and driver-memory matched 5 of 5 probe rows for it.
⇒ what makes that input fail loudly today is a face-level refusal, not a door. The rest of selection still has no door at all, and a future face that is less careful re-opens the same hole.
Why it is worth its own card rather than a rider
Widening PR #17015 to add a route-level parse would have reached packages/runtime and packages/rest for a reason card #16322 does not name, and the two defects are different in kind:
The first is closed. The second is not.
Suggested direction, ⛔ not prescribed
Add the AnalyticsQuery parse at the dataset route so a malformed selection lifts to a 400 at the door the way it does on /analytics/query and /analytics/sql, which also brings the rest of selection under validation rather than only the member that happened to be measured.
⚠️ Whoever takes this should check whether the dataset route's selection is genuinely the same shape as the sibling routes' before reusing their schema — that was not measured here.
Duplicate check — bounded, and stated as such
Run over the 200 newest open issues (#16340 … #17054) of 596 open, with five term controls firing on the same instrument in the same pass (ListViewSchema 4 · Insert operation failed 2 · allDayField 1 · readonly 7 · analytics 8), so a zero is a reading rather than a broken grep.
⚠️ ⛔ GitHub's own free-text issue search was NOT usable for this: is:issue is:open returns 596, but a term query for ListViewSchema returns 0 while open issue #17053 carries ListViewSchema.sort in its title. The text index is not answering on this channel — so any zero from search_issues today measures nothing. The check above is a local grep over fetched bodies instead.
Two candidates hit the subject tokens and both were read in full and ruled out — same route, different defects:
⚠️ The bound is real: 396 older open issues were not swept.
Refs
#16322 (the card whose round measured this) · comment 5570949379 (which asked for the split) · PR #17015 (the face-level refusal that closes the dateRange half) · #17014 (the yesterday two-day window, filed by the same round) · neighbours on the same route, ⛔ not duplicates: #16642 · #16390 · #16178 (timeDimensions[].granularity never buckets).
Filed unassigned, unlabelled and ungraded —
domain:*, type and priority are the triage seat's. Filed by thedomain:engineexecution PM seat as the follow-up ruled at #16322 (comment5594885075), which is what comment5570949379on that card had already asked for: "I would rather see it split than smuggled in."The gap
POST /api/v1/analytics/dataset/queryaccepts its request body and handsselectiononward without parsing it against any schema at the route. Its two siblings do not:/analytics/queryand/analytics/sqllift a malformed selection to a 400 at the door.⇒ a malformed member of
selection—timeDimensionsis the one that was measured, but it is not special — travels intodataset-executorand is answered by whatever the face behind it happens to do with it.What is and is not already closed
dateRangehalf specifically is now closed, but BEHIND the route, not at it. PR #17015 (card #16322) gave the analytics faces a single shared refusal: an unrecogniseddateRangestring is refused withANALYTICS_DATE_RANGE_UNRECOGNIZED/ 400 bydriver-memory's cube face and by bothservice-analyticsstrategies, and a cross-face conformance fixture pins it.Measured before that change, on the built artefacts: the route accepted the literal string
not a range at allall the way to the executor, where both SQL strategies lowered it to a point window bound with the literal name (created_at >= $1 AND created_at <= $2, params['not a range at all', 'not a range at all']) — identically to a valid preset — anddriver-memorymatched 5 of 5 probe rows for it.⇒ what makes that input fail loudly today is a face-level refusal, not a door. The rest of
selectionstill has no door at all, and a future face that is less careful re-opens the same hole.Why it is worth its own card rather than a rider
Widening PR #17015 to add a route-level parse would have reached
packages/runtimeandpackages/restfor a reason card #16322 does not name, and the two defects are different in kind:dateRangestring handling to the closed preset vocabulary (driver half of #16041) #16322's defect: the preset vocabulary was not lowered — a valid preset silently widened to all of history (twelve of thirteen presets matched every probe row, 2020 and 2099 included).The first is closed. The second is not.
Suggested direction, ⛔ not prescribed
Add the
AnalyticsQueryparse at the dataset route so a malformedselectionlifts to a 400 at the door the way it does on/analytics/queryand/analytics/sql, which also brings the rest ofselectionunder validation rather than only the member that happened to be measured.selectionis genuinely the same shape as the sibling routes' before reusing their schema — that was not measured here.Duplicate check — bounded, and stated as such
Run over the 200 newest open issues (#16340 … #17054) of 596 open, with five term controls firing on the same instrument in the same pass (
ListViewSchema4 ·Insert operation failed2 ·allDayField1 ·readonly7 ·analytics8), so a zero is a reading rather than a broken grep.is:issue is:openreturns 596, but a term query forListViewSchemareturns 0 while open issue #17053 carriesListViewSchema.sortin its title. The text index is not answering on this channel — so any zero fromsearch_issuestoday measures nothing. The check above is a local grep over fetched bodies instead.Two candidates hit the subject tokens and both were read in full and ruled out — same route, different defects:
filter(or aratioover two of them) answers 501NOT_IMPLEMENTEDon driver-memory — the ObjectQL strategy hands the conditional aggregate todriver.find, which refuses it, and never falls back to the in-memory evaluator that supports it #16642 (bug·p2·pm:queue·domain:services) — a dataset measure with its ownfilter(or aratioover two) answers 501NOT_IMPLEMENTEDondriver-memory; the route is only where it was measured.p2·pm:queue·domain:services) — dataset dimension resolution returns raw user ids foruserdimensions instead of names.Refs
#16322 (the card whose round measured this) · comment
5570949379(which asked for the split) · PR #17015 (the face-level refusal that closes thedateRangehalf) · #17014 (theyesterdaytwo-day window, filed by the same round) · neighbours on the same route, ⛔ not duplicates: #16642 · #16390 · #16178 (timeDimensions[].granularitynever buckets).