From 12dd820b7a9d63ad53a3bc30f2bee4d92497392d Mon Sep 17 00:00:00 2001 From: riteshshukla04 Date: Sun, 31 May 2026 21:53:38 +0530 Subject: [PATCH] chore: nitro-sqlite --- skills/react-native-nitro-sqlite/SKILL.md | 165 ++++++++++++++++++ .../references/attach-detach.md | 66 +++++++ .../references/batch-and-files.md | 116 ++++++++++++ .../references/concurrency.md | 107 ++++++++++++ .../references/connections.md | 115 ++++++++++++ .../references/migration-and-errors.md | 98 +++++++++++ .../references/queries.md | 146 ++++++++++++++++ .../references/setup.md | 94 ++++++++++ .../references/transactions.md | 131 ++++++++++++++ .../references/typeorm.md | 118 +++++++++++++ 10 files changed, 1156 insertions(+) create mode 100644 skills/react-native-nitro-sqlite/SKILL.md create mode 100644 skills/react-native-nitro-sqlite/references/attach-detach.md create mode 100644 skills/react-native-nitro-sqlite/references/batch-and-files.md create mode 100644 skills/react-native-nitro-sqlite/references/concurrency.md create mode 100644 skills/react-native-nitro-sqlite/references/connections.md create mode 100644 skills/react-native-nitro-sqlite/references/migration-and-errors.md create mode 100644 skills/react-native-nitro-sqlite/references/queries.md create mode 100644 skills/react-native-nitro-sqlite/references/setup.md create mode 100644 skills/react-native-nitro-sqlite/references/transactions.md create mode 100644 skills/react-native-nitro-sqlite/references/typeorm.md diff --git a/skills/react-native-nitro-sqlite/SKILL.md b/skills/react-native-nitro-sqlite/SKILL.md new file mode 100644 index 0000000..47249aa --- /dev/null +++ b/skills/react-native-nitro-sqlite/SKILL.md @@ -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 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 | 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 | diff --git a/skills/react-native-nitro-sqlite/references/attach-detach.md b/skills/react-native-nitro-sqlite/references/attach-detach.md new file mode 100644 index 0000000..401b14d --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/attach-detach.md @@ -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.`, the attached one is `.
`. + +## 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) diff --git a/skills/react-native-nitro-sqlite/references/batch-and-files.md b/skills/react-native-nitro-sqlite/references/batch-and-files.md new file mode 100644 index 0000000..2c70e50 --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/batch-and-files.md @@ -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 +``` + +### 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 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) diff --git a/skills/react-native-nitro-sqlite/references/concurrency.md b/skills/react-native-nitro-sqlite/references/concurrency.md new file mode 100644 index 0000000..747c3ba --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/concurrency.md @@ -0,0 +1,107 @@ +--- +id: concurrency +title: Sync vs async and the per-database operation queue +scope: react-native-nitro-sqlite +keywords: sync, async, queue, DatabaseQueue, busy, Database is busy, ordering, serial, setImmediate, JS thread, UI thread, blocking, in progress, concurrency +--- + +# Sync vs async and the per-database operation queue + +This is the most important behavioral concept in the library and the source of the most confusing errors. + +## Mental model + +Each open database has its own JS-level **operation queue** (`{ queue: [], inProgress: boolean }`), keyed by the db `name`. It exists to serialize the operations that go through it so they never corrupt each other or interleave mid-transaction. + +**Crucial detail — only three operations use this queue:** `executeBatch`, `executeBatchAsync`, and `transaction`. Everything else (`execute`, `executeAsync`, `loadFile`, `loadFileAsync`) calls the native module **directly** and does not touch the JS queue at all. + +Two orthogonal axes: + +- **Blocking vs off-thread.** `execute`, `executeBatch`, `loadFile`, and `tx.execute`/`commit`/`rollback` run synchronously on the **JS thread** and block it until SQLite returns. `executeAsync`, `executeBatchAsync`, `transaction`, and `loadFileAsync` run off the JS thread and return a `Promise`. +- **Queued vs not.** Independently, only `executeBatch` / `executeBatchAsync` / `transaction` are serialized through the JS queue (and only `executeBatch`, being synchronous, can throw "busy"). + +## Which calls touch the queue + +| Call | Queue behavior | +|---|---| +| `db.execute` (sync) | **Bypasses the queue.** Blocks the JS thread; never throws "busy". | +| `db.executeAsync` (async) | **Bypasses the queue.** Off-thread; not serialized by the JS queue. | +| `db.loadFile` (sync) | **Bypasses the queue.** Blocks the JS thread; never throws "busy". | +| `db.loadFileAsync` (async) | **Bypasses the queue.** Off-thread; not serialized by the JS queue. | +| `db.executeBatch` (sync) | Uses `startOperationSync` → **throws if the db is busy** with a queued/in-progress async batch or transaction. | +| `db.executeBatchAsync` (async) | Enqueued (`queueOperationAsync`) — runs serially in submission order. | +| `db.transaction` (async) | Enqueued — runs serially; can't interleave with other transactions/batches. | + +> Practical takeaway: **transactions and async batches are strictly ordered**, and a **synchronous `executeBatch` will throw** if you run it while a batch/transaction is pending. `execute`/`executeAsync`/`loadFile`/`loadFileAsync` are never blocked by the queue and never throw "busy". + +## The "Database is busy" error + +``` +NitroSQLiteError: Cannot run synchronous operation on database. +Database is busy with another operation. +``` + +This happens when a **synchronous `executeBatch`** is called while the queue has an in-progress or pending async batch/transaction (it is the only synchronous queue-aware op): + +```ts +// ❌ Will throw if the async batch hasn't finished +db.executeBatchAsync(bigInsert) // not awaited → queued, inProgress +db.executeBatch(moreInserts) // throws: database is busy +``` + +Fixes: + +```ts +// ✅ await the async op first +await db.executeBatchAsync(bigInsert) +db.executeBatch(moreInserts) + +// ✅ or just use the async variant for both +await db.executeBatchAsync(bigInsert) +await db.executeBatchAsync(moreInserts) +``` + +## Ordering guarantees + +Async queue operations run in the exact order they were submitted, even if you don't `await` each one: + +```ts +const results = await Promise.all([ + db.transaction(async (tx) => tx.execute('UPDATE c SET v = v + 1')), + db.transaction(async (tx) => tx.execute('UPDATE c SET v = v + 1')), + db.executeBatchAsync([{ query: 'INSERT INTO log DEFAULT VALUES' }]), +]) +// Each ran to completion before the next started, in this order. +``` + +## Closing while busy + +`close()` does not block on the queue: if operations are still pending it logs a warning and closes anyway. Always `await` outstanding async work before `close()`: + +```ts +await Promise.all(pendingOps) +db.close() +``` + +## Choosing sync vs async — guidance + +- **Default to async** (`executeAsync`, `executeBatchAsync`, `transaction`) for anything that touches more than a few rows, runs at startup, or happens during animation/scroll. It keeps the JS/UI thread responsive. +- **Use sync** (`execute`) only for tiny, instant reads where a few-millisecond block is acceptable and you know no async op is in flight. +- **Never** mix a not-awaited async batch/transaction with a following synchronous `executeBatch` — that's the classic "busy" crash. + +## Why not just always sync? (it's JSI, so it's "fast") + +JSI removes the *bridge* overhead, but the SQL work itself still takes real time. A large query run synchronously blocks the JS thread → dropped frames. Async moves the SQLite work off-thread; the queue additionally keeps batches and transactions correctly ordered. + +## Gotchas + +- **"Busy" only comes from synchronous `executeBatch`** — never from `execute`, `executeAsync`, `loadFile`, or `loadFileAsync`. If you see it, find the un-awaited async batch/transaction before it. +- **`loadFile`/`loadFileAsync` bypass the queue** — they call native directly. `loadFile` blocks the JS thread (like `execute`); `loadFileAsync` runs off-thread but is not serialized by the JS queue. +- **Plain `execute` won't throw "busy"**, but it still blocks the JS thread and can run concurrently with native async work — prefer wrapping related writes in a transaction. +- **One queue per db `name`.** Two different databases have independent queues and can run truly in parallel. +- **`setImmediate` scheduling** means even a queue with one item yields a tick before running — async ops are never synchronously resolved. + +## Pointers + +- Source: `package/src/DatabaseQueue.ts` (`queueOperationAsync`, `startOperationSync`, `startOperationAsync`) +- Related: [queries.md](./queries.md), [transactions.md](./transactions.md), [batch-and-files.md](./batch-and-files.md) diff --git a/skills/react-native-nitro-sqlite/references/connections.md b/skills/react-native-nitro-sqlite/references/connections.md new file mode 100644 index 0000000..e92dd16 --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/connections.md @@ -0,0 +1,115 @@ +--- +id: connections +title: Opening connections and managing databases +scope: react-native-nitro-sqlite +keywords: open, NitroSQLiteConnection, name, location, close, delete, drop, already open, existing database, documents directory, files directory, sandbox, path +--- + +# Opening connections and managing databases + +## Mental model + +You always work through the JS `open()` wrapper, **not** the raw native module. `open({ name })` does three things: + +1. Opens (creating if missing) the SQLite file via the native module. +2. Registers the database in the internal **operation queue** (see [concurrency.md](./concurrency.md)). +3. Returns a `NitroSQLiteConnection` whose methods already know the db name — you never repeat it. + +```ts +import { open } from 'react-native-nitro-sqlite' +import type { NitroSQLiteConnection } from 'react-native-nitro-sqlite' + +const db: NitroSQLiteConnection = open({ name: 'app.sqlite' }) +``` + +## `open(options)` + +```ts +interface NitroSQLiteConnectionOptions { + name: string // file name (and the key used in the operation queue) + location?: string // optional directory; defaults to the app's data dir +} +``` + +- `name` is the database file name **and** the identity used by the queue and by `attach`/`close`/`delete`. +- `location` lets you open the file in a non-default directory (see "File locations" below). + +**Opening the same name twice throws.** The queue rejects a second `open` of an already-open name: + +``` +NitroSQLiteError: Database app.sqlite is already open. There is already a connection to the database. +``` + +Keep a single connection per database name (e.g. a module-level singleton) and reuse it. + +## The connection object + +```ts +interface NitroSQLiteConnection { + close(): void + delete(): void + attach(dbNameToAttach: string, alias: string, location?: string): void + detach(alias: string): void + transaction: (cb: (tx: Transaction) => Promise) => Promise + execute: ExecuteQuery // sync + executeAsync: ExecuteAsyncQuery // async + executeBatch(commands: BatchQueryCommand[]): BatchQueryResult + executeBatchAsync(commands: BatchQueryCommand[]): Promise + loadFile(location: string): FileLoadResult + loadFileAsync(location: string): Promise +} +``` + +Method-by-method: queries → [queries.md](./queries.md); transactions → [transactions.md](./transactions.md); batch & files → [batch-and-files.md](./batch-and-files.md); attach/detach → [attach-detach.md](./attach-detach.md). + +## Lifecycle: `close()` and `delete()` + +```ts +db.close() // closes the connection and removes it from the operation queue +db.delete() // drops/deletes the database file from disk +``` + +- `close()` frees the queue entry for that name. If there are still queued/in-progress operations, it logs a warning and closes anyway — so `await` your pending async work before closing. +- `delete()` removes the underlying file (it calls the native `drop`). You typically `close()` then `delete()`. +- After `close()`, the **queue-aware** methods (`executeBatch`, `executeBatchAsync`, `transaction`) throw `Database is not open`. Plain `execute`/`executeAsync`/`loadFile` don't perform that JS-level check and will instead fail at the native layer (the handle is gone). Either way, don't use a closed connection — re-`open()` to use it again. + +```ts +// Reset pattern +db.close() +db.delete() +db = open({ name: 'app.sqlite' }) +``` + +## File locations + +Databases are created under the app's data directory by default: + +- **iOS:** the app **Documents** directory (or the App Group container if `RNNitroSQLite_AppGroup` is set — see [setup.md](./setup.md)). +- **Android:** the app **files** directory. + +To open a file elsewhere: + +- Pass an absolute directory in `location`: `open({ name: 'my.sqlite', location: '/some/abs/dir' })`. +- Or use a path relative to the default root, e.g. `open({ name: 'myDb.sqlite', location: '../www' })` to read a bundled DB shipped at `../www/myDb.sqlite`. + +> **iOS sandbox:** you cannot access paths outside the app sandbox. To open a DB that lives elsewhere (e.g. a downloaded file), copy/move it into the sandbox first with a file library (`react-native-fs`, `expo-file-system`, etc.), then `open()` it. + +## Loading / shipping an existing database + +1. Place the `.sqlite` file in your app assets or download it at runtime. +2. Copy it into the app data directory (or the directory you'll pass as `location`). +3. `open({ name: 'prefilled.sqlite', location: '' })`. + +This opens the existing file as-is; no migration is performed. + +## Gotchas + +- **One connection per name.** A second `open()` of the same name throws "already open". +- **`delete()` is irreversible.** It removes the file from disk. +- **`close()` before `delete()`** if you intend to reopen, to avoid the busy warning. +- **`name` is the queue key.** Two different files must have two different `name`s; `location` only changes the directory. + +## Pointers + +- Source: `package/src/operations/session.ts`, `package/src/DatabaseQueue.ts` +- Related: [concurrency.md](./concurrency.md), [attach-detach.md](./attach-detach.md) diff --git a/skills/react-native-nitro-sqlite/references/migration-and-errors.md b/skills/react-native-nitro-sqlite/references/migration-and-errors.md new file mode 100644 index 0000000..a71ef11 --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/migration-and-errors.md @@ -0,0 +1,98 @@ +--- +id: migration-and-errors +title: Error handling and migrating from react-native-quick-sqlite +scope: react-native-nitro-sqlite +keywords: NitroSQLiteError, error handling, instanceof, fromError, try catch, react-native-quick-sqlite, migration, deprecated, 9.0.0, breaking changes, QuickSQLite, open +--- + +# Error handling and migrating from react-native-quick-sqlite + +## Error handling — `NitroSQLiteError` + +Every operation that can fail throws (or rejects with) a `NitroSQLiteError`. The library wraps native and JS errors through `NitroSQLiteError.fromError`, preserving the message, `cause`, and stack where possible. + +```ts +import { open, NitroSQLiteError } from 'react-native-nitro-sqlite' + +try { + const db = open({ name: 'app.sqlite' }) + await db.executeAsync('SELECT * FROM does_not_exist') +} catch (e) { + if (e instanceof NitroSQLiteError) { + console.warn('SQLite error:', e.message) + console.warn('caused by:', e.cause) // original error, when available + } else { + throw e + } +} +``` + +`NitroSQLiteError`: +- `name` is always `'NitroSQLiteError'`. +- `instanceof NitroSQLiteError` works (the prototype chain is maintained). +- `.cause` carries the original error when one was wrapped. +- Static `NitroSQLiteError.fromError(unknown)` converts any thrown value into a `NitroSQLiteError` (used internally; useful if you re-wrap). + +### Errors you'll commonly see + +| Message | Meaning | Fix | +|---|---|---| +| `Database is already open...` | `open()` called twice for the same name | Reuse the existing connection (singleton) | +| `Database is not open...` | `executeBatch`/`executeBatchAsync`/`transaction` after `close()`, or never opened | `open()` first; don't use a closed connection | +| `...is busy with another operation.` | Synchronous `executeBatch` while a queued async batch/transaction is pending | `await` the async op, or use `executeBatchAsync` — see [concurrency.md](./concurrency.md) | +| `Cannot execute query on finalized transaction...` | Using `tx` after `commit()`/`rollback()` | Don't touch `tx` once finalized — see [transactions.md](./transactions.md) | +| SQL syntax / constraint errors | The SQL itself failed | Inspect `e.message` / `e.cause` | + +Async errors reject the promise (catch with `try/await` or `.catch`); sync errors throw synchronously. + +## Migrating from `react-native-quick-sqlite` + +`react-native-quick-sqlite` is **deprecated** in favor of this package. From major version **9.0.0**, the package name is `react-native-nitro-sqlite`. The JS API is intentionally close, so migration is mostly mechanical. + +### 1. Swap the dependency + +```bash +npm uninstall react-native-quick-sqlite +npm install react-native-nitro-sqlite react-native-nitro-modules +npx pod-install +``` + +`react-native-nitro-modules` is now a **required peer dependency** (it wasn't for quick-sqlite). Requires RN 0.75+ / New Architecture — see [setup.md](./setup.md). + +### 2. Update imports + +```ts +// before +import { open } from 'react-native-quick-sqlite' +// after +import { open } from 'react-native-nitro-sqlite' +``` + +The `open({ name, location })` → `NitroSQLiteConnection` pattern is the same. `execute`, `executeAsync`, `executeBatch`, `executeBatchAsync`, `transaction`, `attach`, `detach`, `loadFile`, `close`, `delete` all carry over. + +### 3. Result shape + +Results expose `results` (plain `Row[]`), `rows` (`_array` / `length` / `item`), `rowsAffected`, `insertId`, and optional `metadata` — see [queries.md](./queries.md). If your quick-sqlite code read `rows._array` / `rows.item(i)`, that still works. + +### 4. TypeORM users + +The driver export is `typeORMDriver` and the babel alias is the same `react-native-sqlite-storage` → (now) `react-native-nitro-sqlite`. See [typeorm.md](./typeorm.md). + +### 5. Re-check the concurrency model + +Behavior around the per-database queue and the **"database is busy"** error is the area most likely to surface a latent bug after migrating. Audit any not-awaited async batch/transaction immediately followed by a synchronous `executeBatch`. See [concurrency.md](./concurrency.md). + +### 6. Native compile flags / system SQLite + +Quick-sqlite users who set FTS5 flags, `*_USE_PHONE_VERSION`, or app groups should re-apply them with the nitro-sqlite names: `NITRO_SQLITE_USE_PHONE_VERSION`, `nitroSqliteFlags`, target `RNNitroSQLite`, `RNNitroSQLite_AppGroup`. See [setup.md](./setup.md). + +## Gotchas + +- **Don't catch and silently drop errors inside a `transaction` callback** — that prevents the auto-rollback. +- **Check `instanceof NitroSQLiteError`** rather than matching message strings where possible. +- **Bug fixes for `react-native-quick-sqlite@8.x` continue for a limited time only** — plan the migration. + +## Pointers + +- Source: `package/src/NitroSQLiteError.ts`, `package/src/index.ts` +- Related: [concurrency.md](./concurrency.md), [setup.md](./setup.md), [typeorm.md](./typeorm.md) diff --git a/skills/react-native-nitro-sqlite/references/queries.md b/skills/react-native-nitro-sqlite/references/queries.md new file mode 100644 index 0000000..e7e31cc --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/queries.md @@ -0,0 +1,146 @@ +--- +id: queries +title: Executing queries and reading results +scope: react-native-nitro-sqlite +keywords: execute, executeAsync, params, parameter binding, placeholder, QueryResult, results, rows, _array, item, length, rowsAffected, insertId, metadata, ColumnType, typed rows, generic, SQLiteValue, blob, ArrayBuffer, null +--- + +# Executing queries and reading results + +## Mental model + +A single SQL statement runs via `execute` (sync, on the JS thread) or `executeAsync` (async, off the JS thread). Both call the native module directly — they do **not** go through the per-database operation queue, so they never throw the "database is busy" error. Both return — directly or via `Promise` — the **same** `QueryResult` shape. Pick async by default to avoid blocking the UI (see [concurrency.md](./concurrency.md)). + +```ts +const r = db.execute('SELECT * FROM users WHERE age > ?', [21]) +const r2 = await db.executeAsync('SELECT * FROM users WHERE age > ?', [21]) +``` + +## Signatures + +```ts +type SQLiteValue = boolean | number | string | ArrayBuffer | null +type SQLiteQueryParams = SQLiteValue[] + +// Optional generic Row type for the returned rows +db.execute(query: string, params?: SQLiteQueryParams): QueryResult +db.executeAsync(query: string, params?: SQLiteQueryParams): Promise> +``` + +## Parameter binding + +Always bind values with `?` placeholders and a params array — never string-concatenate (SQL injection + type coercion bugs). + +```ts +db.execute( + 'INSERT INTO users (id, name, age) VALUES (?, ?, ?)', + [1, 'Marc', 24] +) + +const { results } = db.execute( + 'SELECT * FROM users WHERE name = ? AND age >= ?', + ['Marc', 18] +) +``` + +Bindable types: `boolean | number | string | ArrayBuffer | null`. `ArrayBuffer` is stored/read as a SQLite **BLOB**; `null` binds SQL `NULL`. + +## The `QueryResult` shape + +```ts +type QueryResult> = { + rowsAffected: number + insertId?: number + results: Row[] // plain array of row objects (keyed by column name) + metadata?: Record + rows: { // TypeORM-style view of the same rows + _array: Row[] + length: number + item: (idx: number) => Row | undefined + } +} +``` + +Two equivalent ways to read rows — use whichever you like, they hold the same data: + +```ts +const r = db.execute<{ id: number; name: string }>('SELECT id, name FROM users') + +// Option A — plain array +for (const row of r.results) { + console.log(row.id, row.name) +} + +// Option B — TypeORM-style view +console.log(r.rows.length) // number of rows +const first = r.rows.item(0) // Row | undefined +const all = r.rows._array // Row[] +``` + +`rowsAffected` — number of rows changed by INSERT/UPDATE/DELETE. `insertId` — the auto-generated rowid after an INSERT (present when applicable): + +```ts +const ins = db.execute('INSERT INTO users (name) VALUES (?)', ['Marc']) +console.log(ins.rowsAffected) // 1 +console.log(ins.insertId) // e.g. 1 +``` + +> SELECT queries return `rowsAffected: 0` and an empty `insertId`; their data is in `results` / `rows`. + +## Typed rows + +Pass a generic to get typed row objects (no runtime validation — it's a TypeScript convenience): + +```ts +interface User { id: number; name: string; age: number } + +const { results } = await db.executeAsync('SELECT id, name, age FROM users') +results[0].name // typed as string +``` + +## Column metadata + +When `metadata` is present, it maps each column name to its declared type and index: + +```ts +import { ColumnType } from 'react-native-nitro-sqlite' + +const { metadata } = db.execute('SELECT id, name FROM users LIMIT 1') +if (metadata) { + for (const [col, meta] of Object.entries(metadata)) { + console.log(col, meta.type, meta.index) + } +} +``` + +`ColumnType` enum members: `BOOLEAN`, `NUMBER`, `INT64`, `TEXT`, `ARRAY_BUFFER`, `NULL_VALUE`. Declared type is `"UNKNOWN"`-equivalent for dynamic/computed columns (e.g. function results), where SQLite can't infer a static type. + +## Storing blobs (ArrayBuffer) + +```ts +const bytes = new Uint8Array([1, 2, 3, 255]) +db.execute('INSERT INTO files (id, blob) VALUES (?, ?)', [1, bytes.buffer]) + +const { results } = db.execute<{ blob: ArrayBuffer }>('SELECT blob FROM files WHERE id = ?', [1]) +const view = new Uint8Array(results[0].blob) +``` + +Bind the underlying `ArrayBuffer` (e.g. `typedArray.buffer`), and read it back as an `ArrayBuffer`. + +## Sync vs async — when to use which + +- `execute` (sync): small, fast, one-off reads/writes where briefly blocking the JS thread is fine. It calls native directly and never throws "busy", but it does block the JS thread for the duration of the query. +- `executeAsync` (async): anything that could be slow (large reads, many rows, complex joins) — keeps the UI thread free. It also bypasses the operation queue; only `executeBatch`/`executeBatchAsync`/`transaction` are serialized. See [concurrency.md](./concurrency.md). + +## Gotchas + +- **Both `results` and `rows` exist** on every result — don't assume only one. `rows` is mainly for TypeORM compatibility. +- **`insertId` only after INSERT.** It's `undefined` for SELECT/UPDATE/DELETE. +- **Generic types are not validated.** `execute` only affects TypeScript; the runtime returns whatever SQLite produced. +- **No automatic JSON.** Store objects by serializing to TEXT yourself (`JSON.stringify` / `JSON.parse`). +- **Booleans round-trip as the JS `boolean` type** via `SQLiteValue`, but SQLite stores them as integers under the hood; filter with `WHERE flag = ?` passing `true`/`false`. + +## Pointers + +- Source: `package/src/operations/execute.ts`, `package/src/types.ts`, `package/src/specs/NitroSQLiteQueryResult.nitro.ts` +- Related: [transactions.md](./transactions.md), [batch-and-files.md](./batch-and-files.md), [concurrency.md](./concurrency.md) diff --git a/skills/react-native-nitro-sqlite/references/setup.md b/skills/react-native-nitro-sqlite/references/setup.md new file mode 100644 index 0000000..a4e6606 --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/setup.md @@ -0,0 +1,94 @@ +--- +id: setup +title: Installation and native configuration +scope: react-native-nitro-sqlite +keywords: install, pod-install, expo, prebuild, FTS5, Geopoly, compile flags, GCC_PREPROCESSOR_DEFINITIONS, NITRO_SQLITE_USE_PHONE_VERSION, system sqlite, app groups, RNNitroSQLite_AppGroup, nitro-modules, new architecture, peer dependency +--- + +# Installation and native configuration + +## Mental model + +`react-native-nitro-sqlite` is a Nitro Module: it autolinks, has **no Expo config plugin**, and requires the **New Architecture**. The native side bundles its own SQLite C build by default (so behavior is consistent across OS versions), but you can opt into the OS's system SQLite or flip SQLite compile-time flags. + +## Requirements + +- **React Native ≥ 0.75** with the New Architecture enabled. (README text says 0.71+, but `peerDependencies` pins `react-native >= 0.75.0`.) +- **`react-native-nitro-modules` ≥ 0.35.0** — a required peer dependency, installed alongside. +- iOS and Android. (No web.) + +## Install + +```bash +npm install react-native-nitro-sqlite react-native-nitro-modules +npx pod-install +``` + +Expo (bare/prebuild — this is not a config-plugin library, so you must prebuild): + +```bash +npx expo install react-native-nitro-sqlite react-native-nitro-modules +npx expo prebuild +``` + +Rebuild the native app after installing. No `Podfile` edits, no `MainApplication` registration — Nitro handles linking. + +## Use the system (phone) SQLite on iOS + +By default the bundled SQLite is compiled in. To link against the OS's system SQLite instead (smaller binary, but version varies by iOS release): + +```bash +NITRO_SQLITE_USE_PHONE_VERSION=1 npx pod-install +``` + +## Compile-time SQLite options (FTS5, Geopoly, JSON1, etc.) + +SQLite features like FTS5 full-text search or Geopoly are enabled with preprocessor defines on the native target. + +**iOS** — in your app's `ios/Podfile`, inside a `post_install` block: + +```ruby +post_install do |installer| + installer.pods_project.targets.each do |target| + if target.name == "RNNitroSQLite" + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'SQLITE_ENABLE_FTS5=1' + # add more flags by pushing additional entries + end + end + end +end +``` + +**Android** — in `android/gradle.properties`: + +```properties +nitroSqliteFlags="-DSQLITE_ENABLE_FTS5=1" +``` + +Multiple flags: combine them in the same string, e.g. `nitroSqliteFlags="-DSQLITE_ENABLE_FTS5=1 -DSQLITE_ENABLE_GEOPOLY=1"`. + +> Note: enabling FTS5 etc. via flags only works when compiling the **bundled** SQLite (the default). It won't take effect if you switch to the system SQLite on iOS via `NITRO_SQLITE_USE_PHONE_VERSION`. + +## iOS App Groups (share a DB with extensions / widgets) + +To store the database inside an App Group container (so an app extension can read it): + +1. Add the **App Groups** capability in Xcode and create/select your group id (e.g. `group.com.myapp`). +2. In your app's `Info.plist`, set the key `RNNitroSQLite_AppGroup` to that group id. + +The database files then live in the shared app-group directory instead of the app's documents directory. + +## Gotchas + +- **Old Architecture is not supported.** Nitro Modules require the New Architecture; if the app is on the legacy bridge, enable New Arch first. +- **Forgetting `react-native-nitro-modules`.** It is a separate required peer dependency; installing only `react-native-nitro-sqlite` will fail at runtime. +- **Expo Go won't work.** There's native code; use a development build (`expo prebuild` + custom dev client), not Expo Go. +- **Rebuild after install/flag changes.** Compile-flag changes require a clean native rebuild (`pod install` again on iOS; rebuild on Android). + +## Pointers + +- Nitro Modules: https://nitro.margelo.com/ +- Repo: https://github.com/margelo/react-native-nitro-sqlite +- Related: [connections.md](./connections.md) diff --git a/skills/react-native-nitro-sqlite/references/transactions.md b/skills/react-native-nitro-sqlite/references/transactions.md new file mode 100644 index 0000000..2ce10ba --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/transactions.md @@ -0,0 +1,131 @@ +--- +id: transactions +title: Transactions +scope: react-native-nitro-sqlite +keywords: transaction, tx, commit, rollback, BEGIN, COMMIT, ROLLBACK, atomic, auto commit, auto rollback, finalized transaction, queueOperationAsync, isExclusive +--- + +# Transactions + +## Mental model + +`db.transaction(callback)` runs a set of statements atomically. It is **async-only** (returns a `Promise`). Internally it: + +1. Goes through the per-database operation queue (so it can't interleave with other queued ops — see [concurrency.md](./concurrency.md)). +2. Runs `BEGIN TRANSACTION`. +3. Invokes your callback with a `tx` object. +4. If the callback **resolves** without having committed → it runs `COMMIT`. +5. If the callback **throws/rejects** → it runs `ROLLBACK` and re-throws as a `NitroSQLiteError`. + +```ts +await db.transaction(async (tx) => { + tx.execute('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Marc']) + await tx.executeAsync('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, 1]) +}) +// committed automatically here +``` + +## Signature + +```ts +db.transaction: ( + cb: (tx: Transaction) => Promise +) => Promise + +interface Transaction { + execute: ExecuteQuery // sync, scoped to this transaction + executeAsync: ExecuteAsyncQuery + commit(): NitroSQLiteQueryResult + rollback(): NitroSQLiteQueryResult +} +``` + +The value your callback returns is forwarded as the resolved value of `db.transaction(...)`: + +```ts +const newId = await db.transaction(async (tx) => { + const r = tx.execute('INSERT INTO users (name) VALUES (?)', ['Marc']) + return r.insertId +}) +``` + +## Auto commit (default) + +If you don't call `tx.commit()` / `tx.rollback()` yourself and the callback resolves, the transaction commits automatically: + +```ts +await db.transaction(async (tx) => { + tx.execute('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Marc']) + // no explicit commit → auto-committed +}) +``` + +## Auto rollback on error + +Throwing anywhere in the callback rolls everything back: + +```ts +await db.transaction(async (tx) => { + tx.execute('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Marc']) + throw new Error('abort') // → ROLLBACK, nothing persisted +}).catch((e) => { + // e is a NitroSQLiteError +}) +``` + +## Manual commit / rollback + +You can finalize explicitly. After committing or rolling back, the transaction is **finalized** — any further `tx.execute` / `tx.commit` / `tx.rollback` throws `Cannot execute query on finalized transaction`. + +```ts +await db.transaction(async (tx) => { + tx.execute('INSERT INTO users (id) VALUES (?)', [1]) + + if (someCondition) { + tx.rollback() // finalize: undo + return + } + + tx.commit() // finalize: persist + // tx.execute(...) here would throw — transaction is finalized +}) +``` + +## Mixing sync and async inside a transaction + +Both `tx.execute` (sync) and `tx.executeAsync` (async) are available and operate on the same transaction. The whole transaction already holds its queue slot, so internal calls run in the order you write them: + +```ts +await db.transaction(async (tx) => { + const a = tx.execute('SELECT count(*) AS n FROM users') + await tx.executeAsync('INSERT INTO log (n) VALUES (?)', [a.results[0].n]) +}) +``` + +## Ordering of concurrent transactions + +Because transactions go through the queue, multiple `db.transaction(...)` calls run **serially in submission order** — you can fire several without `await`ing each and they won't interleave: + +```ts +const ps = [] +for (let i = 0; i < 10; i++) { + ps.push(db.transaction(async (tx) => { + tx.execute('UPDATE counter SET v = v + 1') + })) +} +await Promise.all(ps) // applied one after another, in order +``` + +## Gotchas + +- **`transaction` is async only** — there is no synchronous `db.transaction`. Always `await` it (or handle the promise). +- **Don't `open`/`close` inside a transaction.** +- **Finalized = locked.** After an explicit `commit()`/`rollback()`, don't touch `tx` again. +- **Throwing rolls back.** Don't swallow errors inside the callback if you want the rollback to happen; let them propagate. +- **The error you catch is a `NitroSQLiteError`** (the original error is wrapped/preserved). See [migration-and-errors.md](./migration-and-errors.md). +- **For pure bulk inserts**, `executeBatch`/`executeBatchAsync` is simpler and also atomic — see [batch-and-files.md](./batch-and-files.md). Use `transaction` when you need to read intermediate results or branch logic. + +## Pointers + +- Source: `package/src/operations/transaction.ts`, `package/src/DatabaseQueue.ts` +- Related: [concurrency.md](./concurrency.md), [batch-and-files.md](./batch-and-files.md) diff --git a/skills/react-native-nitro-sqlite/references/typeorm.md b/skills/react-native-nitro-sqlite/references/typeorm.md new file mode 100644 index 0000000..1be2ee2 --- /dev/null +++ b/skills/react-native-nitro-sqlite/references/typeorm.md @@ -0,0 +1,118 @@ +--- +id: typeorm +title: Using react-native-nitro-sqlite as a TypeORM driver +scope: react-native-nitro-sqlite +keywords: typeorm, typeORMDriver, DataSource, driver, react-native-sqlite-storage, babel, module-resolver, alias, patch-package, package.json exports, reflect-metadata, decorators +--- + +# Using react-native-nitro-sqlite as a TypeORM driver + +## Mental model + +The library ships a `typeORMDriver` export that adapts the connection to TypeORM's `react-native` driver contract (callback-based `executeSql`, `transaction`, `close`, `attach`, `detach`). Because of Metro/Node resolution quirks, two extra setup steps are required: exposing TypeORM's `package.json`, and aliasing the driver module name TypeORM expects. + +> The `typeORMDriver` object is intended **only** for TypeORM. For normal app code use the `open()` connection API directly. + +## Step 1 — Expose TypeORM's `package.json` + +TypeORM needs its own `package.json` resolvable. Add this to TypeORM's `package.json` `exports` map: + +```json +{ + "exports": { + "./package.json": "./package.json" + } +} +``` + +Persist that change across installs with `patch-package`: + +```bash +npx patch-package --exclude 'nothing' typeorm +``` + +(Make sure `patch-package` runs on `postinstall`.) + +## Step 2 — Alias the driver in Babel + +TypeORM's React Native driver imports `react-native-sqlite-storage`. Alias it to this library in `babel.config.js`: + +```js +module.exports = { + // ... + plugins: [ + [ + 'module-resolver', + { + alias: { + 'react-native-sqlite-storage': 'react-native-nitro-sqlite', + }, + }, + ], + ], +} +``` + +Install the plugin: + +```bash +npm i -D babel-plugin-module-resolver +``` + +You'll also typically need decorator support for TypeORM entities: + +```bash +npm i -D babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators +``` + +```js +plugins: [ + 'babel-plugin-transform-typescript-metadata', + ['@babel/plugin-proposal-decorators', { legacy: true }], + // ...module-resolver above +] +``` + +And import `reflect-metadata` once at the app entry: + +```ts +import 'reflect-metadata' +``` + +## Step 3 — Configure the DataSource + +```ts +import { DataSource } from 'typeorm' +import { typeORMDriver } from 'react-native-nitro-sqlite' + +const dataSource = new DataSource({ + type: 'react-native', + database: 'typeormdb', + location: '.', + driver: typeORMDriver, + entities: [/* your entities */], + synchronize: true, +}) + +await dataSource.initialize() +``` + +- `type: 'react-native'` — TypeORM's RN driver type. +- `driver: typeORMDriver` — the export from this library. +- `location` — directory for the DB file (same meaning as `open`'s `location`). + +## What `typeORMDriver` provides + +It exposes `openDatabase(options, ok, fail)` returning a connection with `executeSql` (callback-style, backed by `executeAsync`), `transaction`, `close`, `attach`, and `detach`. You don't call these directly — TypeORM does. + +## Gotchas + +- **Both Babel steps are mandatory.** Missing the alias → TypeORM tries to load `react-native-sqlite-storage` (not installed). Missing decorator plugins → entity decorators fail. +- **`patch-package` must persist.** Without exposing `./package.json`, TypeORM's version detection breaks under Metro. +- **`reflect-metadata` import must come first**, before any entity is imported. +- **Restart Metro with cache reset** after editing `babel.config.js`: `npx react-native start --reset-cache`. + +## Pointers + +- Source: `package/src/typeORM.ts` +- Related: [connections.md](./connections.md), [setup.md](./setup.md)