Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions skills/react-native-nitro-sqlite/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
name: react-native-nitro-sqlite
description: Fast embedded SQLite for React Native via react-native-nitro-sqlite (Nitro Modules / JSI, the successor to react-native-quick-sqlite). Covers installation and native config, opening connections, sync vs async queries, the QueryResult shape (results / rows / rowsAffected / insertId / metadata), parameter binding, transactions, batch execution, loading SQL files, attach/detach across databases, the per-database operation queue (and why sync calls throw when the DB is busy), TypeORM integration, error handling with NitroSQLiteError, blobs/ArrayBuffer, FTS5/Geopoly compile flags, system-SQLite-on-iOS, app groups, and migrating from react-native-quick-sqlite.
license: MIT
metadata:
author: margelo
scope: react-native-nitro-sqlite
tags: react-native, sqlite, database, nitro-modules, jsi, async, transactions, batch, typeorm, fts5, blob, quick-sqlite, migration
---

# react-native-nitro-sqlite

A focused reference for AI coding assistants working in a project that uses `react-native-nitro-sqlite`. Answer using the **real APIs** from this library — not invented ones — by routing to the matching `references/*.md` file below.

> This library is the Nitro Modules successor to `react-native-quick-sqlite`. From `9.0.0` on, the package is `react-native-nitro-sqlite`. Current reference version for this skill: **9.6.0**.

## Mental model

