FAQ & Troubleshooting
Why does my query throw about a missing index?
select(), update(), delete(), and count() all run assertIndexedColumns() before executing:
every column referenced in .where() (and, for select(), .orderBy()) must be either the
schema's primaryKey or the leading column of a declared indexes entry. This is enforced
synchronously, at call time — not a lint warning.
// throws: "Synchronous execute() requires an index covering column \"email\" as its
// leading column (schema \"users\"). Declare it in schema.indexes, or remove it from
// where()/orderBy()."
Database.select(UserSchema).where(eq('email', 'a@b.com')).execute();
Fix by adding an index whose first column is the one you filter/sort by:
indexes: [{ name: 'idx_users_email', columns: ['email'] }],
A composite index only satisfies the rule for its first column — columns: ['a', 'b'] covers
queries filtering on a, not queries filtering on b alone. See
Schemas for how to declare indexes.
Why did my currentUser() call throw?
currentUser() throws "currentUser(): no user set — call Database.setCurrentUser() first" if no
user id has been registered yet. It deliberately fails loud instead of returning null — a null
would silently compile to WHERE col = NULL, matching zero rows without ever explaining why.
Two common causes:
Database.setCurrentUser(id)was never called. Call it right after your app's own login flow resolves the user id.- The app cold-started and
setCurrentUserwasn't called again. The current-user id lives in memory only — it is not persisted across app restarts (unlike credential tokens, which are stored in Keychain/Keystore). CallDatabase.setCurrentUser(id)again once your app's own session/auth state has rehydrated, before any code path that callscurrentUser().
Database.getCurrentUser() is the non-throwing counterpart — returns the id or null — useful for
gating UI without triggering the throw. See Operators & Types.
Background sync never fires on iOS
Background sync (BGTaskScheduler) requires explicit entitlements in the host app — Salve DB
cannot register them for you, and the native scheduler does not throw or log a runtime error if
they're missing; the job simply never gets scheduled. Check both:
Info.plist — the task identifier used internally, plus the processing background mode:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.salvedb.background.sync</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
*.entitlements — the Keychain access group used by the credential provider (needed for
background token refresh):
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yourapp</string>
</array>
Also confirm Database.configure({ background: { minimumInterval, ... } }) was actually passed —
omitting background leaves the job disabled entirely, again silently. minimumInterval on iOS is
only an earliestBeginDate hint; BGTaskScheduler (not this library) decides the actual wake time,
so an occasional multi-hour delay in the simulator/device is expected behavior, not a bug.
What's the max rows I can insert/select at once?
Insert: Database.insert(schema).values(rows).execute() throws if rows.length exceeds
MAX_BATCH_INSERT_ROWS (500) — enforced in InsertQueryBuilder.execute() before any SQL runs:
InsertQueryBuilder: 501 rows exceeds MAX_BATCH_INSERT_ROWS (500). Split into smaller
batches, wrapped in Database.transaction() if they must be atomic.
Split larger batches into chunks of ≤500, wrapping them in Database.transaction() if they need to
commit atomically as a group.
Select: .limit(n) on a select builder defaults to MAX_SYNC_PAGE_SIZE (500) when omitted, and
execute() throws if n exceeds it:
execute() limit (600) exceeds MAX_SYNC_PAGE_SIZE (500).
For more than 500 rows, page through results with .limit()/.offset(), or use
useInfiniteQuery, which manages paging for you.
Can I use DROP/RENAME in migrations?
No. Database.register() only ever runs ADD COLUMN when a schema's version increases — there
is no migration file to write and no way to drop or rename a column or table through this library.
Renaming a column means adding a new column and migrating data yourself (e.g. in application code,
or a one-off Database.execute() raw-SQL pass); the old column stays in the table until you handle
it explicitly. See Schemas for how version and columns drive migrations.