Generic: Escape single quotes when displaying string literal options
diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 7b38f8c..01997db 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs
@@ -911,7 +911,7 @@ write!(f, "{sql_type}({len}")?; if let Some(time_zone) = time_zone { - write!(f, ", '{time_zone}'")?; + write!(f, ", '{}'", escape_single_quote_string(time_zone))?; } write!(f, ")")?;
diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 111439c..426c421 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs
@@ -34,7 +34,7 @@ #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; -use crate::ast::value::escape_single_quote_string; +use crate::ast::value::{escape_single_quote_string, SingleQuotedChar}; use crate::ast::{ display_comma_separated, display_separated, table_constraints::{ @@ -1039,7 +1039,7 @@ AlterTableOperation::Refresh { subpath } => { write!(f, "REFRESH")?; if let Some(path) = subpath { - write!(f, " '{path}'")?; + write!(f, " '{}'", escape_single_quote_string(path))?; } Ok(()) } @@ -1490,7 +1490,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Using(index_type) => write!(f, "USING {index_type}"), - Self::Comment(s) => write!(f, "COMMENT '{s}'"), + Self::Comment(s) => write!(f, "COMMENT '{}'", escape_single_quote_string(s)), } } } @@ -2729,7 +2729,9 @@ write!(f, "STORAGE = {}", storage) } UserDefinedTypeSqlDefinitionOption::Like(name) => write!(f, "LIKE = {}", name), - UserDefinedTypeSqlDefinitionOption::Category(c) => write!(f, "CATEGORY = '{}'", c), + UserDefinedTypeSqlDefinitionOption::Category(c) => { + write!(f, "CATEGORY = {}", SingleQuotedChar(*c)) + } UserDefinedTypeSqlDefinitionOption::Preferred(b) => write!(f, "PREFERRED = {}", b), UserDefinedTypeSqlDefinitionOption::Default(expr) => write!(f, "DEFAULT = {}", expr), UserDefinedTypeSqlDefinitionOption::Element(dt) => write!(f, "ELEMENT = {}", dt), @@ -3238,7 +3240,11 @@ }) = &self.hive_formats { match row_format { - Some(HiveRowFormat::SERDE { class }) => write!(f, " ROW FORMAT SERDE '{class}'")?, + Some(HiveRowFormat::SERDE { class }) => write!( + f, + " ROW FORMAT SERDE '{}'", + escape_single_quote_string(class) + )?, Some(HiveRowFormat::DELIMITED { delimiters }) => { write!(f, " ROW FORMAT DELIMITED")?; if !delimiters.is_empty() { @@ -3270,7 +3276,7 @@ } if !self.external { if let Some(loc) = location { - write!(f, " LOCATION '{loc}'")?; + write!(f, " LOCATION '{}'", escape_single_quote_string(loc))?; } } } @@ -3279,7 +3285,7 @@ write!(f, " STORED AS {file_format}")?; } if let Some(location) = &self.location { - write!(f, " LOCATION '{location}'")?; + write!(f, " LOCATION '{}'", escape_single_quote_string(location))?; } } @@ -3312,21 +3318,33 @@ write!(f, " {options}")?; } if let Some(external_volume) = self.external_volume.as_ref() { - write!(f, " EXTERNAL_VOLUME='{external_volume}'")?; + write!( + f, + " EXTERNAL_VOLUME='{}'", + escape_single_quote_string(external_volume) + )?; } if let Some(catalog) = self.catalog.as_ref() { - write!(f, " CATALOG='{catalog}'")?; + write!(f, " CATALOG='{}'", escape_single_quote_string(catalog))?; } if self.iceberg { if let Some(base_location) = self.base_location.as_ref() { - write!(f, " BASE_LOCATION='{base_location}'")?; + write!( + f, + " BASE_LOCATION='{}'", + escape_single_quote_string(base_location) + )?; } } if let Some(catalog_sync) = self.catalog_sync.as_ref() { - write!(f, " CATALOG_SYNC='{catalog_sync}'")?; + write!( + f, + " CATALOG_SYNC='{}'", + escape_single_quote_string(catalog_sync) + )?; } if let Some(storage_serialization_policy) = self.storage_serialization_policy.as_ref() { @@ -3371,7 +3389,11 @@ } if let Some(default_ddl_collation) = &self.default_ddl_collation { - write!(f, " DEFAULT_DDL_COLLATION='{default_ddl_collation}'",)?; + write!( + f, + " DEFAULT_DDL_COLLATION='{}'", + escape_single_quote_string(default_ddl_collation) + )?; } if let Some(with_aggregation_policy) = &self.with_aggregation_policy { @@ -3391,7 +3413,11 @@ } if let Some(target_lag) = &self.target_lag { - write!(f, " TARGET_LAG='{target_lag}'")?; + write!( + f, + " TARGET_LAG='{}'", + escape_single_quote_string(target_lag) + )?; } if let Some(warehouse) = &self.warehouse { @@ -3858,11 +3884,11 @@ )?; if let Some(connector_type) = &self.connector_type { - write!(f, " TYPE '{connector_type}'")?; + write!(f, " TYPE '{}'", escape_single_quote_string(connector_type))?; } if let Some(url) = &self.url { - write!(f, " URL '{url}'")?; + write!(f, " URL '{}'", escape_single_quote_string(url))?; } if let Some(comment) = &self.comment {
diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs index 5e997ba..3582e3c 100644 --- a/src/ast/helpers/stmt_data_loading.rs +++ b/src/ast/helpers/stmt_data_loading.rs
@@ -27,6 +27,7 @@ use serde::{Deserialize, Serialize}; use crate::ast::helpers::key_value_options::KeyValueOptions; +use crate::ast::value::escape_single_quote_string; use crate::ast::{Ident, ObjectName, SelectItem}; #[cfg(feature = "visitor")] use sqlparser_derive::{Visit, VisitMut}; @@ -87,13 +88,13 @@ impl fmt::Display for StageParamsObject { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(ref url) = self.url { - write!(f, " URL='{url}'")?; + write!(f, " URL='{}'", escape_single_quote_string(url))?; } if let Some(ref storage_integration) = self.storage_integration { write!(f, " STORAGE_INTEGRATION={storage_integration}")?; } if let Some(ref endpoint) = self.endpoint { - write!(f, " ENDPOINT='{endpoint}'")?; + write!(f, " ENDPOINT='{}'", escape_single_quote_string(endpoint))?; } if !self.credentials.options.is_empty() { write!(f, " CREDENTIALS=({})", self.credentials)?; @@ -138,7 +139,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.stage)?; if let Some(pattern) = self.pattern.as_ref() { - write!(f, " PATTERN='{pattern}'")?; + write!(f, " PATTERN='{}'", escape_single_quote_string(pattern))?; } Ok(()) }
diff --git a/src/ast/mod.rs b/src/ast/mod.rs index de925be..b1359b1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs
@@ -126,6 +126,8 @@ NormalizationForm, QuoteDelimitedString, TrimWhereField, Value, ValueWithSpan, }; +use self::value::{escape_single_quote_string, SingleQuotedChar}; + use crate::ast::helpers::key_value_options::KeyValueOptions; use crate::ast::helpers::stmt_data_loading::StageParamsObject; @@ -5286,7 +5288,7 @@ "INSERT{overwrite}{local} DIRECTORY '{path}'", overwrite = if *overwrite { " OVERWRITE" } else { "" }, local = if *local { " LOCAL" } else { "" }, - path = path + path = escape_single_quote_string(path) )?; if let Some(ref ff) = file_format { write!(f, " STORED AS {ff}")? @@ -5447,10 +5449,10 @@ )?; if let Some(l) = location { - write!(f, " LOCATION '{l}'")?; + write!(f, " LOCATION '{}'", escape_single_quote_string(l))?; } if let Some(ml) = managed_location { - write!(f, " MANAGEDLOCATION '{ml}'")?; + write!(f, " MANAGEDLOCATION '{}'", escape_single_quote_string(ml))?; } if let Some(clone) = clone { write!(f, " CLONE {clone}")?; @@ -5465,11 +5467,15 @@ } if let Some(vol) = external_volume { - write!(f, " EXTERNAL_VOLUME = '{vol}'")?; + write!( + f, + " EXTERNAL_VOLUME = '{}'", + escape_single_quote_string(vol) + )?; } if let Some(cat) = catalog { - write!(f, " CATALOG = '{cat}'")?; + write!(f, " CATALOG = '{}'", escape_single_quote_string(cat))?; } if let Some(true) = replace_invalid_characters { @@ -5479,7 +5485,11 @@ } if let Some(collation) = default_ddl_collation { - write!(f, " DEFAULT_DDL_COLLATION = '{collation}'")?; + write!( + f, + " DEFAULT_DDL_COLLATION = '{}'", + escape_single_quote_string(collation) + )?; } if let Some(policy) = storage_serialization_policy { @@ -5487,7 +5497,7 @@ } if let Some(comment) = comment { - write!(f, " COMMENT = '{comment}'")?; + write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?; } if let Some(charset) = default_charset { @@ -5499,7 +5509,7 @@ } if let Some(sync) = catalog_sync { - write!(f, " CATALOG_SYNC = '{sync}'")?; + write!(f, " CATALOG_SYNC = '{}'", escape_single_quote_string(sync))?; } if let Some(mode) = catalog_sync_namespace_mode { @@ -5507,7 +5517,11 @@ } if let Some(delim) = catalog_sync_namespace_flatten_delimiter { - write!(f, " CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '{delim}'")?; + write!( + f, + " CATALOG_SYNC_NAMESPACE_FLATTEN_DELIMITER = '{}'", + escape_single_quote_string(delim) + )?; } if let Some(tags) = with_tags { @@ -5585,7 +5599,7 @@ f, "LOAD DATA {local}INPATH '{inpath}' {overwrite}INTO TABLE {table_name}", local = if *local { "LOCAL " } else { "" }, - inpath = inpath, + inpath = escape_single_quote_string(inpath), overwrite = if *overwrite { "OVERWRITE " } else { "" }, table_name = table_name, )?; @@ -5731,7 +5745,7 @@ )?; } if let Some(url) = url { - write!(f, " SET URL '{url}'")?; + write!(f, " SET URL '{}'", escape_single_quote_string(url))?; } if let Some(owner) = owner { write!(f, " SET OWNER {owner}")?; @@ -6200,7 +6214,7 @@ }; write!(f, "ON {object_type} {object_name} IS ")?; if let Some(c) = comment { - write!(f, "'{c}'") + write!(f, "'{}'", escape_single_quote_string(c)) } else { write!(f, "NULL") } @@ -6307,7 +6321,7 @@ write!(f, " COPY_OPTIONS=({copy_options})")?; } if let Some(comment) = comment { - write!(f, " COMMENT='{}'", comment)?; + write!(f, " COMMENT='{}'", escape_single_quote_string(comment))?; } Ok(()) } @@ -6332,7 +6346,7 @@ write!(f, " {options}")?; } if let Some(comment) = comment { - write!(f, " COMMENT='{}'", comment)?; + write!(f, " COMMENT='{}'", escape_single_quote_string(comment))?; } Ok(()) } @@ -6384,10 +6398,16 @@ } if let Some(files) = files { - write!(f, " FILES = ('{}')", display_separated(files, "', '"))?; + f.write_str(" FILES = (")?; + let mut separator = ""; + for file in files { + write!(f, "{separator}'{}'", escape_single_quote_string(file))?; + separator = ", "; + } + f.write_str(")")?; } if let Some(pattern) = pattern { - write!(f, " PATTERN = '{pattern}'")?; + write!(f, " PATTERN = '{}'", escape_single_quote_string(pattern))?; } if let Some(partition) = partition { write!(f, " PARTITION BY {partition}")?; @@ -6449,7 +6469,7 @@ write!(f, "{query}")?; } if let Some(query_text) = query_text { - write!(f, "'{query_text}'")?; + write!(f, "'{}'", escape_single_quote_string(query_text))?; } write!(f, ") TO {to}")?; if let Some(auth) = auth { @@ -6509,7 +6529,7 @@ Statement::NOTIFY { channel, payload } => { write!(f, "NOTIFY {channel}")?; if let Some(payload) = payload { - write!(f, ", '{payload}'")?; + write!(f, ", '{}'", escape_single_quote_string(payload))?; } Ok(()) } @@ -6543,7 +6563,7 @@ stage, options, } => { - write!(f, "PUT '{source}' {stage}")?; + write!(f, "PUT '{}' {stage}", escape_single_quote_string(source))?; if !options.options.is_empty() { write!(f, " {options}")?; } @@ -9126,10 +9146,10 @@ } SqlOption::Comment(comment) => match comment { CommentDef::WithEq(comment) => { - write!(f, "COMMENT = '{comment}'") + write!(f, "COMMENT = '{}'", escape_single_quote_string(comment)) } CommentDef::WithoutEq(comment) => { - write!(f, "COMMENT '{comment}'") + write!(f, "COMMENT '{}'", escape_single_quote_string(comment)) } }, SqlOption::NamedParenthesizedList(value) => { @@ -9600,12 +9620,12 @@ 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 {}", SingleQuotedChar(*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 {}", SingleQuotedChar(*char)), + Escape(char) => write!(f, "ESCAPE {}", SingleQuotedChar(*char)), ForceQuote(columns) => write!(f, "FORCE_QUOTE ({})", display_comma_separated(columns)), ForceNotNull(columns) => { write!(f, "FORCE_NOT_NULL ({})", display_comma_separated(columns)) @@ -9758,7 +9778,7 @@ } Ok(()) } - Delimiter(char) => write!(f, "DELIMITER '{char}'"), + Delimiter(char) => write!(f, "DELIMITER {}", SingleQuotedChar(*char)), EmptyAsNull => write!(f, "EMPTYASNULL"), Encrypted { auto } => write!(f, "ENCRYPTED{}", if *auto { " AUTO" } else { "" }), Escape => write!(f, "ESCAPE"), @@ -9909,7 +9929,7 @@ 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, "'{}'", escape_single_quote_string(arn)), } } } @@ -9938,8 +9958,8 @@ 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 {}", SingleQuotedChar(*char)), + Escape(char) => write!(f, "ESCAPE {}", SingleQuotedChar(*char)), ForceQuote(columns) => write!(f, "FORCE QUOTE {}", display_comma_separated(columns)), ForceNotNull(columns) => { write!(f, "FORCE NOT NULL {}", display_comma_separated(columns)) @@ -10470,9 +10490,13 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "USING ")?; match self { - CreateFunctionUsing::Jar(uri) => write!(f, "JAR '{uri}'"), - CreateFunctionUsing::File(uri) => write!(f, "FILE '{uri}'"), - CreateFunctionUsing::Archive(uri) => write!(f, "ARCHIVE '{uri}'"), + CreateFunctionUsing::Jar(uri) => write!(f, "JAR '{}'", escape_single_quote_string(uri)), + CreateFunctionUsing::File(uri) => { + write!(f, "FILE '{}'", escape_single_quote_string(uri)) + } + CreateFunctionUsing::Archive(uri) => { + write!(f, "ARCHIVE '{}'", escape_single_quote_string(uri)) + } } } } @@ -10887,7 +10911,12 @@ impl Display for Tag { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}='{}'", self.key, self.value) + write!( + f, + "{}='{}'", + self.key, + escape_single_quote_string(&self.value) + ) } } @@ -10925,7 +10954,9 @@ impl Display for CommentDef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => write!(f, "{comment}"), + CommentDef::WithEq(comment) | CommentDef::WithoutEq(comment) => { + write!(f, "{}", escape_single_quote_string(comment)) + } } } }
diff --git a/src/ast/query.rs b/src/ast/query.rs index 95f9801..fe058dd 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs
@@ -31,6 +31,8 @@ tokenizer::{Token, TokenWithSpan}, }; +use crate::ast::value::escape_single_quote_string; + /// The most complete variant of a `SELECT` query expression, optionally /// including `WITH`, `UNION` / other set operations, and `ORDER BY`. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -3940,7 +3942,7 @@ write!(f, "FOR JSON ")?; write!(f, "{for_json}")?; if let Some(root) = root { - write!(f, ", ROOT('{root}')")?; + write!(f, ", ROOT('{}')", escape_single_quote_string(root))?; } if *include_null_values { write!(f, ", INCLUDE_NULL_VALUES")?; @@ -3966,7 +3968,7 @@ write!(f, ", TYPE")?; } if let Some(root) = root { - write!(f, ", ROOT('{root}')")?; + write!(f, ", ROOT('{}')", escape_single_quote_string(root))?; } if *elements { write!(f, ", ELEMENTS")?; @@ -3998,7 +4000,7 @@ ForXml::Raw(root) => { write!(f, "RAW")?; if let Some(root) = root { - write!(f, "('{root}')")?; + write!(f, "('{}')", escape_single_quote_string(root))?; } Ok(()) } @@ -4007,7 +4009,7 @@ ForXml::Path(root) => { write!(f, "PATH")?; if let Some(root) = root { - write!(f, "('{root}')")?; + write!(f, "('{}')", escape_single_quote_string(root))?; } Ok(()) }
diff --git a/src/ast/value.rs b/src/ast/value.rs index 5b05af6..0580a3f 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs
@@ -618,6 +618,19 @@ escape_quoted_string(s, '\"') } +/// A character written as a single-quoted SQL string literal. +pub(crate) struct SingleQuotedChar(pub(crate) char); + +impl fmt::Display for SingleQuotedChar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0 == '\'' { + f.write_str("''''") + } else { + write!(f, "'{}'", self.0) + } + } +} + pub struct EscapeEscapedStringLiteral<'a>(&'a str); impl fmt::Display for EscapeEscapedStringLiteral<'_> {
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8e08c65..80a8344 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs
@@ -20433,3 +20433,13 @@ dialects.verified_stmt("SET eaac_cion = ! !o"); dialects.one_statement_parses_to("SET eaac_cion = ! ! o", "SET eaac_cion = ! !o"); } + +#[test] +fn display_doubled_quotes_in_string_literal_options() { + pg_and_generic().verified_stmt(r#"CREATE TABLE t (a INT) COMMENT = 'it''s'"#); + pg_and_generic().verified_stmt(r#"CREATE TABLE t (a INT) COMMENT 'it''s'"#); + pg_and_generic().verified_stmt(r#"CREATE EXTERNAL TABLE t () LOCATION 'a''b'"#); + pg_and_generic().verified_stmt( + r#"COPY t FROM 'f' (FORMAT text, DELIMITER '''', NULL 'n''u', QUOTE '''', ESCAPE '''')"#, + ); +}
diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 6986ba0..c55b577 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs
@@ -5071,3 +5071,11 @@ ParserError::ParserError("Expected: stage name identifier, found: .".to_string()), ); } + +#[test] +fn display_doubled_quotes_in_stage_options() { + snowflake().verified_stmt(r#"PUT 'fi''le' @stage"#); + snowflake().verified_stmt(r#"CREATE STAGE s URL='a''b'"#); + snowflake().verified_stmt(r#"CREATE TABLE t (a INT) DEFAULT_DDL_COLLATION='x''y'"#); + snowflake().verified_stmt(r#"COPY INTO t FROM @s FILES = ('a''b') PATTERN = 'x''y'"#); +}