diff --git a/docs/api-package-intros.mjs b/docs/api-package-intros.mjs index 9afbea98..5b58d5d0 100644 --- a/docs/api-package-intros.mjs +++ b/docs/api-package-intros.mjs @@ -1,94 +1,13 @@ const packageIntros = { - 'react-native-nitro-sqlite': `Nitro SQLite provides local SQLite databases for React Native. This page lists the package's exported functions, objects, and types. For a guided introduction, start with [Getting Started](/docs). + 'react-native-nitro-sqlite': `\`open({ name })\` returns a connection with query, transaction, and cleanup methods. This reference lists the package's exported functions, objects, and types. -## Install - -Requires React Native 0.75 or newer, React 17 or newer, and \`react-native-nitro-modules\` 0.37.1 or newer. - -\`\`\`sh -npm install react-native-nitro-sqlite react-native-nitro-modules -\`\`\` - -Install CocoaPods for Apple platforms, then rebuild the native app. Android needs no additional setup for the default database location. Expo projects need a development build; Expo Go cannot load the native module. - -\`\`\`sh -npx pod-install -\`\`\` - -For a React Native macOS app, run \`pod install\` from its \`macos\` directory. See the [macOS configuration guide](/docs/configuration/macos) for the desktop example and database location. - -## Open a database - -\`\`\`ts -import { open } from 'react-native-nitro-sqlite' - -const db = open({ name: 'app.sqlite' }) -db.execute('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)') -db.execute('INSERT INTO notes (body) VALUES (?)', ['Hello']) -const { rows } = db.execute('SELECT id, body FROM notes') -console.log(rows._array) -db.close() -\`\`\` - -Use [Getting Started](/docs) for native setup and a fuller first query. The [concepts](/docs/concepts/databases-and-connections) and [guides](/docs/guides/parameters-and-results) explain database lifetime, query parameters, and results. For the Node-only test export and its API, see [Test SQLite in Node](/docs/guides/node-test-mock). +Start with [Getting Started](/docs) for installation and a first query. Use [Configuration](/docs/configuration) for shared \`package.json\` settings, [database lifecycle](/docs/guides/database-lifecycle) for opening and closing connections, and [parameters and results](/docs/guides/parameters-and-results) for queries and errors. For the Node-only test export, see [Test SQLite in Node](/docs/guides/node-test-mock). `, - 'react-native-nitro-sqlite/mock': `This Node-only export runs SQLite tests through \`better-sqlite3\` without loading the React Native module. Install \`better-sqlite3\` as a development dependency and import this module in your test setup. See [Test SQLite in Node](/docs/guides/node-test-mock) for Jest setup, shared connections, prepared statements, transactions, and cleanup. + 'react-native-nitro-sqlite/mock': `Import \`react-native-nitro-sqlite/mock\` to run SQLite tests through \`better-sqlite3\` without loading the React Native module. See [Test SQLite in Node](/docs/guides/node-test-mock) for installation, Jest setup, shared connections, prepared statements, transactions, and cleanup. `, - 'react-native-nitro-sqlite-vec': `This optional package adds [sqlite-vec](https://github.com/asg017/sqlite-vec) vector search to Nitro SQLite. Its native code is compiled into the core package's SQLite build. The functions and types below cover availability checks, vector tables, and nearest-neighbor searches. - -## Install - -Install Nitro SQLite, Nitro Modules, and the vector companion. The core requires React Native 0.75 or newer, React 17 or newer, and Nitro Modules 0.37.1 or newer. Enable sqlite-vec for every platform you ship. - -\`\`\`sh -npm install react-native-nitro-sqlite react-native-nitro-modules react-native-nitro-sqlite-vec -\`\`\` - -On iOS and visionOS, install Pods with the build flag: - -\`\`\`sh -NITRO_SQLITE_VEC=1 npx pod-install -\`\`\` - -For macOS, run \`NITRO_SQLITE_VEC=1 pod install\` from the app's \`macos\` directory. - -On Android, add this to \`android/gradle.properties\`: - -\`\`\`properties -nitroSqliteVec=true -\`\`\` - -Rebuild the native app after enabling the flag. See the [vector search guide](/docs/integrations/vector-search) for platform setup and usage. - -## Create and search a vector table - -\`\`\`ts -import { open } from 'react-native-nitro-sqlite' -import { - createVectorTable, - isVecAvailable, - knnSearch, -} from 'react-native-nitro-sqlite-vec' - -const db = open({ name: 'vectors.sqlite' }) - -if (!isVecAvailable(db)) { - db.close() - throw new Error('Enable sqlite-vec in the native build') -} - -createVectorTable(db, 'embeddings', { dimensions: 3 }) -db.execute('INSERT INTO embeddings (rowid, embedding) VALUES (?, ?)', [ - 1, - '[0.1, 0.2, 0.3]', -]) - -const matches = knnSearch(db, 'embeddings', [0.1, 0.2, 0.25], 10) -console.log(matches) -db.close() -\`\`\` + 'react-native-nitro-sqlite-vec': `\`createVectorTable()\` creates a vector table, \`knnSearch()\` finds nearby matches, and \`isVecAvailable()\` checks extension availability on an open connection. This optional package adds [sqlite-vec](https://github.com/asg017/sqlite-vec) to Nitro SQLite. -\`isVecAvailable()\` returns \`false\` when its version query fails. The helpers use table and column names as SQL identifiers, so only pass names you control. See the [vector search guide](/docs/integrations/vector-search) for storage types, distance metrics, and custom queries. +Follow the [vector search guide](/docs/integrations/vector-search) for installation, platform setup, and a complete example. The functions and types below describe the helper API. `, } diff --git a/docs/content/docs/concepts/connection-ordering.mdx b/docs/content/docs/concepts/connection-ordering.mdx deleted file mode 100644 index 3bb3fff7..00000000 --- a/docs/content/docs/concepts/connection-ordering.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Connection ordering -description: Understand SQLite isolation and Nitro SQLite's operation queue. ---- - -SQLite controls how separate connections see each other's changes. A transaction sees its own writes, while other connections do not see uncommitted writes under SQLite's normal isolation rules. SQLite serializes writes, but the exact reader and writer behavior depends on its journal mode. See [SQLite's isolation guide](https://www.sqlite.org/isolation.html). - -## Ordering on one connection - -Nitro SQLite also coordinates work per managed connection. Ordinary async statements enter the JavaScript queue in call order. The native connection runs operations one at a time in that order, even when JavaScript submits multiple statements before awaiting them. Batches, transactions, and prepared statement executions wait for earlier work and reserve the connection until they finish. A synchronous call on that connection throws a busy error while work is pending or running: - -```ts -import { open } from 'react-native-nitro-sqlite' - -const db = open({ name: 'notes.sqlite' }) - -await db.executeAsync('CREATE TABLE IF NOT EXISTS notes (body TEXT NOT NULL)') -await db.executeAsync('INSERT INTO notes (body) VALUES (?)', ['Queued write']) -const { rows } = db.execute<{ count: number }>( - 'SELECT COUNT(*) AS count FROM notes', -) -console.log(rows.item(0)?.count) -db.close() -``` - -## Transactions and independent connections - -A transaction occupies that connection's queue until its callback finishes. Inside it, use `tx` methods. Awaiting another queued call on the same connection from the callback leaves both operations waiting. An [independent connection](/docs/guides/multiple-connections) has its own queue and transactions. Direct calls through `NitroSQLite.native` bypass the JavaScript queue, so coordinate them yourself if you use them with a managed connection. Read [sync and async](/docs/guides/sync-and-async) for choosing a call form and [native access](/docs/guides/sync-and-async#global-helpers-and-native-access) for that lower-level boundary. diff --git a/docs/content/docs/concepts/databases-and-connections.mdx b/docs/content/docs/concepts/databases-and-connections.mdx index acd53ab9..b4b4c872 100644 --- a/docs/content/docs/concepts/databases-and-connections.mdx +++ b/docs/content/docs/concepts/databases-and-connections.mdx @@ -1,28 +1,22 @@ --- title: Databases and connections -description: How SQLite files and Nitro SQLite connections relate. +description: SQLite database files, open connections, and persistent data. --- -SQLite is an embedded database. Your app calls the SQLite library directly, without a separate database server. A database usually lives in a file containing its tables, indexes, and other schema objects. A connection is an open handle to that database; closing the handle does not remove the file. See [SQLite's overview](https://www.sqlite.org/about.html) for more background. +SQLite is an embedded database engine. It runs inside an application and reads and writes local files without a separate database server. A database contains tables, indexes, and other schema objects. See [SQLite's overview](https://www.sqlite.org/about.html). -## Open and close a connection +## Connections -In Nitro SQLite, `open()` opens an existing file or creates one when it does not exist. Keep the returned connection while you need to query that database: + -```ts -import { open } from 'react-native-nitro-sqlite' +A connection is an open handle to a database. It holds the state needed to run SQL, including its transaction and connection settings. Several connections can access the same database file; each has its own transaction. [Isolation and concurrency](/docs/concepts/isolation-and-concurrency) explains when they see each other's changes. -const db = open({ name: 'notes.sqlite' }) -const { rows } = await db.executeAsync( - 'SELECT name FROM sqlite_master WHERE type = ?', - ['table'], -) -console.log(rows._array) -db.close() -``` +## File lifetimes -The optional `location` is a directory relative to the platform's database root. Only one default connection can be open for a database name at a time. Pass `connection: 'independent'` to `open()` for another handle to the same file. Finish pending async work before calling `close()`; it is synchronous and fails while that connection is busy. See [multiple connections](/docs/guides/multiple-connections) for a reader and writer example. + -## Delete a database file +Closing a connection releases its resources and leaves committed data in the file. Removing the database file removes that data. -Use `delete()` to remove the file when you no longer need its data. `close()` only releases the handle. For file locations, prepopulated databases, and cleanup, read the [database lifecycle guide](/docs/guides/database-lifecycle). The generated [`NitroSQLiteConnection` reference](/api/react-native-nitro-sqlite/interfaces/NitroSQLiteConnection) lists its methods. +SQLite can also create companion journal files. In [WAL mode](https://www.sqlite.org/wal.html#the_wal_file), committed changes can remain in the `-wal` file until checkpointing copies them into the main file. Copying only the main file while the database is active can lose data. + +For Nitro SQLite's open, close, delete, and file-location APIs, see [database lifecycle](/docs/guides/database-lifecycle). diff --git a/docs/content/docs/concepts/isolation-and-concurrency.mdx b/docs/content/docs/concepts/isolation-and-concurrency.mdx new file mode 100644 index 00000000..9fe59ea1 --- /dev/null +++ b/docs/content/docs/concepts/isolation-and-concurrency.mdx @@ -0,0 +1,32 @@ +--- +title: Isolation and concurrency +description: How SQLite connections share data and coordinate reads and writes. +--- + +A transaction sees its own writes before committing. Separate connections normally see only committed changes. See [SQLite's isolation guide](https://www.sqlite.org/isolation.html). + +## Readers and writers + + + +SQLite supports multiple readers, but only one writer at a time for a database file. Competing writes can fail with `SQLITE_BUSY`. A bounded `busy_timeout` can wait for temporary lock contention; it does not guarantee success. + +## Journal modes + + + +Rollback journaling preserves original pages in a journal while changes are written to the database. Readers and writers can block each other. + +Write-ahead logging, or WAL, writes changes to a separate log so readers can continue while a writer works: + +```sql +PRAGMA journal_mode = WAL; +``` + +A WAL read transaction sees a snapshot of committed data from when it began. It must end before a new transaction can see later commits. Long-lived readers can delay checkpoints and let the WAL file grow. See [SQLite's WAL documentation](https://www.sqlite.org/wal.html#concurrency). + +## Connection settings + +WAL mode persists in the database file. Settings such as `busy_timeout` and `foreign_keys` belong to each connection and need to be set again when opening another one. + +For Nitro SQLite APIs, see [multiple connections](/docs/guides/multiple-connections). For the order of calls on one connection, see [sync and async](/docs/guides/sync-and-async#connection-ordering). diff --git a/docs/content/docs/concepts/meta.json b/docs/content/docs/concepts/meta.json index fc5375d3..697bfb91 100644 --- a/docs/content/docs/concepts/meta.json +++ b/docs/content/docs/concepts/meta.json @@ -1,4 +1,10 @@ { "title": "Concepts", - "pages": ["databases-and-connections", "tables-and-values", "queries-and-indexes", "transactions", "connection-ordering"] + "pages": [ + "databases-and-connections", + "tables-and-values", + "queries-and-indexes", + "transactions", + "isolation-and-concurrency" + ] } diff --git a/docs/content/docs/concepts/queries-and-indexes.mdx b/docs/content/docs/concepts/queries-and-indexes.mdx index cd18515a..82b1cf8f 100644 --- a/docs/content/docs/concepts/queries-and-indexes.mdx +++ b/docs/content/docs/concepts/queries-and-indexes.mdx @@ -1,76 +1,49 @@ --- title: Queries and indexes -description: Read and change rows with SQL, then inspect query plans. +description: SQL queries, query plans, and spatial indexes. --- -SQL statements read or change the database. A `SELECT` can filter rows with `WHERE`, choose columns, and set an order with `ORDER BY`. Without an index that helps a query, SQLite may need to inspect many rows. An index is a separate data structure that SQLite's query planner can use to find matching rows or produce an order. It also takes storage and work to maintain when data changes. [SQLite's query planning guide](https://www.sqlite.org/queryplanner.html) explains how the planner chooses a path. +A `SELECT` reads rows, filters them with `WHERE`, and orders them with `ORDER BY`. An index helps SQLite find matching rows without inspecting every row. It also takes storage and adds work to writes. ## Create an index and inspect the plan -Nitro SQLite sends your SQL through `execute()` or `executeAsync()`. Assuming the `notes` table from [tables and values](/docs/concepts/tables-and-values) exists, use the same methods to create an index and inspect a query plan: +For the `notes` table in [tables and values](/docs/concepts/tables-and-values): -```ts -import { open } from 'react-native-nitro-sqlite' +```sql +CREATE INDEX notes_body_idx ON notes(body); -const db = open({ name: 'notes.sqlite' }) - -await db.executeAsync( - 'CREATE INDEX IF NOT EXISTS notes_body_idx ON notes(body)', -) +SELECT id, body FROM notes WHERE body = 'Buy milk'; +EXPLAIN QUERY PLAN SELECT id, body FROM notes WHERE body = 'Buy milk'; +``` -const { rows } = await db.executeAsync<{ id: number; body: string }>( - 'SELECT id, body FROM notes WHERE body = ?', - ['Buy milk'], -) -console.log(rows._array) +SQLite's query planner chooses whether to use an index. Inspect the plan with representative data before adding indexes for queries your application runs. See [SQLite's query planning guide](https://www.sqlite.org/queryplanner.html). -const plan = await db.executeAsync( - 'EXPLAIN QUERY PLAN SELECT id, body FROM notes WHERE body = ?', - ['Buy milk'], -) -console.log(plan.rows._array) -db.close() -``` +## Parameters and prepared statements -Bind application values with `?`; placeholders cannot stand in for table or column names. Add an index for a query you actually run, then inspect its plan with representative data. [Parameters and results](/docs/guides/parameters-and-results) describes returned rows, and the [performance guide](/docs/guides/performance) covers keeping large reads bounded. +A prepared statement keeps SQL compiled for execution. Parameters such as `?` keep values separate from that SQL, so the same statement can run with different values. Parameters represent values, not table or column names. See [SQLite's expression documentation](https://www.sqlite.org/lang_expr.html). ## Query spatial ranges -An R\*Tree index stores bounding boxes and finds those that overlap a query box. Use it for queries such as finding places inside a map viewport or events that overlap a time interval. A normal B-tree index orders values by its columns; an R\*Tree can narrow a search across several independent coordinate ranges. Bounding boxes can include candidates outside an object's exact geometry, so apply an exact check when your query requires one. See [SQLite's R\*Tree documentation](https://www.sqlite.org/rtree.html). - -Nitro SQLite's bundled build includes `rtree` and `rtree_i32` by default. You still create the virtual table and populate it explicitly. Enabling the module does not create indexes or change queries against ordinary tables. - -```ts -import { open } from 'react-native-nitro-sqlite' - -const db = open({ name: 'places.sqlite' }) - -await db.executeAsync(` - CREATE VIRTUAL TABLE IF NOT EXISTS place_bounds USING rtree( - id, - minLongitude, maxLongitude, - minLatitude, maxLatitude - ) -`) -await db.executeAsync( - 'INSERT OR REPLACE INTO place_bounds VALUES (?, ?, ?, ?, ?)', - [1, 16.36, 16.38, 48.2, 48.22], -) - -const { rows } = await db.executeAsync<{ id: number }>( - `SELECT id FROM place_bounds - WHERE minLongitude <= ? AND maxLongitude >= ? - AND minLatitude <= ? AND maxLatitude >= ?`, - [16.4, 16.35, 48.23, 48.19], -) -console.log(rows._array) // [{ id: 1 }] -db.close() +An R\*Tree virtual table indexes bounding boxes for spatial or interval searches. A normal B-tree index orders values by its columns; an R\*Tree narrows searches across several coordinate ranges. + +```sql +CREATE VIRTUAL TABLE place_bounds USING rtree( + id, + minLongitude, maxLongitude, + minLatitude, maxLatitude +); + +INSERT INTO place_bounds VALUES (1, 16.36, 16.38, 48.2, 48.22); + +SELECT id FROM place_bounds +WHERE minLongitude <= 16.4 AND maxLongitude >= 16.35 + AND minLatitude <= 48.23 AND maxLatitude >= 48.19; ``` -If you store the same objects in an ordinary table, keep their bounding boxes in sync through triggers or updates in the same transaction. R\*Tree data takes storage and adds work to writes. Its storage and index maintenance apply to the tables you create, rather than every database opened by an app that includes the module. +The query returns `1`. Bounding boxes can include points outside an object's exact geometry, so apply an exact check when needed. Keep boxes in sync with the source data through triggers or updates in the same transaction. -`rtree` stores coordinates as 32-bit floats and rounds lower bounds down and upper bounds up. Overlap queries can return extra candidates. Containment queries can miss an entry at an edge unless you expand the query box as described in [SQLite's roundoff guidance](https://www.sqlite.org/rtree.html#roundoff_error). Use `rtree_i32` for signed 32-bit integer coordinates. Both support one to five dimensions. +`rtree` rounds 32-bit floating-point bounds outward. Overlap queries can return extra candidates, while containment queries may need an expanded query box. `rtree_i32` uses signed 32-bit integer coordinates. Both support one to five dimensions. See [SQLite's R\*Tree documentation](https://www.sqlite.org/rtree.html#roundoff_error). -A write can restructure an active R\*Tree scan and fail with `SQLITE_LOCKED`. Nitro SQLite's execution methods, including prepared statement execution, read the complete result before returning, so a completed read does not leave that scan open. +Finish an R\*Tree scan before modifying its table; changes to the scan's index can fail with `SQLITE_LOCKED`. -For apps that do not need R\*Tree, set `nitroSQLite.enableRTree` to `false` in the app's `package.json` and rebuild the native app. See [Apple configuration](/docs/configuration/ios#rtree-support) or [Android configuration](/docs/configuration/android#rtree-support). Disabling it makes R\*Tree tables unavailable. Queries and schema migrations involving those tables can fail with `no such module: rtree`, including a rename of an ordinary table referenced by a view that also uses R\*Tree. System SQLite support depends on the operating system's build. +For Nitro SQLite, see [parameters and results](/docs/guides/parameters-and-results) to run queries, [performance](/docs/guides/performance) to bound database work, and [Configuration](/docs/configuration#rtree-support) for R\*Tree availability. diff --git a/docs/content/docs/concepts/tables-and-values.mdx b/docs/content/docs/concepts/tables-and-values.mdx index 617b7c23..a53b83a3 100644 --- a/docs/content/docs/concepts/tables-and-values.mdx +++ b/docs/content/docs/concepts/tables-and-values.mdx @@ -1,34 +1,30 @@ --- title: Tables and values -description: Define a SQLite schema and understand the values returned to JavaScript. +description: Rows, constraints, and SQLite storage classes. --- -A table organizes records into rows and named columns. Its schema describes those columns and can add constraints such as `PRIMARY KEY` and `NOT NULL`. SQLite has five storage classes for values: `NULL`, `INTEGER`, `REAL`, `TEXT`, and `BLOB`. In ordinary tables, a column's declared type gives it a *type affinity*, which influences conversion but does not generally enforce one storage class. See [SQLite's datatype guide](https://www.sqlite.org/datatype3.html). +A table stores records as rows with named columns. Its schema defines those columns and constraints such as `PRIMARY KEY` and `NOT NULL`. ## Create a table and read its rows -Nitro SQLite does not define tables through a separate schema API. Send SQL to the connection: +```sql +CREATE TABLE notes ( + id INTEGER PRIMARY KEY, + body TEXT NOT NULL +); -```ts -import { open } from 'react-native-nitro-sqlite' +INSERT INTO notes (body) VALUES ('Buy milk'); +SELECT id, body FROM notes; +``` -const db = open({ name: 'notes.sqlite' }) +The primary key identifies each row. `NOT NULL` prevents a row from storing a missing value in `body`. -await db.executeAsync(` - CREATE TABLE IF NOT EXISTS notes ( - id INTEGER PRIMARY KEY, - body TEXT NOT NULL - ) -`) -await db.executeAsync('INSERT INTO notes (body) VALUES (?)', ['Buy milk']) +## Storage classes and type affinity -const { rows } = await db.executeAsync<{ id: number; body: string }>( - 'SELECT id, body FROM notes', -) -console.log(rows._array) -db.close() -``` + + +SQLite values have five storage classes: `NULL`, `INTEGER`, `REAL`, `TEXT`, and `BLOB`. The storage class belongs to the value. In ordinary tables, a column's declared type gives it a _type affinity_, which influences conversion without generally enforcing one storage class. [STRICT tables](https://www.sqlite.org/stricttables.html) enforce column types. -## Values returned to JavaScript +SQLite stores booleans as the integers `0` and `1`. It has no separate date storage class; applications can represent dates as text or numbers. See [SQLite's datatype guide](https://www.sqlite.org/datatype3.html). -The `?` placeholder binds a value separately from the SQL text. The TypeScript row type describes what your code expects; it does not check the schema or convert returned values. SQLite integers and real numbers arrive as JavaScript numbers, blobs as `ArrayBuffer`, and nulls as `null`. SQLite has no separate Boolean storage class, so a bound boolean is stored as an integer and read back as a number. See [parameters and results](/docs/guides/parameters-and-results) for supported values and result shapes. +For binding values and reading JavaScript results in Nitro SQLite, see [parameters and results](/docs/guides/parameters-and-results). diff --git a/docs/content/docs/concepts/transactions.mdx b/docs/content/docs/concepts/transactions.mdx index acfa15ba..eeef37dd 100644 --- a/docs/content/docs/concepts/transactions.mdx +++ b/docs/content/docs/concepts/transactions.mdx @@ -1,26 +1,31 @@ --- title: Transactions and atomicity -description: Group related SQLite changes into one commit or rollback. +description: Group database changes into one commit or rollback. --- -A transaction groups statements into one unit of work. `COMMIT` makes its changes permanent; `ROLLBACK` discards them. Atomicity means a group of related changes can succeed together or be undone together. Without an explicit transaction, SQLite starts and finishes an implicit transaction around each statement. See [SQLite's transaction documentation](https://www.sqlite.org/lang_transaction.html). +A transaction groups statements into one unit of work. `COMMIT` keeps its changes; `ROLLBACK` discards them. Atomicity means related changes succeed together or are undone together. -## Run a callback transaction +## Commit or roll back changes -Nitro SQLite's `db.transaction()` starts a transaction and passes a `tx` object to an async callback. It commits when the callback resolves and rolls back when the callback throws. Assuming the `accounts` table and both account rows exist: + -```ts -import { open } from 'react-native-nitro-sqlite' +For example, assuming both account rows exist: -const db = open({ name: 'accounts.sqlite' }) - -await db.transaction(async (tx) => { - tx.execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [25, 1]) - tx.execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [25, 2]) -}) -db.close() +```sql +BEGIN; +UPDATE accounts SET balance = balance - 25 WHERE id = 1; +UPDATE accounts SET balance = balance + 25 WHERE id = 2; +COMMIT; ``` +Use `ROLLBACK` instead of `COMMIT` to discard both updates. Without an explicit transaction, SQLite starts transactions automatically and commits when the last active statement finishes. + ## Check application rules -This groups the two updates, but the example does not check whether both account IDs exist or whether the sender has enough funds. Add those application checks inside the callback, and throw if they fail. Use `tx.execute()` or `tx.executeAsync()` for all statements on this database during the callback. Await every async `tx` call before returning. For a fixed list of statements, `executeBatchAsync()` also runs them in a transaction. The [transactions guide](/docs/guides/transactions) covers callback behavior and pitfalls; the [batch guide](/docs/guides/batch-operations) covers fixed command lists. +Atomicity does not check whether the sender has enough funds. Check application rules within the transaction or enforce them through schema constraints before committing. + +## Transaction modes + +`BEGIN` defaults to deferred mode. It can start with a read and request write access later. `BEGIN IMMEDIATE` requests write access at the start. `BEGIN EXCLUSIVE` also blocks other readers in rollback journal mode; in WAL mode it behaves like `IMMEDIATE`. See [SQLite's transaction documentation](https://www.sqlite.org/lang_transaction.html). + +For visibility between connections, see [isolation and concurrency](/docs/concepts/isolation-and-concurrency). For Nitro SQLite's callback API and error handling, see [transactions](/docs/guides/transactions). diff --git a/docs/content/docs/guides/attach-databases.mdx b/docs/content/docs/guides/attach-databases.mdx index 915263ba..67824835 100644 --- a/docs/content/docs/guides/attach-databases.mdx +++ b/docs/content/docs/guides/attach-databases.mdx @@ -3,7 +3,7 @@ title: Attach databases description: Query another database file through a named SQLite schema. --- -SQLite's `ATTACH DATABASE` makes another database file available through an existing connection. You give it a schema name, then use that name to distinguish tables in the attached file from tables in the main database. This is useful when one query needs data from both files. +Use `db.attach()` to query another database file through an existing connection. Its alias becomes a SQLite schema name, which distinguishes its tables from those in the main database. Use it when one query needs data from both files. ## Attach and query a database @@ -30,4 +30,4 @@ db.close() The example assumes the tables and files already exist. SQLite can create the attached file when it is missing, so verify the intended path when opening prepopulated data. `attach()` and `detach()` are synchronous connection operations; they fail while the connection queue is busy. A failed attach or detach throws `NitroSQLiteError` through the connection wrapper. -The native implementation inserts the attached file path and alias into SQL. Use application-controlled database names, locations, and aliases rather than arbitrary user input. Query values still belong in `?` parameters. See [database lifecycle](/docs/guides/database-lifecycle) for the location convention and the generated [`NitroSQLiteConnection` reference](/api/react-native-nitro-sqlite/interfaces/NitroSQLiteConnection) for signatures. +Only pass database names, locations, and aliases you control. The library does not bind these as parameters. Query values still belong in `?` parameters. See [database lifecycle](/docs/guides/database-lifecycle) for the location convention and the generated [`NitroSQLiteConnection` reference](/api/react-native-nitro-sqlite/interfaces/NitroSQLiteConnection) for signatures. diff --git a/docs/content/docs/guides/batch-operations.mdx b/docs/content/docs/guides/batch-operations.mdx index 59201032..cc1bebba 100644 --- a/docs/content/docs/guides/batch-operations.mdx +++ b/docs/content/docs/guides/batch-operations.mdx @@ -3,11 +3,11 @@ title: Batch operations description: Execute a fixed list of statements in one native transaction. --- -When you need to make several related changes, a SQLite transaction can commit all of them together or roll them all back if one fails. Grouping a fixed list of statements also avoids making a separate JavaScript call for each write. +Use `db.executeBatch()` or `db.executeBatchAsync()` to run a fixed list of statements together. A transaction commits the whole list or rolls it back if a command fails. A batch also avoids a separate JavaScript call for each write. ## Run a batch -NitroSQLite's `executeBatch()` and `executeBatchAsync()` run a nonempty list of commands in one native exclusive transaction. If a command fails, the batch rolls back and the method throws or rejects. The result contains the sum of `rowsAffected`; it does not include SELECT rows. +Pass a nonempty list of commands. Each batch uses an exclusive transaction. A failure throws or rejects, and the result of a successful batch contains the sum of `rowsAffected`; it does not include SELECT rows. ```ts import type { BatchQueryCommand } from 'react-native-nitro-sqlite' diff --git a/docs/content/docs/guides/database-lifecycle.mdx b/docs/content/docs/guides/database-lifecycle.mdx index 103b1acd..585651e6 100644 --- a/docs/content/docs/guides/database-lifecycle.mdx +++ b/docs/content/docs/guides/database-lifecycle.mdx @@ -3,9 +3,11 @@ title: Database lifecycle description: Open, close, delete, and place database files in an app directory. --- -A SQLite database usually lives in a file. Your app opens a connection to read or change that file, then closes the connection when it is done. Closing a connection leaves the data on disk; deleting the file removes the database. +Use `open({ name })` to open an existing database or create a new one. Keep the returned connection for queries, then call `close()` when done. See [databases and connections](/docs/concepts/databases-and-connections) for the difference between the file and its open handles. -In NitroSQLite, `open({ name })` opens an existing SQLite file or creates a new one. A database name can have only one active default connection per native NitroSQLite root. Close it before opening another default connection with that name, or use `connection: 'independent'` for a separate handle to the same file. See [multiple connections](/docs/guides/multiple-connections). +## Open and close a connection + +Keep the returned connection while you need to query the database. Only one default connection can be open for a database name at a time. Close it before opening another default connection, or use `connection: 'independent'` for a separate connection to the same file. See [multiple connections](/docs/guides/multiple-connections). ```ts import { open } from 'react-native-nitro-sqlite' @@ -19,7 +21,9 @@ await db.executeAsync( db.close() ``` -`close()` releases the connection and its queue. It keeps the file. `delete()` removes the database file and closes that connection if it is open. Deletion fails while another connection still has the file open. You can call `delete()` on the connection after `close()`, using its remembered name and location. Do not use a closed or deleted connection for more queries. +## Delete a database file + +`delete()` removes the database file and closes that connection if it is open. Deletion fails while another connection still has the file open. You can call `delete()` after `close()`; the connection remembers its name and location. Do not use a closed or deleted connection for more queries. ```ts const temporary = open({ name: 'temporary.sqlite' }) @@ -31,24 +35,22 @@ Both methods are synchronous. They fail if an operation on that connection is qu On iOS, when a database is being moved from Documents to Application Support, deletion also cleans up copies and SQLite sidecar files from both locations. -## Runtime teardown - -SQLite connections are native resources. A JavaScript runtime can be destroyed while the app process stays alive, such as when an app switches between React Native hosts. Closing a connection preserves committed data on disk and rolls back any unfinished transaction. - -Each native NitroSQLite root owns its default and independent connections. When that root is destroyed, it closes its handles. A replacement runtime can open the same database names without restarting the process. An older root's cleanup does not close connections opened by a newer root. - -Queued operations and prepared statements keep their original connection. After its owner closes it, those operations fail instead of using a replacement connection with the same name. Await pending work and finalize prepared statements before an intentional runtime teardown when your app controls that transition. - ## File location and prepopulated databases -By default, the root is the app Documents directory on iOS, the app files directory on Android, and an app-specific Application Support directory on macOS. Use the optional `location` as a relative subdirectory under that root, not as an absolute file path: +Pass `location` to `open()` to select a subdirectory under the platform's database root. Use a relative directory, not an absolute file path: ```ts const db = open({ name: 'catalog.sqlite', location: 'databases' }) ``` -The native code joins the root, `location`, and `name` without checking for `..` path segments. Use application-controlled names and locations, not arbitrary user input. +Only pass database names and locations you control. The library does not validate path segments such as `..`. + +To open a prepopulated database, copy the SQLite file into the intended app directory before calling `open()`. Use the same `name` and `location` when you open it. A missing file creates a new, empty database, so verify the copy if your schema must already exist. See [file lifetimes](/docs/concepts/databases-and-connections#file-lifetimes) for WAL companion files. + +The platform pages describe the database roots and storage settings for [iOS](/docs/configuration/ios#database-location), [Android](/docs/configuration/android#database-location), and [macOS](/docs/configuration/macos). The generated [`NitroSQLiteConnection` reference](/api/react-native-nitro-sqlite/interfaces/NitroSQLiteConnection) lists the lifecycle methods. + +## Runtime teardown -To open a prepopulated database, copy the SQLite file into the intended app directory before calling `open()`. Use the same `name` and `location` when you open it. A missing file creates a new, empty database, so verify the copy if your schema must already exist. If you use WAL mode, account for the database's companion files when moving or backing up it. +A JavaScript runtime can be destroyed while the app process stays alive. Nitro SQLite closes that runtime's connections and rolls back unfinished transactions. A replacement runtime can reopen the same databases without restarting the process; the older runtime's cleanup leaves the new connections open. -iOS can use Application Support or an app group instead of Documents; see [iOS configuration](/docs/configuration/ios). Android uses its app files directory; see [Android configuration](/docs/configuration/android). See [macOS configuration](/docs/configuration/macos) for its Application Support location. The generated [`NitroSQLiteConnection` reference](/api/react-native-nitro-sqlite/interfaces/NitroSQLiteConnection) lists the lifecycle methods. +Operations and prepared statements from the old connection fail after it closes. They do not use a replacement connection with the same name. Await pending work and finalize prepared statements before an intentional runtime teardown when your app controls that transition. diff --git a/docs/content/docs/guides/encryption.mdx b/docs/content/docs/guides/encryption.mdx index 9ed26e27..82b63e77 100644 --- a/docs/content/docs/guides/encryption.mdx +++ b/docs/content/docs/guides/encryption.mdx @@ -3,7 +3,7 @@ title: Encrypt a database description: Build Nitro SQLite with licensed SQLite SEE and open a database with a key. --- -SQLite database files are readable without a key by default. The [SQLite Encryption Extension](https://sqlite.org/see) (SEE) encrypts database pages when your app builds against its licensed SQLite source. Nitro SQLite does not include SEE. You need licensed source matching the bundled SQLite version, a native rebuild, and a way to store and retrieve the key outside your app source. A JavaScript-only update cannot enable encryption in an existing app binary. +Pass `encryptionKey` to `open()` to use an encrypted database. This requires a native build with the licensed [SQLite Encryption Extension](https://sqlite.org/see), or SEE. Nitro SQLite does not include SEE; ordinary SQLite files remain readable without a key. You need licensed source matching the bundled SQLite version, a native rebuild, and a way to store and retrieve the key outside your app source. A JavaScript-only update cannot enable encryption in an existing app binary. ## Build with SEE diff --git a/docs/content/docs/guides/load-sql-file.mdx b/docs/content/docs/guides/load-sql-file.mdx index 8d83cbfa..4930859a 100644 --- a/docs/content/docs/guides/load-sql-file.mdx +++ b/docs/content/docs/guides/load-sql-file.mdx @@ -3,11 +3,11 @@ title: Load a SQL file description: Run one SQL statement per nonempty line from an app-accessible file. --- -A SQL file is a text file containing statements that set up or change a database. Apps often use one to create tables or insert initial data. Running those statements in a transaction keeps a failure from leaving only part of the file applied. +Use `db.loadFile(path)` or `await db.loadFileAsync(path)` to run a SQL text file that creates tables or inserts initial data. The import uses one transaction so a failed statement does not leave only part of the file applied. ## Load statements from a file -NitroSQLite's `loadFile(path)` and `loadFileAsync(path)` read an app-accessible SQL text file and execute its nonempty lines in a native exclusive transaction. The `path` is a file-system path to the SQL file, separate from the database `location` option. Copy bundled assets to an accessible file path first if your asset system does not expose one. +Pass an app-accessible file-system path to the SQL file. It is separate from the database's `location` option. Copy bundled assets to an accessible file path first if your asset system does not expose one. Each nonempty line runs as a statement in an exclusive transaction. ```ts const result = await db.loadFileAsync('/absolute/path/to/seed.sql') diff --git a/docs/content/docs/guides/multiple-connections.mdx b/docs/content/docs/guides/multiple-connections.mdx index e1199b15..d2b4c3e1 100644 --- a/docs/content/docs/guides/multiple-connections.mdx +++ b/docs/content/docs/guides/multiple-connections.mdx @@ -5,9 +5,11 @@ description: Open independent SQLite handles for reads and writes against one da `open({ name })` creates the default connection for a database name. Opening a second default connection with that name throws until the first closes. Use `connection: 'independent'` to open another native SQLite handle to the same file. Each returned connection has its own operation queue and transactions; closing one leaves the others open. +Independent connections require the shared [`threadSafe` setting](/docs/configuration#thread-safety) to remain enabled. + ## One writer and one reader -Create the database and finish migrations before opening a read-only connection. Enable [WAL mode](https://www.sqlite.org/wal.html) on the writer if reads should proceed while it writes: +Create the database and finish migrations before opening a read-only connection. Enable [WAL mode](/docs/concepts/isolation-and-concurrency#journal-modes) on the writer if reads should proceed while it writes: ```ts import { open } from 'react-native-nitro-sqlite' @@ -32,33 +34,13 @@ const [_, items] = await Promise.all([ ]) ``` -The read may finish before the insert commits. Await the write first if the read must include it. WAL mode persists in the file, while settings such as `busy_timeout` and `foreign_keys` belong to each connection. Nitro SQLite does not enable WAL or change SQLite's default busy timeout for you. A read-only connection requires an existing file and cannot write, delete the database, or attach another database. +The read may finish before the insert commits. Await the write first if the read must include it. Apply [connection settings](/docs/concepts/isolation-and-concurrency#connection-settings) to each handle. Nitro SQLite does not enable WAL or change SQLite's default busy timeout for you. A read-only connection requires an existing file and cannot write, delete the database, or attach another database. Keep a small number of connections and reuse them. Route writes, migrations, and imports through the writer; route independent reads through the reader. Nitro SQLite does not inspect SQL or choose a connection for each statement. Name-based `NitroSQLite` helpers address the default connection, so keep the object returned by `open()` for an independent connection. The TypeORM driver also uses the default connection. ## Ordering and transactions -Separate connections can run native work concurrently. SQLite still allows only one writer at a time for a database file, and lock contention can produce busy or locked errors. A bounded `busy_timeout` can help with temporary contention. Keep read transactions short, especially in WAL mode, where a long-lived reader can delay checkpoints. - -Each connection has its own queue. Native operations on one connection run in submission order, one at a time. Batches, transactions, and prepared statement executions wait for earlier work and reserve that connection until they finish. Use the callback's `tx` for every operation in a transaction, including reads that must see its uncommitted writes: - -```ts -await writer.transaction(async (tx) => { - await tx.executeAsync('UPDATE items SET value = ? WHERE id = ?', [ - 'edited', - 1, - ]) - const updated = await tx.executeAsync('SELECT * FROM items WHERE id = ?', [1]) - // updated sees this transaction's uncommitted change. -}) - -const committed = await reader.executeAsync( - 'SELECT * FROM items WHERE id = ?', - [1], -) -``` - -Do not await a queued operation on `writer` inside its transaction callback; it waits for the callback to finish. Work on `reader` has a separate transaction and cannot see the writer's uncommitted changes. See [connection ordering](/docs/concepts/connection-ordering) for the queue rules. +Each connection schedules its work independently. Use a transaction callback's `tx` methods for all of its statements. See [connection ordering](/docs/guides/sync-and-async#connection-ordering) and [transaction callbacks](/docs/guides/transactions#keep-operations-inside-the-callback) for Nitro SQLite's rules, and [isolation and concurrency](/docs/concepts/isolation-and-concurrency) for SQLite's reader and writer behavior. ## Close and delete @@ -70,4 +52,4 @@ writer.close() writer.delete() ``` -Open a new connection instead of reusing a closed object. Independent connections require SQLite mutex support and are rejected when SQLite was built with `SQLITE_THREADSAFE=0`. Keep iOS `nitroSQLite.threadSafe` enabled and do not disable SQLite thread safety through Android compile flags. Rebuild the native app after updating Nitro SQLite so it includes the new connection methods. +Open a new connection instead of reusing a closed object. See [database lifecycle](/docs/guides/database-lifecycle) for cleanup and file locations. diff --git a/docs/content/docs/guides/node-test-mock.mdx b/docs/content/docs/guides/node-test-mock.mdx index 89a29fa7..41a2acbb 100644 --- a/docs/content/docs/guides/node-test-mock.mdx +++ b/docs/content/docs/guides/node-test-mock.mdx @@ -3,9 +3,7 @@ title: Test SQLite in Node description: Test shared SQLite connections, statements, and transactions in Node. --- -SQLite connections are handles to a database file. Separate handles can share committed data, and a transaction can keep a consistent snapshot while another connection writes under [WAL](/docs/guides/multiple-connections). - -Use `react-native-nitro-sqlite/mock` to test these behaviors in Node without loading the React Native module. Install `better-sqlite3` as a development dependency in your test project. The mock creates databases in a temporary directory and supports queries, atomic batches, prepared statements, and callback transactions. +Import `react-native-nitro-sqlite/mock` to test SQLite connections in Node without loading the React Native module. Install `better-sqlite3` as a development dependency in your test project. The mock creates databases in a temporary directory and supports queries, atomic batches, prepared statements, and callback transactions. Separate connections share committed data and can keep consistent read snapshots under [WAL](/docs/guides/multiple-connections). ## Set up Jest diff --git a/docs/content/docs/guides/parameters-and-results.mdx b/docs/content/docs/guides/parameters-and-results.mdx index 9af90632..9bf62830 100644 --- a/docs/content/docs/guides/parameters-and-results.mdx +++ b/docs/content/docs/guides/parameters-and-results.mdx @@ -3,7 +3,7 @@ title: Parameters and results description: Bind SQLite values, inspect rows and metadata, and handle query failures. --- -SQLite parameters let you write SQL with placeholders and supply its values separately. SQLite binds each value to a placeholder when it runs the statement, which is useful for searches and writes that depend on user or app data. A query result contains the rows returned by a read, or information about the rows changed by a write. +Pass values as the second argument to `db.execute()` or `db.executeAsync()`, then read the returned `rows`, `rowsAffected`, and `insertId`. In NitroSQLite, pass positional values in an array for `?` placeholders. The public `SQLiteValue` type permits `boolean`, `number`, `string`, `ArrayBuffer`, `null`, and `undefined`. Both `null` and `undefined` bind as SQL NULL. Keep table and column names in your own SQL; parameters bind values, not identifiers. diff --git a/docs/content/docs/guides/performance.mdx b/docs/content/docs/guides/performance.mdx index ca579f8f..0927cd56 100644 --- a/docs/content/docs/guides/performance.mdx +++ b/docs/content/docs/guides/performance.mdx @@ -3,13 +3,11 @@ title: Performance description: Keep database work responsive and avoid unnecessary round trips. --- -SQLite chooses a plan for finding the rows each query needs. Without a useful index, it may have to examine many rows. An index can make filters and joins faster, but it takes storage and adds work to writes. `EXPLAIN QUERY PLAN` shows how SQLite intends to run a query, so inspect it before adding indexes blindly. - -In NitroSQLite, start with the query and the amount of data it reads. Add indexes for columns you filter or join on, inspect query plans with SQLite's `EXPLAIN QUERY PLAN`, and select only the columns and rows your screen needs. Their effect depends on your schema and data. +Use `executeAsync()` for longer queries, bind values as parameters, and fetch only the rows and columns your screen needs. See [queries and indexes](/docs/concepts/queries-and-indexes) for SQLite query plans and index tradeoffs. ## Keep long work off the JavaScript thread -`execute()` and the other synchronous methods block their JavaScript caller. Use `executeAsync()`, `executeBatchAsync()`, and `loadFileAsync()` for work that may take longer. The native async implementations run database work on background threads. A connection runs native operations one at a time in submission order. See [multiple connections](/docs/guides/multiple-connections) when reads need a separate handle. +Use `executeAsync()`, `executeBatchAsync()`, and `loadFileAsync()` for work that may take longer. See [sync and async](/docs/guides/sync-and-async) for choosing a call form and [multiple connections](/docs/guides/multiple-connections) when reads need a separate connection. ```ts const { rows } = await db.executeAsync<{ id: number; title: string }>( @@ -23,9 +21,9 @@ An async call still consumes device CPU and database I/O. Paginate large result ## Reuse SQL text -Before SQLite runs a statement, it compiles the SQL text into a program for its virtual machine. Compiling one statement is quick, but the cost adds up when the same statement runs many times, such as an `INSERT` in a loop. +Pass values as parameters to `execute()` or `executeAsync()` so repeated calls keep the same SQL text. Each connection reuses its 32 most recently used query and data-change statements. This avoids compiling the same SQL again. The cache covers statements starting with `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `REPLACE`, `WITH`, or `VALUES`; other statements, such as `PRAGMA`, `ATTACH`, and `BEGIN`, compile on each call. -Each connection keeps the compiled programs of its 32 most recently used queries and data changes: statements that start with `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `REPLACE`, `WITH`, or `VALUES`. When `execute()` or `executeAsync()` receives the same SQL text again, it reuses the program with the new parameters. Pass values as parameters rather than formatting them into the SQL so the text stays identical between calls. Other statements, such as `PRAGMA`, `ATTACH`, and `BEGIN`, compile on every call because SQLite can apply their effect while compiling them. Use [prepared statements](/docs/guides/prepared-statements) when you want to manage a compiled statement's lifetime yourself. +Use [prepared statements](/docs/guides/prepared-statements) when you need to control a statement's lifetime. ```ts for (const metric of metrics) { @@ -52,6 +50,8 @@ await db.executeBatchAsync([ ]) ``` -Binding values avoids building a new SQL string from user data. The batch implementation executes each expanded command separately. For repeated calls on one connection, [prepared statements](/docs/guides/prepared-statements) keep one SQL command compiled between executions. Measure on your target devices and data sizes before choosing a batch size, prepared statement, or index strategy. +Measure on your target devices and data sizes before choosing a batch size, prepared statement, or index strategy. See [sync and async](/docs/guides/sync-and-async), [batch operations](/docs/guides/batch-operations), and [transactions](/docs/guides/transactions) for the exact ordering rules. + +For SQLite build settings and their tradeoffs, see [Configuration](/docs/configuration#performance-mode). diff --git a/docs/content/docs/guides/prepared-statements.mdx b/docs/content/docs/guides/prepared-statements.mdx index 5d93e8c3..28b20b8d 100644 --- a/docs/content/docs/guides/prepared-statements.mdx +++ b/docs/content/docs/guides/prepared-statements.mdx @@ -3,9 +3,7 @@ title: Prepared statements description: Reuse a compiled SQL statement with different bound values. --- -A prepared statement compiles one SQL command once, then runs it with different values. Use one when the same query runs repeatedly on a connection. For a query that runs only once, `db.execute()` or `db.executeAsync()` is simpler. - -Call `db.prepare(query)` on an open connection. It returns a statement with `execute(params)`, `executeAsync(params)`, `isFinalized`, and `finalize()`. Preparation is synchronous, so it throws if the connection already has work pending or running. +Call `db.prepare(query)` to reuse SQL with different parameter values. It returns a statement with `execute(params)`, `executeAsync(params)`, `isFinalized`, and `finalize()`. Preparation is synchronous and throws if the connection has work pending or running. For a query that runs only once, use `db.execute()` or `db.executeAsync()`. See [parameters and prepared statements](/docs/concepts/queries-and-indexes#parameters-and-prepared-statements) for the SQLite concept. ```ts import { open } from 'react-native-nitro-sqlite' @@ -26,7 +24,7 @@ try { db.close() ``` -The SQL text stays fixed. Pass a positional array for its `?` placeholders on each execution. The accepted values are `boolean`, `number`, `string`, `ArrayBuffer`, `null`, and `undefined`. Both `null` and `undefined` bind as SQL NULL. Parameters bind values, not table or column names. Each run resets the statement and clears its previous bindings, so omitted values do not carry over. Pass every value your query needs each time. +Pass a positional array for the SQL's `?` placeholders on each execution. The [parameter types and binding rules](/docs/guides/parameters-and-results) match the connection's execution methods. Each run clears previous bindings, so omitted values do not carry over. Pass every value your query needs each time. ## Read rows and run asynchronously @@ -52,7 +50,7 @@ try { On a managed connection, `executeAsync()` reserves that connection's queue until it completes. It waits for earlier work and prevents later work from starting. While async work is pending, synchronous `prepare()`, `execute()`, `finalize()`, and `close()` on that connection throw a busy error. Await the last execution before finalizing, then finalize before closing the connection. `isFinalized` reports whether the statement has been finalized; executing it afterward throws. -Do not await a prepared statement's `executeAsync()` from inside `db.transaction()` on the same connection. It waits behind the transaction, while the transaction waits for your callback. Use the callback's `tx.execute()` or `tx.executeAsync()` instead. See [transactions](/docs/guides/transactions) and [connection ordering](/docs/concepts/connection-ordering). +Do not await a prepared statement's `executeAsync()` from inside `db.transaction()` on the same connection. It waits behind the transaction, while the transaction waits for your callback. Use the callback's `tx.execute()` or `tx.executeAsync()` instead. See [transactions](/docs/guides/transactions) and [connection ordering](/docs/guides/sync-and-async#connection-ordering). ## Choose the right query form diff --git a/docs/content/docs/guides/sync-and-async.mdx b/docs/content/docs/guides/sync-and-async.mdx index 42066435..46e96b6e 100644 --- a/docs/content/docs/guides/sync-and-async.mdx +++ b/docs/content/docs/guides/sync-and-async.mdx @@ -3,14 +3,15 @@ title: Sync and async description: Pick the query form that fits your workload and understand connection ordering. --- -SQLite work can take time, especially when a query reads many rows or a transaction writes many of them. A synchronous JavaScript call waits for that work to finish before the app can continue on the same thread. An asynchronous call lets JavaScript continue while the database work runs elsewhere. +Use `db.execute()` for a small query that needs an immediate result, and `await db.executeAsync()` for work that may take longer. A synchronous call blocks its JavaScript caller until SQLite finishes. An asynchronous call returns a promise and lets JavaScript continue while the database work runs in the background. -NitroSQLite's connection offers synchronous `execute`, `executeBatch`, and `loadFile` methods, with matching `Async` methods. A [prepared statement](/docs/guides/prepared-statements) also has `execute()` and `executeAsync()` for repeated SQL. Synchronous calls return before JavaScript continues, so a slow query blocks that JavaScript call. Asynchronous calls run the database work off the JavaScript thread and return a promise. +`executeBatch` and `loadFile` also have matching `Async` methods. A [prepared statement](/docs/guides/prepared-statements) has `execute()` and `executeAsync()` for repeated SQL. ```ts import { open } from 'react-native-nitro-sqlite' const db = open({ name: 'app.sqlite' }) +db.execute('CREATE TABLE IF NOT EXISTS notes (body TEXT NOT NULL)') const smallResult = db.execute('SELECT 1 AS value') const largerResult = await db.executeAsync('SELECT * FROM notes') @@ -20,9 +21,13 @@ console.log(smallResult.results, largerResult.results) Use synchronous calls when immediate results are useful and the work is small. Prefer async calls for queries that may scan many rows, batches, or file imports. [Performance guidance](/docs/guides/performance) covers other ways to keep work bounded. -An async query still delivers its rows to JavaScript, and only the JavaScript thread can turn them into objects. For a large result, `executeAsync()` hands rows over in batches while SQLite reads the rest, so building the objects overlaps with the query instead of starting after it. The promise still resolves once, with every row in order. +`executeAsync()` resolves once with the complete result in row order. Returning many rows still costs JavaScript work, so paginate large results. -An `open()` connection submits ordinary async statements in call order. Native operations on that connection run one at a time in submission order, but await a write before starting a read that depends on it so you can handle write errors. Batches and transactions wait for preceding statements and reserve the connection until complete. A synchronous operation, `close()`, or `delete()` throws a busy error if work is pending or active. Await async work before calling a synchronous method on the same connection: +## Connection ordering + + + +Calls on one `open()` connection run one at a time in submission order. Await a write before a read that depends on it so you can handle write errors. Batches, transactions, and prepared statement executions wait for preceding work and reserve the connection until complete. A synchronous operation, `close()`, or `delete()` throws a busy error if work is pending or active. Await async work before calling a synchronous method on that connection: ```ts await db.executeAsync('INSERT INTO notes (body) VALUES (?)', ['Queued write']) @@ -32,9 +37,11 @@ const total = db console.log(total) ``` -`db.transaction()` also occupies the connection's queue until its callback finishes. Inside that callback, use the provided `tx` methods. Awaiting `db.executeAsync()` or another queued operation on the same connection inside it leaves both operations waiting for each other. See [transactions](/docs/guides/transactions) and [multiple connections](/docs/guides/multiple-connections). + + +Inside `db.transaction()`, use the callback's `tx` methods. Calling and awaiting another async operation on the same `db` leaves it waiting for the callback to finish. See [transactions](/docs/guides/transactions#keep-operations-inside-the-callback) for that restriction and [multiple connections](/docs/guides/multiple-connections#ordering-and-transactions) for work that can run independently. -Prepared statement `executeAsync()` reserves the connection queue until it finishes. Await it before running a synchronous call or finalizing that statement. Inside a transaction callback, use `tx` methods instead of a prepared statement's async method on the same connection. +See [prepared statements](/docs/guides/prepared-statements#connection-ordering-and-cleanup) for statement cleanup. ## Global helpers and native access @@ -56,4 +63,4 @@ try { Native calls bypass the JavaScript queue. If you use them alongside an `open()` connection, coordinate access yourself. A native statement can run inside an active connection transaction without joining its callback's sequence. The raw result is a plain JavaScript object with `rowsAffected`, `insertId`, `results`, and `metadata`; ordinary garbage collection reclaims it. `NitroSQLite.native` itself is a Nitro hybrid object with a `dispose()` member. Do not dispose that shared instance during normal database cleanup; `dispose()` is not a database `close()` call. -The `NitroSQLite` export spreads the hybrid instance, but inherited native methods do not appear on that top-level object. Call them through `.native`. If you compile SQLite with `SQLITE_THREADSAFE=0`, also serialize access across separate database handles and native threads. See the [iOS](/docs/configuration/ios#thread-safety-and-performance-mode) and [Android](/docs/configuration/android#threading) configuration pages. +Call raw methods through `NitroSQLite.native`. Keep the shared [`threadSafe` setting](/docs/configuration#thread-safety) enabled when work can run across multiple database connections. diff --git a/docs/content/docs/guides/transactions.mdx b/docs/content/docs/guides/transactions.mdx index ff3a7196..a2395503 100644 --- a/docs/content/docs/guides/transactions.mdx +++ b/docs/content/docs/guides/transactions.mdx @@ -3,11 +3,13 @@ title: Transactions description: Commit or roll back related statements and avoid queue deadlocks. --- -A SQLite transaction groups related statements into one unit of work. If the transaction commits, SQLite keeps the changes. If it rolls back, SQLite discards them. For example, a transfer between accounts should not leave only one balance updated. +Use `db.transaction()` to commit or roll back related statements together. See [transactions and atomicity](/docs/concepts/transactions) for the SQLite behavior behind this API. ## Run a transaction -In NitroSQLite, use `db.transaction()` when several statements must succeed or fail together. It starts a SQLite transaction and passes a transaction object to an async callback. The callback's return value becomes the value of the transaction promise. +`db.transaction()` passes a `tx` object to an async callback. It commits when the callback resolves and rolls back when it throws. The callback's return value becomes the value of the transaction promise. This example assumes the tables and account rows already exist: + + ```ts const transferId = await db.transaction(async (tx) => { @@ -23,6 +25,8 @@ const transferId = await db.transaction(async (tx) => { }) ``` +Throw from the callback when an application check fails. Use `tx` for those checks as well as the updates. + ## Commit and rollback behavior When the callback resolves, the wrapper commits unless you already called `tx.commit()` or `tx.rollback()`. When it rejects or throws, the wrapper rolls back unless the transaction has already been finalized. An explicit `tx.rollback()` does not itself reject the callback: it ends the transaction, and the callback can still resolve. Calls to `tx.execute`, `tx.executeAsync`, `tx.commit`, or `tx.rollback` after finalization throw. @@ -33,7 +37,9 @@ If rollback also fails, the rejection preserves the original message and appends ## Keep operations inside the callback -The connection queue is occupied for the whole callback. Use `tx.execute` or `tx.executeAsync` for **every** statement on this database during that callback, including statements in helpers. + + +The connection queue is occupied for the whole callback. Use `tx.execute` or `tx.executeAsync` for every statement on this database during that callback, including statements in helpers. > [!TIP] > Pass `tx` to any helper that runs statements during the transaction. @@ -53,6 +59,6 @@ await db.transaction(async (tx) => { Await every `tx.executeAsync()` before the callback returns or before manually finalizing. While an async statement is pending, `tx.execute()`, `tx.commit()`, and `tx.rollback()` throw instead of running out of order. If the callback throws, the wrapper waits for pending async statements before rolling back. A thrown error after an explicit commit or rollback cannot undo that completed transaction. -`tx.commit()` and `tx.rollback()` are synchronous. Their declared return type exposes the native query result fields. The JavaScript helper also adds `rows` at runtime, but that field is not in their declared type. Use them to finalize the transaction, not to retrieve rows. The transaction starts with `BEGIN TRANSACTION`; the global [`NitroSQLite.transaction`](/api/react-native-nitro-sqlite/variables/NitroSQLite) helper also accepts `isExclusive` to use `BEGIN EXCLUSIVE TRANSACTION`. +`tx.commit()` and `tx.rollback()` are synchronous. Their declared return type exposes the native query result fields. The JavaScript helper also adds `rows` at runtime, but that field is not in their declared type. Use them to finalize the transaction, not to retrieve rows. The transaction starts with `BEGIN TRANSACTION`; the global [`NitroSQLite.transaction`](/api/react-native-nitro-sqlite/variables/NitroSQLite) helper also accepts `isExclusive` to use `BEGIN EXCLUSIVE TRANSACTION`. See [transaction modes](/docs/concepts/transactions#transaction-modes) for the SQLite differences. For a fixed list of statements and parameter sets, [batch operations](/docs/guides/batch-operations) need less callback code. See the generated [`Transaction` reference](/api/react-native-nitro-sqlite/interfaces/Transaction) for exact method types. diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index bedc4ef5..73e68616 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -80,4 +80,6 @@ Bind user supplied values as parameters rather than putting them into the SQL st ## Keep going -If SQLite is new to you, start with [databases and connections](/docs/concepts/databases-and-connections), [tables and values](/docs/concepts/tables-and-values), and [queries and indexes](/docs/concepts/queries-and-indexes). For work that may take longer, use `await db.executeAsync(...)` and read [sync and async](/docs/guides/sync-and-async) before mixing the two forms. Use [prepared statements](/docs/guides/prepared-statements) for repeated SQL, or [transactions](/docs/guides/transactions) and [batches](/docs/guides/batch-operations) for related writes. The [generated API reference](/api) lists the public exports and method signatures. +Start with [databases and connections](/docs/concepts/databases-and-connections) for SQLite fundamentals. The Concepts section covers data, queries, transactions, and isolation. + +For Nitro SQLite APIs, see [database lifecycle](/docs/guides/database-lifecycle), [sync and async](/docs/guides/sync-and-async), and [parameters and results](/docs/guides/parameters-and-results). The [generated API reference](/api) lists public exports and method signatures. diff --git a/docs/content/docs/integrations/typeorm.mdx b/docs/content/docs/integrations/typeorm.mdx index 335d47cf..1e572e32 100644 --- a/docs/content/docs/integrations/typeorm.mdx +++ b/docs/content/docs/integrations/typeorm.mdx @@ -3,9 +3,7 @@ title: TypeORM description: Use Nitro SQLite as the database driver for TypeORM's React Native DataSource. --- -TypeORM maps application entities to database tables and builds SQL for operations on those entities. Its `DataSource` needs a driver to open SQLite and run the generated queries in a React Native app. - -Nitro SQLite exports `typeORMDriver` for TypeORM's `react-native` database type. TypeORM calls the driver, which opens a regular Nitro SQLite connection and uses its async query method. Use [`open()`](/api/react-native-nitro-sqlite/functions/open) directly when you do not need TypeORM. +Pass `typeORMDriver` to a TypeORM `DataSource` with `type: 'react-native'` to use Nitro SQLite. TypeORM maps your entities to tables and runs their queries through the driver. Use [`open()`](/api/react-native-nitro-sqlite/functions/open) directly when you do not need TypeORM. ## Configure module resolution diff --git a/docs/content/docs/integrations/vector-search.mdx b/docs/content/docs/integrations/vector-search.mdx index 631c008a..e20aeb7c 100644 --- a/docs/content/docs/integrations/vector-search.mdx +++ b/docs/content/docs/integrations/vector-search.mdx @@ -1,19 +1,13 @@ --- title: Vector search with sqlite-vec -description: Enable the optional sqlite-vec native build and use its typed helpers. +description: Add vector search with the optional sqlite-vec package and its typed helpers. --- -Vector search compares numeric arrays, often called embeddings, to find nearby matches. SQLite's virtual table mechanism lets an extension provide a table with its own storage and search behavior. `sqlite-vec` uses this mechanism for nearest-neighbor queries. +Use `createVectorTable()` and `knnSearch()` from `react-native-nitro-sqlite-vec` to store vectors and find nearby matches. Vectors are numeric arrays, often called embeddings. The optional package adds sqlite-vec's `vec0` virtual tables to SQLite for those queries. -`react-native-nitro-sqlite-vec` is an optional companion package. Its native code is compiled into Nitro SQLite's library and registered with SQLite before a database opens. There is no runtime extension-loading step. +## Enable vector search -Install the companion alongside `react-native-nitro-sqlite`. Then enable the build on each platform you ship: - -- iOS: set `NITRO_SQLITE_VEC=1` when installing Pods. The companion pod compiles sqlite-vec and the core pod enables its registration code. -- macOS: set `NITRO_SQLITE_VEC=1` when installing Pods from the app's `macos` directory. It uses the same Apple build flag as iOS. -- Android: set `nitroSqliteVec=true` in `android/gradle.properties`. The core library compiles sqlite-vec's C sources and registration code. - -Rebuild the native app after changing a flag. The companion package must be installed for the flag to resolve its source files. See the [iOS](/docs/configuration/ios#vector-search), [macOS](/docs/configuration/macos), and [Android](/docs/configuration/android#vector-search) configuration pages for details. +Install `react-native-nitro-sqlite-vec` alongside `react-native-nitro-sqlite`. Enable it for each platform you ship using the [iOS](/docs/configuration/ios#vector-search), [macOS](/docs/configuration/macos#apple-build-options), or [Android](/docs/configuration/android#vector-search) configuration page, then rebuild the native app. Check availability with `isVecAvailable(db)` before running vector queries. A JavaScript update alone cannot enable the extension. ## Create and search a vector table diff --git a/docs/next.config.mjs b/docs/next.config.mjs index 9e09b0ad..0d27654e 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -13,6 +13,11 @@ const config = { destination: '/docs/configuration', permanent: true, }, + { + source: '/docs/concepts/connection-ordering', + destination: '/docs/concepts/isolation-and-concurrency', + permanent: true, + }, { source: '/docs/api', destination: '/api',