Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added D1 Support #270

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions worker-sys/src/d1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use ::js_sys::Object;
use wasm_bindgen::prelude::*;

use js_sys::{Array, Promise};

#[wasm_bindgen]
extern "C" {
#[derive(Debug, Clone)]
pub type D1Result;

#[wasm_bindgen(structural, method, getter, js_name=results)]
pub fn results(this: &D1Result) -> Option<Array>;

#[wasm_bindgen(structural, method, getter, js_name=success)]
pub fn success(this: &D1Result) -> bool;

#[wasm_bindgen(structural, method, getter, js_name=error)]
pub fn error(this: &D1Result) -> Option<String>;

#[wasm_bindgen(structural, method, getter, js_name=meta)]
pub fn meta(this: &D1Result) -> Object;
}

#[wasm_bindgen]
extern "C" {
#[derive(Debug, Clone)]
pub type D1ExecResult;

#[wasm_bindgen(structural, method, getter, js_name=count)]
pub fn count(this: &D1ExecResult) -> Option<u32>;

#[wasm_bindgen(structural, method, getter, js_name=time)]
pub fn time(this: &D1ExecResult) -> Option<f64>;
}

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends=::js_sys::Object, js_name=D1Database)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub type D1Database;

#[wasm_bindgen(structural, method, js_class=D1Database, js_name=prepare)]
pub fn prepare(this: &D1Database, query: &str) -> D1PreparedStatement;

#[wasm_bindgen(structural, method, js_class=D1Database, js_name=dump)]
pub fn dump(this: &D1Database) -> Promise;

#[wasm_bindgen(structural, method, js_class=D1Database, js_name=batch)]
pub fn batch(this: &D1Database, statements: Array) -> Promise;

#[wasm_bindgen(structural, method, js_class=D1Database, js_name=exec)]
pub fn exec(this: &D1Database, query: &str) -> Promise;
}

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends=::js_sys::Object, js_name=D1PreparedStatement)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub type D1PreparedStatement;

#[wasm_bindgen(structural, method, catch, variadic, js_class=D1PreparedStatement, js_name=bind)]
pub fn bind(this: &D1PreparedStatement, values: Array) -> Result<D1PreparedStatement, JsValue>;

#[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=first)]
pub fn first(this: &D1PreparedStatement, col_name: Option<&str>) -> Promise;

#[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=run)]
pub fn run(this: &D1PreparedStatement) -> Promise;

#[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=all)]
pub fn all(this: &D1PreparedStatement) -> Promise;

#[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=raw)]
pub fn raw(this: &D1PreparedStatement) -> Promise;
}
1 change: 1 addition & 0 deletions worker-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod abort;
pub mod cache;
pub mod cf;
pub mod context;
pub mod d1;
pub mod durable_object;
pub mod dynamic_dispatch;
pub mod fetcher;
Expand Down
216 changes: 216 additions & 0 deletions worker/src/d1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use js_sys::Array;
use js_sys::ArrayBuffer;
use js_sys::Uint8Array;
use serde::de::Deserialize;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use worker_sys::d1::D1Database as D1DatabaseSys;
use worker_sys::d1::D1ExecResult;
use worker_sys::d1::D1PreparedStatement as D1PreparedStatementSys;
use worker_sys::d1::D1Result as D1ResultSys;

use crate::env::EnvBinding;
use crate::Error;
use crate::Result;

// A D1 Database.
pub struct D1Database(D1DatabaseSys);

impl D1Database {
/// Prepare a query statement from a query string.
pub fn prepare<T: Into<String>>(&self, query: T) -> D1PreparedStatement {
self.0.prepare(&query.into()).into()
}

/// Dump the data in the database to a `Vec`.
pub async fn dump(&self) -> Result<Vec<u8>> {
let array_buffer = JsFuture::from(self.0.dump()).await?;
let array_buffer = array_buffer.dyn_into::<ArrayBuffer>()?;
let array = Uint8Array::new(&array_buffer);
let mut vec = Vec::with_capacity(array.length() as usize);
array.copy_to(&mut vec);
Ok(vec)
}

/// Batch execute one or more statements against the database.
///
/// Returns the results in the same order as the provided statements.
pub async fn batch(&self, statements: Vec<D1PreparedStatement>) -> Result<Vec<D1Result>> {
let statements = statements.into_iter().map(|s| s.0).collect::<Array>();
let results = JsFuture::from(self.0.batch(statements)).await?;
let results = results.dyn_into::<Array>()?;
let mut vec = Vec::with_capacity(results.length() as usize);
for result in results.iter() {
let result = result.dyn_into::<D1ResultSys>()?;
vec.push(D1Result(result));
}
Ok(vec)
}

/// Execute one or more queries directly against the database.
///
/// The input can be one or multiple queries separated by `\n`.
///
/// # Considerations
///
/// This method can have poorer performance (prepared statements can be reused
/// in some cases) and, more importantly, is less safe. Only use this
/// method for maintenance and one-shot tasks (example: migration jobs).
///
/// If an error occurs, an exception is thrown with the query and error
/// messages, execution stops and further statements are not executed.
pub async fn exec(&self, query: &str) -> Result<D1ExecResult> {
let result = JsFuture::from(self.0.exec(query)).await?;
Ok(result.into())
}
}

impl EnvBinding for D1Database {
const TYPE_NAME: &'static str = "D1Database";
}

