sqlx로 데이터베이스 쓰기 문서의 검증 코드다.
//! sqlx 0.9에서 `format!`으로 만든 String을 `sqlx::query`에 바로 넘기면 컴파일 오류가 나는지 확인한다.
//! 의존성: sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "sqlite", "macros", "migrate"] }
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/46_sqlx-database_verify.rs"
//!
//! 이 바이너리 옆의 deps 폴더에 있는 sqlx rlib을 `--extern`으로 넘겨 코드 조각을 rustc로 컴파일한다.
use std::path::PathBuf;
use std::process::Command;
// 이 파일 자체도 sqlx를 링크해야 deps 폴더에 rlib이 생긴다.
#[allow(dead_code)]
fn link_sqlx() -> sqlx::query::Query<'static, sqlx::Sqlite, sqlx::sqlite::SqliteArguments> {
sqlx::query("SELECT 1")
}
fn deps_dir() -> PathBuf {
let exe = std::env::current_exe().expect("실행 파일 경로");
exe.parent().expect("부모 폴더").join("deps")
}
fn sqlx_rlib(deps: &PathBuf) -> PathBuf {
let mut found: Vec<PathBuf> = std::fs::read_dir(deps)
.expect("deps 읽기")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| {
let name = p.file_name().unwrap().to_string_lossy().into_owned();
name.starts_with("libsqlx-") && name.ends_with(".rlib")
})
.collect();
found.sort();
found.pop().expect("libsqlx-*.rlib")
}
fn compile_with_sqlx(code: &str) -> (bool, String) {
let deps = deps_dir();
let rlib = sqlx_rlib(&deps);
let dir = std::env::temp_dir().join(format!("vr46_{}_{}", std::process::id(), code.len()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("snippet.rs");
std::fs::write(&file, code).unwrap();
let mut extern_arg = std::ffi::OsString::from("sqlx=");
extern_arg.push(&rlib);
let mut dep_arg = std::ffi::OsString::from("dependency=");
dep_arg.push(&deps);
let out = Command::new("rustc")
.args(["--edition", "2024", "--crate-type=lib", "--emit=metadata"])
.arg("-L")
.arg(dep_arg)
.arg("--extern")
.arg(extern_arg)
.arg("--out-dir")
.arg(&dir)
.arg(&file)
.output()
.expect("rustc 실행");
let _ = std::fs::remove_dir_all(&dir);
(
out.status.success(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
const DYNAMIC: &str = r#"
pub fn by_code(code: &str) {
let sql = format!("SELECT * FROM links WHERE code = '{code}'");
let _q = sqlx::query::<sqlx::Sqlite>(sql);
}
"#;
const ASSERTED: &str = r#"
pub fn by_code(code: &str) {
let sql = format!("SELECT * FROM links WHERE code = '{code}'");
let _q = sqlx::query::<sqlx::Sqlite>(sqlx::AssertSqlSafe(sql));
}
"#;
const BOUND: &str = r#"
pub fn by_code(code: &str) {
let _q = sqlx::query::<sqlx::Sqlite>("SELECT * FROM links WHERE code = ?").bind(code.to_owned());
}
"#;
fn main() {
let (ok, err) = compile_with_sqlx(DYNAMIC);
assert!(!ok, "String을 바로 넘기면 컴파일되지 않아야 한다");
assert!(err.contains("E0277"), "{err}");
assert!(
err.contains("dynamic SQL strings should be audited for possible injections"),
"{err}"
);
let first = err
.lines()
.find(|l| l.starts_with("error"))
.unwrap_or_default();
println!("String 직접 전달: {first}");
let (ok, err) = compile_with_sqlx(ASSERTED);
assert!(ok, "AssertSqlSafe로 감싸면 컴파일된다: {err}");
println!("AssertSqlSafe(String): 컴파일됨");
let (ok, err) = compile_with_sqlx(BOUND);
assert!(ok, "&'static str + bind는 컴파일된다: {err}");
println!("&'static str + bind: 컴파일됨");
println!("ALL CHECKS PASSED");
}