@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,198 @@
|
|
|
1
|
+
# @aiquants/duckdb-helper 設計書 - 全体概要
|
|
2
|
+
|
|
3
|
+
## 1. パッケージ概要
|
|
4
|
+
|
|
5
|
+
| 項目 | 内容 |
|
|
6
|
+
|------|------|
|
|
7
|
+
| パッケージ名 | `@aiquants/duckdb-helper` |
|
|
8
|
+
| バージョン | 1.6.0 |
|
|
9
|
+
| ライセンス | MIT |
|
|
10
|
+
| 目的 | ブラウザ上で DuckDB-WASM を簡単に利用するためのヘルパーライブラリ |
|
|
11
|
+
| 主な利用者 | React ベースの Web アプリケーション開発者 |
|
|
12
|
+
|
|
13
|
+
## 2. このパッケージが解決する課題
|
|
14
|
+
|
|
15
|
+
DuckDB-WASM をブラウザで使う場合、以下のような複雑な手順が必要になる。
|
|
16
|
+
|
|
17
|
+
1. WASM バンドルの取得とブラウザ互換バンドルの選択
|
|
18
|
+
2. Web Worker の作成と DuckDB インスタンスの初期化
|
|
19
|
+
3. コネクション管理(作成・再利用・破棄)
|
|
20
|
+
4. クエリ結果(Apache Arrow Table)の JavaScript 配列への変換
|
|
21
|
+
5. React コンポーネントでの状態管理(初期化中・準備完了・エラー)
|
|
22
|
+
|
|
23
|
+
このパッケージは上記すべてをラップし、**数行のコードで DuckDB を利用可能**にする。
|
|
24
|
+
|
|
25
|
+
## 3. アーキテクチャ図
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
graph TD
|
|
29
|
+
subgraph App["利用側アプリケーション"]
|
|
30
|
+
direction TB
|
|
31
|
+
subgraph Hooks["React Hooks"]
|
|
32
|
+
useDuckDB["useDuckDB"]
|
|
33
|
+
useDuckDBQuery["useDuckDBQuery"]
|
|
34
|
+
end
|
|
35
|
+
subgraph Service["サービス層(シングルトン)"]
|
|
36
|
+
DuckDBService["DuckDBService"]
|
|
37
|
+
end
|
|
38
|
+
subgraph Utils["ユーティリティ層"]
|
|
39
|
+
Logger["Logger"]
|
|
40
|
+
helpers["helpers"]
|
|
41
|
+
types["types"]
|
|
42
|
+
end
|
|
43
|
+
useDuckDB --> DuckDBService
|
|
44
|
+
useDuckDBQuery --> DuckDBService
|
|
45
|
+
DuckDBService --> Logger
|
|
46
|
+
DuckDBService --> helpers
|
|
47
|
+
DuckDBService --> types
|
|
48
|
+
end
|
|
49
|
+
DuckDBService --> duckdb_wasm["@duckdb/duckdb-wasm\n(外部依存)"]
|
|
50
|
+
duckdb_wasm --> Worker["Web Worker\n(WASM / ブラウザランタイム)"]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 4. ディレクトリ構成
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
packages/duckdb-helper/
|
|
57
|
+
├── src/
|
|
58
|
+
│ ├── index.ts # エントリーポイント(公開 API の定義)
|
|
59
|
+
│ ├── hooks/
|
|
60
|
+
│ │ └── useDuckDB.ts # React Hooks(useDuckDB, useDuckDBQuery)
|
|
61
|
+
│ ├── services/
|
|
62
|
+
│ │ └── duckdb.ts # DuckDBService クラス(シングルトン)
|
|
63
|
+
│ ├── types/
|
|
64
|
+
│ │ └── duckdb.ts # 型定義(DuckDBQueryResult, DuckDBRow)
|
|
65
|
+
│ └── utils/
|
|
66
|
+
│ ├── duckdb-helpers.ts # ヘルパー関数群
|
|
67
|
+
│ └── simple-logger.ts # ログユーティリティ
|
|
68
|
+
├── tests/ # 単体テスト(Vitest, カバレッジ 100%)
|
|
69
|
+
│ ├── services/duckdb.test.ts # DuckDBService
|
|
70
|
+
│ ├── hooks/useDuckDB.test.tsx # useDuckDB / useDuckDBQuery
|
|
71
|
+
│ ├── utils/
|
|
72
|
+
│ │ ├── duckdb-helpers.test.ts
|
|
73
|
+
│ │ └── simple-logger.test.ts
|
|
74
|
+
│ ├── index.test.ts # 公開 API サーフェス
|
|
75
|
+
│ └── integration/
|
|
76
|
+
│ └── dummy.test.ts # 結果ハンドリングのスモークテスト
|
|
77
|
+
├── docs/
|
|
78
|
+
│ ├── specs/ # 設計仕様書 + テスト仕様書
|
|
79
|
+
│ ├── guides/ # ガイド(入門・開発)
|
|
80
|
+
│ └── useDuckDB.md # API リファレンス
|
|
81
|
+
├── dist/ # ビルド成果物(自動生成)
|
|
82
|
+
├── package.json
|
|
83
|
+
├── tsconfig.json # 本体(src のみ)の型チェック設定
|
|
84
|
+
├── tsconfig.test.json # tests を含む型チェック設定
|
|
85
|
+
├── vitest.config.ts # テスト + カバレッジ設定(閾値 100%)
|
|
86
|
+
└── tsup.config.ts # ビルド設定
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 5. 公開 API 一覧
|
|
90
|
+
|
|
91
|
+
`src/index.ts` で公開されるすべてのエクスポート:
|
|
92
|
+
|
|
93
|
+
| カテゴリ | エクスポート名 | 種別 | 説明 |
|
|
94
|
+
|----------|---------------|------|------|
|
|
95
|
+
| Hooks | `useDuckDB` | 関数 | DuckDB の状態管理と操作を提供する React Hook |
|
|
96
|
+
| Hooks | `useDuckDBQuery` | 関数 | SQL クエリの実行と結果管理を行う React Hook |
|
|
97
|
+
| Service | `DuckDBService` | クラス | DuckDB 操作のシングルトンサービス(名前付きエクスポート) |
|
|
98
|
+
| Service | `duckDBService` | インスタンス | `DuckDBService.getInstance()` の名前付きエクスポート(`services/duckdb.ts` のデフォルトエクスポートを re-export) |
|
|
99
|
+
| Types | `UseDuckDBResult` | 型 | `useDuckDB` の戻り値の型 |
|
|
100
|
+
| Types | `DuckDBQueryResult` | 型 | クエリ結果のインターフェース |
|
|
101
|
+
| Types | `DuckDBRow` | 型 | 行データのヘルパー型 |
|
|
102
|
+
| Utils | `duckdbTableToArray` | 関数 | クエリ結果を型付き配列に変換 |
|
|
103
|
+
| Utils | `getDuckDBRowCount` | 関数 | 結果の行数を取得 |
|
|
104
|
+
| Utils | `getDuckDBColumnCount` | 関数 | 結果の列数を取得 |
|
|
105
|
+
| Utils | `isDuckDBTable` | 関数 | 結果が有効な DuckDB テーブルか判定(型ガード) |
|
|
106
|
+
| Logger | `Logger` | クラス | ログユーティリティ(v1.6.0 で公開) |
|
|
107
|
+
| Logger | `LogLevel` | enum | ログレベル(DEBUG/INFO/WARN/ERROR/NONE、v1.6.0 で公開) |
|
|
108
|
+
| Logger | `ILogger` | 型 | カスタムロガー実装用インターフェース(v1.6.0 で公開) |
|
|
109
|
+
|
|
110
|
+
## 6. 依存関係
|
|
111
|
+
|
|
112
|
+
### 本番依存(dependencies)
|
|
113
|
+
|
|
114
|
+
| パッケージ | バージョン | 用途 |
|
|
115
|
+
|-----------|-----------|------|
|
|
116
|
+
| `@duckdb/duckdb-wasm` | ^1.30.0 | DuckDB の WASM 実装本体 |
|
|
117
|
+
|
|
118
|
+
### ピア依存(peerDependencies)- オプション
|
|
119
|
+
|
|
120
|
+
| パッケージ | バージョン | 用途 |
|
|
121
|
+
|-----------|-----------|------|
|
|
122
|
+
| `react` | `^19.2.7`(catalog) | React Hooks を使う場合に必要 |
|
|
123
|
+
| `react-dom` | `^19.2.7`(catalog) | React Hooks を使う場合に必要 |
|
|
124
|
+
|
|
125
|
+
> **ポイント**: React はオプショナルなピア依存。Hooks を使わずサービス層のみ利用する場合、React は不要。
|
|
126
|
+
|
|
127
|
+
## 7. データフロー
|
|
128
|
+
|
|
129
|
+
### 初期化フロー
|
|
130
|
+
|
|
131
|
+
```mermaid
|
|
132
|
+
flowchart TD
|
|
133
|
+
A["useDuckDB(autoInitialize=true)"] --> B["マウント時に initialize() を呼び出し"]
|
|
134
|
+
B --> C["DuckDBService.initialize()"]
|
|
135
|
+
C --> D{workerInstance が存在?}
|
|
136
|
+
D -- Yes --> E["workerInstance を返す"]
|
|
137
|
+
D -- No --> F{初期化中?}
|
|
138
|
+
F -- Yes --> G["既存の initPromise を返す(重複防止)"]
|
|
139
|
+
F -- No --> H["1. jsDelivr CDN からバンドル一覧を取得"]
|
|
140
|
+
H --> I["2. ブラウザ互換バンドルを自動選択"]
|
|
141
|
+
I --> J["3. Blob URL で Web Worker を作成"]
|
|
142
|
+
J --> K["4. AsyncDuckDB インスタンスを作成"]
|
|
143
|
+
K --> L["5. WASM モジュールをインスタンス化"]
|
|
144
|
+
L --> M["6. splink_udfs 拡張をインストール・ロード\n(失敗しても初期化は続行)"]
|
|
145
|
+
M --> N["7. Blob URL を解放"]
|
|
146
|
+
N --> O{結果}
|
|
147
|
+
O -- 成功 --> P["workerInstance を公開 → status: ready"]
|
|
148
|
+
O -- 失敗/タイムアウト --> Q["状態を完全リセット(workerInstance=null 等)\nWorker/Blob URL を破棄 → status: error(再試行可能)"]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### クエリ実行フロー
|
|
152
|
+
|
|
153
|
+
```mermaid
|
|
154
|
+
flowchart TD
|
|
155
|
+
A["executeQuery(sql)"] --> Q0["operationQueue に載せて他操作と直列化"]
|
|
156
|
+
Q0 --> B["getConnection()"]
|
|
157
|
+
B --> C{コネクションが存在?}
|
|
158
|
+
C -- No --> D["initialize() → connect()"]
|
|
159
|
+
C -- Yes --> E["既存コネクションを返す"]
|
|
160
|
+
D --> F["connection.query(sql) を実行"]
|
|
161
|
+
E --> F
|
|
162
|
+
F --> G["60秒タイムアウトで Promise.race"]
|
|
163
|
+
G -- 成功 --> H["Apache Arrow Table を返す"]
|
|
164
|
+
G -- タイムアウト --> I["使用中コネクションを破棄(次回は新規接続)→ reject"]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 8. 設計上の重要な判断
|
|
168
|
+
|
|
169
|
+
| 判断事項 | 採用方針 | 理由 |
|
|
170
|
+
|---------|---------|------|
|
|
171
|
+
| シングルトンパターン | 採用 | アプリ全体で WASM インスタンスを1つだけ保持し、メモリを節約 |
|
|
172
|
+
| CDN からのバンドル取得 | jsDelivr を使用 | `@duckdb/duckdb-wasm` 公式推奨の配布方法 |
|
|
173
|
+
| Blob URL による Worker 作成 | 採用 | CORS 制約を回避し、CDN の Worker スクリプトをロード |
|
|
174
|
+
| splink_udfs 拡張の自動ロード | 初期化時に実行 | 名寄せ(レコードリンケージ)機能を標準提供 |
|
|
175
|
+
| 初期化タイムアウト | 120秒 | WASM ダウンロードを考慮した長めの設定 |
|
|
176
|
+
| クエリタイムアウト | 60秒 | 長時間クエリによる UI フリーズを防止 |
|
|
177
|
+
| バッチインサート | 1000行単位 | 大量データ投入時のメモリ効率とパフォーマンスのバランス |
|
|
178
|
+
| 初期化失敗後の完全リセット(v1.6.0) | 採用 | 失敗/タイムアウト時に `workerInstance` 等を null に戻し、再試行可能にする(旧実装は復旧不能だった) |
|
|
179
|
+
| テーブル構築のトランザクション化(v1.6.0) | 採用 | DROP/CREATE/INSERT を原子的に行い、失敗時は ROLLBACK で半端なテーブルを残さない |
|
|
180
|
+
| 操作の直列化キュー(v1.6.0) | 採用 | 単一コネクション上で構築 DDL/DML と read が割り込まないようにする |
|
|
181
|
+
| 整数は BIGINT を採用(v1.6.0) | 採用 | int32 超の値でのオーバーフローを回避 |
|
|
182
|
+
| TIMESTAMP 推定の厳密化(v1.6.0) | 採用 | ハイフンを含むだけの非日付文字列を誤って TIMESTAMP にしない |
|
|
183
|
+
| ステータス同期は push 型のみ(v1.6.0) | 採用 | 1秒ポーリングを撤去し、リスナー通知に一本化(消費者ごとの常設タイマーを排除) |
|
|
184
|
+
| Logger/LogLevel/ILogger の公開(v1.6.0) | 採用 | 利用側からログレベルを制御可能にする |
|
|
185
|
+
|
|
186
|
+
## 9. 関連ドキュメント
|
|
187
|
+
|
|
188
|
+
| ドキュメント | パス | 内容 |
|
|
189
|
+
|-------------|------|------|
|
|
190
|
+
| 型定義仕様書 | [01-types.md](./2026.04.11%20%5BAI%5D%2001-types.md) | 型定義の詳細 |
|
|
191
|
+
| サービス仕様書 | [02-service.md](./2026.04.11%20%5BAI%5D%2002-service.md) | DuckDBService の全メソッド仕様 |
|
|
192
|
+
| Hooks 仕様書 | [03-hooks.md](./2026.04.11%20%5BAI%5D%2003-hooks.md) | useDuckDB / useDuckDBQuery の仕様 |
|
|
193
|
+
| ユーティリティ仕様書 | [04-utils.md](./2026.04.11%20%5BAI%5D%2004-utils.md) | ヘルパー関数の仕様 |
|
|
194
|
+
| Logger 仕様書 | [05-logger.md](./2026.04.11%20%5BAI%5D%2005-logger.md) | ログユーティリティの仕様 |
|
|
195
|
+
| ビルド設定仕様書 | [06-build-config.md](./2026.04.11%20%5BAI%5D%2006-build-config.md) | tsup / TypeScript 設定の詳細 |
|
|
196
|
+
| テスト仕様書 | [07-testing.md](./2026.07.05%20%5BAI%5D%2007-testing.md) | テスト戦略・テストケース・カバレッジ |
|
|
197
|
+
| 入門ガイド | [getting-started.md](../guides/2026.04.11%20%5BAI%5D%20getting-started.md) | 初めて使う人向けガイド |
|
|
198
|
+
| 開発ガイド | [development.md](../guides/2026.04.11%20%5BAI%5D%20development.md) | 開発・保守作業向けガイド |
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 型定義仕様書
|
|
2
|
+
|
|
3
|
+
> ソースファイル: `src/types/duckdb.ts`
|
|
4
|
+
|
|
5
|
+
## 1. 概要
|
|
6
|
+
|
|
7
|
+
このファイルは、DuckDB のクエリ結果を TypeScript で安全に扱うための型定義を提供する。
|
|
8
|
+
DuckDB-WASM のクエリ結果は内部的に Apache Arrow Table 形式だが、このライブラリでは直接 Arrow に依存せず、必要最小限のインターフェースを定義している。
|
|
9
|
+
|
|
10
|
+
## 2. 型定義一覧
|
|
11
|
+
|
|
12
|
+
### 2.1 `DuckDBQueryResult<T>`
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
export interface DuckDBQueryResult<T = unknown> {
|
|
16
|
+
toArray(): T[]
|
|
17
|
+
numRows: number
|
|
18
|
+
numCols: number
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| プロパティ/メソッド | 型 | 説明 |
|
|
23
|
+
|-------------------|------|------|
|
|
24
|
+
| `toArray()` | `T[]` | クエリ結果の全行を JavaScript 配列として返すメソッド |
|
|
25
|
+
| `numRows` | `number` | 結果の行数 |
|
|
26
|
+
| `numCols` | `number` | 結果の列数 |
|
|
27
|
+
|
|
28
|
+
#### 設計意図
|
|
29
|
+
|
|
30
|
+
- **なぜ独自インターフェースか**: `@duckdb/duckdb-wasm` が返す Apache Arrow Table の型を直接使うと、Arrow ライブラリへの強い依存が発生する。この最小インターフェースにより、利用側は Arrow の知識なしに結果を扱える。
|
|
31
|
+
- **ジェネリクス `T`**: デフォルトは `unknown`。利用側が行の型を指定することで、`toArray()` の戻り値に型安全性を持たせられる。
|
|
32
|
+
|
|
33
|
+
#### 使用例
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// 型パラメータなし(unknown[] が返る)
|
|
37
|
+
const result: DuckDBQueryResult = await executeQuery("SELECT 1")
|
|
38
|
+
const rows = result.toArray() // unknown[]
|
|
39
|
+
|
|
40
|
+
// 型パラメータあり(型安全)
|
|
41
|
+
interface User { id: number; name: string }
|
|
42
|
+
const result: DuckDBQueryResult<User> = await executeQuery("SELECT * FROM users")
|
|
43
|
+
const rows = result.toArray() // User[]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2.2 `DuckDBRow<T>`
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export type DuckDBRow<T> = T
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| 項目 | 内容 |
|
|
53
|
+
|------|------|
|
|
54
|
+
| 定義 | ジェネリクス `T` をそのまま返すエイリアス型 |
|
|
55
|
+
| 用途 | コード上の可読性向上。「この `T` は DuckDB の行データである」という意味を明示する |
|
|
56
|
+
|
|
57
|
+
#### 設計意図
|
|
58
|
+
|
|
59
|
+
機能的には `T` と同一だが、コード上で「この型は DuckDB テーブルの1行を表す」という意図を伝える**ドキュメンテーション型**として存在する。
|
|
60
|
+
|
|
61
|
+
#### 使用例
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type UserRow = DuckDBRow<{
|
|
65
|
+
id: number
|
|
66
|
+
name: string
|
|
67
|
+
email: string | null
|
|
68
|
+
}>
|
|
69
|
+
|
|
70
|
+
// 以下は同じ意味だが、DuckDBRow を使うと「DuckDB の行データ」という意図が明確
|
|
71
|
+
// type UserRow = { id: number; name: string; email: string | null }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 3. 型の関係図
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
DuckDBQueryResult<T>
|
|
78
|
+
│
|
|
79
|
+
├── toArray() → T[] (= DuckDBRow<T>[])
|
|
80
|
+
├── numRows: number
|
|
81
|
+
└── numCols: number
|
|
82
|
+
|
|
83
|
+
DuckDBRow<T> = T ← ドキュメンテーション用エイリアス
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 4. 他モジュールとの関係
|
|
87
|
+
|
|
88
|
+
| 参照元ファイル | 参照する型 | 使われ方 |
|
|
89
|
+
|--------------|-----------|---------|
|
|
90
|
+
| `index.ts` | `DuckDBQueryResult`, `DuckDBRow` | 公開 API として re-export(型のみ) |
|
|
91
|
+
| `utils/duckdb-helpers.ts` | `DuckDBQueryResult` | `duckdbTableToArray` 内の型アサーション(`as DuckDBQueryResult<T>`)、`isDuckDBTable` の型ガード戻り値(`result is DuckDBQueryResult`)。なお `getDuckDBRowCount` / `getDuckDBColumnCount` は `DuckDBQueryResult` を参照せず、`unknown` 型の入力に対して `in` 演算子と `typeof` で直接プロパティを検査する |
|
|
92
|
+
| `hooks/useDuckDB.ts` | (直接参照なし) | フック内部では `unknown` 型で結果を返す |
|
|
93
|
+
| `services/duckdb.ts` | (直接参照なし) | `executeQuery` の戻り値は `Promise<unknown>`(独立して `unknown` を採用) |
|
|
94
|
+
|
|
95
|
+
> **補足**: `DuckDBRow` は `index.ts` から re-export されているが、`src/` 内のどのモジュールでも実際には使用されていない。利用側アプリケーションでのドキュメンテーション用途を想定した型。
|
|
96
|
+
|
|
97
|
+
## 5. 新人向けポイント
|
|
98
|
+
|
|
99
|
+
1. **`DuckDBQueryResult` は「DuckDB の結果はこういう形ですよ」という約束事**。実際の結果は Apache Arrow Table だが、このインターフェースの3つのメンバーだけ知っていればOK。
|
|
100
|
+
2. **`DuckDBRow` は使っても使わなくても動作は同じ**。ただしチーム内で「これは DuckDB の行データ」と分かりやすくするために使う。
|
|
101
|
+
3. **型パラメータ `<T>` を指定すると、IDE の補完が効くようになる**。可能な限り具体的な型を指定しよう。
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# DuckDBService 詳細仕様書
|
|
2
|
+
|
|
3
|
+
> ソースファイル: `src/services/duckdb.ts`
|
|
4
|
+
> 最終更新: 2026-07-05(v1.6.0 — 初期化ライフサイクル / トランザクション / 型推定の堅牢化)
|
|
5
|
+
|
|
6
|
+
## 1. 概要
|
|
7
|
+
|
|
8
|
+
`DuckDBService` は、ブラウザ上で DuckDB-WASM を操作するための**シングルトンサービスクラス**。
|
|
9
|
+
Web Worker の初期化、コネクション管理、SQL クエリの実行、テーブル操作をすべて担当する。
|
|
10
|
+
|
|
11
|
+
## 2. クラス設計
|
|
12
|
+
|
|
13
|
+
### 2.1 デザインパターン
|
|
14
|
+
|
|
15
|
+
- **シングルトンパターン**: `getInstance()` で唯一のインスタンスを取得する。アプリ全体で DuckDB Worker を1つだけ保持し、メモリとリソースを節約する。
|
|
16
|
+
- **オブザーバーパターン**: `addStatusListener` / `removeStatusListener` でステータス変更を購読できる(push 型)。React Hook (`useDuckDB`) がこれを利用する。**状態遷移のたびに `notifyListeners()` が必ず呼ばれる**ため、ポーリングは不要。
|
|
17
|
+
- **操作直列化キュー**: 単一の共有コネクション上で、`executeQuery` / `getTableInfo` / `listTables` と `createTableFromData` の DDL/DML が相互に割り込まないよう、内部の `operationQueue` で直列化する。
|
|
18
|
+
|
|
19
|
+
### 2.2 プライベートプロパティ
|
|
20
|
+
|
|
21
|
+
| プロパティ名 | 型 | 初期値 | 説明 |
|
|
22
|
+
|-------------|------|--------|------|
|
|
23
|
+
| `instance` | `DuckDBService \| null` | `null` | シングルトンインスタンス(static) |
|
|
24
|
+
| `operationFlags` | `Map<string, Promise<void>>` | 空の Map | **進行中の**テーブル作成 Promise(重複呼び出しは同じ Promise を await する) |
|
|
25
|
+
| `workerInstance` | `AsyncDuckDB \| null` | `null` | DuckDB Worker インスタンス(**instantiate 成功後にのみ設定**) |
|
|
26
|
+
| `connectionInstance` | `AsyncDuckDBConnection \| null` | `null` | DB コネクション(再利用される) |
|
|
27
|
+
| `connectionPromise` | `Promise<AsyncDuckDBConnection> \| null` | `null` | コネクション生成中の Promise(同時 connect の二重実行防止) |
|
|
28
|
+
| `isInitializing` | `boolean` | `false` | 初期化中フラグ |
|
|
29
|
+
| `initPromise` | `Promise<AsyncDuckDB> \| null` | `null` | 初期化 Promise(重複呼び出し防止用) |
|
|
30
|
+
| `hasError` | `boolean` | `false` | エラー発生フラグ |
|
|
31
|
+
| `pendingWorker` | `Worker \| null` | `null` | 初期化中の Worker(タイムアウト時などに terminate するため保持) |
|
|
32
|
+
| `pendingWorkerUrl` | `string \| null` | `null` | 初期化中の Blob URL(同上、revoke するため保持) |
|
|
33
|
+
| `operationQueue` | `Promise<unknown>` | `Promise.resolve()` | 操作直列化キュー |
|
|
34
|
+
| `listeners` | `Set<() => void>` | 空の Set | ステータス変更リスナー群 |
|
|
35
|
+
|
|
36
|
+
## 3. メソッド仕様
|
|
37
|
+
|
|
38
|
+
### 3.1 `getInstance(): DuckDBService` (static)
|
|
39
|
+
|
|
40
|
+
インスタンスが未作成なら生成し、シングルトンを返す。JavaScript はシングルスレッドのため排他制御は不要。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### 3.2 `getStatus(): "not-initialized" | "initializing" | "ready" | "error"`
|
|
45
|
+
|
|
46
|
+
| ステータス | 条件 | 意味 |
|
|
47
|
+
|-----------|------|------|
|
|
48
|
+
| `"error"` | `hasError === true` | 初期化に失敗した(最優先で判定) |
|
|
49
|
+
| `"initializing"` | `isInitializing === true` | 初期化処理中 |
|
|
50
|
+
| `"ready"` | `workerInstance !== null` | 利用可能 |
|
|
51
|
+
| `"not-initialized"` | 上記いずれでもない | まだ初期化されていない |
|
|
52
|
+
|
|
53
|
+
> **v1.6.0 での整合性改善**: 初期化に失敗すると `workerInstance` は必ず `null` にリセットされる。したがって `isReady()`(`workerInstance !== null`)と `getStatus()`(`"error"`)が矛盾しなくなった(旧実装では失敗後に `isReady() === true` かつ `getStatus() === "error"` という不整合が起きていた)。
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### 3.3 `initialize(): Promise<AsyncDuckDB>`
|
|
58
|
+
|
|
59
|
+
| 項目 | 内容 |
|
|
60
|
+
|------|------|
|
|
61
|
+
| 戻り値 | 初期化された `AsyncDuckDB` インスタンス |
|
|
62
|
+
| 例外 | 初期化失敗時(タイムアウト含む)に throw |
|
|
63
|
+
| タイムアウト | 120秒(120,000ms、`INIT_TIMEOUT_MS`) |
|
|
64
|
+
|
|
65
|
+
**処理フロー**:
|
|
66
|
+
|
|
67
|
+
```mermaid
|
|
68
|
+
flowchart TD
|
|
69
|
+
A["initialize()"] --> B{workerInstance が存在?}
|
|
70
|
+
B -- Yes --> C["そのまま返す(高速パス)"]
|
|
71
|
+
B -- No --> D{既に初期化中?}
|
|
72
|
+
D -- Yes --> E["initPromise を返す(重複防止)"]
|
|
73
|
+
D -- No --> F["isInitializing = true, hasError = false\nリスナーに通知(initializing)\nアボートシグナル + タイムアウトタイマーを用意"]
|
|
74
|
+
F --> G["Promise.race(\n internalInitializeWorker(signal),\n 120秒タイムアウト\n)"]
|
|
75
|
+
G -- 成功 --> H["clearTimeout\nisInitializing = false\nリスナー通知(ready)\nAsyncDuckDB を返す"]
|
|
76
|
+
G -- 失敗/タイムアウト --> I["clearTimeout\nsignal.aborted = true\nisInitializing = false, hasError = true\nworkerInstance / connectionInstance / connectionPromise / initPromise を null\nabortPendingWorker()(Worker terminate + Blob URL revoke)\nリスナー通知(error)\nエラーを throw"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**v1.6.0 での重要な修正**:
|
|
80
|
+
|
|
81
|
+
1. **失敗後の完全リセットと再試行可能性**: 失敗/タイムアウト時に `workerInstance` を含む全状態を初期化する。旧実装では失敗しても `workerInstance` が「生成済み・未 instantiate」のまま残り、次回 `initialize()` が高速パスで壊れたインスタンスを返し、`hasError` も解除されず、**セッション全体で永久に復旧不能**になっていた。現在は `initialize()` を再度呼べばクリーンに再初期化される。
|
|
82
|
+
2. **タイマーの確実な解除**: タイムアウト用 `setTimeout` を成功/失敗どちらの経路でも `clearTimeout` する(旧実装は解除漏れで最大120秒間タイマーが残留)。
|
|
83
|
+
3. **タイムアウト後の遅延結果を無視**: アボートシグナルにより、タイムアウト後に遅れて `instantiate()` が解決しても、壊れた/整合しないインスタンスを公開しない。
|
|
84
|
+
|
|
85
|
+
**重複呼び出し防止**: 同時に `initialize()` が複数回呼ばれても、2回目以降は `initPromise` を返すため Worker は1回だけ作成される。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### 3.4 `internalInitializeWorker(signal): Promise<AsyncDuckDB>` (private)
|
|
90
|
+
|
|
91
|
+
Worker の実際の初期化処理。**`workerInstance` は全ステップ成功後にのみ `this` に公開する**(途中で失敗/アボートした場合はローカルの Worker / Blob URL を破棄し、壊れたインスタンスを絶対に残さない)。
|
|
92
|
+
|
|
93
|
+
| ステップ | 処理内容 | 補足 |
|
|
94
|
+
|---------|---------|------|
|
|
95
|
+
| 1 | `duckdb.getJsDelivrBundles()` | jsDelivr CDN のバンドル一覧 |
|
|
96
|
+
| 2 | `duckdb.selectBundle()` → **アボート確認** | ブラウザ互換バンドルを選択 |
|
|
97
|
+
| 3 | Blob URL を作成し Web Worker を生成、`pendingWorker`/`pendingWorkerUrl` に退避 | 失敗時の破棄用 |
|
|
98
|
+
| 4 | `new duckdb.AsyncDuckDB(logger, worker)`(ローカル変数 `db`) | まだ `this` に代入しない |
|
|
99
|
+
| 5 | `db.instantiate()` → **アボート確認** | WASM モジュール読み込み |
|
|
100
|
+
| 6 | `splink_udfs` を一時コネクションで INSTALL/LOAD(失敗しても続行、`finally` で close)→ **アボート確認** | 名寄せ用 UDF |
|
|
101
|
+
| 7 | **成功**: `this.workerInstance = db`、`pendingWorker`/`pendingWorkerUrl` をクリア、Blob URL を revoke | ここで初めて公開 |
|
|
102
|
+
| 失敗時 | ローカル `worker.terminate()` と `URL.revokeObjectURL()` を**必ず**実行(各々 try/catch で握りつぶす)、エラーを rethrow | Worker/Blob URL リーク防止 |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### 3.5 `getConnection(): Promise<AsyncDuckDBConnection>`
|
|
107
|
+
|
|
108
|
+
| 項目 | 内容 |
|
|
109
|
+
|------|------|
|
|
110
|
+
| 戻り値 | DuckDB コネクション(1つを保持・再利用) |
|
|
111
|
+
| 動作 | コネクションが無ければ `initialize()` → `connect()` で作成。あれば再利用 |
|
|
112
|
+
|
|
113
|
+
**v1.6.0 での修正**: 生成中の `connectionPromise` を共有することで、**同時呼び出し時の二重 `connect()` を防止**する。`connect()` が失敗した場合は `connectionPromise` を `null` に戻し、次回リトライ可能にする。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### 3.6 `executeQuery(sql: string): Promise<unknown>`
|
|
118
|
+
|
|
119
|
+
| 項目 | 内容 |
|
|
120
|
+
|------|------|
|
|
121
|
+
| 戻り値 | クエリ結果(Apache Arrow Table)。型は `unknown` |
|
|
122
|
+
| タイムアウト | 60秒(60,000ms、`QUERY_TIMEOUT_MS`) |
|
|
123
|
+
| 直列化 | `operationQueue` で他操作と直列化(テーブル構築中の割り込み read を防止) |
|
|
124
|
+
|
|
125
|
+
**v1.6.0 での修正**:
|
|
126
|
+
- タイムアウト用 `setTimeout` を成功/失敗どちらの経路でも `clearTimeout` する(旧実装はクエリ1回ごとに最大60秒残留するタイマーを生成していた)。
|
|
127
|
+
- **タイムアウト時にコネクションを破棄する**: タイムアウトしたクエリはワーカー側で走り続けるため、使用中コネクションを `connectionInstance`/`connectionPromise` から外して close(best-effort)する。これにより後続クエリが詰まったコネクションで待たされず、次回は新しいコネクションで実行される(内部の `runQueryWithTimeout` が担当)。コネクション取得(`getConnection`)自体がハングした場合もタイムアウトが働き、破棄対象が無ければ何もしない。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 3.7 `executeQueries(queries: string[]): Promise<unknown[]>`
|
|
132
|
+
|
|
133
|
+
配列の順番通りに1つずつ**逐次実行**し、結果を配列で返す。CREATE → INSERT → SELECT のような依存クエリ列に適する。
|
|
134
|
+
|
|
135
|
+
**v1.6.0 での修正**: バッチ全体を**1つのキュー単位**(単一 `enqueue`)として実行するため、他操作がクエリ列の途中に割り込まない(`executeQuery` を個別に enqueue する旧実装では、Q1 と Q2 の間に別操作が挟まり得た)。各クエリには従来どおり 60 秒タイムアウトが適用される。なお DB レベルのトランザクション(原子性)ではないため、原子性が必要な場合は `createTableFromData` か明示的な BEGIN/COMMIT を用いる。
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### 3.8 `createTableFromData<T>(tableName, data, options): Promise<void>`
|
|
140
|
+
|
|
141
|
+
JavaScript の配列データから DuckDB テーブルを作成する高レベルメソッド。
|
|
142
|
+
|
|
143
|
+
#### 引数
|
|
144
|
+
|
|
145
|
+
| 引数名 | 型 | 必須 | 説明 |
|
|
146
|
+
|--------|------|------|------|
|
|
147
|
+
| `tableName` | `string` | Yes | 作成するテーブル名(識別子として安全にクォートされる) |
|
|
148
|
+
| `data` | `T[]` | Yes | 投入データ配列(1行以上必須) |
|
|
149
|
+
| `options.dropIfExists` | `boolean` | No | `true` で既存テーブルを削除してから作成 |
|
|
150
|
+
| `options.primaryKey` | `string` | No | 主キーに設定するカラム名 |
|
|
151
|
+
| `options.verbose` | `boolean` | No | **ログ出力の有無のみ**を制御(デフォルト `true`)。DB への追加クエリは発行しない |
|
|
152
|
+
|
|
153
|
+
#### 処理フロー
|
|
154
|
+
|
|
155
|
+
```mermaid
|
|
156
|
+
flowchart TD
|
|
157
|
+
A["createTableFromData(...)"] --> B{data が空 (null / 長さ0)?}
|
|
158
|
+
B -- Yes --> C["Error: 'Data array is empty' を throw"]
|
|
159
|
+
B -- No --> D{operationFlags に同名の進行中 Promise?}
|
|
160
|
+
D -- Yes --> E["**その Promise を await して返す**\n(早期 void 解決しない)"]
|
|
161
|
+
D -- No --> F["buildTable を operationQueue に積み、\noperationFlags に Promise を登録"]
|
|
162
|
+
F --> G["await 完了 → finally で operationFlags を削除"]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**v1.6.0 での重複排除の修正**: 同名テーブルへの同時呼び出しは、進行中の**同一 Promise を await** する。旧実装は2回目の呼び出しが「テーブル未完成のまま即 `void` 解決」し、呼び出し側が存在しない/部分的なテーブルを read してしまう危険があった。
|
|
166
|
+
|
|
167
|
+
#### `buildTable`(private)— トランザクションによる原子的構築
|
|
168
|
+
|
|
169
|
+
```mermaid
|
|
170
|
+
flowchart TD
|
|
171
|
+
A["列名 = 全行の和集合(collectColumnNames)"] --> B["型推定(先頭 100 行、inferColumnTypes)"]
|
|
172
|
+
B --> C["BEGIN TRANSACTION"]
|
|
173
|
+
C --> D{dropIfExists?}
|
|
174
|
+
D -- Yes --> E["DROP TABLE IF EXISTS"]
|
|
175
|
+
D -- No --> F["CREATE TABLE"]
|
|
176
|
+
E --> F
|
|
177
|
+
F --> G["1000 行ずつ INSERT(formatValueForSQL)"]
|
|
178
|
+
G --> H["COMMIT"]
|
|
179
|
+
H -.->|途中で失敗| I["ROLLBACK(失敗しても warn のみ)→ 元エラーを rethrow"]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- **原子性**: DROP/CREATE/INSERT を1トランザクションにまとめ、途中失敗時は `ROLLBACK` する。**半端に作成されたテーブルを残さない**(旧実装は失敗時にデータ無し/部分ロードのテーブルが残った)。
|
|
183
|
+
- **列名は全行の和集合**(`collectColumnNames`)から決定する。旧実装は先頭行のキーのみを使い、後続行だけが持つ列を**無警告で欠落**させていた。CREATE と INSERT で同じ列集合を使う。
|
|
184
|
+
- **`null`/`undefined` 行**はすべて `NULL` として挿入する(不揃いデータへの耐性)。
|
|
185
|
+
|
|
186
|
+
#### 型推定ルール(`inferColumnTypes`、サンプル = 先頭 100 行)
|
|
187
|
+
|
|
188
|
+
| JavaScript の値 | 推定される DuckDB 型 | 条件 |
|
|
189
|
+
|----------------|---------------------|------|
|
|
190
|
+
| `number`(有限・整数) | `BIGINT` | `Number.isFinite` かつ `Number.isInteger`(桁あふれ回避のため一律 BIGINT) |
|
|
191
|
+
| `number`(有限・小数) | `DOUBLE` | 有限だが整数でない |
|
|
192
|
+
| `bigint` | `BIGINT` | `typeof === "bigint"` |
|
|
193
|
+
| `number`(非有限) | (数値扱いしない) | `NaN` / `Infinity` は文字列カウント(挿入時は NULL) |
|
|
194
|
+
| `boolean` | `BOOLEAN` | |
|
|
195
|
+
| `Date` オブジェクト | `TIMESTAMP` | `instanceof Date` |
|
|
196
|
+
| `string`(**厳密**日付) | `TIMESTAMP` | `TIMESTAMP_PATTERN`(`YYYY-MM-DD[ T HH:MM...]`)に一致し、かつ `new Date` が有効 |
|
|
197
|
+
| その他 / 混在 | `VARCHAR` | 上記以外、値がすべて null、型が混在する場合のフォールバック |
|
|
198
|
+
|
|
199
|
+
> **v1.6.0 での修正**:
|
|
200
|
+
> - 整数は `INTEGER` ではなく **`BIGINT`** を採用し、int32 を超える値でのオーバーフローを解消。
|
|
201
|
+
> - TIMESTAMP 推定を厳密化。旧実装の「`new Date()` が有効 かつ `"-"` を含む」は緩すぎ、`ABC-123` のような文字列まで日付扱いして後続行のキャスト失敗を招いた。現在は厳密な日付/日時パターンに一致する場合のみ TIMESTAMP。
|
|
202
|
+
> - サンプルは実際に先頭 100 行を使用(旧実装は「100 行」と称しつつ内部で 10 行に絞る二重スライスのバグがあった)。
|
|
203
|
+
|
|
204
|
+
#### SQL 値フォーマットルール(`formatValueForSQL`)
|
|
205
|
+
|
|
206
|
+
| JavaScript の値 | SQL 出力 | 補足 |
|
|
207
|
+
|----------------|---------|------|
|
|
208
|
+
| `null` / `undefined` | `NULL` | |
|
|
209
|
+
| `string` | `'エスケープ済み'` | シングルクォートを `''` にエスケープ |
|
|
210
|
+
| `Date` | `'2024-01-01T00:00:00.000Z'` | ISO 8601 |
|
|
211
|
+
| `boolean` | `TRUE` / `FALSE` | |
|
|
212
|
+
| `bigint` | `123` | `toString()` |
|
|
213
|
+
| `number`(有限) | `123` / `45.67` | |
|
|
214
|
+
| `number`(非有限: NaN/Infinity) | `NULL` | 有効な SQL 数値リテラルでないため NULL 化 |
|
|
215
|
+
| オブジェクト / 配列 | `'{"x":1}'` / `'["a","b"]'` | `JSON.stringify` してクォート・エスケープ。stringify 不能(循環参照・`undefined`)は `NULL` |
|
|
216
|
+
|
|
217
|
+
> **v1.6.0 での修正**: 旧実装はオブジェクト/配列を `String(value)` で `[object Object]` や `a,b`(列数ずれ)に、非有限数を裸の `NaN`/`Infinity`(無効 SQL)に変換し、INSERT バッチ全体を失敗させ得た。
|
|
218
|
+
|
|
219
|
+
#### 識別子のクォート(`quoteIdentifier`)
|
|
220
|
+
|
|
221
|
+
`tableName` / カラム名 / `primaryKey` はすべて `"..."` でクォートし、内部のダブルクォートを `""` にエスケープする。空文字列や文字列以外の識別子は `Error("Invalid SQL identifier")` を投げる。旧実装は `tableName` を無クォートで DDL/DML に、`information_schema` の `WHERE table_name = '...'` に**文字列リテラルとして生挿入**しており、スペース/予約語/クォートを含む名前で破綻し、リテラル注入点も存在した(診断クエリ自体も v1.6.0 で撤去)。
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### 3.9 `getTableInfo(tableName: string): Promise<unknown>`
|
|
226
|
+
|
|
227
|
+
`DESCRIBE "<tableName>"`(クォート済み)を `operationQueue` 経由で実行し、カラム情報を返す。
|
|
228
|
+
|
|
229
|
+
### 3.10 `listTables(): Promise<unknown>`
|
|
230
|
+
|
|
231
|
+
`SHOW TABLES` を `operationQueue` 経由で実行し、テーブル一覧を返す。
|
|
232
|
+
|
|
233
|
+
### 3.11 `isReady(): boolean`
|
|
234
|
+
|
|
235
|
+
`workerInstance !== null` を返す。§3.2 のとおり失敗時は `null` にリセットされるため `getStatus()` と整合する。
|
|
236
|
+
|
|
237
|
+
### 3.12 `cleanup(): Promise<void>`
|
|
238
|
+
|
|
239
|
+
| ステップ | 処理 |
|
|
240
|
+
|---------|------|
|
|
241
|
+
| 1 | コネクションを close |
|
|
242
|
+
| 2 | Worker を terminate |
|
|
243
|
+
| 3 | `pendingWorker`/`pendingWorkerUrl` を破棄(`abortPendingWorker`) |
|
|
244
|
+
| 4 | 全内部状態(`isInitializing`/`hasError`/`initPromise`/`connectionPromise`/`operationQueue`/`operationFlags`)をリセット |
|
|
245
|
+
| 5 | リスナーに通知してから `listeners` をクリア |
|
|
246
|
+
|
|
247
|
+
teardown 中に例外が出ても `Logger.error` でログするのみで throw しない。cleanup 後に `initialize()` を再度呼べば再初期化できる。
|
|
248
|
+
|
|
249
|
+
### 3.13 `resetInstance(): void` (static)
|
|
250
|
+
|
|
251
|
+
`cleanup()` を呼び出し(**await しない**)、直後に `instance = null` にする。主にテスト用。`instance` が既に `null` の場合は何もしない。
|
|
252
|
+
|
|
253
|
+
## 4. 内部定数
|
|
254
|
+
|
|
255
|
+
| 定数 | 値 | 用途 |
|
|
256
|
+
|------|------|------|
|
|
257
|
+
| `INIT_TIMEOUT_MS` | `120000` | 初期化タイムアウト |
|
|
258
|
+
| `QUERY_TIMEOUT_MS` | `60000` | クエリタイムアウト |
|
|
259
|
+
| `BATCH_SIZE` | `1000` | バッチ INSERT の行数 |
|
|
260
|
+
| `TYPE_INFERENCE_SAMPLE_SIZE` | `100` | 型推定のサンプル行数 |
|
|
261
|
+
| `TIMESTAMP_PATTERN` | 正規表現 | 文字列を TIMESTAMP とみなす厳密パターン |
|