Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ tempdir = "0.3.7"
[package.metadata.docs.rs]
# docs.rs cannot download libchdb or link native libraries.
# Setting DOCS_RS env var tells build.rs to skip native build steps.
rustc-args = ["--cfg", "docsrs"]
rustc-args = ["--cfg", "docsrs"]
125 changes: 105 additions & 20 deletions src/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
//! log level, and custom command-line arguments.

use std::borrow::Cow;
use std::ffi::CString;
use std::fmt::Display;

use crate::error::Error;
use crate::format::OutputFormat;
use crate::log_level::LogLevel;

Expand Down Expand Up @@ -35,12 +34,31 @@ use crate::log_level::LogLevel;
#[derive(Debug)]
pub enum Arg<'a> {
/// `--config-file=<value>`
///
/// Can be used to specify a custom configuration
/// file for the session, allowing one to configure various aspects of the
/// session's behavior (e.g., SSL settings to use with the `remoteSecure`
/// function).
///
/// An example SSL configuration file (`chdb_ssl.xml`) might look like this:
/// ```xml
/// <clickhouse>
/// <openSSL>
/// <client>
/// <caConfig>path_to_server_ca_cert.pem</caConfig>
/// </client>
/// </openSSL>
/// </clickhouse>
/// ```
/// where `path_to_server_ca_cert.pem` is the path to the CA certificate
/// of the remote server.
ConfigFilePath(Cow<'a, str>),
/// `--log-level=<value>`
LogLevel(LogLevel),
/// `--output-format=<value>`
OutputFormat(OutputFormat),
/// --multiquery
/// Emitted as `-n` (short for `--multiquery`).
MultiQuery,
/// Custom argument.
///
Expand All @@ -52,26 +70,12 @@ pub enum Arg<'a> {
/// 1. Arg::Custom("multiline".to_string().into(), None).
/// 2. Arg::Custom("multiline".into(), None).
///
/// We should tell user where to look for officially supported arguments.
/// Here is some hint for now: <https://github.com/fixcik/chdb-rs/blob/master/OPTIONS.md>.
/// Officially supported arguments can be found by running
/// `clickhouse-client --help` in the terminal.
Custom(Cow<'a, str>, Option<Cow<'a, str>>),
}

impl<'a> Arg<'a> {
#[allow(dead_code)]
pub(crate) fn to_cstring(&self) -> Result<CString, Error> {
Ok(match self {
Self::ConfigFilePath(v) => CString::new(format!("--config-file={v}")),
Self::LogLevel(v) => CString::new(format!("--log-level={}", v.as_str())),
Self::OutputFormat(v) => CString::new(format!("--output-format={}", v.as_str())),
Self::MultiQuery => CString::new("-n"),
Self::Custom(k, v) => match v {
None => CString::new(k.as_ref()),
Some(v) => CString::new(format!("--{k}={v}")),
},
}?)
}

/// Extract `OutputFormat` from an `Arg` if it is an `OutputFormat` variant.
///
/// This is a helper method used internally to extract output format information
Expand All @@ -84,6 +88,21 @@ impl<'a> Arg<'a> {
}
}

impl Display for Arg<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConfigFilePath(v) => write!(f, "--config-file={v}"),
Self::LogLevel(v) => write!(f, "--log-level={}", v.as_str()),
Self::OutputFormat(v) => write!(f, "--output-format={}", v.as_str()),
Self::MultiQuery => write!(f, "-n"),
Self::Custom(k, v) => match v {
None => write!(f, "--{}", k.as_ref()),
Some(v) => write!(f, "--{k}={v}"),
},
}
}
}

