usememos/memos · error

timestamp() argument must be an RFC3339 string or epoch int

Error message

timestamp() argument must be an RFC3339 string or epoch int

What it means

After arity checking, evaluateTimestamp accepts only an RFC3339-formatted string or an int64 epoch. Any other literal type (double, bool, uint beyond conversion) reaches the default branch and the filter is rejected because no SQL-comparable timestamp can be produced.

Source

Thrown at internal/filter/parser.go:609

func evaluateTimestamp(call *exprv1.Expr_Call) (int64, bool, error) {
	if len(call.Args) != 1 {
		return 0, false, errors.New("timestamp() expects one argument")
	}
	value, err := getConstValue(call.Args[0])
	if err != nil {
		return 0, false, errors.Wrap(err, "timestamp() only supports literal arguments")
	}
	switch v := value.(type) {
	case string:
		ts, err := time.Parse(time.RFC3339, v)
		if err != nil {
			return 0, false, errors.Wrap(err, "invalid timestamp literal")
		}
		return ts.Unix(), true, nil
	case int64:
		return v, true, nil
	default:
		return 0, false, errors.New("timestamp() argument must be an RFC3339 string or epoch int")
	}
}

// evaluateDuration folds duration("<go-duration>") into a number of seconds.
func evaluateDuration(call *exprv1.Expr_Call) (int64, bool, error) {
	if len(call.Args) != 1 {
		return 0, false, errors.New("duration() expects one argument")
	}
	value, err := getConstValue(call.Args[0])
	if err != nil {
		return 0, false, errors.Wrap(err, "duration() only supports literal arguments")
	}
	str, ok := value.(string)
	if !ok {
		return 0, false, errors.New("duration() argument must be a string")
	}
	d, err := time.ParseDuration(str)
	if err != nil {

View on GitHub (pinned to 14d757ce1f)

Solutions

  1. Use a full RFC3339 string with time and zone: timestamp("2024-01-01T00:00:00Z")
  2. Use integer epoch seconds: timestamp(1704067200)
  3. Convert milliseconds to integer seconds before embedding

Example fix

// before
created_ts > timestamp(1704067200000.0)

// after
created_ts > timestamp(1704067200)
Defensive patterns

Strategy: type-guard

Validate before calling

// Go: validate the literal kind before embedding
func validTimestampArg(v any) bool { _, s := v.(string); return s }
func validEpoch(v any) bool { _, i := v.(int64); return i }

Type guard

const isRfc3339OrEpoch = (v: unknown): boolean =>
  (typeof v === 'string' && !Number.isNaN(Date.parse(v)) && /T\d\d:\d\d:\d\d/.test(v)) ||
  (typeof v === 'number' && Number.isInteger(v));

Prevention

When it happens

Trigger: timestamp(1.5), timestamp(true), or a date string in a non-RFC3339 format that still parses as a CEL literal but fails the type switch after string parsing is bypassed — specifically non-string non-int literals.

Common situations: Passing Unix milliseconds as a double (1704067200000.0), booleans from a buggy template, or assuming ISO dates without time component ("2024-01-01") are accepted — note bare "2024-01-01" fails earlier with 'invalid timestamp literal' instead.

Related errors


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