Skip to content

Commit db8e22d

Browse files
committed
Redshift: Add more copy options
1 parent 4b8797e commit db8e22d

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
@@ -8759,41 +8759,81 @@ impl fmt::Display for CopyOption {
87598759

87608760
/// An option in `COPY` statement before PostgreSQL version 9.0.
87618761
///
8762-
/// <https://www.postgresql.org/docs/8.4/sql-copy.html>
8762+
/// [PostgreSQL](https://www.postgresql.org/docs/8.4/sql-copy.html)
8763+
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_COPY-alphabetical-parm-list.html)
87638764
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
87648765
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
87658766
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
87668767
pub enum CopyLegacyOption {
8768+
/// ACCEPTANYDATE
8769+
AcceptAnyDate,
8770+
/// ACCEPTINVCHARS
8771+
AcceptInvChars(Option<String>),
87678772
/// BINARY
87688773
Binary,
8769-
/// DELIMITER \[ AS \] 'delimiter_character'
8770-
Delimiter(char),
8771-
/// NULL \[ AS \] 'null_string'
8772-
Null(String),
8774+
/// BLANKSASNULL
8775+
BlankAsNull,
87738776
/// CSV ...
87748777
Csv(Vec<CopyLegacyCsvOption>),
8778+
/// DATEFORMAT \[ AS \] {'dateformat_string' | 'auto' }
8779+
DateFormat(Option<String>),
8780+
/// DELIMITER \[ AS \] 'delimiter_character'
8781+
Delimiter(char),
8782+
/// EMPTYASNULL
8783+
EmptyAsNull,
87758784
/// IAM_ROLE { DEFAULT | 'arn:aws:iam::123456789:role/role1' }
87768785
IamRole(IamRoleKind),
87778786
/// IGNOREHEADER \[ AS \] number_rows
87788787
IgnoreHeader(u64),
8788+
/// NULL \[ AS \] 'null_string'
8789+
Null(String),
8790+
/// TIMEFORMAT \[ AS \] {'timeformat_string' | 'auto' | 'epochsecs' | 'epochmillisecs' }
8791+
TimeFormat(Option<String>),
8792+
/// TRUNCATECOLUMNS
8793+
TruncateColumns,
87798794
}
87808795

