fix(apiserver): FilterRows must not route non-bitmask operators through bitmask matching (bug-1680)

FilterRows routed ANY condition on job_status/host_status through
MatchesBitmaskCondition based purely on the field name, regardless of
the condition's actual operator. MatchesBitmaskCondition only
implements OpEQ/OpNE (the only two shapes buildIntCondition's in/notin
bitmask branch ever produces) and silently returns false for anything
else -- so xcecli's `-a` flag, which builds `{job_status:notnull:}`
(Operator: OpNOTNULL), matched zero rows unconditionally. `xcecli jobs
list -a` showed "no job found" even when jobs genuinely existed and
were correctly queryable through every other view.

FilterRows now requires the operator to actually be OpEQ/OpNE before
taking the bitmask path; any other operator on these fields correctly
falls through to the generic MatchesCondition evaluator, which already
implements OpNOTNULL/OpISNULL correctly -- no changes needed to either
evaluator itself, only the dispatch condition.

New query_test.go (no test file existed for this package before):
TestFilterRows_JobStatusNotNull_MatchesRealRows reproduces the bug
(independently confirmed RED: 0 of 3 rows matched) and
TestFilterRows_JobStatusBitmaskEQ_StillWorks locks in that the actual
bitmask shape used by every other status flag is unaffected. Live-
verified: `xcecli jobs list -a` now correctly shows jobs across every
status, immediately, no restart needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
Rongsong Shen 2026-08-25 04:04:08 +00:00
parent 0b04ab89b1
commit 9a230f0897
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 73 additions and 1 deletions

View file

@ -101,7 +101,16 @@ func FilterRows(rows []utils.OrderMap, conditions []utils.Condition) []utils.Ord
for _, row := range rows {
match := true
for _, cond := range conditions {
if IsBitmaskField(cond.Column) {
// bug-1680: IsBitmaskField alone is not enough to route into the
// bitmask path -- OpEQ/OpNE are the only two operator shapes
// buildIntCondition's in/notin bitmask branch ever produces.
// Any other operator on job_status/host_status (e.g. OpNOTNULL,
// from xcecli's -a flag building `{job_status:notnull:}`) must
// fall through to the generic evaluator below, which already
// handles it correctly -- MatchesBitmaskCondition's own switch
// only knows OpEQ/OpNE and silently returns false (never
// matching) for anything else.
if IsBitmaskField(cond.Column) && (cond.Operator == utils.OpEQ || cond.Operator == utils.OpNE) {
bitmask, ok := cond.Value.Value.(int64)
if !ok {
match = false

View file

@ -0,0 +1,63 @@
package store
import (
"testing"
"apiserver/utils"
)
// bug-1680: FilterRows unconditionally routes any condition on a bitmask
// field (job_status/host_status) through MatchesBitmaskCondition, regardless
// of the condition's actual operator. MatchesBitmaskCondition only knows how
// to evaluate OpEQ/OpNE (the only shapes buildIntCondition's in/notin bitmask
// branch ever produces) and silently returns false for anything else --
// including OpNOTNULL, the shape xcecli's `-a` flag produces via
// `{job_status:notnull:}`. The result: `-a` matches zero rows, every time,
// regardless of how many jobs actually exist.
func TestFilterRows_JobStatusNotNull_MatchesRealRows(t *testing.T) {
rows := []utils.OrderMap{
{Fields: []string{"job_status"}, Data: map[string]interface{}{"job_status": int64(1)}}, // PEND
{Fields: []string{"job_status"}, Data: map[string]interface{}{"job_status": int64(2)}}, // RUN
{Fields: []string{"job_status"}, Data: map[string]interface{}{"job_status": int64(8)}}, // DONE
}
// Mirrors exactly what buildIntCondition's null/notnull scalar path
// produces for `{job_status:notnull:}` (types.go): Operator: OpNOTNULL,
// Value: {TypeInt, 0} -- the 0 is a placeholder, never meant to be
// interpreted as a bitmask.
cond := utils.Condition{
Column: "job_status",
Operator: utils.OpNOTNULL,
Value: utils.Value{Type: utils.TypeInt, Value: int64(0)},
}
result := FilterRows(rows, []utils.Condition{cond})
if len(result) != len(rows) {
t.Errorf("FilterRows with job_status:notnull = %d rows, want %d (every row has a non-nil job_status)", len(result), len(rows))
}
}
// Regression coverage: the actual bitmask shape (OpEQ/OpNE, produced by
// buildIntCondition's in/notin branch) must keep working exactly as before.
func TestFilterRows_JobStatusBitmaskEQ_StillWorks(t *testing.T) {
rows := []utils.OrderMap{
{Fields: []string{"job_status"}, Data: map[string]interface{}{"job_status": int64(1)}}, // PEND
{Fields: []string{"job_status"}, Data: map[string]interface{}{"job_status": int64(2)}}, // RUN
}
// Mirrors buildIntCondition's bitmask branch for `{job_status:in:(PEND)}`:
// op = OpNE, Value = the OR'd bitmask of the requested status names.
cond := utils.Condition{
Column: "job_status",
Operator: utils.OpNE,
Value: utils.Value{Type: utils.TypeInt, Value: int64(1)},
}
result := FilterRows(rows, []utils.Condition{cond})
if len(result) != 1 {
t.Fatalf("FilterRows with job_status bitmask EQ = %d rows, want 1", len(result))
}
if result[0].Data["job_status"] != int64(1) {
t.Errorf("matched row job_status = %v, want 1 (PEND)", result[0].Data["job_status"])
}
}