risingwavelabs/risingwave · error · ParseFormatError

unrecognized format() type specifier

Error message

unrecognized format() type specifier "{0}"

What it means

The format() function's format-string parser (ParseFormatError::UnrecognizedSpecifierType) fails when it encounters a type specifier character it does not recognize inside %(...) patterns. Only a fixed set of specifier types (e.g. s, i/d, f and similar) is accepted; anything else is rejected with the offending char in the message.

Solutions

  1. Replace the unsupported specifier with a supported one (s for string, d/i for integer, f for float, etc.).
  2. Escape or rephrase literal % sequences in the format string so they aren't parsed as specifiers.
  3. Consult the format() documentation for the exact list of accepted type specifier characters.

Example fix

// before
SELECT format('%(a)d and %(b)x', a := 1, b := 255);
// after
SELECT format('%(a)d and %(b)s', a := 1, b := to_hex(255));
Defensive patterns

Strategy: validation

Validate before calling

const SUPPORTED: &str = "sdif"; // per format() docs
fn check_specifiers(fmt: &str) -> Result<(), char> {
    let bytes: Vec<char> = fmt.chars().collect();
    for i in 0..bytes.len() {
        if bytes[i] == '%' && i + 1 < bytes.len() && bytes[i+1] == '(' {
            if let Some(close) = bytes[i+2..].iter().position(|&c| c == ')') {
                let spec = bytes[i + 3 + close];
                if !SUPPORTED.contains(spec) { return Err(spec); }
            }
        }
    }
    Ok(())
}

Try / catch

let formatted = sqlx::query_scalar::<_, String>("SELECT format($1, $2)")
    .bind(fmt).bind(args)
    .await
    .map_err(|e| if e.to_string().contains("unrecognized format() type specifier") {
        FormatStringError(e.to_string())
    } else { e.into() })?;

Prevention

When it happens

Trigger: Calling `SELECT format(...)` with a format string containing a specifier type character outside the supported set, e.g. `format('%(x)q', ...)` — the parser reads 'q' and returns UnrecognizedSpecifierType('q').

Common situations: Porting format strings from C printf or Python %-formatting that use unsupported conversion characters; typos in specifier letters; literals containing % followed by parentheses intended as plain text without escaping.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/a67d59e485180003. Report an issue: GitHub.

Appendix: source

Thrown at src/expr/impl/src/scalar/format.rs:121

struct Specifier {
    // TODO: support position, flags and width.
    ty: SpecifierType,
}

#[derive(Debug)]
enum FormatterNode {
    Specifier(Specifier),
    Literal(String),
}

#[derive(Debug)]
struct Formatter {
    nodes: Vec<FormatterNode>,
}

#[derive(Debug, thiserror::Error)]
enum ParseFormatError {
    #[error("unrecognized format() type specifier \"{0}\"")]
    UnrecognizedSpecifierType(char),
    #[error("unterminated format() type specifier")]
    UnterminatedSpecifier,
}

impl FromStr for Formatter {
    type Err = ParseFormatError;

    /// Parse the format string into a high-efficient representation.
    /// <https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT>
    fn from_str(format: &str) -> std::result::Result<Self, ParseFormatError> {
        // 8 is a good magic number here, it can cover an input like 'Testing %s, %s, %s, %%'.
        let mut nodes = Vec::with_capacity(8);
        let mut after_percent = false;
        let mut literal = String::with_capacity(8);
        for c in format.chars() {
            if after_percent && c == '%' {
                literal.push('%');

View on GitHub (pinned to 6469eb736d)