Repository: expo/expo
Pull request #35896: [sqlite] run async tasks on module specific queue
Author: login: Kudo
Labels: name: bot: passed checks
description: ExpoBot has nothing to complain about
name: bot: fingerprint changed
Created: 2025-04-04T07:54:49Z | Merged: 2025-04-08T03:18:28Z

# Why

fixes #34531
close ENG-15356

# How

run async tasks on specific module queue that also supports parallel tasks

# Test Plan

ci passed

# Checklist

- [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting)
- [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin).
- [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)


Linked issue #34531: Expo SQLite not using Dispatcher.IO can cause blocking the main thread
### Minimal reproducible example

https://github.com/atorrescogollo/expo-demo-sqlite-without-dispatcher-io

### What platform(s) does this occur on?

Android

### Where did you reproduce the issue?

in a development build

### Summary

SQLite module doesn't seem to use Coroutines with any Dispatcher. This can make the main thread to be blocked when using another non concurrency safe module (e.g.: [blocker module](https://github.com/atorrescogollo/expo-demo-sqlite-without-dispatcher-io/blob/main/modules/blocker/android/src/main/java/expo/modules/blocker/BlockerModule.kt))

All Sync methods state:
>Note: Running heavy tasks with this function can block the JavaScript thread and affect performance.

So I understand that I can't use Sync methods directly if I want to do heavy queries. However, Async methods (that don't have that note) are not that different since they use the same thread and heavy queries can still affect in performance. **I think those Async methods should use Dispatcher.IO (or other)** to ensure not to block the main thread. Something like what video or printer modules do.

### Reproducible repo behaviour
There is a timer that updates the current date using a non concurrency safe module (it sends an event and it updates the state on the listener). That works fine until I click the button to run a bulk of SQLite queries. Then the timer freezes until all queries finish.

https://github.com/user-attachments/assets/9df4e862-2cab-417f-9c40-ecc232b5952a

### References

https://github.com/expo/expo/blob/49042d4dd47ce24676494714f4c2ede5315ef928/packages/expo-sqlite/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt#L166-L171


### Environment

```text
expo-env-info 1.2.2 environment info:
    System:
      OS: macOS 15.2
      Shell: 5.2.37 - /opt/homebrew/bin/bash
    Binaries:
      Node: 22.12.0 - ~/.nvm/versions/node/v22.12.0/bin/node
      npm: 10.9.0 - ~/.nvm/versions/node/v22.12.0/bin/npm
      Watchman: 2024.11.25.00 - /opt/homebrew/bin/watchman
    Managers:
      CocoaPods: 1.16.2 - /Users/atorres/.rbenv/shims/pod
    SDKs:
      iOS SDK:
        Platforms: DriverKit 24.2, iOS 18.2, macOS 15.2, tvOS 18.2, visionOS 2.2, watchOS 11.2
      Android SDK:
        API Levels: 28, 30, 31, 32, 33, 34, 35
        Build Tools: 30.0.3, 33.0.0, 34.0.0, 35.0.0
        System Images: android-33 | Google Play ARM 64 v8a
    IDEs:
      Android Studio: 2024.2 AI-242.23726.103.2422.12816248
      Xcode: 16.2/16C5032a - /usr/bin/xcodebuild
    npmPackages:
      expo: ~52.0.28 => 52.0.28 
      react: 18.3.1 => 18.3.1 
      react-native: 0.76.6 => 0.76.6 
    Expo Workflow: bare
```

### Expo Doctor Diagnostics

```text
Enabled experimental React Native Directory checks. Unset the EXPO_DOCTOR_ENABLE_DIRECTORY_CHECK environment variable to disable this check.
✔ Check package.json for common issues
✔ Check Expo config for common issues
✔ Check native tooling versions
✔ Check dependencies for packages that should not be installed directly
✔ Check npm/ yarn versions
✔ Check for issues with Metro config
✔ Check if the project meets version requirements for submission to app stores
✔ Check for app config fields that may not be synced in a non-CNG project
✔ Check for common project setup issues
✔ Validate packages against React Native Directory package metadata
✔ Check Expo config (app.json/ app.config.js) schema
✔ Check for legacy global CLI installed locally
✔ Check that native modules do not use incompatible support packages
✔ Check that packages match versions required by installed Expo SDK
✔ Check that native modules use compatible support package versions for installed Expo SDK

Didn't find any issues with the project!
```
Linked issue comments: user: login: expo-bot
created_at: 2025-03-31T19:37:52Z
body: Thank you for filing this issue!
  This comment acknowledges we believe this may be a bug and there’s enough information to investigate it.
  However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.

author_association: COLLABORATOR

