Skip to content

Commit 5d5c90c

Browse files
authored
Redshift: Add more copy options (#2008)
1 parent 6e80e5c commit 5d5c90c

File tree

4 files changed

+138
-16
lines changed

4 files changed

+138
-16
lines changed

src/ast/mod.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8774,41 +8774,81 @@ impl fmt::Display for CopyOption {
87748774

87758775
/// An option in `COPY` statement before PostgreSQL version 9.0.
87768776
///
8777-
/// <https://www.postgresql.org/docs/8.4/sql-copy.html>
8777+
/// [PostgreSQL](https://www.postgresql.org/docs/8.4/sql-copy.html)
8778+
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_COPY-alphabetical-parm-list.html)
87788779
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
87798780
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
87808781
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
87818782
pub enum CopyLegacyOption {
8783+
/// ACCEPTANYDATE
8784+
AcceptAnyDate,
8785+
/// ACCEPTINVCHARS
8786+
AcceptInvChars(Option<String>),
87828787
/// BINARY
87838788
Binary,
8784-
/// DELIMITER \[ AS \] 'delimiter_character'
8785-
Delimiter(char),
8786-
/// NULL \[ AS \] 'null_string'
8787-
Null(String),
8789+
/// BLANKSASNULL
8790+
BlankAsNull,
87888791
/// CSV ...
87898792
Csv(Vec<CopyLegacyCsvOption>),
8793+
/// DATEFORMAT \[ AS \] {'dateformat_string' | 'auto' }
8794+
DateFormat(Option<String>),
8795+
/// DELIMITER \[ AS \] 'delimiter_character'
8796+
Delimiter(char),
8797+
/// EMPTYASNULL
8798+
EmptyAsNull,
87908799
/// IAM_ROLE { DEFAULT | 'arn:aws:iam::123456789:role/role1' }
87918800
IamRole(IamRoleKind),
87928801
/// IGNOREHEADER \[ AS \] number_rows
87938802
IgnoreHeader(u64),
8803+
/// NULL \[ AS \] 'null_string'
8804+
Null(String),
8805+
/// TIMEFORMAT \[ AS \] {'timeformat_string' | 'auto' | 'epochsecs' | 'epochmillisecs' }
8806+
TimeFormat(Option<String>),
8807+
/// TRUNCATECOLUMNS
8808+
TruncateColumns,
87948809
}
87958810

87968811
impl fmt::Display for CopyLegacyOption {
87978812
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87988813
use CopyLegacyOption::*;
87998814
match self {
8815+
AcceptAnyDate => write!(f, "ACCEPTANYDATE"),
8816+
AcceptInvChars(ch) => {
8817+
write!(f, "ACCEPTINVCHARS")?;
8818+
if let Some(ch) = ch {
8819+
write!(f, " '{}'", value::escape_single_quote_string(ch))?;
8820+
}
8821+
Ok(())
8822+
}
88008823
Binary => write!(f, "BINARY"),
8801-
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
8802-
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
8824+
BlankAsNull => write!(f, "BLANKSASNULL"),
88038825
Csv(opts) => {
88048826
write!(f, "CSV")?;
88058827
if !opts.is_empty() {
88068828
write!(f, " {}", display_separated(opts, " "))?;
88078829
}
88088830
Ok(())
88098831
}
8832+
DateFormat(fmt) => {
8833+
write!(f, "DATEFORMAT")?;
8834+
if let Some(fmt) = fmt {
8835+
write!(f, " '{}'", value::escape_single_quote_string(fmt))?;
8836+
}
8837+
Ok(())
8838+
}
8839+
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
8840+
EmptyAsNull => write!(f, "EMPTYASNULL"),
88108841
IamRole(role) => write!(f, "IAM_ROLE {role}"),
88118842
IgnoreHeader(num_rows) => write!(f, "IGNOREHEADER {num_rows}"),
8843+
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
8844+
TimeFormat(fmt) => {
8845+
write!(f, "TIMEFORMAT")?;
8846+
if let Some(fmt) = fmt {
8847+
write!(f, " '{}'", value::escape_single_quote_string(fmt))?;
8848+
}
8849+
Ok(())
8850+
}
8851+
TruncateColumns => write!(f, "TRUNCATECOLUMNS"),
88128852
}
88138853
}
88148854
}

