mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-22 06:54:07 +00:00
Add support for more postgres COPY
options (#446)
* implement parsing COPY statement * support COPY option syntax before PostgreSQL version 9.0 Signed-off-by: Runji Wang <wangrunji0408@163.com> * update COPY tests Signed-off-by: Runji Wang <wangrunji0408@163.com> * improve docs for COPY Signed-off-by: Runji Wang <wangrunji0408@163.com> * test and fix AS in COPY Signed-off-by: Runji Wang <wangrunji0408@163.com> * recover original test cases * fix cargo clippy
This commit is contained in:
parent
fd8f2df10d
commit
bfd416d978
4 changed files with 545 additions and 124 deletions
195
src/ast/mod.rs
195
src/ast/mod.rs
|
@ -741,16 +741,16 @@ pub enum Statement {
|
||||||
table_name: ObjectName,
|
table_name: ObjectName,
|
||||||
/// COLUMNS
|
/// COLUMNS
|
||||||
columns: Vec<Ident>,
|
columns: Vec<Ident>,
|
||||||
/// VALUES a vector of values to be copied
|
|
||||||
values: Vec<Option<String>>,
|
|
||||||
/// file name of the data to be copied from
|
|
||||||
filename: Option<Ident>,
|
|
||||||
/// delimiter character
|
|
||||||
delimiter: Option<Ident>,
|
|
||||||
/// CSV HEADER
|
|
||||||
csv_header: bool,
|
|
||||||
/// If true, is a 'COPY TO' statement. If false is a 'COPY FROM'
|
/// If true, is a 'COPY TO' statement. If false is a 'COPY FROM'
|
||||||
to: bool,
|
to: bool,
|
||||||
|
/// The source of 'COPY FROM', or the target of 'COPY TO'
|
||||||
|
target: CopyTarget,
|
||||||
|
/// WITH options (from PostgreSQL version 9.0)
|
||||||
|
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>>,
|
||||||
},
|
},
|
||||||
/// UPDATE
|
/// UPDATE
|
||||||
Update {
|
Update {
|
||||||
|
@ -1143,37 +1143,25 @@ impl fmt::Display for Statement {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name,
|
table_name,
|
||||||
columns,
|
columns,
|
||||||
values,
|
|
||||||
delimiter,
|
|
||||||
filename,
|
|
||||||
csv_header,
|
|
||||||
to,
|
to,
|
||||||
|
target,
|
||||||
|
options,
|
||||||
|
legacy_options,
|
||||||
|
values,
|
||||||
} => {
|
} => {
|
||||||
write!(f, "COPY {}", table_name)?;
|
write!(f, "COPY {}", table_name)?;
|
||||||
if !columns.is_empty() {
|
if !columns.is_empty() {
|
||||||
write!(f, " ({})", display_comma_separated(columns))?;
|
write!(f, " ({})", display_comma_separated(columns))?;
|
||||||
}
|
}
|
||||||
|
write!(f, " {} {}", if *to { "TO" } else { "FROM" }, target)?;
|
||||||
if let Some(name) = filename {
|
if !options.is_empty() {
|
||||||
if *to {
|
write!(f, " ({})", display_comma_separated(options))?;
|
||||||
write!(f, " TO {}", name)?
|
|
||||||
} else {
|
|
||||||
write!(f, " FROM {}", name)?;
|
|
||||||
}
|
}
|
||||||
} else if *to {
|
if !legacy_options.is_empty() {
|
||||||
write!(f, " TO stdin ")?
|
write!(f, " {}", display_separated(legacy_options, " "))?;
|
||||||
} else {
|
|
||||||
write!(f, " FROM stdin ")?;
|
|
||||||
}
|
|
||||||
if let Some(delimiter) = delimiter {
|
|
||||||
write!(f, " DELIMITER {}", delimiter)?;
|
|
||||||
}
|
|
||||||
if *csv_header {
|
|
||||||
write!(f, " CSV HEADER")?;
|
|
||||||
}
|
}
|
||||||
if !values.is_empty() {
|
if !values.is_empty() {
|
||||||
write!(f, ";")?;
|
writeln!(f, ";")?;
|
||||||
writeln!(f)?;
|
|
||||||
let mut delim = "";
|
let mut delim = "";
|
||||||
for v in values {
|
for v in values {
|
||||||
write!(f, "{}", delim)?;
|
write!(f, "{}", delim)?;
|
||||||
|
@ -1184,8 +1172,6 @@ impl fmt::Display for Statement {
|
||||||
write!(f, "\\N")?;
|
write!(f, "\\N")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if filename.is_none() {
|
|
||||||
write!(f, "\n\\.")?;
|
write!(f, "\n\\.")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -2185,6 +2171,151 @@ impl fmt::Display for SqliteOnConflict {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum CopyTarget {
|
||||||
|
Stdin,
|
||||||
|
Stdout,
|
||||||
|
File {
|
||||||
|
/// The path name of the input or output file.
|
||||||
|
filename: String,
|
||||||
|
},
|
||||||
|
Program {
|
||||||
|
/// A command to execute
|
||||||
|
command: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CopyTarget {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use CopyTarget::*;
|
||||||
|
match self {
|
||||||
|
Stdin { .. } => write!(f, "STDIN"),
|
||||||
|
Stdout => write!(f, "STDOUT"),
|
||||||
|
File { filename } => write!(f, "'{}'", value::escape_single_quote_string(filename)),
|
||||||
|
Program { command } => write!(
|
||||||
|
f,
|
||||||
|
"PROGRAM '{}'",
|
||||||
|
value::escape_single_quote_string(command)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An option in `COPY` statement.
|
||||||
|
///
|
||||||
|
/// <https://www.postgresql.org/docs/14/sql-copy.html>
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum CopyOption {
|
||||||
|
/// FORMAT format_name
|
||||||
|
Format(Ident),
|
||||||
|
/// FREEZE \[ boolean \]
|
||||||
|
Freeze(bool),
|
||||||
|
/// DELIMITER 'delimiter_character'
|
||||||
|
Delimiter(char),
|
||||||
|
/// NULL 'null_string'
|
||||||
|
Null(String),
|
||||||
|
/// HEADER \[ boolean \]
|
||||||
|
Header(bool),
|
||||||
|
/// QUOTE 'quote_character'
|
||||||
|
Quote(char),
|
||||||
|
/// ESCAPE 'escape_character'
|
||||||
|
Escape(char),
|
||||||
|
/// FORCE_QUOTE { ( column_name [, ...] ) | * }
|
||||||
|
ForceQuote(Vec<Ident>),
|
||||||
|
/// FORCE_NOT_NULL ( column_name [, ...] )
|
||||||
|
ForceNotNull(Vec<Ident>),
|
||||||
|
/// FORCE_NULL ( column_name [, ...] )
|
||||||
|
ForceNull(Vec<Ident>),
|
||||||
|
/// ENCODING 'encoding_name'
|
||||||
|
Encoding(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CopyOption {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use CopyOption::*;
|
||||||
|
match self {
|
||||||
|
Format(name) => write!(f, "FORMAT {}", name),
|
||||||
|
Freeze(true) => write!(f, "FREEZE"),
|
||||||
|
Freeze(false) => write!(f, "FREEZE FALSE"),
|
||||||
|
Delimiter(char) => write!(f, "DELIMITER '{}'", char),
|
||||||
|
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
|
||||||
|
Header(true) => write!(f, "HEADER"),
|
||||||
|
Header(false) => write!(f, "HEADER FALSE"),
|
||||||
|
Quote(char) => write!(f, "QUOTE '{}'", char),
|
||||||
|
Escape(char) => write!(f, "ESCAPE '{}'", char),
|
||||||
|
ForceQuote(columns) => write!(f, "FORCE_QUOTE ({})", display_comma_separated(columns)),
|
||||||
|
ForceNotNull(columns) => {
|
||||||
|
write!(f, "FORCE_NOT_NULL ({})", display_comma_separated(columns))
|
||||||
|
}
|
||||||
|
ForceNull(columns) => write!(f, "FORCE_NULL ({})", display_comma_separated(columns)),
|
||||||
|
Encoding(name) => write!(f, "ENCODING '{}'", value::escape_single_quote_string(name)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An option in `COPY` statement before PostgreSQL version 9.0.
|
||||||
|
///
|
||||||
|
/// <https://www.postgresql.org/docs/8.4/sql-copy.html>
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum CopyLegacyOption {
|
||||||
|
/// BINARY
|
||||||
|
Binary,
|
||||||
|
/// DELIMITER \[ AS \] 'delimiter_character'
|
||||||
|
Delimiter(char),
|
||||||
|
/// NULL \[ AS \] 'null_string'
|
||||||
|
Null(String),
|
||||||
|
/// CSV ...
|
||||||
|
Csv(Vec<CopyLegacyCsvOption>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CopyLegacyOption {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use CopyLegacyOption::*;
|
||||||
|
match self {
|
||||||
|
Binary => write!(f, "BINARY"),
|
||||||
|
Delimiter(char) => write!(f, "DELIMITER '{}'", char),
|
||||||
|
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
|
||||||
|
Csv(opts) => write!(f, "CSV {}", display_separated(opts, " ")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `CSV` option in `COPY` statement before PostgreSQL version 9.0.
|
||||||
|
///
|
||||||
|
/// <https://www.postgresql.org/docs/8.4/sql-copy.html>
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum CopyLegacyCsvOption {
|
||||||
|
/// HEADER
|
||||||
|
Header,
|
||||||
|
/// QUOTE \[ AS \] 'quote_character'
|
||||||
|
Quote(char),
|
||||||
|
/// ESCAPE \[ AS \] 'escape_character'
|
||||||
|
Escape(char),
|
||||||
|
/// FORCE QUOTE { column_name [, ...] | * }
|
||||||
|
ForceQuote(Vec<Ident>),
|
||||||
|
/// FORCE NOT NULL column_name [, ...]
|
||||||
|
ForceNotNull(Vec<Ident>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CopyLegacyCsvOption {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
use CopyLegacyCsvOption::*;
|
||||||
|
match self {
|
||||||
|
Header => write!(f, "HEADER"),
|
||||||
|
Quote(char) => write!(f, "QUOTE '{}'", char),
|
||||||
|
Escape(char) => write!(f, "ESCAPE '{}'", char),
|
||||||
|
ForceQuote(columns) => write!(f, "FORCE QUOTE {}", display_comma_separated(columns)),
|
||||||
|
ForceNotNull(columns) => {
|
||||||
|
write!(f, "FORCE NOT NULL {}", display_comma_separated(columns))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
|
|
@ -195,6 +195,7 @@ define_keywords!(
|
||||||
EACH,
|
EACH,
|
||||||
ELEMENT,
|
ELEMENT,
|
||||||
ELSE,
|
ELSE,
|
||||||
|
ENCODING,
|
||||||
END,
|
END,
|
||||||
END_EXEC = "END-EXEC",
|
END_EXEC = "END-EXEC",
|
||||||
END_FRAME,
|
END_FRAME,
|
||||||
|
@ -227,10 +228,15 @@ define_keywords!(
|
||||||
FLOOR,
|
FLOOR,
|
||||||
FOLLOWING,
|
FOLLOWING,
|
||||||
FOR,
|
FOR,
|
||||||
|
FORCE,
|
||||||
|
FORCE_NOT_NULL,
|
||||||
|
FORCE_NULL,
|
||||||
|
FORCE_QUOTE,
|
||||||
FOREIGN,
|
FOREIGN,
|
||||||
FORMAT,
|
FORMAT,
|
||||||
FRAME_ROW,
|
FRAME_ROW,
|
||||||
FREE,
|
FREE,
|
||||||
|
FREEZE,
|
||||||
FROM,
|
FROM,
|
||||||
FULL,
|
FULL,
|
||||||
FUNCTION,
|
FUNCTION,
|
||||||
|
@ -372,8 +378,10 @@ define_keywords!(
|
||||||
PRIMARY,
|
PRIMARY,
|
||||||
PRIVILEGES,
|
PRIVILEGES,
|
||||||
PROCEDURE,
|
PROCEDURE,
|
||||||
|
PROGRAM,
|
||||||
PURGE,
|
PURGE,
|
||||||
QUARTER,
|
QUARTER,
|
||||||
|
QUOTE,
|
||||||
RANGE,
|
RANGE,
|
||||||
RANK,
|
RANK,
|
||||||
RCFILE,
|
RCFILE,
|
||||||
|
@ -449,6 +457,7 @@ define_keywords!(
|
||||||
STDDEV_POP,
|
STDDEV_POP,
|
||||||
STDDEV_SAMP,
|
STDDEV_SAMP,
|
||||||
STDIN,
|
STDIN,
|
||||||
|
STDOUT,
|
||||||
STORED,
|
STORED,
|
||||||
STRING,
|
STRING,
|
||||||
SUBMULTISET,
|
SUBMULTISET,
|
||||||
|
|
177
src/parser.rs
177
src/parser.rs
|
@ -2235,53 +2235,162 @@ impl<'a> Parser<'a> {
|
||||||
pub fn parse_copy(&mut self) -> Result<Statement, ParserError> {
|
pub fn parse_copy(&mut self) -> Result<Statement, ParserError> {
|
||||||
let table_name = self.parse_object_name()?;
|
let table_name = self.parse_object_name()?;
|
||||||
let columns = self.parse_parenthesized_column_list(Optional)?;
|
let columns = self.parse_parenthesized_column_list(Optional)?;
|
||||||
let to_or_from = self.expect_one_of_keywords(&[Keyword::FROM, Keyword::TO])?;
|
let to = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::TO]) {
|
||||||
let to: bool = match to_or_from {
|
Some(Keyword::FROM) => false,
|
||||||
Keyword::TO => true,
|
Some(Keyword::TO) => true,
|
||||||
Keyword::FROM => false,
|
_ => self.expected("FROM or TO", self.peek_token())?,
|
||||||
_ => unreachable!("something wrong while parsing copy statment :("),
|
|
||||||
};
|
};
|
||||||
let mut filename = None;
|
let target = if self.parse_keyword(Keyword::STDIN) {
|
||||||
// check whether data has to be copied form table or std in.
|
CopyTarget::Stdin
|
||||||
if !self.parse_keyword(Keyword::STDIN) {
|
} else if self.parse_keyword(Keyword::STDOUT) {
|
||||||
filename = Some(self.parse_identifier()?)
|
CopyTarget::Stdout
|
||||||
|
} else if self.parse_keyword(Keyword::PROGRAM) {
|
||||||
|
CopyTarget::Program {
|
||||||
|
command: self.parse_literal_string()?,
|
||||||
}
|
}
|
||||||
// parse copy options.
|
} else {
|
||||||
let mut delimiter = None;
|
CopyTarget::File {
|
||||||
let mut csv_header = false;
|
filename: self.parse_literal_string()?,
|
||||||
loop {
|
|
||||||
if let Some(keyword) = self.parse_one_of_keywords(&[Keyword::DELIMITER, Keyword::CSV]) {
|
|
||||||
match keyword {
|
|
||||||
Keyword::DELIMITER => {
|
|
||||||
delimiter = Some(self.parse_identifier()?);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
Keyword::CSV => {
|
};
|
||||||
self.expect_keyword(Keyword::HEADER)?;
|
let _ = self.parse_keyword(Keyword::WITH);
|
||||||
csv_header = true
|
let mut options = vec![];
|
||||||
|
if self.consume_token(&Token::LParen) {
|
||||||
|
options = self.parse_comma_separated(Parser::parse_copy_option)?;
|
||||||
|
self.expect_token(&Token::RParen)?;
|
||||||
}
|
}
|
||||||
_ => unreachable!("something wrong while parsing copy statment :("),
|
let mut legacy_options = vec![];
|
||||||
|
while let Some(opt) = self.maybe_parse(|parser| parser.parse_copy_legacy_option()) {
|
||||||
|
legacy_options.push(opt);
|
||||||
}
|
}
|
||||||
}
|
let values = if let CopyTarget::Stdin = target {
|
||||||
break;
|
|
||||||
}
|
|
||||||
// copy the values from stdin if there is no file to be copied from.
|
|
||||||
let mut values = vec![];
|
|
||||||
if filename.is_none() {
|
|
||||||
self.expect_token(&Token::SemiColon)?;
|
self.expect_token(&Token::SemiColon)?;
|
||||||
values = self.parse_tsv();
|
self.parse_tsv()
|
||||||
}
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
Ok(Statement::Copy {
|
Ok(Statement::Copy {
|
||||||
table_name,
|
table_name,
|
||||||
columns,
|
columns,
|
||||||
values,
|
|
||||||
filename,
|
|
||||||
delimiter,
|
|
||||||
csv_header,
|
|
||||||
to,
|
to,
|
||||||
|
target,
|
||||||
|
options,
|
||||||
|
legacy_options,
|
||||||
|
values,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_copy_option(&mut self) -> Result<CopyOption, ParserError> {
|
||||||
|
let ret = match self.parse_one_of_keywords(&[
|
||||||
|
Keyword::FORMAT,
|
||||||
|
Keyword::FREEZE,
|
||||||
|
Keyword::DELIMITER,
|
||||||
|
Keyword::NULL,
|
||||||
|
Keyword::HEADER,
|
||||||
|
Keyword::QUOTE,
|
||||||
|
Keyword::ESCAPE,
|
||||||
|
Keyword::FORCE_QUOTE,
|
||||||
|
Keyword::FORCE_NOT_NULL,
|
||||||
|
Keyword::FORCE_NULL,
|
||||||
|
Keyword::ENCODING,
|
||||||
|
]) {
|
||||||
|
Some(Keyword::FORMAT) => CopyOption::Format(self.parse_identifier()?),
|
||||||
|
Some(Keyword::FREEZE) => CopyOption::Freeze(!matches!(
|
||||||
|
self.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]),
|
||||||
|
Some(Keyword::FALSE)
|
||||||
|
)),
|
||||||
|
Some(Keyword::DELIMITER) => CopyOption::Delimiter(self.parse_literal_char()?),
|
||||||
|
Some(Keyword::NULL) => CopyOption::Null(self.parse_literal_string()?),
|
||||||
|
Some(Keyword::HEADER) => CopyOption::Header(!matches!(
|
||||||
|
self.parse_one_of_keywords(&[Keyword::TRUE, Keyword::FALSE]),
|
||||||
|
Some(Keyword::FALSE)
|
||||||
|
)),
|
||||||
|
Some(Keyword::QUOTE) => CopyOption::Quote(self.parse_literal_char()?),
|
||||||
|
Some(Keyword::ESCAPE) => CopyOption::Escape(self.parse_literal_char()?),
|
||||||
|
Some(Keyword::FORCE_QUOTE) => {
|
||||||
|
CopyOption::ForceQuote(self.parse_parenthesized_column_list(Mandatory)?)
|
||||||
|
}
|
||||||
|
Some(Keyword::FORCE_NOT_NULL) => {
|
||||||
|
CopyOption::ForceNotNull(self.parse_parenthesized_column_list(Mandatory)?)
|
||||||
|
}
|
||||||
|
Some(Keyword::FORCE_NULL) => {
|
||||||
|
CopyOption::ForceNull(self.parse_parenthesized_column_list(Mandatory)?)
|
||||||
|
}
|
||||||
|
Some(Keyword::ENCODING) => CopyOption::Encoding(self.parse_literal_string()?),
|
||||||
|
_ => self.expected("option", self.peek_token())?,
|
||||||
|
};
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_copy_legacy_option(&mut self) -> Result<CopyLegacyOption, ParserError> {
|
||||||
|
let ret = match self.parse_one_of_keywords(&[
|
||||||
|
Keyword::BINARY,
|
||||||
|
Keyword::DELIMITER,
|
||||||
|
Keyword::NULL,
|
||||||
|
Keyword::CSV,
|
||||||
|
]) {
|
||||||
|
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
|
||||||
|
Some(Keyword::DELIMITER) => {
|
||||||
|
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
|
||||||
|
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
|
||||||
|
}
|
||||||
|
Some(Keyword::NULL) => {
|
||||||
|
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
|
||||||
|
CopyLegacyOption::Null(self.parse_literal_string()?)
|
||||||
|
}
|
||||||
|
Some(Keyword::CSV) => CopyLegacyOption::Csv({
|
||||||
|
let mut opts = vec![];
|
||||||
|
while let Some(opt) =
|
||||||
|
self.maybe_parse(|parser| parser.parse_copy_legacy_csv_option())
|
||||||
|
{
|
||||||
|
opts.push(opt);
|
||||||
|
}
|
||||||
|
opts
|
||||||
|
}),
|
||||||
|
_ => self.expected("option", self.peek_token())?,
|
||||||
|
};
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_copy_legacy_csv_option(&mut self) -> Result<CopyLegacyCsvOption, ParserError> {
|
||||||
|
let ret = match self.parse_one_of_keywords(&[
|
||||||
|
Keyword::HEADER,
|
||||||
|
Keyword::QUOTE,
|
||||||
|
Keyword::ESCAPE,
|
||||||
|
Keyword::FORCE,
|
||||||
|
]) {
|
||||||
|
Some(Keyword::HEADER) => CopyLegacyCsvOption::Header,
|
||||||
|
Some(Keyword::QUOTE) => {
|
||||||
|
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
|
||||||
|
CopyLegacyCsvOption::Quote(self.parse_literal_char()?)
|
||||||
|
}
|
||||||
|
Some(Keyword::ESCAPE) => {
|
||||||
|
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
|
||||||
|
CopyLegacyCsvOption::Escape(self.parse_literal_char()?)
|
||||||
|
}
|
||||||
|
Some(Keyword::FORCE) if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) => {
|
||||||
|
CopyLegacyCsvOption::ForceNotNull(
|
||||||
|
self.parse_comma_separated(Parser::parse_identifier)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Some(Keyword::FORCE) if self.parse_keywords(&[Keyword::QUOTE]) => {
|
||||||
|
CopyLegacyCsvOption::ForceQuote(
|
||||||
|
self.parse_comma_separated(Parser::parse_identifier)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => self.expected("csv option", self.peek_token())?,
|
||||||
|
};
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_literal_char(&mut self) -> Result<char, ParserError> {
|
||||||
|
let s = self.parse_literal_string()?;
|
||||||
|
if s.len() != 1 {
|
||||||
|
return parser_err!(format!("Expect a char, found {:?}", s));
|
||||||
|
}
|
||||||
|
Ok(s.chars().next().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a tab separated values in
|
/// Parse a tab separated values in
|
||||||
/// COPY payload
|
/// COPY payload
|
||||||
pub fn parse_tsv(&mut self) -> Vec<Option<String>> {
|
pub fn parse_tsv(&mut self) -> Vec<Option<String>> {
|
||||||
|
|
|
@ -375,7 +375,7 @@ fn parse_drop_schema_if_exists() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_copy_example() {
|
fn parse_copy_from_stdin() {
|
||||||
let sql = r#"COPY public.actor (actor_id, first_name, last_name, last_update, value) FROM stdin;
|
let sql = r#"COPY public.actor (actor_id, first_name, last_name, last_update, value) FROM stdin;
|
||||||
1 PENELOPE GUINESS 2006-02-15 09:34:33 0.11111
|
1 PENELOPE GUINESS 2006-02-15 09:34:33 0.11111
|
||||||
2 NICK WAHLBERG 2006-02-15 09:34:33 0.22222
|
2 NICK WAHLBERG 2006-02-15 09:34:33 0.22222
|
||||||
|
@ -487,14 +487,13 @@ fn test_copy_from() {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name: ObjectName(vec!["users".into()]),
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
columns: vec![],
|
columns: vec![],
|
||||||
filename: Some(Ident {
|
to: false,
|
||||||
value: "data.csv".to_string(),
|
target: CopyTarget::File {
|
||||||
quote_style: Some('\'')
|
filename: "data.csv".to_string(),
|
||||||
}),
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![],
|
||||||
values: vec![],
|
values: vec![],
|
||||||
delimiter: None,
|
|
||||||
csv_header: false,
|
|
||||||
to: false
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -504,17 +503,13 @@ fn test_copy_from() {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name: ObjectName(vec!["users".into()]),
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
columns: vec![],
|
columns: vec![],
|
||||||
filename: Some(Ident {
|
to: false,
|
||||||
value: "data.csv".to_string(),
|
target: CopyTarget::File {
|
||||||
quote_style: Some('\'')
|
filename: "data.csv".to_string(),
|
||||||
}),
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
|
||||||
values: vec![],
|
values: vec![],
|
||||||
delimiter: Some(Ident {
|
|
||||||
value: ",".to_string(),
|
|
||||||
quote_style: Some('\'')
|
|
||||||
}),
|
|
||||||
csv_header: false,
|
|
||||||
to: false
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -524,19 +519,18 @@ fn test_copy_from() {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name: ObjectName(vec!["users".into()]),
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
columns: vec![],
|
columns: vec![],
|
||||||
filename: Some(Ident {
|
to: false,
|
||||||
value: "data.csv".to_string(),
|
target: CopyTarget::File {
|
||||||
quote_style: Some('\'')
|
filename: "data.csv".to_string(),
|
||||||
}),
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![
|
||||||
|
CopyLegacyOption::Delimiter(','),
|
||||||
|
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
|
||||||
|
],
|
||||||
values: vec![],
|
values: vec![],
|
||||||
delimiter: Some(Ident {
|
|
||||||
value: ",".to_string(),
|
|
||||||
quote_style: Some('\'')
|
|
||||||
}),
|
|
||||||
csv_header: true,
|
|
||||||
to: false
|
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -547,14 +541,13 @@ fn test_copy_to() {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name: ObjectName(vec!["users".into()]),
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
columns: vec![],
|
columns: vec![],
|
||||||
filename: Some(Ident {
|
to: true,
|
||||||
value: "data.csv".to_string(),
|
target: CopyTarget::File {
|
||||||
quote_style: Some('\'')
|
filename: "data.csv".to_string(),
|
||||||
}),
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![],
|
||||||
values: vec![],
|
values: vec![],
|
||||||
delimiter: None,
|
|
||||||
csv_header: false,
|
|
||||||
to: true
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -564,17 +557,13 @@ fn test_copy_to() {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name: ObjectName(vec!["users".into()]),
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
columns: vec![],
|
columns: vec![],
|
||||||
filename: Some(Ident {
|
to: true,
|
||||||
value: "data.csv".to_string(),
|
target: CopyTarget::File {
|
||||||
quote_style: Some('\'')
|
filename: "data.csv".to_string(),
|
||||||
}),
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
|
||||||
values: vec![],
|
values: vec![],
|
||||||
delimiter: Some(Ident {
|
|
||||||
value: ",".to_string(),
|
|
||||||
quote_style: Some('\'')
|
|
||||||
}),
|
|
||||||
csv_header: false,
|
|
||||||
to: true
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -584,17 +573,200 @@ fn test_copy_to() {
|
||||||
Statement::Copy {
|
Statement::Copy {
|
||||||
table_name: ObjectName(vec!["users".into()]),
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
columns: vec![],
|
columns: vec![],
|
||||||
filename: Some(Ident {
|
to: true,
|
||||||
value: "data.csv".to_string(),
|
target: CopyTarget::File {
|
||||||
quote_style: Some('\'')
|
filename: "data.csv".to_string(),
|
||||||
}),
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![
|
||||||
|
CopyLegacyOption::Delimiter(','),
|
||||||
|
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
|
||||||
|
],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_copy_from() {
|
||||||
|
let sql = "COPY table (a, b) FROM 'file.csv' WITH
|
||||||
|
(
|
||||||
|
FORMAT CSV,
|
||||||
|
FREEZE,
|
||||||
|
FREEZE TRUE,
|
||||||
|
FREEZE FALSE,
|
||||||
|
DELIMITER ',',
|
||||||
|
NULL '',
|
||||||
|
HEADER,
|
||||||
|
HEADER TRUE,
|
||||||
|
HEADER FALSE,
|
||||||
|
QUOTE '\"',
|
||||||
|
ESCAPE '\\',
|
||||||
|
FORCE_QUOTE (a, b),
|
||||||
|
FORCE_NOT_NULL (a),
|
||||||
|
FORCE_NULL (b),
|
||||||
|
ENCODING 'utf8'
|
||||||
|
)";
|
||||||
|
assert_eq!(
|
||||||
|
pg_and_generic().one_statement_parses_to(sql, ""),
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["table".into()]),
|
||||||
|
columns: vec!["a".into(), "b".into()],
|
||||||
|
to: false,
|
||||||
|
target: CopyTarget::File {
|
||||||
|
filename: "file.csv".into()
|
||||||
|
},
|
||||||
|
options: vec![
|
||||||
|
CopyOption::Format("CSV".into()),
|
||||||
|
CopyOption::Freeze(true),
|
||||||
|
CopyOption::Freeze(true),
|
||||||
|
CopyOption::Freeze(false),
|
||||||
|
CopyOption::Delimiter(','),
|
||||||
|
CopyOption::Null("".into()),
|
||||||
|
CopyOption::Header(true),
|
||||||
|
CopyOption::Header(true),
|
||||||
|
CopyOption::Header(false),
|
||||||
|
CopyOption::Quote('"'),
|
||||||
|
CopyOption::Escape('\\'),
|
||||||
|
CopyOption::ForceQuote(vec!["a".into(), "b".into()]),
|
||||||
|
CopyOption::ForceNotNull(vec!["a".into()]),
|
||||||
|
CopyOption::ForceNull(vec!["b".into()]),
|
||||||
|
CopyOption::Encoding("utf8".into()),
|
||||||
|
],
|
||||||
|
legacy_options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_copy_to() {
|
||||||
|
let stmt = pg().verified_stmt("COPY users TO 'data.csv'");
|
||||||
|
assert_eq!(
|
||||||
|
stmt,
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
|
columns: vec![],
|
||||||
|
to: true,
|
||||||
|
target: CopyTarget::File {
|
||||||
|
filename: "data.csv".to_string(),
|
||||||
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let stmt = pg().verified_stmt("COPY country TO STDOUT (DELIMITER '|')");
|
||||||
|
assert_eq!(
|
||||||
|
stmt,
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["country".into()]),
|
||||||
|
columns: vec![],
|
||||||
|
to: true,
|
||||||
|
target: CopyTarget::Stdout,
|
||||||
|
options: vec![CopyOption::Delimiter('|')],
|
||||||
|
legacy_options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let stmt =
|
||||||
|
pg().verified_stmt("COPY country TO PROGRAM 'gzip > /usr1/proj/bray/sql/country_data.gz'");
|
||||||
|
assert_eq!(
|
||||||
|
stmt,
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["country".into()]),
|
||||||
|
columns: vec![],
|
||||||
|
to: true,
|
||||||
|
target: CopyTarget::Program {
|
||||||
|
command: "gzip > /usr1/proj/bray/sql/country_data.gz".into(),
|
||||||
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_copy_from_before_v9_0() {
|
||||||
|
let stmt = pg().verified_stmt("COPY users FROM 'data.csv' BINARY DELIMITER ',' NULL 'null' CSV HEADER QUOTE '\"' ESCAPE '\\' FORCE NOT NULL column");
|
||||||
|
assert_eq!(
|
||||||
|
stmt,
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
|
columns: vec![],
|
||||||
|
to: false,
|
||||||
|
target: CopyTarget::File {
|
||||||
|
filename: "data.csv".to_string(),
|
||||||
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![
|
||||||
|
CopyLegacyOption::Binary,
|
||||||
|
CopyLegacyOption::Delimiter(','),
|
||||||
|
CopyLegacyOption::Null("null".into()),
|
||||||
|
CopyLegacyOption::Csv(vec![
|
||||||
|
CopyLegacyCsvOption::Header,
|
||||||
|
CopyLegacyCsvOption::Quote('\"'),
|
||||||
|
CopyLegacyCsvOption::Escape('\\'),
|
||||||
|
CopyLegacyCsvOption::ForceNotNull(vec!["column".into()]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// test 'AS' keyword
|
||||||
|
let sql = "COPY users FROM 'data.csv' DELIMITER AS ',' NULL AS 'null' CSV QUOTE AS '\"' ESCAPE AS '\\'";
|
||||||
|
assert_eq!(
|
||||||
|
pg_and_generic().one_statement_parses_to(sql, ""),
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
|
columns: vec![],
|
||||||
|
to: false,
|
||||||
|
target: CopyTarget::File {
|
||||||
|
filename: "data.csv".to_string(),
|
||||||
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![
|
||||||
|
CopyLegacyOption::Delimiter(','),
|
||||||
|
CopyLegacyOption::Null("null".into()),
|
||||||
|
CopyLegacyOption::Csv(vec![
|
||||||
|
CopyLegacyCsvOption::Quote('\"'),
|
||||||
|
CopyLegacyCsvOption::Escape('\\'),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
values: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_copy_to_before_v9_0() {
|
||||||
|
let stmt = pg().verified_stmt("COPY users TO 'data.csv' BINARY DELIMITER ',' NULL 'null' CSV HEADER QUOTE '\"' ESCAPE '\\' FORCE QUOTE column");
|
||||||
|
assert_eq!(
|
||||||
|
stmt,
|
||||||
|
Statement::Copy {
|
||||||
|
table_name: ObjectName(vec!["users".into()]),
|
||||||
|
columns: vec![],
|
||||||
|
to: true,
|
||||||
|
target: CopyTarget::File {
|
||||||
|
filename: "data.csv".to_string(),
|
||||||
|
},
|
||||||
|
options: vec![],
|
||||||
|
legacy_options: vec![
|
||||||
|
CopyLegacyOption::Binary,
|
||||||
|
CopyLegacyOption::Delimiter(','),
|
||||||
|
CopyLegacyOption::Null("null".into()),
|
||||||
|
CopyLegacyOption::Csv(vec![
|
||||||
|
CopyLegacyCsvOption::Header,
|
||||||
|
CopyLegacyCsvOption::Quote('\"'),
|
||||||
|
CopyLegacyCsvOption::Escape('\\'),
|
||||||
|
CopyLegacyCsvOption::ForceQuote(vec!["column".into()]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
values: vec![],
|
values: vec![],
|
||||||
delimiter: Some(Ident {
|
|
||||||
value: ",".to_string(),
|
|
||||||
quote_style: Some('\'')
|
|
||||||
}),
|
|
||||||
csv_header: true,
|
|
||||||
to: true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue