use js_sys::futures::JsFuture;
use js_sys::{Array, Object, Promise, Reflect};
use sqlx_cloudflare_core::js::{database_error, from_js, to_js_array};
use sqlx_cloudflare_core::{BatchResult, QueryResult};
use sqlx_core::error::Error;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use worker::worker_sys::types::{
D1Database as D1DatabaseSys, D1PreparedStatement as D1PreparedStatementSys,
D1Result as D1ResultSys,
};
use crate::{D1ArgumentValue, D1BatchResult, D1QueryResult, D1Row};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Object)]
type Statement;
#[wasm_bindgen(method, catch, js_name = raw)]
fn raw_with_options(this: &Statement, options: &JsValue) -> Result<Promise, JsValue>;
}
pub(crate) fn prepare(
db: &worker::D1Database,
sql: &str,
arguments: &[D1ArgumentValue],
) -> Result<D1PreparedStatementSys, Error> {
let db: &D1DatabaseSys = AsRef::<JsValue>::as_ref(db).unchecked_ref();
let statement = db.prepare(sql).map_err(|error| database_error(&error))?;
if arguments.is_empty() {
return Ok(statement);
}
let values = to_js_array(arguments)?;
statement
.bind(values)
.map_err(|error| database_error(&error))
}
pub(crate) async fn fetch(
statement: &D1PreparedStatementSys,
limit: Option<usize>,
) -> Result<Vec<D1Row>, Error> {
let options = Object::new();
Reflect::set(&options, &"columnNames".into(), &JsValue::TRUE)
.map_err(|error| protocol_error(&error))?;
let promise = statement
.unchecked_ref::<Statement>()
.raw_with_options(&options)
.map_err(|error| database_error(&error))?;
let result: Array = JsFuture::from(promise)
.await
.map_err(|error| database_error(&error))?
.dyn_into()
.map_err(|_| Error::Protocol("D1 `raw()` did not return an array".into()))?;
let mut result = result.iter();
let Some(names) = result.next() else {
return Ok(Vec::new());
};
let names = array(names)?
.iter()
.map(|name| {
name.as_string().ok_or_else(|| {
Error::Protocol("D1 returned a column name that is not a string".into())
})
})
.collect::<Result<Vec<_>, _>>()?;
let rows = result
.take(limit.unwrap_or(usize::MAX))
.map(|row| array(row)?.iter().map(|value| from_js(&value)).collect())
.collect::<Result<Vec<Vec<_>>, _>>()?;
D1Row::from_result(names, rows)
}
pub(crate) async fn run(statement: &D1PreparedStatementSys) -> Result<D1QueryResult, Error> {
let promise = statement.run().map_err(|error| database_error(&error))?;
let result: D1ResultSys = JsFuture::from(promise)
.await
.map_err(|error| database_error(&error))?
.unchecked_into();
query_result(&result)
}
pub(crate) async fn batch(
db: &worker::D1Database,
statements: Vec<D1PreparedStatementSys>,
) -> Result<Vec<D1BatchResult>, Error> {
let db: &D1DatabaseSys = AsRef::<JsValue>::as_ref(db).unchecked_ref();
let statements = statements.into_iter().collect::<Array>();
let promise = db
.batch(statements)
.map_err(|error| database_error(&error))?;
let results = array(
JsFuture::from(promise)
.await
.map_err(|error| database_error(&error))?,
)?;
results
.iter()
.map(|result| {
let result: &D1ResultSys = result.unchecked_ref();
Ok(BatchResult::new(rows(result)?, query_result(result)?))
})
.collect()
}
fn rows(result: &D1ResultSys) -> Result<Vec<D1Row>, Error> {
let Some(objects) = result.results().map_err(|error| protocol_error(&error))? else {
return Ok(Vec::new());
};
let Some(first) = objects.iter().next() else {
return Ok(Vec::new());
};
let names = Object::keys(first.unchecked_ref::<Object>())
.iter()
.map(|name| {
name.as_string().ok_or_else(|| {
Error::Protocol("D1 returned a column name that is not a string".into())
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut rows = Vec::with_capacity(objects.length() as usize);
for object in objects.iter() {
let mut values = Vec::with_capacity(names.len());
for name in &names {
let value = Reflect::get(&object, &JsValue::from_str(name))
.map_err(|error| protocol_error(&error))?;
values.push(from_js(&value)?);
}
rows.push(values);
}
D1Row::from_result(names, rows)
}
fn query_result(result: &D1ResultSys) -> Result<D1QueryResult, Error> {
let meta = result.meta().map_err(|error| protocol_error(&error))?;
let number = |key: &str| {
Reflect::get(&meta, &key.into())
.ok()
.and_then(|value| value.as_f64())
};
Ok(QueryResult::new(
number("changes").map_or(0, whole_number_u64),
number("last_row_id").map_or(0, whole_number_i64),
))
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "D1's counts are whole, non-negative and far below 2^53"
)]
fn whole_number_u64(value: f64) -> u64 {
value as u64
}
#[allow(
clippy::cast_possible_truncation,
reason = "a rowid D1 reports is a whole number it could represent exactly"
)]
fn whole_number_i64(value: f64) -> i64 {
value as i64
}
fn array(value: JsValue) -> Result<Array, Error> {
value
.dyn_into()
.map_err(|_| Error::Protocol("D1 returned a row that is not an array".into()))
}
fn protocol_error(error: &JsValue) -> Error {
Error::Protocol(format!("unexpected response from D1: {error:?}"))
}