From b8d5e3e4b2c541b35f30af7902172544deac9de0 Mon Sep 17 00:00:00 2001 From: Cora Sutton Date: Fri, 11 Sep 2026 06:09:41 +0000 Subject: [PATCH] DuckDB dialect support for escape string literals --- src/dialect/duckdb.rs | 4 ++++ src/parser/mod.rs | 5 ++--- tests/sqlparser_duckdb.rs | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 2e3673bc4c..3cf98ce344 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -142,4 +142,8 @@ impl Dialect for DuckDbDialect { fn supports_numeric_literal_underscores(&self) -> bool { true } + + fn supports_string_escape_constant(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5edc437145..0c395510ac 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1965,8 +1965,7 @@ impl<'a> Parser<'a> { ), }) } - Token::EscapedStringLiteral(_) if dialect_is!(dialect is PostgreSqlDialect | GenericDialect) => - { + Token::EscapedStringLiteral(_) if self.dialect.supports_string_escape_constant() => { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } @@ -12669,7 +12668,7 @@ impl<'a> Parser<'a> { }) => Ok(value), Token::SingleQuotedString(s) => Ok(s), Token::DoubleQuotedString(s) => Ok(s), - Token::EscapedStringLiteral(s) if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { + Token::EscapedStringLiteral(s) if self.dialect.supports_string_escape_constant() => { Ok(s) } Token::UnicodeStringLiteral(s) => Ok(s), diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a338ef7a82..25cebe01d6 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -23,7 +23,7 @@ use sqlparser::tokenizer::Span; use test_utils::*; use sqlparser::ast::*; -use sqlparser::dialect::{DuckDbDialect, GenericDialect}; +use sqlparser::dialect::{AnsiDialect, DuckDbDialect, GenericDialect}; use sqlparser::parser::ParserError; fn duckdb() -> TestedDialects { @@ -910,3 +910,39 @@ fn test_duckdb_lambda_function() { let sql_transform = "SELECT list_transform([1, 2, 3], lambda x : x * 2)"; duckdb().verified_stmt(sql_transform); } + +#[test] +fn test_escape_string_literal() { + duckdb().verified_stmt(r#"SELECT E'a\nb' AS value"#); + + assert_eq!( + duckdb().verified_expr(r#"E'a\nb'"#), + Expr::Value((Value::EscapedStringLiteral("a\nb".to_string())).with_empty_span()) + ); + assert_eq!( + duckdb().verified_expr(r#"'a\nb'"#), + Expr::Value((Value::SingleQuotedString(r#"a\nb"#.to_string())).with_empty_span()) + ); + duckdb().one_statement_parses_to( + r#"COPY data TO E'out\n.csv'"#, + r#"COPY data TO 'out +.csv'"#, + ); + + let err = duckdb() + .parse_sql_statements("SELECT E'unterminated") + .unwrap_err(); + assert_eq!( + err, + ParserError::TokenizerError( + "Unterminated encoded string literal at Line: 1, Column: 8".to_string() + ) + ); + + let ansi = TestedDialects::new(vec![Box::new(AnsiDialect {})]); + assert_eq!( + ansi.parse_sql_statements(r#"SELECT E'a\nb' AS value"#) + .unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: AS".to_string()) + ); +}