Diff:
diff --git a/apps/test-suite/tests/SQLite.ts b/apps/test-suite/tests/SQLite.ts
index cbda9649b96380..fe0533cdb7db4f 100644
--- a/apps/test-suite/tests/SQLite.ts
+++ b/apps/test-suite/tests/SQLite.ts
@@ -247,8 +247,8 @@ CREATE TABLE IF NOT EXISTS nulling (id INTEGER PRIMARY KEY NOT NULL, x NUMERIC,
 `);
       await db.runAsync('INSERT INTO nulling (x, y) VALUES (?, ?)', [null, null]);
       const statement = await db.prepareAsync('INSERT INTO nulling (x, y) VALUES (?, ?)');
-      statement.executeAsync(null, null);
-      statement.finalizeAsync();
+      await statement.executeAsync(null, null);
+      await statement.finalizeAsync();
 
       const results = await db.getAllAsync<{ x: number | null; y: number | null }>(
         'SELECT * FROM nulling'
diff --git a/packages/expo-sqlite/CHANGELOG.md b/packages/expo-sqlite/CHANGELOG.md
index e65f5a36f8d1b0..7215a405d91d4d 100644
--- a/packages/expo-sqlite/CHANGELOG.md
+++ b/packages/expo-sqlite/CHANGELOG.md
@@ -31,6 +31,10 @@
 
 - Updated SQLite to 3.49.1 and SQLCipher to 4.7.0. ([#35741](https://github.com/expo/expo/pull/35741) by [@kudo](https://github.com/kudo))
 
+### ⚠️ Notices
+
+- Changed async tasks to run on a dedicated parallel queue. ([#35896](https://github.com/expo/expo/pull/35896) by [@kudo](https://github.com/kudo))
+
 ## 15.1.4 - 2025-04-02
 
 ### 🐛 Bug fixes
diff --git a/packages/expo-sqlite/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt b/packages/expo-sqlite/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt
index d48d43184d341a..208a2f8196c3cc 100644
--- a/packages/expo-sqlite/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt
+++ b/packages/expo-sqlite/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt
@@ -9,6 +9,8 @@ import androidx.core.os.bundleOf
 import expo.modules.kotlin.exception.Exceptions
 import expo.modules.kotlin.modules.Module
 import expo.modules.kotlin.modules.ModuleDefinition
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import java.io.File
 import java.io.IOException
 
@@ -22,6 +24,8 @@ class SQLiteModule : Module() {
   private val context: Context
     get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
 
+  private val moduleCoroutineScope = CoroutineScope(Dispatchers.IO)
+
   override fun definition() = ModuleDefinition {
     Name("ExpoSQLite")
 
@@ -52,7 +56,7 @@ class SQLiteModule : Module() {
 
     AsyncFunction("deleteDatabaseAsync") { databasePath: String ->
       deleteDatabase(databasePath)
-    }
+    }.runOnQueue(moduleCoroutineScope)
     Function("deleteDatabaseSync") { databasePath: String ->
       deleteDatabase(databasePath)
     }
@@ -67,29 +71,22 @@ class SQLiteModule : Module() {
         throw OpenDatabaseException(assetDatabasePath)
       }
       assetFile.copyTo(dbFile, forceOverwrite)
-    }
+    }.runOnQueue(moduleCoroutineScope)
 
     AsyncFunction("ensureDatabasePathExistsAsync") { databasePath: String ->
       ensureDatabasePathExists(databasePath)
-    }
+    }.runOnQueue(moduleCoroutineScope)
     Function("ensureDatabasePathExistsSync") { databasePath: String ->
       ensureDatabasePathExists(databasePath)
     }
 
     AsyncFunction("backupDatabaseAsync") { destDatabase: NativeDatabase, destDatabaseName: String, sourceDatabase: NativeDatabase, sourceDatabaseName: String ->
       backupDatabase(destDatabase, destDatabaseName, sourceDatabase, sourceDatabaseName)
-    }
+    }.runOnQueue(moduleCoroutineScope)
     Function("backupDatabaseSync") { destDatabase: NativeDatabase, destDatabaseName: String, sourceDatabase: NativeDatabase, sourceDatabaseName: String ->
       backupDatabase(destDatabase, destDatabaseName, sourceDatabase, sourceDatabaseName)
     }
 
-    AsyncFunction("deleteDatabaseAsync") { databasePath: String ->
-      deleteDatabase(databasePath)
-    }
-    Function("deleteDatabaseSync") { databasePath: String ->
-      deleteDatabase(databasePath)
-    }
-
     // region NativeDatabase
 
     Class(NativeDatabase::class) {
@@ -127,7 +124,7 @@ class SQLiteModule : Module() {
 
       AsyncFunction("initAsync") { database: NativeDatabase ->
         initDb(database)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("initSync") { database: NativeDatabase ->
         initDb(database)
       }
@@ -135,7 +132,7 @@ class SQLiteModule : Module() {
       AsyncFunction("isInTransactionAsync") { database: NativeDatabase ->
         maybeThrowForClosedDatabase(database)
         return@AsyncFunction database.ref.sqlite3_get_autocommit() == 0
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("isInTransactionSync") { database: NativeDatabase ->
         maybeThrowForClosedDatabase(database)
         return@Function database.ref.sqlite3_get_autocommit() == 0
@@ -147,7 +144,7 @@ class SQLiteModule : Module() {
         if (db != null) {
           closeDatabase(db)
         }
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("closeSync") { database: NativeDatabase ->
         maybeThrowForClosedDatabase(database)
         val db = removeCachedDatabase(database)
@@ -158,28 +155,28 @@ class SQLiteModule : Module() {
 
       AsyncFunction("execAsync") { database: NativeDatabase, source: String ->
         exec(database, source)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("execSync") { database: NativeDatabase, source: String ->
         exec(database, source)
       }
 
       AsyncFunction("serializeAsync") { database: NativeDatabase, databaseName: String ->
         return@AsyncFunction serialize(database, databaseName)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("serializeSync") { database: NativeDatabase, databaseName: String ->
         return@Function serialize(database, databaseName)
       }
 
       AsyncFunction("prepareAsync") { database: NativeDatabase, statement: NativeStatement, source: String ->
         prepareStatement(database, statement, source)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("prepareSync") { database: NativeDatabase, statement: NativeStatement, source: String ->
         prepareStatement(database, statement, source)
       }
 
       AsyncFunction("createSessionAsync") { database: NativeDatabase, session: NativeSession, dbName: String ->
         sessionCreate(database, session, dbName)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("createSessionSync") { database: NativeDatabase, session: NativeSession, dbName: String ->
         sessionCreate(database, session, dbName)
       }
@@ -189,7 +186,7 @@ class SQLiteModule : Module() {
         if (database.ref.libsql_sync() != NativeDatabaseBinding.SQLITE_OK) {
           throw SQLiteErrorException(database.ref.convertSqlLiteErrorToString())
         }
-      }
+      }.runOnQueue(moduleCoroutineScope)
     }
 
     // endregion NativeDatabase
@@ -203,28 +200,28 @@ class SQLiteModule : Module() {
 
       AsyncFunction("runAsync") { statement: NativeStatement, database: NativeDatabase, bindParams: Map<String, Any>, bindBlobParams: Map<String, ByteArray>, shouldPassAsArray: Boolean ->
         return@AsyncFunction run(statement, database, bindParams, bindBlobParams, shouldPassAsArray)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("runSync") { statement: NativeStatement, database: NativeDatabase, bindParams: Map<String, Any>, bindBlobParams: Map<String, ByteArray>, shouldPassAsArray: Boolean ->
         return@Function run(statement, database, bindParams, bindBlobParams, shouldPassAsArray)
       }
 
       AsyncFunction("stepAsync") { statement: NativeStatement, database: NativeDatabase ->
         return@AsyncFunction step(statement, database)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("stepSync") { statement: NativeStatement, database: NativeDatabase ->
         return@Function step(statement, database)
       }
 
       AsyncFunction("getAllAsync") { statement: NativeStatement, database: NativeDatabase ->
         return@AsyncFunction getAll(statement, database)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("getAllSync") { statement: NativeStatement, database: NativeDatabase ->
         return@Function getAll(statement, database)
       }
 
       AsyncFunction("resetAsync") { statement: NativeStatement, database: NativeDatabase ->
         return@AsyncFunction reset(statement, database)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("resetSync") { statement: NativeStatement, database: NativeDatabase ->
         return@Function reset(statement, database)
       }
@@ -232,7 +229,7 @@ class SQLiteModule : Module() {
       AsyncFunction("getColumnNamesAsync") { statement: NativeStatement ->
         maybeThrowForFinalizedStatement(statement)
         return@AsyncFunction statement.ref.getColumnNames()
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("getColumnNamesSync") { statement: NativeStatement ->
         maybeThrowForFinalizedStatement(statement)
         return@Function statement.ref.getColumnNames()
@@ -240,7 +237,7 @@ class SQLiteModule : Module() {
 
       AsyncFunction("finalizeAsync") { statement: NativeStatement, database: NativeDatabase ->
         return@AsyncFunction finalize(statement, database)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("finalizeSync") { statement: NativeStatement, database: NativeDatabase ->
         return@Function finalize(statement, database)
       }
@@ -257,49 +254,49 @@ class SQLiteModule : Module() {
 
       AsyncFunction("attachAsync") { session: NativeSession, database: NativeDatabase, table: String? ->
         sessionAttach(database, session, table)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("attachSync") { session: NativeSession, database: NativeDatabase, table: String? ->
         sessionAttach(database, session, table)
       }
 
       AsyncFunction("enableAsync") { session: NativeSession, database: NativeDatabase, enabled: Boolean ->
         sessionEnable(database, session, enabled)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("enableSync") { session: NativeSession, database: NativeDatabase, enabled: Boolean ->
         sessionEnable(database, session, enabled)
       }
 
       AsyncFunction("closeAsync") { session: NativeSession, database: NativeDatabase ->
         sessionClose(database, session)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("closeSync") { session: NativeSession, database: NativeDatabase ->
         sessionClose(database, session)
       }
 
       AsyncFunction("createChangesetAsync") { session: NativeSession, database: NativeDatabase ->
         return@AsyncFunction sessionCreateChangeset(database, session)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("createChangesetSync") { session: NativeSession, database: NativeDatabase ->
         return@Function sessionCreateChangeset(database, session)
       }
 
       AsyncFunction("createInvertedChangesetAsync") { session: NativeSession, database: NativeDatabase ->
         return@AsyncFunction sessionCreateInvertedChangeset(database, session)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("createInvertedChangesetSync") { session: NativeSession, database: NativeDatabase ->
         return@Function sessionCreateInvertedChangeset(database, session)
       }
 
       AsyncFunction("applyChangesetAsync") { session: NativeSession, database: NativeDatabase, changeset: ByteArray ->
         sessionApplyChangeset(database, session, changeset)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("applyChangesetSync") { session: NativeSession, database: NativeDatabase, changeset: ByteArray ->
         sessionApplyChangeset(database, session, changeset)
       }
 
       AsyncFunction("invertChangesetAsync") { session: NativeSession, database: NativeDatabase, changeset: ByteArray ->
         return@AsyncFunction sessionInvertChangeset(database, session, changeset)
-      }
+      }.runOnQueue(moduleCoroutineScope)
       Function("invertChangesetSync") { session: NativeSession, database: NativeDatabase, changeset: ByteArray ->
         return@Function sessionInvertChangeset(database, session, changeset)
       }
diff --git a/packages/expo-sqlite/ios/SQLiteModule.swift b/packages/expo-sqlite/ios/SQLiteModule.swift
index 9eb9b3c7099a21..c087c05a8341eb 100644
--- a/packages/expo-sqlite/ios/SQLiteModule.swift
+++ b/packages/expo-sqlite/ios/SQLiteModule.swift
@@ -7,6 +7,8 @@ private typealias SQLiteColumnValues = [Any]
 private let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self)
 private let MEMORY_DB_NAME = ":memory:"
 
+private let moduleQueue = DispatchQueue(label: "expo.module.sqlite.AsyncQueue", qos: .userInitiated, attributes: .concurrent)
+
 public final class SQLiteModule: Module {
   // Store unmanaged (SQLiteModule, Database) pairs for sqlite callbacks,
   // will release the pair when `closeDatabase` is called.
@@ -47,7 +49,7 @@ public final class SQLiteModule: Module {
 
     AsyncFunction("deleteDatabaseAsync") { (databasePath: String) in
       try deleteDatabase(databasePath: databasePath)
-    }
+    }.runOnQueue(moduleQueue)
     Function("deleteDatabaseSync") { (databasePath: String) in
       try deleteDatabase(databasePath: databasePath)
     }
@@ -64,11 +66,11 @@ public final class SQLiteModule: Module {
       }
       try? fileManager.removeItem(atPath: path.absoluteString)
       try fileManager.copyItem(atPath: assetPath, toPath: path.standardizedFileURL.path)
-    }
+    }.runOnQueue(moduleQueue)
 
     AsyncFunction("ensureDatabasePathExistsAsync") { (databasePath: String) in
       try ensureDatabasePathExists(path: databasePath)
-    }
+    }.runOnQueue(moduleQueue)
     Function("ensureDatabasePathExistsSync") { (databasePath: String) in
       try ensureDatabasePathExists(path: databasePath)
     }
@@ -76,7 +78,7 @@ public final class SQLiteModule: Module {
     // swiftlint:disable:next line_length
     AsyncFunction("backupDatabaseAsync") { (destDatabase: NativeDatabase, destDatabaseName: String, sourceDatabase: NativeDatabase, sourceDatabaseName: String) in
       try backupDatabase(destDatabase: destDatabase, destDatabaseName: destDatabaseName, sourceDatabase: sourceDatabase, sourceDatabaseName: sourceDatabaseName)
-    }
+    }.runOnQueue(moduleQueue)
     Function("backupDatabaseSync") { (destDatabase: NativeDatabase, destDatabaseName: String, sourceDatabase: NativeDatabase, sourceDatabaseName: String) in
       try backupDatabase(destDatabase: destDatabase, destDatabaseName: destDatabaseName, sourceDatabase: sourceDatabase, sourceDatabaseName: sourceDatabaseName)
     }
@@ -110,7 +112,7 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("initAsync") { (database: NativeDatabase) in
         try initDb(database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("initSync") { (database: NativeDatabase) in
         try initDb(database: database)
       }
@@ -118,7 +120,7 @@ public final class SQLiteModule: Module {
       AsyncFunction("isInTransactionAsync") { (database: NativeDatabase) -> Bool in
         try maybeThrowForClosedDatabase(database)
         return exsqlite3_get_autocommit(database.pointer) == 0
-      }
+      }.runOnQueue(moduleQueue)
       Function("isInTransactionSync") { (database: NativeDatabase) -> Bool in
         try maybeThrowForClosedDatabase(database)
         return exsqlite3_get_autocommit(database.pointer) == 0
@@ -129,7 +131,7 @@ public final class SQLiteModule: Module {
         if let db = removeCachedDatabase(of: database) {
           try closeDatabase(db)
         }
-      }
+      }.runOnQueue(moduleQueue)
       Function("closeSync") { (database: NativeDatabase) in
         try maybeThrowForClosedDatabase(database)
         if let db = removeCachedDatabase(of: database) {
@@ -139,28 +141,28 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("execAsync") { (database: NativeDatabase, source: String) in
         try exec(database: database, source: source)
-      }
+      }.runOnQueue(moduleQueue)
       Function("execSync") { (database: NativeDatabase, source: String) in
         try exec(database: database, source: source)
       }
 
       AsyncFunction("serializeAsync") { (database: NativeDatabase, databaseName: String) in
         try serialize(database: database, databaseName: databaseName)
-      }
+      }.runOnQueue(moduleQueue)
       Function("serializeSync") { (database: NativeDatabase, databaseName: String) in
         try serialize(database: database, databaseName: databaseName)
       }
 
       AsyncFunction("prepareAsync") { (database: NativeDatabase, statement: NativeStatement, source: String) in
         try prepareStatement(database: database, statement: statement, source: source)
-      }
+      }.runOnQueue(moduleQueue)
       Function("prepareSync") { (database: NativeDatabase, statement: NativeStatement, source: String) in
         try prepareStatement(database: database, statement: statement, source: source)
       }
 
       AsyncFunction("createSessionAsync") { (database: NativeDatabase, session: NativeSession, dbName: String) in
         try sessionCreate(database: database, session: session, dbName: dbName)
-      }
+      }.runOnQueue(moduleQueue)
       Function("createSessionSync") { (database: NativeDatabase, session: NativeSession, dbName: String) in
         try sessionCreate(database: database, session: session, dbName: dbName)
       }
@@ -178,7 +180,7 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("runAsync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: Data], shouldPassAsArray: Bool) -> [String: Any] in
         return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
-      }
+      }.runOnQueue(moduleQueue)
       Function("runSync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: Data], shouldPassAsArray: Bool) -> [String: Any] in
         return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
       }
@@ -187,35 +189,35 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("stepAsync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
         return try step(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("stepSync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
         return try step(statement: statement, database: database)
       }
 
       AsyncFunction("getAllAsync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
         return try getAll(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("getAllSync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
         return try getAll(statement: statement, database: database)
       }
 
       AsyncFunction("resetAsync") { (statement: NativeStatement, database: NativeDatabase) in
         try reset(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("resetSync") { (statement: NativeStatement, database: NativeDatabase) in
         try reset(statement: statement, database: database)
       }
 
       AsyncFunction("getColumnNamesAsync") { (statement: NativeStatement) -> SQLiteColumnNames in
         return try getColumnNames(statement: statement)
-      }
+      }.runOnQueue(moduleQueue)
       Function("getColumnNamesSync") { (statement: NativeStatement) -> SQLiteColumnNames in
         return try getColumnNames(statement: statement)
       }
 
       AsyncFunction("finalizeAsync") { (statement: NativeStatement, database: NativeDatabase) in
         try finalize(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("finalizeSync") { (statement: NativeStatement, database: NativeDatabase) in
         try finalize(statement: statement, database: database)
       }
@@ -231,49 +233,49 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("attachAsync") { (session: NativeSession, database: NativeDatabase, table: String?) in
         try sessionAttach(database: database, session: session, table: table)
-      }
+      }.runOnQueue(moduleQueue)
       Function("attachSync") { (session: NativeSession, database: NativeDatabase, table: String?) in
         try sessionAttach(database: database, session: session, table: table)
       }
 
       AsyncFunction("enableAsync") { (session: NativeSession, database: NativeDatabase, enabled: Bool) in
         try sessionEnable(database: database, session: session, enabled: enabled)
-      }
+      }.runOnQueue(moduleQueue)
       Function("enableSync") { (session: NativeSession, database: NativeDatabase, enabled: Bool) in
         try sessionEnable(database: database, session: session, enabled: enabled)
       }
 
       AsyncFunction("closeAsync") { (session: NativeSession, database: NativeDatabase) in
         try sessionClose(database: database, session: session)
-      }
+      }.runOnQueue(moduleQueue)
       Function("closeSync") { (session: NativeSession, database: NativeDatabase) in
         try sessionClose(database: database, session: session)
       }
 
       AsyncFunction("createChangesetAsync") { (session: NativeSession, database: NativeDatabase) -> Data in
         return try sessionCreateChangeset(database: database, session: session)
-      }
+      }.runOnQueue(moduleQueue)
       Function("createChangesetSync") { (session: NativeSession, database: NativeDatabase) -> Data in
         return try sessionCreateChangeset(database: database, session: session)
       }
 
       AsyncFunction("createInvertedChangesetAsync") { (session: NativeSession, database: NativeDatabase) -> Data in
         return try sessionCreateInvertedChangeset(database: database, session: session)
-      }
+      }.runOnQueue(moduleQueue)
       Function("createInvertedChangesetSync") { (session: NativeSession, database: NativeDatabase) -> Data in
         return try sessionCreateInvertedChangeset(database: database, session: session)
       }
 
       AsyncFunction("applyChangesetAsync") { (session: NativeSession, database: NativeDatabase, changeset: Data) in
         try sessionApplyChangeset(database: database, session: session, changeset: changeset)
-      }
+      }.runOnQueue(moduleQueue)
       Function("applyChangesetSync") { (session: NativeSession, database: NativeDatabase, changeset: Data) in
         try sessionApplyChangeset(database: database, session: session, changeset: changeset)
       }
 
       AsyncFunction("invertChangesetAsync") { (session: NativeSession, database: NativeDatabase, changeset: Data) -> Data in
         return try sessionInvertChangeset(database: database, session: session, changeset: changeset)
-      }
+      }.runOnQueue(moduleQueue)
       Function("invertChangesetSync") { (session: NativeSession, database: NativeDatabase, changeset: Data) -> Data in
         return try sessionInvertChangeset(database: database, session: session, changeset: changeset)
       }
diff --git a/packages/expo-sqlite/ios/SQLiteModuleLibSQL.swift b/packages/expo-sqlite/ios/SQLiteModuleLibSQL.swift
index 8b23846adfaaf4..b0b975b20181b0 100644
--- a/packages/expo-sqlite/ios/SQLiteModuleLibSQL.swift
+++ b/packages/expo-sqlite/ios/SQLiteModuleLibSQL.swift
@@ -7,6 +7,8 @@ private typealias SQLiteColumnNames = [String]
 private typealias SQLiteColumnValues = [Any]
 private let MEMORY_DB_NAME = ":memory:"
 
+private let moduleQueue = DispatchQueue(label: "expo.module.sqlite.AsyncQueue", qos: .userInitiated, attributes: .concurrent)
+
 public final class SQLiteModule: Module {
   // Store unmanaged (SQLiteModule, Database) pairs for sqlite callbacks,
   // will release the pair when `closeDatabase` is called.
@@ -48,7 +50,7 @@ public final class SQLiteModule: Module {
 
     AsyncFunction("deleteDatabaseAsync") { (databasePath: String) in
       try deleteDatabase(databasePath: databasePath)
-    }
+    }.runOnQueue(moduleQueue)
     Function("deleteDatabaseSync") { (databasePath: String) in
       try deleteDatabase(databasePath: databasePath)
     }
@@ -65,18 +67,18 @@ public final class SQLiteModule: Module {
       }
       try? fileManager.removeItem(atPath: path.absoluteString)
       try fileManager.copyItem(atPath: assetPath, toPath: path.standardizedFileURL.path)
-    }
+    }.runOnQueue(moduleQueue)
 
     AsyncFunction("ensureDatabasePathExistsAsync") { (databasePath: String) in
       try ensureDatabasePathExists(path: databasePath)
-    }
+    }.runOnQueue(moduleQueue)
     Function("ensureDatabasePathExistsSync") { (databasePath: String) in
       try ensureDatabasePathExists(path: databasePath)
     }
 
     AsyncFunction("backupDatabaseAsync") { (_: NativeDatabase, _: String, _: NativeDatabase, _: String) in
       throw UnsupportedOperationException()
-    }
+    }.runOnQueue(moduleQueue)
     Function("backupDatabaseSync") { (_: NativeDatabase, _: String, _: NativeDatabase, _: String) in
       throw UnsupportedOperationException()
     }
@@ -146,14 +148,14 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("initAsync") { (database: NativeDatabase) in
         try initDb(database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("initSync") { (database: NativeDatabase) in
         try initDb(database: database)
       }
 
       AsyncFunction("isInTransactionAsync") { (_: NativeDatabase) -> Bool in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("isInTransactionSync") { (_: NativeDatabase) -> Bool in
         throw UnsupportedOperationException()
       }
@@ -163,7 +165,7 @@ public final class SQLiteModule: Module {
         if let db = removeCachedDatabase(of: database) {
           try closeDatabase(db)
         }
-      }
+      }.runOnQueue(moduleQueue)
       Function("closeSync") { (database: NativeDatabase) in
         try maybeThrowForClosedDatabase(database)
         if let db = removeCachedDatabase(of: database) {
@@ -173,28 +175,28 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("execAsync") { (database: NativeDatabase, source: String) in
         try exec(database: database, source: source)
-      }
+      }.runOnQueue(moduleQueue)
       Function("execSync") { (database: NativeDatabase, source: String) in
         try exec(database: database, source: source)
       }
 
       AsyncFunction("serializeAsync") { (database: NativeDatabase, databaseName: String) in
         try serialize(database: database, databaseName: databaseName)
-      }
+      }.runOnQueue(moduleQueue)
       Function("serializeSync") { (database: NativeDatabase, databaseName: String) in
         try serialize(database: database, databaseName: databaseName)
       }
 
       AsyncFunction("prepareAsync") { (database: NativeDatabase, statement: NativeStatement, source: String) in
         try prepareStatement(database: database, statement: statement, source: source)
-      }
+      }.runOnQueue(moduleQueue)
       Function("prepareSync") { (database: NativeDatabase, statement: NativeStatement, source: String) in
         try prepareStatement(database: database, statement: statement, source: source)
       }
 
       AsyncFunction("createSessionAsync") { (_: NativeDatabase, _: NativeSession, _: String) in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("createSessionSync") { (_: NativeDatabase, _: NativeSession, _: String) in
         throw UnsupportedOperationException()
       }
@@ -205,7 +207,7 @@ public final class SQLiteModule: Module {
           let err = convertLibSqlErrorToString(errMsg)
           throw SQLiteErrorException(convertLibSqlErrorToString(errMsg))
         }
-      }
+      }.runOnQueue(moduleQueue)
     }
 
     // MARK: - NativeStatement
@@ -220,7 +222,7 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("runAsync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: Data], shouldPassAsArray: Bool) -> [String: Any] in
         return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
-      }
+      }.runOnQueue(moduleQueue)
       Function("runSync") { (statement: NativeStatement, database: NativeDatabase, bindParams: [String: Any], bindBlobParams: [String: Data], shouldPassAsArray: Bool) -> [String: Any] in
         return try run(statement: statement, database: database, bindParams: bindParams, bindBlobParams: bindBlobParams, shouldPassAsArray: shouldPassAsArray)
       }
@@ -229,35 +231,35 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("stepAsync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
         return try step(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("stepSync") { (statement: NativeStatement, database: NativeDatabase) -> SQLiteColumnValues? in
         return try step(statement: statement, database: database)
       }
 
       AsyncFunction("getAllAsync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
         return try getAll(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("getAllSync") { (statement: NativeStatement, database: NativeDatabase) -> [SQLiteColumnValues] in
         return try getAll(statement: statement, database: database)
       }
 
       AsyncFunction("resetAsync") { (statement: NativeStatement, database: NativeDatabase) in
         try reset(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("resetSync") { (statement: NativeStatement, database: NativeDatabase) in
         try reset(statement: statement, database: database)
       }
 
       AsyncFunction("getColumnNamesAsync") { (statement: NativeStatement) -> SQLiteColumnNames in
         return try getColumnNames(statement: statement)
-      }
+      }.runOnQueue(moduleQueue)
       Function("getColumnNamesSync") { (statement: NativeStatement) -> SQLiteColumnNames in
         return try getColumnNames(statement: statement)
       }
 
       AsyncFunction("finalizeAsync") { (statement: NativeStatement, database: NativeDatabase) in
         try finalize(statement: statement, database: database)
-      }
+      }.runOnQueue(moduleQueue)
       Function("finalizeSync") { (statement: NativeStatement, database: NativeDatabase) in
         try finalize(statement: statement, database: database)
       }
@@ -273,49 +275,49 @@ public final class SQLiteModule: Module {
 
       AsyncFunction("attachAsync") { (_: NativeSession, _: NativeDatabase, _: String?) in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("attachSync") { (_: NativeSession, _: NativeDatabase, _: String?) in
         throw UnsupportedOperationException()
       }
 
       AsyncFunction("enableAsync") { (_: NativeSession, _: NativeDatabase, _: Bool) in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("enableSync") { (_: NativeSession, _: NativeDatabase, _: Bool) in
         throw UnsupportedOperationException()
       }
 
       AsyncFunction("closeAsync") { (_: NativeSession, _: NativeDatabase) in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("closeSync") { (_: NativeSession, _: NativeDatabase) in
         throw UnsupportedOperationException()
       }
 
       AsyncFunction("createChangesetAsync") { (_: NativeSession, _: NativeDatabase) -> Data in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("createChangesetSync") { (_: NativeSession, _: NativeDatabase) -> Data in
         throw UnsupportedOperationException()
       }
 
       AsyncFunction("createInvertedChangesetAsync") { (_: NativeSession, _: NativeDatabase) -> Data in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("createInvertedChangesetSync") { (_: NativeSession, _: NativeDatabase) -> Data in
         throw UnsupportedOperationException()
       }
 
       AsyncFunction("applyChangesetAsync") { (_: NativeSession, _: NativeDatabase, _: Data) in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("applyChangesetSync") { (_: NativeSession, _: NativeDatabase, _: Data) in
         throw UnsupportedOperationException()
       }
 
       AsyncFunction("invertChangesetAsync") { (_: NativeSession, _: NativeDatabase, _: Data) -> Data in
         throw UnsupportedOperationException()
-      }
+      }.runOnQueue(moduleQueue)
       Function("invertChangesetSync") { (_: NativeSession, _: NativeDatabase, _: Data) -> Data in
         throw UnsupportedOperationException()
       }


Reviews:
user: login: Kudo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T07:59:02Z

Review comments:
diff_hunk: @@ -247,8 +247,8 @@ CREATE TABLE IF NOT EXISTS nulling (id INTEGER PRIMARY KEY NOT NULL, x NUMERIC,
 `);
       await db.runAsync('INSERT INTO nulling (x, y) VALUES (?, ?)', [null, null]);
       const statement = await db.prepareAsync('INSERT INTO nulling (x, y) VALUES (?, ?)');
