Repository: denoland/deno
Pull request: fix(ext/node): sqlite handle empty blob being NULL

Fixes https://github.com/denoland/deno/issues/28672

Changed files:
filename: .github/workflows/ci.generate.ts
patch: @@ -5,7 +5,7 @@ import { stringify } from "jsr:@std/yaml@^0.221/stringify";
 // Bump this number when you want to purge the cache.
 // Note: the tools/release/01_bump_crate_versions.ts script will update this version
 // automatically via regex, so ensure that this line maintains this format.
-const cacheVersion = 47;
+const cacheVersion = 48;
 
 const ubuntuX86Runner = "ubuntu-24.04";
 const ubuntuX86XlRunner = "ubuntu-24.04-xl";

filename: .github/workflows/ci.yml
patch: @@ -184,8 +184,8 @@ jobs:
             ~/.cargo/registry/index
             ~/.cargo/registry/cache
             ~/.cargo/git/db
-          key: '47-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}'
-          restore-keys: '47-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-'
+          key: '48-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles(''Cargo.lock'') }}'
+          restore-keys: '48-cargo-home-${{ matrix.os }}-${{ matrix.arch }}-'
         if: '!(matrix.skip)'
       - uses: dsherret/rust-toolchain-file@v1
         if: '!(matrix.skip)'
@@ -381,7 +381,7 @@ jobs:
             !./target/*/*.zip
             !./target/*/*.tar.gz
           key: never_saved
-          restore-keys: '47-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-'
+          restore-keys: '48-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-'
       - name: Apply and update mtime cache
         if: '!(matrix.skip) && (!startsWith(github.ref, ''refs/tags/''))'
         uses: ./.github/mtime_cache
@@ -699,7 +699,7 @@ jobs:
             !./target/*/gn_root
             !./target/*/*.zip
             !./target/*/*.tar.gz
-          key: '47-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}'
+          key: '48-cargo-target-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.profile }}-${{ matrix.job }}-${{ github.sha }}'
   wasm:
     name: build wasm32
     needs:

filename: ext/node/ops/sqlite/statement.rs
patch: @@ -173,11 +173,16 @@ impl StatementSync {
         ffi::SQLITE_BLOB => {
           let value = ffi::sqlite3_column_blob(self.inner, index);
           let size = ffi::sqlite3_column_bytes(self.inner, index);
-          let value =
-            std::slice::from_raw_parts(value as *const u8, size as usize);
-          let bs = v8::ArrayBuffer::new_backing_store_from_vec(value.to_vec())
-            .make_shared();
-          let ab = v8::ArrayBuffer::with_backing_store(scope, &bs);
+          let ab = if size == 0 {
+            v8::ArrayBuffer::new(scope, 0)
+          } else {
+            let value =
+              std::slice::from_raw_parts(value as *const u8, size as usize);
+            let bs =
+              v8::ArrayBuffer::new_backing_store_from_vec(value.to_vec())
+                .make_shared();
+            v8::ArrayBuffer::with_backing_store(scope, &bs)
+          };
           v8::Uint8Array::new(scope, ab, 0, size as _).unwrap().into()
         }
         ffi::SQLITE_NULL => v8::null(scope).into(),
@@ -255,8 +260,17 @@ impl StatementSync {
       unsafe { ffi::sqlite3_bind_null(raw, index) }
     } else if value.is_array_buffer_view() {
       let value: v8::Local<v8::ArrayBufferView> = value.try_into().unwrap();
-      let data = value.data();
-      let size = value.byte_length();
+      let mut data = value.data();
+      let mut size = value.byte_length();
+
+      // data may be NULL if length is 0 or ab is detached. we need to pass a valid pointer
+      // to sqlite3_bind_blob, so we use a static empty array in this case.
+      if data.is_null() {
+        static EMPTY: [u8; 0] = [];
+
+        data = EMPTY.as_ptr() as *mut _;
+        size = 0;
+      }
 
       // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt
       // as it lives as long as the StatementSync instance.

filename: tests/unit_node/sqlite_test.ts
patch: @@ -319,3 +319,17 @@ RETURNING id
   const id1 = insertPeople.run({ name: "Flash", birthdate: "1956-07-16" });
   assertEquals(id1, { lastInsertRowid: 1, changes: 1 });
 });
+
+Deno.test("[node/sqlite] StatementSync empty blob", () => {
+  const db = new DatabaseSync(":memory:");
+
+  db.exec("CREATE TABLE foo(a BLOB NOT NULL)");
+
+  db.prepare("INSERT into foo (a) values (?)")
+    .all(new Uint8Array([]));
+
+  const result = db.prepare("SELECT * from foo").get();
+  assertEquals(result, { a: new Uint8Array([]), __proto__: null });
+
+  db.close();
+});