Skip to main content

sqlite_graphrag/parsers/
mod.rs

1//! Input format parsers (timestamp, range validators).
2
3use chrono::DateTime;
4
5/// Accepts a Unix epoch (integer >= 0) or RFC 3339 timestamp and returns the Unix epoch.
6pub fn parse_expected_updated_at(s: &str) -> Result<i64, String> {
7    if let Ok(secs) = s.parse::<i64>() {
8        if secs >= 0 {
9            return Ok(secs);
10        }
11    }
12    DateTime::parse_from_rfc3339(s)
13        .map(|dt| dt.timestamp())
14        .map_err(|e| {
15            format!(
16                "value must be a Unix epoch (integer >= 0) or RFC 3339 (e.g. 2026-04-19T12:00:00Z): {e}"
17            )
18        })
19}
20
21/// Validates `-k`/`--k` for `recall` and `hybrid-search` to the inclusive range `1..=4096`.
22///
23/// The upper bound matches the `sqlite-vec` knn limit; values above it would surface a leaky
24/// engine error such as `k value in knn query too large, provided 10000 and the limit is 4096`.
25/// Validating at parse time turns the failure into a clean Clap error before any database work.
26pub fn parse_k_range(s: &str) -> Result<usize, String> {
27    let value: usize = s
28        .parse()
29        .map_err(|_| format!("'{s}' is not a valid non-negative integer"))?;
30    if !(1..=4096).contains(&value) {
31        return Err(format!(
32            "k must be between 1 and 4096 (inclusive); got {value}"
33        ));
34    }
35    Ok(value)
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn accepts_unix_epoch() {
44        assert_eq!(parse_expected_updated_at("1700000000").unwrap(), 1700000000);
45    }
46
47    #[test]
48    fn accepts_zero() {
49        assert_eq!(parse_expected_updated_at("0").unwrap(), 0);
50    }
51
52    #[test]
53    fn accepts_rfc_3339_utc() {
54        let result = parse_expected_updated_at("2020-01-01T00:00:00Z");
55        assert!(result.is_ok());
56        assert_eq!(result.unwrap(), 1577836800);
57    }
58
59    #[test]
60    fn accepts_rfc_3339_with_offset() {
61        let result = parse_expected_updated_at("2026-04-19T12:00:00+00:00");
62        assert!(result.is_ok());
63    }
64
65    #[test]
66    fn rejects_invalid_string() {
67        assert!(parse_expected_updated_at("bananas").is_err());
68    }
69
70    #[test]
71    fn rejects_negative() {
72        let err = parse_expected_updated_at("-1");
73        assert!(err.is_err());
74    }
75
76    #[test]
77    fn error_message_mentions_format() {
78        let msg = parse_expected_updated_at("invalid").unwrap_err();
79        assert!(msg.contains("RFC 3339") || msg.contains("Unix epoch"));
80    }
81
82    #[test]
83    fn k_accepts_valid_range_endpoints() {
84        assert_eq!(parse_k_range("1").unwrap(), 1);
85        assert_eq!(parse_k_range("4096").unwrap(), 4096);
86        assert_eq!(parse_k_range("10").unwrap(), 10);
87    }
88
89    #[test]
90    fn k_rejects_zero() {
91        let msg = parse_k_range("0").unwrap_err();
92        assert!(msg.contains("between 1 and 4096"));
93    }
94
95    #[test]
96    fn k_rejects_above_limit() {
97        let msg = parse_k_range("10000").unwrap_err();
98        assert!(msg.contains("between 1 and 4096"));
99    }
100
101    #[test]
102    fn k_rejects_non_integer() {
103        let msg = parse_k_range("abc").unwrap_err();
104        assert!(msg.contains("not a valid"));
105    }
106
107    #[test]
108    fn k_rejects_negative() {
109        // usize parser fails on negatives before range check
110        assert!(parse_k_range("-5").is_err());
111    }
112}