87818796
impl fmt::Display for CopyLegacyOption {
87828797
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87838798
use CopyLegacyOption::*;
87848799
match self {
8800+
AcceptAnyDate => write!(f, "ACCEPTANYDATE"),
8801+
AcceptInvChars(ch) => {
8802+
write!(f, "ACCEPTINVCHARS")?;
8803+
if let Some(ch) = ch {
8804+
write!(f, " '{}'", value::escape_single_quote_string(ch))?;
8805+
}
8806+
Ok(())
8807+
}
87858808
Binary => write!(f, "BINARY"),
8786-
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
8787-
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
8809+
BlankAsNull => write!(f, "BLANKSASNULL"),
87888810
Csv(opts) => {
87898811
write!(f, "CSV")?;
87908812
if !opts.is_empty() {
87918813
write!(f, " {}", display_separated(opts, " "))?;
87928814
}
87938815
Ok(())
87948816
}
8817+
DateFormat(fmt) => {
8818+
write!(f, "DATEFORMAT")?;
8819+
if let Some(fmt) = fmt {
8820+
write!(f, " '{}'", value::escape_single_quote_string(fmt))?;
8821+
}
8822+
Ok(())
8823+
}
8824+
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
8825+
EmptyAsNull => write!(f, "EMPTYASNULL"),
87958826
IamRole(role) => write!(f, "IAM_ROLE {role}"),
87968827
IgnoreHeader(num_rows) => write!(f, "IGNOREHEADER {num_rows}"),
8828+
Null(string) => write!(f, "NULL '{}'", value::escape_single_quote_string(string)),
8829+
TimeFormat(fmt) => {
8830+
write!(f, "TIMEFORMAT")?;
8831+
if let Some(fmt) = fmt {
8832+
write!(f, " '{}'", value::escape_single_quote_string(fmt))?;
8833+
}
8834+
Ok(())
8835+
}
8836+
TruncateColumns => write!(f, "TRUNCATECOLUMNS"),
87978837
}
87988838
}
87998839
}

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,
@@ -254,6 +257,7 @@ define_keywords!(
254257
DATA_RETENTION_TIME_IN_DAYS,
255258
DATE,
256259
DATE32,
260+
DATEFORMAT,
257261
DATETIME,
258262
DATETIME64,
259263
DAY,
@@ -312,6 +316,7 @@ define_keywords!(
312316
ELSE,
313317
ELSEIF,
314318
EMPTY,
319+
EMPTYASNULL,
315320
ENABLE,
316321
ENABLE_SCHEMA_EVOLUTION,
317322
ENCODING,
@@ -927,6 +932,7 @@ define_keywords!(
927932
THEN,
928933
TIES,
929934
TIME,
935+
TIMEFORMAT,
930936
TIMESTAMP,
931937
TIMESTAMPTZ,
932938
TIMESTAMP_NTZ,
@@ -955,6 +961,7 @@ define_keywords!(
955961
TRIM_ARRAY,
956962
TRUE,
957963
TRUNCATE,
964+
TRUNCATECOLUMNS,
958965
TRY,
959966
TRY_CAST,
960967
TRY_CONVERT,

src/parser/mod.rs

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

95459545
fn parse_copy_legacy_option(&mut self) -> Result<CopyLegacyOption, ParserError> {
9546+
// FORMAT \[ AS \] is optional
9547+
if self.parse_keyword(Keyword::FORMAT) {
9548+
let _ = self.parse_keyword(Keyword::AS);
9549+
}
9550+
95469551
let ret = match self.parse_one_of_keywords(&[
9552+
Keyword::ACCEPTANYDATE,
9553+
Keyword::ACCEPTINVCHARS,
95479554
Keyword::BINARY,
9548-
Keyword::DELIMITER,
9549-
Keyword::NULL,
9555+
Keyword::BLANKSASNULL,
95509556
Keyword::CSV,
9557+
Keyword::DATEFORMAT,
9558+
Keyword::DELIMITER,
9559+
Keyword::EMPTYASNULL,
95519560
Keyword::IAM_ROLE,
95529561
Keyword::IGNOREHEADER,
9562+
Keyword::NULL,
9563+
Keyword::TIMEFORMAT,
9564+
Keyword::TRUNCATECOLUMNS,
95539565
]) {
9554-
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
9555-
Some(Keyword::DELIMITER) => {
9566+
Some(Keyword::ACCEPTANYDATE) => CopyLegacyOption::AcceptAnyDate,
9567+
Some(Keyword::ACCEPTINVCHARS) => {
95569568
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9557-
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
9558-
}
9559-
Some(Keyword::NULL) => {
9560-
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9561-
CopyLegacyOption::Null(self.parse_literal_string()?)
9569+
let ch = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9570+
Some(self.parse_literal_string()?)
9571+
} else {
9572+
None
9573+
};
9574+
CopyLegacyOption::AcceptInvChars(ch)
95629575
}
9576+
Some(Keyword::BINARY) => CopyLegacyOption::Binary,
9577+
Some(Keyword::BLANKSASNULL) => CopyLegacyOption::BlankAsNull,
95639578
Some(Keyword::CSV) => CopyLegacyOption::Csv({
95649579
let mut opts = vec![];
95659580
while let Some(opt) =
@@ -9569,12 +9584,40 @@ impl<'a> Parser<'a> {
95699584
}
95709585
opts
95719586
}),
9587+
Some(Keyword::DATEFORMAT) => {
9588+
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9589+
let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9590+
Some(self.parse_literal_string()?)
9591+
} else {
9592+
None
9593+
};
9594+
CopyLegacyOption::DateFormat(fmt)
9595+
}
9596+
Some(Keyword::DELIMITER) => {
9597+
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9598+
CopyLegacyOption::Delimiter(self.parse_literal_char()?)
9599+
}
9600+
Some(Keyword::EMPTYASNULL) => CopyLegacyOption::EmptyAsNull,
95729601
Some(Keyword::IAM_ROLE) => CopyLegacyOption::IamRole(self.parse_iam_role_kind()?),
95739602
Some(Keyword::IGNOREHEADER) => {
95749603
let _ = self.parse_keyword(Keyword::AS);
95759604
let num_rows = self.parse_literal_uint()?;
95769605
CopyLegacyOption::IgnoreHeader(num_rows)
95779606
}
9607+
Some(Keyword::NULL) => {
9608+
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9609+
CopyLegacyOption::Null(self.parse_literal_string()?)
9610+
}
9611+
Some(Keyword::TIMEFORMAT) => {
9612+
let _ = self.parse_keyword(Keyword::AS); // [ AS ]
9613+
let fmt = if matches!(self.peek_token().token, Token::SingleQuotedString(_)) {
9614+
Some(self.parse_literal_string()?)
9615+
} else {
9616+
None
9617+
};
9618+
CopyLegacyOption::TimeFormat(fmt)
9619+
}
9620+
Some(Keyword::TRUNCATECOLUMNS) => CopyLegacyOption::TruncateColumns,
95789621
_ => self.expected("option", self.peek_token())?,
95799622
};
95809623
Ok(ret)

tests/sqlparser_common.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16768,4 +16768,36 @@ fn parse_copy_options() {
1676816768
}
1676916769
_ => unreachable!(),
1677016770
}
16771+
one_statement_parses_to(
16772+
concat!(
16773+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ",
16774+
"ACCEPTANYDATE ",
16775+
"ACCEPTINVCHARS AS '*' ",
16776+
"BLANKSASNULL ",
16777+
"CSV ",
16778+
"DATEFORMAT AS 'DD-MM-YYYY' ",
16779+
"EMPTYASNULL ",
16780+
"IAM_ROLE DEFAULT ",
16781+
"IGNOREHEADER AS 1 ",
16782+
"TIMEFORMAT AS 'auto' ",
16783+
"TRUNCATECOLUMNS",
16784+
),
16785+
concat!(
16786+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' ",
16787+
"ACCEPTANYDATE ",
16788+
"ACCEPTINVCHARS '*' ",
16789+
"BLANKSASNULL ",
16790+
"CSV ",
16791+
"DATEFORMAT 'DD-MM-YYYY' ",
16792+
"EMPTYASNULL ",
16793+
"IAM_ROLE DEFAULT ",
16794+
"IGNOREHEADER 1 ",
16795+
"TIMEFORMAT 'auto' ",
16796+
"TRUNCATECOLUMNS",
16797+
),
16798+
);
16799+
one_statement_parses_to(
16800+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' FORMAT AS CSV",
16801+
"COPY dst (c1, c2, c3) FROM 's3://redshift-downloads/tickit/category_pipe.txt' CSV",
16802+
);
1677116803
}

0 commit comments

Comments
 (0)