/// Extract `OutputFormat` from a slice of `Arg`s.
///
/// This function searches through the provided arguments and returns the first
Expand All @@ -96,7 +115,73 @@ impl<'a> Arg<'a> {
/// # Returns
///
/// Returns the first `OutputFormat` found, or `OutputFormat::TabSeparated` as default.
pub(crate) fn extract_output_format(args: Option<&[Arg]>) -> OutputFormat {
pub(crate) fn extract_output_format(args: Option<&[Arg]>, default: OutputFormat) -> OutputFormat {
args.and_then(|args| args.iter().find_map(|a| a.as_output_format()))
.unwrap_or(OutputFormat::TabSeparated)
.unwrap_or(default)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_arg_display_config_file_path() {
assert_eq!(
Arg::ConfigFilePath(Cow::from("my.xml")).to_string(),
"--config-file=my.xml"
);
}

#[test]
fn test_arg_display_log_level() {
assert_eq!(
Arg::LogLevel(LogLevel::Trace).to_string(),
"--log-level=trace"
);
assert_eq!(
Arg::LogLevel(LogLevel::Debug).to_string(),
"--log-level=debug"
);
assert_eq!(
Arg::LogLevel(LogLevel::Info).to_string(),
"--log-level=information"
);
assert_eq!(
Arg::LogLevel(LogLevel::Warn).to_string(),
"--log-level=warning"
);
assert_eq!(
Arg::LogLevel(LogLevel::Error).to_string(),
"--log-level=error"
);
}

#[test]
fn test_arg_display_output_format() {
assert_eq!(
Arg::OutputFormat(OutputFormat::JSONEachRow).to_string(),
"--output-format=JSONEachRow"
);
}

#[test]
fn test_arg_display_multi_query() {
assert_eq!(Arg::MultiQuery.to_string(), "-n");
}

#[test]
fn test_arg_display_custom_key_only() {
assert_eq!(
Arg::Custom("multiline".into(), None).to_string(),
"--multiline"
);
}

#[test]
fn test_arg_display_custom_key_value() {
assert_eq!(
Arg::Custom("priority".into(), Some("1".into())).to_string(),
"--priority=1"
);
}
}
53 changes: 38 additions & 15 deletions src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
use std::ffi::{c_char, CString};

use crate::arrow_stream::{ArrowArray, ArrowSchema, ArrowStream};
use crate::bindings;
use crate::error::{Error, Result};
use crate::format::OutputFormat;
use crate::query_result::QueryResult;
use crate::{bindings, CHDB_PROGRAM_NAME};

/// A connection to a chDB database.
///
Expand Down Expand Up @@ -49,21 +49,20 @@ unsafe impl Send for Connection {}
impl Connection {
/// Connect to chDB with the given command-line arguments.
///
/// This is a low-level function that allows you to pass arbitrary arguments
/// to the chDB connection. For most use cases, prefer [`open_in_memory`](Self::open_in_memory)
/// or [`open_with_path`](Self::open_with_path).
/// Use [crate::session::SessionBuilder] for a higher-level API that supports
/// sessions and persistent storage.
///
/// # Arguments
///
/// * `args` - Array of command-line arguments (e.g., `["clickhouse", "--path=/tmp/db"]`)
/// * `args` - Array of command-line arguments (e.g., `["--path=/tmp/db"]`)
///
/// # Examples
///
/// ```no_run
/// use chdb_rust::connection::Connection;
///
/// // Connect with custom arguments
/// let conn = Connection::open(&["clickhouse", "--path=/tmp/mydb"])?;
/// let conn = Connection::open(&["--path=/tmp/mydb"])?;
/// # Ok::<(), chdb_rust::error::Error>(())
/// ```
///
Expand All @@ -72,14 +71,14 @@ impl Connection {
/// Returns [`Error::ConnectionFailed`] if the
/// connection cannot be established.
pub fn open(args: &[&str]) -> Result<Self> {
let c_args: Vec<CString> = args
.iter()
.map(|s| CString::new(*s))
.collect::<std::result::Result<Vec<_>, _>>()?;
let c_args: Vec<CString> = std::iter::once(CHDB_PROGRAM_NAME)
.chain(args.iter().copied())
.map(CString::new)
.collect::<std::result::Result<_, _>>()?;

let mut argv: Vec<*mut c_char> = c_args.iter().map(|s| s.as_ptr() as *mut c_char).collect();

let conn_ptr = unsafe { bindings::chdb_connect(argv.len() as i32, argv.as_mut_ptr()) };
let argv: Vec<*const c_char> = c_args.iter().map(|s| s.as_ptr()).collect();
let conn_ptr =
unsafe { bindings::chdb_connect(argv.len() as i32, argv.as_ptr() as *mut *mut c_char) };

if conn_ptr.is_null() {
return Err(Error::ConnectionFailed);
Expand Down Expand Up @@ -113,7 +112,7 @@ impl Connection {
/// Returns [`Error::ConnectionFailed`] if the
/// connection cannot be established.
pub fn open_in_memory() -> Result<Self> {
Self::open(&["clickhouse"])
Self::open(&[])
}

/// Connect to a database at the given path.
Expand All @@ -138,9 +137,10 @@ impl Connection {
///
/// Returns [`Error::ConnectionFailed`] if the
/// connection cannot be established.
#[deprecated(note = "Use `SessionBuilder` instead")]
pub fn open_with_path(path: &str) -> Result<Self> {
let path_arg = format!("--path={path}");
Self::open(&["clickhouse", &path_arg])
Self::open(&[&path_arg])
}

/// Execute a query and return the result.
Expand Down Expand Up @@ -383,3 +383,26 @@ impl Drop for Connection {
}
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::{error::Result, test_utils::tempdir};

#[test]
fn test_connection_open_with_explicit_data_path() -> Result<()> {
let tmp = tempdir();
let path_arg = format!(
"--path={}",
tmp.path().to_str().expect("temp path is not valid UTF-8")
);
Connection::open(&[&path_arg])?;

assert!(
tmp.path().read_dir()?.next().is_some(),
"expected chDB to create files in the data dir"
);

Ok(())
}
}
8 changes: 7 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,17 @@ pub mod log_level;
pub mod query_result;
pub mod session;

#[cfg(test)]
mod test_utils;

use crate::arg::{extract_output_format, Arg};
use crate::connection::Connection;
use crate::error::Result;
use crate::format::OutputFormat;
use crate::query_result::QueryResult;

pub(crate) const CHDB_PROGRAM_NAME: &str = "clickhouse";

/// Execute a one-off query using an in-memory connection.
///
/// This function creates a temporary in-memory database connection, executes the query,
Expand Down Expand Up @@ -101,6 +107,6 @@ use crate::query_result::QueryResult;
/// - The query execution fails
pub fn execute(query: &str, query_args: Option<&[Arg]>) -> Result<QueryResult> {
let conn = Connection::open_in_memory()?;
let fmt = extract_output_format(query_args);
let fmt = extract_output_format(query_args, OutputFormat::TabSeparated);
conn.query(query, fmt)
}
Loading
Loading