Skip to content

Commit 7480aa7

Browse files
Capture the COPY ... FROM STDIN payload verbatim
1 parent d398304 commit 7480aa7

4 files changed

Lines changed: 139 additions & 67 deletions

File tree

‎src/ast/mod.rs‎

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3678,8 +3678,11 @@ pub enum Statement {
36783678
options: Vec<CopyOption>,
36793679
/// WITH options (before PostgreSQL version 9.0)
36803680
legacy_options: Vec<CopyLegacyOption>,
3681-
/// VALUES a vector of values to be copied
3682-
values: Vec<Option<String>>,
3681+
/// The inline payload of `COPY ... FROM STDIN`, as written, without the
3682+
/// `\.` terminator. Interpreting it (field separators, escapes and the
3683+
/// `\N` null marker) is left to the caller. `None` when the statement
3684+
/// carries no inline payload.
3685+
payload: Option<String>,
36833686
},
36843687
/// ```sql
36853688
/// COPY INTO <table> | <location>
@@ -5353,7 +5356,7 @@ impl fmt::Display for Statement {
53535356
target,
53545357
options,
53555358
legacy_options,
5356-
values,
5359+
payload,
53575360
} => {
53585361
write!(f, "COPY")?;
53595362
match source {
@@ -5375,19 +5378,10 @@ impl fmt::Display for Statement {
53755378
if !legacy_options.is_empty() {
53765379
write!(f, " {}", display_separated(legacy_options, " "))?;
53775380
}
5378-
if !values.is_empty() {
5379-
writeln!(f, ";")?;
5380-
let mut delim = "";
5381-
for v in values {
5382-
write!(f, "{delim}")?;
5383-
delim = "\t";
5384-
if let Some(v) = v {
5385-
write!(f, "{v}")?;
5386-
} else {
5387-
write!(f, "\\N")?;
5388-
}
5389-
}
5390-
write!(f, "\n\\.")?;
5381+
if let Some(payload) = payload {
5382+
// The payload starts on the line after the command and ends
5383+
// at the `\.` terminator.
5384+
write!(f, ";\n{payload}\\.")?;
53915385
}
53925386
Ok(())
53935387
}

‎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
@@ -12037,20 +12037,20 @@ impl<'a> Parser<'a> {
1203712037
while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option())? {
1203812038
legacy_options.push(opt);
1203912039
}
12040-
let values =
12040+
let payload =
1204112041
if matches!(target, CopyTarget::Stdin) && self.peek_token_ref().token != Token::EOF {
1204212042
self.expect_token(&Token::SemiColon)?;
12043-
self.parse_tsv()
12043+
self.parse_copy_payload()
1204412044
} else {
12045-
vec![]
12045+
None
1204612046
};
1204712047
Ok(Statement::Copy {
1204812048
source,
1204912049
to,
1205012050
target,
1205112051
options,
1205212052
legacy_options,
12053-
values,
12053+
payload,
1205412054
})
1205512055
}
1205612056

@@ -12391,40 +12391,50 @@ impl<'a> Parser<'a> {
1239112391
Ok(s.chars().next().unwrap())
1239212392
}
1239312393

12394-
/// Parse a tab separated values in
12395-
/// COPY payload
12396-
pub fn parse_tsv(&mut self) -> Vec<Option<String>> {
12397-
self.parse_tab_value()
12398-
}
12399-
12400-
/// Parse a single tab-separated value row used by `COPY` payload parsing.
12401-
pub fn parse_tab_value(&mut self) -> Vec<Option<String>> {
12402-
let mut values = vec![];
12403-
let mut content = String::new();
12404-
while let Some(t) = self.next_token_no_skip().map(|t| &t.token) {
12405-
match t {
12406-
Token::Whitespace(Whitespace::Tab) => {
12407-
values.push(Some(core::mem::take(&mut content)));
12408-
}
12409-
Token::Whitespace(Whitespace::Newline) => {
12410-
values.push(Some(core::mem::take(&mut content)));
12411-
}
12394+
/// Parse the inline payload of `COPY ... FROM STDIN` as written.
12395+
///
12396+
/// The payload is data rather than SQL, so it is captured without being
12397+
/// interpreted: the data starts on the line following the command and runs
12398+
/// up to the `\.` terminator, which is consumed but not returned. Field
12399+
/// separators, escapes and the `\N` null marker are left to the caller.
12400+
///
12401+
/// Returns `None` when nothing but the statement delimiter follows, so a
12402+
/// bare `COPY t FROM STDIN;` is not given a payload it never carried.
12403+
fn parse_copy_payload(&mut self) -> Option<String> {
12404+
use core::fmt::Write;
12405+
12406+
let mut payload = String::new();
12407+
// The newline ending the command line is a delimiter, not data.
12408+
if self.peek_nth_token_no_skip_ref(0).token == Token::Whitespace(Whitespace::Newline) {
12409+
let _ = self.next_token_no_skip();
12410+
}
12411+
while let Some(token) = self.next_token_no_skip().map(|t| &t.token) {
12412+
match token {
1241212413
Token::Backslash => {
12413-
if self.consume_token(&Token::Period) {
12414-
return values;
12415-
}
12416-
if let Token::Word(w) = self.next_token().token {
12417-
if w.value == "N" {
12418-
values.push(None);
12419-
}
12414+
// `\.` terminates the payload, every other backslash is data.
12415+
if self.peek_nth_token_no_skip_ref(0).token == Token::Period {
12416+
let _ = self.next_token_no_skip();
12417+
return Some(payload);
1242012418
}
12419+
payload.push('\\');
1242112420
}
12422-
_ => {
12423-
content.push_str(&t.to_string());
12421+
// `Display` for a single line comment appends a newline that the
12422+
// tokenizer left in the stream as a separate token, so render it
12423+
// here instead to avoid duplicating it.
12424+
Token::Whitespace(Whitespace::SingleLineComment { prefix, comment }) => {
12425+
payload.push_str(prefix);
12426+
payload.push_str(comment);
12427+
}
12428+
// Rendering into the buffer avoids a temporary `String` per
12429+
// token, and a payload can hold an entire table.
12430+
token => {
12431+
let _ = write!(payload, "{token}");
1242412432
}
1242512433
}
1242612434
}
12427-
values
12435+
// An unterminated payload keeps whatever data it holds, but an empty one
12436+
// means the statement had no inline data at all.
12437+
(!payload.is_empty()).then_some(payload)
1242812438
}
1242912439

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

‎tests/sqlparser_postgres.rs‎

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,7 +1455,10 @@ Kwara & Kogi
14551455
PHP ₱ USD $
14561456
\N Some other value
14571457
\\."#;
1458-
pg_and_generic().one_statement_parses_to(sql, "");
1458+
// The payload survives printing unchanged, so the only difference from the
1459+
// input is the normalized `STDIN` keyword.
1460+
let canonical = sql.replacen("FROM stdin;", "FROM STDIN;", 1);
1461+
pg_and_generic().one_statement_parses_to(sql, &canonical);
14591462
}
14601463

14611464
#[test]
@@ -1472,7 +1475,7 @@ fn parse_copy_from_stdin_without_semicolon() {
14721475
target: CopyTarget::Stdin,
14731476
options: vec![],
14741477
legacy_options: vec![CopyLegacyOption::Null("null".into())],
1475-
values: vec![],
1478+
payload: None,
14761479
}
14771480
);
14781481
}
@@ -1501,11 +1504,11 @@ fn parse_copy_from_stdin_without_semicolon_variants() {
15011504
Statement::Copy {
15021505
to: false,
15031506
target: CopyTarget::Stdin,
1504-
values,
1507+
payload,
15051508
..
15061509
} => {
15071510
assert!(
1508-
values.is_empty(),
1511+
payload.is_none(),
15091512
"expected no inline COPY payload for `{sql}`"
15101513
);
15111514
}
@@ -1530,7 +1533,7 @@ fn test_copy_from() {
15301533
},
15311534
options: vec![],
15321535
legacy_options: vec![],
1533-
values: vec![],
1536+
payload: None,
15341537
}
15351538
);
15361539

@@ -1548,7 +1551,7 @@ fn test_copy_from() {
15481551
},
15491552
options: vec![],
15501553
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
1551-
values: vec![],
1554+
payload: None,
15521555
}
15531556
);
15541557

@@ -1569,7 +1572,7 @@ fn test_copy_from() {
15691572
CopyLegacyOption::Delimiter(','),
15701573
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
15711574
],
1572-
values: vec![],
1575+
payload: None,
15731576
}
15741577
);
15751578
}
@@ -1590,7 +1593,7 @@ fn test_copy_to() {
15901593
},
15911594
options: vec![],
15921595
legacy_options: vec![],
1593-
values: vec![],
1596+
payload: None,
15941597
}
15951598
);
15961599

