Skip to content

Commit 4da1a78

Browse files
authored
fix: add BLOB support to SQL storage (#794)
* fix: add BLOB support to SQL storage Fixes missing BLOB support in SQL storage functionality including ArrayBuffer conversion, iterator methods for BLOB data, and test coverage for various BLOB operations. * utilize js_val.dyn_into for blobs
1 parent 9f581f8 commit 4da1a78

File tree

3 files changed

+396
-9
lines changed

3 files changed

+396
-9
lines changed

worker-sandbox/src/sql_iterator.rs

Lines changed: 246 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use serde::Deserialize;
2-
use worker::{durable_object, wasm_bindgen, Env, Request, Response, Result, SqlStorage, State};
2+
use worker::{
3+
durable_object, wasm_bindgen, Env, Request, Response, Result, SqlStorage, SqlStorageValue,
4+
State,
5+
};
36

47
/// A Durable Object that demonstrates SQL cursor iterator methods.
58
///
@@ -26,6 +29,38 @@ struct BadProduct {
2629
in_stock: i32,
2730
}
2831

32+
#[derive(Debug)]
33+
struct BlobData {
34+
id: i32,
35+
name: String,
36+
data: Vec<u8>,
37+
}
38+
39+
impl BlobData {
40+
fn from_raw_row(row: &[SqlStorageValue]) -> Option<Self> {
41+
if row.len() != 3 {
42+
return None;
43+
}
44+
45+
let id = match &row[0] {
46+
SqlStorageValue::Integer(i) => *i as i32,
47+
_ => return None,
48+
};
49+
50+
let name = match &row[1] {
51+
SqlStorageValue::String(s) => s.clone(),
52+
_ => return None,
53+
};
54+
55+
let data = match &row[2] {
56+
SqlStorageValue::Blob(bytes) => bytes.clone(),
57+
_ => return None,
58+
};
59+
60+
Some(BlobData { id, name, data })
61+
}
62+
}
63+
2964
impl DurableObject for SqlIterator {
3065
fn new(state: State, _env: Env) -> Self {
3166
let sql = state.storage().sql();
@@ -36,6 +71,12 @@ impl DurableObject for SqlIterator {
3671
None,
3772
).expect("create table");
3873

74+
sql.exec(
75+
"CREATE TABLE IF NOT EXISTS blob_data(id INTEGER PRIMARY KEY, name TEXT, data BLOB);",
76+
None,
77+
)
78+
.expect("create blob table");
79+
3980
// Check if we need to seed data
4081
let count: Vec<serde_json::Value> = sql
4182
.exec("SELECT COUNT(*) as count FROM products;", None)
@@ -68,6 +109,38 @@ impl DurableObject for SqlIterator {
68109
}
69110
}
70111

112+
let blob_count: Vec<serde_json::Value> = sql
113+
.exec("SELECT COUNT(*) as count FROM blob_data;", None)
114+
.expect("blob count query")
115+
.to_array()
116+
.expect("blob count result");
117+
118+
if blob_count
119+
.first()
120+
.and_then(|v| v.get("count"))
121+
.and_then(serde_json::Value::as_i64)
122+
.unwrap_or(0)
123+
== 0
124+
{
125+
let blob_test_data = vec![
126+
("binary_data", vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE]),
127+
("empty_blob", vec![]),
128+
("text_as_blob", "Hello, World!".as_bytes().to_vec()),
129+
(
130+
"large_blob",
131+
(0u8..=255).cycle().take(1000).collect::<Vec<u8>>(),
132+
),
133+
];
134+
135+
for (name, data) in blob_test_data {
136+
sql.exec(
137+
"INSERT INTO blob_data(name, data) VALUES (?, ?);",
138+
vec![name.into(), SqlStorageValue::Blob(data)],
139+
)
140+
.expect("insert blob data");
141+
}
142+
}
143+
71144
Self { sql }
72145
}
73146

