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:
Runji Wang 2022-04-03 18:37:12 +08:00 committed by GitHub
parent fd8f2df10d
commit bfd416d978
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 545 additions and 124 deletions

View file

@ -741,16 +741,16 @@ pub enum Statement {
table_name: ObjectName,
/// COLUMNS
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'
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 {
@ -1143,37 +1143,25 @@ impl fmt::Display for Statement {
Statement::Copy {
table_name,
columns,
values,
delimiter,
filename,
csv_header,
to,
target,
options,
legacy_options,
values,
} => {
write!(f, "COPY {}", table_name)?;
if !columns.is_empty() {
write!(f, " ({})", display_comma_separated(columns))?;
}
if let Some(name) = filename {
if *to {
write!(f, " TO {}", name)?
} else {
write!(f, " FROM {}", name)?;
write!(f, " {} {}", if *to { "TO" } else { "FROM" }, target)?;
if !options.is_empty() {
write!(f, " ({})", display_comma_separated(options))?;
}
} else if *to {
write!(f, " TO stdin ")?
} else {
write!(f, " FROM stdin ")?;
}
if let Some(delimiter) = delimiter {
write!(f, " DELIMITER {}", delimiter)?;
}
if *csv_header {
write!(f, " CSV HEADER")?;
if !legacy_options.is_empty() {
write!(f, " {}", display_separated(legacy_options, " "))?;
}
if !values.is_empty() {
write!(f, ";")?;
writeln!(f)?;
writeln!(f, ";")?;
let mut delim = "";
for v in values {
write!(f, "{}", delim)?;
@ -1184,8 +1172,6 @@ impl fmt::Display for Statement {
write!(f, "\\N")?;
}
}
}
if filename.is_none() {
write!(f, "\n\\.")?;
}
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)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

View file

@ -195,6 +195,7 @@ define_keywords!(
EACH,
ELEMENT,
ELSE,
ENCODING,
END,
END_EXEC = "END-EXEC",
END_FRAME,
@ -227,10 +228,15 @@ define_keywords!(
FLOOR,
FOLLOWING,
FOR,
FORCE,
FORCE_NOT_NULL,
FORCE_NULL,
FORCE_QUOTE,
FOREIGN,
FORMAT,
FRAME_ROW,
FREE,
FREEZE,
FROM,
FULL,
FUNCTION,
@ -372,8 +378,10 @@ define_keywords!(
PRIMARY,
PRIVILEGES,
PROCEDURE,
PROGRAM,
PURGE,
QUARTER,
QUOTE,
RANGE,
RANK,
RCFILE,
@ -449,6 +457,7 @@ define_keywords!(
STDDEV_POP,
STDDEV_SAMP,
STDIN,
STDOUT,
STORED,
STRING,
SUBMULTISET,

View file

@ -2235,53 +2235,162 @@ impl<'a> Parser<'a> {
pub fn parse_copy(&mut self) -> Result<Statement, ParserError> {
let table_name = self.parse_object_name()?;
let columns = self.parse_parenthesized_column_list(Optional)?;
let to_or_from = self.expect_one_of_keywords(&[Keyword::FROM, Keyword::TO])?;
let to: bool = match to_or_from {
Keyword::TO => true,
Keyword::FROM => false,
_ => unreachable!("something wrong while parsing copy statment :("),
let to = match self.parse_one_of_keywords(&[Keyword::FROM, Keyword::TO]) {
Some(Keyword::FROM) => false,
Some(Keyword::TO) => true,
_ => self.expected("FROM or TO", self.peek_token())?,
};
let mut filename = None;
// check whether data has to be copied form table or std in.
if !self.parse_keyword(Keyword::STDIN) {
filename = Some(self.parse_identifier()?)
let target = if self.parse_keyword(Keyword::STDIN) {
CopyTarget::Stdin
} else if self.parse_keyword(Keyword::STDOUT) {
CopyTarget::Stdout
} else if self.parse_keyword(Keyword::PROGRAM) {
CopyTarget::Program {
command: self.parse_literal_string()?,
}
// parse copy options.
let mut delimiter = None;
let mut csv_header = false;
loop {
if let Some(keyword) = self.parse_one_of_keywords(&[Keyword::DELIMITER, Keyword::CSV]) {
match keyword {
Keyword::DELIMITER => {
delimiter = Some(self.parse_identifier()?);
continue;
} else {
CopyTarget::File {
filename: self.parse_literal_string()?,
}
Keyword::CSV => {
self.expect_keyword(Keyword::HEADER)?;
csv_header = true
};
let _ = self.parse_keyword(Keyword::WITH);
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);
}
}
break;
}
// copy the values from stdin if there is no file to be copied from.
let mut values = vec![];
if filename.is_none() {
let values = if let CopyTarget::Stdin = target {
self.expect_token(&Token::SemiColon)?;
values = self.parse_tsv();
}
self.parse_tsv()
} else {
vec![]
};
Ok(Statement::Copy {
table_name,
columns,
values,
filename,
delimiter,
csv_header,
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
/// COPY payload
pub fn parse_tsv(&mut self) -> Vec<Option<String>> {

View file

@ -375,7 +375,7 @@ fn parse_drop_schema_if_exists() {
}
#[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;
1 PENELOPE GUINESS 2006-02-15 09:34:33 0.11111
2 NICK WAHLBERG 2006-02-15 09:34:33 0.22222
@ -487,14 +487,13 @@ fn test_copy_from() {
Statement::Copy {
table_name: ObjectName(vec!["users".into()]),
columns: vec![],
filename: Some(Ident {
value: "data.csv".to_string(),
quote_style: Some('\'')
}),
to: false,
target: CopyTarget::File {
filename: "data.csv".to_string(),
},
options: vec![],
legacy_options: vec![],
values: vec![],
delimiter: None,
csv_header: false,
to: false
}
);
@ -504,17 +503,13 @@ fn test_copy_from() {
Statement::Copy {
table_name: ObjectName(vec!["users".into()]),
columns: vec![],
filename: Some(Ident {
value: "data.csv".to_string(),
quote_style: Some('\'')
}),
to: false,
target: CopyTarget::File {
filename: "data.csv".to_string(),
},
options: vec![],
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
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 {
table_name: ObjectName(vec!["users".into()]),
columns: vec![],
filename: Some(Ident {
value: "data.csv".to_string(),
quote_style: Some('\'')
}),
to: false,
target: CopyTarget::File {
filename: "data.csv".to_string(),
},
options: vec![],
legacy_options: vec![
CopyLegacyOption::Delimiter(','),
CopyLegacyOption::Csv(vec![CopyLegacyCsvOption::Header,])
],
values: vec![],
delimiter: Some(Ident {
value: ",".to_string(),
quote_style: Some('\'')
}),
csv_header: true,
to: false
}
)
);
}
#[test]
@ -547,14 +541,13 @@ fn test_copy_to() {
Statement::Copy {
table_name: ObjectName(vec!["users".into()]),
columns: vec![],
filename: Some(Ident {
value: "data.csv".to_string(),
quote_style: Some('\'')
}),
to: true,
target: CopyTarget::File {
filename: "data.csv".to_string(),
},
options: vec![],
legacy_options: vec![],
values: vec![],
delimiter: None,
csv_header: false,
to: true
}
);
@ -564,17 +557,13 @@ fn test_copy_to() {
Statement::Copy {
table_name: ObjectName(vec!["users".into()]),
columns: vec![],
filename: Some(Ident {
value: "data.csv".to_string(),
quote_style: Some('\'')
}),
to: true,
target: CopyTarget::File {
filename: "data.csv".to_string(),
},
options: vec![],
legacy_options: vec![CopyLegacyOption::Delimiter(',')],
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 {
table_name: ObjectName(vec!["users".into()]),
columns: vec![],
filename: Some(Ident {
value: "data.csv".to_string(),
quote_style: Some('\'')
}),
to: true,
target: CopyTarget::File {
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![],
delimiter: Some(Ident {
value: ",".to_string(),
quote_style: Some('\'')
}),
csv_header: true,
to: true
}
)
}