mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-05 01:04:22 +01:00
Merge 'Initial pass on Rust bindings' from Pekka Enberg
This implements libSQL compatible Rust API on top of Limbo's core. The purpose of this is to allow libraries and apps that build on libSQL to use Limbo. Closes #597
This commit is contained in:
80
Cargo.lock
generated
80
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -5,6 +5,7 @@ resolver = "2"
|
||||
members = [
|
||||
"bindings/java",
|
||||
"bindings/python",
|
||||
"bindings/rust",
|
||||
"bindings/wasm",
|
||||
"cli",
|
||||
"sqlite3",
|
||||
|
||||
16
bindings/rust/Cargo.toml
Normal file
16
bindings/rust/Cargo.toml
Normal file
@@ -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"] }
|
||||
128
bindings/rust/src/lib.rs
Normal file
128
bindings/rust/src/lib.rs
Normal file
@@ -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<limbo_core::LimboError> for Error {
|
||||
fn from(_err: limbo_core::LimboError) -> Self {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
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<Database> {
|
||||
match self.path.as_str() {
|
||||
":memory:" => {
|
||||
let io: Arc<dyn limbo_core::IO> = 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<limbo_core::Database>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn connect(self) -> Result<Connection> {
|
||||
let conn = self.inner.connect();
|
||||
Ok(Connection { inner: conn })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
inner: Rc<limbo_core::Connection>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub async fn query(&self, sql: &str, params: impl IntoParams) -> Result<Rows> {
|
||||
let mut stmt = self.prepare(sql).await?;
|
||||
stmt.query(params).await
|
||||
}
|
||||
|
||||
pub async fn execute(&self, sql: &str, params: impl IntoParams) -> Result<u64> {
|
||||
let mut stmt = self.prepare(sql).await?;
|
||||
stmt.execute(params).await
|
||||
}
|
||||
|
||||
pub async fn prepare(&self, sql: &str) -> Result<Statement> {
|
||||
let stmt = self.inner.prepare(sql)?;
|
||||
Ok(Statement {
|
||||
_inner: Rc::new(stmt),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Statement {
|
||||
_inner: Rc<limbo_core::Statement>,
|
||||
}
|
||||
|
||||
impl Statement {
|
||||
pub async fn query(&mut self, params: impl IntoParams) -> Result<Rows> {
|
||||
let _params = params.into_params()?;
|
||||
todo!();
|
||||
}
|
||||
|
||||
pub async fn execute(&mut self, params: impl IntoParams) -> Result<u64> {
|
||||
let _params = params.into_params()?;
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoValue {
|
||||
fn into_value(self) -> Result<Value>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Params {
|
||||
None,
|
||||
Positional(Vec<Value>),
|
||||
Named(Vec<(String, Value)>),
|
||||
}
|
||||
pub struct Transaction {}
|
||||
|
||||
pub struct Rows {
|
||||
_inner: Rc<limbo_core::Rows>,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
pub async fn next(&mut self) -> Result<Option<Row>> {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Row {}
|
||||
|
||||
impl Row {
|
||||
pub fn get_value(&self, _index: usize) -> Result<Value> {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
313
bindings/rust/src/params.rs
Normal file
313
bindings/rust/src/params.rs
Normal file
@@ -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<Params>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[doc(hidden)]
|
||||
pub enum Params {
|
||||
None,
|
||||
Positional(Vec<Value>),
|
||||
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<I>(iter: I) -> impl IntoParams
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: IntoValue,
|
||||
{
|
||||
iter.into_iter().collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
impl Sealed for () {}
|
||||
impl IntoParams for () {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
Ok(Params::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sealed for Params {}
|
||||
impl IntoParams for Params {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue> Sealed for Vec<T> {}
|
||||
impl<T: IntoValue> IntoParams for Vec<T> {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
let values = self
|
||||
.into_iter()
|
||||
.map(|i| i.into_value())
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Params::Positional(values))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue> Sealed for Vec<(String, T)> {}
|
||||
impl<T: IntoValue> IntoParams for Vec<(String, T)> {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
let values = self
|
||||
.into_iter()
|
||||
.map(|(k, v)| Ok((k, v.into_value()?)))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Params::Named(values))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue, const N: usize> Sealed for [T; N] {}
|
||||
impl<T: IntoValue, const N: usize> IntoParams for [T; N] {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
self.into_iter().collect::<Vec<_>>().into_params()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue, const N: usize> Sealed for [(&str, T); N] {}
|
||||
impl<T: IntoValue, const N: usize> IntoParams for [(&str, T); N] {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
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::<Result<Vec<_>>>()?
|
||||
.into_params()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue + Clone, const N: usize> Sealed for &[T; N] {}
|
||||
impl<T: IntoValue + Clone, const N: usize> IntoParams for &[T; N] {
|
||||
fn into_params(self) -> Result<Params> {
|
||||
self.iter().cloned().collect::<Vec<_>>().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<Params> {
|
||||
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<Params> {
|
||||
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<Value>;
|
||||
}
|
||||
|
||||
impl<T> IntoValue for T
|
||||
where
|
||||
T: TryInto<Value>,
|
||||
T::Error: Into<crate::BoxError>,
|
||||
{
|
||||
fn into_value(self) -> Result<Value> {
|
||||
self.try_into()
|
||||
.map_err(|e| Error::ToSqlConversionFailure(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Result<Value> {
|
||||
fn into_value(self) -> Result<Value> {
|
||||
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])
|
||||
);
|
||||
}
|
||||
}
|
||||
364
bindings/rust/src/value.rs
Normal file
364
bindings/rust/src/value.rs
Normal file
@@ -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<u8>),
|
||||
}
|
||||
|
||||
/// 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<ValueType, Self::Err> {
|
||||
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<u8>> {
|
||||
if let Self::Blob(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i8> for Value {
|
||||
fn from(value: i8) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Value {
|
||||
fn from(value: i16) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(value: i32) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Value {
|
||||
Value::Integer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Value {
|
||||
fn from(value: u8) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Value {
|
||||
fn from(value: u16) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value {
|
||||
fn from(value: u32) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u64> for Value {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from(value: u64) -> Result<Value> {
|
||||
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<f32> for Value {
|
||||
fn from(value: f32) -> Value {
|
||||
Value::Real(value as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> 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<String> 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<Vec<u8>> for Value {
|
||||
fn from(value: Vec<u8>) -> Value {
|
||||
Value::Blob(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(value: bool) -> Value {
|
||||
Value::Integer(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for Value
|
||||
where
|
||||
T: Into<Value>,
|
||||
{
|
||||
fn from(value: Option<T>) -> 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<ValueRef<'_>> 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<Option<T>> for ValueRef<'a>
|
||||
where
|
||||
T: Into<ValueRef<'a>>,
|
||||
{
|
||||
#[inline]
|
||||
fn from(s: Option<T>) -> ValueRef<'a> {
|
||||
match s {
|
||||
Some(x) => x.into(),
|
||||
None => ValueRef::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user