pub fn to_snake_case(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() {
if i != 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
pub fn validate_sql_identifier(value: &str, span: proc_macro2::Span) -> syn::Result<()> {
if value.contains('"') {
return Err(syn::Error::new(
span,
"SQL 식별자에는 큰따옴표를 사용할 수 없습니다",
));
}
Ok(())
}
pub fn extract_named_params(sql: &str) -> Vec<String> {
let bytes = sql.as_bytes();
let mut out: Vec<String> = Vec::new();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\'' => {
i += 1;
while i < bytes.len() {
if bytes[i] == b'\'' {
if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
i += 2;
continue;
}
break;
}
i += 1;
}
i += 1;
}
b'"' => {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
i += 1;
}
i += 1;
}
b'`' => {
i += 1;
while i < bytes.len() && bytes[i] != b'`' {
i += 1;
}
i += 1;
}
b'[' => {
i += 1;
while i < bytes.len() && bytes[i] != b']' {
i += 1;
}
i += 1;
}
b'-' if i + 1 < bytes.len() && bytes[i + 1] == b'-' => {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < bytes.len() && bytes[i + 1] == b'*' => {
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
i += 1;
}
i += 2;
}
b':' if i + 1 < bytes.len() && bytes[i + 1] == b':' => {
i += 2;
}
b':' => {
let start = i + 1;
let mut j = start;
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
}
if j > start {
let name = &sql[start..j];
if !out.iter().any(|n| n == name) {
out.push(name.to_string());
}
}
i = j;
}
prefix @ (b'@' | b'$') => {
let start = i + 1;
let mut j = start;
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
}
if j > start {
let name = format!("{}{}", prefix as char, &sql[start..j]);
if !out.contains(&name) {
out.push(name);
}
}
i = j;
}
b'?' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => {
let start = i;
let mut j = i + 1;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let name = sql[start..j].to_string();
if !out.contains(&name) {
out.push(name);
}
i = j;
}
_ => i += 1,
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snake_case_basic() {
assert_eq!(to_snake_case("TodoDao"), "todo_dao");
assert_eq!(to_snake_case("UserPrefsDao"), "user_prefs_dao");
assert_eq!(to_snake_case("Db"), "db");
}
#[test]
fn params_extraction() {
assert_eq!(
extract_named_params("SELECT * FROM t WHERE a = :a AND b = ':not' AND c = :b -- :no"),
vec!["a", "b"]
);
assert_eq!(extract_named_params("SELECT :x, :x, \":q\""), vec!["x"]);
}
#[test]
fn params_skip_quoted_identifiers() {
assert_eq!(
extract_named_params("SELECT `a:b`, [c:d] FROM t WHERE x = :x"),
vec!["x"]
);
assert!(extract_named_params("SELECT `a:b FROM t WHERE x = :x").is_empty());
}
#[test]
fn unsupported_parameter_forms_are_detected() {
assert_eq!(
extract_named_params("SELECT @id, $name, ?12"),
vec!["@id", "$name", "?12"]
);
}
}