diff --git a/Cargo.lock b/Cargo.lock index 20c63f8..66308b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,6 +512,12 @@ dependencies = [ "cc", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "js-sys" version = "0.3.78" @@ -524,7 +530,7 @@ dependencies = [ [[package]] name = "lbug" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "arrow", @@ -534,6 +540,7 @@ dependencies = [ "rust_decimal", "rust_decimal_macros", "rustversion", + "serde_json", "tempfile", "time", "uuid", @@ -838,6 +845,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -858,6 +874,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1232,3 +1261,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index e0c1c1e..11037ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lbug" -version = "0.16.0" +version = "0.17.0" description = "An in-process property graph database management system built for query speed and scalability" keywords = ["database", "graph", "ffi"] readme = "lbug-src/README.md" @@ -31,6 +31,7 @@ include = [ arrow = { version = "55", optional = true, default-features = false, features = ["ffi"] } cxx = "=1.0.138" # the last version that builds on clang 15 rust_decimal = { version = "1.37", default-features = false } +serde_json = "1" time = "0.3" uuid = "1.6" diff --git a/include/lbug_rs.h b/include/lbug_rs.h index b979184..ad7fa76 100644 --- a/include/lbug_rs.h +++ b/include/lbug_rs.h @@ -138,9 +138,7 @@ inline void connection_set_query_timeout(lbug::main::Connection& connection, uin /* PreparedStatement */ rust::String prepared_statement_error_message(const lbug::main::PreparedStatement& statement); inline bool prepared_statement_is_read_only(const lbug::main::PreparedStatement& statement) { - lbug_prepared_statement c_statement{ - const_cast(&statement), nullptr}; - return lbug_prepared_statement_is_read_only(&c_statement); + return statement.isReadOnly(); } inline bool prepared_statement_is_success(const lbug::main::PreparedStatement& statement) { return statement.isSuccess(); diff --git a/src/error.rs b/src/error.rs index 5f9d326..44dbedc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,8 @@ pub enum Error { FailedPreparedStatement(String), /// Message produced when you attempt to pass read-only types over the FFI boundary ReadOnlyType(LogicalType), + /// Message produced when JSON serialization fails + JsonError(serde_json::Error), #[cfg(feature = "arrow")] ArrowError(arrow::error::ArrowError), } @@ -25,6 +27,7 @@ impl std::fmt::Display for Error { Error::ReadOnlyType(typ) => { write!(f, "Attempted to pass read only type {typ:?} over ffi!") } + Error::JsonError(err) => write!(f, "{err}"), #[cfg(feature = "arrow")] Error::ArrowError(err) => write!(f, "{err}"), } @@ -41,6 +44,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::CxxException(cxx) => Some(cxx), + Error::JsonError(err) => Some(err), _ => None, } } @@ -52,6 +56,12 @@ impl From for Error { } } +impl From for Error { + fn from(item: serde_json::Error) -> Self { + Error::JsonError(item) + } +} + #[cfg(feature = "arrow")] impl From for Error { fn from(item: arrow::error::ArrowError) -> Self { diff --git a/src/ffi.rs b/src/ffi.rs index e3fe36f..1c97595 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -86,6 +86,7 @@ pub(crate) mod ffi { UNION = 56, UUID = 59, + JSON = 60, } // From types.h @@ -113,6 +114,7 @@ pub(crate) mod ffi { // Variable size types. STRING = 20, + JSON = 21, LIST = 22, ARRAY = 23, STRUCT = 24, diff --git a/src/lib.rs b/src/lib.rs index 1b74844..45e02d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// /// This is `external` when `LBUG_LIBRARY_DIR`/`LBUG_INCLUDE_DIR` were supplied, `source` when the /// bundled C++ source was built, or a value such as `run:LadybugDB/ladybug/25646256977` or -/// `release:LadybugDB/ladybug/v0.16.0` when a precompiled archive was downloaded. +/// `release:LadybugDB/ladybug/v0.17.0` when a precompiled archive was downloaded. pub const LBUG_LIBRARY_SOURCE: &str = env!("LBUG_PRECOMPILED_SOURCE"); /// The directory containing the linked precompiled Lbug library, if one was used. pub const LBUG_LIBRARY_DIR: &str = env!("LBUG_PRECOMPILED_LIBRARY_DIR"); diff --git a/src/logical_type.rs b/src/logical_type.rs index 80c2bab..6834b89 100644 --- a/src/logical_type.rs +++ b/src/logical_type.rs @@ -83,6 +83,8 @@ pub enum LogicalType { }, /// Correponds to [`Value::UUID`](crate::value::Value::UUID) UUID, + /// Corresponds to [`Value::Json`](crate::value::Value::Json) + Json, /// Correponds to [`Value::Decimal`](crate::value::Value::Decimal) Decimal { precision: u32, @@ -188,6 +190,7 @@ impl From<&ffi::LogicalType> for LogicalType { } } LogicalTypeID::UUID => LogicalType::UUID, + LogicalTypeID::JSON => LogicalType::Json, LogicalTypeID::DECIMAL => { let precision = ffi::logical_type_get_decimal_precision(logical_type); let scale = ffi::logical_type_get_decimal_scale(logical_type); @@ -230,7 +233,8 @@ impl From<&LogicalType> for cxx::UniquePtr { | LogicalType::Node | LogicalType::Rel | LogicalType::RecursiveRel - | LogicalType::UUID => ffi::create_logical_type(typ.id()), + | LogicalType::UUID + | LogicalType::Json => ffi::create_logical_type(typ.id()), LogicalType::List { child_type } => { ffi::create_logical_type_list(child_type.as_ref().into()) } @@ -304,6 +308,7 @@ impl LogicalType { LogicalType::Map { .. } => LogicalTypeID::MAP, LogicalType::Union { .. } => LogicalTypeID::UNION, LogicalType::UUID => LogicalTypeID::UUID, + LogicalType::Json => LogicalTypeID::JSON, LogicalType::Decimal { .. } => LogicalTypeID::DECIMAL, } } diff --git a/src/value.rs b/src/value.rs index 4427302..56b84ba 100644 --- a/src/value.rs +++ b/src/value.rs @@ -15,6 +15,7 @@ pub enum ConversionError { TimestampNs(i64), TimestampMs(i64), TimestampSec(i64), + Json(String, serde_json::Error), } impl std::fmt::Display for ConversionError { @@ -26,7 +27,7 @@ impl std::fmt::Display for ConversionError { impl std::fmt::Debug for ConversionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ConversionError::{ - Date, Timestamp, TimestampMs, TimestampNs, TimestampSec, TimestampTz, + Date, Json, Timestamp, TimestampMs, TimestampNs, TimestampSec, TimestampTz, }; match self { Date(days) => write!(f, "Could not convert Lbug date offset of UNIX_EPOCH + {days} days to time::Date"), @@ -35,6 +36,7 @@ impl std::fmt::Debug for ConversionError { TimestampNs(ns) => write!(f, "Could not convert Lbug timestamp_ns offset of UNIX_EPOCH + {ns} nanoseconds to time::OffsetDateTime"), TimestampMs(ms) => write!(f, "Could not convert Lbug timestamp_ms offset of UNIX_EPOCH + {ms} milliseconds to time::OffsetDateTime"), TimestampSec(sec) => write!(f, "Could not convert Lbug timestamp_sec offset of UNIX_EPOCH + {sec} seconds to time::OffsetDateTime"), + Json(value, err) => write!(f, "Could not convert Lbug JSON value {value:?}: {err}"), } } } @@ -237,6 +239,7 @@ pub enum Value { InternalID(InternalID), /// String(String), + Json(serde_json::Value), Blob(Vec), // TODO: Enforce type of contents // LogicalType is necessary so that we can pass the correct type to the C++ API if the list is empty. @@ -296,6 +299,7 @@ impl std::fmt::Display for Value { Value::Int128(x) => write!(f, "{x}"), Value::Date(x) => write!(f, "{x}"), Value::String(x) => write!(f, "{x}"), + Value::Json(x) => write!(f, "{x}"), Value::Blob(x) => write!(f, "{x:x?}"), Value::Null(_) => write!(f, ""), Value::List(_, x) | Value::Array(_, x) => display_list(f, x), @@ -369,6 +373,7 @@ impl From<&Value> for LogicalType { Value::TimestampMs(_) => LogicalType::TimestampMs, Value::TimestampSec(_) => LogicalType::TimestampSec, Value::String(_) => LogicalType::String, + Value::Json(_) => LogicalType::Json, Value::Blob(_) => LogicalType::Blob, Value::Null(x) => x.clone(), Value::List(x, _) => LogicalType::List { @@ -458,6 +463,12 @@ impl TryFrom<&ffi::Value> for Value { LogicalTypeID::FLOAT => Ok(Value::Float(ffi::value_get_float(value))), LogicalTypeID::DOUBLE => Ok(Value::Double(ffi::value_get_double(value))), LogicalTypeID::STRING => Ok(Value::String(ffi::value_get_string(value).to_string())), + LogicalTypeID::JSON => { + let json = ffi::value_get_string(value).to_string(); + serde_json::from_str(&json) + .map(Value::Json) + .map_err(|err| ConversionError::Json(json, err)) + } LogicalTypeID::BLOB => Ok(Value::Blob( ffi::value_get_string(value).as_bytes().to_vec(), )), @@ -754,6 +765,10 @@ impl TryInto> for Value { ffi::LogicalTypeID::STRING, value.as_bytes(), )), + Value::Json(value) => Ok(ffi::create_value_string( + ffi::LogicalTypeID::JSON, + &serde_json::to_vec(&value)?, + )), Value::Blob(value) => Ok(ffi::create_value_string(ffi::LogicalTypeID::BLOB, &value)), Value::Timestamp(value) => { Ok(ffi::create_value_timestamp(datetime_to_timestamp_t(value))) @@ -953,6 +968,12 @@ impl From for Value { } } +impl From for Value { + fn from(item: serde_json::Value) -> Self { + Value::Json(item) + } +} + impl From<&str> for Value { fn from(item: &str) -> Self { Value::String(item.to_string())