Skip to content

Commit 652c896

Browse files
Capture the COPY ... FROM STDIN payload verbatim
1 parent 1ba145b commit 652c896

4 files changed

Lines changed: 136 additions & 67 deletions

File tree

‎src/ast/mod.rs‎

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3687,8 +3687,11 @@ pub enum Statement {
36873687
options: Vec<CopyOption>,
36883688
/// WITH options (before PostgreSQL version 9.0)
36893689
legacy_options: Vec<CopyLegacyOption>,
3690-
/// VALUES a vector of values to be copied
3691-
values: Vec<Option<String>>,
3690+
/// The inline payload of `COPY ... FROM STDIN`, as written, without the
3691+
/// `\.` terminator. Interpreting it (field separators, escapes and the
3692+
/// `\N` null marker) is left to the caller. `None` when the statement
3693+
/// carries no inline payload.
3694+
payload: Option<String>,
36923695
},
36933696
/// ```sql
36943697
/// COPY INTO <table> | <location>
@@ -5367,7 +5370,7 @@ impl fmt::Display for Statement {
53675370
target,
53685371
options,
53695372
legacy_options,
5370-
values,
5373+
payload,
53715374
} => {
53725375
write!(f, "COPY")?;
53735376
match source {
@@ -5389,19 +5392,10 @@ impl fmt::Display for Statement {
53895392
if !legacy_options.is_empty() {
53905393
write!(f, " {}", display_separated(legacy_options, " "))?;
53915394
}
5392-
if !values.is_empty() {
5393-
writeln!(f, ";")?;
5394-
let mut delim = "";
5395-
for v in values {
5396-
write!(f, "{delim}")?;
5397-
delim = "\t";
5398-
if let Some(v) = v {
5399-
write!(f, "{v}")?;
5400-
} else {
5401-
write!(f, "\\N")?;
5402-
}
5403-
}
5404-
write!(f, "\n\\.")?;
5395+
if let Some(payload) = payload {
5396+
// The payload starts on the line after the command and ends
5397+
// at the `\.` terminator.
5398+
write!(f, ";\n{payload}\\.")?;
54055399
}
54065400
Ok(())
54075401
}

‎src/ast/spans.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ impl Spanned for Statement {
347347
target: _,
348348
options: _,
349349
legacy_options: _,
350-
values: _,
350+
payload: _,
351351
} => source.span(),
352352
Statement::CopyIntoSnowflake {
353353
into: _,

‎src/parser/mod.rs‎

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12134,20 +12134,20 @@ impl<'a> Parser<'a> {
1213412134
while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option())? {
1213512135
legacy_options.push(opt);
1213612136
}
12137-
let values =
12137+
let payload =
1213812138
if matches!(target, CopyTarget::Stdin) && self.peek_token_ref().token != Token::EOF {
1213912139
self.expect_token(&Token::SemiColon)?;
12140-
self.parse_tsv()
12140+
self.parse_copy_payload()
1214112141
} else {
12142-
vec![]
12142+
None
1214312143
};
1214412144
Ok(Statement::Copy {
1214512145
source,
1214612146
to,
1214712147
target,
1214812148
options,
1214912149
legacy_options,
12150-
values,
12150+
payload,
1215112151
})
1215212152
}
1215312153

@@ -12488,40 +12488,50 @@ impl<'a> Parser<'a> {
1248812488
Ok(s.chars().next().unwrap())
1248912489
}
1249012490

12491-
/// Parse a tab separated values in
12492-
/// COPY payload
12493-
pub fn parse_tsv(&mut self) -> Vec<Option<String>> {
12494-
self.parse_tab_value()
12495-
}
12496-
12497-
/// Parse a single tab-separated value row used by `COPY` payload parsing.
12498-
pub fn parse_tab_value(&mut self) -> Vec<Option<String>> {
12499-
let mut values = vec![];
12500-
let mut content = String::new();
12501-
while let Some(t) = self.next_token_no_skip().map(|t| &t.token) {
12502-
match t {
12503-
Token::Whitespace(Whitespace::Tab) => {
12504-
values.push(Some(core::mem::take(&mut content)));
12505-
}
12506-
Token::Whitespace(Whitespace::Newline) => {
12507-
values.push(Some(core::mem::take(&mut content)));
12508-
}
12491+
/// Parse the inline payload of `COPY ... FROM STDIN` as written.
12492+
///
12493+
/// The payload is data rather than SQL, so it is captured uninterpreted.
12494+
/// It starts on the line following the command and runs up to the `\.`
12495+
/// terminator, which is consumed but not returned. Field
12496+
/// separators, escapes and the `\N` null marker are left to the caller.
12497+
///
12498+
/// Returns `None` when nothing but the statement delimiter follows, so a
12499+
/// bare `COPY t FROM STDIN;` is not given a payload it never carried.
12500+
fn parse_copy_payload(&mut self) -> Option<String> {
12501+
use core::fmt::Write;
12502+
12503+
let mut payload = String::new();
12504+
// The newline ending the command line is a delimiter, not data.
12505+
if self.peek_nth_token_no_skip_ref(0).token == Token::Whitespace(Whitespace::Newline) {
12506+
let _ = self.next_token_no_skip();
12507+
}
12508+
while let Some(token) = self.next_token_no_skip().map(|t| &t.token) {
12509+
match token {
1250912510
Token::Backslash => {
12510-
if self.consume_token(&Token::Period) {
12511-
return values;
12512-
}
12513-
if let Token::Word(w) = self.next_token().token {
12514-
if w.value == "N" {
12515-
values.push(None);
12516-
}
12511+
// `\.` terminates the payload, every other backslash is data.
12512+
if self.peek_nth_token_no_skip_ref(0).token == Token::Period {
12513+
let _ = self.next_token_no_skip();
12514+
return Some(payload);
1251712515
}
12516+
payload.push('\\');
1251812517
}
12519-
_ => {
12520-
content.push_str(&t.to_string());
12518+
// `Display` for a single line comment appends a newline that the
12519+
// tokenizer left in the stream as a separate token, so render it
12520+
// here instead to avoid duplicating it.
12521+
Token::Whitespace(Whitespace::SingleLineComment { prefix, comment }) => {
12522+
payload.push_str(prefix);
12523+
payload.push_str(comment);
12524+
}
12525+
// Rendering into the buffer avoids a temporary `String` per
12526+
// token, and a payload can hold an entire table.
12527+
token => {
12528+
let _ = write!(payload, "{token}");
1252112529
}
1252212530
}
1252312531
}
12524-
values
12532+
// An unterminated payload keeps whatever data it holds, but an empty one
12533+
// means the statement had no inline data at all.
12534+
(!payload.is_empty()).then_some(payload)
1252512535
}
1252612536

1252712537
/// Parse a literal value (numbers, strings, date/time, booleans)

‎tests/sqlparser_postgres.rs‎

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,7 +1443,10 @@ Kwara & Kogi
14431443
PHP ₱ USD $
14441444
\N Some other value
14451445
\\."#;
1446-
pg_and_generic().one_statement_parses_to(sql, "");
1446+
// The payload survives printing unchanged, so the only difference from the
1447+
// input is the normalized `STDIN` keyword.
1448+
let canonical = sql.replacen("FROM stdin;", "FROM STDIN;", 1);
1449+
pg_and_generic().one_statement_parses_to(sql, &canonical);
14471450
}
14481451

14491452
#[test]
@@ -1460,7 +1463,7 @@ fn parse_copy_from_stdin_without_semicolon() {
14601463
target: CopyTarget::Stdin,
14611464
options: vec![],
14621465
legacy_options: vec![CopyLegacyOption::Null("null".into())],
1463-
values: vec![],
1466+
payload: None,
14641467
}
14651468
);
14661469
}
@@ -1489,11 +1492,11 @@ fn parse_copy_from_stdin_without_semicolon_variants() {
14891492
Statement::Copy {
14901493
to: false,
14911494
target: CopyTarget::Stdin,
1492-
values,
1495+
payload,
14931496
..
14941497
} => {
14951498
assert!(
1496-
values.is_empty(),
1499+
payload.is_none(),
14971500
"expected no inline COPY payload for `{sql}`"
14981501
);
14991502
}
@@ -1518,7 +1521,7 @@ fn test_copy_from() {
15181521
},
15191522
options: vec![],
15201523
legacy_options: vec![],
1521-
values: vec![],
1524+
payload: None,
15221525
}
15231526
);
15241527

@@ -1536,7 +1539,7 @@ fn test_copy_from() {
15361539
},
15371540
options: vec![],
15381541
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
1539-
values: vec![],
1542+
payload: None,
15401543
}
15411544
);
15421545

@@ -1557,7 +1560,7 @@ fn test_copy_from() {
15571560
CopyLegacyOption::Delimiter(','),
15581561
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
15591562
],
1560-
values: vec![],
1563+
payload: None,
15611564
}
15621565
);
15631566
}
@@ -1578,7 +1581,7 @@ fn test_copy_to() {
15781581
},
15791582
options: vec![],
15801583
legacy_options: vec![],
1581-
values: vec![],
1584+
payload: None,
15821585
}
15831586
);
15841587

