{"record":{"id":"3573ea59b1440a0b","repo":"risingwavelabs/risingwave","slug":"unterminated-format-type-specifier","errorCode":null,"errorMessage":"unterminated format() type specifier","messagePattern":"unterminated format\\(\\) type specifier","errorType":"error_code","errorClass":"ParseFormatError","httpStatus":null,"severity":"error","filePath":"src/expr/impl/src/scalar/format.rs","lineNumber":123,"sourceCode":"    ty: SpecifierType,\n}\n\n#[derive(Debug)]\nenum FormatterNode {\n    Specifier(Specifier),\n    Literal(String),\n}\n\n#[derive(Debug)]\nstruct Formatter {\n    nodes: Vec<FormatterNode>,\n}\n\n#[derive(Debug, thiserror::Error)]\nenum ParseFormatError {\n    #[error(\"unrecognized format() type specifier \\\"{0}\\\"\")]\n    UnrecognizedSpecifierType(char),\n    #[error(\"unterminated format() type specifier\")]\n    UnterminatedSpecifier,\n}\n\nimpl FromStr for Formatter {\n    type Err = ParseFormatError;\n\n    /// Parse the format string into a high-efficient representation.\n    /// <https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT>\n    fn from_str(format: &str) -> std::result::Result<Self, ParseFormatError> {\n        // 8 is a good magic number here, it can cover an input like 'Testing %s, %s, %s, %%'.\n        let mut nodes = Vec::with_capacity(8);\n        let mut after_percent = false;\n        let mut literal = String::with_capacity(8);\n        for c in format.chars() {\n            if after_percent && c == '%' {\n                literal.push('%');\n                after_percent = false;\n            } else if after_percent {","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/expr/impl/src/scalar/format.rs#L105-L141","documentation":"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.","triggerScenarios":"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.","commonSituations":"Truncated format strings built via dynamic string concatenation; copy-paste losing trailing characters; literals ending in % that were meant as plain text.","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."],"exampleFix":"// before\nSELECT format('name: %(name');\n// after\nSELECT format('name: %(name)s');","handlingStrategy":"validation","validationCode":"fn specifiers_terminated(fmt: &str) -> bool {\n    let chars: Vec<char> = fmt.chars().collect();\n    for i in 0..chars.len() {\n        if chars[i] == '%' && i + 1 < chars.len() && chars[i+1] == '(' {\n            let closed = chars[i+2..].iter().position(|&c| c == ')');\n            match closed {\n                None => return false,\n                Some(off) => {\n                    let spec_idx = i + 3 + off;\n                    if spec_idx >= chars.len() { return false; }\n                }\n            }\n        }\n    }\n    true\n}","typeGuard":null,"tryCatchPattern":"if !specifiers_terminated(&fmt) {\n    return Err(\"format string has an unterminated %(...) specifier\".into());\n}\nlet result = run_format_query(&fmt, args)?;","preventionTips":["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()."],"tags":["format","parsing","sql"],"backgroundTag":"invalid-argument-format","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}