0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PGLite+Drizzleを使ってSQLライクにIndexedDBのマイグレーションをしてみたメモ

Last updated at Posted at 2025-10-24

概要

サーバーレスで実現!ブラウザだけでベクトル検索で行っているのをみて、試してみた。

この時点のソースコード

環境

  • node v22.21.0
  • bun 1.3.1
  • drizzle-kit 0.31.5
  • drizzle-orm 0.44.6
  • electric-sql/pglite 0.3.11

モノレポ: 今回関係あるディレクトリは下記。

- apps
  - frontend # フロントエンド ( React )
- packages
  - rdb      # O/R Mappter & マイグレーション ( Drizzle )

手順

スキーマファイルの作成

packages/rdb/src/schema.ts
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';

export const scenariosTable = pgTable('scenarios', {
  id: uuid().primaryKey(),
  title: text('title').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at')
    .notNull()
    .$onUpdate(() => new Date()),
});

export type InsertScenario = typeof scenariosTable.$inferInsert;

マイグレーションファイルの作成

drizzle-kit generateの実行。package.jsonにgenerateというの名前のスクリプトを追加。

bun run generate

実行すると、下記ファイルが作成される

- migrations
  - 0000_dashing_iron_fist.sql
  - meta
    - _journal.json
    - 0000_snapshot.json

初期データ追加

今回はランダムデータのseedは使わず、固定値を直接作成されたマイグレーションファイルに書き込むことにする。

migrations/0000_dashing_iron_fist.sql
CREATE TABLE "scenarios" (
	"id" uuid PRIMARY KEY NOT NULL,
	"title" text NOT NULL,
	"created_at" timestamp DEFAULT now() NOT NULL,
	"updated_at" timestamp NOT NULL
);

+ INSERT INTO "scenarios" ("id", "title", "created_at", "updated_at")
+ VALUES ('3f81c321-1941-4247-9f5d-37bb6a9e8e45', 'サンプルシナリオ', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

フロントエンドで実行するため、マイグレーションファイルをJSONにバンドル

/packages/rdb/compile-migrations.mts
import { readMigrationFiles } from 'drizzle-orm/migrator';
import { writeFile } from 'node:fs/promises';

const migrations = readMigrationFiles({ migrationsFolder: './migrations' });

// 各マイグレーションのSQL文を分割(複数のSQL文が含まれる場合)
const processedMigrations = migrations.map((migration) => ({
  ...migration,
  sql: migration.sql.flatMap((sqlString) => {
    return sqlString
      .split(';')
      .map((stmt) => stmt.trim())
      .filter((stmt) => stmt.length > 0)
      .map((stmt) => stmt + ';');
  }),
}));

await writeFile('./src/db/migrations.json', JSON.stringify(processedMigrations));

bun run compile-migrations.mtsの実行。package.jsonにcompileというの名前のスクリプトを追加。

bun run compile

下記ファイルが作成される

src/db/migrations.json
[
  {
    "sql": [
        "CREATE TABLE \"scenarios\" (\n\t\"id\" uuid PRIMARY KEY NOT NULL,\n\t\"title\" text NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp NOT NULL\n);"
      , "INSERT INTO \"scenarios\" (\"id\", \"title\", \"created_at\", \"updated_at\")\nVALUES ('3f81c321-1941-4247-9f5d-37bb6a9e8e45', 'サンプルシナリオ', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);"
      ]
  , "bps":true
  , "folderMillis":1761239160699
  , "hash":"cf70d20619cdf022a3cd7069bef9abf9506bff4c24e75b83de0ff39ca2f2e945"
  }
]

実行用スクリプト作成

サーバーレスで実現!ブラウザだけでベクトル検索を参考に記載。

src/db/runMigrate.ts
import migrations from './migrations.json';
import { db } from './db';

export async function runMigrate() {
  // @ts-expect-error drizzleのdialectから直接呼ぶ
  await db.dialect.migrate(migrations, db.session, {
    migrationsTable: 'drizzle_migrations',
  });
}
src/index.ts
export * from './db/runMigrate';

フロントエンド埋め込み

/apps/frontend/src/main.tsx
+ import { runMigrate } from '@trpg-scenario-maker/rdb/db/runMigrate';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './app/App';

import './index.css';

+ await runMigrate();

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

データの取得

src/queries/scenario/index.ts
import { desc } from 'drizzle-orm';
import { db } from '../..';
import { type InsertScenario, scenariosTable } from '../../schema';

// Create
export async function createScenario(data: InsertScenario) {
  const [result] = await db.insert(scenariosTable).values(data).returning();
  return result;
}

// Read
export async function getScenarios() {
  return db
    .select({
      id: scenariosTable.id,
      title: scenariosTable.title,
      createdAt: scenariosTable.createdAt,
      updatedAt: scenariosTable.updatedAt,
    })
    .from(scenariosTable)
    .orderBy(desc(scenariosTable.updatedAt));
}

// Update
export async function updateScenario(id: string, data: { title: string }) {
  const [result] = await db
    .update(scenariosTable)
    .set({ title: data.title, updatedAt: new Date() })
    .where(eq(scenariosTable.id, id))
    .returning();
  return result;
}

// Delete
export async function deleteScenario(id: string) {
  await db.delete(scenariosTable).where(eq(scenariosTable.id, id));
}

// Count ( 生SQL )
export async function getScenarioCount() {
  const result = await db.execute<{ cnt: number }>('SELECT count(*) as cnt FROM scenarios');
  const [ret] = result.rows;
  return ret?.cnt ?? 0;
}

確認

image.png

初期データが表示できることを確認

WebWorker版 (2025/10/25 追記)

この時点のソースコード

┌─────────────────────────────────────────────┐
│  Frontend (Main Thread)                     │
│  ┌─────────────┐       ┌─────────────────┐  │
│  │ React UI    │◄─────►│ Redux Store     │  │
│  └─────────────┘       └────────┬────────┘  │
│                                 │           │
│                        ┌────────▼────────┐  │
│                        │ dbWorkerClient  │  │
│                        └────────┬────────┘  │
└─────────────────────────────────┼───────────┘
                                  │ postMessage
                         ┌────────▼────────┐
                         │  Web Worker     │
                         │  (db.worker.ts) │
                         └────────┬────────┘
                                  │
                         ┌────────▼────────┐
                         │  PGlite         │
                         │  (PostgreSQL)   │
                         └────────┬────────┘
                                  │
                         ┌────────▼────────┐
                         │  IndexedDB      │
                         └─────────────────┘

Typescript + WebWorker + Vite でビルドすると、 data:video/mp2t;base64 で期待しないビルド結果となったことに対応したメモ も参考。

参考

サーバーレスで実現!ブラウザだけでベクトル検索

PGLite
github-pglite

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?