Skip to content

Commit cd76e7d

Browse files
yoavcloudayman-sigma
authored andcommitted
Redshift: Add more copy options (apache#2008)
1 parent d83d99a commit cd76e7d

4 files changed

Lines changed: 138 additions & 16 deletions

File tree

src/ast/mod.rs

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

88048804
/// An option in `COPY` statement before PostgreSQL version 9.0.
88058805
///
8806-
/// <https://www.postgresql.org/docs/8.4/sql-copy.html>
8806+
/// [PostgreSQL](https://www.postgresql.org/docs/8.4/sql-copy.html)
8807+
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_COPY-alphabetical-parm-list.html)
88078808
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
88088809
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88098810
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
88108811
pub enum CopyLegacyOption {
8812+
/// ACCEPTANYDATE
8813+
AcceptAnyDate,
8814+
/// ACCEPTINVCHARS
8815+
AcceptInvChars(Option<String>),
88118816
/// BINARY
88128817
Binary,
8813-
/// DELIMITER \[ AS \] 'delimiter_character'
8814-
Delimiter(char),
8815-
/// NULL \[ AS \] 'null_string'
8816-
Null(String),
8818+
/// BLANKSASNULL
8819+
BlankAsNull,
88178820
/// CSV ...
88188821
Csv(Vec<CopyLegacyCsvOption>),
8822+
/// DATEFORMAT \[ AS \] {'dateformat_string' | 'auto' }
8823+
DateFormat(Option<String>),
8824+
/// DELIMITER \[ AS \] 'delimiter_character'
8825+
Delimiter(char),
8826+
/// EMPTYASNULL
8827+
EmptyAsNull,
88198828
/// IAM_ROLE { DEFAULT | 'arn:aws:iam::123456789:role/role1' }
88208829
IamRole(IamRoleKind),
88218830
/// IGNOREHEADER \[ AS \] number_rows
88228831
IgnoreHeader(u64),
8832+
/// NULL \[ AS \] 'null_string'
8833+
Null(String),
8834+
/// TIMEFORMAT \[ AS \] {'timeformat_string' | 'auto' | 'epochsecs' | 'epochmillisecs' }
8835+
TimeFormat(Option<String>),
8836+
/// TRUNCATECOLUMNS
8837+
TruncateColumns,
88238838
}
88248839

88258840
impl fmt::Display for CopyLegacyOption {
88268841
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88278842
use CopyLegacyOption::*;
88288843
match self {
8844+
AcceptAnyDate => write!(f, "ACCEPTANYDATE"),
8845+
AcceptInvChars(ch) => {
8846+
write!(f, "ACCEPTINVCHARS")?;
8847+
if let Some(ch) = ch {
8848+
write!(f, " '{}'", value::escape_single_quote_string(ch))?;
8849+
}
8850+
Ok(())
8851+
}
88298852
Binary => write!(f, "BINARY"),
8830-
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
8831-
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
8853+
BlankAsNull => write!(f, "BLANKSASNULL"),
88328854
Csv(opts) => {
88338855
write!(f, "CSV")?;
88348856
if !opts.is_empty() {
88358857
write!(f, " {}", display_separated(opts, " "))?;
88368858
}
88378859
Ok(())
88388860
}
8861+
DateFormat(fmt) => {
8862+
write!(f, "DATEFORMAT")?;
8863+
if let Some(fmt) = fmt {
8864+
write!(f, " '{}'", value::escape_single_quote_string(fmt))?;
8865+
}
8866+
Ok(())
8867+
}
8868+
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
8869+
EmptyAsNull => write!(f, "EMPTYASNULL"),
88398870
IamRole(role) => write!(f, "IAM_ROLE {role}"),
88408871
IgnoreHeader(num_rows) => write!(f, "IGNOREHEADER {num_rows}"),
8872+
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
8873+
TimeFormat(fmt) => {
8874+
write!(f, "TIMEFORMAT")?;
8875+
if let Some(fmt) = fmt {
8876+
write!(f, " '{}'", value::escape_single_quote_string(fmt))?;
8877+
}
8878+
Ok(())
8879+
}
8880+
TruncateColumns => write!(f, "TRUNCATECOLUMNS"),
88418881
}
88428882
}
88438883
}

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
@@ -9633,23 +9633,38 @@ impl<'a> Parser<'a> {
96339633
}
96349634