impl JsCast for D1Database {
fn instanceof(val: &JsValue) -> bool {
val.is_instance_of::<D1DatabaseSys>()
}

fn unchecked_from_js(val: JsValue) -> Self {
Self(val.into())
}

fn unchecked_from_js_ref(val: &JsValue) -> &Self {
unsafe { &*(val as *const JsValue as *const Self) }
}
}

impl From<D1Database> for JsValue {
fn from(database: D1Database) -> Self {
JsValue::from(database.0)
}
}

impl AsRef<JsValue> for D1Database {
fn as_ref(&self) -> &JsValue {
&self.0
}
}

impl From<D1DatabaseSys> for D1Database {
fn from(inner: D1DatabaseSys) -> Self {
Self(inner)
}
}

// A D1 prepared query statement.
pub struct D1PreparedStatement(D1PreparedStatementSys);

impl D1PreparedStatement {
/// Bind one or more parameters to the statement.
///
/// D1 follows the SQLite convention for prepared statements parameter binding.
///
/// # Considerations
///
/// Supports Ordered (?NNNN) and Anonymous (?) parameters - named parameters are currently not supported.
///
pub fn bind<T>(&self, values: &[&T]) -> Result<Self>
FlareLine marked this conversation as resolved.
Show resolved Hide resolved
where
T: serde::ser::Serialize + ?Sized,
{
let mut params = Vec::new();
for value in values.iter() {
let res = serde_wasm_bindgen::to_value(value)?;
params.push(res);
}

let array: Array = params.into_iter().collect::<Array>();

match self.0.bind(array) {
Ok(stmt) => Ok(D1PreparedStatement(stmt)),
Err(err) => Err(Error::from(err)),
}
}

/// Return the first row of results.
///
/// If `col_name` is `Some`, returns that single value, otherwise returns the entire object.
///
/// If the query returns no rows, then this will return `None`.
///
/// If the query returns rows, but column does not exist, then this will return an `Err`.
pub async fn first<T>(&self, col_name: Option<&str>) -> Result<Option<T>>
where
T: for<'a> Deserialize<'a>,
{
let js_value = JsFuture::from(self.0.first(col_name)).await?;
let value = serde_wasm_bindgen::from_value(js_value)?;
Ok(value)
}

/// Executes a query against the database but only return metadata.
pub async fn run(&self) -> Result<D1Result> {
let result = JsFuture::from(self.0.run()).await?;
Ok(D1Result(result.into()))
}

/// Executes a query against the database and returns all rows and metadata.
pub async fn all(&self) -> Result<D1Result> {
let result = JsFuture::from(self.0.all()).await?;
Ok(D1Result(result.into()))
}

/// Executes a query against the database and returns a `Vec` of rows instead of objects.
pub async fn raw<T>(&self) -> Result<Vec<Vec<T>>>
where
T: for<'a> Deserialize<'a>,
{
let result = JsFuture::from(self.0.raw()).await?;
let result = result.dyn_into::<Array>()?;
let mut vec = Vec::with_capacity(result.length() as usize);
for value in result.iter() {
let value = serde_wasm_bindgen::from_value(value)?;
vec.push(value);
}
Ok(vec)
}
}

impl From<D1PreparedStatementSys> for D1PreparedStatement {
fn from(inner: D1PreparedStatementSys) -> Self {
Self(inner)
}
}

// The result of a D1 query execution.
pub struct D1Result(D1ResultSys);

impl D1Result {
/// Returns `true` if the result indicates a success, otherwise `false`.
pub fn success(&self) -> bool {
self.0.success()
}

/// Return the error contained in this result.
///
/// Returns `None` if the result indicates a success.
pub fn error(&self) -> Option<String> {
self.0.error()
}

/// Retrieve the collection of result objects, or an `Err` if an error occurred.
pub fn results<T>(&self) -> Result<Vec<T>>
where
T: for<'a> Deserialize<'a>,
{
if let Some(results) = self.0.results() {
let mut vec = Vec::with_capacity(results.length() as usize);
for result in results.iter() {
let result = serde_wasm_bindgen::from_value(result)?;
vec.push(result);
}
Ok(vec)
} else {
Ok(Vec::new())
}
}
}
7 changes: 6 additions & 1 deletion worker/src/env.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::Error;
#[cfg(feature = "queue")]
use crate::Queue;
use crate::{d1::D1Database, error::Error};
FlareLine marked this conversation as resolved.
Show resolved Hide resolved
use crate::{durable::ObjectNamespace, Bucket, DynamicDispatcher, Fetcher, Result};

use js_sys::Object;
Expand Down Expand Up @@ -69,6 +69,11 @@ impl Env {
pub fn bucket(&self, binding: &str) -> Result<Bucket> {
self.get_binding(binding)
}

/// Access a D1 Database by the binding name configured in your wrangler.toml file.
pub fn d1(&self, binding: &str) -> Result<D1Database> {
self.get_binding(binding)
}
}

pub trait EnvBinding: Sized + JsCast {
Expand Down
2 changes: 2 additions & 0 deletions worker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub use crate::abort::*;
pub use crate::cache::{Cache, CacheDeletionOutcome};
pub use crate::context::Context;
pub use crate::cors::Cors;
pub use crate::d1::*;
pub use crate::date::{Date, DateInit};
pub use crate::delay::Delay;
pub use crate::durable::*;
Expand Down Expand Up @@ -52,6 +53,7 @@ mod cache;
mod cf;
mod context;
mod cors;
mod d1;
FlareLine marked this conversation as resolved.
Show resolved Hide resolved
mod date;
mod delay;
pub mod durable;
Expand Down