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
- Replace the unsupported specifier with a supported one (s for string, d/i for integer, f for float, etc.).
- Escape or rephrase literal % sequences in the format string so they aren't parsed as specifiers.
- 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
- Use only documented specifier characters (s, d/i, f, etc.) in format strings.
- Escape literal % sequences intended as plain text.
- Unit-test format strings extracted from config/templates.
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
- unterminated format() type specifier
- illegal message id string
- missing FORMAT ... ENCODE ...
- missing FORMAT ... ENCODE ...
- {0}
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)