@@ -1608,7 +1611,7 @@ fn test_copy_to() {
16081611
},
16091612
options: vec![],
16101613
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
1611-
values: vec![],
1614+
payload: None,
16121615
}
16131616
);
16141617

@@ -1629,7 +1632,7 @@ fn test_copy_to() {
16291632
CopyLegacyOption::Delimiter(','),
16301633
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
16311634
],
1632-
values: vec![],
1635+
payload: None,
16331636
}
16341637
)
16351638
}
@@ -1683,7 +1686,7 @@ fn parse_copy_from() {
16831686
CopyOption::Encoding("utf8".into()),
16841687
],
16851688
legacy_options: vec![],
1686-
values: vec![],
1689+
payload: None,
16871690
}
16881691
);
16891692
}
@@ -1713,7 +1716,7 @@ fn parse_copy_to() {
17131716
},
17141717
options: vec![],
17151718
legacy_options: vec![],
1716-
values: vec![],
1719+
payload: None,
17171720
}
17181721
);
17191722

@@ -1729,7 +1732,7 @@ fn parse_copy_to() {
17291732
target: CopyTarget::Stdout,
17301733
options: vec![CopyOption::Delimiter('|')],
17311734
legacy_options: vec![],
1732-
values: vec![],
1735+
payload: None,
17331736
}
17341737
);
17351738