@@ -1596,7 +1599,7 @@ fn test_copy_to() {
15961599
},
15971600
options: vec![],
15981601
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
1599-
values: vec![],
1602+
payload: None,
16001603
}
16011604
);
16021605

@@ -1617,7 +1620,7 @@ fn test_copy_to() {
16171620
CopyLegacyOption::Delimiter(','),
16181621
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
16191622
],
1620-
values: vec![],
1623+
payload: None,
16211624
}
16221625
)
16231626
}
@@ -1671,7 +1674,7 @@ fn parse_copy_from() {
16711674
CopyOption::Encoding("utf8".into()),
16721675
],
16731676
legacy_options: vec![],
1674-
values: vec![],
1677+
payload: None,
16751678
}
16761679
);
16771680
}
@@ -1701,7 +1704,7 @@ fn parse_copy_to() {
17011704
},
17021705
options: vec![],
17031706
legacy_options: vec![],
1704-
values: vec![],
1707+
payload: None,
17051708
}
17061709
);
17071710

@@ -1717,7 +1720,7 @@ fn parse_copy_to() {
17171720
target: CopyTarget::Stdout,
17181721
options: vec![CopyOption::Delimiter('|')],
17191722
legacy_options: vec![],
1720-
values: vec![],
1723+
payload: None,
17211724
}
17221725
);
17231726