-      statement.executeAsync(null, null);
-      statement.finalizeAsync();
+      await statement.executeAsync(null, null);
+      await statement.finalizeAsync();
path: apps/test-suite/tests/SQLite.ts
user: login: Kudo
body: should add await here because now the native module doesn't run tasks in a serial queue. this fixes the test to run in correct state
created_at: 2025-04-04T07:57:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,29 +71,22 @@ class SQLiteModule : Module() {
         throw OpenDatabaseException(assetDatabasePath)
       }
       assetFile.copyTo(dbFile, forceOverwrite)
-    }
+    }.runOnQueue(moduleCoroutineScope)
 
     AsyncFunction("ensureDatabasePathExistsAsync") { databasePath: String ->
       ensureDatabasePathExists(databasePath)
-    }
+    }.runOnQueue(moduleCoroutineScope)
     Function("ensureDatabasePathExistsSync") { databasePath: String ->
       ensureDatabasePathExists(databasePath)
     }
 
     AsyncFunction("backupDatabaseAsync") { destDatabase: NativeDatabase, destDatabaseName: String, sourceDatabase: NativeDatabase, sourceDatabaseName: String ->
       backupDatabase(destDatabase, destDatabaseName, sourceDatabase, sourceDatabaseName)
-    }
+    }.runOnQueue(moduleCoroutineScope)
     Function("backupDatabaseSync") { destDatabase: NativeDatabase, destDatabaseName: String, sourceDatabase: NativeDatabase, sourceDatabaseName: String ->
       backupDatabase(destDatabase, destDatabaseName, sourceDatabase, sourceDatabaseName)
     }
 
