diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..351570d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,55 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + name: ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + matrix: + job: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + + - target: x86_64-apple-darwin + os: macos-latest + + - target: aarch64-apple-darwin + os: macos-latest + + # TODO + # - target: x86_64-pc-windows-msvc + # os: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ matrix.job.target }} + + - name: Setup Rust + run: | + rustup update + rustup target add ${{ matrix.job.target }} + + - name: Cargo fmt + run: | + cargo fmt --all -- --check + + - name: Cargo test + run: | + cargo test --target ${{ matrix.job.target }} + + - name: Cargo run --example + run: | + cargo run --example basic --target ${{ matrix.job.target }} diff --git a/.gitignore b/.gitignore index d01bd1a..c9ff619 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,6 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock - -# These are backup files generated by rustfmt **/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information *.pdb - -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +.DS_Store +test.db/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d1cc8a3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crossdb"] + path = crossdb + url = https://github.com/crossdb-org/crossdb diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0438d61 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "crossdb" +version = "0.0.1" +edition = "2021" +license = "MIT" +description = "CrossDB Rust Driver" +readme = "README.md" +homepage = "hhttps://github.com/crossdb-org/crossdb-rust" +repository = "https://github.com/crossdb-org/crossdb-rust.git" + +[dependencies] +thiserror = "1.0" + +[build-dependencies] +bindgen = "0.70" +cc = "1.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..665b6ec --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# crossdb-rs + +```toml +[dependencies] +crossdb = { git = "https://github.com/crossdb-org/crossdb-rust" } +``` + +```rs +use crossdb::Connection; + +fn main() { + let conn = Connection::open_with_memory().unwrap(); + let mut rst = conn.exec("select * from system.databases;").unwrap(); + + for i in 0..rst.column_count() { + println!("Column {i}: {} {}", rst.column_name(i), rst.column_type(i)); + } + + while let Some(row) = (&mut rst).next() { + dbg!(row); + } +} +``` + +## TODO +* Add more apis +* Windows support +* Dynamic link crossdb +* use serde to serialize/deserialize diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d71a498 --- /dev/null +++ b/build.rs @@ -0,0 +1,26 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindgen::builder() + .header("crossdb/include/crossdb.h") + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .unwrap() + .write_to_file(out_path.join("./bindings.rs")) + .unwrap(); + + let mut builder = cc::Build::new(); + builder + .file("crossdb/src/crossdb.c") + .include("crossdb/include") + .flag("-fPIC") + .opt_level(2) + .static_flag(true) + .compile("crossdb"); + println!("cargo:rustc-link-lib=static=crossdb"); + println!("cargo:rustc-link-lib=pthread"); + + println!("cargo:rerun-if-changed=crossdb/"); +} diff --git a/crossdb b/crossdb new file mode 160000 index 0000000..53faea9 --- /dev/null +++ b/crossdb @@ -0,0 +1 @@ +Subproject commit 53faea90b630509c02372b9e170243467796e9a7 diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..b40df0a --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,14 @@ +use crossdb::Connection; + +fn main() { + let conn = Connection::open("test.db").unwrap(); + let mut rst = conn.exec("select * FROM system.databases;").unwrap(); + + for i in 0..rst.column_count() { + println!("Column {i}: {} {}", rst.column_name(i), rst.column_type(i)); + } + + while let Some(row) = (&mut rst).next() { + dbg!(row); + } +} diff --git a/src/column.rs b/src/column.rs new file mode 100644 index 0000000..120fb93 --- /dev/null +++ b/src/column.rs @@ -0,0 +1,86 @@ +use crate::*; + +// https://crossdb.org/client/api-c/#xdb_type_t +#[derive(Debug, Clone, Copy)] +pub enum ColumnType { + Null, + TinyInt, + SmallInt, + Int, + BigInt, + UTinyInt, + USmallInt, + UInt, + UBigInt, + Float, + Double, + Timestamp, + Char, + Binary, + VChar, + VBinary, + Max, +} + +impl From for ColumnType { + #[allow(non_upper_case_globals)] + fn from(value: u32) -> Self { + match value { + xdb_type_t_XDB_TYPE_NULL => ColumnType::Null, + xdb_type_t_XDB_TYPE_TINYINT => ColumnType::TinyInt, + xdb_type_t_XDB_TYPE_SMALLINT => ColumnType::SmallInt, + xdb_type_t_XDB_TYPE_INT => ColumnType::Int, + xdb_type_t_XDB_TYPE_BIGINT => ColumnType::BigInt, + xdb_type_t_XDB_TYPE_UTINYINT => ColumnType::UTinyInt, + xdb_type_t_XDB_TYPE_USMALLINT => ColumnType::USmallInt, + xdb_type_t_XDB_TYPE_UINT => ColumnType::UInt, + xdb_type_t_XDB_TYPE_UBIGINT => ColumnType::UBigInt, + xdb_type_t_XDB_TYPE_FLOAT => ColumnType::Float, + xdb_type_t_XDB_TYPE_DOUBLE => ColumnType::Double, + xdb_type_t_XDB_TYPE_TIMESTAMP => ColumnType::Timestamp, + xdb_type_t_XDB_TYPE_CHAR => ColumnType::Char, + xdb_type_t_XDB_TYPE_BINARY => ColumnType::Binary, + xdb_type_t_XDB_TYPE_VCHAR => ColumnType::VChar, + xdb_type_t_XDB_TYPE_VBINARY => ColumnType::VBinary, + xdb_type_t_XDB_TYPE_MAX => ColumnType::Max, + _ => unreachable!(), + } + } +} + +impl Display for ColumnType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ColumnType::Null => write!(f, "NULL"), + ColumnType::TinyInt => write!(f, "TINYINT"), + ColumnType::SmallInt => write!(f, "SMALLINT"), + ColumnType::Int => write!(f, "INT"), + ColumnType::BigInt => write!(f, "BIGINT"), + ColumnType::UTinyInt => write!(f, "UTINYINT"), + ColumnType::USmallInt => write!(f, "USMALLINT"), + ColumnType::UInt => write!(f, "UINT"), + ColumnType::UBigInt => write!(f, "UBIGINT"), + ColumnType::Float => write!(f, "FLOAT"), + ColumnType::Double => write!(f, "DOUBLE"), + ColumnType::Timestamp => write!(f, "TIMESTAMP"), + ColumnType::Char => write!(f, "CHAR"), + ColumnType::Binary => write!(f, "BINARY"), + ColumnType::VChar => write!(f, "VCHAR"), + ColumnType::VBinary => write!(f, "VBINARY"), + ColumnType::Max => write!(f, "MAX"), + } + } +} + +impl ColumnType { + pub(crate) fn all(res: &xdb_res_t) -> Vec { + let mut vec = Vec::with_capacity(res.col_count as usize); + for i in 0..vec.capacity() { + unsafe { + let t = xdb_column_type(res.col_meta, i as u16); + vec.push(Self::from(t)); + } + } + vec + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..fd92603 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,13 @@ +use std::ffi::NulError; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("CString error: {0}")] + CString(#[from] NulError), + #[error("UTF8 error: {0}")] + Utf8(#[from] std::str::Utf8Error), + #[error("Query error: {0}, {1}")] + Query(u16, String), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f9d344d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,126 @@ +#![allow( + dead_code, + non_snake_case, + non_camel_case_types, + non_upper_case_globals +)] + +mod crossdb_sys { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +mod column; +mod error; +mod value; + +pub use column::ColumnType; +pub use error::{Error, Result}; +pub use value::Value; + +use crossdb_sys::*; +use std::ffi::{CStr, CString}; +use std::fmt::Display; + +#[derive(Debug)] +pub struct Connection { + ptr: *mut xdb_conn_t, +} + +impl Drop for Connection { + fn drop(&mut self) { + unsafe { + xdb_close(self.ptr); + } + } +} + +impl Connection { + pub fn open>(path: P) -> Result { + let path = CString::new(path.as_ref())?; + let ptr = unsafe { xdb_open(path.as_ptr()) }; + Ok(Self { ptr }) + } + + pub fn open_with_memory() -> Result { + Self::open(":memory:") + } + + pub fn exec>(&self, sql: S) -> Result { + let sql = CString::new(sql.as_ref())?; + unsafe { + let ptr = xdb_exec(self.ptr, sql.as_ptr()); + let res = *ptr; + if res.errcode as u32 != xdb_errno_e_XDB_OK { + let msg = CStr::from_ptr(xdb_errmsg(ptr)).to_str()?.to_string(); + return Err(Error::Query(res.errcode, msg)); + } + Ok(ExecResult { + ptr, + col_meta: res.col_meta, + column_count: res.col_count as usize, + row_count: res.row_count as usize, + column_types: ColumnType::all(&res), + row_index: 0, + }) + } + } +} + +#[derive(Debug)] +pub struct ExecResult { + ptr: *mut xdb_res_t, + col_meta: u64, + column_count: usize, + row_count: usize, + column_types: Vec, + row_index: usize, +} + +impl Drop for ExecResult { + fn drop(&mut self) { + unsafe { + xdb_free_result(self.ptr); + } + } +} + +impl ExecResult { + pub fn column_count(&self) -> usize { + self.column_count + } + + pub fn row_count(&self) -> usize { + self.row_count + } + + pub fn column_name<'a>(&'a self, i: usize) -> &'a str { + unsafe { + let name = xdb_column_name(self.col_meta, i as u16); + CStr::from_ptr(name).to_str().unwrap() + } + } + + pub fn column_type(&self, i: usize) -> ColumnType { + self.column_types[i] + } +} + +impl<'a> Iterator for &'a mut ExecResult { + type Item = Vec>; + + fn next(&mut self) -> Option { + if self.row_count <= self.row_index { + return None; + } + let mut values = Vec::with_capacity(self.column_count); + unsafe { + let y = xdb_fetch_row(self.ptr); + for x in 0..self.column_count { + let value = Value::from_result(self.col_meta, y, x as u16, self.column_type(x)); + values.push(value); + } + } + self.row_index += 1; + Some(values) + } +} diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..79d7c15 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,38 @@ +use crate::*; + +#[derive(Debug, Clone, PartialEq)] +pub enum Value<'a> { + Null, + I8(i8), + I16(i16), + I32(i32), + I64(i64), + F32(f32), + F64(f64), + Char(&'a str), +} + +impl<'a> Value<'a> { + // TODO: If you know the detailed format, you can access the pointer directly + // https://crossdb.org/client/api-c/#xdb_column_int + pub(crate) unsafe fn from_result( + meta: u64, + row: *mut xdb_row_t, + col: u16, + t: ColumnType, + ) -> Value<'a> { + match t { + ColumnType::TinyInt => Value::I8(xdb_column_int(meta, row, col) as _), + ColumnType::SmallInt => Value::I16(xdb_column_int(meta, row, col) as _), + ColumnType::Int => Value::I32(xdb_column_int(meta, row, col) as _), + ColumnType::BigInt => Value::I64(xdb_column_int64(meta, row, col)), + ColumnType::Float => Value::F32(xdb_column_float(meta, row, col)), + ColumnType::Double => Value::F64(xdb_column_double(meta, row, col)), + ColumnType::Char => { + let s = CStr::from_ptr(xdb_column_str(meta, row, col)); + Value::Char(s.to_str().unwrap()) + } + _ => unimplemented!(), + } + } +}