@electric-sql/client
Version:
Postgres everywhere - your data, in sync, wherever you need it.
190 lines (146 loc) • 4.92 kB
Markdown
---
name: electric-orm
description: >
Use Electric with Drizzle ORM or Prisma for the write path. Covers getting
pg_current_xact_id() from ORM transactions using Drizzle tx.execute(sql)
and Prisma $queryRaw, running migrations that preserve REPLICA IDENTITY
FULL, and schema management patterns compatible with Electric shapes.
Load when using Drizzle or Prisma alongside Electric for writes.
type: composition
library: electric
library_version: '1.5.10'
requires:
- electric-shapes
- electric-schema-shapes
sources:
- 'electric-sql/electric:AGENTS.md'
- 'electric-sql/electric:website/docs/guides/troubleshooting.md'
---
This skill builds on electric-shapes and electric-schema-shapes. Read those first.
# Electric — ORM Integration
## Setup
### Drizzle ORM
```ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { sql } from 'drizzle-orm'
import { todos } from './schema'
const db = drizzle(pool)
// Write with txid for Electric reconciliation
async function createTodo(text: string, userId: string) {
return await db.transaction(async (tx) => {
const [row] = await tx
.insert(todos)
.values({
id: crypto.randomUUID(),
text,
userId,
})
.returning()
const [{ txid }] = await tx.execute<{ txid: string }>(
sql`SELECT pg_current_xact_id()::xid::text AS txid`
)
return { id: row.id, txid: parseInt(txid) }
})
}
```
### Prisma
```ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function createTodo(text: string, userId: string) {
return await prisma.$transaction(async (tx) => {
const todo = await tx.todo.create({
data: { id: crypto.randomUUID(), text, userId },
})
const [{ txid }] = await tx.$queryRaw<[{ txid: string }]>`
SELECT pg_current_xact_id()::xid::text AS txid
`
return { id: todo.id, txid: parseInt(txid) }
})
}
```
## Core Patterns
### Drizzle migration with REPLICA IDENTITY
```ts
// In migration file
import { sql } from 'drizzle-orm'
export async function up(db) {
await db.execute(sql`
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
text TEXT NOT NULL,
completed BOOLEAN DEFAULT false
)
`)
await db.execute(sql`ALTER TABLE todos REPLICA IDENTITY FULL`)
}
```
### Prisma migration with REPLICA IDENTITY
```sql
-- prisma/migrations/001_init/migration.sql
CREATE TABLE "todos" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"text" TEXT NOT NULL,
"completed" BOOLEAN DEFAULT false
);
ALTER TABLE "todos" REPLICA IDENTITY FULL;
```
### Collection onInsert with ORM
```ts
import { createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
export const todoCollection = createCollection(
electricCollectionOptions({
id: 'todos',
schema: todoSchema,
getKey: (row) => row.id,
shapeOptions: { url: '/api/todos' },
onInsert: async ({ transaction }) => {
const newTodo = transaction.mutations[0].modified
const { txid } = await createTodo(newTodo.text, newTodo.userId)
return { txid }
},
})
)
```
## Common Mistakes
### HIGH Not returning txid from ORM write operations
Wrong:
```ts
// Drizzle — no txid returned
const [todo] = await db.insert(todos).values({ text: 'New' }).returning()
return { id: todo.id }
```
Correct:
```ts
// Drizzle — txid in same transaction
const result = await db.transaction(async (tx) => {
const [row] = await tx.insert(todos).values({ text: 'New' }).returning()
const [{ txid }] = await tx.execute<{ txid: string }>(
sql`SELECT pg_current_xact_id()::xid::text AS txid`
)
return { id: row.id, txid: parseInt(txid) }
})
```
ORMs do not return `pg_current_xact_id()` by default. Add a raw SQL query for txid within the same transaction. Without it, optimistic state may drop before the synced version arrives, causing UI flicker.
Source: `AGENTS.md:116-119`
### MEDIUM Running migrations that drop replica identity
Wrong:
```ts
// ORM migration recreates table without REPLICA IDENTITY
await db.execute(sql`DROP TABLE todos`)
await db.execute(sql`CREATE TABLE todos (...)`)
// Missing: ALTER TABLE todos REPLICA IDENTITY FULL
```
Correct:
```ts
await db.execute(sql`DROP TABLE todos`)
await db.execute(sql`CREATE TABLE todos (...)`)
await db.execute(sql`ALTER TABLE todos REPLICA IDENTITY FULL`)
```
Some migration tools reset table properties. Always ensure `REPLICA IDENTITY FULL` is set after table recreation. Without it, Electric cannot stream updates and deletes correctly.
Source: `website/docs/guides/troubleshooting.md:373`
See also: electric-new-feature/SKILL.md — Full write-path journey including txid handshake.
See also: electric-schema-shapes/SKILL.md — Schema design affects both shapes and ORM queries.
## Version
Targets @electric-sql/client v1.5.10.