One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Database

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.

Usage

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.

How it works

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.

API reference

MethodReturnsDoes
open(options)A connectionOpens or creates a file
openAsync(options)A promise of oneThe same, awaited
openKeyValue(options)A key-value storeTakes 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:

MethodRuns onUse it for
executeSyncJS threadSmall reads
executeIts poolAny query
executeBatchIts poolMany writes in one transaction
transactionIts poolReads and writes that must all succeed
closeEnding the connection
Key-value storeTypeDescription
getItem(key: string) => string | nullThe value, or null when the key is missing.
(key: string) => string | nullThe value, or null when the key is missing.
setItem(key: string, value: string) => voidWrite or replace a value.
(key: string, value: string) => voidWrite or replace a value.
removeItem(key: string) => voidDelete one key.
(key: string) => voidDelete one key.
getAllKeys() => string[]Every key, sorted.
() => string[]Every key, sorted.
clear() => voidDelete every key in this store.
() => voidDelete every key in this store.
close() => voidEnd this connection. Later calls on it throw.
() => voidEnd this connection. Later calls on it throw.

Platform differences

iOSAndroidWeb
SQLiteOP SQLiteOP SQLiteThrows
Default folderLibraryDatabases
Key-value storeOne file per nameOne file per nameLocal storage

Limits and gotchas

Examples

Save several rows at once

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.

Remember a draft per screen

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.