servers/postgres/copy_in/
options.rs1use pgwire::error::{PgWireError, PgWireResult};
27
28use crate::postgres::copy_in::bad_copy_data;
29use crate::postgres::types::PgErrorCode;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub(crate) enum CopyOptionValue {
34 String(String),
36 Number(String),
38 Ident(String),
40 Boolean(bool),
42 Flag,
44}
45
46impl CopyOptionValue {
47 pub(crate) fn kind(&self) -> &'static str {
49 match self {
50 CopyOptionValue::String(_) => "a string value",
51 CopyOptionValue::Number(_) => "a numeric value",
52 CopyOptionValue::Ident(_) => "an identifier value",
53 CopyOptionValue::Boolean(_) => "a boolean value",
54 CopyOptionValue::Flag => "no value",
55 }
56 }
57}
58
59#[derive(Debug, Default)]
64pub(crate) struct CopyOptionSet {
65 entries: Vec<(String, CopyOptionValue)>,
66}
67
68impl CopyOptionSet {
69 pub(crate) fn insert(&mut self, key: String, value: CopyOptionValue) -> PgWireResult<()> {
71 if self.entries.iter().any(|(k, _)| *k == key) {
72 return Err(bad_copy_data(format!(
73 "conflicting or redundant {} option",
74 key.to_uppercase()
75 )));
76 }
77 self.entries.push((key, value));
78 Ok(())
79 }
80
81 pub(crate) fn take_value(&mut self, key: &str) -> Option<CopyOptionValue> {
84 let key = key.to_lowercase();
85 let index = self.entries.iter().position(|(k, _)| *k == key)?;
86 Some(self.entries.remove(index).1)
87 }
88
89 pub(crate) fn take_format(&mut self) -> PgWireResult<Option<String>> {
92 let Some(value) = self.take_value("format") else {
93 return Ok(None);
94 };
95 match value {
96 CopyOptionValue::Ident(name) | CopyOptionValue::String(name) => {
97 Ok(Some(name.to_lowercase()))
98 }
99 other => Err(bad_copy_data(format!(
100 "FORMAT requires an identifier value, got {}",
101 other.kind()
102 ))),
103 }
104 }
105
106 pub(crate) fn take_string(&mut self, key: &str) -> PgWireResult<Option<String>> {
108 let Some(value) = self.take_value(key) else {
109 return Ok(None);
110 };
111 match value {
112 CopyOptionValue::String(s) => Ok(Some(s)),
113 other => Err(bad_copy_data(format!(
114 "COPY option {} requires a string value, got {}",
115 key.to_uppercase(),
116 other.kind()
117 ))),
118 }
119 }
120
121 pub(crate) fn take_single_byte(&mut self, key: &str) -> PgWireResult<Option<u8>> {
125 let Some(value) = self.take_string(key)? else {
126 return Ok(None);
127 };
128 let mut chars = value.chars();
129 let (c, rest) = (chars.next(), chars.as_str());
130 match c {
131 Some('\t') if rest.is_empty() => Ok(Some(b'\t')),
132 Some(c) if rest.is_empty() && c.is_ascii() && !c.is_ascii_control() => {
133 Ok(Some(c as u8))
134 }
135 _ => Err(bad_copy_data(format!(
136 "COPY option {} must be a single one-byte character other than a newline or carriage return",
137 key.to_uppercase()
138 ))),
139 }
140 }
141
142 pub(crate) fn take_flag(&mut self, key: &str) -> PgWireResult<Option<bool>> {
144 let Some(value) = self.take_value(key) else {
145 return Ok(None);
146 };
147 match value {
148 CopyOptionValue::Flag => Ok(Some(true)),
149 CopyOptionValue::Boolean(b) => Ok(Some(b)),
150 other => Err(bad_copy_data(format!(
151 "COPY option {} requires a boolean value, got {}",
152 key.to_uppercase(),
153 other.kind()
154 ))),
155 }
156 }
157
158 pub(crate) fn reject_leftovers(self, format: &str) -> PgWireResult<()> {
162 if self.entries.is_empty() {
163 return Ok(());
164 }
165 let unknown = self
166 .entries
167 .iter()
168 .map(|(key, _)| key.to_uppercase())
169 .collect::<Vec<_>>()
170 .join(", ");
171 Err(PgWireError::UserError(Box::new(
172 PgErrorCode::Ec0A000.to_err_info(format!(
173 "COPY option(s) {unknown} are not supported by format {format}"
174 )),
175 )))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn new_set(entries: &[(&str, CopyOptionValue)]) -> CopyOptionSet {
184 let mut set = CopyOptionSet::default();
185 for (key, value) in entries {
186 set.insert((*key).to_string(), value.clone()).unwrap();
187 }
188 set
189 }
190
191 #[test]
192 fn test_insert_rejects_duplicates() {
193 let mut set = CopyOptionSet::default();
194 set.insert("format".to_string(), CopyOptionValue::Ident("csv".into()))
195 .unwrap();
196 let err = set
197 .insert("format".to_string(), CopyOptionValue::Ident("text".into()))
198 .unwrap_err();
199 assert!(err.to_string().contains("redundant FORMAT"));
200 }
201
202 #[test]
203 fn test_take_single_byte_tab_allowed() {
204 let mut set = new_set(&[("delimiter", CopyOptionValue::String("\t".into()))]);
206 assert_eq!(set.take_single_byte("DELIMITER").unwrap(), Some(b'\t'));
207
208 for bad in ["\n", "\r", "\0"] {
210 let mut set = new_set(&[("delimiter", CopyOptionValue::String(bad.into()))]);
211 assert!(
212 set.take_single_byte("DELIMITER").is_err(),
213 "{bad:?} must be rejected"
214 );
215 }
216 }
217
218 #[test]
219 fn test_take_string_and_single_byte() {
220 let mut set = new_set(&[
221 ("null", CopyOptionValue::String("nil".into())),
222 ("delimiter", CopyOptionValue::String("|".into())),
223 ]);
224 assert_eq!(set.take_string("null").unwrap().as_deref(), Some("nil"));
225 assert_eq!(set.take_single_byte("delimiter").unwrap(), Some(b'|'));
226
227 let mut set = new_set(&[("delimiter", CopyOptionValue::Number("42".into()))]);
228 let err = set.take_single_byte("delimiter").unwrap_err();
229 assert!(err.to_string().contains("requires a string value"));
230
231 let mut set = new_set(&[("delimiter", CopyOptionValue::String("ab".into()))]);
232 let err = set.take_single_byte("delimiter").unwrap_err();
233 assert!(err.to_string().contains("single one-byte"));
234 }
235
236 #[test]
237 fn test_take_flag() {
238 let mut set = new_set(&[
239 ("header", CopyOptionValue::Flag),
240 ("frozen", CopyOptionValue::Boolean(false)),
241 ]);
242 assert_eq!(set.take_flag("header").unwrap(), Some(true));
243 assert_eq!(set.take_flag("frozen").unwrap(), Some(false));
244 assert_eq!(set.take_flag("absent").unwrap(), None);
245 }
246
247 #[test]
248 fn test_take_format_normalizes() {
249 let mut set = new_set(&[("format", CopyOptionValue::Ident("CSV".into()))]);
250 assert_eq!(set.take_format().unwrap().as_deref(), Some("csv"));
251 }
252
253 #[test]
254 fn test_reject_leftovers_lists_all() {
255 let mut set = new_set(&[
256 ("precision", CopyOptionValue::String("ns".into())),
257 ("weird", CopyOptionValue::Flag),
258 ]);
259 set.take_string("precision").unwrap();
260 let err = set.reject_leftovers("text").unwrap_err();
261 assert!(
262 err.to_string().contains("WEIRD"),
263 "leftover option must be listed: {err}"
264 );
265 assert!(
266 err.to_string().contains("not supported by format text"),
267 "{err}"
268 );
269 assert!(!err.to_string().contains("PRECISION"));
271 }
272
273 #[test]
274 fn test_raw_value_escape_hatch() {
275 let mut set = new_set(&[("precision", CopyOptionValue::Ident("ns".into()))]);
276 assert_eq!(
277 set.take_value("precision"),
278 Some(CopyOptionValue::Ident("ns".into()))
279 );
280 }
281}