@@ -1736,7 +1739,7 @@ fn parse_copy_to() {
17361739
},
17371740
options: vec![],
17381741
legacy_options: vec![],
1739-
values: vec![],
1742+
payload: None,
17401743
}
17411744
);
17421745

@@ -1806,7 +1809,7 @@ fn parse_copy_to() {
18061809
},
18071810
options: vec![],
18081811
legacy_options: vec![],
1809-
values: vec![],
1812+
payload: None,
18101813
}
18111814
)
18121815
}
@@ -1837,7 +1840,7 @@ fn parse_copy_from_before_v9_0() {
18371840
CopyLegacyCsvOption::ForceNotNull(vec!["column".into()]),
18381841
]),
18391842
],
1840-
values: vec![],
1843+
payload: None,
18411844
}
18421845
);
18431846

@@ -1863,7 +1866,7 @@ fn parse_copy_from_before_v9_0() {
18631866
CopyLegacyCsvOption::Escape('\\'),
18641867
]),
18651868
],
1866-
values: vec![],
1869+
payload: None,
18671870
}
18681871
);
18691872
}
@@ -1894,7 +1897,7 @@ fn parse_copy_to_before_v9_0() {
18941897
CopyLegacyCsvOption::ForceQuote(vec!["column".into()]),
18951898
]),
18961899
],
1897-
values: vec![],
1900+
payload: None,
18981901
}
18991902
)
19001903
}
@@ -10126,3 +10129,65 @@ fn parse_bitstring_literal_escaping() {
1012610129
pg_and_generic().verified_stmt("SELECT B''''");
1012710130
pg_and_generic().verified_stmt("SELECT B'it''s'");
1012810131
}
10132+
10133+
#[test]
10134+
fn parse_copy_from_stdin_payload_is_captured_as_written() {
10135+
let check = |sql: &str, printed: &str, expected: Option<&str>| match pg_and_generic()
10136+
.one_statement_parses_to(sql, printed)
10137+
{
10138+
Statement::Copy { payload, .. } => {
10139+
assert_eq!(payload.as_deref(), expected, "for {sql:?}")
10140+
}
10141+
other => panic!("expected COPY, got {other:?}"),
10142+
};
10143+
10144+
// Tabs, newlines and the `\N` null marker are all left uninterpreted.
10145+
let rows = "COPY t (a, b) FROM STDIN;\n1\t\\N\n2\ty\n\\.";
10146+
check(rows, rows, Some("1\t\\N\n2\ty\n"));
10147+
10148+
// Only the keyword casing is normalized.
10149+
check(
10150+
"COPY t (a, b) FROM stdin;\n1\t\\N\n2\ty\n\\.",
10151+
rows,
10152+
Some("1\t\\N\n2\ty\n"),
10153+
);
10154+
10155+
// An empty payload is still a payload, distinct from a `COPY` that carries
10156+
// no inline data at all.
10157+
let empty = "COPY t (a, b) FROM STDIN;\n\\.";
10158+
check(empty, empty, Some(""));
10159+
10160+
// A statement delimiter with nothing behind it is not a payload, so
10161+
// printing must not invent data and a `\.` terminator.
10162+
check(
10163+
"COPY t (a, b) FROM STDIN;",
10164+
"COPY t (a, b) FROM STDIN",
10165+
None,
10166+
);
10167+
check(
10168+
"COPY t (a, b) FROM STDIN;\n",
10169+
"COPY t (a, b) FROM STDIN",
10170+
None,
10171+
);
10172+
10173+
// A payload cut short keeps the rows it does have, and printing restores
10174+
// the terminator the input was missing.
10175+
check(
10176+
"COPY t (a, b) FROM STDIN;\n1\t2\n",
10177+
"COPY t (a, b) FROM STDIN;\n1\t2\n\\.",
10178+
Some("1\t2\n"),
10179+
);
10180+
10181+
// The declared format does not change how the payload is captured.
10182+
let csv = "COPY t (a, b) FROM STDIN (FORMAT CSV);\na,b\n1,\"x y\"\n\\.";
10183+
check(csv, csv, Some("a,b\n1,\"x y\"\n"));
10184+
}
10185+
10186+
#[test]
10187+
fn parse_copy_from_stdin_payload_with_comment_marker() {
10188+
// A `--` in the payload is data, but the tokenizer still reports it as a
10189+
// comment, and printing that token appends a newline the tokenizer already
10190+
// emitted separately.
10191+
pg_and_generic().verified_stmt("COPY t (a, b) FROM STDIN;\n1\t-- not a comment\n2\ty\n\\.");
10192+
pg_and_generic().verified_stmt("COPY t (a, b) FROM STDIN;\n1\t/* nor is this */\n\\.");
10193+
}

0 commit comments

Comments
 (0)