@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,298 @@
1
+ # React Hooks 詳細仕様書
2
+
3
+ > ソースファイル: `src/hooks/useDuckDB.ts`
4
+
5
+ ## 1. 概要
6
+
7
+ React コンポーネントから DuckDB を利用するための2つのカスタムフックを提供する。
8
+
9
+ | Hook 名 | 用途 | 難易度 |
10
+ |---------|------|--------|
11
+ | `useDuckDB` | DuckDB の初期化・状態管理・各種操作を提供する汎用フック | 中級 |
12
+ | `useDuckDBQuery` | 単一 SQL クエリの実行と結果管理に特化した簡易フック | 初級 |
13
+
14
+ ## 2. `useDuckDB` フック
15
+
16
+ ### 2.1 シグネチャ
17
+
18
+ ```typescript
19
+ export const useDuckDB = (autoInitialize = true): UseDuckDBResult
20
+ ```
21
+
22
+ ### 2.2 引数
23
+
24
+ | 引数名 | 型 | デフォルト | 説明 |
25
+ |--------|------|-----------|------|
26
+ | `autoInitialize` | `boolean` | `true` | コンポーネントマウント時に自動で DuckDB を初期化するか |
27
+
28
+ ### 2.3 戻り値(`UseDuckDBResult`)
29
+
30
+ ```typescript
31
+ export interface UseDuckDBResult {
32
+ status: "not-initialized" | "initializing" | "ready" | "error"
33
+ error: string | null
34
+ executeQuery: (sql: string) => Promise<unknown>
35
+ executeQueries: (queries: string[]) => Promise<unknown[]>
36
+ createTableFromData: <T extends Record<string, unknown>>(
37
+ tableName: string,
38
+ data: T[],
39
+ options?: {
40
+ dropIfExists?: boolean
41
+ primaryKey?: string
42
+ verbose?: boolean
43
+ },
44
+ ) => Promise<void>
45
+ getTableInfo: (tableName: string) => Promise<unknown>
46
+ listTables: () => Promise<unknown>
47
+ isReady: boolean
48
+ initialize: () => Promise<void>
49
+ }
50
+ ```
51
+
52
+ | プロパティ | 型 | 説明 |
53
+ |-----------|------|------|
54
+ | `status` | `string` | 現在の DuckDB ステータス(4種類) |
55
+ | `error` | `string \| null` | エラーメッセージ。正常時は `null` |
56
+ | `executeQuery` | `Function` | SQL を実行して結果を返す。`status !== "ready"` で throw |
57
+ | `executeQueries` | `Function` | 複数 SQL を逐次実行。`status !== "ready"` で throw |
58
+ | `createTableFromData` | `Function` | データ配列からテーブルを作成。`status !== "ready"` で throw |
59
+ | `getTableInfo` | `Function` | テーブルの DESCRIBE 結果を返す。`status !== "ready"` で throw |
60
+ | `listTables` | `Function` | SHOW TABLES の結果を返す。`status !== "ready"` で throw |
61
+ | `isReady` | `boolean` | `status === "ready"` のショートカット |
62
+ | `initialize` | `Function` | 手動で DuckDB を初期化する |
63
+
64
+ ### 2.4 内部動作の詳細
65
+
66
+ #### ステータス同期の仕組み
67
+
68
+ `useDuckDB` は2つの方法で `DuckDBService` のステータスを React の状態に同期する。
69
+
70
+ **方法1: ステータスリスナー**
71
+ ```
72
+ DuckDBService
73
+ ├── addStatusListener(updateStatus) ← マウント時に登録
74
+ └── removeStatusListener(updateStatus) ← アンマウント時に解除
75
+ ```
76
+
77
+ **方法2: ポーリング(フォールバック)**
78
+ ```
79
+ setInterval(pollStatus, 1000) ← 1秒ごとにステータスをチェック
80
+ ```
81
+
82
+ > **なぜ2つの方法があるのか**: リスナーだけではステータス変更が漏れる可能性がある(例: 別のコンポーネントから直接サービスを操作した場合)。ポーリングは保険として機能する。
83
+
84
+ **ポーリングのガード条件**(2つの条件を AND で結合):
85
+ ```typescript
86
+ if (currentStatus !== status && currentStatus !== "initializing") {
87
+ setStatus(currentStatus)
88
+ }
89
+ ```
90
+ 1. `currentStatus !== status` — サービスの現在のステータスが React state と異なる場合のみ更新
91
+ 2. `currentStatus !== "initializing"` — サービスが `"initializing"` を返す場合は更新しない
92
+
93
+ 両方を満たした場合のみ React state が更新される。`"initializing"` への遷移はリスナー経由でのみ通知される。
94
+
95
+ **SSR 環境での動作**: `typeof window === "undefined"` の場合(SSR / Node.js 環境)、`setInterval` は設定されず、`pollStatus` を1回だけ同期的に実行してから effect を終了する。ブラウザ環境でも `setInterval` 設定直後に `pollStatus()` が1回即時実行される。
96
+
97
+ #### 自動初期化フロー
98
+
99
+ ```mermaid
100
+ flowchart TD
101
+ A["コンポーネントマウント"] --> B{autoInitialize === true\nかつ status === 'not-initialized'?}
102
+ B -- Yes --> C["initialize() を呼び出し"]
103
+ C -- 成功 --> D["status が 'ready' に変わる"]
104
+ C -- 失敗 --> E["error にメッセージをセット"]
105
+ B -- No --> F["何もしない\n(手動で initialize() を呼ぶ必要がある)"]
106
+ ```
107
+
108
+ #### 各操作関数の共通ガード
109
+
110
+ `executeQuery`、`executeQueries`、`createTableFromData`、`getTableInfo`、`listTables` はすべて、実行前に以下のチェックを行う。
111
+
112
+ ```typescript
113
+ if (status !== "ready") {
114
+ throw new Error("DuckDB is not ready. Please wait for initialization to complete.")
115
+ }
116
+ ```
117
+
118
+ > **重要**: `isReady` を確認してから操作関数を呼ぶのが安全なパターン。
119
+
120
+ ### 2.5 使用パターン
121
+
122
+ #### パターン1: 自動初期化(推奨)
123
+
124
+ ```tsx
125
+ const MyComponent = () => {
126
+ const { status, executeQuery, isReady, error } = useDuckDB()
127
+
128
+ if (error) return <p>Error: {error}</p>
129
+ if (!isReady) return <p>Loading DuckDB... ({status})</p>
130
+
131
+ return <button onClick={() => executeQuery("SELECT 1")}>Run</button>
132
+ }
133
+ ```
134
+
135
+ #### パターン2: 手動初期化
136
+
137
+ ```tsx
138
+ const MyComponent = () => {
139
+ const { status, initialize, isReady } = useDuckDB(false)
140
+
141
+ const handleInit = async () => {
142
+ await initialize()
143
+ }
144
+
145
+ return (
146
+ <div>
147
+ <p>Status: {status}</p>
148
+ {!isReady && <button onClick={handleInit}>Initialize</button>}
149
+ </div>
150
+ )
151
+ }
152
+ ```
153
+
154
+ #### パターン3: テーブル作成
155
+
156
+ ```tsx
157
+ const DataLoader = ({ data }: { data: Record<string, unknown>[] }) => {
158
+ const { createTableFromData, isReady } = useDuckDB()
159
+
160
+ useEffect(() => {
161
+ if (isReady && data.length > 0) {
162
+ createTableFromData("my_table", data, { dropIfExists: true })
163
+ }
164
+ }, [isReady, data, createTableFromData])
165
+
166
+ return <p>Loading data...</p>
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 3. `useDuckDBQuery` フック
173
+
174
+ ### 3.1 シグネチャ
175
+
176
+ ```typescript
177
+ export const useDuckDBQuery = <T = unknown>(
178
+ sql: string,
179
+ dependencies: DependencyList = [],
180
+ ): {
181
+ data: T | null
182
+ loading: boolean
183
+ error: string | null
184
+ refetch: () => Promise<void>
185
+ }
186
+ ```
187
+
188
+ ### 3.2 引数
189
+
190
+ | 引数名 | 型 | デフォルト | 説明 |
191
+ |--------|------|-----------|------|
192
+ | `sql` | `string` | (必須) | 実行する SQL クエリ |
193
+ | `dependencies` | `DependencyList` | `[]` | この配列の値が変わるとクエリが再実行される |
194
+
195
+ ### 3.3 戻り値
196
+
197
+ | プロパティ | 型 | 説明 |
198
+ |-----------|------|------|
199
+ | `data` | `T \| null` | クエリ結果。未実行・エラー時は `null` |
200
+ | `loading` | `boolean` | クエリ実行中は `true` |
201
+ | `error` | `string \| null` | エラーメッセージ。正常時は `null` |
202
+ | `refetch` | `() => Promise<void>` | クエリを手動で再実行する関数 |
203
+
204
+ ### 3.4 内部動作
205
+
206
+ ```mermaid
207
+ flowchart TD
208
+ A["useDuckDBQuery(sql, dependencies)"] --> B["内部で useDuckDB() を呼び出し\n(自動初期化)"]
209
+ B --> C["refetch 関数を定義"]
210
+ C --> D{"!(isReady && sql.trim())\nガード条件(単一複合条件)"}
211
+ D -- "true\n(未準備 or 空SQL)" --> E["何もしない(return)"]
212
+ D -- "false\n(準備完了 かつ 有効SQL)" --> G["executeQuery(sql) を実行"]
213
+ G -- 成功 --> H["data にセット"]
214
+ G -- 失敗 --> I["error にメッセージをセット"]
215
+ C --> J["useEffect で refetch を自動実行\nトリガー: refetch参照変更 + dependencies変更"]
216
+ ```
217
+
218
+ ### 3.5 再実行のタイミング
219
+
220
+ 以下のいずれかが変わるとクエリが再実行される:
221
+
222
+ 1. `sql` 文字列が変更された
223
+ 2. `dependencies` 配列の要素が変更された
224
+ 3. `isReady` が `false` → `true` に変わった(初期化完了)
225
+ 4. `refetch()` を手動で呼んだ
226
+
227
+ ### 3.6 使用パターン
228
+
229
+ #### パターン1: 基本的なデータ取得
230
+
231
+ ```tsx
232
+ const UserList = () => {
233
+ const { data, loading, error } = useDuckDBQuery<UserTable>(
234
+ "SELECT * FROM users LIMIT 100"
235
+ )
236
+
237
+ if (loading) return <p>Loading...</p>
238
+ if (error) return <p>Error: {error}</p>
239
+ if (!data) return <p>No data</p>
240
+
241
+ const rows = duckdbTableToArray<User>(data)
242
+ return <ul>{rows.map(u => <li key={u.id}>{u.name}</li>)}</ul>
243
+ }
244
+ ```
245
+
246
+ #### パターン2: パラメータ付きクエリ
247
+
248
+ ```tsx
249
+ const UserDetail = ({ userId }: { userId: number }) => {
250
+ const { data, loading, error } = useDuckDBQuery(
251
+ `SELECT * FROM users WHERE id = ${userId}`,
252
+ [userId] // userId が変わるとクエリが再実行される
253
+ )
254
+
255
+ if (loading) return <p>Loading...</p>
256
+ if (error) return <p>Error: {error}</p>
257
+
258
+ return <pre>{JSON.stringify(data)}</pre>
259
+ }
260
+ ```
261
+
262
+ #### パターン3: 手動リフェッチ
263
+
264
+ ```tsx
265
+ const Dashboard = () => {
266
+ const { data, loading, refetch } = useDuckDBQuery(
267
+ "SELECT COUNT(*) as total FROM orders"
268
+ )
269
+
270
+ return (
271
+ <div>
272
+ <p>Total: {JSON.stringify(data)}</p>
273
+ <button onClick={refetch} disabled={loading}>
274
+ Refresh
275
+ </button>
276
+ </div>
277
+ )
278
+ }
279
+ ```
280
+
281
+ ## 4. 2つの Hook の使い分け
282
+
283
+ | 判断基準 | `useDuckDB` | `useDuckDBQuery` |
284
+ |---------|-------------|------------------|
285
+ | 単一クエリの取得だけ | | 推奨 |
286
+ | 複数クエリを実行したい | 推奨 | |
287
+ | テーブルを作成したい | 推奨 | |
288
+ | 初期化タイミングを制御したい | 推奨 | |
289
+ | DuckDB のステータスを細かく監視 | 推奨 | |
290
+ | 最もシンプルに使いたい | | 推奨 |
291
+
292
+ ## 5. 新人向けポイント
293
+
294
+ 1. **まずは `useDuckDBQuery` から試そう**。SQL を渡すだけで結果が `data` に入る。
295
+ 2. **`isReady` のチェックを忘れずに**。DuckDB の初期化には数秒かかるため、初期化前に操作すると例外が発生する。
296
+ 3. **`dependencies` は React の `useEffect` の依存配列と同じ考え方**。中の値が変われば再実行される。
297
+ 4. **`useDuckDB` の操作関数はすべて `useCallback` でメモ化されている**。子コンポーネントに props として渡してもパフォーマンス問題は起きにくい。
298
+ 5. **1つの画面で複数の `useDuckDB` を呼んでもOK**。内部では同一のシングルトンサービスを共有する。
@@ -0,0 +1,220 @@
1
+ # ユーティリティ関数仕様書
2
+
3
+ > ソースファイル: `src/utils/duckdb-helpers.ts`
4
+
5
+ ## 1. 概要
6
+
7
+ DuckDB のクエリ結果(Apache Arrow Table)を JavaScript で扱いやすい形に変換するためのユーティリティ関数群。
8
+ すべての関数は `unknown` 型の入力を安全に処理し、不正な値に対してはデフォルト値を返す(例外を投げない)。
9
+
10
+ ## 2. 関数一覧
11
+
12
+ | 関数名 | 戻り値 | 説明 |
13
+ |--------|--------|------|
14
+ | `duckdbTableToArray<T>` | `T[]` | クエリ結果を型付き配列に変換 |
15
+ | `getDuckDBRowCount` | `number` | クエリ結果の行数を取得 |
16
+ | `getDuckDBColumnCount` | `number` | クエリ結果の列数を取得 |
17
+ | `isDuckDBTable` | `boolean` | 結果が有効な DuckDB テーブルか判定(型ガード) |
18
+
19
+ ## 3. 各関数の詳細仕様
20
+
21
+ ### 3.1 `duckdbTableToArray<T>(result: unknown): T[]`
22
+
23
+ クエリ結果を JavaScript の型付き配列に変換する。
24
+
25
+ #### シグネチャ
26
+
27
+ ```typescript
28
+ export const duckdbTableToArray = <T>(result: unknown): T[]
29
+ ```
30
+
31
+ #### 引数
32
+
33
+ | 引数名 | 型 | 説明 |
34
+ |--------|------|------|
35
+ | `result` | `unknown` | `executeQuery()` の戻り値(Apache Arrow Table) |
36
+
37
+ #### 戻り値
38
+
39
+ | 条件 | 戻り値 |
40
+ |------|--------|
41
+ | `result` が null/undefined | `[]`(空配列) |
42
+ | `result` がオブジェクトでない | `[]`(空配列) |
43
+ | `result` に `toArray` メソッドがない | `[]`(空配列) |
44
+ | `result` に `toArray` メソッドがある | `result.toArray()` の結果を `T[]` として返す |
45
+
46
+ #### 処理フロー
47
+
48
+ ```mermaid
49
+ flowchart TD
50
+ A["duckdbTableToArray(result)"] --> B{result が null/undefined?}
51
+ B -- Yes --> C["[] を返す"]
52
+ B -- No --> D{result が object?}
53
+ D -- No --> C
54
+ D -- Yes --> E{toArray メソッドがある?}
55
+ E -- No --> C
56
+ E -- Yes --> F["result.toArray() を T[] として返す"]
57
+ ```
58
+
59
+ #### 使用例
60
+
61
+ ```typescript
62
+ import { duckdbTableToArray, duckDBService } from '@aiquants/duckdb-helper'
63
+
64
+ interface StockPrice {
65
+ ticker: string
66
+ price: number
67
+ date: string
68
+ }
69
+
70
+ const result = await duckDBService.executeQuery("SELECT * FROM stock_prices LIMIT 10")
71
+
72
+ // 型パラメータを指定して安全に変換
73
+ const rows: StockPrice[] = duckdbTableToArray<StockPrice>(result)
74
+
75
+ // rows[0].ticker ← IDE の補完が効く
76
+ // rows[0].price ← number 型として扱える
77
+ ```
78
+
79
+ ---
80
+
81
+ ### 3.2 `getDuckDBRowCount(result: unknown): number`
82
+
83
+ クエリ結果の行数を取得する。
84
+
85
+ #### シグネチャ
86
+
87
+ ```typescript
88
+ export const getDuckDBRowCount = (result: unknown): number
89
+ ```
90
+
91
+ #### 戻り値
92
+
93
+ | 条件 | 戻り値 |
94
+ |------|--------|
95
+ | `result` が null/undefined | `0` |
96
+ | `result` がオブジェクトでない | `0` |
97
+ | `result` に `numRows` プロパティがない | `0` |
98
+ | `result.numRows` が number でない | `0` |
99
+ | 上記以外 | `result.numRows` の値 |
100
+
101
+ #### 使用例
102
+
103
+ ```typescript
104
+ import { getDuckDBRowCount, duckDBService } from '@aiquants/duckdb-helper'
105
+
106
+ const result = await duckDBService.executeQuery("SELECT * FROM users")
107
+ const count = getDuckDBRowCount(result) // 例: 42
108
+ console.log(`${count} 件のデータがあります`)
109
+ ```
110
+
111
+ ---
112
+
113
+ ### 3.3 `getDuckDBColumnCount(result: unknown): number`
114
+
115
+ クエリ結果の列数を取得する。
116
+
117
+ #### シグネチャ
118
+
119
+ ```typescript
120
+ export const getDuckDBColumnCount = (result: unknown): number
121
+ ```
122
+
123
+ #### 戻り値
124
+
125
+ | 条件 | 戻り値 |
126
+ |------|--------|
127
+ | `result` が null/undefined | `0` |
128
+ | `result` がオブジェクトでない | `0` |
129
+ | `result` に `numCols` プロパティがない | `0` |
130
+ | `result.numCols` が number でない | `0` |
131
+ | 上記以外 | `result.numCols` の値 |
132
+
133
+ #### 使用例
134
+
135
+ ```typescript
136
+ import { getDuckDBColumnCount, duckDBService } from '@aiquants/duckdb-helper'
137
+
138
+ const result = await duckDBService.executeQuery("SELECT id, name, email FROM users")
139
+ const cols = getDuckDBColumnCount(result) // 3
140
+ ```
141
+
142
+ ---
143
+
144
+ ### 3.4 `isDuckDBTable(result: unknown): result is DuckDBQueryResult`
145
+
146
+ 結果が有効な DuckDB テーブルかどうかを判定する **TypeScript 型ガード**関数。
147
+
148
+ #### シグネチャ
149
+
150
+ ```typescript
151
+ export const isDuckDBTable = (result: unknown): result is DuckDBQueryResult
152
+ ```
153
+
154
+ #### 判定条件(すべて満たす必要がある)
155
+
156
+ | 条件 | 説明 |
157
+ |------|------|
158
+ | `result` が null/undefined でない | |
159
+ | `result` が object である | |
160
+ | `result` に `toArray` キーが存在する | `"toArray" in result`(存在チェック) |
161
+ | `result.toArray` が関数である | `typeof result.toArray === "function"`(型チェック) |
162
+ | `result` に `numRows` キーが存在する | `"numRows" in result`(存在チェック) |
163
+ | `result.numRows` が数値である | `typeof result.numRows === "number"`(型チェック) |
164
+
165
+ > **注意**: `numCols` は判定条件に含まれない。`toArray` と `numRows` の2つのプロパティのみで判定する。
166
+
167
+ #### 型ガードとしての効果
168
+
169
+ ```typescript
170
+ import { isDuckDBTable, duckDBService } from '@aiquants/duckdb-helper'
171
+
172
+ const result: unknown = await duckDBService.executeQuery("SELECT 1")
173
+
174
+ if (isDuckDBTable(result)) {
175
+ // ここでは result の型が DuckDBQueryResult に絞り込まれる
176
+ const rows = result.toArray() // OK: toArray() が使える(実行時に検証済み)
177
+ const count = result.numRows // OK: numRows にアクセスできる(実行時に検証済み)
178
+ const cols = result.numCols // 型定義上アクセス可能だが、isDuckDBTable は numCols を検証しない
179
+ }
180
+ ```
181
+
182
+ #### なぜ型ガードが必要か
183
+
184
+ `DuckDBService.executeQuery()` の戻り値は `unknown` 型。TypeScript の型安全性を維持するため、使用前に型を絞り込む必要がある。`isDuckDBTable` を使うと `if` ブロック内で安全にプロパティにアクセスできる。
185
+
186
+ ## 4. 設計方針
187
+
188
+ ### 4.1 防御的プログラミング
189
+
190
+ すべての関数は以下の方針で設計されている。
191
+
192
+ 1. **例外を投げない**: 不正な入力に対してはデフォルト値(`[]`, `0`, `false`)を返す
193
+ 2. **null/undefined 安全**: 最初に null チェックを行う
194
+ 3. **型チェック**: `typeof` と `in` 演算子で段階的に型を検証する
195
+
196
+ ### 4.2 なぜ `unknown` を受け取るのか
197
+
198
+ `DuckDBService.executeQuery()` の戻り値が `unknown` なので、ヘルパー関数も `unknown` を受け取る設計になっている。これにより:
199
+
200
+ - `executeQuery()` の結果をそのまま渡せる
201
+ - キャストなしで安全に型変換できる
202
+ - 不正な値が渡されてもランタイムエラーにならない
203
+
204
+ ## 5. 他モジュールとの関係
205
+
206
+ ```mermaid
207
+ flowchart LR
208
+ A["executeQuery()"] -- "unknown" --> B["isDuckDBTable()\n型ガードで検証"]
209
+ A --> C["duckdbTableToArray&lt;T&gt;()\nT[] に変換"]
210
+ A --> D["getDuckDBRowCount()\n行数を取得"]
211
+ A --> E["getDuckDBColumnCount()\n列数を取得"]
212
+ B -- "DuckDBQueryResult" --> F["型安全に利用"]
213
+ ```
214
+
215
+ ## 6. 新人向けポイント
216
+
217
+ 1. **`duckdbTableToArray` が最もよく使う関数**。クエリ結果を JavaScript の配列にするにはこれを使う。
218
+ 2. **型パラメータ `<T>` は省略可能**。省略すると `unknown[]` になるが、できるだけ指定しよう。
219
+ 3. **`isDuckDBTable` は条件分岐で使う**。結果が有効かどうか事前にチェックしたい場合に便利。
220
+ 4. **すべての関数は安全**。何を渡してもクラッシュしない。ただしゴミデータを渡せば空配列や `0` が返るだけ。