Introduce D1PreparedStatement.bind_refs which does not take ownership (#493)

* Introduce D1PreparedStatement.bind_refs which does not take ownership of arguments

* Do not consume prepared statement when binding and batch binding

* Fix D1 tests in CI

* Introduce D1 type enum, support IntoIterator bind arguments

* D1Type Create JsValue on construction

* Another pass on the API ergonomics

* nits / docs

* Tweak batch_bind
This commit is contained in:
Kevin Flansburg 2024-03-26 12:48:03 -04:00 committed by GitHub
parent c4107bf04b
commit a55205fa6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 144 additions and 18 deletions

View File

@ -1,8 +1,7 @@
use crate::SomeSharedData;
use serde::Deserialize;
use worker::*;
use crate::SomeSharedData;
#[derive(Deserialize)]
struct Person {
id: u32,
@ -17,7 +16,9 @@ pub async fn prepared_statement(
_data: SomeSharedData,
) -> Result<Response> {
let db = env.d1("DB")?;
let stmt = worker::query!(&db, "SELECT * FROM people WHERE name = ?", "Ryan Upton")?;
let unbound_stmt = worker::query!(&db, "SELECT * FROM people WHERE name = ?");
let stmt = unbound_stmt.bind_refs(&D1Type::Text("Ryan Upton"))?;
// All rows
let results = stmt.all().await?;
@ -48,6 +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 person = stmt_2.first::<Person>(None).await?.unwrap();
assert_eq!(person.name, "John Smith");
assert_eq!(person.age, 92);
let prepared_argument = D1PreparedArgument::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")
}
@ -105,7 +117,7 @@ pub async fn error(_req: Request, env: Env, _data: SomeSharedData) -> Result<Res
if let Error::D1(error) = error {
assert_eq!(
error.cause(),
"Error in line 1: THIS IS NOT VALID SQL: SqliteError: near \"THIS\": syntax error"
"Error in line 1: THIS IS NOT VALID SQL: near \"THIS\": syntax error"
)
} else {
panic!("expected D1 error");

View File

@ -1,11 +1,8 @@
import { describe, test, expect } from "vitest";
const hasLocalDevServer = await fetch("http://localhost:8787/request")
.then((resp) => resp.ok)
.catch(() => false);
import { mf } from "./mf";
async function exec(query: string): Promise<number> {
const resp = await fetch("http://127.0.0.1:8787/d1/exec", {
const resp = await mf.dispatchFetch("http://fake.host/d1/exec", {
method: "POST",
body: query.split("\n").join(""),
});
@ -15,7 +12,7 @@ async function exec(query: string): Promise<number> {
return Number(body);
}
describe.skipIf(!hasLocalDevServer)("d1", () => {
describe("d1", () => {
test("create table", async () => {
const query = `CREATE TABLE IF NOT EXISTS uniqueTable (
id INTEGER PRIMARY KEY,
@ -49,22 +46,17 @@ describe.skipIf(!hasLocalDevServer)("d1", () => {
});
test("prepared statement", async () => {
const resp = await fetch("http://127.0.0.1:8787/d1/prepared");
const resp = await mf.dispatchFetch("http://fake.host/d1/prepared");
expect(resp.status).toBe(200);
});
test("batch", async () => {
const resp = await fetch("http://127.0.0.1:8787/d1/batch");
expect(resp.status).toBe(200);
});
test("dump", async () => {
const resp = await fetch("http://127.0.0.1:8787/d1/dump");
const resp = await mf.dispatchFetch("http://fake.host/d1/batch");
expect(resp.status).toBe(200);
});
test("error", async () => {
const resp = await fetch("http://127.0.0.1:8787/d1/error");
const resp = await mf.dispatchFetch("http://fake.host/d1/error");
expect(resp.status).toBe(200);
});
});

View File

@ -37,6 +37,7 @@ export const mf = new Miniflare({
compatibilityDate: "2023-05-18",
cache: true,
cachePersist: false,
d1Databases: ["DB"],
d1Persist: false,
kvPersist: false,
r2Persist: false,

View File

@ -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;
@ -25,6 +27,9 @@ pub mod macros;
// A D1 Database.
pub struct D1Database(D1DatabaseSys);
unsafe impl Sync for D1Database {}
unsafe impl Send for D1Database {}
impl D1Database {
/// Prepare a query statement from a query string.
pub fn prepare<T: Into<String>>(&self, query: T) -> D1PreparedStatement {
@ -127,6 +132,93 @@ impl From<D1DatabaseSys> for D1Database {
}
}
/// Possible argument types that can be bound to [`D1PreparedStatement`]
/// See https://developers.cloudflare.com/d1/build-with-d1/d1-client-api/#type-conversion
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
// D1 does not support `BigInt`
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 `D1PreparedArgument`
/// which does this once on construction.
pub struct D1PreparedArgument<'a> {
value: &'a D1Type<'a>,
js_value: JsValue,
}
impl<'a> D1PreparedArgument<'a> {
pub fn new(value: &'a D1Type) -> D1PreparedArgument<'a> {
Self {
value,
js_value: value.into(),
}
}
}
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(),
}
}
}
impl<'a> Deref for D1PreparedArgument<'a> {
type Target = D1Type<'a>;
fn deref(&self) -> &Self::Target {
self.value
}
}
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)
}
}
impl<'a> IntoIterator for &'a D1PreparedArgument<'a> {
type Item = &'a D1PreparedArgument<'a>;
type IntoIter = Once<&'a D1PreparedArgument<'a>>;
/// Allows a single &D1PreparedArgument to be passed to `bind_refs`, without placing it in an array.
fn into_iter(self) -> Self::IntoIter {
once(self)
}
}
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)
}
}
impl<'a> D1Argument for D1PreparedArgument<'a> {
fn js_value(&self) -> impl AsRef<JsValue> {
&self.js_value
}
}
// A D1 prepared query statement.
#[derive(Clone)]
pub struct D1PreparedStatement(D1PreparedStatementSys);
@ -150,6 +242,35 @@ 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, U: 'a>(&self, values: T) -> Result<Self>
where
T: IntoIterator<Item = &'a U>,
U: D1Argument,
{
let array: Array = values.into_iter().map(|t| t.js_value()).collect::<Array>();
match self.0.bind(array) {
Ok(stmt) => Ok(D1PreparedStatement(stmt)),
Err(err) => Err(Error::from(err)),
}
}
/// 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, V: 'a>(&self, values: T) -> Result<Vec<Self>>
where
T: IntoIterator<Item = U>,
U: IntoIterator<Item = &'a V>,
V: D1Argument,
{
values
.into_iter()
.map(|batch| self.bind_refs(batch))
.collect()
}
/// Return the first row of results.
///
/// If `col_name` is `Some`, returns that single value, otherwise returns the entire object.