usememos/memos · error

comparison must start with a field reference or supported fu

Error message

comparison must start with a field reference or supported function

What it means

The renderer translates a ComparisonCondition by switching on the left-hand expression type: FieldRef, FunctionValue, or FieldAccessorValue. Anything else — literals, arithmetic results, or custom ValueExpr implementations — cannot start an SQL comparison, so this error is thrown.

Source

Thrown at internal/filter/render.go:181

		}
		switch field.Kind {
		case FieldKindBoolColumn:
			return r.renderBoolColumnComparison(field, cond.Operator, cond.Right)
		case FieldKindJSONBool:
			return r.renderJSONBoolComparison(field, cond.Operator, cond.Right)
		case FieldKindJSONExists:
			return r.renderJSONExistsComparison(field, cond.Operator, cond.Right)
		case FieldKindScalar:
			return r.renderScalarComparison(field, cond.Operator, cond.Right)
		default:
			return renderResult{}, errors.Errorf("field %q does not support comparison", field.Name)
		}
	case *FunctionValue:
		return r.renderFunctionComparison(left, cond.Operator, cond.Right)
	case *FieldAccessorValue:
		return r.renderAccessorComparison(left, cond.Operator, cond.Right)
	default:
		return renderResult{}, errors.New("comparison must start with a field reference or supported function")
	}
}

// accessorSpec maps a CEL timestamp accessor to per-dialect SQL date-part tokens
// and the offset to subtract so the result matches CEL's base (e.g. CEL months
// are 0-based but every dialect reports 1-based, so off=1). off is indexed
// [sqlite, postgres, mysql].
type accessorSpec struct {
	sqlite string // strftime format specifier
	pg     string // EXTRACT field
	mysql  string // function name
	off    [3]int
}

var accessorSpecs = map[string]accessorSpec{
	"getFullYear":   {"%Y", "YEAR", "YEAR", [3]int{0, 0, 0}},
	"getMonth":      {"%m", "MONTH", "MONTH", [3]int{1, 1, 1}},
	"getDate":       {"%d", "DAY", "DAYOFMONTH", [3]int{0, 0, 0}},

View on GitHub (pinned to 14d757ce1f)

Solutions

  1. Put the field or function on the left: content.size() > 42 instead of 42 < content.size()
  2. If extending the library, add a case for your custom ValueExpr type in renderComparison or convert it to a FieldRef before rendering
  3. Fold literal-vs-literal comparisons in the parser instead of sending them to the renderer

Example fix

// before
`42 < content.size()`

// after
`content.size() > 42`
Defensive patterns

Strategy: validation

Validate before calling

// In Go, before rendering, verify the comparison left side is a supported type
func isRenderableLeft(v filter.ValueExpr) bool {
    switch v.(type) {
    case *filter.FieldRef, *filter.FunctionValue, *filter.FieldAccessorValue:
        return true
    }
    return false
}

// ok := isRenderableLeft(cond.Left)

Type guard

func isRenderableLeft(v filter.ValueExpr) bool {
    switch v.(type) {
    case *filter.FieldRef, *filter.FunctionValue, *filter.FieldAccessorValue:
        return true
    default:
        return false
    }
}

Try / catch

if _, err := r.Render(cond); err != nil {
    if strings.Contains(err.Error(), "must start with a field reference") {
        return userErr("put the field or function on the left of the comparison")
    }
    return err
}

Prevention

When it happens

Trigger: A filter like 42 < content.size or "abc" == tag — starting a comparison with a literal. Also triggered when user code constructs a ComparisonCondition with a custom ValueExpr implementation the renderer does not know.

Common situations: Reversed comparisons written by users; library extensions adding new ValueExpr types without extending renderComparison; programmatic construction of filter conditions.

Related errors


AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15). Data as JSON: /api/errors/2509c70980942fe1. Report an issue: GitHub.