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, } fn default_histpath() -> Result { 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 { let bytes = read_to_end(get_histfile_path(default_histpath)?)?; Ok(Self { bytes }) } async fn entries(&mut self) -> Result { 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 { // 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);" ); } }