{"record":{"id":"cd578468f5cc3d60","repo":"risingwavelabs/risingwave","slug":"must-be-or-cd5784","errorCode":null,"errorMessage":"`{}` must be {}, or {}","messagePattern":"`(.+?)` must be (.+?), or (.+?)","errorType":"validation","errorClass":"SinkError","httpStatus":null,"severity":"error","filePath":"src/connector/src/sink/file_sink/s3.rs","lineNumber":144,"sourceCode":"\nimpl UnknownFields for S3Config {\n    fn unknown_fields(&self) -> HashMap<String, String> {\n        self.unknown_fields.clone()\n    }\n}\n\ncrate::impl_sink_unknown_fields!(S3Config);\n\nimpl OpendalSinkBackend for S3Sink {\n    type Properties = S3Config;\n\n    const SINK_NAME: &'static str = S3_SINK;\n\n    fn from_btreemap(btree_map: BTreeMap<String, String>) -> Result<Self::Properties> {\n        let config = serde_json::from_value::<S3Config>(serde_json::to_value(btree_map).unwrap())\n            .map_err(|e| SinkError::Config(anyhow!(e)))?;\n        if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {\n            return Err(SinkError::Config(anyhow!(\n                \"`{}` must be {}, or {}\",\n                SINK_TYPE_OPTION,\n                SINK_TYPE_APPEND_ONLY,\n                SINK_TYPE_UPSERT\n            )));\n        }\n        Ok(config)\n    }\n\n    fn new_operator(properties: S3Config) -> Result<Operator> {\n        FileSink::<S3Sink>::new_s3_sink(&properties.common)\n    }\n\n    fn get_path(properties: Self::Properties) -> String {\n        properties.common.path.unwrap_or_default()\n    }\n\n    fn get_engine_type() -> super::opendal_sink::EngineType {","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/connector/src/sink/file_sink/s3.rs#L126-L162","documentation":"S3 file sink config validation in from_btreemap: the `type` property (r#type) held a value other than the two supported sink types (append-only / deb-mutating, per the `{}` must be {}, or {} template). It is a strict enum guard run right after serde-parsing S3Config, so a typo'd or unsupported sink type string triggers it.","triggerScenarios":"`CREATE SINK ... WITH (connector='s3', type='xxx')` where `xxx` is neither SINK_TYPE_APPEND_ONLY nor SINK_TYPE_UPSERT (including typos like 'append_only' vs 'append-only').","commonSituations":"Typo in the type option, mixing up RisingWave's sink `type` with an unrelated 's3 output type' option from another system, or omitting separator conventions.","solutions":["Set `type='append-only'` or `type='upsert'` in the WITH clause","Check exact spelling/hyphenation of the allowed values","If unsure, omit `type` if your use case matches the default, or consult docs for sink type semantics"],"exampleFix":"// before\nWITH (connector='s3', type='append_only', ...)\n// after\nWITH (connector='s3', type='append-only', ...)","handlingStrategy":"validation","validationCode":"const allowed = ['append-only', 'upsert'];\nif (opts.type !== undefined && !allowed.includes(opts.type)) {\n  throw new Error(`type must be one of ${allowed.join(', ')}`);\n}","typeGuard":"const isValidSinkType = (t) => t === 'append-only' || t === 'upsert';","tryCatchPattern":"try { createSink(opts); } catch (e) { if (String(e).includes('must be append-only, or upsert')) { opts.type = 'append-only'; createSink(opts); } else { throw e; } }","preventionTips":["Use the exact hyphenated lowercase literals for sink `type`","Standardize sink DDL templates per backend","Map application semantics (append vs upsert) to the correct type in your deploy tooling"],"tags":["config","s3","sink","validation"],"backgroundTag":"invalid-enum-value","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}