Skip to content

Commit 569e533

Browse files
authored
fix: escape chars (#415)
1 parent 25687ad commit 569e533

1 file changed

Lines changed: 39 additions & 13 deletions

File tree

core/src/database/sql_type_wrapper.rs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ use tokio_postgres::types::{to_sql_checked, IsNull, ToSql, Type as PgType};
1515
use tracing::error;
1616
use 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)]
1927
pub 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

Comments
 (0)