You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by lu...@apache.org on 2026/09/23 04:11:15 UTC

(datafusion-sqlparser-rs) 01/01: Generic: Escape single quotes when displaying string literal options

This is an automated email from the ASF dual-hosted git repository.

LucaCappelletti94 pushed a commit to branch escape-statement-strings
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit 07083de0c5b94da6aae0795288584e7666ab6d68
Author: LucaCappelletti94 <ca...@gmail.com>
AuthorDate: Wed Sep 23 06:11:04 2026 +0200

    Generic: Escape single quotes when displaying string literal options
---
 src/ast/data_type.rs                 |  2 +-
 src/ast/ddl.rs                       | 60 +++++++++++++++-------
 src/ast/helpers/stmt_data_loading.rs |  7 +--
 src/ast/mod.rs                       | 97 ++++++++++++++++++++++++------------
 src/ast/query.rs                     | 10 ++--
 src/ast/value.rs                     | 13 +++++
 tests/sqlparser_common.rs            | 10 ++++
 tests/sqlparser_snowflake.rs         |  8 +++
 8 files changed, 149 insertions(+), 58 deletions(-)

diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs
index be1acd1a7..6be7dbed3 100644
--- a/src/ast/data_type.rs
+++ b/src/ast/data_type.rs
@@ -893,7 +893,7 @@ fn format_clickhouse_datetime_precision_and_timezone(
     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 66f2cad3e..9de62f12a 100644
--- a/src/ast/ddl.rs
+++ b/src/ast/ddl.rs
@@ -34,7 +34,7 @@ use serde::{Deserialize, Serialize};
 #[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::{
@@ -1028,7 +1028,7 @@ impl fmt::Display for AlterTableOperation {
             AlterTableOperation::Refresh { subpath } => {
                 write!(f, "REFRESH")?;
                 if let Some(path) = subpath {
-                    write!(f, " '{path}'")?;
+                    write!(f, " '{}'", escape_single_quote_string(path))?;
                 }
                 Ok(())
             }
@@ -1479,7 +1479,7 @@ impl fmt::Display for IndexOption {
     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)),
         }
     }
 }
@@ -2718,7 +2718,9 @@ impl fmt::Display for UserDefinedTypeSqlDefinitionOption {
                 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),
@@ -3173,7 +3175,7 @@ impl fmt::Display for CreateTable {
         // Hive table comment should be after column definitions, please refer to:
         // [Hive](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-CreateTable)
         if let Some(comment) = &self.comment {
-            write!(f, " COMMENT '{comment}'")?;
+            write!(f, " COMMENT '{}'", comment)?;
         }
 
         // Only for SQLite
@@ -3227,7 +3229,11 @@ impl fmt::Display for CreateTable {
         }) = &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() {
@@ -3259,7 +3265,7 @@ impl fmt::Display for CreateTable {
             }
             if !self.external {
                 if let Some(loc) = location {
-                    write!(f, " LOCATION '{loc}'")?;
+                    write!(f, " LOCATION '{}'", escape_single_quote_string(loc))?;
                 }
             }
         }
@@ -3268,7 +3274,7 @@ impl fmt::Display for CreateTable {
                 write!(f, " STORED AS {file_format}")?;
             }
             if let Some(location) = &self.location {
-                write!(f, " LOCATION '{location}'")?;
+                write!(f, " LOCATION '{}'", escape_single_quote_string(location))?;
             }
         }
 
