@@ -15,6 +15,14 @@ use tokio_postgres::types::{to_sql_checked, IsNull, ToSql, Type as PgType};
1515use tracing:: error;
1616use uuid:: Uuid ;
1717
18+ /// Escape a string for safe use inside a ClickHouse single-quoted literal.
19+ /// Backslashes must be escaped first, then single quotes — ClickHouse uses
20+ /// C-style backslash escaping, so an unescaped trailing `\` would consume
21+ /// the closing `'` delimiter and break out of the string.
22+ fn ch_escape ( s : & str ) -> String {
23+ s. replace ( '\\' , "\\ \\ " ) . replace ( '\'' , "\\ '" )
24+ }
25+
1826#[ derive( Debug , Clone ) ]
1927pub enum EthereumSqlTypeWrapper {
2028 // Boolean
@@ -449,14 +457,10 @@ impl EthereumSqlTypeWrapper {
449457 ) ,
450458
451459 // Strings and Bytes
452- EthereumSqlTypeWrapper :: String ( value) => format ! ( "'{}'" , value . replace ( "'" , " \\ '" ) ) ,
460+ EthereumSqlTypeWrapper :: String ( value) => format ! ( "'{}'" , ch_escape ( value ) ) ,
453461 EthereumSqlTypeWrapper :: VecString ( values) => format ! (
454462 "[{}]" ,
455- values
456- . iter( )
457- . map( |v| format!( "'{}'" , v. replace( "'" , "\\ '" ) ) )
458- . collect:: <Vec <_>>( )
459- . join( ", " )
463+ values. iter( ) . map( |v| format!( "'{}'" , ch_escape( v) ) ) . collect:: <Vec <_>>( ) . join( ", " )
460464 ) ,
461465 EthereumSqlTypeWrapper :: Bytes ( value) => format ! ( "'0x{}'" , hex:: encode( value) ) ,
462466 EthereumSqlTypeWrapper :: VecBytes ( values) => format ! (
@@ -491,14 +495,14 @@ impl EthereumSqlTypeWrapper {
491495 EthereumSqlTypeWrapper :: U64Nullable ( v) => v. to_string ( ) ,
492496 EthereumSqlTypeWrapper :: U256Nullable ( v) => v. to_string ( ) ,
493497 EthereumSqlTypeWrapper :: U64BigInt ( v) => v. to_string ( ) ,
494- EthereumSqlTypeWrapper :: StringVarchar ( v) => format ! ( "'{}'" , v . replace ( "'" , " \\ '" ) ) ,
495- EthereumSqlTypeWrapper :: StringChar ( v) => format ! ( "'{}'" , v . replace ( "'" , " \\ '" ) ) ,
496- EthereumSqlTypeWrapper :: StringNullable ( v) => format ! ( "'{}'" , v . replace ( "'" , " \\ '" ) ) ,
498+ EthereumSqlTypeWrapper :: StringVarchar ( v) => format ! ( "'{}'" , ch_escape ( v ) ) ,
499+ EthereumSqlTypeWrapper :: StringChar ( v) => format ! ( "'{}'" , ch_escape ( v ) ) ,
500+ EthereumSqlTypeWrapper :: StringNullable ( v) => format ! ( "'{}'" , ch_escape ( v ) ) ,
497501 EthereumSqlTypeWrapper :: StringVarcharNullable ( v) => {
498- format ! ( "'{}'" , v . replace ( "'" , " \\ '" ) )
502+ format ! ( "'{}'" , ch_escape ( v ) )
499503 }
500504 EthereumSqlTypeWrapper :: StringCharNullable ( v) => {
501- format ! ( "'{}'" , v . replace ( "'" , " \\ '" ) )
505+ format ! ( "'{}'" , ch_escape ( v ) )
502506 }
503507 EthereumSqlTypeWrapper :: AddressNullable ( v) => format ! ( "'{v}'" ) ,
504508 EthereumSqlTypeWrapper :: BytesNullable ( v) => format ! ( "'0x{}'" , hex:: encode( v) ) ,
@@ -518,7 +522,7 @@ impl EthereumSqlTypeWrapper {
518522 "[{}]" ,
519523 values
520524 . iter( )
521- . map( |v| format!( "'{}'" , v . replace ( "'" , " \\ '" ) ) )
525+ . map( |v| format!( "'{}'" , ch_escape ( v ) ) )
522526 . collect:: <Vec <_>>( )
523527 . join( ", " )
524528 )
@@ -528,7 +532,7 @@ impl EthereumSqlTypeWrapper {
528532 "[{}]" ,
529533 values
530534 . iter( )
531- . map( |v| format!( "'{}'" , v . replace ( "'" , " \\ '" ) ) )
535+ . map( |v| format!( "'{}'" , ch_escape ( v ) ) )
532536 . collect:: <Vec <_>>( )
533537 . join( ", " )
534538 )
@@ -2459,6 +2463,28 @@ mod tests {
24592463 assert_eq ! ( wrapper. to_clickhouse_value( ) , "'abc\\ 'def'" ) ;
24602464 }
24612465
2466+ #[ test]
2467+ fn test_ch_string_escapes_backslash_before_quotes ( ) {
2468+ // A trailing backslash without escaping would consume the closing quote:
2469+ // 'hello\' → ClickHouse sees the \' as an escaped quote, string never closes.
2470+ let wrapper = EthereumSqlTypeWrapper :: String ( "hello\\ " . to_string ( ) ) ;
2471+ assert_eq ! ( wrapper. to_clickhouse_value( ) , "'hello\\ \\ '" ) ;
2472+
2473+ // Backslash followed by quote: both must be escaped independently.
2474+ let wrapper = EthereumSqlTypeWrapper :: StringVarchar ( "a\\ 'b" . to_string ( ) ) ;
2475+ assert_eq ! ( wrapper. to_clickhouse_value( ) , "'a\\ \\ \\ 'b'" ) ;
2476+ }
2477+
2478+ #[ test]
2479+ fn test_ch_string_escapes_backslash_in_vec ( ) {
2480+ let wrapper = EthereumSqlTypeWrapper :: VecStringVarchar ( vec ! [
2481+ "normal" . to_string( ) ,
2482+ "back\\ slash" . to_string( ) ,
2483+ "quo'te" . to_string( ) ,
2484+ ] ) ;
2485+ assert_eq ! ( wrapper. to_clickhouse_value( ) , "['normal', 'back\\ \\ slash', 'quo\\ 'te']" ) ;
2486+ }
2487+
24622488 #[ test]
24632489 fn test_ch_vec_string_char_is_quoted ( ) {
24642490 let wrapper =
0 commit comments