materialize-db
SQL-based state materializer for raw SQL projects
@rippledb/materialize-db
A SQL-based materializer that projects changes into domain tables. Works with db-sqlite and db-turso.
Installation
pnpm add @rippledb/materialize-dbnpm install @rippledb/materialize-dbyarn add @rippledb/materialize-dbWhat Is Materialization?
Materialization is the process of applying changes to your domain tables so you can query them efficiently.
Without materialization:
-- Expensive: replay all changes to compute current state
SELECT * FROM ripple_changes
WHERE entity = 'todos' AND entity_id = 'todo-1'
ORDER BY seq;
-- Then apply LWW in code...With materialization:
-- Fast: directly query current state
SELECT * FROM todos WHERE id = 'todo-1';Basic Usage
import { defineSchema, s } from "@rippledb/core";
import { SqliteDb } from "@rippledb/db-sqlite";
import { createSyncMaterializer } from "@rippledb/materialize-db";
const schema = defineSchema({
todos: { id: s.string(), title: s.string(), done: s.boolean() },
users: { id: s.string(), name: s.string(), email: s.string() },
});
const db = new SqliteDb({
filename: "./data.db",
schema,
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
dialect: "sqlite",
// Optional overrides:
tableMap: { todos: "todos", users: "users" },
fieldMap: {
todos: { id: "id", title: "title", done: "done" },
users: { id: "id", name: "name", email: "email" },
},
}),
});How It Works
- Change arrives via
db.append() - Load existing tags from
ripple_tagstable - Apply field-level LWW — compare HLC timestamps per field
- Update domain table with winning values
- Update tags table with new timestamps
All happens atomically in the same transaction.
Configuration
schema
Schema descriptor used for entity/field discovery.
dialect
Built-in SQL dialect. Currently supported:
'sqlite'— For SQLite and Turso
import { createSyncMaterializer } from "@rippledb/materialize-db";
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
dialect: "sqlite",
tableMap: { todos: "todos" },
fieldMap: { todos: { id: "id", title: "title", done: "done" } },
});tableMap?
Maps entity names (from your schema) to database table names. If omitted, entity names are used as-is.
const sqlConfig = {
dialect: "sqlite",
tableMap: {
todos: "todos", // Entity 'todos' → table 'todos'
users: "app_users", // Entity 'users' → table 'app_users'
},
} as const;
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
...sqlConfig,
});fieldMap?
Maps schema field names to database column names. If omitted, field names are used as-is.
const sqlConfig = {
dialect: "sqlite",
tableMap: { todos: "todos" },
fieldMap: {
todos: {
id: "id",
title: "title",
done: "is_done", // Schema field 'done' → column 'is_done'
},
},
} as const;
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
...sqlConfig,
});tagsTable?
Name of the table storing entity tags/metadata. Default: 'ripple_tags'
const sqlConfig = {
dialect: "sqlite",
tableMap: { todos: "todos" },
tagsTable: "my_ripple_tags",
} as const;
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
...sqlConfig,
});Tags Table Schema
The materializer automatically creates a ripple_tags table:
CREATE TABLE ripple_tags (
entity TEXT NOT NULL,
id TEXT NOT NULL,
data TEXT NOT NULL, -- JSON: current field values
tags TEXT NOT NULL, -- JSON: field → HLC timestamp
deleted INTEGER NOT NULL DEFAULT 0,
deleted_tag TEXT,
PRIMARY KEY (entity, id)
);Custom Commands
For databases not covered by built-in dialects, provide custom SQL commands:
import {
createSyncSqlExecutor,
createSyncMaterializer,
} from "@rippledb/materialize-db";
const sqlConfig = {
tableMap: { todos: "todos" },
loadCommand: (tagsTable) => `
SELECT data, tags, deleted, deleted_tag
FROM ${tagsTable}
WHERE entity = ? AND id = ?
`,
saveCommand: (tagsTable) => `
INSERT INTO ${tagsTable} (entity, id, data, tags, deleted, deleted_tag)
VALUES (?, ?, ?, ?, 0, NULL)
ON CONFLICT (entity, id) DO UPDATE SET
data = excluded.data,
tags = excluded.tags,
deleted = 0,
deleted_tag = NULL
`,
removeCommand: (tagsTable) => `
INSERT INTO ${tagsTable} (entity, id, data, tags, deleted, deleted_tag)
VALUES (?, ?, ?, ?, 1, ?)
ON CONFLICT (entity, id) DO UPDATE SET
data = excluded.data,
tags = excluded.tags,
deleted = 1,
deleted_tag = excluded.deleted_tag
`,
} as const;
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
executor: createSyncSqlExecutor(sqlConfig),
tableMap: sqlConfig.tableMap,
});Custom Executor
For full control, provide a custom executor:
materializer: ({ db, schema }) =>
createMaterializer({
schema,
db,
tableMap: { todos: "todos" },
fieldMap: { todos: { id: "id", title: "title", done: "done" } },
executor: {
ensureTagsTable: async (db) => {
// Create tags table if needed
},
loadTags: async (db, entity, id) => {
// Load and return TagsRow or null
},
saveTags: async (db, entity, id, dataJson, tagsJson) => {
// Upsert tags row
},
removeTags: async (db, entity, id, dataJson, tagsJson, deletedTag) => {
// Upsert tags row with deleted flag
},
saveEntity: async (db, tableName, id, columns, values, updates) => {
// Upsert domain table row
},
},
});Sync vs Async
For SQLite with better-sqlite3, use the sync variants:
// Sync adapter for better-sqlite3
materializer: ({ db, schema }) =>
createSyncMaterializer({
schema,
db,
dialect: "sqlite",
tagsTable: "ripple_tags",
tableMap: { todos: "todos" },
fieldMap: { todos: { id: "id", title: "title", done: "done" } },
});For async databases (like Turso), use createMaterializer:
import { createMaterializer } from "@rippledb/materialize-db";
materializer: ({ db, schema }) =>
createMaterializer({
schema,
db,
dialect: "sqlite",
tableMap: { todos: "todos" },
fieldMap: { todos: { id: "id", title: "title", done: "done" } },
});Domain Table Requirements
Your domain tables must:
- Have an
idcolumn (or whatever your schema uses as primary key) - Have columns for each field in
fieldMap - Already exist (the materializer doesn't create them)
CREATE TABLE todos (
id TEXT PRIMARY KEY,
title TEXT,
done INTEGER -- SQLite doesn't have boolean, use 0/1
);Related
- db-sqlite — SQLite database adapter
- db-turso — Turso database adapter
- materialize-drizzle — Type-safe alternative
- Core Concepts — Understand field-level LWW