Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions test/src/d1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,32 @@ pub async fn session_bookmark_roundtrip(
Response::ok("ok")
}

#[worker::send]
pub async fn blob_roundtrip(_req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let db = env.d1("DB")?;

db.exec("CREATE TABLE IF NOT EXISTS blobs (id INTEGER PRIMARY KEY, data BLOB NOT NULL);")
.await?;

let bytes: &[u8] = &[0x00, 0x01, 0x02, 0xfd, 0xfe, 0xff];
let insert = worker::query!(&db, "INSERT OR REPLACE INTO blobs (id, data) VALUES (1, ?)");
insert.bind_refs(&D1Type::Blob(bytes))?.run().await?;

// Verify the bound value arrived in SQLite byte-for-byte.
let stmt = worker::query!(&db, "SELECT hex(data) AS h FROM blobs WHERE id = 1");
let hex = stmt.first::<String>(Some("h")).await?.unwrap();
assert_eq!(hex, "000102FDFEFF");

let insert = worker::query!(&db, "INSERT OR REPLACE INTO blobs (id, data) VALUES (2, ?)");
insert.bind_refs(&D1Type::Blob(&[]))?.run().await?;

let stmt = worker::query!(&db, "SELECT length(data) AS l FROM blobs WHERE id = 2");
let len = stmt.first::<u32>(Some("l")).await?.unwrap();
assert_eq!(len, 0);

Response::ok("ok")
}

#[worker::send]
pub async fn exec(mut req: Request, env: Env, _data: SomeSharedData) -> Result<Response> {
let db = env.d1("DB")?;
Expand Down
1 change: 1 addition & 0 deletions test/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ macro_rules! add_routes (
add_route!($obj, get, "/d1/retrieve_optional_none", d1::retrieve_optional_none);
add_route!($obj, get, "/d1/retrieve_optional_some", d1::retrieve_optional_some);
add_route!($obj, get, "/d1/retrive_first_none", d1::retrive_first_none);
add_route!($obj, get, "/d1/blob_roundtrip", d1::blob_roundtrip);
add_route!($obj, get, "/kv/get", kv::get);
add_route!($obj, get, "/kv/get-not-found", kv::get_not_found);
add_route!($obj, get, "/kv/list-keys", kv::list_keys);
Expand Down
6 changes: 6 additions & 0 deletions test/tests/d1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ describe("d1", () => {
expect(await exec(query)).toBe(1);
});

test("blob roundtrip", async () => {
const resp = await mf.dispatchFetch(`${mfUrl}d1/blob_roundtrip`);
expect(await resp.text()).toBe("ok");
expect(resp.status).toBe(200);
});

test("jsvalue_null_is_null", async () => {
const resp = await mf.dispatchFetch(`${mfUrl}d1/jsvalue_null_is_null`);
expect(await resp.text()).toBe("ok");
Expand Down
5 changes: 4 additions & 1 deletion worker/src/d1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@ impl<'a> From<&'a D1Type<'a>> for JsValue {
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(),
// D1's documented BLOB representation is an `ArrayBuffer`; copying
// through a `Uint8Array` crosses the JS boundary once instead of
// serializing element-by-element into a number array.
D1Type::Blob(a) => js_sys::Uint8Array::from(a).buffer().into(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmph. this is a good change, but might break some consumers. Worth a break here IMO

}
}
}
Expand Down
2 changes: 1 addition & 1 deletion worker/src/http/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl HttpBody for Body {
stream
.poll_next_unpin(cx)
.map_ok(|buf| {
let bytes = Bytes::copy_from_slice(&js_sys::Uint8Array::from(buf).to_vec());
let bytes = Bytes::from(js_sys::Uint8Array::from(buf).to_vec());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1 win in the PR. Maybe worth breaking to its own PR if the rest of it is spooky/controversial

Frame::data(bytes)
})
.map_err(Error::Internal)
Expand Down
14 changes: 13 additions & 1 deletion worker/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ impl Response {

/// Access this response's body encoded as JSON.
pub async fn json<B: DeserializeOwned>(&mut self) -> Result<B> {
serde_json::from_str(&self.text().await?).map_err(Error::from)
if let ResponseBody::Body(bytes) = &self.body {
return serde_json::from_slice(bytes).map_err(Error::from);
}
serde_json::from_slice(&self.bytes().await?).map_err(Error::from)
}

/// Access this response's body encoded as raw bytes.
Expand Down Expand Up @@ -304,6 +307,15 @@ impl Response {
return Err(Error::RustError("WebSockets cannot be cloned".into()));
}

// Only stream bodies need the JS `Response.clone()` to tee the stream;
// fixed and empty bodies can be duplicated on the Rust side.
if !matches!(self.body, ResponseBody::Stream(_)) {
return Ok(Self {
body: self.body.clone(),
init: self.init.clone(),
});
}

let edge = web_sys::Response::from(&*self);
let cloned = edge.clone()?;

Expand Down
32 changes: 9 additions & 23 deletions worker/src/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,20 +265,12 @@ where
type Item = Result<T>;

fn next(&mut self) -> Option<Self::Item> {
let result = self.cursor.inner.next();
let result: js_sys::IteratorNext = self.cursor.inner.next().unchecked_into();

let done = js_sys::Reflect::get(&result, &JsValue::from("done"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(true);

if done {
if result.done() {
None
} else {
let value = js_sys::Reflect::get(&result, &JsValue::from("value"))
.map_err(Error::from)
.and_then(|js_val| swb::from_value(js_val).map_err(Error::from));
Some(value)
Some(swb::from_value(result.value()).map_err(Error::from))
}
}
}
Expand Down Expand Up @@ -313,7 +305,9 @@ impl Iterator for SqlCursorRawIterator {
}

fn js_array_to_sql_storage_values(js_val: JsValue) -> Result<Vec<SqlStorageValue>> {
let array = js_sys::Array::from(&js_val);
let array: js_sys::Array = js_val
.dyn_into()
.map_err(|_| Error::from("Expected an array of SQL values"))?;
let mut values = Vec::with_capacity(array.length() as usize);

for i in 0..array.length() {
Expand Down Expand Up @@ -396,20 +390,12 @@ impl Iterator for SqlCursor {
type Item = Result<JsValue>;

fn next(&mut self) -> Option<Self::Item> {
let result = self.inner.next();

// Extract 'done' property from iterator result
let done = js_sys::Reflect::get(&result, &JsValue::from("done"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(true);
let result: js_sys::IteratorNext = self.inner.next().unchecked_into();

if done {
if result.done() {
None
} else {
// Extract 'value' property from iterator result
let value = js_sys::Reflect::get(&result, &JsValue::from("value")).map_err(Error::from);
Some(value)
Some(Ok(result.value()))
}
}
}