diff --git a/Cargo.lock b/Cargo.lock index 915456422..fd7bec345 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1236,6 +1236,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "limbo_libsql" +version = "0.0.11" +dependencies = [ + "limbo_core", + "thiserror 2.0.9", + "tokio", +] + [[package]] name = "limbo_macros" version = "0.0.11" @@ -1365,6 +1374,17 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "mockall" version = "0.13.1" @@ -1524,7 +1544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.6", + "thiserror 2.0.9", "ucd-trie", ] @@ -2129,6 +2149,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bf3a9dccf2c079bf1465d449a485c85b36443caf765f2f127bfec28b180f75" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2150,6 +2179,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "sqlite3-parser" version = "0.13.0" @@ -2323,11 +2362,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.9", ] [[package]] @@ -2343,9 +2382,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", @@ -2362,6 +2401,35 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "tracing" version = "0.1.41" diff --git a/Cargo.toml b/Cargo.toml index 92897cbb4..bf1eb1062 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ resolver = "2" members = [ "bindings/java", "bindings/python", + "bindings/rust", "bindings/wasm", "cli", "sqlite3", diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 000000000..bdf101c56 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright 2025 the Limbo authors. All rights reserved. MIT license. + +[package] +name = "limbo_libsql" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +limbo_core = { path = "../../core" } +thiserror = "2.0.9" + +[dev-dependencies] +tokio = { version = "1.29.1", features = ["full"] } \ No newline at end of file diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 000000000..13705d08e --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -0,0 +1,128 @@ +pub mod params; +mod value; + +pub use params::params_from_iter; + +use crate::params::*; +use crate::value::*; +use std::rc::Rc; +use std::sync::Arc; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("SQL conversion failure: `{0}`")] + ToSqlConversionFailure(crate::BoxError), +} + +impl From for Error { + fn from(_err: limbo_core::LimboError) -> Self { + todo!(); + } +} + +pub(crate) type BoxError = Box; + +pub type Result = std::result::Result; +pub struct Builder { + path: String, +} + +impl Builder { + pub fn new_local(path: &str) -> Self { + Self { + path: path.to_string(), + } + } + + #[allow(unused_variables, clippy::arc_with_non_send_sync)] + pub async fn build(self) -> Result { + match self.path.as_str() { + ":memory:" => { + let io: Arc = Arc::new(limbo_core::MemoryIO::new()?); + let db = limbo_core::Database::open_file(io, self.path.as_str())?; + Ok(Database { inner: db }) + } + _ => todo!(), + } + } +} + +pub struct Database { + inner: Arc, +} + +impl Database { + pub fn connect(self) -> Result { + let conn = self.inner.connect(); + Ok(Connection { inner: conn }) + } +} + +pub struct Connection { + inner: Rc, +} + +impl Connection { + pub async fn query(&self, sql: &str, params: impl IntoParams) -> Result { + let mut stmt = self.prepare(sql).await?; + stmt.query(params).await + } + + pub async fn execute(&self, sql: &str, params: impl IntoParams) -> Result { + let mut stmt = self.prepare(sql).await?; + stmt.execute(params).await + } + + pub async fn prepare(&self, sql: &str) -> Result { + let stmt = self.inner.prepare(sql)?; + Ok(Statement { + _inner: Rc::new(stmt), + }) + } +} + +pub struct Statement { + _inner: Rc, +} + +impl Statement { + pub async fn query(&mut self, params: impl IntoParams) -> Result { + let _params = params.into_params()?; + todo!(); + } + + pub async fn execute(&mut self, params: impl IntoParams) -> Result { + let _params = params.into_params()?; + todo!(); + } +} + +pub trait IntoValue { + fn into_value(self) -> Result; +} + +#[derive(Debug, Clone)] +pub enum Params { + None, + Positional(Vec), + Named(Vec<(String, Value)>), +} +pub struct Transaction {} + +pub struct Rows { + _inner: Rc, +} + +impl Rows { + pub async fn next(&mut self) -> Result> { + todo!(); + } +} + +pub struct Row {} + +impl Row { + pub fn get_value(&self, _index: usize) -> Result { + todo!(); + } +} diff --git a/bindings/rust/src/params.rs b/bindings/rust/src/params.rs new file mode 100644 index 000000000..c15b6adb5 --- /dev/null +++ b/bindings/rust/src/params.rs @@ -0,0 +1,313 @@ +//! This module contains all `Param` related utilities and traits. + +use crate::{Error, Result, Value}; + +mod sealed { + pub trait Sealed {} +} + +use sealed::Sealed; + +/// Converts some type into parameters that can be passed +/// to libsql. +/// +/// The trait is sealed and not designed to be implemented by hand +/// but instead provides a few ways to use it. +/// +/// # Passing parameters to libsql +/// +/// Many functions in this library let you pass parameters to libsql. Doing this +/// lets you avoid any risk of SQL injection, and is simpler than escaping +/// things manually. These functions generally contain some paramter that generically +/// accepts some implementation this trait. +/// +/// # Positional parameters +/// +/// These can be supplied in a few ways: +/// +/// - For heterogeneous parameter lists of 16 or less items a tuple syntax is supported +/// by doing `(1, "foo")`. +/// - For hetergeneous parameter lists of 16 or greater, the [`limbo_libsql::params!`] is supported +/// by doing `limbo_libsql::params![1, "foo"]`. +/// - For homogeneous paramter types (where they are all the same type), const arrays are +/// supported by doing `[1, 2, 3]`. +/// +/// # Example (positional) +/// +/// ```rust,no_run +/// # use limbo_libsql::{Connection, params}; +/// # async fn run(conn: Connection) -> limbo_libsql::Result<()> { +/// let mut stmt = conn.prepare("INSERT INTO test (a, b) VALUES (?1, ?2)").await?; +/// +/// // Using a tuple: +/// stmt.execute((0, "foobar")).await?; +/// +/// // Using `limbo_libsql::params!`: +/// stmt.execute(params![1i32, "blah"]).await?; +/// +/// // array literal — non-references +/// stmt.execute([2i32, 3i32]).await?; +/// +/// // array literal — references +/// stmt.execute(["foo", "bar"]).await?; +/// +/// // Slice literal, references: +/// stmt.execute([2i32, 3i32]).await?; +/// +/// # Ok(()) +/// # } +/// ``` +/// +/// # Named paramters +/// +/// - For heterogeneous parameter lists of 16 or less items a tuple syntax is supported +/// by doing `(("key1", 1), ("key2", "foo"))`. +/// - For hetergeneous parameter lists of 16 or greater, the [`limbo_libsql::params!`] is supported +/// by doing `limbo_libsql::named_params!["key1": 1, "key2": "foo"]`. +/// - For homogeneous paramter types (where they are all the same type), const arrays are +/// supported by doing `[("key1", 1), ("key2, 2), ("key3", 3)]`. +/// +/// # Example (named) +/// +/// ```rust,no_run +/// # use limbo_libsql::{Connection, named_params}; +/// # async fn run(conn: Connection) -> limbo_libsql::Result<()> { +/// let mut stmt = conn.prepare("INSERT INTO test (a, b) VALUES (:key1, :key2)").await?; +/// +/// // Using a tuple: +/// stmt.execute(((":key1", 0), (":key2", "foobar"))).await?; +/// +/// // Using `limbo_libsql::named_params!`: +/// stmt.execute(named_params! {":key1": 1i32, ":key2": "blah" }).await?; +/// +/// // const array: +/// stmt.execute([(":key1", 2i32), (":key2", 3i32)]).await?; +/// +/// # Ok(()) +/// # } +/// ``` +pub trait IntoParams: Sealed { + // Hide this because users should not be implementing this + // themselves. We should consider sealing this trait. + #[doc(hidden)] + fn into_params(self) -> Result; +} + +#[derive(Debug, Clone)] +#[doc(hidden)] +pub enum Params { + None, + Positional(Vec), + Named(Vec<(String, Value)>), +} + +/// Convert an owned iterator into Params. +/// +/// # Example +/// +/// ```rust +/// # use limbo_libsql::{Connection, params_from_iter, Rows}; +/// # async fn run(conn: &Connection) { +/// +/// let iter = vec![1, 2, 3]; +/// +/// conn.query( +/// "SELECT * FROM users WHERE id IN (?1, ?2, ?3)", +/// params_from_iter(iter) +/// ) +/// .await +/// .unwrap(); +/// # } +/// ``` +pub fn params_from_iter(iter: I) -> impl IntoParams +where + I: IntoIterator, + I::Item: IntoValue, +{ + iter.into_iter().collect::>() +} + +impl Sealed for () {} +impl IntoParams for () { + fn into_params(self) -> Result { + Ok(Params::None) + } +} + +impl Sealed for Params {} +impl IntoParams for Params { + fn into_params(self) -> Result { + Ok(self) + } +} + +impl Sealed for Vec {} +impl IntoParams for Vec { + fn into_params(self) -> Result { + let values = self + .into_iter() + .map(|i| i.into_value()) + .collect::>>()?; + + Ok(Params::Positional(values)) + } +} + +impl Sealed for Vec<(String, T)> {} +impl IntoParams for Vec<(String, T)> { + fn into_params(self) -> Result { + let values = self + .into_iter() + .map(|(k, v)| Ok((k, v.into_value()?))) + .collect::>>()?; + + Ok(Params::Named(values)) + } +} + +impl Sealed for [T; N] {} +impl IntoParams for [T; N] { + fn into_params(self) -> Result { + self.into_iter().collect::>().into_params() + } +} + +impl Sealed for [(&str, T); N] {} +impl IntoParams for [(&str, T); N] { + fn into_params(self) -> Result { + self.into_iter() + // TODO: Pretty unfortunate that we need to allocate here when we know + // the str is likely 'static. Maybe we should convert our param names + // to be `Cow<'static, str>`? + .map(|(k, v)| Ok((k.to_string(), v.into_value()?))) + .collect::>>()? + .into_params() + } +} + +impl Sealed for &[T; N] {} +impl IntoParams for &[T; N] { + fn into_params(self) -> Result { + self.iter().cloned().collect::>().into_params() + } +} + +// NOTICE: heavily inspired by rusqlite +macro_rules! tuple_into_params { + ($count:literal : $(($field:tt $ftype:ident)),* $(,)?) => { + impl<$($ftype,)*> Sealed for ($($ftype,)*) where $($ftype: IntoValue,)* {} + impl<$($ftype,)*> IntoParams for ($($ftype,)*) where $($ftype: IntoValue,)* { + fn into_params(self) -> Result { + let params = Params::Positional(vec![$(self.$field.into_value()?),*]); + Ok(params) + } + } + } +} + +macro_rules! named_tuple_into_params { + ($count:literal : $(($field:tt $ftype:ident)),* $(,)?) => { + impl<$($ftype,)*> Sealed for ($((&str, $ftype),)*) where $($ftype: IntoValue,)* {} + impl<$($ftype,)*> IntoParams for ($((&str, $ftype),)*) where $($ftype: IntoValue,)* { + fn into_params(self) -> Result { + let params = Params::Named(vec![$((self.$field.0.to_string(), self.$field.1.into_value()?)),*]); + Ok(params) + } + } + } +} + +named_tuple_into_params!(2: (0 A), (1 B)); +named_tuple_into_params!(3: (0 A), (1 B), (2 C)); +named_tuple_into_params!(4: (0 A), (1 B), (2 C), (3 D)); +named_tuple_into_params!(5: (0 A), (1 B), (2 C), (3 D), (4 E)); +named_tuple_into_params!(6: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F)); +named_tuple_into_params!(7: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G)); +named_tuple_into_params!(8: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H)); +named_tuple_into_params!(9: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I)); +named_tuple_into_params!(10: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J)); +named_tuple_into_params!(11: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K)); +named_tuple_into_params!(12: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L)); +named_tuple_into_params!(13: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M)); +named_tuple_into_params!(14: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N)); +named_tuple_into_params!(15: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O)); +named_tuple_into_params!(16: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O), (15 P)); + +tuple_into_params!(2: (0 A), (1 B)); +tuple_into_params!(3: (0 A), (1 B), (2 C)); +tuple_into_params!(4: (0 A), (1 B), (2 C), (3 D)); +tuple_into_params!(5: (0 A), (1 B), (2 C), (3 D), (4 E)); +tuple_into_params!(6: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F)); +tuple_into_params!(7: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G)); +tuple_into_params!(8: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H)); +tuple_into_params!(9: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I)); +tuple_into_params!(10: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J)); +tuple_into_params!(11: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K)); +tuple_into_params!(12: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L)); +tuple_into_params!(13: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M)); +tuple_into_params!(14: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N)); +tuple_into_params!(15: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O)); +tuple_into_params!(16: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O), (15 P)); + +// TODO: Should we rename this to `ToSql` which makes less sense but +// matches the error variant we have in `Error`. Or should we change the +// error variant to match this breaking the few people that currently use +// this error variant. +pub trait IntoValue { + fn into_value(self) -> Result; +} + +impl IntoValue for T +where + T: TryInto, + T::Error: Into, +{ + fn into_value(self) -> Result { + self.try_into() + .map_err(|e| Error::ToSqlConversionFailure(e.into())) + } +} + +impl IntoValue for Result { + fn into_value(self) -> Result { + self + } +} + +/// Construct positional params from a hetergeneous set of params types. +#[macro_export] +macro_rules! params { + () => { + () + }; + ($($value:expr),* $(,)?) => {{ + use $crate::params::IntoValue; + [$($value.into_value()),*] + + }}; +} + +/// Construct named params from a hetergeneous set of params types. +#[macro_export] +macro_rules! named_params { + () => { + () + }; + ($($param_name:literal: $value:expr),* $(,)?) => {{ + use $crate::params::IntoValue; + [$(($param_name, $value.into_value())),*] + }}; +} + +#[cfg(test)] +mod tests { + use crate::Value; + + #[test] + fn test_serialize_array() { + assert_eq!( + params!([0; 16])[0].as_ref().unwrap(), + &Value::Blob(vec![0; 16]) + ); + } +} diff --git a/bindings/rust/src/value.rs b/bindings/rust/src/value.rs new file mode 100644 index 000000000..672444afc --- /dev/null +++ b/bindings/rust/src/value.rs @@ -0,0 +1,364 @@ +use std::str::FromStr; + +use crate::{Error, Result}; + +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + Null, + Integer(i64), + Real(f64), + Text(String), + Blob(Vec), +} + +/// The possible types a column can be in libsql. +#[derive(Debug, Copy, Clone)] +pub enum ValueType { + Integer = 1, + Real, + Text, + Blob, + Null, +} + +impl FromStr for ValueType { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + match s { + "TEXT" => Ok(ValueType::Text), + "INTEGER" => Ok(ValueType::Integer), + "BLOB" => Ok(ValueType::Blob), + "NULL" => Ok(ValueType::Null), + "REAL" => Ok(ValueType::Real), + _ => Err(()), + } + } +} + +impl Value { + /// Returns `true` if the value is [`Null`]. + /// + /// [`Null`]: Value::Null + #[must_use] + pub fn is_null(&self) -> bool { + matches!(self, Self::Null) + } + + /// Returns `true` if the value is [`Integer`]. + /// + /// [`Integer`]: Value::Integer + #[must_use] + pub fn is_integer(&self) -> bool { + matches!(self, Self::Integer(..)) + } + + /// Returns `true` if the value is [`Real`]. + /// + /// [`Real`]: Value::Real + #[must_use] + pub fn is_real(&self) -> bool { + matches!(self, Self::Real(..)) + } + + pub fn as_real(&self) -> Option<&f64> { + if let Self::Real(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the value is [`Text`]. + /// + /// [`Text`]: Value::Text + #[must_use] + pub fn is_text(&self) -> bool { + matches!(self, Self::Text(..)) + } + + pub fn as_text(&self) -> Option<&String> { + if let Self::Text(v) = self { + Some(v) + } else { + None + } + } + + pub fn as_integer(&self) -> Option<&i64> { + if let Self::Integer(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the value is [`Blob`]. + /// + /// [`Blob`]: Value::Blob + #[must_use] + pub fn is_blob(&self) -> bool { + matches!(self, Self::Blob(..)) + } + + pub fn as_blob(&self) -> Option<&Vec> { + if let Self::Blob(v) = self { + Some(v) + } else { + None + } + } +} + +impl From for Value { + fn from(value: i8) -> Value { + Value::Integer(value as i64) + } +} + +impl From for Value { + fn from(value: i16) -> Value { + Value::Integer(value as i64) + } +} + +impl From for Value { + fn from(value: i32) -> Value { + Value::Integer(value as i64) + } +} + +impl From for Value { + fn from(value: i64) -> Value { + Value::Integer(value) + } +} + +impl From for Value { + fn from(value: u8) -> Value { + Value::Integer(value as i64) + } +} + +impl From for Value { + fn from(value: u16) -> Value { + Value::Integer(value as i64) + } +} + +impl From for Value { + fn from(value: u32) -> Value { + Value::Integer(value as i64) + } +} + +impl TryFrom for Value { + type Error = crate::Error; + + fn try_from(value: u64) -> Result { + if value > i64::MAX as u64 { + Err(Error::ToSqlConversionFailure( + "u64 is too large to fit in an i64".into(), + )) + } else { + Ok(Value::Integer(value as i64)) + } + } +} + +impl From for Value { + fn from(value: f32) -> Value { + Value::Real(value as f64) + } +} + +impl From for Value { + fn from(value: f64) -> Value { + Value::Real(value) + } +} + +impl From<&str> for Value { + fn from(value: &str) -> Value { + Value::Text(value.to_owned()) + } +} + +impl From for Value { + fn from(value: String) -> Value { + Value::Text(value) + } +} + +impl From<&[u8]> for Value { + fn from(value: &[u8]) -> Value { + Value::Blob(value.to_owned()) + } +} + +impl From> for Value { + fn from(value: Vec) -> Value { + Value::Blob(value) + } +} + +impl From for Value { + fn from(value: bool) -> Value { + Value::Integer(value as i64) + } +} + +impl From> for Value +where + T: Into, +{ + fn from(value: Option) -> Self { + match value { + Some(inner) => inner.into(), + None => Value::Null, + } + } +} + +/// A borrowed version of `Value`. +#[derive(Debug)] +pub enum ValueRef<'a> { + Null, + Integer(i64), + Real(f64), + Text(&'a [u8]), + Blob(&'a [u8]), +} + +impl ValueRef<'_> { + pub fn data_type(&self) -> ValueType { + match *self { + ValueRef::Null => ValueType::Null, + ValueRef::Integer(_) => ValueType::Integer, + ValueRef::Real(_) => ValueType::Real, + ValueRef::Text(_) => ValueType::Text, + ValueRef::Blob(_) => ValueType::Blob, + } + } + + /// Returns `true` if the value ref is [`Null`]. + /// + /// [`Null`]: ValueRef::Null + #[must_use] + pub fn is_null(&self) -> bool { + matches!(self, Self::Null) + } + + /// Returns `true` if the value ref is [`Integer`]. + /// + /// [`Integer`]: ValueRef::Integer + #[must_use] + pub fn is_integer(&self) -> bool { + matches!(self, Self::Integer(..)) + } + + pub fn as_integer(&self) -> Option<&i64> { + if let Self::Integer(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the value ref is [`Real`]. + /// + /// [`Real`]: ValueRef::Real + #[must_use] + pub fn is_real(&self) -> bool { + matches!(self, Self::Real(..)) + } + + pub fn as_real(&self) -> Option<&f64> { + if let Self::Real(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the value ref is [`Text`]. + /// + /// [`Text`]: ValueRef::Text + #[must_use] + pub fn is_text(&self) -> bool { + matches!(self, Self::Text(..)) + } + + pub fn as_text(&self) -> Option<&[u8]> { + if let Self::Text(v) = self { + Some(v) + } else { + None + } + } + + /// Returns `true` if the value ref is [`Blob`]. + /// + /// [`Blob`]: ValueRef::Blob + #[must_use] + pub fn is_blob(&self) -> bool { + matches!(self, Self::Blob(..)) + } + + pub fn as_blob(&self) -> Option<&[u8]> { + if let Self::Blob(v) = self { + Some(v) + } else { + None + } + } +} + +impl From> for Value { + fn from(vr: ValueRef<'_>) -> Value { + match vr { + ValueRef::Null => Value::Null, + ValueRef::Integer(i) => Value::Integer(i), + ValueRef::Real(r) => Value::Real(r), + ValueRef::Text(s) => Value::Text(String::from_utf8_lossy(s).to_string()), + ValueRef::Blob(b) => Value::Blob(b.to_vec()), + } + } +} + +impl<'a> From<&'a str> for ValueRef<'a> { + fn from(s: &str) -> ValueRef<'_> { + ValueRef::Text(s.as_bytes()) + } +} + +impl<'a> From<&'a [u8]> for ValueRef<'a> { + fn from(s: &[u8]) -> ValueRef<'_> { + ValueRef::Blob(s) + } +} + +impl<'a> From<&'a Value> for ValueRef<'a> { + fn from(v: &'a Value) -> ValueRef<'a> { + match *v { + Value::Null => ValueRef::Null, + Value::Integer(i) => ValueRef::Integer(i), + Value::Real(r) => ValueRef::Real(r), + Value::Text(ref s) => ValueRef::Text(s.as_bytes()), + Value::Blob(ref b) => ValueRef::Blob(b), + } + } +} + +impl<'a, T> From> for ValueRef<'a> +where + T: Into>, +{ + #[inline] + fn from(s: Option) -> ValueRef<'a> { + match s { + Some(x) => x.into(), + None => ValueRef::Null, + } + } +}