96359635
fn parse_copy_legacy_option(&mut self) -> Result<CopyLegacyOption, ParserError> {
9636+
// FORMAT \[ AS \] is optional
9637+
if self.parse_keyword(Keyword::FORMAT) {
9638+
let _ = self.parse_keyword(Keyword::AS);
9639+
}
9640+
96369641
let ret = match self.parse_one_of_keywords(&[
9642+
Keyword::ACCEPTANYDATE,
9643+
Keyword::ACCEPTINVCHARS,
96379644
Keyword::BINARY,
9638-
Keyword::DELIMITER,
9639-
Keyword::NULL,
9645+
Keyword::BLANKSASNULL,
96409646
Keyword::CSV,
9647+
Keyword::DATEFORMAT,
9648+
Keyword::DELIMITER,
9649+
Keyword::EMPTYASNULL,
96419650
Keyword::IAM_ROLE,
96429651
Keyword::IGNOREHEADER,
9652+
Keyword::NULL,
9653+
Keyword::TIMEFORMAT,
9654+
Keyword::TRUNCATECOLUMNS,
96439655
]) {
9644-
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
9645-
Some(Keyword::DELIMITER) => {
9656+
Some(Keyword::ACCEPTANYDATE) => CopyLegacyOption::AcceptAnyDate,
9657+
Some(Keyword::ACCEPTINVCHARS) => {
96469658
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9647-
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
9648-
}
9649-
Some(Keyword::NULL) => {
9650-
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9651-
CopyLegacyOption::Null(self.parse_literal_string()?)
9659+
let ch = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9660+
Some(self.parse_literal_string()?)
9661+
} else {
9662+
None
9663+
};
9664+
CopyLegacyOption::AcceptInvChars(ch)
96529665
}
9666+
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
9667+
Some(Keyword::BLANKSASNULL) => CopyLegacyOption::BlankAsNull,
96539668
Some(Keyword::CSV) => CopyLegacyOption::Csv({
96549669
let mut opts = vec![];
96559670
while let Some(opt) =
@@ -9659,12 +9674,40 @@ impl<'a> Parser<'a> {
96599674
}
96609675
opts
96619676
}),
9677+
Some(Keyword::DATEFORMAT) => {
9678+
let _ = self.parse_keyword(Keyword::AS);
9679+
let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9680+
Some(self.parse_literal_string()?)
9681+
} else {
9682+
None
9683+
};
9684+
CopyLegacyOption::DateFormat(fmt)
9685+
}
9686+
Some(Keyword::DELIMITER) => {
9687+
let _ = self.parse_keyword(Keyword::AS);
9688+
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
9689+
}
9690+
Some(Keyword::EMPTYASNULL) => CopyLegacyOption::EmptyAsNull,
96629691
Some(Keyword::IAM_ROLE) => CopyLegacyOption::IamRole(self.parse_iam_role_kind()?),
96639692
Some(Keyword::IGNOREHEADER) => {
96649693
let _ = self.parse_keyword(Keyword::AS);
96659694
let num_rows = self.parse_literal_uint()?;
96669695
CopyLegacyOption::IgnoreHeader(num_rows)
96679696
}
9697+
Some(Keyword::NULL) => {
9698+
let _ = self.parse_keyword(Keyword::AS);
9699+
CopyLegacyOption::Null(self.parse_literal_string()?)
9700+
}
9701+
Some(Keyword::TIMEFORMAT) => {
9702+
let _ = self.parse_keyword(Keyword::AS);
9703+
let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9704+
Some(self.parse_literal_string()?)
9705+
} else {
9706+
None
9707+
};
9708+
CopyLegacyOption::TimeFormat(fmt)
9709+
}
9710+
Some(Keyword::TRUNCATECOLUMNS) => CopyLegacyOption::TruncateColumns,
96689711
_ => self.expected("option", self.peek_token())?,
96699712
};
96709713
Ok(ret)

tests/sqlparser_common.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16866,6 +16866,38 @@ fn parse_copy_options() {
1686616866
}
1686716867
_ => unreachable!(),
1686816868
}
16869+
one_statement_parses_to(
16870+
concat!(
16871+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ",
16872+
"ACCEPTANYDATE ",
16873+
"ACCEPTINVCHARS AS '*' ",
16874+
"BLANKSASNULL ",
16875+
"CSV ",
16876+
"DATEFORMAT AS 'DD-MM-YYYY' ",
16877+
"EMPTYASNULL ",
16878+
"IAM_ROLE DEFAULT ",
16879+
"IGNOREHEADER AS 1 ",
16880+
"TIMEFORMAT AS 'auto' ",
16881+
"TRUNCATECOLUMNS",
16882+
),
16883+
concat!(
16884+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ",
16885+
"ACCEPTANYDATE ",
16886+
"ACCEPTINVCHARS '*' ",
16887+
"BLANKSASNULL ",
16888+
"CSV ",
16889+
"DATEFORMAT 'DD-MM-YYYY' ",
16890+
"EMPTYASNULL ",
16891+
"IAM_ROLE DEFAULT ",
16892+
"IGNOREHEADER 1 ",
16893+
"TIMEFORMAT 'auto' ",
16894+
"TRUNCATECOLUMNS",
16895+
),
16896+
);
16897+
one_statement_parses_to(
16898+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' FORMAT AS CSV",
16899+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' CSV",
16900+
);
1686916901
}
1687016902

1687116903
#[test]

0 commit comments

Comments
 (0)