Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3694,8 +3694,10 @@ pub enum Statement {
options: Vec<CopyOption>,
/// WITH options (before PostgreSQL version 9.0)
legacy_options: Vec<CopyLegacyOption>,
/// VALUES a vector of values to be copied
values: Vec<Option<String>>,
/// The inline payload of `COPY ... FROM STDIN`, captured verbatim
/// from the source text. `None` when the statement carries no
/// inline data.
payload: Option<CopyPayload>,
},
/// ```sql
/// COPY INTO <table> | <location>
Expand Down Expand Up @@ -5375,7 +5377,7 @@ impl fmt::Display for Statement {
target,
options,
legacy_options,
values,
payload,
} => {
write!(f, "COPY")?;
match source {
Expand All @@ -5397,19 +5399,15 @@ impl fmt::Display for Statement {
if !legacy_options.is_empty() {
write!(f, " {}", display_separated(legacy_options, " "))?;
}
if !values.is_empty() {
if let Some(payload) = payload {
// The payload starts on the line after the command and ends
// at the `\.` terminator line, which is only written when
// present in the source.
writeln!(f, ";")?;
let mut delim = "";
for v in values {
write!(f, "{delim}")?;
delim = "\t";
if let Some(v) = v {
write!(f, "{v}")?;
} else {
write!(f, "\\N")?;
}
write!(f, "{}", payload.data)?;
if payload.terminated {
write!(f, "\\.")?;
}
write!(f, "\n\\.")?;
}
Ok(())
}
Expand Down Expand Up @@ -9558,6 +9556,20 @@ impl fmt::Display for CopyTarget {
}
}

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
/// The inline payload of a `COPY ... FROM STDIN` statement, captured
/// verbatim from the source text.
pub struct CopyPayload {
/// The payload as written, starting on the line after the command and
/// running up to the `\.` terminator line. Field separators, escapes and
/// the `\N` null marker are left for the caller to interpret.
pub data: String,
/// Whether the `\.` terminator line was present.
pub terminated: bool,
}

#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
Expand Down
2 changes: 1 addition & 1 deletion src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ impl Spanned for Statement {
target: _,
options: _,
legacy_options: _,
values: _,
payload: _,
} => source.span(),
Statement::CopyIntoSnowflake {
into: _,
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ impl Dialect for GenericDialect {
true
}

fn supports_copy_from_stdin_data(&self) -> bool {
true
}

fn supports_user_host_grantee(&self) -> bool {
true
}
Expand Down
6 changes: 6 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,12 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports inline data for
/// `COPY ... FROM STDIN`, captured verbatim by the tokenizer.
fn supports_copy_from_stdin_data(&self) -> bool {
false
}

/// Returns true if `#` starts a single line comment only when followed by an ASCII
/// space or `!`, so `# comment` and `#! comment` are comments but `#comment` is not.
fn requires_hash_comment_space(&self) -> bool {
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ impl Dialect for PostgreSqlDialect {
true
}

fn supports_copy_from_stdin_data(&self) -> bool {
true
}

fn supports_string_escape_constant(&self) -> bool {
true
}
Expand Down
61 changes: 17 additions & 44 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12194,23 +12194,32 @@ impl<'a> Parser<'a> {
while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option())? {
legacy_options.push(opt);
}
let values =
if matches!(target, CopyTarget::Stdin) && self.peek_token_ref().token != Token::EOF {
self.expect_token(&Token::SemiColon)?;
self.parse_tsv()
} else {
vec![]
};
let payload = if matches!(target, CopyTarget::Stdin) {
self.take_copy_payload()
} else {
None
};
Ok(Statement::Copy {
source,
to,
target,
options,
legacy_options,
values,
payload,
})
}

/// Take the raw inline payload of a `COPY ... FROM STDIN` statement, or
/// `None` when the statement carried no inline data.
fn take_copy_payload(&mut self) -> Option<CopyPayload> {
let Token::CopyData { data, terminated } = &self.peek_token_ref().token else {
return None;
};
let (data, terminated) = (data.clone(), *terminated);
self.next_token();
Some(CopyPayload { data, terminated })
}

/// Parse [Statement::Open]
fn parse_open(&mut self) -> Result<Statement, ParserError> {
self.expect_keyword(Keyword::OPEN)?;
Expand Down Expand Up @@ -12548,42 +12557,6 @@ impl<'a> Parser<'a> {
Ok(s.chars().next().unwrap())
}

/// Parse a tab separated values in
/// COPY payload
pub fn parse_tsv(&mut self) -> Vec<Option<String>> {
self.parse_tab_value()
}

/// Parse a single tab-separated value row used by `COPY` payload parsing.
pub fn parse_tab_value(&mut self) -> Vec<Option<String>> {
let mut values = vec![];
let mut content = String::new();
while let Some(t) = self.next_token_no_skip().map(|t| &t.token) {
match t {
Token::Whitespace(Whitespace::Tab) => {
values.push(Some(core::mem::take(&mut content)));
}
Token::Whitespace(Whitespace::Newline) => {
values.push(Some(core::mem::take(&mut content)));
}
Token::Backslash => {
if self.consume_token(&Token::Period) {
return values;
}
if let Token::Word(w) = self.next_token().token {
if w.value == "N" {
values.push(None);
}
}
}
_ => {
content.push_str(&t.to_string());
}
}
}
values
}

/// Parse a literal value (numbers, strings, date/time, booleans)
pub fn parse_value(&mut self) -> Result<ValueWithSpan, ParserError> {
let next_token = self.next_token();
Expand Down
Loading