Summary
The time_range block is gated on Since != "", so an until-only range (and a since with an empty Column) is silently ignored — no filter, no 400.
Detail
internal/query/builder.go:327:
if timeRange != nil && timeRange.Column != "" && timeRange.Since != "" {
The Until clause lives inside this block, so {"time_range":{"column":"ts","until":"2026-01-01T00:00:00Z"}} emits no predicate and returns rows from all time (LIMIT-capped, arbitrary window) with HTTP 200. builder_test.go covers since-only and since+until, never until-only. The public HTTP API accepts this shape even though the TS SDK's timeRange(column, since, until?) can't express it.
Fix direction
Bind since and until independently; reject time_range with a non-empty bound but empty column as a 400.
Found in a repo-wide audit; verified by code trace. Distinct from #293 (duration suffixes) and #295 (docs).
Summary
The
time_rangeblock is gated onSince != "", so an until-only range (and a since with an emptyColumn) is silently ignored — no filter, no 400.Detail
internal/query/builder.go:327:The
Untilclause lives inside this block, so{"time_range":{"column":"ts","until":"2026-01-01T00:00:00Z"}}emits no predicate and returns rows from all time (LIMIT-capped, arbitrary window) with HTTP 200.builder_test.gocovers since-only and since+until, never until-only. The public HTTP API accepts this shape even though the TS SDK'stimeRange(column, since, until?)can't express it.Fix direction
Bind
sinceanduntilindependently; rejecttime_rangewith a non-empty bound but emptycolumnas a 400.Found in a repo-wide audit; verified by code trace. Distinct from #293 (duration suffixes) and #295 (docs).