From 8d01f4c64968967adc741eb22a556565acda1e1b Mon Sep 17 00:00:00 2001 From: Alexandre Faria Date: Mon, 9 Dec 2024 22:52:35 +0000 Subject: [PATCH 1/5] Fixed the d1 support for when the first finds no rows and should return None. --- worker/src/d1/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worker/src/d1/mod.rs b/worker/src/d1/mod.rs index bb62b133..c1350c17 100644 --- a/worker/src/d1/mod.rs +++ b/worker/src/d1/mod.rs @@ -286,6 +286,9 @@ impl D1PreparedStatement { { let result = JsFuture::from(self.0.first(col_name)?).await; let js_value = cast_to_d1_error(result)?; + if js_value.is_null() { + return Ok(None); + } let value = serde_wasm_bindgen::from_value(js_value)?; Ok(value) } From 36b034375fdcb172263a5ad372db463261fa4df2 Mon Sep 17 00:00:00 2001 From: Alexandre Faria Date: Mon, 9 Dec 2024 23:59:47 +0000 Subject: [PATCH 2/5] Added some test cases for d1 optionals support aka as nulls support. --- worker-sandbox/Cargo.toml | 1 + worker-sandbox/src/d1.rs | 211 +++++++++++++++++++++++++++++- worker-sandbox/src/lib.rs | 2 +- worker-sandbox/src/router.rs | 69 ++++++++++ worker-sandbox/tests/d1.spec.ts | 72 ++++++++++ worker-sandbox/tests/mf-socket.ts | 2 +- worker-sandbox/tests/mf.ts | 4 +- 7 files changed, 356 insertions(+), 5 deletions(-) diff --git a/worker-sandbox/Cargo.toml b/worker-sandbox/Cargo.toml index dafed263..5e9b19bc 100644 --- a/worker-sandbox/Cargo.toml +++ b/worker-sandbox/Cargo.toml @@ -39,6 +39,7 @@ serde-wasm-bindgen = "0.6.1" md5 = "0.7.0" tokio-stream = "0.1.15" tokio = { version = "1.28", default-features = false, features=['io-util'] } +wasm-bindgen-test.workspace = true worker-kv = { path = "../worker-kv" } [dependencies.axum] diff --git a/worker-sandbox/src/d1.rs b/worker-sandbox/src/d1.rs index 36a16fed..b7692be8 100644 --- a/worker-sandbox/src/d1.rs +++ b/worker-sandbox/src/d1.rs @@ -1,6 +1,9 @@ +use crate::{js_sys::{Object, Reflect}, wasm_bindgen}; use crate::SomeSharedData; use serde::Deserialize; -use worker::*; +use wasm_bindgen::JsValue; +use wasm_bindgen_test::wasm_bindgen_test; +use worker::{D1PreparedArgument, D1Type, Env, Error, Request, Response, Result}; #[derive(Deserialize)] struct Person { @@ -125,3 +128,209 @@ pub async fn error(_req: Request, env: Env, _data: SomeSharedData) -> Result, + age: Option, +} + +#[wasm_bindgen_test] +pub fn test_js_value_is_null() { + assert!(JsValue::NULL.is_null()); +} + +#[wasm_bindgen_test] +pub fn test_serialize_option_none() { + let serializer = serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true); + + let none_value: Option = None; + let js_value = ::serde::ser::Serialize::serialize(&none_value, &serializer).unwrap(); + + assert!(js_value.is_null(), "Expected null, got {:?}", js_value); +} + +#[wasm_bindgen_test] +pub fn test_deserialize_option_none() { + let js_value = Object::new(); + Reflect::set(&js_value, &JsValue::from_str("id"), &JsValue::from_f64(1.0)).unwrap(); + Reflect::set(&js_value, &JsValue::from_str("name"), &JsValue::NULL).unwrap(); + Reflect::set(&js_value, &JsValue::from_str("age"), &JsValue::NULL).unwrap(); + + let js_value: JsValue = js_value.into(); + + let value: NullablePerson = serde_wasm_bindgen::from_value(js_value).unwrap(); + + assert_eq!(value.id, 1); + assert_eq!(value.name, None); + assert_eq!(value.age, None); +} + +#[worker::send] +pub async fn jsvalue_null_is_null( + _req: Request, + _env: Env, + _data: SomeSharedData, +) -> Result { + console_error_panic_hook::set_once(); + + assert!(wasm_bindgen::JsValue::NULL.is_null()); + + Response::ok("ok") +} + +#[worker::send] +pub async fn serialize_optional_none( + _req: Request, + _env: Env, + _data: SomeSharedData, +) -> Result { + console_error_panic_hook::set_once(); + let serializer = serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true); + + let none: Option = None; + let js_none = ::serde::ser::Serialize::serialize(&none, &serializer).unwrap(); + assert!(js_none.is_null()); + + Response::ok("ok") +} + +#[worker::send] +pub async fn serialize_optional_some( + _req: Request, + _env: Env, + _data: SomeSharedData, +) -> Result { + console_error_panic_hook::set_once(); + let serializer = serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true); + + let some: Option = Some("Hello".to_string()); + let js_some = ::serde::ser::Serialize::serialize(&some, &serializer).unwrap(); + assert!(js_some.is_string()); + + Response::ok("ok") +} + +#[worker::send] +pub async fn deserialize_optional_none( + _req: Request, + _env: Env, + _data: SomeSharedData, +) -> Result { + console_error_panic_hook::set_once(); + + let js_value = Object::new(); + Reflect::set(&js_value, &JsValue::from_str("id"), &JsValue::from_f64(1.0)).unwrap(); + Reflect::set(&js_value, &JsValue::from_str("name"), &JsValue::NULL).unwrap(); + Reflect::set(&js_value, &JsValue::from_str("age"), &JsValue::NULL).unwrap(); + + let js_value: JsValue = js_value.into(); + + let value: NullablePerson = serde_wasm_bindgen::from_value(js_value).unwrap(); + + assert_eq!(value.id, 1); + assert_eq!(value.name, None); + assert_eq!(value.age, None); + + Response::ok("ok") +} + +#[worker::send] +pub async fn insert_and_retrieve_optional_none( + _req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let db = env.d1("DB")?; + + let query = worker::query!( + &db, + "INSERT INTO nullable_people (id, name, age) VALUES (?1, ?2, ?3)", + &3, + &None::, + &None:: + )?; + query.run().await?; + + let stmt = worker::query!(&db, "SELECT * FROM nullable_people WHERE id = 3"); + let person = stmt.first::(None).await?.unwrap(); + assert_eq!(person.id, 3); + assert_eq!(person.name, None); + assert_eq!(person.age, None); + + Response::ok("ok") +} + +#[worker::send] +pub async fn insert_and_retrieve_optional_some( + _req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let db = env.d1("DB")?; + let query = worker::query!( + &db, + "INSERT INTO nullable_people (id, name, age) VALUES (?1, ?2, ?3)", + &4, + &"Dude", + &12 + )?; + query.run().await?; + + let stmt = worker::query!(&db, "SELECT * FROM nullable_people WHERE id = 4"); + let person = stmt.first::(None).await?.unwrap(); + assert_eq!(person.id, 4); + assert_eq!(person.name, Some("Dude".to_string())); + assert_eq!(person.age, Some(12)); + + Response::ok("ok") +} + +#[worker::send] +pub async fn retrieve_optional_none( + _req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let db = env.d1("DB")?; + + let stmt = worker::query!(&db, "SELECT * FROM nullable_people WHERE id = 1"); + let person = stmt.first::(None).await?.unwrap(); + assert_eq!(person.id, 1); + assert_eq!(person.name, None); + assert_eq!(person.age, None); + + Response::ok("ok") +} + +#[worker::send] +pub async fn retrieve_optional_some( + _req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let db = env.d1("DB")?; + + let stmt = worker::query!(&db, "SELECT * FROM nullable_people WHERE id = 2"); + let person = stmt.first::(None).await?.unwrap(); + assert_eq!(person.id, 2); + assert_eq!(person.name, Some("Wynne Ogley".to_string())); + assert_eq!(person.age, Some(67)); + + Response::ok("ok") +} + +#[worker::send] +pub async fn retrive_first_none( + _req: Request, + env: Env, + _data: SomeSharedData, +) -> Result { + let db = env.d1("DB")?; + + let stmt = worker::query!(&db, "SELECT * FROM nullable_people WHERE id = 9999"); + assert!(stmt.first::(None).await?.is_none()); + + Response::ok("ok") +} diff --git a/worker-sandbox/src/lib.rs b/worker-sandbox/src/lib.rs index 24113c49..877fabe0 100644 --- a/worker-sandbox/src/lib.rs +++ b/worker-sandbox/src/lib.rs @@ -72,7 +72,7 @@ type HandlerResponse = http::Response; #[cfg(not(feature = "http"))] type HandlerResponse = Response; -#[event(fetch)] +#[event(fetch, respond_with_errors)] pub async fn main( request: HandlerRequest, env: Env, diff --git a/worker-sandbox/src/router.rs b/worker-sandbox/src/router.rs index 025a028f..494c2f39 100644 --- a/worker-sandbox/src/router.rs +++ b/worker-sandbox/src/router.rs @@ -197,6 +197,42 @@ pub fn make_router(data: SomeSharedData, env: Env) -> axum::Router { .route("/d1/dump", get(handler!(d1::dump))) .route("/d1/exec", post(handler!(d1::exec))) .route("/d1/error", get(handler!(d1::error))) + .route( + "/d1/jsvalue_null_is_null", + get(handler!(d1::jsvalue_null_is_null)), + ) + .route( + "/d1/serialize_optional_none", + get(handler!(d1::serialize_optional_none)), + ) + .route( + "/d1/serialize_optional_some", + get(handler!(d1::serialize_optional_some)), + ) + .route( + "/d1/deserialize_optional_none", + get(handler!(d1::deserialize_optional_none)), + ) + .route( + "/d1/insert_and_retrieve_optional_none", + get(handler!(d1::insert_and_retrieve_optional_none)), + ) + .route( + "/d1/insert_and_retrieve_optional_some", + get(handler!(d1::insert_and_retrieve_optional_some)), + ) + .route( + "/d1/retrieve_optional_none", + get(handler!(d1::retrieve_optional_none)), + ) + .route( + "/d1/retrieve_optional_some", + get(handler!(d1::retrieve_optional_some)), + ) + .route( + "/d1/retrive_first_none", + get(handler!(d1::retrive_first_none)), + ) .route("/kv/get", get(handler!(kv::get))) .route("/kv/get-not-found", get(handler!(kv::get_not_found))) .route("/kv/list-keys", get(handler!(kv::list_keys))) @@ -339,6 +375,39 @@ pub fn make_router<'a>(data: SomeSharedData) -> Router<'a, SomeSharedData> { .get_async("/d1/dump", handler!(d1::dump)) .post_async("/d1/exec", handler!(d1::exec)) .get_async("/d1/error", handler!(d1::error)) + .get_async( + "/d1/jsvalue_null_is_null", + handler!(d1::jsvalue_null_is_null), + ) + .get_async( + "/d1/serialize_optional_none", + handler!(d1::serialize_optional_none), + ) + .get_async( + "/d1/serialize_optional_some", + handler!(d1::serialize_optional_some), + ) + .get_async( + "/d1/deserialize_optional_none", + handler!(d1::deserialize_optional_none), + ) + .get_async( + "/d1/insert_and_retrieve_optional_none", + handler!(d1::insert_and_retrieve_optional_none), + ) + .get_async( + "/d1/insert_and_retrieve_optional_some", + handler!(d1::insert_and_retrieve_optional_some), + ) + .get_async( + "/d1/retrieve_optional_none", + handler!(d1::retrieve_optional_none), + ) + .get_async( + "/d1/retrieve_optional_some", + handler!(d1::retrieve_optional_some), + ) + .get_async("/d1/retrive_first_none", handler!(d1::retrive_first_none)) .get_async("/kv/get", handler!(kv::get)) .get_async("/kv/get-not-found", handler!(kv::get_not_found)) .get_async("/kv/list-keys", handler!(kv::list_keys)) diff --git a/worker-sandbox/tests/d1.spec.ts b/worker-sandbox/tests/d1.spec.ts index 71a706c9..4ea2aa10 100644 --- a/worker-sandbox/tests/d1.spec.ts +++ b/worker-sandbox/tests/d1.spec.ts @@ -59,4 +59,76 @@ describe("d1", () => { const resp = await mf.dispatchFetch("http://fake.host/d1/error"); expect(resp.status).toBe(200); }); + + test("create table nullable", async () => { + let query = `CREATE TABLE IF NOT EXISTS nullable_people ( + id INTEGER PRIMARY KEY, + name TEXT, + age INTEGER + );`; + + expect(await exec(query)).toBe(1); + + query = `INSERT OR IGNORE INTO nullable_people + (id, name, age) + VALUES + (1, NULL, NULL), + (2, 'Wynne Ogley', 67)`; + + expect(await exec(query)).toBe(1); + }); + + test("jsvalue_null_is_null", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/jsvalue_null_is_null"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("serialize_optional_none", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/serialize_optional_none"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("serialize_optional_some", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/serialize_optional_some"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("deserialize_optional_none", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/deserialize_optional_none"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("insert_and_retrieve_optional_none", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/insert_and_retrieve_optional_none"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("insert_and_retrieve_optional_some", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/insert_and_retrieve_optional_some"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("retrieve_optional_none", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/retrieve_optional_none"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("retrieve_optional_some", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/retrieve_optional_some"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); + + test("retrive_first_none", async () => { + const resp = await mf.dispatchFetch("http://fake.host/d1/retrive_first_none"); + expect(await resp.text()).toBe("ok"); + expect(resp.status).toBe(200); + }); }); diff --git a/worker-sandbox/tests/mf-socket.ts b/worker-sandbox/tests/mf-socket.ts index c5dd3013..e8e62511 100644 --- a/worker-sandbox/tests/mf-socket.ts +++ b/worker-sandbox/tests/mf-socket.ts @@ -11,7 +11,7 @@ export const server = createServer(function (socket) { export const mf = new Miniflare({ scriptPath: "./build/worker/shim.mjs", - compatibilityDate: "2023-05-18", + compatibilityDate: "2024-12-05", modules: true, modulesRules: [ { type: "CompiledWasm", include: ["**/*.wasm"], fallthrough: true }, diff --git a/worker-sandbox/tests/mf.ts b/worker-sandbox/tests/mf.ts index 469f4f40..87b26329 100644 --- a/worker-sandbox/tests/mf.ts +++ b/worker-sandbox/tests/mf.ts @@ -1,5 +1,5 @@ import { Miniflare, Response } from "miniflare"; -import { MockAgent } from "undici"; +import { MockAgent } from 'undici'; const mockAgent = new MockAgent(); @@ -34,7 +34,7 @@ mockAgent export const mf = new Miniflare({ scriptPath: "./build/worker/shim.mjs", - compatibilityDate: "2023-05-18", + compatibilityDate: "2024-12-05", cache: true, cachePersist: false, d1Databases: ["DB"], From 999ca9aa878ad340fb6aa3e10628777f6f1fb70b Mon Sep 17 00:00:00 2001 From: Alexandre Faria Date: Wed, 11 Dec 2024 23:00:39 +0000 Subject: [PATCH 3/5] Dropped some wasm-bindgen-test tests because for some reason they weren't starting because of some strange dependency. --- worker-sandbox/Cargo.toml | 1 - worker-sandbox/src/d1.rs | 32 -------------------------------- 2 files changed, 33 deletions(-) diff --git a/worker-sandbox/Cargo.toml b/worker-sandbox/Cargo.toml index 5e9b19bc..dafed263 100644 --- a/worker-sandbox/Cargo.toml +++ b/worker-sandbox/Cargo.toml @@ -39,7 +39,6 @@ serde-wasm-bindgen = "0.6.1" md5 = "0.7.0" tokio-stream = "0.1.15" tokio = { version = "1.28", default-features = false, features=['io-util'] } -wasm-bindgen-test.workspace = true worker-kv = { path = "../worker-kv" } [dependencies.axum] diff --git a/worker-sandbox/src/d1.rs b/worker-sandbox/src/d1.rs index b7692be8..f36bab5a 100644 --- a/worker-sandbox/src/d1.rs +++ b/worker-sandbox/src/d1.rs @@ -2,7 +2,6 @@ use crate::{js_sys::{Object, Reflect}, wasm_bindgen}; use crate::SomeSharedData; use serde::Deserialize; use wasm_bindgen::JsValue; -use wasm_bindgen_test::wasm_bindgen_test; use worker::{D1PreparedArgument, D1Type, Env, Error, Request, Response, Result}; #[derive(Deserialize)] @@ -136,37 +135,6 @@ struct NullablePerson { age: Option, } -#[wasm_bindgen_test] -pub fn test_js_value_is_null() { - assert!(JsValue::NULL.is_null()); -} - -#[wasm_bindgen_test] -pub fn test_serialize_option_none() { - let serializer = serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true); - - let none_value: Option = None; - let js_value = ::serde::ser::Serialize::serialize(&none_value, &serializer).unwrap(); - - assert!(js_value.is_null(), "Expected null, got {:?}", js_value); -} - -#[wasm_bindgen_test] -pub fn test_deserialize_option_none() { - let js_value = Object::new(); - Reflect::set(&js_value, &JsValue::from_str("id"), &JsValue::from_f64(1.0)).unwrap(); - Reflect::set(&js_value, &JsValue::from_str("name"), &JsValue::NULL).unwrap(); - Reflect::set(&js_value, &JsValue::from_str("age"), &JsValue::NULL).unwrap(); - - let js_value: JsValue = js_value.into(); - - let value: NullablePerson = serde_wasm_bindgen::from_value(js_value).unwrap(); - - assert_eq!(value.id, 1); - assert_eq!(value.name, None); - assert_eq!(value.age, None); -} - #[worker::send] pub async fn jsvalue_null_is_null( _req: Request, From 5af94dca3bfa918834fad5d69705fe46af470c4e Mon Sep 17 00:00:00 2001 From: Alexandre Faria Date: Wed, 11 Dec 2024 23:02:09 +0000 Subject: [PATCH 4/5] Fixed back the import for a matter of consistency. --- worker-sandbox/tests/mf.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker-sandbox/tests/mf.ts b/worker-sandbox/tests/mf.ts index 87b26329..a4f58df7 100644 --- a/worker-sandbox/tests/mf.ts +++ b/worker-sandbox/tests/mf.ts @@ -1,5 +1,5 @@ import { Miniflare, Response } from "miniflare"; -import { MockAgent } from 'undici'; +import { MockAgent } from "undici"; const mockAgent = new MockAgent(); From d638ac1438f0ee852c2eac1377ba490110de2a8b Mon Sep 17 00:00:00 2001 From: Alexandre Faria Date: Sun, 29 Dec 2024 23:38:14 +0000 Subject: [PATCH 5/5] Removed is_null check as its also now done currently. --- worker/src/d1/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/worker/src/d1/mod.rs b/worker/src/d1/mod.rs index c1350c17..bb62b133 100644 --- a/worker/src/d1/mod.rs +++ b/worker/src/d1/mod.rs @@ -286,9 +286,6 @@ impl D1PreparedStatement { { let result = JsFuture::from(self.0.first(col_name)?).await; let js_value = cast_to_d1_error(result)?; - if js_value.is_null() { - return Ok(None); - } let value = serde_wasm_bindgen::from_value(js_value)?; Ok(value) }