mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-21 22:44:08 +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,
|
||||
/// 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))]
|
||||
|
|
|
@ -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,
|
||||
|
|
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> {
|
||||
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>> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue