diff --git a/test/src/d1.rs b/test/src/d1.rs index ca5dade1a..6e123f769 100644 --- a/test/src/d1.rs +++ b/test/src/d1.rs @@ -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::().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::() + .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::(None).await?.unwrap(); assert_eq!(person.name, "John Smith"); diff --git a/worker-sys/src/types/d1.rs b/worker-sys/src/types/d1.rs index 220f05f7b..b8f320f8b 100644 --- a/worker-sys/src/types/d1.rs +++ b/worker-sys/src/types/d1.rs @@ -97,4 +97,13 @@ extern "C" { #[wasm_bindgen(structural, method, catch, js_class=D1PreparedStatement, js_name=raw)] pub fn raw(this: &D1PreparedStatement) -> Result; + + /// `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; } diff --git a/worker/src/d1/mod.rs b/worker/src/d1/mod.rs index 6497596aa..a09b1d43a 100644 --- a/worker/src/d1/mod.rs +++ b/worker/src/d1/mod.rs @@ -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(&self) -> Result<(Vec, Vec>)> + 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::()?; + + 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 = 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> { let result = JsFuture::from(self.0.raw()?).await;