RippleDB
RippleDB
Adapters

materialize-drizzle

Type-safe materializer using Drizzle ORM schemas

@rippledb/materialize-drizzle

A type-safe materializer that uses Drizzle ORM table definitions. No raw SQL needed.

Installation

pnpm add @rippledb/materialize-drizzle drizzle-orm
npm install @rippledb/materialize-drizzle drizzle-orm
yarn add @rippledb/materialize-drizzle drizzle-orm

Why Use This?

Featurematerialize-dbmaterialize-drizzle
Type safetyNoYes
Raw SQLYesNo
Drizzle integrationManualNative
Multi-databaseVia dialectsVia Drizzle

Basic Usage (SQLite)

import { defineSchema, s } from "@rippledb/core";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { getTableConfig } from "drizzle-orm/sqlite-core";
import { SqliteDb } from "@rippledb/db-sqlite";
import { createDrizzleSyncMaterializer } from "@rippledb/materialize-drizzle";

const schema = defineSchema({
  todos: { id: s.string(), title: s.string(), done: s.boolean() },
});

// Define your domain table
const todosTable = sqliteTable("todos", {
  id: text("id").primaryKey(),
  title: text("title"),
  done: integer("done"),
});

// Define the tags table (required)
const tagsTable = sqliteTable("ripple_tags", {
  entity: text("entity").notNull(),
  id: text("id").notNull(),
  data: text("data").notNull(),
  tags: text("tags").notNull(),
  deleted: integer("deleted").notNull().default(0),
  deleted_tag: text("deleted_tag"),
});

const db = new SqliteDb<MySchema>({
  filename: "./data.db",
  schema,
  materializer: ({ db, schema }) => {
    const drizzleDb = drizzle(db);
    return createDrizzleSyncMaterializer({
      schema,
      tableMap: { todos: todosTable },
      tagsTableDef: tagsTable,
      getTableConfig,
    });
  },
});

Sync vs Async

Use the right function based on your database driver:

DriverFunction
better-sqlite3createDrizzleSyncMaterializer
pg, mysql2, @libsql/clientcreateDrizzleMaterializer
// Synchronous (SQLite with better-sqlite3)
import { createDrizzleSyncMaterializer } from "@rippledb/materialize-drizzle";

// Asynchronous (PostgreSQL, MySQL, Turso)
import { createDrizzleMaterializer } from "@rippledb/materialize-drizzle";

Configuration

schema

Schema descriptor used for field discovery and type inference.

tableMap

Maps entity names to Drizzle table definitions.

createDrizzleSyncMaterializer({
  schema,
  tableMap: {
    todos: todosTable,
    users: usersTable,
  },
  // ...
});

tagsTableDef

Drizzle table definition for the tags table. Required columns:

  • entity (text, not null)
  • id (text, not null)
  • data (text, not null)
  • tags (text, not null)
  • deleted (integer, not null, default 0)
  • deleted_tag (text, nullable)

Primary key: (entity, id)

getTableConfig

Import from your Drizzle dialect:

import { getTableConfig } from "drizzle-orm/sqlite-core";
import { getTableConfig } from "drizzle-orm/pg-core";
import { getTableConfig } from "drizzle-orm/mysql-core";

fieldMap

Optional mapping from schema fields to column names. If omitted, field names are used as-is.

createDrizzleSyncMaterializer({
  schema,
  // ...
  fieldMap: {
    todos: {
      id: "id",
      title: "title",
      done: "done",
    },
  },
});

Without fieldMap, only the ripple_tags table is updated. Add fieldMap to also write to domain tables.

normalizeValue

Transform values before writing to the database. Useful for SQLite boolean conversion:

createDrizzleSyncMaterializer({
  schema,
  // ...
  normalizeValue: (value, { tableName, columnName }) => {
    // Convert booleans to integers for SQLite
    if (typeof value === "boolean") {
      return value ? 1 : 0;
    }
    return value;
  },
});

ensureTagsTable

Hook to create the tags table if it doesn't exist:

createDrizzleSyncMaterializer({
  schema,
  // ...
  ensureTagsTable: () => {
    db.exec(`
      CREATE TABLE IF NOT EXISTS ripple_tags (
        entity TEXT NOT NULL,
        id TEXT NOT NULL,
        data TEXT NOT NULL,
        tags TEXT NOT NULL,
        deleted INTEGER NOT NULL DEFAULT 0,
        deleted_tag TEXT,
        PRIMARY KEY (entity, id)
      )
    `);
  },
});

Full Example (PostgreSQL)

import { Client } from "pg";
import { drizzle } from "drizzle-orm/node-postgres";
import { pgTable, text, integer, primaryKey } from "drizzle-orm/pg-core";
import { getTableConfig } from "drizzle-orm/pg-core";
import { createDrizzleMaterializer } from "@rippledb/materialize-drizzle";

const todosTable = pgTable("todos", {
  id: text("id").primaryKey(),
  title: text("title"),
  done: integer("done"),
});

const tagsTable = pgTable(
  "ripple_tags",
  {
    entity: text("entity").notNull(),
    id: text("id").notNull(),
    data: text("data").notNull(),
    tags: text("tags").notNull(),
    deleted: integer("deleted").notNull().default(0),
    deleted_tag: text("deleted_tag"),
  },
  (t) => ({
    pk: primaryKey({ columns: [t.entity, t.id] }),
  }),
);

const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
const drizzleDb = drizzle(client);

// Use with db-drizzle
const rippleDb = new DrizzleDb({
  db: drizzleDb,
  changesTable,
  idempotencyTable,
  getTableConfig,
  materializer: ({ schema }) =>
    createDrizzleMaterializer({
      schema,
      tableMap: { todos: todosTable },
      tagsTableDef: tagsTable,
      getTableConfig,
      fieldMap: { todos: { id: "id", title: "title", done: "done" } },
    }),
});

Using with db-sqlite

When using db-sqlite, you must use the sync version:

import { SqliteDb } from "@rippledb/db-sqlite";
import { createDrizzleSyncMaterializer } from "@rippledb/materialize-drizzle";

const db = new SqliteDb<MySchema>({
  filename: "./data.db",
  schema,
  materializer: ({ db, schema }) => {
    const drizzleDb = drizzle(db);
    return createDrizzleSyncMaterializer({
      schema,
      tableMap: { todos: todosTable },
      tagsTableDef: tagsTable,
      getTableConfig,
      fieldMap: { todos: { id: "id", title: "title", done: "done" } },
    });
  },
});

Using with db-drizzle

For the most integrated experience, use both db-drizzle and materialize-drizzle:

import { DrizzleDb } from "@rippledb/db-drizzle";
import { createDrizzleMaterializer } from "@rippledb/materialize-drizzle";

const db = new DrizzleDb<MySchema, typeof drizzleDb>({
  db: drizzleDb,
  changesTable,
  idempotencyTable,
  getTableConfig,
  isSync: true,
  schema,
  materializer: ({ schema }) =>
    createDrizzleMaterializer({
      schema,
      tableMap: { todos: todosTable },
      tagsTableDef: tagsTable,
      getTableConfig,
      fieldMap: { todos: { id: "id", title: "title", done: "done" } },
    }),
});

On this page