use std::borrow::Cow;
#[must_use]
pub fn escape_sql_string_literal(s: &str) -> Cow<'_, str> {
if s.contains('\'') {
Cow::Owned(s.replace('\'', "''"))
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_string_borrows() {
let out = escape_sql_string_literal("");
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, "");
}
#[test]
fn clean_string_borrows() {
let out = escape_sql_string_literal("hello world");
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, "hello world");
}
#[test]
fn single_quote_in_middle_escapes() {
let out = escape_sql_string_literal("O'Brien");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "O''Brien");
}
#[test]
fn leading_quote_escapes() {
let out = escape_sql_string_literal("'leading");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "''leading");
}
#[test]
fn trailing_quote_escapes() {
let out = escape_sql_string_literal("trailing'");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "trailing''");
}
#[test]
fn only_quote_escapes() {
let out = escape_sql_string_literal("'");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "''");
}
#[test]
fn multiple_quotes_escape_each() {
let out = escape_sql_string_literal("a'b'c'");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "a''b''c''");
}
#[test]
fn consecutive_quotes_escape_each() {
let out = escape_sql_string_literal("''");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "''''");
}
#[test]
fn unicode_preserved_when_borrowed() {
let out = escape_sql_string_literal("héllo 世界");
assert!(matches!(out, Cow::Borrowed(_)));
assert_eq!(out, "héllo 世界");
}
#[test]
fn unicode_preserved_when_owned() {
let out = escape_sql_string_literal("héllo'世界");
assert!(matches!(out, Cow::Owned(_)));
assert_eq!(out, "héllo''世界");
}
#[test]
fn matches_naive_replace() {
for s in [
"",
"x",
"'",
"''",
"a'",
"'a",
"a'b",
"O'Brien's",
"no quotes here",
"héllo 世界",
] {
assert_eq!(
escape_sql_string_literal(s).as_ref(),
s.replace('\'', "''"),
"mismatch for input {s:?}"
);
}
}
}