`react-native-nitro-sqlite` embeds the SQLite C engine and exposes it to JS over [Nitro Modules](https://nitro.margelo.com/) (JSI) — no bridge, no serialization, direct synchronous access. It is **not** a key-value store; it is real SQL. (For key-value storage use [`react-native-mmkv`](https://github.com/mrousavy/react-native-mmkv) instead.)

The five things that trip people up:

1. **Every operation comes in sync and async form.** `db.execute(...)` runs on the JS thread and returns immediately. `db.executeAsync(...)` runs the SQL on a separate native thread and returns a `Promise`. Prefer **async** for anything non-trivial so you don't block the UI.
2. **A per-database operation queue serializes only `executeBatch`, `executeBatchAsync`, and `transaction`.** `executeBatchAsync` and `transaction` are queued and run **serially**; synchronous `executeBatch` **throws** if the database is busy with a queued/in-progress async batch or transaction: `"Database <name> is busy with another operation."` Plain `execute`, `executeAsync`, `loadFile`, and `loadFileAsync` **bypass the queue** (call native directly) — they never throw "busy". This is the single biggest behavioral gotcha — see [`references/concurrency.md`](./references/concurrency.md).
3. **The result object has two views of the rows.** `result.results` is a plain `Row[]`. `result.rows` is the TypeORM-style view with `_array`, `length`, and `item(idx)`. Both exist on every `QueryResult`. Plus `rowsAffected`, optional `insertId`, and optional `metadata`.
4. **`transaction` is async-only and auto-manages BEGIN/COMMIT/ROLLBACK.** If the callback resolves, it commits; if it throws, it rolls back. You can also call `tx.commit()` / `tx.rollback()` manually.
5. **You use the JS `open()` wrapper, not the raw native module.** `open({ name })` returns a `NitroSQLiteConnection` bound to that database name, so you never pass the db name into every call. It also registers the database in the operation queue and throws if it's already open.

> **Requires React Native 0.75+ with the New Architecture, plus `react-native-nitro-modules` (>= 0.35.0) as a peer dependency.** (The README mentions 0.71+, but `package.json` pins the peer to RN ≥ 0.75.)

## Routing table — problem to reference

Load the matching file from `references/` before writing code. Each reference cites real APIs verified against the library source.

| User is asking about… | Read |
|---|---|
| Installing, native build config, FTS5/Geopoly flags, system SQLite on iOS, app groups, Expo prebuild, RN/Nitro version requirements | [`references/setup.md`](./references/setup.md) |
| `open()`, `NitroSQLiteConnection`, `location`, opening/loading an existing DB, `close()`, `delete()`, "already open" errors, file locations on iOS/Android | [`references/connections.md`](./references/connections.md) |
| `execute` / `executeAsync`, parameter binding (`?`), the `QueryResult` shape (`results`, `rows._array/length/item`, `rowsAffected`, `insertId`, `metadata`), typed rows, `ColumnType`, storing `null`/blobs | [`references/queries.md`](./references/queries.md) |
| `db.transaction()`, `tx.execute` / `tx.executeAsync` / `tx.commit` / `tx.rollback`, auto commit/rollback, finalized-transaction errors | [`references/transactions.md`](./references/transactions.md) |
| `executeBatch` / `executeBatchAsync`, `BatchQueryCommand`, one query with many param sets, `loadFile` / `loadFileAsync`, `FileLoadResult` | [`references/batch-and-files.md`](./references/batch-and-files.md) |
| Sync vs async, the per-database queue, "Database is busy" errors, ordering guarantees, keeping the UI responsive, `setImmediate` scheduling | [`references/concurrency.md`](./references/concurrency.md) |
| `attach` / `detach`, JOINs across database files, aliases | [`references/attach-detach.md`](./references/attach-detach.md) |
| Using it as a TypeORM driver, `typeORMDriver`, babel alias, `patch-package`, `DataSource` config | [`references/typeorm.md`](./references/typeorm.md) |
| `NitroSQLiteError`, error handling, `instanceof`, migrating from `react-native-quick-sqlite`, breaking changes | [`references/migration-and-errors.md`](./references/migration-and-errors.md) |

If the question doesn't match any row, read [`references/connections.md`](./references/connections.md) first — most usage starts with `open()`.

## Installation

```bash
npm install react-native-nitro-sqlite react-native-nitro-modules
npx pod-install
```

Expo:
```bash
npx expo install react-native-nitro-sqlite react-native-nitro-modules
npx expo prebuild
```

There is **no Expo config plugin** and no manual native linking — Nitro autolinks. Rebuild the app after install. See [`references/setup.md`](./references/setup.md) for compile-time options.

## Quick reference — full API surface

```ts
import { open, NitroSQLiteError, typeORMDriver } from 'react-native-nitro-sqlite'
import type {
NitroSQLiteConnection,
QueryResult,
BatchQueryCommand,
BatchQueryResult,
FileLoadResult,
Transaction,
SQLiteValue,
ColumnType,
} from 'react-native-nitro-sqlite'

// Open — returns a connection bound to this db name
const db = open({ name: 'app.sqlite' }) // { name, location? }

// Query (sync — blocks JS thread)
const r = db.execute('SELECT * FROM users WHERE id = ?', [1])
r.results // Row[] (plain array of row objects)
r.rows._array // Row[] (TypeORM-style view)
r.rows.length // number
r.rows.item(0) // Row | undefined
r.rowsAffected // number
r.insertId // number | undefined (after INSERT)
r.metadata // Record<colName, { name, type: ColumnType, index }> | undefined

// Query (async — runs off the JS thread)
const r2 = await db.executeAsync<{ id: number; name: string }>('SELECT * FROM users')

// Transaction (async only; auto commit / rollback)
await db.transaction(async (tx) => {
tx.execute('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Marc'])
await tx.executeAsync('UPDATE users SET name = ? WHERE id = ?', ['Marc', 1])
// throw → rollback; resolve → commit; or tx.commit() / tx.rollback()
})

// Batch (one transaction, many statements)
const batch: BatchQueryCommand[] = [
{ query: 'CREATE TABLE IF NOT EXISTS t (id INTEGER, age INTEGER)' },
{ query: 'INSERT INTO t (id, age) VALUES (?, ?)', params: [1, 10] },
// one query, many param sets:
{ query: 'INSERT INTO t (id, age) VALUES (?, ?)', params: [[2, 20], [3, 30]] },
]
db.executeBatch(batch) // sync (throws if db busy)
await db.executeBatchAsync(batch) // async (queued)

// Load a .sql file
db.loadFile('/abs/path/dump.sql') // FileLoadResult { rowsAffected?, commands? }
await db.loadFileAsync('/abs/path/dump.sql')

// Attach / detach another database file
db.attach('other.sqlite', 'other', '/dir') // (dbNameToAttach, alias, location?)
db.detach('other')

// Lifecycle
db.close() // close connection (and free the queue)
db.delete() // drop/delete the database file
```

## SQLite value types

`SQLiteValue = boolean | number | string | ArrayBuffer | null`. Bind these as params and read them back from rows. `ArrayBuffer` maps to SQLite BLOB. `ColumnType` enum: `BOOLEAN`, `NUMBER`, `INT64`, `TEXT`, `ARRAY_BUFFER`, `NULL_VALUE`.

## Testing / verifying the skill is loaded

Good test questions:

- *"How do I run many inserts without blocking the UI?"* → should use `executeAsync` or `executeBatchAsync` / `transaction`, and mention the operation queue — **not** a synchronous loop of `db.execute`.
- *"Why does my `db.executeBatch` throw 'Database is busy'?"* → should explain the per-database queue: a sync op can't run while an async op is in flight; use the async variant or `await` the pending op. See [`references/concurrency.md`](./references/concurrency.md).
- *"How do I read the inserted row id and affected rows?"* → `result.insertId` and `result.rowsAffected`, and rows via `result.results` or `result.rows._array`.
- *"How do I wire this into TypeORM?"* → `typeORMDriver` + babel `module-resolver` alias of `react-native-sqlite-storage` + `patch-package` to expose TypeORM's `package.json`.

## References

| File | Description |
|------|-------------|
| [setup.md](./references/setup.md) | Install, native build config, FTS5/Geopoly, system SQLite, app groups, Expo, version requirements |
| [connections.md](./references/connections.md) | `open()`, connection object, locations, existing databases, close/delete |
| [queries.md](./references/queries.md) | `execute`/`executeAsync`, params, result shape, typed rows, metadata, blobs |
| [transactions.md](./references/transactions.md) | `db.transaction`, the `tx` object, auto/manual commit & rollback |
| [batch-and-files.md](./references/batch-and-files.md) | `executeBatch`/`Async`, `BatchQueryCommand`, `loadFile`/`Async` |
| [concurrency.md](./references/concurrency.md) | Sync vs async, the per-database operation queue, "busy" errors, ordering |
| [attach-detach.md](./references/attach-detach.md) | `attach`/`detach`, cross-database JOINs |
| [typeorm.md](./references/typeorm.md) | TypeORM driver setup and configuration |
| [migration-and-errors.md](./references/migration-and-errors.md) | `NitroSQLiteError`, migrating from `react-native-quick-sqlite` |

## Problem → Reference Mapping

| Problem | Reference | Action |
|---------|-----------|--------|
| Don't know where to start | [connections.md](./references/connections.md) | `open({ name })` |
| App freezes during DB work | [concurrency.md](./references/concurrency.md) | Switch to `executeAsync` / `transaction` |
| "Database is busy" error | [concurrency.md](./references/concurrency.md) | Use async variant or `await` pending op |
| Need atomic multi-statement writes | [transactions.md](./references/transactions.md) | `await db.transaction(...)` |
| Bulk insert many rows | [batch-and-files.md](./references/batch-and-files.md) | `executeBatchAsync` with array-of-arrays params |
| Read insert id / affected rows | [queries.md](./references/queries.md) | `result.insertId` / `result.rowsAffected` |
| JOIN across two db files | [attach-detach.md](./references/attach-detach.md) | `db.attach(file, alias)` |
| Use an ORM | [typeorm.md](./references/typeorm.md) | `typeORMDriver` + babel alias |
| Coming from quick-sqlite | [migration-and-errors.md](./references/migration-and-errors.md) | Rename package, update peer deps |
| Need FTS5 / full-text search | [setup.md](./references/setup.md) | Add `SQLITE_ENABLE_FTS5=1` compile flag |
66 changes: 66 additions & 0 deletions skills/react-native-nitro-sqlite/references/attach-detach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
id: attach-detach
title: Attaching and detaching databases
scope: react-native-nitro-sqlite
keywords: attach, detach, alias, cross-database join, multiple databases, ATTACH DATABASE, location
---

# Attaching and detaching databases

## Mental model

SQLite can attach a second database file to an open connection under an **alias**, so you can query both in one statement (e.g. JOIN across files, or copy data between databases). `react-native-nitro-sqlite` exposes this as `attach` / `detach` on the connection.

```ts
const db = open({ name: 'main.sqlite' })

db.attach('stats.sqlite', 'stats', '/optional/dir')
const { results } = db.execute(`
SELECT u.id, u.name, s.score
FROM main.users u
INNER JOIN stats.scores s ON s.user_id = u.id
`)
db.detach('stats')
```

## Signatures

```ts
db.attach(dbNameToAttach: string, alias: string, location?: string): void
db.detach(alias: string): void
```

- `dbNameToAttach` — the file name of the database to attach.
- `alias` — the schema name you reference it by in SQL (`alias.tablename`).
- `location` — optional directory of the file to attach (same semantics as `open`'s `location`; see [connections.md](./connections.md)). Omit to use the default data directory.

Reference tables by alias: the main DB is `main.<table>`, the attached one is `<alias>.<table>`.

## Detaching

Call `detach(alias)` when you no longer need the attached database. Detaching frees the lock/handle. You don't strictly have to detach before closing — **closing the main connection detaches everything** automatically — but detach explicitly if the attach was only needed for a short operation.

## Typical uses

- **Cross-file JOINs** — combine a read-only reference DB with the app's writable DB.
- **Copying / migrating data** — `INSERT INTO main.t SELECT * FROM other.t`.
- **Separating concerns** — keep large/optional datasets in their own file, attach on demand.

```ts
// Copy rows from an attached seed database into the main one
db.attach('seed.sqlite', 'seed')
db.execute('INSERT INTO main.products SELECT * FROM seed.products')
db.detach('seed')
```

## Gotchas

- **`attach`/`detach` are synchronous** (`void`). Wrap heavy cross-db work that follows in `executeAsync`/`transaction` as usual.
- **Alias collisions** — don't reuse an alias that's already attached; detach first.
- **iOS sandbox** applies to the attached file's `location` too — it must be inside the sandbox. Copy external files in first.
- **Attached DBs and transactions** — a single transaction can span main + attached tables since they share the connection.

## Pointers

- Source: `package/src/operations/session.ts` (delegates to native `attach`/`detach`)
- Related: [connections.md](./connections.md), [queries.md](./queries.md)
116 changes: 116 additions & 0 deletions skills/react-native-nitro-sqlite/references/batch-and-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
id: batch-and-files
title: Batch execution and loading SQL files
scope: react-native-nitro-sqlite
keywords: executeBatch, executeBatchAsync, BatchQueryCommand, BatchQueryResult, params array of arrays, bulk insert, loadFile, loadFileAsync, FileLoadResult, dump, seed
---

# Batch execution and loading SQL files

## Batch: many statements in one transaction

`executeBatch` / `executeBatchAsync` run a list of statements inside a **single transaction** — ideal for bulk inserts and seeding. Each command is a `{ query, params? }`.

```ts
import type { BatchQueryCommand } from 'react-native-nitro-sqlite'

const commands: BatchQueryCommand[] = [
{ query: 'CREATE TABLE IF NOT EXISTS t (id INTEGER, age INTEGER)' },
{ query: 'INSERT INTO t (id, age) VALUES (?, ?)', params: [1, 10] },
{ query: 'INSERT INTO t (id, age) VALUES (?, ?)', params: [2, 20] },
]

const { rowsAffected } = db.executeBatch(commands) // sync
// or
const r = await db.executeBatchAsync(commands) // async (queued)
```

### Signatures and types

```ts
interface BatchQueryCommand {
query: string
params?: SQLiteQueryParams | SQLiteQueryParams[] // one set, OR many sets
}

interface BatchQueryResult {
rowsAffected?: number
}

db.executeBatch(commands: BatchQueryCommand[]): BatchQueryResult
db.executeBatchAsync(commands: BatchQueryCommand[]): Promise<BatchQueryResult>
```

### One query, many parameter sets (efficient bulk insert)

When you run the **same** statement many times, declare it **once** and pass `params` as an **array of arrays**. The library executes the prepared statement for each inner array — far cheaper than N separate commands.

```ts
const commands: BatchQueryCommand[] = [
{
query: 'INSERT INTO t (id, age) VALUES (?, ?)',
params: [
[3, 30],
[4, 40],
[5, 50],
],
},
]
await db.executeBatchAsync(commands)
```

So `params` is either:
- a single param set: `[1, 10]`, or
- multiple param sets for the same query: `[[1, 10], [2, 20]]`.

### Atomicity

The whole batch runs in one transaction: if any statement fails, the batch errors (as a `NitroSQLiteError`) and nothing is committed.

## Loading SQL files

`loadFile` / `loadFileAsync` read a `.sql` file from disk and execute every statement in it (e.g. a dump or seed file).

```ts
const r = db.loadFile('/absolute/path/to/dump.sql') // sync
// or
const r2 = await db.loadFileAsync('/absolute/path/to/dump.sql') // async

// FileLoadResult extends BatchQueryResult:
r.rowsAffected // number | undefined — total rows affected
r.commands // number | undefined — how many SQL commands were executed
```

```ts
interface FileLoadResult extends BatchQueryResult {
rowsAffected?: number
commands?: number
}
```

- Use an **absolute path** (subject to the iOS sandbox — copy the file into the app dir first if needed; see [connections.md](./connections.md)).
- Prefer `loadFileAsync` for large files so you don't block the JS thread.

## Sync vs async (important)

- `executeBatch` (sync) goes through the **busy check**: if the database is currently running or has a queued async batch/transaction, it **throws** `Database <name> is busy with another operation.` Use `executeBatchAsync`, or `await` the pending work first.
- `executeBatchAsync` is **queued** and runs serially in submission order (alongside `transaction`).
- `loadFile` / `loadFileAsync` **do not use the queue** — they call the native module directly (like `execute` / `executeAsync`). `loadFile` blocks the JS thread; `loadFileAsync` runs off-thread. Neither throws "busy" and neither is serialized by the JS queue.

See [concurrency.md](./concurrency.md) for the full queue model.

## Choosing batch vs transaction

- **Batch** — fixed list of statements, especially many inserts of the same shape. Simplest and fastest for bulk writes.
- **Transaction** — when you need to read intermediate results, branch logic, or compute later params from earlier rows. See [transactions.md](./transactions.md).

## Gotchas

- **`params` array-of-arrays only makes sense with one `query`** repeated. Don't mix it up with separate command objects unless the queries differ.
- **`BatchQueryResult` has only `rowsAffected`** — no per-statement results. If you need returned rows, use individual `execute`/`executeAsync` calls or a transaction.
- **Sync batch throws when busy** — the most common surprise; switch to async.

## Pointers

- Source: `package/src/operations/executeBatch.ts`, `package/src/operations/session.ts`, `package/src/types.ts`
- Related: [transactions.md](./transactions.md), [concurrency.md](./concurrency.md)
Loading