-    AsyncFunction("deleteDatabaseAsync") { databasePath: String ->
-      deleteDatabase(databasePath)
-    }
-    Function("deleteDatabaseSync") { databasePath: String ->
-      deleteDatabase(databasePath)
-    }
-
path: packages/expo-sqlite/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt
user: login: Kudo
body: redundant definitions added from #35604
created_at: 2025-04-04T07:58:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-04T07:54:52Z
body: <p><a href="https://linear.app/expo/issue/ENG-15356/expo-sqlite-not-using-dispatcherio-can-cause-blocking-the-main-thread">ENG-15356 Expo SQLite not using Dispatcher.IO can cause blocking the main thread</a></p>
author_association: NONE
user: login: expo-bot
created_at: 2025-04-04T07:56:20Z
body: <!-- pr-labeler comment -->
The Pull Request introduced fingerprint changes against the base commit: 981c3fd69f40078100266e82dd41139b0a5dbae7
<details><summary>Fingerprint diff</summary>

```json
[
  {
    "op": "changed",
    "beforeSource": {
      "type": "dir",
      "filePath": "../../packages/expo-sqlite/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "9be7b31876d9a38283f724f063ae29efe8cc9af6"
    },
    "afterSource": {
      "type": "dir",
      "filePath": "../../packages/expo-sqlite/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "a92673101771d0b5b08a803b81e9416fb2c8ccd3"
    }
  },
  {
    "op": "changed",
    "beforeSource": {
      "type": "dir",
      "filePath": "../../packages/expo-sqlite/ios",
      "reasons": [
        "expoAutolinkingIos"
      ],
      "hash": "bd69649cde0c9b556ea386cf25a45251acf6be99"
    },
    "afterSource": {
      "type": "dir",
      "filePath": "../../packages/expo-sqlite/ios",
      "reasons": [
        "expoAutolinkingIos"
      ],
      "hash": "eda9b2f823f1988ccf5b8a43d634416be44cdbe0"
    }
  }
]
```

</details>

---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*

author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-04-04T09:00:16Z
body: Subscribed to pull request
| File Patterns | Mentions |
| - | - |
| packages/expo-sqlite/\*\* | @kudo, @alanjhughes |

Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)
<!-- codemention header -->
author_association: CONTRIBUTOR