@aiquants/duckdb-helper 1.4.1 → 1.6.1
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.
- package/dist/index.d.mts +164 -5
- package/dist/index.d.ts +164 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/docs/guides/2026.04.11 [AI] development.md +291 -0
- package/docs/guides/2026.04.11 [AI] getting-started.md +278 -0
- package/docs/specs/2026.04.11 [AI] 00-overview.md +198 -0
- package/docs/specs/2026.04.11 [AI] 01-types.md +101 -0
- package/docs/specs/2026.04.11 [AI] 02-service.md +261 -0
- package/docs/specs/2026.04.11 [AI] 03-hooks.md +287 -0
- package/docs/specs/2026.04.11 [AI] 04-utils.md +220 -0
- package/docs/specs/2026.04.11 [AI] 05-logger.md +186 -0
- package/docs/specs/2026.04.11 [AI] 06-build-config.md +188 -0
- package/docs/specs/2026.07.05 [AI] 07-testing.md +184 -0
- package/package.json +13 -9
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
#### ステータス同期の仕組み(v1.6.0 で push 型に一本化)
|
|
67
|
+
|
|
68
|
+
`useDuckDB` は**ステータスリスナー(push 型)のみ**で `DuckDBService` のステータスを React の状態に同期する。
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
DuckDBService
|
|
72
|
+
├── addStatusListener(updateStatus) ← マウント時に登録し、updateStatus() を1回即時実行して初期値を取得
|
|
73
|
+
└── removeStatusListener(updateStatus) ← アンマウント時に解除
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`DuckDBService` は状態遷移(initializing / ready / error)と `cleanup()` のたびに必ず `notifyListeners()` を呼ぶため、リスナーだけで全遷移を漏れなく受け取れる。
|
|
77
|
+
|
|
78
|
+
> **v1.6.0 での変更(1秒ポーリングの撤去)**: 旧実装は上記リスナーに加えて `setInterval(pollStatus, 1000)` を各フックインスタンスに常設していた。`useDuckDBQuery` は内部で `useDuckDB()` を呼ぶため、`root.tsx` のアプリ全体マウントと合わせて **消費者ごとに永久 1 秒タイマー** が積み上がり、しかも push リスナーと完全に重複していた。リスナーで全遷移が届く以上ポーリングは不要と判断し、effect ごと削除した。
|
|
79
|
+
|
|
80
|
+
**SSR 環境での動作**: リスナー登録は `useEffect` 内で行われ、SSR(サーバー)では effect が実行されないため副作用は発生しない。初期レンダリングの `status` は `"not-initialized"` となる。
|
|
81
|
+
|
|
82
|
+
#### 自動初期化フロー
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart TD
|
|
86
|
+
A["コンポーネントマウント"] --> B{autoInitialize === true\nかつ status === 'not-initialized'?}
|
|
87
|
+
B -- Yes --> C["initialize() を呼び出し"]
|
|
88
|
+
C -- 成功 --> D["status が 'ready' に変わる"]
|
|
89
|
+
C -- 失敗 --> E["error にメッセージをセット"]
|
|
90
|
+
B -- No --> F["何もしない\n(手動で initialize() を呼ぶ必要がある)"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 各操作関数の共通ガード
|
|
94
|
+
|
|
95
|
+
`executeQuery`、`executeQueries`、`createTableFromData`、`getTableInfo`、`listTables` はすべて、実行前に以下のチェックを行う。
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
if (status !== "ready") {
|
|
99
|
+
throw new Error("DuckDB is not ready. Please wait for initialization to complete.")
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> **重要**: `isReady` を確認してから操作関数を呼ぶのが安全なパターン。
|
|
104
|
+
|
|
105
|
+
### 2.5 使用パターン
|
|
106
|
+
|
|
107
|
+
#### パターン1: 自動初期化(推奨)
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
const MyComponent = () => {
|
|
111
|
+
const { status, executeQuery, isReady, error } = useDuckDB()
|
|
112
|
+
|
|
113
|
+
if (error) return <p>Error: {error}</p>
|
|
114
|
+
if (!isReady) return <p>Loading DuckDB... ({status})</p>
|
|
115
|
+
|
|
116
|
+
return <button onClick={() => executeQuery("SELECT 1")}>Run</button>
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### パターン2: 手動初期化
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
const MyComponent = () => {
|
|
124
|
+
const { status, initialize, isReady } = useDuckDB(false)
|
|
125
|
+
|
|
126
|
+
const handleInit = async () => {
|
|
127
|
+
await initialize()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<p>Status: {status}</p>
|
|
133
|
+
{!isReady && <button onClick={handleInit}>Initialize</button>}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### パターン3: テーブル作成
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
const DataLoader = ({ data }: { data: Record<string, unknown>[] }) => {
|
|
143
|
+
const { createTableFromData, isReady } = useDuckDB()
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (isReady && data.length > 0) {
|
|
147
|
+
createTableFromData("my_table", data, { dropIfExists: true })
|
|
148
|
+
}
|
|
149
|
+
}, [isReady, data, createTableFromData])
|
|
150
|
+
|
|
151
|
+
return <p>Loading data...</p>
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 3. `useDuckDBQuery` フック
|
|
158
|
+
|
|
159
|
+
### 3.1 シグネチャ
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
export const useDuckDBQuery = <T = unknown>(
|
|
163
|
+
sql: string,
|
|
164
|
+
dependencies: DependencyList = [],
|
|
165
|
+
): {
|
|
166
|
+
data: T | null
|
|
167
|
+
loading: boolean
|
|
168
|
+
error: string | null
|
|
169
|
+
refetch: () => Promise<void>
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3.2 引数
|
|
174
|
+
|
|
175
|
+
| 引数名 | 型 | デフォルト | 説明 |
|
|
176
|
+
|--------|------|-----------|------|
|
|
177
|
+
| `sql` | `string` | (必須) | 実行する SQL クエリ |
|
|
178
|
+
| `dependencies` | `DependencyList` | `[]` | この配列の値が変わるとクエリが再実行される |
|
|
179
|
+
|
|
180
|
+
### 3.3 戻り値
|
|
181
|
+
|
|
182
|
+
| プロパティ | 型 | 説明 |
|
|
183
|
+
|-----------|------|------|
|
|
184
|
+
| `data` | `T \| null` | クエリ結果。未実行・エラー時は `null` |
|
|
185
|
+
| `loading` | `boolean` | クエリ実行中は `true` |
|
|
186
|
+
| `error` | `string \| null` | エラーメッセージ。正常時は `null` |
|
|
187
|
+
| `refetch` | `() => Promise<void>` | クエリを手動で再実行する関数 |
|
|
188
|
+
|
|
189
|
+
### 3.4 内部動作
|
|
190
|
+
|
|
191
|
+
```mermaid
|
|
192
|
+
flowchart TD
|
|
193
|
+
A["useDuckDBQuery(sql, dependencies)"] --> B["内部で useDuckDB() を呼び出し\n(自動初期化)"]
|
|
194
|
+
B --> C["refetch 関数を定義"]
|
|
195
|
+
C --> D{"!(isReady && sql.trim())\nガード条件(単一複合条件)"}
|
|
196
|
+
D -- "true\n(未準備 or 空SQL)" --> E["何もしない(return)"]
|
|
197
|
+
D -- "false\n(準備完了 かつ 有効SQL)" --> G["executeQuery(sql) を実行"]
|
|
198
|
+
G -- 成功 --> H["data にセット"]
|
|
199
|
+
G -- 失敗 --> I["error にメッセージをセット"]
|
|
200
|
+
C --> J["useEffect で refetch を自動実行\nトリガー: refetch参照変更 + dependencies変更"]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 3.5 再実行のタイミング
|
|
204
|
+
|
|
205
|
+
以下のいずれかが変わるとクエリが再実行される:
|
|
206
|
+
|
|
207
|
+
1. `sql` 文字列が変更された
|
|
208
|
+
2. `dependencies` 配列の要素が変更された
|
|
209
|
+
3. `isReady` が `false` → `true` に変わった(初期化完了)
|
|
210
|
+
4. `refetch()` を手動で呼んだ
|
|
211
|
+
|
|
212
|
+
> **`data` は生の Arrow Table**: `data` は `executeQuery()` の戻り値そのもの(Apache Arrow Table)であり、**行配列ではない**。`.length` や `.map` は持たないため、行として扱うときは必ず `duckdbTableToArray<Row>(data)` で変換すること(型パラメータ `T` には行の型ではなく Arrow Table 相当の型を想定する)。単一クエリの型付き行配列がほしい場合は `useDuckDB().executeQuery()` + `duckdbTableToArray()` の組み合わせが明快。
|
|
213
|
+
|
|
214
|
+
> **`dependencies` の扱い(v1.6.0)**: 内部では可変長の `dependencies` を effect の依存配列へ直接スプレッドせず、`Object.is` 比較で変化を検知する**固定サイズのバージョン番号**(`useDependencyVersion`)に変換して `[refetch, version]` を依存にする。これにより、レンダー間で `dependencies` の**長さが変わっても** React の「依存配列サイズ変更」エラーを起こさず、要素の変化(React と同じ参照比較)で再フェッチされる。
|
|
215
|
+
|
|
216
|
+
### 3.6 使用パターン
|
|
217
|
+
|
|
218
|
+
#### パターン1: 基本的なデータ取得
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
const UserList = () => {
|
|
222
|
+
const { data, loading, error } = useDuckDBQuery<UserTable>(
|
|
223
|
+
"SELECT * FROM users LIMIT 100"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if (loading) return <p>Loading...</p>
|
|
227
|
+
if (error) return <p>Error: {error}</p>
|
|
228
|
+
if (!data) return <p>No data</p>
|
|
229
|
+
|
|
230
|
+
const rows = duckdbTableToArray<User>(data)
|
|
231
|
+
return <ul>{rows.map(u => <li key={u.id}>{u.name}</li>)}</ul>
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### パターン2: パラメータ付きクエリ
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
const UserDetail = ({ userId }: { userId: number }) => {
|
|
239
|
+
const { data, loading, error } = useDuckDBQuery(
|
|
240
|
+
`SELECT * FROM users WHERE id = ${userId}`,
|
|
241
|
+
[userId] // userId が変わるとクエリが再実行される
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if (loading) return <p>Loading...</p>
|
|
245
|
+
if (error) return <p>Error: {error}</p>
|
|
246
|
+
|
|
247
|
+
return <pre>{JSON.stringify(data)}</pre>
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### パターン3: 手動リフェッチ
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const Dashboard = () => {
|
|
255
|
+
const { data, loading, refetch } = useDuckDBQuery(
|
|
256
|
+
"SELECT COUNT(*) as total FROM orders"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div>
|
|
261
|
+
<p>Total: {JSON.stringify(data)}</p>
|
|
262
|
+
<button onClick={refetch} disabled={loading}>
|
|
263
|
+
Refresh
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 4. 2つの Hook の使い分け
|
|
271
|
+
|
|
272
|
+
| 判断基準 | `useDuckDB` | `useDuckDBQuery` |
|
|
273
|
+
|---------|-------------|------------------|
|
|
274
|
+
| 単一クエリの取得だけ | | 推奨 |
|
|
275
|
+
| 複数クエリを実行したい | 推奨 | |
|
|
276
|
+
| テーブルを作成したい | 推奨 | |
|
|
277
|
+
| 初期化タイミングを制御したい | 推奨 | |
|
|
278
|
+
| DuckDB のステータスを細かく監視 | 推奨 | |
|
|
279
|
+
| 最もシンプルに使いたい | | 推奨 |
|
|
280
|
+
|
|
281
|
+
## 5. 新人向けポイント
|
|
282
|
+
|
|
283
|
+
1. **まずは `useDuckDBQuery` から試そう**。SQL を渡すだけで結果が `data` に入る。
|
|
284
|
+
2. **`isReady` のチェックを忘れずに**。DuckDB の初期化には数秒かかるため、初期化前に操作すると例外が発生する。
|
|
285
|
+
3. **`dependencies` は React の `useEffect` の依存配列と同じ考え方**。中の値が変われば再実行される。
|
|
286
|
+
4. **`useDuckDB` の操作関数はすべて `useCallback` でメモ化されている**。子コンポーネントに props として渡してもパフォーマンス問題は起きにくい。
|
|
287
|
+
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<T>()\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` が返るだけ。
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Logger 仕様書
|
|
2
|
+
|
|
3
|
+
> ソースファイル: `src/utils/simple-logger.ts`
|
|
4
|
+
|
|
5
|
+
## 1. 概要
|
|
6
|
+
|
|
7
|
+
ライブラリ内部で使用するログ出力ユーティリティ。ログレベルによるフィルタリング、プレフィックス付与、カスタムロガー実装の差し替えが可能。
|
|
8
|
+
|
|
9
|
+
## 2. ログレベル(`LogLevel` enum)
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
export enum LogLevel {
|
|
13
|
+
DEBUG = 0, // 最も詳細(開発中のデバッグ情報)
|
|
14
|
+
INFO = 1, // 一般的な情報(初期化ステップなど)
|
|
15
|
+
WARN = 2, // 警告(処理は継続するが注意が必要)
|
|
16
|
+
ERROR = 3, // エラー(処理が失敗)
|
|
17
|
+
NONE = 4, // ログ出力なし
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| レベル | 数値 | 出力対象 | 用途例 |
|
|
22
|
+
|--------|------|---------|--------|
|
|
23
|
+
| `DEBUG` | 0 | debug, info, warn, error すべて出力 | 開発中のデバッグ |
|
|
24
|
+
| `INFO` | 1 | info, warn, error を出力 | 初期化ステップの確認 |
|
|
25
|
+
| `WARN` | 2 | warn, error のみ出力 | **デフォルト** |
|
|
26
|
+
| `ERROR` | 3 | error のみ出力 | 本番環境での最小ログ |
|
|
27
|
+
| `NONE` | 4 | 何も出力しない | テスト時のログ抑制 |
|
|
28
|
+
|
|
29
|
+
**判定ルール**: `設定レベル <= メソッドのレベル` の場合にログが出力される。
|
|
30
|
+
|
|
31
|
+
## 3. `ILogger` インターフェース
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
export interface ILogger {
|
|
35
|
+
debug(message?: unknown, ...optionalParams: unknown[]): void
|
|
36
|
+
info(message?: unknown, ...optionalParams: unknown[]): void
|
|
37
|
+
warn(message?: unknown, ...optionalParams: unknown[]): void
|
|
38
|
+
error(message?: unknown, ...optionalParams: unknown[]): void
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`console` オブジェクトと互換のインターフェース。カスタムロガーを作る場合はこれを実装する。
|
|
43
|
+
|
|
44
|
+
## 4. `Logger` クラス
|
|
45
|
+
|
|
46
|
+
### 4.1 二重のAPI: インスタンスメソッドと静的メソッド
|
|
47
|
+
|
|
48
|
+
`Logger` は2つの使い方ができる。
|
|
49
|
+
|
|
50
|
+
| 使い方 | 対象 | 用途 |
|
|
51
|
+
|--------|------|------|
|
|
52
|
+
| **静的メソッド** | 内部のデフォルトインスタンス | ライブラリ内部での標準的な使用 |
|
|
53
|
+
| **インスタンスメソッド** | 個別のインスタンス | カスタム設定が必要な場合 |
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// 静的メソッド(ライブラリ内部で使用)
|
|
57
|
+
Logger.info("初期化開始")
|
|
58
|
+
Logger.error("クエリ失敗:", error)
|
|
59
|
+
|
|
60
|
+
// インスタンスメソッド(カスタム用途)
|
|
61
|
+
const myLogger = new Logger(LogLevel.DEBUG, "[my-app]")
|
|
62
|
+
myLogger.debug("デバッグ情報")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 4.2 コンストラクタ
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
constructor(
|
|
69
|
+
level: LogLevel = LogLevel.WARN,
|
|
70
|
+
prefix: string = "[duckdb-helper]",
|
|
71
|
+
impl: ILogger = console
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| 引数 | デフォルト | 説明 |
|
|
76
|
+
|------|-----------|------|
|
|
77
|
+
| `level` | `LogLevel.WARN` | 最小出力レベル |
|
|
78
|
+
| `prefix` | `"[duckdb-helper]"` | ログメッセージの先頭に付与する文字列 |
|
|
79
|
+
| `impl` | `console` | 実際のログ出力を行うオブジェクト |
|
|
80
|
+
|
|
81
|
+
### 4.3 デフォルトインスタンス(static)
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
private static instance: Logger = new Logger(LogLevel.WARN, "[duckdb-helper]")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- 静的メソッド(`Logger.info()` など)はこのインスタンスを使用する
|
|
88
|
+
- デフォルトレベルは `WARN` なので、`info` や `debug` のログは出力されない
|
|
89
|
+
- `DuckDBService` 内の初期化ログ(`Logger.info("🔧 Starting...")` など)はデフォルトでは非表示
|
|
90
|
+
|
|
91
|
+
### 4.4 静的メソッド一覧
|
|
92
|
+
|
|
93
|
+
#### ログ出力
|
|
94
|
+
|
|
95
|
+
| メソッド | 出力条件 |
|
|
96
|
+
|---------|---------|
|
|
97
|
+
| `Logger.debug(message, ...params)` | `level <= DEBUG (0)` |
|
|
98
|
+
| `Logger.info(message, ...params)` | `level <= INFO (1)` |
|
|
99
|
+
| `Logger.warn(message, ...params)` | `level <= WARN (2)` |
|
|
100
|
+
| `Logger.error(message, ...params)` | `level <= ERROR (3)` |
|
|
101
|
+
|
|
102
|
+
#### 設定変更
|
|
103
|
+
|
|
104
|
+
| メソッド | 説明 |
|
|
105
|
+
|---------|------|
|
|
106
|
+
| `Logger.setLevel(level)` | デフォルトインスタンスのログレベルを変更 |
|
|
107
|
+
| `Logger.setImplementation(impl)` | デフォルトインスタンスのロガー実装を差し替え |
|
|
108
|
+
| `Logger.setPrefix(prefix)` | デフォルトインスタンスのプレフィックスを変更 |
|
|
109
|
+
|
|
110
|
+
### 4.5 メッセージフォーマット
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
private formatMessage(message: unknown): unknown[] {
|
|
114
|
+
if (typeof message === "string") {
|
|
115
|
+
return [`${this.prefix} ${message}`]
|
|
116
|
+
}
|
|
117
|
+
return [this.prefix, message]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| 入力 | 出力 |
|
|
122
|
+
|------|------|
|
|
123
|
+
| `"初期化中..."` | `["[duckdb-helper] 初期化中..."]` |
|
|
124
|
+
| `{ key: "value" }` | `["[duckdb-helper]", { key: "value" }]` |
|
|
125
|
+
|
|
126
|
+
文字列の場合はプレフィックスを結合し、オブジェクトの場合はプレフィックスを別引数として渡す(console の展開表示を活用)。
|
|
127
|
+
|
|
128
|
+
## 5. ライブラリ内での使用箇所
|
|
129
|
+
|
|
130
|
+
| ファイル | 使用メソッド | 内容 |
|
|
131
|
+
|---------|------------|------|
|
|
132
|
+
| `services/duckdb.ts` | `Logger.info` | Worker 初期化の各ステップのログ |
|
|
133
|
+
| `services/duckdb.ts` | `Logger.warn` | splink_udfs ロード失敗時の警告 |
|
|
134
|
+
| `services/duckdb.ts` | `Logger.error` | 初期化失敗・クエリ失敗のエラーログ |
|
|
135
|
+
| `hooks/useDuckDB.ts` | `Logger.info` | ステータス変更・自動初期化のログ |
|
|
136
|
+
| `hooks/useDuckDB.ts` | `Logger.error` | 初期化失敗のエラーログ |
|
|
137
|
+
|
|
138
|
+
## 6. 利用側でのログレベル制御
|
|
139
|
+
|
|
140
|
+
### 開発中(すべてのログを表示)
|
|
141
|
+
|
|
142
|
+
> **v1.6.0 で公開**: `Logger` / `LogLevel` / `ILogger` は `index.ts` から公開エクスポートされるようになった。利用側から直接インポートしてログレベルを制御できる(旧バージョンではエクスポートされておらず、初期化ログを有効化する手段が無かった)。
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { Logger, LogLevel } from '@aiquants/duckdb-helper'
|
|
146
|
+
|
|
147
|
+
Logger.setLevel(LogLevel.DEBUG)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 本番環境(エラーのみ表示)
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
Logger.setLevel(LogLevel.ERROR)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### テスト(ログを完全に抑制)
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
Logger.setLevel(LogLevel.NONE)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### カスタムロガーに差し替え
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const customLogger: ILogger = {
|
|
166
|
+
debug: (...args) => myLoggingService.log('debug', ...args),
|
|
167
|
+
info: (...args) => myLoggingService.log('info', ...args),
|
|
168
|
+
warn: (...args) => myLoggingService.log('warn', ...args),
|
|
169
|
+
error: (...args) => myLoggingService.log('error', ...args),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Logger.setImplementation(customLogger)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 7. 注意事項
|
|
176
|
+
|
|
177
|
+
1. **Logger / LogLevel / ILogger は `index.ts` から公開エクスポートされている(v1.6.0〜)**。`import { Logger, LogLevel } from '@aiquants/duckdb-helper'` で利用でき、`Logger.setLevel()` などでログレベルや実装を制御できる。
|
|
178
|
+
2. **デフォルトレベルは `WARN`**。つまり `DuckDBService` 内の `Logger.info(...)` による初期化ログは通常表示されない。ただし初期化の**失敗**(`Logger.error`)は WARN でも表示される。詳細なステップを見たい場合は `Logger.setLevel(LogLevel.DEBUG)` を設定する。
|
|
179
|
+
3. **リスナーでの例外はキャッチされる**。`DuckDBService.notifyListeners()` 内でリスナーが例外を投げた場合、`Logger.error` でログに出力されるが、他のリスナーへの通知は続行される。
|
|
180
|
+
|
|
181
|
+
## 8. 新人向けポイント
|
|
182
|
+
|
|
183
|
+
1. **普段は Logger を意識しなくてOK**。ライブラリ内部で勝手にログを出す仕組み。
|
|
184
|
+
2. **デバッグしたい時は `Logger.setLevel(LogLevel.DEBUG)` するだけ**。初期化の各ステップが詳細に表示される。
|
|
185
|
+
3. **テストで邪魔なログを消すには `Logger.setLevel(LogLevel.NONE)`**。
|
|
186
|
+
4. **プレフィックス `[duckdb-helper]` でログをフィルタリング**できる。ブラウザの DevTools で検索すると便利。
|