SQLite on iOS and Android through OP SQLite, plus named key-value stores that also work on web
One.Database opens SQLite databases on iOS and Android. It is OP SQLite,
which ships inside one: a C++ SQLite build reached through JSI, with
synchronous queries for small reads and asynchronous queries that run off the
JavaScript thread. Reach for it for local data with real queries: a message
cache, an offline queue, a sync engine’s store.
For a few named string values, One.Database.openKeyValue gives a small
store with the same API on every platform, web included. For one app-wide
store, see Storage; for secrets, see
Secure Store.
import { One } from 'one'
const db = One.Database.open({ name: 'app.sqlite' })db.executeSync('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL)')
async function addNote(body: string) { await db.execute('INSERT INTO notes (body) VALUES (?)', [body])}
async function listNotes() { const { rows } = await db.execute('SELECT id, body FROM notes ORDER BY id DESC') return rows}Open once at module scope and reuse the connection. Pass values as ?
parameters, which SQLite binds and escapes, never as text in the query.
Opening. open creates the file if it does not exist and returns the
connection at once. openAsync does the same and resolves the connection, so
code shared with other runtimes can await it. Without a location, the file
lives in the app’s Library folder on iOS and in the app’s databases folder on
Android, both private to the app and kept across launches and updates.
Threads. executeSync runs on the JavaScript thread and returns the
result directly, which suits small reads during render. execute,
executeBatch, and transaction hand the work to the connection’s own
thread pool and resolve when it finishes, so long queries never block the UI.
Key-value stores. openKeyValue({ name }) opens one-kv-<name>.sqlite
with one table of string keys and string values. Every call is synchronous:
setItem is an insert or update, getItem a lookup by primary key. Opening
the same name later reads what earlier connections wrote. On web each name is
a prefix in localStorage instead, with the same methods and the same
argument checks.
Web. open throws and openAsync rejects on web, since SQLite there is
not part of one. openKeyValue works.
| Method | Returns | Does |
|---|---|---|
| open(options) | A connection | Opens or creates a file |
| openAsync(options) | A promise of one | The same, awaited |
| openKeyValue(options) | A key-value store | Takes a name |
open and openAsync take OP SQLite’s options: name is the file name,
location a folder, readOnly blocks writes, and failOnCreate fails
instead of creating a missing file.
The connection’s main methods:
| Method | Runs on | Use it for |
|---|---|---|
| executeSync | JS thread | Small reads |
| execute | Its pool | Any query |
| executeBatch | Its pool | Many writes in one transaction |
| transaction | Its pool | Reads and writes that must all succeed |
| close | Ending the connection |
| Key-value store | Type | Description |
|---|---|---|
getItem(key: string) => string | nullThe value, or null when the key is missing. | (key: string) => string | null | The value, or null when the key is missing. |
setItem(key: string, value: string) => voidWrite or replace a value. | (key: string, value: string) => void | Write or replace a value. |
removeItem(key: string) => voidDelete one key. | (key: string) => void | Delete one key. |
getAllKeys() => string[]Every key, sorted. | () => string[] | Every key, sorted. |
clear() => voidDelete every key in this store. | () => void | Delete every key in this store. |
close() => voidEnd this connection. Later calls on it throw. | () => void | End this connection. Later calls on it throw. |
| iOS | Android | Web | |
|---|---|---|---|
| SQLite | OP SQLite | OP SQLite | Throws |
| Default folder | Library | Databases | |
| Key-value store | One file per name | One file per name | Local storage |
transaction or executeBatch
commits on its own, so a failure halfway leaves earlier rows written. Group
related writes so they land together.executeSync holds the JavaScript thread for
the whole query. Keep it to reads you need during render.setItem throws on anything else, so
call JSON.stringify and JSON.parse for objects.close throws. Open the
name again to get a new connection.localStorage, so it
shares that origin’s quota and is lost when people clear site data.import { One } from 'one'
const db = One.Database.open({ name: 'app.sqlite' })
async function saveContacts(contacts: { id: string; name: string }[]) { await db.transaction(async (tx) => { for (const contact of contacts) { await tx.execute('INSERT OR REPLACE INTO contacts (id, name) VALUES (?, ?)', [contact.id, contact.name]) } })}Either every contact is saved or none is: an error inside the callback rolls back the transaction.
import { useState } from 'react'import { TextInput } from 'react-native'import { One } from 'one'
const drafts = One.Database.openKeyValue({ name: 'drafts' })
function Composer({ threadId }: { threadId: string }) { const [text, setText] = useState(() => drafts.getItem(threadId) ?? '') return ( <TextInput value={text} onChangeText={(next) => { setText(next) drafts.setItem(threadId, next) }} /> )}Reads are synchronous, so the saved draft is there on the first render with
no loading state, and the same code keeps drafts in localStorage on web.
Edit this page on GitHub.