@@ -79,7 +152,10 @@ impl DurableObject for SqlIterator {
79152
"/next" => self.handle_next(),
80153
"/raw" => self.handle_raw(),
81154
"/next-invalid" => self.handle_next_invalid(),
82-
_ => Response::ok("SQL Iterator Test - try /next, /raw, or /next-invalid endpoints"),
155+
"/blob-next" => self.handle_blob_next(),
156+
"/blob-raw" => self.handle_blob_raw(),
157+
"/blob-roundtrip" => self.handle_blob_roundtrip(),
158+
_ => Response::ok("SQL Iterator Test - try /next, /raw, /next-invalid, /blob-next, /blob-raw, or /blob-roundtrip endpoints"),
83159
}
84160
}
85161
}
@@ -166,6 +242,174 @@ impl SqlIterator {
166242

167243
Response::ok(response_body)
168244
}
245+
246+
fn handle_blob_next(&self) -> Result<Response> {
247+
let cursor = self
248+
.sql
249+
.exec("SELECT * FROM blob_data ORDER BY id;", None)?;
250+
251+
let mut results = Vec::new();
252+
let iterator = cursor.raw();
253+
254+
for result in iterator {
255+
match result {
256+
Ok(row) => {
257+
if let Some(blob_data) = BlobData::from_raw_row(&row) {
258+
let data_preview = if blob_data.data.len() <= 10 {
259+
format!("{:?}", blob_data.data)
260+
} else {
261+
format!(
262+
"{:?}...[{} bytes total]",
263+
&blob_data.data[..10],
264+
blob_data.data.len()
265+
)
266+
};
267+
268+
results.push(format!(
269+
"BlobData {}: {} - data: {}",
270+
blob_data.id, blob_data.name, data_preview
271+
));
272+
} else {
273+
results.push("Error: Failed to parse blob row".to_string());
274+
}
275+
}
276+
Err(e) => {
277+
results.push(format!("Error reading blob row: {e}"));
278+
}
279+
}
280+
}
281+
282+
let response_body = format!("blob-next() iterator results:\n{}", results.join("\n"));
283+
284+
Response::ok(response_body)
285+
}
286+
287+
fn handle_blob_raw(&self) -> Result<Response> {
288+
let cursor = self
289+
.sql
290+
.exec("SELECT * FROM blob_data ORDER BY id;", None)?;
291+
292+
let mut results = Vec::new();
293+
let column_names = cursor.column_names();
294+
results.push(format!("Columns: {}", column_names.join(", ")));
295+
296+
let iterator = cursor.raw();
297+
298+
for result in iterator {
299+
match result {
300+
Ok(row) => {
301+
let mut row_str = Vec::new();
302+
for (i, value) in row.iter().enumerate() {
303+
if i == 2 {
304+
// 'data' column is index 2 (id=0, name=1, data=2)
305+
match value {
306+
SqlStorageValue::Blob(bytes) => {
307+
if bytes.len() <= 10 {
308+
row_str.push(format!("Blob({:?})", bytes));
309+
} else {
310+
row_str.push(format!(
311+
"Blob({:?}...[{} bytes])",
312+
&bytes[..10],
313+
bytes.len()
314+
));
315+
}
316+
}
317+
_ => row_str.push(format!("{:?}", value)),
318+
}
319+
} else {
320+
row_str.push(format!("{:?}", value));
321+
}
322+
}
323+
results.push(format!("Row: [{}]", row_str.join(", ")));
324+
}
325+
Err(e) => {
326+
results.push(format!("Error reading blob row: {e}"));
327+
}
328+
}
329+
}
330+
331+
let response_body = format!("blob-raw() iterator results:\n{}", results.join("\n"));
332+
333+
Response::ok(response_body)
334+
}
335+
336+
fn handle_blob_roundtrip(&self) -> Result<Response> {
337+
// Test data roundtrip: insert a BLOB and immediately read it back
338+
let test_data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF];
339+
let test_name = "roundtrip_test";
340+
341+
// Insert test data
342+
self.sql.exec(
343+
"INSERT INTO blob_data(name, data) VALUES (?, ?);",
344+
vec![test_name.into(), SqlStorageValue::Blob(test_data.clone())],
345+
)?;
346+
347+
// Read it back using both methods (raw iterator approach for both)
348+
let cursor_next = self.sql.exec(
349+
"SELECT * FROM blob_data WHERE name = ? ORDER BY id DESC LIMIT 1;",
350+
vec![test_name.into()],
351+
)?;
352+
353+
let cursor_raw = self.sql.exec(
354+
"SELECT * FROM blob_data WHERE name = ? ORDER BY id DESC LIMIT 1;",
355+
vec![test_name.into()],
356+
)?;
357+
358+
let mut results = Vec::new();
359+
results.push(format!("Original data: {:?}", test_data));
360+
361+
// Test "next()" style result by converting raw data to BlobData struct
362+
let next_raw_iterator = cursor_next.raw();
363+
for result in next_raw_iterator {
364+
match result {
365+
Ok(row) => {
366+
if let Some(blob_data) = BlobData::from_raw_row(&row) {
367+
let matches = blob_data.data == test_data;
368+
results.push(format!(
369+
"next() result: {:?}, matches_original: {}",
370+
blob_data.data, matches
371+
));
372+
} else {
373+
results.push("next() error: Failed to parse blob row".to_string());
374+
}
375+
}
376+
Err(e) => {
377+
results.push(format!("next() error: {e}"));
378+
}
379+
}
380+
}
381+
382+
// Test raw iterator
383+
let raw_iterator = cursor_raw.raw();
384+
for result in raw_iterator {
385+
match result {
386+
Ok(row) => {
387+
if let Some(SqlStorageValue::Blob(data)) = row.get(2) {
388+
let matches = data == &test_data;
389+
results.push(format!(
390+
"raw() result: {:?}, matches_original: {}",
391+
data, matches
392+
));
393+
} else {
394+
results.push("raw() error: data column is not a blob".to_string());
395+
}
396+
}
397+
Err(e) => {
398+
results.push(format!("raw() error: {e}"));
399+
}
400+
}
401+
}
402+
403+
// Clean up test data
404+
self.sql.exec(
405+
"DELETE FROM blob_data WHERE name = ?;",
406+
vec![test_name.into()],
407+
)?;
408+
409+
let response_body = format!("blob-roundtrip test results:\n{}", results.join("\n"));
410+
411+
Response::ok(response_body)
412+
}
169413
}
170414

171415
#[worker::send]

0 commit comments

Comments
 (0)