Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9730,12 +9730,12 @@ impl fmt::Display for CopyOption {
Format(name) => write!(f, "FORMAT {name}"),
Freeze(true) => write!(f, "FREEZE"),
Freeze(false) => write!(f, "FREEZE FALSE"),
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
Delimiter(char) => write!(f, "DELIMITER '{}'", value::escape_single_quote_char(*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}'"),
Quote(char) => write!(f, "QUOTE '{}'", value::escape_single_quote_char(*char)),
Escape(char) => write!(f, "ESCAPE '{}'", value::escape_single_quote_char(*char)),
ForceQuote(columns) => write!(f, "FORCE_QUOTE ({})", display_comma_separated(columns)),
ForceNotNull(columns) => {
write!(f, "FORCE_NOT_NULL ({})", display_comma_separated(columns))
Expand Down Expand Up @@ -9888,7 +9888,7 @@ impl fmt::Display for CopyLegacyOption {
}
Ok(())
}
Delimiter(char) => write!(f, "DELIMITER '{char}'"),
Delimiter(char) => write!(f, "DELIMITER '{}'", value::escape_single_quote_char(*char)),
EmptyAsNull => write!(f, "EMPTYASNULL"),
Encrypted { auto } => write!(f, "ENCRYPTED{}", if *auto { " AUTO" } else { "" }),
Escape => write!(f, "ESCAPE"),
Expand Down Expand Up @@ -10039,7 +10039,7 @@ impl fmt::Display for IamRoleKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
IamRoleKind::Default => write!(f, "DEFAULT"),
IamRoleKind::Arn(arn) => write!(f, "'{arn}'"),
IamRoleKind::Arn(arn) => write!(f, "'{}'", value::escape_single_quote_string(arn)),
}
}
}
Expand Down Expand Up @@ -10068,8 +10068,8 @@ impl fmt::Display for CopyLegacyCsvOption {
use CopyLegacyCsvOption::*;
match self {
Header => write!(f, "HEADER"),
Quote(char) => write!(f, "QUOTE '{char}'"),
Escape(char) => write!(f, "ESCAPE '{char}'"),
Quote(char) => write!(f, "QUOTE '{}'", value::escape_single_quote_char(*char)),
Escape(char) => write!(f, "ESCAPE '{}'", value::escape_single_quote_char(*char)),
ForceQuote(columns) => write!(f, "FORCE QUOTE {}", display_comma_separated(columns)),
ForceNotNull(columns) => {
write!(f, "FORCE NOT NULL {}", display_comma_separated(columns))
Expand Down
18 changes: 18 additions & 0 deletions src/ast/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,24 @@ pub fn escape_single_quote_string(s: &str) -> EscapeQuotedString<'_> {
escape_quoted_string(s, '\'')
}

pub struct EscapeSingleQuoteChar(char);

impl fmt::Display for EscapeSingleQuoteChar {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.0 == '\'' {
f.write_str("''")
} else {
write!(f, "{}", self.0)
}
}
}

/// Convenience wrapper for escaping a single character for a single-quoted
/// literal (`'`).
pub fn escape_single_quote_char(c: char) -> EscapeSingleQuoteChar {
EscapeSingleQuoteChar(c)
}

/// Convenience wrapper for escaping strings for double-quoted literals (`").`
pub fn escape_double_quote_string(s: &str) -> EscapeQuotedString<'_> {
escape_quoted_string(s, '\"')
Expand Down
75 changes: 75 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13014,3 +13014,78 @@ fn display_create_foreign_table_partition_of_propagates_formatter_errors() {
assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}");
}
}

#[test]
fn parse_copy_option_quoted_chars_round_trip() {
let stmt = pg().verified_stmt(
"COPY y TO STDOUT (FORMAT CSV, QUOTE '''', ESCAPE '''', DELIMITER '''', NULL 'it''s')",
);
assert_eq!(
stmt,
Statement::Copy {
source: CopySource::Table {
table_name: ObjectName::from(vec!["y".into()]),
columns: vec![],
},
to: true,
target: CopyTarget::Stdout,
options: vec![
CopyOption::Format("CSV".into()),
CopyOption::Quote('\''),
CopyOption::Escape('\''),
CopyOption::Delimiter('\''),
CopyOption::Null("it's".into()),
],
legacy_options: vec![],
payload: None,
}
);

let stmt = pg().verified_stmt("COPY y TO STDOUT DELIMITER '''' CSV QUOTE '''' ESCAPE ''''");
assert_eq!(
stmt,
Statement::Copy {
source: CopySource::Table {
table_name: ObjectName::from(vec!["y".into()]),
columns: vec![],
},
to: true,
target: CopyTarget::Stdout,
options: vec![],
legacy_options: vec![
CopyLegacyOption::Delimiter('\''),
CopyLegacyOption::Csv(vec![
CopyLegacyCsvOption::Quote('\''),
CopyLegacyCsvOption::Escape('\''),
]),
],
payload: None,
}
);

pg().one_statement_parses_to(
"COPY y TO stdout WITH CSV QUOTE '''' DELIMITER '|'",
"COPY y TO STDOUT CSV QUOTE '''' DELIMITER '|'",
);

let stmt = pg()
.verified_stmt("COPY users TO 'data.csv' IAM_ROLE 'arn:aws:iam::123456789:role/ro''le'");
assert_eq!(
stmt,
Statement::Copy {
source: CopySource::Table {
table_name: ObjectName::from(vec!["users".into()]),
columns: vec![],
},
to: true,
target: CopyTarget::File {
filename: "data.csv".into(),
},
options: vec![],
legacy_options: vec![CopyLegacyOption::IamRole(IamRoleKind::Arn(
"arn:aws:iam::123456789:role/ro'le".into(),
))],
payload: None,
}
);
}
Loading