@@ -3301,21 +3307,33 @@ impl fmt::Display for CreateTable {
             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() {
@@ -3360,7 +3378,11 @@ impl fmt::Display for CreateTable {
         }
 
         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 {
@@ -3380,7 +3402,11 @@ impl fmt::Display for CreateTable {
         }
 
         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 {
@@ -3847,15 +3873,15 @@ impl fmt::Display for CreateConnector {
         )?;
 
         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 {
-            write!(f, " COMMENT = '{comment}'")?;
+            write!(f, " COMMENT = '{}'", comment)?;
         }
 
         if let Some(with_dcproperties) = &self.with_dcproperties {
diff --git a/src/ast/helpers/stmt_data_loading.rs b/src/ast/helpers/stmt_data_loading.rs
index 5e997bac7..3582e3c57 100644
--- a/src/ast/helpers/stmt_data_loading.rs
+++ b/src/ast/helpers/stmt_data_loading.rs
@@ -27,6 +27,7 @@ use core::fmt;
 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 @@ pub struct StageLoadSelectItem {
 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 @@ impl fmt::Display for FileStagingCommand {
     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 dcde4fc38..06dc98821 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -126,6 +126,8 @@ pub use self::value::{
     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;
 
@@ -5272,7 +5274,7 @@ impl fmt::Display for Statement {
                     "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}")?
@@ -5433,10 +5435,10 @@ impl fmt::Display for Statement {
                 )?;
 
                 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}")?;
@@ -5451,11 +5453,15 @@ impl fmt::Display for Statement {
                 }
 
                 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 {
@@ -5465,7 +5471,11 @@ impl fmt::Display for Statement {
                 }
 
                 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 {
@@ -5473,7 +5483,7 @@ impl fmt::Display for Statement {
                 }
 
                 if let Some(comment) = comment {
-                    write!(f, " COMMENT = '{comment}'")?;
+                    write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?;
                 }
 
                 if let Some(charset) = default_charset {
@@ -5485,7 +5495,7 @@ impl fmt::Display for Statement {
                 }
 
                 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 {
@@ -5493,7 +5503,11 @@ impl fmt::Display for Statement {
                 }
 
                 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 {
@@ -5571,7 +5585,7 @@ impl fmt::Display for Statement {
                     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,
                 )?;
@@ -5716,7 +5730,7 @@ impl fmt::Display for Statement {
                     )?;
                 }
                 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}")?;
@@ -6185,7 +6199,7 @@ impl fmt::Display for Statement {
                 };
                 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")
                 }
@@ -6292,7 +6306,7 @@ impl fmt::Display for Statement {
                     write!(f, " COPY_OPTIONS=({copy_options})")?;
                 }
                 if let Some(comment) = comment {
-                    write!(f, " COMMENT='{}'", comment)?;
+                    write!(f, " COMMENT='{}'", escape_single_quote_string(comment))?;
                 }
                 Ok(())
             }
@@ -6317,7 +6331,7 @@ impl fmt::Display for Statement {
                     write!(f, " {options}")?;
                 }
                 if let Some(comment) = comment {
-                    write!(f, " COMMENT='{}'", comment)?;
+                    write!(f, " COMMENT='{}'", escape_single_quote_string(comment))?;
                 }
                 Ok(())
             }
@@ -6369,10 +6383,16 @@ impl fmt::Display for Statement {
                 }
 
                 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}")?;
@@ -6434,7 +6454,7 @@ impl fmt::Display for Statement {
                     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 {
@@ -6494,7 +6514,7 @@ impl fmt::Display for Statement {
             Statement::NOTIFY { channel, payload } => {
                 write!(f, "NOTIFY {channel}")?;
                 if let Some(payload) = payload {
-                    write!(f, ", '{payload}'")?;
+                    write!(f, ", '{}'", escape_single_quote_string(payload))?;
                 }
                 Ok(())
             }
@@ -6528,7 +6548,7 @@ impl fmt::Display for Statement {
                 stage,
                 options,
             } => {
-                write!(f, "PUT '{source}' {stage}")?;
+                write!(f, "PUT '{}' {stage}", escape_single_quote_string(source))?;
                 if !options.options.is_empty() {
                     write!(f, " {options}")?;
                 }
@@ -9118,10 +9138,10 @@ impl fmt::Display for SqlOption {
             }
             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) => {
@@ -9592,12 +9612,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 {}", 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))
@@ -9750,7 +9770,7 @@ impl fmt::Display for CopyLegacyOption {
                 }
                 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"),
@@ -9901,7 +9921,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, "'{}'", escape_single_quote_string(arn)),
         }
     }
 }
@@ -9930,8 +9950,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 {}", 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))
@@ -10462,9 +10482,13 @@ impl fmt::Display for CreateFunctionUsing {
     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))
+            }
         }
     }
 }
@@ -10882,7 +10906,12 @@ impl Tag {
 
 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)
+        )
     }
 }
 
@@ -10920,7 +10949,9 @@ pub enum CommentDef {
 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 98a9c84ae..9fd8cc105 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -31,6 +31,8 @@ use crate::{
     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)]
@@ -3937,7 +3939,7 @@ impl fmt::Display for ForClause {
                 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")?;
@@ -3963,7 +3965,7 @@ impl fmt::Display for ForClause {
                     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")?;
@@ -3995,7 +3997,7 @@ impl fmt::Display for ForXml {
             ForXml::Raw(root) => {
                 write!(f, "RAW")?;
                 if let Some(root) = root {
-                    write!(f, "('{root}')")?;
+                    write!(f, "('{}')", escape_single_quote_string(root))?;
                 }
                 Ok(())
             }
@@ -4004,7 +4006,7 @@ impl fmt::Display for ForXml {
             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 5b05af694..0580a3f7a 100644
--- a/src/ast/value.rs
+++ b/src/ast/value.rs
@@ -618,6 +618,19 @@ pub fn escape_double_quote_string(s: &str) -> EscapeQuotedString<'_> {
     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 9fb208bd5..2653bf67c 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -20143,3 +20143,13 @@ fn parse_stage_table_factor() {
         ParserError::ParserError("Expected: identifier, found: @".to_string()),
     );
 }
+
+#[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 37222341b..0f4a5b9d2 100644
--- a/tests/sqlparser_snowflake.rs
+++ b/tests/sqlparser_snowflake.rs
@@ -4959,3 +4959,11 @@ fn test_stage_name_delimiters() {
         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'"#);
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org