mcpField
E
electric-schema-shapes

electric-schema-shapes

github.com/electric-sqlデータ・データベース10.4k54 回インストール3 週間前に更新
リポジトリ

electric-schema-shapes について

Electric と @electric-sql/client を使う開発者向けに、Shape 同期を想定したスキーマ設計や複数テーブルの扱い、楽観的書き込みを解説するスキルです。単一テーブルの Shape、クライアント側の結合、トランザクション内の txid 連携を扱います。

主な機能

  1. 01単一テーブル向けの Shape 設計
  2. 02複数 Shape のクライアント側結合
  3. 03レプリカモードの選択
  4. 04トランザクション内での txid 取得
  5. 05列挙型のフィルター処理

活用シーン

  1. 01Shape 同期向けのテーブル設計
  2. 02クライアント側での複数テーブル結合
  3. 03楽観的書き込みと同期結果の整合

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/electric-sql/electric --skill electric-schema-shapes

インストール後、Claude Code で/electric-schema-shapesを入力して読み込まれたか確認します。

詳細

このスキルは electric-shapes を基盤としています。ShapeStream の設定については、先にそちらをお読みください。

Electric — スキーマとシェイプ

セットアップ

各シェイプが同期するテーブルは1つなので、それを踏まえてテーブルを設計してください。テーブルをまたぐデータには、複数のシェイプを使い、クライアント側で結合してください。

-- Schema designed for Electric shapes
CREATE TABLE todos (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL,
  text TEXT NOT NULL,
  completed BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE todos REPLICA IDENTITY FULL;
import { ShapeStream } from '@electric-sql/client'

const todoStream = new ShapeStream({
  url: '/api/todos', // Proxy sets: table=todos, where=org_id=$1
})

コアパターン

複数のシェイプを使ったテーブル間のデータ
// Each shape syncs one table — join client-side
const todoStream = new ShapeStream({ url: '/api/todos' })
const userStream = new ShapeStream({ url: '/api/users' })

// With TanStack DB, use .join() in live queries:
// q.from({ todo: todoCollection })
//   .join({ user: userCollection }, ({ todo, user }) => eq(todo.userId, user.id))
レプリカモードの選択
// Default: only changed columns sent on update
const stream = new ShapeStream({ url: '/api/todos' })

// Full: all columns + old_value on updates (more bandwidth, needed for diffs)
const stream = new ShapeStream({
  url: '/api/todos',
  params: { replica: 'full' },
})
楽観的書き込みのためのバックエンド txid ハンドシェイク

変更と同じトランザクション内で pg_current_xact_id()::xid::text を呼び出してください。トランザクション外でクエリすると、異なる txid が取得されるため、クライアント側で調整されません。

// API endpoint — txid MUST be in the same transaction as the INSERT
app.post('/api/todos', async (req, res) => {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    const result = await client.query(
      'INSERT INTO todos (id, text, org_id) VALUES ($1, $2, $3) RETURNING id',
      [crypto.randomUUID(), req.body.text, req.body.orgId]
    )
    const txResult = await client.query(
      'SELECT pg_current_xact_id()::xid::text AS txid'
    )
    await client.query('COMMIT')
    // txid accepts number | bigint | `${bigint}`
    res.json({ id: result.rows[0].id, txid: parseInt(txResult.rows[0].txid) })
  } finally {
    client.release()
  }
})
// Client awaits txid before dropping optimistic state
await todoCollection.utils.awaitTxId(txid)

よくある間違い

HIGH 複数のテーブルにまたがるシェイプの設計

誤り:

const stream = new ShapeStream({
  url: '/api/data',
  params: {
    table: 'todos JOIN users ON todos.user_id = users.id',
  },
})

正しい例:

const todoStream = new ShapeStream({ url: '/api/todos' })
const userStream = new ShapeStream({ url: '/api/users' })

シェイプは単一テーブルのみを対象とします。テーブルをまたぐデータには、複数のシェイプを使い、TanStack DB のライブクエリでクライアント側から結合する必要があります。

出典:AGENTS.md:104-105

MEDIUM WHERE で enum カラムを使う際に text へキャストしない

誤り:

// Proxy route
originUrl.searchParams.set('where', "status IN ('active', 'done')")

正しい例:

originUrl.searchParams.set('where', "status::text IN ('active', 'done')")

WHERE 句で enum 型を使うには、明示的な ::text キャストが必要です。キャストしないと、クエリが失敗したり、予期しない結果を返したりする場合があります。

出典:packages/sync-service/lib/electric/replication/eval/env/known_functions.ex

HIGH 楽観的書き込みのための txid ハンドシェイクを設定しない

誤り:

// Backend: just INSERT, return id
app.post('/api/todos', async (req, res) => {
  const result = await db.query(
    'INSERT INTO todos (text) VALUES ($1) RETURNING id',
    [req.body.text]
  )
  res.json({ id: result.rows[0].id })
})

正しい例:

// Backend: INSERT and return txid in same transaction
app.post('/api/todos', async (req, res) => {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    const result = await client.query(
      'INSERT INTO todos (text) VALUES ($1) RETURNING id',
      [req.body.text]
    )
    const txResult = await client.query(
      'SELECT pg_current_xact_id()::xid::text AS txid'
    )
    await client.query('COMMIT')
    res.json({ id: result.rows[0].id, txid: parseInt(txResult.rows[0].txid) })
  } finally {
    client.release()
  }
})

txid がないと、Electric から同期済みのバージョンが届く前に楽観的な状態が破棄され、UI がちらつきます。クライアントは awaitTxId(txid) を使い、実際のデータが同期されるまで楽観的な状態を保持します。

出典:AGENTS.md:116-119

関連項目:electric-shapes/SKILL.md — シェイプは不変です。動的フィルターには、新しい ShapeStream インスタンスが必要です。 関連項目:electric-orm/SKILL.md — スキーマ設計はシェイプ(読み取り)と ORM クエリ(書き込み)の両方に影響します。

バージョン

@electric-sql/client v1.5.10 を対象としています。

コメント