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