Skip to content
Contents

Embedded

Use Rindle as an embedded database

Coming soon. This page describes an API that is not in a release yet. It is here for review.

Rindle can be used as a standalone embedded database, for example as part of a client-side application.

The API is the same everywhere: migrate to change the schema, materialize a SELECT and subscribe to it, and write with plain SQL.

npm i @rindle/wasm
import { openDatabase } from "@rindle/wasm";

// Stored in OPFS. It survives reloads.
const db = await openDatabase("app.db");

await db.migrate("0001_init", `
  CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, closed INTEGER);
`);

type Issue = { id: number; title: string };
const open = db.materialize<Issue>(
  `SELECT id, title FROM issue WHERE closed = ? ORDER BY id DESC`, [0],
);

// Fires now, then after every commit that affects it.
open.subscribe((rows) => console.table(rows));

await db.sql.execute({ sql: `INSERT INTO issue (title, closed) VALUES (?, ?)`, args: ["first", 0] });

In React, useLiveQuery(db, sql, params) from @rindle/react does the subscribing for you.

npm i @rindle/replica
import { openDatabase } from "@rindle/replica";

// A SQLite file on disk.
const db = await openDatabase("app.db");

await db.migrate("0001_init", `
  CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, closed INTEGER);
`);

const open = db.materialize<{ id: number; title: string }>(
  `SELECT id, title FROM issue WHERE closed = ? ORDER BY id DESC`, [0],
);
open.subscribe((rows) => console.table(rows));

await db.sql.execute({ sql: `INSERT INTO issue (title, closed) VALUES (?, ?)`, args: ["first", 0] });
npm i @rindle/react-native @rindle/react
import { openDatabase } from "@rindle/react-native";
import { useLiveQuery } from "@rindle/react";

// A SQLite file in the app's documents directory.
const db = await openDatabase("app.db");

await db.migrate("0001_init", `
  CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, closed INTEGER);
`);

function Issues() {
  const { rows } = useLiveQuery<{ id: number; title: string }>(
    db, `SELECT id, title FROM issue WHERE closed = ? ORDER BY id DESC`, [0],
  );
  return rows.map((i) => <Text key={i.id}>{i.title}</Text>);
}

await db.sql.execute({ sql: `INSERT INTO issue (title, closed) VALUES (?, ?)`, args: ["first", 0] });

Add https://github.com/rocicorp/rindle as a Swift package.

import Rindle

let db = try Database.open(appSupport.appendingPathComponent("app.db"))

try db.migrate("0001_init", """
  CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, closed INTEGER);
""")

struct Issue: Decodable { let id: Int; let title: String }
let open = try db.materialize(Issue.self,
  "SELECT id, title FROM issue WHERE closed = ? ORDER BY id DESC", [0])

let sub = open.subscribe { rows in print(rows) }

try db.sql.execute("INSERT INTO issue (title, closed) VALUES (?, ?)", ["first", 0])

In SwiftUI, a LiveQuery is Observable: read open.rows in a view and it re-renders on commit.

// build.gradle.kts
implementation("sh.rindle:rindle-android:0.1.0")
import sh.rindle.Database

val db = Database.open(File(context.filesDir, "app.db"))

db.migrate("0001_init", """
  CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, closed INTEGER);
""")

data class Issue(val id: Long, val title: String)
val open = db.materialize<Issue>(
  "SELECT id, title FROM issue WHERE closed = ? ORDER BY id DESC", listOf(0))

open.subscribe { rows -> println(rows) }

db.sql.execute("INSERT INTO issue (title, closed) VALUES (?, ?)", listOf("first", 0))

In Compose, open.rows is a StateFlow: val rows by open.rows.collectAsState().

cargo add rindle
use rindle::Database;

let db = Database::open("app.db")?;

db.migrate("0001_init", "
  CREATE TABLE issue (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, closed INTEGER);
")?;

#[derive(serde::Deserialize, Debug)]
struct Issue { id: i64, title: String }

let open = db.materialize::<Issue>(
    "SELECT id, title FROM issue WHERE closed = ? ORDER BY id DESC", (0,))?;

open.subscribe(|rows| println!("{rows:?}"));

db.sql().execute("INSERT INTO issue (title, closed) VALUES (?, ?)", ("first", 0))?;

Any SQLite

Rindle is also a SQLite extension. Load it into any SQLite host and subscribe with SQL:

.load ./rindle

SELECT id, snapshot FROM rindle_subscribe(
  'SELECT id, title FROM issue WHERE closed = 0 ORDER BY id DESC');

INSERT INTO issue (title, closed) VALUES ('first', 0);

-- The changes since the last read, one row per commit.
SELECT seq, delta FROM rindle_changes(1);

Adding a Server

The same SQL runs against the Rindle server. When you need many clients on one database, start with Get Started.