risingwavelabs/risingwave · error · ParseFormatError
unterminated format() type specifier
Error message
unterminated format() type specifier
What it means
ParseFormatError::UnterminatedSpecifier is raised when a format() type specifier starts but the format string ends before the specifier is closed — e.g. a trailing `%(` or `%(...)s` cut off. The parser reaches end-of-input while still inside a specifier.
Solutions
- Complete the specifier: ensure every `%(...)` pattern has its full type character and closing syntax, e.g. `%(x)s`.
- If a literal percent is intended, escape it per the format() rules rather than leaving a bare trailing %.
- Print/log the format string before calling format() to spot truncation.
Example fix
// before
SELECT format('name: %(name');
// after
SELECT format('name: %(name)s'); Defensive patterns
Strategy: validation
Validate before calling
fn specifiers_terminated(fmt: &str) -> bool {
let chars: Vec<char> = fmt.chars().collect();
for i in 0..chars.len() {
if chars[i] == '%' && i + 1 < chars.len() && chars[i+1] == '(' {
let closed = chars[i+2..].iter().position(|&c| c == ')');
match closed {
None => return false,
Some(off) => {
let spec_idx = i + 3 + off;
if spec_idx >= chars.len() { return false; }
}
}
}
}
true
} Try / catch
if !specifiers_terminated(&fmt) {
return Err("format string has an unterminated %(...) specifier".into());
}
let result = run_format_query(&fmt, args)?; Prevention
- Never build format strings by naive string concatenation that can truncate specifiers.
- Escape or avoid trailing bare % characters in templates.
- Add a format-string lint/test for dynamic templates before executing format().
When it happens
Trigger: Calling format() with a format string that ends mid-specifier, such as `format('value: %(x')` or `format('num: %' )` where the closing part of the specifier is missing.
Common situations: Truncated format strings built via dynamic string concatenation; copy-paste losing trailing characters; literals ending in % that were meant as plain text.
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
- unrecognized 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/3573ea59b1440a0b.
Report an issue: GitHub.
Appendix: source
Thrown at src/expr/impl/src/scalar/format.rs:123
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('%');
after_percent = false;
} else if after_percent {View on GitHub (pinned to 6469eb736d)