Skip to content

Commit

Permalink
Another pass on the API ergonomics
Browse files Browse the repository at this point in the history
  • Loading branch information
kflansburg committed Mar 25, 2024
1 parent ac41bca commit 4b29162
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 26 deletions.
10 changes: 8 additions & 2 deletions worker-sandbox/src/d1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down Expand Up @@ -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::<Person>(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::<Person>(None).await?.unwrap();
assert_eq!(person.name, "Dorian Fischer");
assert_eq!(person.age, 19);

Response::ok("ok")
}

Expand Down
101 changes: 77 additions & 24 deletions worker/src/d1/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -132,38 +134,87 @@ impl From<D1DatabaseSys> 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<Self> {
Ok(D1Type(serde_wasm_bindgen::to_value(a)?))
pub trait D1Argument {
fn js_value(&self) -> impl AsRef<JsValue>;
}

impl<'a> D1Argument for D1Type<'a> {
fn js_value(&self) -> impl AsRef<JsValue> {
Into::<JsValue>::into(self)
}
}

fn inner(&self) -> &JsValue {
&self.0
impl<'a> D1Argument for PreparedArgument<'a> {
fn js_value(&self) -> impl AsRef<JsValue> {
&self.js_value
}
}

Expand Down Expand Up @@ -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<Self>
pub fn bind_refs<'a, T, U: 'a>(&self, values: T) -> Result<Self>
where
T: IntoIterator<Item = &'a &'a D1Type>,
T: IntoIterator<Item = &'a U>,
U: D1Argument,
{
let array: Array = values.into_iter().map(|t| t.inner()).collect::<Array>();
let array: Array = values.into_iter().map(|t| t.js_value()).collect::<Array>();

match self.0.bind(array) {
Ok(stmt) => Ok(D1PreparedStatement(stmt)),
Expand All @@ -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<Vec<Self>>
pub fn batch_bind<'a, U: 'a, T: 'a, V: 'a>(&self, values: T) -> Result<Vec<Self>>
where
T: IntoIterator<Item = &'a &'a U>,
&'a U: IntoIterator<Item = &'a &'a D1Type>,
&'a U: IntoIterator<Item = &'a V>,
V: D1Argument,
{
values
.into_iter()
Expand Down

0 comments on commit 4b29162

Please sign in to comment.