1pub fn inject_max_execution_time(sql: &str, ms: u32) -> String {
6 let trimmed = sql.trim_start();
7 let leading = sql.len() - trimmed.len();
8 let lower = trimmed.to_ascii_lowercase();
9 if !lower.starts_with("select") {
10 return sql.to_string();
11 }
12 if lower.contains("/*+ max_execution_time") {
13 return sql.to_string();
14 }
15 let after = leading + "select".len();
16 format!(
17 "{}{} /*+ MAX_EXECUTION_TIME({ms}) */{}",
18 &sql[..leading],
19 &sql[leading..after],
20 &sql[after..]
21 )
22}
23
24#[cfg(test)]
25mod tests {
26 use super::*;
27
28 #[test]
29 fn injects_after_select() {
30 assert_eq!(
31 inject_max_execution_time("SELECT * FROM users", 5000),
32 "SELECT /*+ MAX_EXECUTION_TIME(5000) */ * FROM users"
33 );
34 }
35
36 #[test]
37 fn lowercase_and_leading_whitespace() {
38 assert_eq!(
39 inject_max_execution_time(" select 1", 50),
40 " select /*+ MAX_EXECUTION_TIME(50) */ 1"
41 );
42 }
43
44 #[test]
46 fn matches_legacy_fixtures() {
47 let path = concat!(
48 env!("CARGO_MANIFEST_DIR"),
49 "/tests/fixtures/legacy/hints.json"
50 );
51 let Ok(data) = std::fs::read_to_string(path) else {
54 eprintln!("skipping: legacy fixture corpus not present ({path})");
55 return;
56 };
57 let cases: Vec<serde_json::Value> = serde_json::from_str(&data).unwrap();
58 assert!(!cases.is_empty());
59 for case in cases {
60 let sql = case["sql"].as_str().unwrap();
61 let ms = case["ms"].as_u64().unwrap() as u32;
62 let expected = case["result"].as_str().unwrap();
63 assert_eq!(inject_max_execution_time(sql, ms), expected, "sql: {sql:?}");
64 }
65 }
66
67 #[test]
68 fn existing_hint_left_alone() {
69 let sql = "SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM users";
70 assert_eq!(inject_max_execution_time(sql, 2000), sql);
71 }
72
73 #[test]
74 fn non_select_untouched() {
75 assert_eq!(
76 inject_max_execution_time("UPDATE users SET x = 1", 5),
77 "UPDATE users SET x = 1"
78 );
79 }
80}