@@ -1748,7 +1751,7 @@ fn parse_copy_to() {
17481751
},
17491752
options: vec![],
17501753
legacy_options: vec![],
1751-
values: vec![],
1754+
payload: None,
17521755
}
17531756
);
17541757

@@ -1818,7 +1821,7 @@ fn parse_copy_to() {
18181821
},
18191822
options: vec![],
18201823
legacy_options: vec![],
1821-
values: vec![],
1824+
payload: None,
18221825
}
18231826
)
18241827
}
@@ -1849,7 +1852,7 @@ fn parse_copy_from_before_v9_0() {
18491852
CopyLegacyCsvOption::ForceNotNull(vec!["column".into()]),
18501853
]),
18511854
],
1852-
values: vec![],
1855+
payload: None,
18531856
}
18541857
);
18551858

@@ -1875,7 +1878,7 @@ fn parse_copy_from_before_v9_0() {
18751878
CopyLegacyCsvOption::Escape('\\'),
18761879
]),
18771880
],
1878-
values: vec![],
1881+
payload: None,
18791882
}
18801883
);
18811884
}
@@ -1906,7 +1909,7 @@ fn parse_copy_to_before_v9_0() {
19061909
CopyLegacyCsvOption::ForceQuote(vec!["column".into()]),
19071910
]),
19081911
],
1909-
values: vec![],
1912+
payload: None,
19101913
}
19111914
)
19121915
}
@@ -10022,3 +10025,68 @@ fn parse_pg_roots_render_apart_from_operand() {
1002210025
pg().verified_stmt("SELECT ||/ -2");
1002310026
pg().verified_stmt("SELECT |/ ||/ 2");
1002410027
}
10028+
10029+
#[test]
10030+
fn parse_copy_from_stdin_payload_is_captured_as_written() {
10031+
// Reported symptoms: the payload arrived as a flat list of fields with no
10032+
// row separator, so two rows of two columns came back as six values, and
10033+
// printing a parsed statement never reproduced its input.
10034+
let check = |sql: &str, printed: &str, expected: Option<&str>| match pg_and_generic()
10035+
.one_statement_parses_to(sql, printed)
10036+
{
10037+
Statement::Copy { payload, .. } => {
10038+
assert_eq!(payload.as_deref(), expected, "for {sql:?}")
10039+
}
10040+
other => panic!("expected COPY, got {other:?}"),
10041+
};
10042+
10043+
// Tabs, newlines and the `\N` null marker are all left uninterpreted.
10044+
let rows = "COPY t (a, b) FROM STDIN;\n1\t\\N\n2\ty\n\\.";
10045+
check(rows, rows, Some("1\t\\N\n2\ty\n"));
10046+
10047+
// Only the keyword casing is normalized.
10048+
check(
10049+
"COPY t (a, b) FROM stdin;\n1\t\\N\n2\ty\n\\.",
10050+
rows,
10051+
Some("1\t\\N\n2\ty\n"),
10052+
);
10053+
10054+
// An empty payload is still a payload, distinct from a `COPY` that carries
10055+
// no inline data at all.
10056+
let empty = "COPY t (a, b) FROM STDIN;\n\\.";
10057+
check(empty, empty, Some(""));
10058+
10059+
// A statement delimiter with nothing behind it is not a payload, so
10060+
// printing must not invent data and a `\.` terminator.
10061+
check(
10062+
"COPY t (a, b) FROM STDIN;",
10063+
"COPY t (a, b) FROM STDIN",
10064+
None,
10065+
);
10066+
check(
10067+
"COPY t (a, b) FROM STDIN;\n",
10068+
"COPY t (a, b) FROM STDIN",
10069+
None,
10070+
);
10071+
10072+
// A payload cut short keeps the rows it does have, and printing restores
10073+
// the terminator the input was missing.
10074+
check(
10075+
"COPY t (a, b) FROM STDIN;\n1\t2\n",
10076+
"COPY t (a, b) FROM STDIN;\n1\t2\n\\.",
10077+
Some("1\t2\n"),
10078+
);
10079+
10080+
// The declared format does not change how the payload is captured.
10081+
let csv = "COPY t (a, b) FROM STDIN (FORMAT CSV);\na,b\n1,\"x y\"\n\\.";
10082+
check(csv, csv, Some("a,b\n1,\"x y\"\n"));
10083+
}
10084+
10085+
#[test]
10086+
fn parse_copy_from_stdin_payload_with_comment_marker() {
10087+
// A `--` in the payload is data, but the tokenizer still reports it as a
10088+
// comment, and printing that token appends a newline the tokenizer already
10089+
// emitted separately.
10090+
pg_and_generic().verified_stmt("COPY t (a, b) FROM STDIN;\n1\t-- not a comment\n2\ty\n\\.");
10091+
pg_and_generic().verified_stmt("COPY t (a, b) FROM STDIN;\n1\t/* nor is this */\n\\.");
10092+
}

0 commit comments

Comments
 (0)