mirror of
https://github.com/cloudflare/workers-rs.git
synced 2026-02-01 14:36:45 +00:00
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:
parent
c4107bf04b
commit
a55205fa6f
@ -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");
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user