aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client/src/import/replxx.rs
blob: 47d566cf5fe25294b54730dd8d2f3236e0616843 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use std::{path::PathBuf, str};

use async_trait::async_trait;
use directories::UserDirs;
use eyre::{Result, eyre};
use time::{OffsetDateTime, PrimitiveDateTime, macros::format_description};

use super::{Importer, Loader, get_histfile_path, unix_byte_lines};
use crate::history::History;
use crate::import::read_to_end;

#[derive(Debug)]
pub struct Replxx {
    bytes: Vec<u8>,
}

fn default_histpath() -> Result<PathBuf> {
    let user_dirs = UserDirs::new().ok_or_else(|| eyre!("could not find user directories"))?;
    let home_dir = user_dirs.home_dir();

    // There is no default histfile for replxx.
    // Here we try a couple of common names.
    let mut candidates = ["replxx_history.txt", ".histfile"].iter();
    loop {
        match candidates.next() {
            Some(candidate) => {
                let histpath = home_dir.join(candidate);
                if histpath.exists() {
                    break Ok(histpath);
                }
            }
            None => {
                break Err(eyre!(
                    "Could not find history file. Try setting and exporting $HISTFILE"
                ));
            }
        }
    }
}

#[async_trait]
impl Importer for Replxx {
    const NAME: &'static str = "replxx";

    async fn new() -> Result<Self> {
        let bytes = read_to_end(get_histfile_path(default_histpath)?)?;
        Ok(Self { bytes })
    }

    async fn entries(&mut self) -> Result<usize> {
        Ok(super::count_lines(&self.bytes) / 2)
    }

    async fn load(self, h: &mut impl Loader) -> Result<()> {
        let mut timestamp = OffsetDateTime::UNIX_EPOCH;

        for b in unix_byte_lines(&self.bytes) {
            let s = std::str::from_utf8(b)?;
            match try_parse_line_as_timestamp(s) {
                Some(t) => timestamp = t,
                None => {
                    // replxx uses ETB character (0x17) as line breaker
                    let cmd = s.replace('\u{0017}', "\n");
                    let imported = History::import().timestamp(timestamp).command(cmd);

                    h.push(imported.build().into()).await?;
                }
            }
        }

        Ok(())
    }
}

fn try_parse_line_as_timestamp(line: &str) -> Option<OffsetDateTime> {
    // replxx history date time format: ### yyyy-mm-dd hh:mm:ss.xxx
    let date_time_str = line.strip_prefix("### ")?;
    let format =
        format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]");

    let primitive_date_time = PrimitiveDateTime::parse(date_time_str, format).ok()?;
    // There is no safe way to get local time offset.
    // For simplicity let's just assume UTC.
    Some(primitive_date_time.assume_utc())
}

#[cfg(test)]
mod test {

    use crate::import::{Importer, tests::TestLoader};

    use super::Replxx;

    #[tokio::test]
    async fn parse_complex() {
        let bytes = r#"### 2024-02-10 22:16:28.302
select * from remote('127.0.0.1:20222', view(select 1))
### 2024-02-10 22:16:36.919
select * from numbers(10)
### 2024-02-10 22:16:41.710
select * from system.numbers
### 2024-02-10 22:19:28.655
select 1
### 2024-02-22 11:15:33.046
CREATE TABLE test( stamp DateTime('UTC'))ENGINE = MergeTreePARTITION BY toDate(stamp)order by tuple() as select toDateTime('2020-01-01')+number*60 from numbers(80000);
"#
        .as_bytes()
        .to_owned();

        let replxx = Replxx { bytes };

        let mut loader = TestLoader::default();
        replxx.load(&mut loader).await.unwrap();
        let mut history = loader.buf.into_iter();

        // simple wrapper for replxx history entry
        macro_rules! history {
            ($timestamp:expr_2021, $command:expr_2021) => {
                let h = history.next().expect("missing entry in history");
                assert_eq!(h.command.as_str(), $command);
                assert_eq!(h.timestamp.unix_timestamp(), $timestamp);
            };
        }

        history!(
            1707603388,
            "select * from remote('127.0.0.1:20222', view(select 1))"
        );
        history!(1707603396, "select * from numbers(10)");
        history!(1707603401, "select * from system.numbers");
        history!(1707603568, "select 1");
        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);"
        );
    }
}