servers/postgres/copy_in/
parse.rs1use datafusion::sql::sqlparser::dialect::GenericDialect;
29use datafusion::sql::sqlparser::tokenizer::{Token, Tokenizer};
30use pgwire::error::PgWireResult;
31
32use crate::postgres::copy_in::options::{CopyOptionSet, CopyOptionValue};
33use crate::postgres::copy_in::{bad_copy_data, copy_in_must_run_alone_error};
34
35#[derive(Debug)]
42pub(super) struct RawCopyFromStdin {
43 pub(super) table: Vec<String>,
46 pub(super) columns: Vec<String>,
48 pub(super) options: CopyOptionSet,
50}
51
52pub(super) enum CopyInScan {
53 NotCopyIn,
55 CopyIn(RawCopyFromStdin),
57}
58
59pub(super) fn scan_copy_from_stdin(sql: &str) -> PgWireResult<CopyInScan> {
61 let trimmed = sql.trim_start();
64 let starts_like_copy = trimmed
65 .as_bytes()
66 .get(..4)
67 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"copy"));
68 let starts_with_comment = trimmed.starts_with("--") || trimmed.starts_with("/*");
69 if !starts_like_copy && !starts_with_comment {
70 return Ok(CopyInScan::NotCopyIn);
71 }
72
73 let tokens = match Tokenizer::new(&GenericDialect {}, sql).tokenize() {
74 Ok(tokens) => tokens,
75 Err(_) => return Ok(CopyInScan::NotCopyIn),
77 };
78 let tokens: Vec<Token> = tokens
80 .into_iter()
81 .filter(|token| !matches!(token, Token::Whitespace(_)))
82 .collect();
83 let mut cursor = Tokens {
84 tokens: &tokens,
85 pos: 0,
86 };
87
88 if !cursor.take_keyword("copy") {
89 return Ok(CopyInScan::NotCopyIn);
90 }
91
92 let table = match parse_object_name(&mut cursor) {
93 Some(table) => table,
94 None => return Ok(CopyInScan::NotCopyIn),
95 };
96 let columns = match parse_column_list(&mut cursor) {
97 Some(columns) => columns,
98 None => return Ok(CopyInScan::NotCopyIn),
99 };
100
101 if !cursor.take_keyword("from") || !cursor.take_keyword("stdin") {
102 return Ok(CopyInScan::NotCopyIn);
104 }
105
106 let mut options = CopyOptionSet::default();
107 let has_options = if cursor.take_keyword("with") {
109 if !matches!(cursor.peek(), Some(Token::LParen)) {
110 return Err(bad_copy_data("expected '(' after WITH in COPY FROM STDIN"));
111 }
112 true
113 } else {
114 matches!(cursor.peek(), Some(Token::LParen))
115 };
116 if has_options {
117 cursor.next();
119 parse_option_list(&mut cursor, &mut options)?;
120 }
121
122 while let Some(token) = cursor.next() {
124 if !matches!(token, Token::SemiColon) {
125 return Err(copy_in_must_run_alone_error());
126 }
127 }
128
129 Ok(CopyInScan::CopyIn(RawCopyFromStdin {
130 table,
131 columns,
132 options,
133 }))
134}
135
136struct Tokens<'a> {
137 tokens: &'a [Token],
138 pos: usize,
139}
140
141impl<'a> Tokens<'a> {
142 fn next(&mut self) -> Option<&'a Token> {
143 let token = self.tokens.get(self.pos)?;
144 self.pos += 1;
145 Some(token)
146 }
147
148 fn peek(&mut self) -> Option<&'a Token> {
149 self.tokens.get(self.pos)
150 }
151
152 fn take_keyword(&mut self, keyword: &str) -> bool {
154 match self.peek() {
155 Some(Token::Word(w))
156 if w.quote_style.is_none() && w.value.eq_ignore_ascii_case(keyword) =>
157 {
158 self.pos += 1;
159 true
160 }
161 _ => false,
162 }
163 }
164}
165
166fn expect_ident(cursor: &mut Tokens<'_>) -> Option<String> {
169 match cursor.next() {
170 Some(Token::Word(w)) => Some(if w.quote_style.is_none() {
171 w.value.to_lowercase()
172 } else {
173 w.value.clone()
174 }),
175 _ => None,
176 }
177}
178
179fn parse_object_name(cursor: &mut Tokens<'_>) -> Option<Vec<String>> {
180 let mut parts = vec![expect_ident(cursor)?];
181 while matches!(cursor.peek(), Some(Token::Period)) {
182 cursor.next();
183 parts.push(expect_ident(cursor)?);
184 }
185 Some(parts)
186}
187
188fn parse_column_list(cursor: &mut Tokens<'_>) -> Option<Vec<String>> {
192 if !matches!(cursor.peek(), Some(Token::LParen)) {
193 return Some(Vec::new());
194 }
195 cursor.next();
196 let mut columns = vec![expect_ident(cursor)?];
197 loop {
198 match cursor.next() {
199 Some(Token::Comma) => columns.push(expect_ident(cursor)?),
200 Some(Token::RParen) => break,
201 _ => return None,
202 }
203 }
204 Some(columns)
205}
206
207fn parse_option_list(cursor: &mut Tokens<'_>, options: &mut CopyOptionSet) -> PgWireResult<()> {
213 loop {
214 let key = match cursor.next() {
215 Some(Token::Word(w)) if w.quote_style.is_none() => w.value.to_lowercase(),
216 _ => return Err(bad_copy_data("expected an option name after '(' or ','")),
217 };
218 let value = match cursor.peek() {
219 Some(Token::Comma) | Some(Token::RParen) => CopyOptionValue::Flag,
220 Some(Token::SingleQuotedString(s)) => {
221 let value = CopyOptionValue::String(s.clone());
222 cursor.next();
223 value
224 }
225 Some(Token::EscapedStringLiteral(s)) => {
226 let value = CopyOptionValue::String(s.clone());
230 cursor.next();
231 value
232 }
233 Some(Token::Number(n, _)) => {
234 let value = CopyOptionValue::Number(n.clone());
235 cursor.next();
236 value
237 }
238 Some(Token::Word(w)) => {
239 let value = if w.quote_style.is_none() {
240 match w.value.to_ascii_lowercase().as_str() {
241 "true" => CopyOptionValue::Boolean(true),
242 "false" => CopyOptionValue::Boolean(false),
243 _ => CopyOptionValue::Ident(w.value.clone()),
244 }
245 } else {
246 CopyOptionValue::Ident(w.value.clone())
247 };
248 cursor.next();
249 value
250 }
251 _ => return Err(bad_copy_data("expected an option value or ',' or ')'")),
252 };
253 options.insert(key, value)?;
254 match cursor.next() {
255 Some(Token::Comma) => continue,
256 Some(Token::RParen) => break,
257 _ => {
258 return Err(bad_copy_data(
259 "expected ',' or ')' in COPY FROM STDIN options",
260 ));
261 }
262 }
263 }
264 Ok(())
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn scan(sql: &str) -> PgWireResult<Option<RawCopyFromStdin>> {
272 Ok(match scan_copy_from_stdin(sql)? {
273 CopyInScan::CopyIn(raw) => Some(raw),
274 CopyInScan::NotCopyIn => None,
275 })
276 }
277
278 #[test]
279 fn test_scan_basic() {
280 let raw = scan("COPY t FROM STDIN").unwrap().unwrap();
281 assert_eq!(raw.table, vec!["t"]);
282 assert!(raw.columns.is_empty());
283 }
284
285 #[test]
286 fn test_scan_qualified_table_and_columns() {
287 let raw = scan("COPY db.public.Metrics (ts, \"Host\") FROM STDIN")
288 .unwrap()
289 .unwrap();
290 assert_eq!(raw.table, vec!["db", "public", "metrics"]);
291 assert_eq!(raw.columns, vec!["ts", "Host"]);
292 }
293
294 #[test]
295 fn test_scan_not_copy_in() {
296 assert!(scan("COPY (SELECT 1) TO STDOUT").unwrap().is_none());
297 assert!(
298 scan("COPY t TO '/tmp/x.csv' WITH (FORMAT csv)")
299 .unwrap()
300 .is_none()
301 );
302 assert!(scan("SELECT 1").unwrap().is_none());
303 assert!(scan("COPY t FROM '/tmp/x.csv'").unwrap().is_none());
305 }
306
307 #[test]
308 fn test_scan_leading_comment() {
309 let raw = scan("-- a comment\nCOPY t FROM STDIN").unwrap().unwrap();
310 assert_eq!(raw.table, vec!["t"]);
311 }
312
313 #[test]
314 fn test_scan_folds_unquoted_identifiers() {
315 let raw = scan("copy Metrics (Host, ts) from stdin").unwrap().unwrap();
317 assert_eq!(raw.table, vec!["metrics"]);
318 assert_eq!(raw.columns, vec!["host", "ts"]);
319 }
320
321 #[test]
322 fn test_scan_preserves_quoted_identifiers() {
323 let raw = scan("COPY \"Metrics\" (\"Host\") FROM STDIN")
324 .unwrap()
325 .unwrap();
326 assert_eq!(raw.table, vec!["Metrics"]);
327 assert_eq!(raw.columns, vec!["Host"]);
328 }
329
330 #[test]
331 fn test_scan_options_kinds() {
332 let raw = scan(
333 "COPY t FROM STDIN WITH (FORMAT csv, DELIMITER ';', HEADER, \
334 skip_bad_records true, precision 3, mode strict)",
335 )
336 .unwrap()
337 .unwrap();
338 let mut options = raw.options;
339 assert_eq!(options.take_format().unwrap().as_deref(), Some("csv"));
340 assert_eq!(options.take_single_byte("DELIMITER").unwrap(), Some(b';'));
341 assert_eq!(options.take_flag("HEADER").unwrap(), Some(true));
342 assert_eq!(
343 options.take_value("skip_bad_records"),
344 Some(CopyOptionValue::Boolean(true))
345 );
346 assert_eq!(
347 options.take_value("precision"),
348 Some(CopyOptionValue::Number("3".into()))
349 );
350 assert_eq!(
351 options.take_value("mode"),
352 Some(CopyOptionValue::Ident("strict".into()))
353 );
354 }
355
356 #[test]
357 fn test_scan_escaped_string_constants() {
358 let mut raw = scan("COPY t FROM STDIN WITH (FORMAT csv, DELIMITER E'\\t')")
361 .unwrap()
362 .unwrap();
363 assert_eq!(
364 raw.options.take_single_byte("DELIMITER").unwrap(),
365 Some(b'\t')
366 );
367
368 let mut raw = scan("COPY t FROM STDIN WITH (FORMAT csv, DELIMITER E'\\\\')")
371 .unwrap()
372 .unwrap();
373 assert_eq!(
374 raw.options.take_single_byte("DELIMITER").unwrap(),
375 Some(b'\\')
376 );
377
378 let err = match crate::postgres::copy_in::copy_from_stdin_from_sql(
384 "COPY t FROM STDIN WITH (FORMAT csv, DELIMITER E'\\\\t')",
385 ) {
386 Ok(Some(_)) => panic!("expected the multi-character delimiter to be rejected"),
387 Ok(None) => panic!("expected the statement to scan as copy-in"),
388 Err(e) => e,
389 };
390 assert!(
391 err.to_string().contains("single one-byte character"),
392 "{err}"
393 );
394 }
395
396 #[test]
397 fn test_scan_semicolon_inside_option_value() {
398 let mut raw = scan("COPY t FROM STDIN WITH (NULL ';')").unwrap().unwrap();
400 assert_eq!(
401 raw.options.take_string("NULL").unwrap().as_deref(),
402 Some(";")
403 );
404 }
405
406 #[test]
407 fn test_scan_trailing_statements_rejected() {
408 let err = scan("COPY t FROM STDIN; SELECT 1").unwrap_err();
409 assert!(err.to_string().contains("alone"));
410
411 assert!(scan("COPY t FROM STDIN;").unwrap().is_some());
413 assert!(scan("COPY t FROM STDIN ; ;").unwrap().is_some());
414 }
415
416 #[test]
417 fn test_scan_options_without_with_keyword() {
418 let raw = scan("COPY t FROM STDIN (FORMAT csv, HEADER)")
419 .unwrap()
420 .unwrap();
421 let mut options = raw.options;
422 assert_eq!(options.take_format().unwrap().as_deref(), Some("csv"));
423 assert_eq!(options.take_flag("HEADER").unwrap(), Some(true));
424 assert!(options.reject_leftovers("csv").is_ok());
425 }
426
427 #[test]
428 fn test_scan_malformed_after_stdin() {
429 let err = scan("COPY t FROM STDIN WITH").unwrap_err();
430 assert!(err.to_string().contains("'('"), "{err}");
431 let err = scan("COPY t FROM STDIN WITH (").unwrap_err();
432 assert!(err.to_string().contains("option"), "{err}");
433 assert!(scan("COPY t (a,").unwrap().is_none());
436 }
437
438 #[test]
439 fn test_scan_redundant_option() {
440 let err = scan("COPY t FROM STDIN WITH (FORMAT csv, FORMAT text)").unwrap_err();
441 assert!(err.to_string().contains("redundant FORMAT"));
442 }
443}