use std::path::PathBuf;
use std::{fs, io};
#[cfg(feature = "arrow")]
use arrow::array::{RecordBatch, RecordBatchReader};
#[cfg(feature = "arrow")]
use arrow::datatypes::SchemaRef;
use crate::arg::{extract_output_format, Arg};
#[cfg(feature = "arrow")]
use crate::arrow_insert::{
insert_record_batch, insert_record_batch_direct, insert_record_batch_reader,
insert_record_batches,
};
#[cfg(feature = "arrow")]
use crate::arrow_options::InsertOptions;
use crate::connection::Connection;
use crate::error::{Error, Result};
use crate::format::OutputFormat;
use crate::query_result::QueryResult;
pub struct SessionBuilder<'a> {
data_path: PathBuf,
default_format: OutputFormat,
arguments: Vec<Arg<'a>>,
auto_cleanup: bool,
}
#[derive(Debug)]
pub struct Session {
conn: Connection,
data_path: PathBuf,
default_format: OutputFormat,
auto_cleanup: bool,
}
impl<'a> SessionBuilder<'a> {
pub fn new() -> Self {
Self {
data_path: PathBuf::new(),
default_format: OutputFormat::TabSeparated,
arguments: Vec::new(),
auto_cleanup: false,
}
}
pub fn with_data_path(mut self, path: impl Into<PathBuf>) -> Self {
self.data_path = path.into();
self
}
pub fn with_arg(mut self, arg: Arg<'a>) -> Self {
if let Some(fmt) = arg.as_output_format() {
self.default_format = fmt;
} else {
self.arguments.push(arg);
}
self
}
pub fn with_auto_cleanup(mut self, value: bool) -> Self {
self.auto_cleanup = value;
self
}
pub fn build(mut self) -> Result<Session> {
if self.data_path.as_os_str().is_empty() {
let mut default_path = std::env::current_dir()?;
default_path.push("chdb");
self.data_path = default_path;
}
let dir_already_existed = self.data_path.exists();
fs::create_dir_all(&self.data_path)?;
let probe_path = self.data_path.join(".write_probe");
if let Err(e) = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&probe_path)
{
if !dir_already_existed {
fs::remove_dir(&self.data_path).ok();
}
return Err(match e.kind() {
io::ErrorKind::PermissionDenied => Error::InsufficientPermissions,
_ => Error::Io(e),
});
}
fs::remove_file(&probe_path).ok();
let data_path = self.data_path.to_str().ok_or(Error::PathError)?;
let path_arg = format!("--path={data_path}");
let owned: Vec<String> = self.arguments.iter().map(|a| a.to_string()).collect();
let mut args: Vec<&str> = Vec::with_capacity(1 + owned.len());
args.push(&path_arg);
args.extend(owned.iter().map(String::as_str));
let conn = Connection::open(&args)?;
Ok(Session {
conn,
data_path: self.data_path,
default_format: self.default_format,
auto_cleanup: self.auto_cleanup,
})
}
}
impl Default for SessionBuilder<'_> {
fn default() -> Self {
Self::new()
}
}
impl Session {
pub fn execute(&self, query: &str, query_args: Option<&[Arg]>) -> Result<QueryResult> {
let fmt = extract_output_format(query_args, self.default_format);
self.conn.query(query, fmt)
}
pub fn connection(&self) -> &Connection {
&self.conn
}
#[cfg(feature = "arrow")]
pub fn insert_record_batch(
&self,
dest_table: &str,
stream_name: &str,
batch: RecordBatch,
options: InsertOptions,
) -> Result<()> {
insert_record_batch(self.connection(), dest_table, stream_name, batch, options)
}
#[cfg(feature = "arrow")]
pub fn insert_record_batch_direct(
&self,
dest_table: &str,
batch: RecordBatch,
options: InsertOptions,
) -> Result<()> {
insert_record_batch_direct(self.connection(), dest_table, batch, options)
}
#[cfg(feature = "arrow")]
pub fn insert_record_batches(
&self,
dest_table: &str,
stream_name: &str,
schema: SchemaRef,
batches: Vec<RecordBatch>,
options: InsertOptions,
) -> Result<()> {
insert_record_batches(
self.connection(),
dest_table,
stream_name,
schema,
batches,
options,
)
}
#[cfg(feature = "arrow")]
pub fn insert_record_batch_reader(
&self,
dest_table: &str,
stream_name: &str,
reader: Box<dyn RecordBatchReader + Send>,
options: InsertOptions,
) -> Result<()> {
insert_record_batch_reader(self.connection(), dest_table, stream_name, reader, options)
}
}
impl Drop for Session {
fn drop(&mut self) {
if self.auto_cleanup {
fs::remove_dir_all(&self.data_path).ok();
}
}
}
#[cfg(test)]
mod test {
use crate::test_utils::tempdir;
use super::*;
#[test]
fn test_session_builder_no_args_still_builds() -> Result<()> {
let tmp = tempdir();
SessionBuilder::new().with_data_path(tmp.path()).build()?;
Ok(())
}
#[test]
fn test_query_uses_default_output_format() -> Result<()> {
let tmp = tempdir();
let session = SessionBuilder::new().with_data_path(tmp.path()).build()?;
let resp = session.execute("SELECT 'foo' AS name, 1 AS count", None)?;
assert_eq!(resp.data_utf8_lossy(), "foo\t1\n");
Ok(())
}
#[test]
fn test_query_output_format_overrides_session_builder_output_format() -> Result<()> {
let tmp = tempdir();
let session = SessionBuilder::new()
.with_data_path(tmp.path())
.with_arg(Arg::OutputFormat(OutputFormat::Parquet))
.build()?;
let resp = session.execute(
"SELECT 1 AS count",
Some(&[Arg::OutputFormat(OutputFormat::JSONEachRow)]),
)?;
assert_eq!(resp.data_utf8_lossy(), "{\"count\":1}\n");
Ok(())
}
}