Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5d5c90c

Browse files
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)

AltStyle によって変換されたページ (->オリジナル) /