Parser.comparison_operator — the production behind a watcher's WHEN condition — is declared:
private lazy val comparison_operator: PackratParser[ComparisonOperator] =
eq | ne | diff | gt | ge | lt | le
gt precedes ge, and lt precedes le. These are string literals, not anchored tokens, so on >= the > branch matches its first character and succeeds, leaving = for the value production — which then fails.
Measured, with controls
Against a real CREATE WATCHER on origin/main (767ec074), varying only the operator:
WHEN ctx.payload.hits.total > 0 OK
WHEN ctx.payload.hits.total >= 0 REJECTED "A value or a date/datetime function must be provided for comparison"
WHEN ctx.payload.hits.total < 0 OK
WHEN ctx.payload.hits.total <= 0 REJECTED (same message)
WHEN ctx.payload.hits.total = 0 OK
WHEN ctx.payload.hits.total <> 0 OK
<> survives because ne precedes lt/le.
So a watcher can only be written with a strict inequality. >= and <= — the natural spelling for a threshold alert — are rejected, and the error message names neither the operator nor the real cause.
Fix
Order longest-first, matching WhereParser.comparisonOp, which has always had it right:
eq | ne | diff | ge | gt | le | lt
The production is private lazy val with a single caller, so the blast radius is the watcher WHEN clause only.
How it was found
While sizing the prefix hazard for a future MySQL <=> (null-safe equality) operator — <= is a prefix of <=>, so the same ordering question had to be answered for the operator alternations. The WhereParser one was correct; this one was not. Unrelated to that work otherwise.
Parser.comparison_operator— the production behind a watcher'sWHENcondition — is declared:gtprecedesge, andltprecedesle. These are string literals, not anchored tokens, so on>=the>branch matches its first character and succeeds, leaving=for the value production — which then fails.Measured, with controls
Against a real
CREATE WATCHERonorigin/main(767ec074), varying only the operator:<>survives becauseneprecedeslt/le.So a watcher can only be written with a strict inequality.
>=and<=— the natural spelling for a threshold alert — are rejected, and the error message names neither the operator nor the real cause.Fix
Order longest-first, matching
WhereParser.comparisonOp, which has always had it right:The production is
private lazy valwith a single caller, so the blast radius is the watcherWHENclause only.How it was found
While sizing the prefix hazard for a future MySQL
<=>(null-safe equality) operator —<=is a prefix of<=>, so the same ordering question had to be answered for the operator alternations. TheWhereParserone was correct; this one was not. Unrelated to that work otherwise.