PostgreSQLがブラウザで動く!?PGliteを試してみた

IT

PGliteとは

PGliteは、PostgreSQLをWebAssemblyバイナリとしてビルドしたTypeScript(JavaScript)のクライアントライブラリです。これを使用すると、ブラウザやNode.js上でPostgreSQLを動作させることができます。

最近では、ブラウザ内でAI機能付きのPostgreSQLを動作させる『database.build』というサービスも登場しました。これはPGliteを基盤としています。

この記事では、ブラウザ内で動作するPostgreSQLがどのようなものなのか、どのようなことができるのかを実際に試しながら解説します。

前提

以下の環境で動作確認を行っています。

oswindows11(4cpu、16GB)
PGlite0.2.17
typescript5.8.2
vue3.5.13
IDEVisual Studio Code

npm create vue@latest で作成したプロジェクトで検証しています。事前にお好みの方法で環境を用意してください。

基本的なDB操作

PGliteの公式ドキュメントを参考に進めていきます。

インストール

まず、PGliteをインストールします。

npm install @electric-sql/pglite

Viteを使用している場合は、vite.config.ts に以下の設定の追加が必要になります。

依存関係の最適化からpgliteを除外する

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

export default defineConfig({
  plugins: [vue(), vueDevTools()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  // 追加
  optimizeDeps: {
    exclude: ['@electric-sql/pglite'],
  },
})

in-memory Postgresを試す

まずは、データをメモリ内に保存するシンプルな例を試してみます。

<script setup lang="ts">
import { PGlite } from '@electric-sql/pglite'
import type { Results } from '@electric-sql/pglite'
import { onMounted, ref } from 'vue'

let db = new PGlite()

onMounted(async () => {
  await db.exec(`
    CREATE TABLE IF NOT EXISTS todo (
      id SERIAL PRIMARY KEY,
      task TEXT,
      done BOOLEAN DEFAULT false
    );
    INSERT INTO todo (task, done) VALUES ('Install PGlite from NPM', true);
    INSERT INTO todo (task, done) VALUES ('Load PGlite', true);
    INSERT INTO todo (task) VALUES ('Update a task');
  `)
})

type Todo = {
  id: number
  task: string
  done: boolean
}

const result = ref<Results<Todo>>({
  rows: [],
  affectedRows: 0,
  fields: [],
})
const getAsync = async () => {
  result.value = await db.query<Todo>('SELECT * FROM todo')
}

const task = ref('')
const addAsync = async () => {
  await db.exec(`INSERT INTO todo (task) VALUES ('${task.value}')`)
  await getAsync()
}
</script>

上記の例では以下を3つの処理を実施しています。

  • 初回ロード時に、テーブルを作成し、初期データを挿入
  • Getボタンを押下することで、データを検索
  • Addボタンを押下することで、データを追加

以下のHTMLでは初期データや追加したデータを表示します。

<template>
  <main>
    <div class="row">
      <label for="task" class="col-sm-1 col-form-label fw-bold">Task</label>
      <div class="col-sm-4">
        <input id="task" type="text" v-model="task" class="form-control" />
      </div>
      <div class="col-sm-1">
        <button class="btn btn-primary" @click="addAsync">Add</button>
      </div>
    </div>

    <div class="row mt-3">
      <div class="col-sm-2">
        <p class="fw-bold col-form-label">Todo List</p>
      </div>
      <div class="col-sm-1 offset-sm-3">
        <button class="btn btn-primary" @click="getAsync">Get</button>
      </div>
    </div>
    <ul>
      <li v-for="todo in result.rows" :key="todo.id">
        <input type="checkbox" v-model="todo.done" />
        {{ todo.task }}
      </li>
    </ul>
  </main>
</template>

データはメモリ内に保持されているため、ブラウザを更新したり閉じたりするとデータは消えてきまいます。しかし、ファイルにデータを保存する方法もあります。

In-Memoryデータの保存

保存するためには dumpDataDir メソッドを使って、データを tarball 形式で書き出し、それを保存できます。dbインスタンス作成時にtarballを指定することで保存したデータを読み込むことができます。

<script setup lang="ts">
(略)
const url = ref('')
const dumpAsync = async () => {
  const file = await db.dumpDataDir()
  url.value = URL.createObjectURL(file)

}

const blob = ref<File | null>(null)
const getFile = async () => {
  const fileInput = document.getElementById('file') as HTMLInputElement
  if (fileInput.files && fileInput.files.length > 0) {
    blob.value = fileInput.files[0]
    db = new PGlite({ loadDataDir: blob.value })
    await getAsync()
  }
}

</script>
<template>
  <main>
    (略)
    <div v-if="url !== ''">
      <a :href="url" download="pgdata.tar.gz">{{ url }}</a>
    </div>
    <div>
      <input type="file" id="file" v-on:change="getFile" />
    </div>
  </main>
</template>

Dumpボタンを押下すると、ダウンロード用のURLが画面に表示されるので、クリックしてデータを保存します。(拡張子はデフォルトで .tar.gz

保存したデータを読み込むと、追加した「o」タスクが確認できます。

保存したデータを展開すると以下のような形式となっています。

IndexedDBの利用

データを永続化するにはブラウザのIndexedDBを活用できます。IndexedDBを利用するためには、PGliteインスタンス作成時に「idb://{DB名}」を指定します。

const db = new PGlite('idb://testdb')

基本的な操作方法は In-Memory の場合と同じですが、「idb://{DB名} 」を指定することで自動的に IndexedDB に保存されます

初回ロード時にデータを挿入しているので、画面を更新するたびに3つのデータが重複して追加されてしまいますが、ロード直前に追加したtestタスクが保存されていることが分かります。

開発者ツールの「Application」からIndexedDBの内容を確認できます。

PGlite REPL

PGliteには、ブラウザ上でインタラクティブにSQLを実行できる REPL(Read-Eval-Print Loop) 機能もあります

インストール

npm install @electric-sql/pglite-repl

設定

main.tsに以下の設定を追加します。

app.config.compilerOptions.isCustomElement = (tag) => {
  return tag.startsWith('pglite--')
}

実装

以下のモジュールをインポートして、replコンポーネントを追加するだけで使用できます。

import '@electric-sql/pglite-repl/webcomponent'
<template>
  <main>
    (略)
    <div>
      <pglite-repl :pg="db" />
    </div>
  </main>
</template>

SQLを実行するだけでなく、インテリセンスもききます。以下は、IndexedDBに保存されたデータを確認した結果です。また、テーブル名を入力しようとするときに、候補が表示されていることも分かります。

最後に

PGliteを使うと、ブラウザやNode.js上でPostgreSQLを簡単に動作させることができます。

  • In-Memoryでのデータ操作
  • IndexedDBによるデータの永続化
  • ブラウザ上でSQLを実行できるREPL

設定も簡単ですぐに試すことができました。PGliteには他にも便利な機能があります。ReactやVueのプロジェクトでは専用のライブラリも用意されていますので、次回試してみたいと思います。

コメント