src/keywords.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ define_keywords!(
7676
ABS,
7777
ABSENT,
7878
ABSOLUTE,
79+
ACCEPTANYDATE,
80+
ACCEPTINVCHARS,
7981
ACCESS,
8082
ACCOUNT,
8183
ACTION,
@@ -138,6 +140,7 @@ define_keywords!(
138140
BIND,
139141
BINDING,
140142
BIT,
143+
BLANKSASNULL,
141144
BLOB,
142145
BLOCK,
143146
BLOOM,
@@ -255,6 +258,7 @@ define_keywords!(
255258
DATA_RETENTION_TIME_IN_DAYS,
256259
DATE,
257260
DATE32,
261+
DATEFORMAT,
258262
DATETIME,
259263
DATETIME64,
260264
DAY,
@@ -314,6 +318,7 @@ define_keywords!(
314318
ELSE,
315319
ELSEIF,
316320
EMPTY,
321+
EMPTYASNULL,
317322
ENABLE,
318323
ENABLE_SCHEMA_EVOLUTION,
319324
ENCODING,
@@ -933,6 +938,7 @@ define_keywords!(
933938
THEN,
934939
TIES,
935940
TIME,
941+
TIMEFORMAT,
936942
TIMESTAMP,
937943
TIMESTAMPTZ,
938944
TIMESTAMP_NTZ,
@@ -961,6 +967,7 @@ define_keywords!(
961967
TRIM_ARRAY,
962968
TRUE,
963969
TRUNCATE,
970+
TRUNCATECOLUMNS,
964971
TRY,
965972
TRY_CAST,
966973
TRY_CONVERT,

src/parser/mod.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9602,23 +9602,38 @@ impl<'a> Parser<'a> {
96029602
}
96039603

96049604
fn parse_copy_legacy_option(&mut self) -> Result<CopyLegacyOption, ParserError> {
9605+
// FORMAT \[ AS \] is optional
9606+
if self.parse_keyword(Keyword::FORMAT) {
9607+
let _ = self.parse_keyword(Keyword::AS);
9608+
}
9609+
96059610
let ret = match self.parse_one_of_keywords(&[
9611+
Keyword::ACCEPTANYDATE,
9612+
Keyword::ACCEPTINVCHARS,
96069613
Keyword::BINARY,
9607-
Keyword::DELIMITER,
9608-
Keyword::NULL,
9614+
Keyword::BLANKSASNULL,
96099615
Keyword::CSV,
9616+
Keyword::DATEFORMAT,
9617+
Keyword::DELIMITER,
9618+
Keyword::EMPTYASNULL,
96109619
Keyword::IAM_ROLE,
96119620
Keyword::IGNOREHEADER,
9621+
Keyword::NULL,
9622+
Keyword::TIMEFORMAT,
9623+
Keyword::TRUNCATECOLUMNS,
96129624
]) {
9613-
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
9614-
Some(Keyword::DELIMITER) => {
9625+
Some(Keyword::ACCEPTANYDATE) => CopyLegacyOption::AcceptAnyDate,
9626+
Some(Keyword::ACCEPTINVCHARS) => {
96159627
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9616-
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
9617-
}
9618-
Some(Keyword::NULL) => {
9619-
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9620-
CopyLegacyOption::Null(self.parse_literal_string()?)
9628+
let ch = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9629+
Some(self.parse_literal_string()?)
9630+
} else {
9631+
None
9632+
};
9633+
CopyLegacyOption::AcceptInvChars(ch)
96219634
}
9635+
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
9636+
Some(Keyword::BLANKSASNULL) => CopyLegacyOption::BlankAsNull,
96229637
Some(Keyword::CSV) => CopyLegacyOption::Csv({
96239638
let mut opts = vec![];
96249639
while let Some(opt) =
@@ -9628,12 +9643,40 @@ impl<'a> Parser<'a> {
96289643
}
96299644
opts
96309645
}),
9646+
Some(Keyword::DATEFORMAT) => {
9647+
let _ = self.parse_keyword(Keyword::AS);
9648+
let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9649+
Some(self.parse_literal_string()?)
9650+
} else {
9651+
None
9652+
};
9653+
CopyLegacyOption::DateFormat(fmt)
9654+
}
9655+
Some(Keyword::DELIMITER) => {
9656+
let _ = self.parse_keyword(Keyword::AS);
9657+
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
9658+
}
9659+
Some(Keyword::EMPTYASNULL) => CopyLegacyOption::EmptyAsNull,
96319660
Some(Keyword::IAM_ROLE) => CopyLegacyOption::IamRole(self.parse_iam_role_kind()?),
96329661
Some(Keyword::IGNOREHEADER) => {
96339662
let _ = self.parse_keyword(Keyword::AS);
96349663
let num_rows = self.parse_literal_uint()?;
96359664
CopyLegacyOption::IgnoreHeader(num_rows)
96369665
}
9666+
Some(Keyword::NULL) => {
9667+
let _ = self.parse_keyword(Keyword::AS);
9668+
CopyLegacyOption::Null(self.parse_literal_string()?)
9669+
}
9670+
Some(Keyword::TIMEFORMAT) => {
9671+
let _ = self.parse_keyword(Keyword::AS);
9672+
let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9673+
Some(self.parse_literal_string()?)
9674+
} else {
9675+
None
9676+
};
9677+
CopyLegacyOption::TimeFormat(fmt)
9678+
}
9679+
Some(Keyword::TRUNCATECOLUMNS) => CopyLegacyOption::TruncateColumns,
96379680
_ => self.expected("option", self.peek_token())?,
96389681
};
96399682
Ok(ret)

tests/sqlparser_common.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16840,6 +16840,38 @@ fn parse_copy_options() {
1684016840
}
1684116841
_ => unreachable!(),
1684216842
}
16843+
one_statement_parses_to(
16844+
concat!(
16845+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ",
16846+
"ACCEPTANYDATE ",
16847+
"ACCEPTINVCHARS AS '*' ",
16848+
"BLANKSASNULL ",
16849+
"CSV ",
16850+
"DATEFORMAT AS 'DD-MM-YYYY' ",
16851+
"EMPTYASNULL ",
16852+
"IAM_ROLE DEFAULT ",
16853+
"IGNOREHEADER AS 1 ",
16854+
"TIMEFORMAT AS 'auto' ",
16855+
"TRUNCATECOLUMNS",
16856+
),
16857+
concat!(
16858+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ",
16859+
"ACCEPTANYDATE ",
16860+
"ACCEPTINVCHARS '*' ",
16861+
"BLANKSASNULL ",
16862+
"CSV ",
16863+
"DATEFORMAT 'DD-MM-YYYY' ",
16864+
"EMPTYASNULL ",
16865+
"IAM_ROLE DEFAULT ",
16866+
"IGNOREHEADER 1 ",
16867+
"TIMEFORMAT 'auto' ",
16868+
"TRUNCATECOLUMNS",
16869+
),
16870+
);
16871+
one_statement_parses_to(
16872+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' FORMAT AS CSV",
16873+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' CSV",
16874+
);
1684316875
}
1684416876

1684516877
#[test]

0 commit comments

Comments
 (0)