atuin_client/import/
replxx.rs

1use std::{path::PathBuf, str};
2
3use async_trait::async_trait;
4use directories::UserDirs;
5use eyre::{eyre, Result};
6use time::{macros::format_description, OffsetDateTime, PrimitiveDateTime};
7
8use super::{get_histpath, unix_byte_lines, Importer, Loader};
9use crate::history::History;
10use crate::import::read_to_end;
11
12#[derive(Debug)]
13pub struct Replxx {
14    bytes: Vec<u8>,
15}
16
17fn default_histpath() -> Result<PathBuf> {
18    let user_dirs = UserDirs::new().ok_or_else(|| eyre!("could not find user directories"))?;
19    let home_dir = user_dirs.home_dir();
20
21    // There is no default histfile for replxx.
22    // For simplicity let's use the most common one.
23    Ok(home_dir.join(".histfile"))
24}
25
26#[async_trait]
27impl Importer for Replxx {
28    const NAME: &'static str = "replxx";
29
30    async fn new() -> Result<Self> {
31        let bytes = read_to_end(get_histpath(default_histpath)?)?;
32        Ok(Self { bytes })
33    }
34
35    async fn entries(&mut self) -> Result<usize> {
36        Ok(super::count_lines(&self.bytes) / 2)
37    }
38
39    async fn load(self, h: &mut impl Loader) -> Result<()> {
40        let mut timestamp = OffsetDateTime::UNIX_EPOCH;
41
42        for b in unix_byte_lines(&self.bytes) {
43            let s = std::str::from_utf8(b)?;
44            match try_parse_line_as_timestamp(s) {
45                Some(t) => timestamp = t,
46                None => {
47                    // replxx uses ETB character (0x17) as line breaker
48                    let cmd = s.replace('\u{0017}', "\n");
49                    let imported = History::import().timestamp(timestamp).command(cmd);
50
51                    h.push(imported.build().into()).await?;
52                }
53            }
54        }
55
56        Ok(())
57    }
58}
59
60fn try_parse_line_as_timestamp(line: &str) -> Option<OffsetDateTime> {
61    // replxx history date time format: ### yyyy-mm-dd hh:mm:ss.xxx
62    let date_time_str = line.strip_prefix("### ")?;
63    let format =
64        format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]");
65
66    let primitive_date_time = PrimitiveDateTime::parse(date_time_str, format).ok()?;
67    // There is no safe way to get local time offset.
68    // For simplicity let's just assume UTC.
69    Some(primitive_date_time.assume_utc())
70}
71
72#[cfg(test)]
73mod test {
74
75    use crate::import::{tests::TestLoader, Importer};
76
77    use super::Replxx;
78
79    #[tokio::test]
80    async fn parse_complex() {
81        let bytes = r#"### 2024-02-10 22:16:28.302
82select * from remote('127.0.0.1:20222', view(select 1))
83### 2024-02-10 22:16:36.919
84select * from numbers(10)
85### 2024-02-10 22:16:41.710
86select * from system.numbers
87### 2024-02-10 22:19:28.655
88select 1
89### 2024-02-22 11:15:33.046
90CREATE TABLE test( stamp DateTime('UTC'))ENGINE = MergeTreePARTITION BY toDate(stamp)order by tuple() as select toDateTime('2020-01-01')+number*60 from numbers(80000);
91"#
92        .as_bytes()
93        .to_owned();
94
95        let replxx = Replxx { bytes };
96
97        let mut loader = TestLoader::default();
98        replxx.load(&mut loader).await.unwrap();
99        let mut history = loader.buf.into_iter();
100
101        // simple wrapper for replxx history entry
102        macro_rules! history {
103            ($timestamp:expr, $command:expr) => {
104                let h = history.next().expect("missing entry in history");
105                assert_eq!(h.command.as_str(), $command);
106                assert_eq!(h.timestamp.unix_timestamp(), $timestamp);
107            };
108        }
109
110        history!(
111            1707603388,
112            "select * from remote('127.0.0.1:20222', view(select 1))"
113        );
114        history!(1707603396, "select * from numbers(10)");
115        history!(1707603401, "select * from system.numbers");
116        history!(1707603568, "select 1");
117        history!(1708600533, "CREATE TABLE test\n( stamp DateTime('UTC'))\nENGINE = MergeTree\nPARTITION BY toDate(stamp)\norder by tuple() as select toDateTime('2020-01-01')+number*60 from numbers(80000);");
118    }
119}