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:
parent
0b04ab89b1
commit
9a230f0897
2 changed files with 73 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
63
src/frontend/apiserver/store/query_test.go
Normal file
63
src/frontend/apiserver/store/query_test.go
Normal 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"])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue