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
18 changes: 18 additions & 0 deletions test/src/d1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ pub async fn prepared_statement(
assert_eq!(columns[1].as_str(), Some("Ryan Upton"));
assert_eq!(columns[2].as_u64(), Some(21));

// The same rows, plus the column names they are positioned by.
let (column_names, rows) = stmt.raw_with_column_names::<serde_json::Value>().await?;
assert_eq!(column_names, vec!["id", "name", "age"]);
assert_eq!(rows.len(), 1);
let columns = &rows[0];

assert_eq!(columns[0].as_u64(), Some(6));
assert_eq!(columns[1].as_str(), Some("Ryan Upton"));
assert_eq!(columns[2].as_u64(), Some(21));

// Column names are still reported when the statement matches no rows.
let (column_names, rows) = worker::query!(&db, "SELECT * FROM people WHERE name = ?")
.bind_refs(&D1Type::Text("Nobody At All"))?
.raw_with_column_names::<serde_json::Value>()
.await?;
assert_eq!(column_names, vec!["id", "name", "age"]);
assert!(rows.is_empty());

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");
Expand Down
9 changes: 9 additions & 0 deletions worker-sys/src/types/d1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,13 @@ extern "C" {

#[wasm_bindgen(structural, method, catch, js_class=D1PreparedStatement, js_name=raw)]
pub fn raw(this: &D1PreparedStatement) -> Result<Promise, JsValue>;

/// `raw()` with an options object, e.g. `{ columnNames: true }`.
///
/// Bound separately because wasm-bindgen cannot overload `raw` on arity.
#[wasm_bindgen(structural, method, catch, js_class=D1PreparedStatement, js_name=raw)]
pub fn raw_with_options(
this: &D1PreparedStatement,
options: &::js_sys::Object,
) -> Result<Promise, JsValue>;
}
38 changes: 38 additions & 0 deletions worker/src/d1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,44 @@ impl D1PreparedStatement {
Ok(vec)
}

/// Executes a query against the database and returns the column names alongside a `Vec` of
/// rows, as `(column_names, rows)`.
///
/// This calls `raw({ columnNames: true })`, which returns the column names as the first array
/// of the result. They are split out here so the rows stay uniformly typed as `T`.
///
/// Useful when a caller indexes rows positionally but still needs to know which column each
/// position refers to, such as an ORM mapping layer.
///
/// The column names are returned even when the query matches no rows, so this can be used to
/// inspect a statement's shape without a result set.
pub async fn raw_with_column_names<T>(&self) -> Result<(Vec<String>, Vec<Vec<T>>)>
where
T: for<'a> Deserialize<'a>,
{
let options = js_sys::Object::new();
js_sys::Reflect::set(&options, &JsValue::from_str("columnNames"), &JsValue::TRUE)?;

let result = JsFuture::from(self.0.raw_with_options(&options)?).await;
let result = cast_to_d1_error(result)?;
let result = result.dyn_into::<Array>()?;

let mut iter = result.iter();
// The header is always the first element, even for a zero-row result, so this branch is
// only a guard against an unexpectedly empty array rather than a normal case.
let Some(header) = iter.next() else {
return Ok((Vec::new(), Vec::new()));
};
let column_names: Vec<String> = serde_wasm_bindgen::from_value(header)?;

let mut rows = Vec::with_capacity(result.length().saturating_sub(1) as usize);
for value in iter {
rows.push(serde_wasm_bindgen::from_value(value)?);
}

Ok((column_names, rows))
}

/// Executes a query against the database and returns a `Vec` of JsValues.
pub async fn raw_js_value(&self) -> Result<Vec<JsValue>> {
let result = JsFuture::from(self.0.raw()?).await;
Expand Down