db-sqlite
SQLite database adapter using better-sqlite3
@rippledb/db-sqlite
A production-ready SQLite adapter for RippleDB using better-sqlite3.
Installation
pnpm add @rippledb/db-sqlite better-sqlite3npm install @rippledb/db-sqlite better-sqlite3yarn add @rippledb/db-sqlite better-sqlite3Basic Usage
import { defineSchema, s } from "@rippledb/core";
import { SqliteDb } from "@rippledb/db-sqlite";
const schema = defineSchema({
todos: { id: s.string(), title: s.string(), done: s.boolean() },
});
const db = new SqliteDb({
filename: "./data.db",
schema,
});
// Append changes
await db.append({
stream: "user-123",
changes: [
/* ... */
],
});
// Pull changes
const { changes, nextCursor } = await db.pull({
stream: "user-123",
cursor: null,
});
// Close when done
db.close();With Materialization
To project changes into domain tables, add a materializer:
import { SqliteDb } from "@rippledb/db-sqlite";
import { defineSchema, s } from "@rippledb/core";
import { createSyncMaterializer } from "@rippledb/materialize-db";
const schema = defineSchema({
todos: { id: s.string(), title: s.string(), done: s.boolean() },
});
const db = new SqliteDb({
filename: "./data.db",
schema,
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
dialect: "sqlite",
tableMap: { todos: "todos" },
fieldMap: { todos: { id: "id", title: "title", done: "done" } },
}),
});See materialize-db for full configuration options.
Options
filename
Path to the SQLite database file. Use :memory: for an in-memory database.
new SqliteDb({ filename: "./data.db", schema });
new SqliteDb({ filename: ":memory:", schema });db
Alternatively, pass an existing better-sqlite3 database instance:
import Database from "better-sqlite3";
const sqlite = new Database("./data.db");
const db = new SqliteDb({ db: sqlite, schema });When using db, RippleDB will not close the connection when you call
db.close(). You're responsible for managing the connection lifecycle.
pragmas
SQLite pragmas to apply when creating the database. Only used with filename.
new SqliteDb({
filename: "./data.db",
schema,
pragmas: [
"journal_mode = WAL",
"synchronous = NORMAL",
"cache_size = -64000",
],
});Default: ['journal_mode = WAL']
schema
Schema descriptor used for entity/field discovery and type inference.
materializer
Factory function that returns a materializer adapter. Called with { db, schema }.
import { createSyncMaterializer } from "@rippledb/materialize-db";
new SqliteDb({
filename: "./data.db",
schema,
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
dialect: "sqlite",
tableMap: { todos: "todos" },
fieldMap: { todos: { id: "id", title: "title", done: "done" } },
}),
});Internal Tables
SqliteDb automatically creates these tables:
-- Change log
CREATE TABLE ripple_changes (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
stream TEXT NOT NULL,
change_json TEXT NOT NULL
);
-- Idempotency tracking
CREATE TABLE ripple_idempotency (
stream TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
last_seq INTEGER NOT NULL,
PRIMARY KEY (stream, idempotency_key)
);Using with Drizzle
You can use db-sqlite with Drizzle for your domain queries while RippleDB manages the sync:
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { SqliteDb } from "@rippledb/db-sqlite";
import { createDrizzleSyncMaterializer } from "@rippledb/materialize-drizzle";
// Create the underlying SQLite connection
const sqlite = new Database("./data.db");
// Create tables
sqlite.exec(`
CREATE TABLE IF NOT EXISTS todos (id TEXT PRIMARY KEY, title TEXT, done INTEGER);
CREATE TABLE IF NOT EXISTS ripple_tags (...);
`);
// Use Drizzle for queries
const drizzleDb = drizzle(sqlite);
// Use SqliteDb for RippleDB sync
const rippleDb = new SqliteDb({
db: sqlite, // Share the connection
schema,
materializer: ({ db, schema }) =>
createDrizzleSyncMaterializer({
schema,
tableMap: { todos: todosTable },
tagsTableDef: tagsTable,
getTableConfig,
// ...
}),
});
// Now you can use drizzleDb for queries and rippleDb for sync
const todos = drizzleDb.select().from(todosTable).all();Performance Tips
- Enable WAL mode (default): Improves concurrent read performance
- Use prepared statements: SqliteDb pre-prepares all statements
- Batch changes: Append multiple changes in one call to reduce transaction overhead
Type Safety
SqliteDb is generic over your schema:
import { defineSchema, s, type InferSchema } from "@rippledb/core";
const schema = defineSchema({
todos: { id: s.string(), title: s.string(), done: s.boolean() },
users: { id: s.string(), name: s.string() },
});
type MySchema = InferSchema<typeof schema>;
const db = new SqliteDb<MySchema>({ filename: "./data.db", schema });
// Changes are typed
await db.append({
stream: "user-123",
changes: [
{
entity: "todos", // Must be 'todos' | 'users'
entityId: "todo-1",
kind: "upsert",
patch: { title: "Buy milk" }, // Typed based on entity
tags: { title: hlc() },
hlc: hlc(),
},
],
});Related
- materialize-db — SQL-based materializer
- materialize-drizzle — Drizzle-based materializer
- Server Reference — Full Db interface