pocketbase/pocketbase · error

unsupported token type %q

Error message

unsupported token type %q

What it means

Returned by tools/search's resolveToken when the filter token's type is none of Text, Number, or Function — the only token types this resolver can turn into dbx parameters/identifiers. It formats token.Type with %q, so the message carries the numeric type constant. Like the unknown-operator guard, this indicates grammar/resolver skew rather than ordinary user input: fexpr produced a token type the search package cannot compile.

Source

Thrown at tools/search/filter.go:319

		placeholder := "t" + security.PseudorandomString(8)

		return &ResolverResult{
			Identifier: "{:" + placeholder + "}",
			Params:     dbx.Params{placeholder: cast.ToFloat64(token.Literal)},
		}, nil
	case fexpr.TokenFunction:
		fn, ok := TokenFunctions[token.Literal]
		if !ok {
			return nil, fmt.Errorf("unknown function %q", token.Literal)
		}

		args, _ := token.Meta.([]fexpr.Token)
		return fn(func(argToken fexpr.Token) (*ResolverResult, error) {
			return resolveToken(argToken, fieldResolver)
		}, args...)
	}

	return nil, fmt.Errorf("unsupported token type %q", token.Type)
}

// Resolves = and != expressions in an attempt to minimize the COALESCE
// usage and to gracefully handle null vs empty string normalizations.
//
// The expression `a = "" OR a is null` tends to perform better than
// `COALESCE(a, "") = ""` since the direct match can be accomplished
// with a seek while the COALESCE will induce a table scan.
func resolveEqualExpr(equal bool, left, right *ResolverResult) dbx.Expression {
	equalOp := "="
	nullEqualOp := "IS"
	concatOp := "OR"
	nullExpr := "IS NULL"
	if !equal {
		// always use `IS NOT` instead of `!=` because direct non-equal comparisons
		// to nullable column values that are actually NULL yields to NULL instead of TRUE, eg.:
		// `'example' != nullableColumn` -> NULL even if nullableColumn row value is NULL
		equalOp = "IS NOT"

View on GitHub (pinned to 5d217ddb50)

Solutions

  1. Align the fexpr dependency version with the tools/search version (pin both; no replace directives).
  2. If maintaining a fork that adds token types, extend resolveToken with a case for each new type.
  3. Sanitize/limit user filter input to the documented syntax so unsupported constructs never reach the resolver.

Example fix

# before (go.mod)
replace github.com/pocketbase/fexpr =​> github.com/myorg/fexpr v0.0.0-custom
# after
# drop the replace, run:
go mod tidy
Defensive patterns

Strategy: validation

Validate before calling

// constrain user filter input to the documented grammar before parsing
var filterRe = regexp.MustCompile(`^[a-zA-Z0-9_.\s'"=\!~<>(),%-]+$`)
if !filterRe.MatchString(rawFilter) {
    return errors.New("filter contains unsupported syntax")
}

Try / catch

if err != nil && strings.Contains(err.Error(), "unsupported token type") {
    // grammar/resolver version skew: verify fexpr dependency pinning; reject input
}

Prevention

When it happens

Trigger: Effectively unreachable with the bundled fexpr, whose tokenizer only emits Text/Number/Function token types. It would surface with a custom or newer fexpr emitting new token kinds (e.g. raw regex or date tokens), or if Meta carried an unexpected type into a function path.

Common situations: Forked fexpr grammars; go.mod replace directives on fexpr; experimental branches adding token types ahead of resolver support.

Related errors


AI-assisted analysis of pocketbase/pocketbase@5d217ddb50 (2026-08-15). Data as JSON: /api/errors/396dc92619e61d71. Report an issue: GitHub.