diff --git a/worker-sandbox/src/d1.rs b/worker-sandbox/src/d1.rs index e22945fe..94a3ed7e 100644 --- a/worker-sandbox/src/d1.rs +++ b/worker-sandbox/src/d1.rs @@ -18,7 +18,7 @@ pub async fn prepared_statement( let db = env.d1("DB")?; let unbound_stmt = worker::query!(&db, "SELECT * FROM people WHERE name = ?"); - let stmt = unbound_stmt.bind_refs(&[&D1Type::text("Ryan Upton")])?; + let stmt = unbound_stmt.bind_refs(&D1Type::Text("Ryan Upton"))?; // All rows let results = stmt.all().await?; @@ -49,11 +49,17 @@ pub async fn prepared_statement( assert_eq!(columns[1].as_str(), Some("Ryan Upton")); assert_eq!(columns[2].as_u64(), Some(21)); - let stmt_2 = unbound_stmt.bind_refs(&[&D1Type::text("John Smith")])?; + let stmt_2 = unbound_stmt.bind_refs([&D1Type::Text("John Smith")])?; let person = stmt_2.first::(None).await?.unwrap(); assert_eq!(person.name, "John Smith"); assert_eq!(person.age, 92); + let prepared_argument = PreparedArgument::new(&D1Type::Text("Dorian Fischer")); + let stmt_3 = unbound_stmt.bind_refs(&prepared_argument)?; + let person = stmt_3.first::(None).await?.unwrap(); + assert_eq!(person.name, "Dorian Fischer"); + assert_eq!(person.age, 19); + Response::ok("ok") } diff --git a/worker/src/d1/mod.rs b/worker/src/d1/mod.rs index 4afb5f25..84e5a4a7 100644 --- a/worker/src/d1/mod.rs +++ b/worker/src/d1/mod.rs @@ -1,5 +1,7 @@ use std::fmt::Display; use std::fmt::Formatter; +use std::iter::{once, Once}; +use std::ops::Deref; use std::result::Result as StdResult; use js_sys::Array; @@ -132,38 +134,87 @@ impl From for D1Database { /// Possible arguments that can be bound to [`D1PreparedStatement`] /// See https://developers.cloudflare.com/d1/build-with-d1/d1-client-api/#type-conversion -pub struct D1Type(JsValue); +pub enum D1Type<'a> { + Null, + Real(f64), + // I believe JS always casts to float. Documentation states it can accept up to 53 bits of signed precision + // so I went with i32 here. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type + Integer(i32), + Text(&'a str), + Boolean(bool), + Blob(&'a [u8]), +} + +/// A pre-computed argument for `bind_refs`. +/// +/// Arguments must be converted to `JsValue` when bound. If you plan to +/// re-use the same argument multiple times, consider using a `PreparedArgument` +/// which does this once on construction. +pub struct PreparedArgument<'a> { + value: &'a D1Type<'a>, + js_value: JsValue, +} -impl D1Type { - pub fn null() -> Self { - D1Type(JsValue::null()) +impl<'a> PreparedArgument<'a> { + pub fn new(value: &'a D1Type) -> PreparedArgument<'a> { + Self { + value, + js_value: value.into(), + } } +} - pub fn real(f: f64) -> Self { - D1Type(JsValue::from_f64(f)) +impl<'a> From<&'a D1Type<'a>> for JsValue { + fn from(value: &'a D1Type<'a>) -> Self { + match value { + D1Type::Null => JsValue::null(), + D1Type::Real(f) => JsValue::from_f64(*f), + D1Type::Integer(i) => JsValue::from_f64(*i as f64), + D1Type::Text(s) => JsValue::from_str(s), + D1Type::Boolean(b) => JsValue::from_bool(*b), + D1Type::Blob(a) => serde_wasm_bindgen::to_value(a).unwrap(), + } } +} - pub fn integer(i: i32) -> Self { - // I believe JS always casts to float. Documentation states it can accept up to 53 bits of signed precision - // so I went with i32 here. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type - D1Type(JsValue::from_f64(i as f64)) +impl<'a> Deref for PreparedArgument<'a> { + type Target = D1Type<'a>; + fn deref(&self) -> &Self::Target { + self.value } +} - pub fn text(s: &str) -> Self { - D1Type(JsValue::from_str(s)) +impl<'a> IntoIterator for &'a D1Type<'a> { + type Item = &'a D1Type<'a>; + type IntoIter = Once<&'a D1Type<'a>>; + /// Allows a single &D1Type to be passed to `bind_refs`, without placing it in an array. + fn into_iter(self) -> Self::IntoIter { + once(self) } +} - pub fn boolean(b: bool) -> Self { - D1Type(JsValue::from_bool(b)) +impl<'a> IntoIterator for &'a PreparedArgument<'a> { + type Item = &'a PreparedArgument<'a>; + type IntoIter = Once<&'a PreparedArgument<'a>>; + /// Allows a single &PreparedArgument to be passed to `bind_refs`, without placing it in an array. + fn into_iter(self) -> Self::IntoIter { + once(self) } +} - pub fn blob(a: &[u8]) -> Result { - Ok(D1Type(serde_wasm_bindgen::to_value(a)?)) +pub trait D1Argument { + fn js_value(&self) -> impl AsRef; +} + +impl<'a> D1Argument for D1Type<'a> { + fn js_value(&self) -> impl AsRef { + Into::::into(self) } +} - fn inner(&self) -> &JsValue { - &self.0 +impl<'a> D1Argument for PreparedArgument<'a> { + fn js_value(&self) -> impl AsRef { + &self.js_value } } @@ -192,11 +243,12 @@ impl D1PreparedStatement { /// Bind one or more parameters to the statement. /// Returns a new statement with the bound parameters, leaving the old statement available for reuse. - pub fn bind_refs<'a, T>(&self, values: T) -> Result + pub fn bind_refs<'a, T, U: 'a>(&self, values: T) -> Result where - T: IntoIterator, + T: IntoIterator, + U: D1Argument, { - let array: Array = values.into_iter().map(|t| t.inner()).collect::(); + let array: Array = values.into_iter().map(|t| t.js_value()).collect::(); match self.0.bind(array) { Ok(stmt) => Ok(D1PreparedStatement(stmt)), @@ -206,10 +258,11 @@ impl D1PreparedStatement { /// Bind a batch of parameter values, returning a batch of prepared statements. /// Result can be passed to [`D1Database::batch`] to execute the statements. - pub fn batch_bind<'a, U: 'a, T: 'a>(&self, values: T) -> Result> + pub fn batch_bind<'a, U: 'a, T: 'a, V: 'a>(&self, values: T) -> Result> where T: IntoIterator, - &'a U: IntoIterator, + &'a U: IntoIterator, + V: D1Argument, { values .into_iter()