@aiquants/duckdb-helper 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ # @aiquants/duckdb-helper 開発ガイド
2
+
3
+ このガイドは、`@aiquants/duckdb-helper` パッケージの開発・保守・改修を行う開発者向けです。
4
+
5
+ ## 1. 開発環境のセットアップ
6
+
7
+ ### 前提条件
8
+
9
+ | ツール | バージョン |
10
+ |--------|-----------|
11
+ | Node.js | >= 16.0.0 |
12
+ | pnpm | >= 8.0.0 |
13
+
14
+ ### 初回セットアップ
15
+
16
+ ```bash
17
+ # リポジトリルートで依存をインストール
18
+ pnpm install
19
+
20
+ # パッケージディレクトリに移動
21
+ cd packages/duckdb-helper
22
+
23
+ # ビルド確認
24
+ pnpm run build
25
+
26
+ # 型チェック
27
+ pnpm run type-check
28
+ ```
29
+
30
+ ## 2. 開発ワークフロー
31
+
32
+ ### 日常的なコマンド
33
+
34
+ | やりたいこと | コマンド |
35
+ |-------------|---------|
36
+ | ビルド | `pnpm run build` |
37
+ | ウォッチモードで開発 | `pnpm run dev` |
38
+ | 型チェック | `pnpm run type-check` |
39
+ | Lint | `pnpm run lint` |
40
+ | Lint の自動修正 | `pnpm run lint:fix` |
41
+ | フォーマット | `pnpm run format` |
42
+ | フォーマットの自動修正 | `pnpm run format:fix` |
43
+ | Lint + フォーマット | `pnpm run check` |
44
+ | Lint + フォーマットの自動修正 | `pnpm run check:fix` |
45
+ | テスト | `pnpm run test` |
46
+ | テスト(カバレッジ付き) | `pnpm run test:coverage` |
47
+ | ライセンスチェック | `pnpm run license-check` |
48
+
49
+ ### 開発中の推奨フロー
50
+
51
+ ```bash
52
+ # ターミナル1: ウォッチモードでビルド
53
+ pnpm run dev
54
+
55
+ # ターミナル2: 必要に応じてテスト
56
+ pnpm run test
57
+
58
+ # コミット前に
59
+ pnpm run check:fix && pnpm run type-check
60
+ ```
61
+
62
+ ## 3. ディレクトリ構成と役割
63
+
64
+ ```
65
+ src/
66
+ ├── index.ts # エントリーポイント
67
+ │ # → 公開 API をここで管理する
68
+ │ # → 新しいエクスポートを追加する際はここに追記
69
+ │
70
+ ├── hooks/
71
+ │ └── useDuckDB.ts # React Hooks
72
+ │ # → React 固有のロジックはここに書く
73
+ │ # → サービス層の薄いラッパー
74
+ │
75
+ ├── services/
76
+ │ └── duckdb.ts # DuckDBService(コアロジック)
77
+ │ # → DuckDB の初期化・クエリ実行など
78
+ │ # → React に依存しない純粋なサービス
79
+ │
80
+ ├── types/
81
+ │ └── duckdb.ts # 型定義
82
+ │ # → インターフェースと型エイリアス
83
+ │
84
+ └── utils/
85
+ ├── duckdb-helpers.ts # ヘルパー関数
86
+ │ # → クエリ結果の変換・検証ユーティリティ
87
+ │
88
+ └── simple-logger.ts # ロガー
89
+ # → ライブラリ内部のログ管理
90
+ ```
91
+
92
+ ### ファイルの責務境界
93
+
94
+ | 変更したい内容 | 編集するファイル |
95
+ |---------------|----------------|
96
+ | DuckDB の初期化方法を変えたい | `services/duckdb.ts` の `internalInitializeWorker()` |
97
+ | 新しい DuckDB 拡張を追加したい | `services/duckdb.ts` の `internalInitializeWorker()` |
98
+ | クエリ実行のタイムアウトを変更したい | `services/duckdb.ts` の `executeQuery()` 内の `TIMEOUT_MS` |
99
+ | 型推定ロジックを改善したい | `services/duckdb.ts` の `inferColumnTypes()` |
100
+ | React Hook の API を変えたい | `hooks/useDuckDB.ts` |
101
+ | 新しいヘルパー関数を追加したい | `utils/duckdb-helpers.ts` + `index.ts` にエクスポート追加 |
102
+ | ログレベルのデフォルトを変えたい | `utils/simple-logger.ts` の static instance |
103
+ | 公開 API を追加したい | `index.ts` にエクスポートを追記 |
104
+
105
+ ## 4. コードの読み方ガイド
106
+
107
+ ### DuckDBService を理解する順序
108
+
109
+ 1. **`getStatus()`** を読む → ステータスの4状態を理解する
110
+ 2. **`initialize()`** を読む → 重複防止とタイムアウトの仕組みを理解する
111
+ 3. **`internalInitializeWorker()`** を読む → 実際の WASM 初期化の流れを理解する
112
+ 4. **`getConnection()`** を読む → コネクションの遅延初期化を理解する
113
+ 5. **`executeQuery()`** を読む → クエリ実行とタイムアウトを理解する
114
+ 6. **`createTableFromData()`** を読む → 型推定とバッチ INSERT を理解する
115
+
116
+ ### useDuckDB を理解する順序
117
+
118
+ 1. **`UseDuckDBResult` インターフェース** を読む → 戻り値の全体像を把握する
119
+ 2. **`useDuckDB` 関数** の useState/useEffect を読む → ステータス同期の仕組みを理解する
120
+ 3. **各操作関数(executeQuery 等)** を読む → すべて `useCallback` でメモ化されていることを確認する
121
+ 4. **`useDuckDBQuery`** を読む → `useDuckDB` の上に構築されたシンプルなフック
122
+
123
+ ## 5. 新機能の追加手順
124
+
125
+ ### 例: 新しいヘルパー関数を追加する
126
+
127
+ **ステップ1**: `src/utils/duckdb-helpers.ts` に関数を追加
128
+
129
+ ```typescript
130
+ /**
131
+ * Get first N rows from DuckDB result
132
+ * DuckDB 結果から最初の N 行を取得します
133
+ */
134
+ export const getFirstRows = <T>(result: unknown, count: number): T[] => {
135
+ const rows = duckdbTableToArray<T>(result)
136
+ return rows.slice(0, count)
137
+ }
138
+ ```
139
+
140
+ **ステップ2**: `src/index.ts` にエクスポートを追加
141
+
142
+ ```typescript
143
+ export {
144
+ duckdbTableToArray,
145
+ getDuckDBColumnCount,
146
+ getDuckDBRowCount,
147
+ isDuckDBTable,
148
+ getFirstRows, // ← 追加
149
+ } from "./utils/duckdb-helpers"
150
+ ```
151
+
152
+ **ステップ3**: テストを追加
153
+
154
+ ```typescript
155
+ // tests/unit/duckdb-helpers.test.ts
156
+ import { describe, expect, it } from "vitest"
157
+ import { getFirstRows } from "../../src/utils/duckdb-helpers"
158
+
159
+ describe("getFirstRows", () => {
160
+ it("should return first N rows", () => {
161
+ const mockResult = {
162
+ toArray: () => [{ id: 1 }, { id: 2 }, { id: 3 }],
163
+ numRows: 3,
164
+ numCols: 1,
165
+ }
166
+ expect(getFirstRows(mockResult, 2)).toEqual([{ id: 1 }, { id: 2 }])
167
+ })
168
+ })
169
+ ```
170
+
171
+ **ステップ4**: ビルド・テスト・型チェック
172
+
173
+ ```bash
174
+ pnpm run type-check && pnpm run test && pnpm run build
175
+ ```
176
+
177
+ ## 6. テストのガイドライン
178
+
179
+ ### テストフレームワーク
180
+
181
+ - **Vitest** を使用
182
+ - 設定は `vitest.config.ts`(またはデフォルト設定)
183
+
184
+ ### テストの分類
185
+
186
+ | ディレクトリ | 種類 | 説明 |
187
+ |-------------|------|------|
188
+ | `tests/unit/` | 単体テスト | 個々の関数・クラスのテスト |
189
+ | `tests/integration/` | 統合テスト | 複数モジュールの連携テスト |
190
+
191
+ ### テスト時の注意
192
+
193
+ - DuckDB-WASM はブラウザ環境が必要なため、Service のフルテストはブラウザテストランナーが必要
194
+ - ユーティリティ関数(`duckdb-helpers.ts`)は Node.js 環境でテスト可能
195
+ - React Hooks のテストには `@testing-library/react` の使用を推奨
196
+
197
+ ## 7. リリース手順
198
+
199
+ ### バージョンアップ
200
+
201
+ ```bash
202
+ # パッチ版 (1.4.1 → 1.4.2): バグ修正
203
+ pnpm run publish:patch
204
+
205
+ # マイナー版 (1.4.1 → 1.5.0): 機能追加(後方互換あり)
206
+ pnpm run publish:minor
207
+
208
+ # メジャー版 (1.4.1 → 2.0.0): 破壊的変更
209
+ pnpm run publish:major
210
+ ```
211
+
212
+ ### リリース前チェックリスト
213
+
214
+ - [ ] 型チェックが通ること(`pnpm run type-check`)
215
+ - [ ] テストが通ること(`pnpm run test`)
216
+ - [ ] Lint エラーがないこと(`pnpm run check`)
217
+ - [ ] ビルドが成功すること(`pnpm run build`)
218
+ - [ ] ライセンスチェックが通ること(`pnpm run license-check`)
219
+ - [ ] `index.ts` のエクスポートが正しいこと
220
+ - [ ] 破壊的変更がある場合はメジャーバージョンアップ
221
+
222
+ ### `prepublishOnly` スクリプト
223
+
224
+ `pnpm publish` 実行時に自動で以下が実行される:
225
+
226
+ ```text
227
+ clean → typecheck → test(--if-present) → build
228
+ ```
229
+
230
+ `test` は `--if-present` フラグ付きで実行されるため、テストスクリプトが定義されていない場合はスキップされる。それ以外のステップが失敗すると公開は中断される。
231
+
232
+ ## 8. アーキテクチャ上の注意点
233
+
234
+ ### シングルトンの影響
235
+
236
+ `DuckDBService` はシングルトンなので:
237
+
238
+ - アプリ全体で1つの DuckDB インスタンスを共有する
239
+ - テスト間で状態が残る可能性がある → `DuckDBService.resetInstance()` でリセット
240
+ - SSR 環境では Web Worker が使えないため初期化に失敗する
241
+
242
+ ### コネクションの共有
243
+
244
+ - `getConnection()` は1つのコネクションを返す
245
+ - 複数のコンポーネントが同時にクエリを実行しても、内部では同一コネクションが使われる
246
+ - DuckDB-WASM は単一コネクションでの逐次実行を前提としている
247
+
248
+ ### メモリ管理
249
+
250
+ - WASM インスタンスはメモリを消費する
251
+ - 不要になったら `cleanup()` でリソースを解放する
252
+ - テーブルデータは DuckDB の内部メモリに保持される
253
+
254
+ ## 9. よくある質問
255
+
256
+ ### Q: 新しい DuckDB 拡張を追加するには?
257
+
258
+ `services/duckdb.ts` の `internalInitializeWorker()` 内、splink_udfs のインストール箇所の後に追加する:
259
+
260
+ ```typescript
261
+ try {
262
+ await connection.query("INSTALL 拡張名 FROM community;")
263
+ await connection.query("LOAD 拡張名;")
264
+ Logger.info("✅ 拡張名 loaded")
265
+ } catch (error) {
266
+ Logger.warn("⚠️ Failed to load 拡張名:", error)
267
+ }
268
+ ```
269
+
270
+ ### Q: Logger を公開 API にしたい
271
+
272
+ `src/index.ts` にエクスポートを追加する:
273
+
274
+ ```typescript
275
+ export { Logger, LogLevel } from "./utils/simple-logger"
276
+ export type { ILogger } from "./utils/simple-logger"
277
+ ```
278
+
279
+ ### Q: React なしで使いたい
280
+
281
+ `DuckDBService` は React に依存していないので、そのまま使える:
282
+
283
+ ```typescript
284
+ import { DuckDBService } from '@aiquants/duckdb-helper'
285
+
286
+ const service = DuckDBService.getInstance()
287
+ await service.initialize()
288
+ const result = await service.executeQuery('SELECT 1')
289
+ ```
290
+
291
+ React はオプショナルなピア依存なので、インストールしなくてもパッケージの利用は可能。
@@ -0,0 +1,278 @@
1
+ # @aiquants/duckdb-helper 入門ガイド
2
+
3
+ このガイドは、`@aiquants/duckdb-helper` を初めて使う開発者向けのステップバイステップの導入手順書です。
4
+
5
+ ## 前提知識
6
+
7
+ - TypeScript / JavaScript の基本
8
+ - React の基本(Hooks の概念)
9
+ - SQL の基本構文
10
+
11
+ ## 1. DuckDB とは
12
+
13
+ DuckDB は高速な列指向の分析用データベースエンジンです。通常はサーバーで動作しますが、**DuckDB-WASM** を使うとブラウザ上で直接 SQL を実行できます。
14
+
15
+ このライブラリは、DuckDB-WASM のセットアップと利用を簡単にするためのヘルパーです。
16
+
17
+ ## 2. インストール
18
+
19
+ ```bash
20
+ # pnpm(推奨)
21
+ pnpm add @aiquants/duckdb-helper
22
+
23
+ # npm
24
+ npm install @aiquants/duckdb-helper
25
+
26
+ # yarn
27
+ yarn add @aiquants/duckdb-helper
28
+ ```
29
+
30
+ React Hooks を使う場合は、React 18 以上が必要です。
31
+
32
+ ```bash
33
+ pnpm add react react-dom
34
+ ```
35
+
36
+ ## 3. 最初のクエリを実行する
37
+
38
+ ### 3.1 サービスを直接使う方法(React なし)
39
+
40
+ ```typescript
41
+ import { duckDBService, duckdbTableToArray } from '@aiquants/duckdb-helper'
42
+
43
+ async function main() {
44
+ // ステップ1: DuckDB を初期化する
45
+ // (WASM のダウンロードがあるため、初回は数秒かかる)
46
+ await duckDBService.initialize()
47
+
48
+ // ステップ2: SQL を実行する
49
+ const result = await duckDBService.executeQuery('SELECT 42 AS answer')
50
+
51
+ // ステップ3: 結果を JavaScript 配列に変換する
52
+ const rows = duckdbTableToArray<{ answer: number }>(result)
53
+
54
+ console.log(rows)
55
+ // → [{ answer: 42 }]
56
+ }
57
+
58
+ main()
59
+ ```
60
+
61
+ ### 3.2 React Hook を使う方法(推奨)
62
+
63
+ ```tsx
64
+ import { useDuckDBQuery, duckdbTableToArray } from '@aiquants/duckdb-helper'
65
+
66
+ interface QueryResult {
67
+ answer: number
68
+ }
69
+
70
+ const MyFirstComponent = () => {
71
+ // SQL を渡すだけで自動的に初期化 → 実行 → 結果取得
72
+ const { data, loading, error } = useDuckDBQuery<unknown>(
73
+ 'SELECT 42 AS answer'
74
+ )
75
+
76
+ if (loading) return <p>読み込み中...</p>
77
+ if (error) return <p>エラー: {error}</p>
78
+ if (!data) return <p>データなし</p>
79
+
80
+ const rows = duckdbTableToArray<QueryResult>(data)
81
+
82
+ return (
83
+ <div>
84
+ <h1>初めてのクエリ結果</h1>
85
+ <p>答え: {rows[0]?.answer}</p>
86
+ </div>
87
+ )
88
+ }
89
+ ```
90
+
91
+ ## 4. データをテーブルに投入する
92
+
93
+ JavaScript の配列データを DuckDB テーブルとして登録し、SQL で分析できます。
94
+
95
+ ```tsx
96
+ import { useDuckDB, duckdbTableToArray } from '@aiquants/duckdb-helper'
97
+ import { useEffect, useState } from 'react'
98
+
99
+ // サンプルデータ
100
+ const sampleData = [
101
+ { id: 1, name: '田中太郎', age: 30, department: '開発' },
102
+ { id: 2, name: '鈴木花子', age: 25, department: '営業' },
103
+ { id: 3, name: '佐藤次郎', age: 35, department: '開発' },
104
+ { id: 4, name: '高橋美咲', age: 28, department: '人事' },
105
+ ]
106
+
107
+ const DataAnalysis = () => {
108
+ const { isReady, createTableFromData, executeQuery } = useDuckDB()
109
+ const [result, setResult] = useState<string>('')
110
+
111
+ useEffect(() => {
112
+ const analyze = async () => {
113
+ if (!isReady) return
114
+
115
+ // ステップ1: データをテーブルとして作成
116
+ await createTableFromData('employees', sampleData, {
117
+ dropIfExists: true, // 既存テーブルがあれば削除
118
+ })
119
+
120
+ // ステップ2: SQL で分析
121
+ const queryResult = await executeQuery(
122
+ 'SELECT department, COUNT(*) as count, AVG(age) as avg_age FROM employees GROUP BY department'
123
+ )
124
+
125
+ const rows = duckdbTableToArray<{
126
+ department: string
127
+ count: number
128
+ avg_age: number
129
+ }>(queryResult)
130
+
131
+ setResult(JSON.stringify(rows, null, 2))
132
+ }
133
+
134
+ analyze()
135
+ }, [isReady, createTableFromData, executeQuery])
136
+
137
+ return (
138
+ <div>
139
+ <h1>部署別分析</h1>
140
+ <pre>{result || '分析中...'}</pre>
141
+ </div>
142
+ )
143
+ }
144
+ ```
145
+
146
+ ## 5. 初期化の仕組みを理解する
147
+
148
+ DuckDB の初期化には以下のステップがあり、**初回は数秒かかります**。
149
+
150
+ ```
151
+ 1. CDN から WASM バンドルをダウンロード
152
+ 2. ブラウザに最適なバンドルを自動選択
153
+ 3. Web Worker を作成
154
+ 4. WASM モジュールを読み込み
155
+ 5. splink_udfs 拡張をインストール(オプション)
156
+ ```
157
+
158
+ ### ステータスの遷移
159
+
160
+ ```mermaid
161
+ stateDiagram-v2
162
+ state "not-initialized" as not_init
163
+ [*] --> not_init
164
+ not_init --> initializing : initialize()
165
+ initializing --> ready : 成功
166
+ initializing --> error : 失敗
167
+ ```
168
+
169
+ ### 初期化を待つパターン
170
+
171
+ ```tsx
172
+ const MyComponent = () => {
173
+ const { status, isReady, error } = useDuckDB()
174
+
175
+ // ステータスに応じた UI の出し分け
176
+ switch (status) {
177
+ case 'not-initialized':
178
+ return <p>待機中...</p>
179
+ case 'initializing':
180
+ return <p>DuckDB を準備しています...</p>
181
+ case 'error':
182
+ return <p>エラーが発生しました: {error}</p>
183
+ case 'ready':
184
+ return <p>準備完了!</p>
185
+ }
186
+ }
187
+ ```
188
+
189
+ ## 6. よくあるパターン
190
+
191
+ ### パターン A: パラメータでクエリを変える
192
+
193
+ ```tsx
194
+ const UserDetail = ({ userId }: { userId: number }) => {
195
+ const { data, loading } = useDuckDBQuery(
196
+ `SELECT * FROM users WHERE id = ${userId}`,
197
+ [userId] // ← userId が変わるとクエリが再実行される
198
+ )
199
+
200
+ if (loading) return <p>読み込み中...</p>
201
+ // ...
202
+ }
203
+ ```
204
+
205
+ ### パターン B: 複数クエリを順番に実行
206
+
207
+ ```typescript
208
+ const { executeQueries } = useDuckDB()
209
+
210
+ // CREATE → INSERT → SELECT を順番に実行
211
+ const results = await executeQueries([
212
+ "CREATE TABLE temp (id INTEGER, value VARCHAR)",
213
+ "INSERT INTO temp VALUES (1, 'hello'), (2, 'world')",
214
+ "SELECT * FROM temp"
215
+ ])
216
+
217
+ // results[2] が SELECT の結果
218
+ ```
219
+
220
+ ### パターン C: 手動で初期化タイミングを制御
221
+
222
+ ```tsx
223
+ const LazyInit = () => {
224
+ const { initialize, status, isReady } = useDuckDB(false) // false = 自動初期化しない
225
+
226
+ return (
227
+ <div>
228
+ <p>Status: {status}</p>
229
+ {!isReady && (
230
+ <button onClick={() => initialize()}>
231
+ DuckDB を初期化する
232
+ </button>
233
+ )}
234
+ </div>
235
+ )
236
+ }
237
+ ```
238
+
239
+ ## 7. トラブルシューティング
240
+
241
+ ### Q: 初期化が失敗する
242
+
243
+ **原因**: ブラウザが Web Worker または WASM をサポートしていない、もしくはネットワークが CDN にアクセスできない。
244
+
245
+ **対処**:
246
+ 1. ブラウザの DevTools コンソールでエラーを確認
247
+ 2. jsDelivr CDN(`cdn.jsdelivr.net`)へのアクセスが許可されているか確認
248
+ 3. CORS やプロキシの設定を確認
249
+
250
+ ### Q: クエリが "DuckDB is not ready" エラーになる
251
+
252
+ **原因**: 初期化が完了する前にクエリを実行しようとしている。
253
+
254
+ **対処**:
255
+ ```tsx
256
+ // isReady をチェックしてから実行
257
+ if (isReady) {
258
+ const result = await executeQuery('SELECT 1')
259
+ }
260
+ ```
261
+
262
+ ### Q: 大量データの投入が遅い
263
+
264
+ **対処**:
265
+ - `createTableFromData` は内部で1000行ずつバッチ INSERT する
266
+ - データが非常に大きい場合は、CSV/Parquet ファイルを直接読み込む DuckDB のネイティブ機能の使用を検討
267
+
268
+ ### Q: 初期化ログが表示されない
269
+
270
+ **原因**: デフォルトのログレベルが `WARN` のため、`info` レベルのログは非表示。
271
+
272
+ **対処**: `Logger` と `LogLevel` は現在公開 API としてエクスポートされておらず、`package.json` の `exports` にもサブパスが未定義のため、利用側から直接ログレベルを変更することはできない。ログレベル制御が必要な場合は、パッケージ側で `index.ts` にエクスポートを追加する対応が必要(詳細は [Logger 仕様書](../spec/2026.04.11%20%5BAI%5D%2005-logger.md) を参照)。
273
+
274
+ ## 8. 次のステップ
275
+
276
+ - 各モジュールの詳細は [設計書](../spec/) を参照
277
+ - API リファレンスは [useDuckDB.md](../useDuckDB.md) を参照
278
+ - 開発・保守作業については [開発ガイド](./2026.04.11%20%5BAI%5D%20development.md) を参照