Skip to content

fix(android): match iOS SQLITE_THREADSAFE=2 and SQLITE_STRICT_SUBTYPE=1#403

Merged
ospfranco merged 1 commit into
OP-Engineering:mainfrom
pbbadenhorst:fix/android-match-ios-threadsafe-strict-subtype
May 15, 2026
Merged

fix(android): match iOS SQLITE_THREADSAFE=2 and SQLITE_STRICT_SUBTYPE=1#403
ospfranco merged 1 commit into
OP-Engineering:mainfrom
pbbadenhorst:fix/android-match-ios-threadsafe-strict-subtype

Conversation

@pbbadenhorst
Copy link
Copy Markdown
Contributor

Summary

Aligns Android's performanceMode compile flags with iOS so the two platforms produce SQLite builds with the same threading and subtype-strictness profile.

iOS (op-sqlite.podspec line 174) sets:

-DSQLITE_STRICT_SUBTYPE=1 -DSQLITE_THREADSAFE=2

Android (this PR) now sets the same. Previously Android set -DSQLITE_THREADSAFE=1 (serialized) and didn't set -DSQLITE_STRICT_SUBTYPE at all.

Why each flag

  • SQLITE_THREADSAFE=2 (multithread): each DBHostObject owns a single-worker thread pool, so a given connection is only ever touched by one thread at a time — serialized mode's internal mutex is wasted overhead. iOS has shipped at =2 for a while; aligning Android picks up the same small perf win and removes a platform discrepancy. Note: this inherits iOS's existing assumption that consumers do not call executeSync() from the JS thread while an async execute() is in flight on the same DB. The library doesn't lock around that today on either platform.
  • SQLITE_STRICT_SUBTYPE=1: enforces subtype safety for SQL function return values. Pure correctness flag with no perf cost. Matches iOS.

Verification

Built the example app on arm64-v8a and grepped the generated compile_commands.json for the sqlite3.c compile line — both flags now present:

... -DSQLITE_STRICT_SUBTYPE=1 ... -DSQLITE_THREADSAFE=2 ...

Then ran ./scripts/test-android.sh against a local Pixel 7 Pro emulator (API 34). Full release APK build, install, in-app @op-engineering/op-test suite run — OPSQLITE_TEST_RESULT:PASS.

Notes

@ospfranco
Copy link
Copy Markdown
Contributor

you have some conflicts, please fix them and I will merge this PR

@pbbadenhorst pbbadenhorst force-pushed the fix/android-match-ios-threadsafe-strict-subtype branch from 1ed26aa to a33e615 Compare May 14, 2026 21:41
@pbbadenhorst
Copy link
Copy Markdown
Contributor Author

What this PR changes

One line, in android/build.gradle inside the if (performanceMode) block of defaultSqliteFlags:

- "-DSQLITE_USE_ALLOCA=1", "-DSQLITE_THREADSAFE=1"
+ "-DSQLITE_USE_ALLOCA=1", "-DSQLITE_STRICT_SUBTYPE=1", "-DSQLITE_THREADSAFE=2"

Two effects: adds -DSQLITE_STRICT_SUBTYPE=1, bumps -DSQLITE_THREADSAFE from 1 to 2.

Where iOS already has these

op-sqlite.podspec#L174:

optimizedCflags = ' -DSQLITE_DQS=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
  -DSQLITE_LIKE_DOESNT_MATCH_BLOBS=1 -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_DEPRECATED=1 \
  -DSQLITE_OMIT_PROGRESS_CALLBACK=1 -DSQLITE_OMIT_SHARED_CACHE=1 -DSQLITE_USE_ALLOCA=1 \
  -DSQLITE_STRICT_SUBTYPE=1 -DSQLITE_THREADSAFE=2'

That string is appended to other_cflags only when performance_mode is true (podspec lines 192–195) — same gating shape as Android's if (performanceMode) { defaultSqliteFlags += [...] }. After this PR, both platforms ship the same set of compile flags when the consumer opts into performanceMode.

What the flags mean

SQLITE_THREADSAFE=2 — pick the default threading model

SQLite has three compile-time threading modes (sqlite.org/threadsafe):

Value Mode Internal locking Caller responsibility
0 Single-thread None Only one thread may touch the whole library
1 Serialized Full mutex on every API call SQLite handles it; multiple threads may share a connection
2 Multi-thread Mutex only on shared globals Each connection used by one thread at a time

The compile-time setting only chooses the default — a connection's actual mode can be overridden when it is opened by passing SQLITE_OPEN_NOMUTEX (multi-thread) or SQLITE_OPEN_FULLMUTEX (serialized) to sqlite3_open_v2().

op-sqlite always opens connections with SQLITE_OPEN_FULLMUTEX (cpp/bridge.cpp#L95-L96):

int flags =
    SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;

So at runtime every connection is serialized regardless of whether the compile-time default is =1 or =2. This PR brings Android's compile-time flags into parity with iOS — it does not materially change SQLite's per-connection mutex behaviour unless SQLITE_OPEN_FULLMUTEX is removed in a separate change. Any real perf win from =2 only lands after that follow-up (which would also need to be reconciled with the executeSync vs async execute paths on the same DB, since SQLite would no longer serialize them for us).

SQLITE_STRICT_SUBTYPE=1 — catch incorrectly registered SQL functions

SQLite values carry an optional "subtype", a small integer tag that SQL functions can attach to their results via sqlite3_result_subtype(). JSON1 / jsonb use this so functions like json() tag their output as already-parsed JSON, letting downstream JSON functions skip re-parsing.

With SQLITE_STRICT_SUBTYPE=1 (sqlite.org/compile.html#strict_subtype), SQLite raises an error if an application-defined SQL function calls sqlite3_result_subtype() without having been registered with the SQLITE_RESULT_SUBTYPE function flag. It catches extension authors who forgot the registration flag — what would otherwise be silent miscommunication between functions becomes a hard error at the boundary.

This is a correctness / debuggability flag. No perf impact, no behaviour change for code that registers its functions correctly. iOS already enables it under performanceMode; this brings Android in line.

Why both flags belong inside performanceMode

performanceMode is the consumer's signal that they accept the perf-leaning trade-offs (deprecated APIs removed, shared cache off, DQS=0, etc.). THREADSAFE=2 slots into that bucket as the compile-time default Android would use if a future change relaxed SQLITE_OPEN_FULLMUTEX. STRICT_SUBTYPE=1 is technically orthogonal (correctness, not perf) but iOS already includes it in optimizedCflags, so keeping the two platforms aligned is the simpler story.

If a consumer wants to opt out, the sqliteFlags override from #402 lets them — e.g. "sqliteFlags": "-DSQLITE_THREADSAFE=1" restores =1 as the compile-time default on Android.

@ospfranco ospfranco merged commit 9fa908a into OP-Engineering:main May 15, 2026
10 checks passed
@pbbadenhorst pbbadenhorst deleted the fix/android-match-ios-threadsafe-strict-subtype branch May 15, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants