Skip to main content

sequel_mcp/sql/
hints.rs

1//! MAX_EXECUTION_TIME hint injection (legacy `sql/hints.ts` port).
2
3/// Inject `/*+ MAX_EXECUTION_TIME(ms) */` after the leading `select`
4/// keyword when not already present. Case-insensitive, first-statement only.
5pub 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    /// Differential check against the generated legacy fixtures.
45    #[test]
46    fn matches_legacy_fixtures() {
47        let path = concat!(
48            env!("CARGO_MANIFEST_DIR"),
49            "/tests/fixtures/legacy/hints.json"
50        );
51        // Untracked corpus generated from the legacy checkout; skip on
52        // fresh CI checkouts where it is absent.
53        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}