gastownhall/beads · error

unexpected token %q at position %d (expected end of query)

Error message

unexpected token %q at position %d (expected end of query)

What it means

After successfully parsing an expression, Parse (internal/query/parser.go:119) checks that the next token is EOF. A leftover token means trailing content that the grammar cannot attach to any node — a complete expression was parsed, but the string continues with junk. The message echoes the offending token value, position, and the expectation.

Source

Thrown at internal/query/parser.go:119

}

// Parse parses the query string and returns the root AST node.
func (p *Parser) Parse() (Node, error) {
	if err := p.advance(); err != nil {
		return nil, err
	}

	if p.current.Type == TokenEOF {
		return nil, fmt.Errorf("empty query")
	}

	node, err := p.parseOr()
	if err != nil {
		return nil, err
	}

	if p.current.Type != TokenEOF {
		return nil, fmt.Errorf("unexpected token %q at position %d (expected end of query)", p.current.Value, p.current.Pos)
	}

	return node, nil
}

// advance moves to the next token.
func (p *Parser) advance() error {
	if p.peeked != nil {
		p.current = *p.peeked
		p.peeked = nil
		return nil
	}
	tok, err := p.lexer.NextToken()
	if err != nil {
		return err
	}
	p.current = tok
	return nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Insert an explicit boolean operator between conditions: `status=open AND priority>1`.
  2. Remove the stray trailing token (often an extra ')' or ',').
  3. Balance parentheses so the whole expression is consumed before EOF.
  4. Wrap multi-word values in quotes so they lex as one TokenString instead of several tokens: `title="bug report"`.

Example fix

// before
query.Parse("status=open priority>1")
// after
query.Parse("status=open AND priority>1")
Defensive patterns

Strategy: validation

Validate before calling

// Join adjacent comparisons with AND before parsing (simple heuristic)
func joinConditions(q string) string {
    return regexp.MustCompile(`(\)|\w|")\s+(?:(?i)(and|or|not)\b)@!`).ReplaceAllString(q, "$1 ") // validate manually instead
}

Try / catch

node, err := query.Parse(q)
if err != nil && strings.Contains(err.Error(), "expected end of query") {
    return fmt.Errorf("trailing content after a complete expression; join conditions with AND/OR: %w", err)
}

Prevention

When it happens

Trigger: query.Parse with trailing junk after a valid expression: `status=open status != closed` (two comparisons with no AND/OR), `status=open)` (stray closing paren), `status=open , priority>1` (comma outside a list context), or `NOT` alone followed by nothing then more content that terminates an expression early.

Common situations: Users writing two conditions space-separated without AND (`priority<2 status=open`) as in other search DSLs; an unbalanced `)` that terminates the group early leaving leftovers; stray commas copied from list-syntax examples.

Understand the failure class

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/b6a77107fbad1a90. Report an issue: GitHub.