feat(clients): add nim-allographer client with BaraDB driver support
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

- Pure Nim wire protocol client (async + sync, no C FFI)
- Query Builder: fluent API, SQL generator, execution, transactions
- Schema Builder: create/alter/drop tables, column operations
- Connection pool with aging and timeout handling
- ORM integration via compile-time DB_BARADB switch
- Tests: test_open, test_query
- Documentation: README.md, PLAN_BARADB.md
This commit is contained in:
2026-05-21 15:49:11 +03:00
parent 2d310a33a1
commit 6bfc5b3a3c
654 changed files with 109419 additions and 0 deletions
@@ -0,0 +1,222 @@
# Surreal driver 効率改善レポート
> [!WARNING]
> SurrealDB support is planned for v3. The current implementation and proposed improvements are for reference only and will be addressed in v3.
`src/allographer/query_builder/libs/surreal/` と、その呼び出し元である `models/surreal/` を調査し、`documents/rdb/postgres_efficiency_report.md``320-allographerpostgresql-非同期待機改善-設計書.mdc` と同じ観点で、処理効率と待機制御の改善余地を整理した。
MariaDB / PostgreSQL 側で既に改善済みのポイントと比べると、Surreal 側は以下の 2 点が特に目立つ。
- 接続プール待機はすでに `Deque` + `MonoTime` ベースで改善済み
- 一方で HTTP リクエスト自体の timeout 制御と、初期化時の重複 round-trip は未整理
---
## P0: 重大
### 1. `timeout` 引数が HTTP リクエスト経路で使われていない
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_impl.nim` 47-112行
```nim
proc query*(db:SurrealConn, query: string, args: JsonNode, timeout:int):Future[JsonNode] {.async.} =
let query = dbFormat(query, args)
let resp = db.client.post(&"{db.host}:{db.port}/sql", query).await
let body = resp.body().await.parseJson()
```
**問題:**
- `query` / `exec` / `info` の全経路で `timeout` が未使用
- 遅い HTTP 応答やハングした接続があると、その接続スロットを無期限に占有する
- `getFreeConn` 側は `MonoTime` で待機期限を持っていても、実リクエストが返らない限りコネクションは返却されず、スループットが崩れる
- PostgreSQL 設計書で整理した「待機制御を driver 内で完結させる」という方針と比べると、Surreal だけ実処理 timeout が抜けている
**改善案:**
- `post()``body()``withTimeout` で包み、`MonoTime` ベースの deadline へ統一する
- もしくは `AsyncHttpClient` 側の timeout 設定を `dbOpen` で明示し、driver 引数 `timeout` と同期させる
- エラー整形も共通化し、timeout 時は `DbError` へ寄せる
---
## P1: 中程度
### 2. `dbOpen` が接続数ぶん `/status` と `DEFINE NAMESPACE / DATABASE` を逐次実行している
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_open.nim` 17-35行
```nim
for i in 0..<maxConnections:
let client = newAsyncHttpClient()
...
var resp = client.get(url).await
...
resp = client.post(url, &"DEFINE NAMESPACE `{namespace}`; USE NS `{namespace}`; DEFINE DATABASE `{database}`").await
```
**問題:**
- 初期化コストが `maxConnections` に比例して直線的に増える
- 各接続ごとに `/status``DEFINE ...` を逐次実行しており、プール 10 本なら最低 20 回の HTTP round-trip が発生する
- `DEFINE NAMESPACE` / `DEFINE DATABASE` はプール内の全接続で毎回やる必要が薄く、重複コストになりやすい
- ループが直列なので、起動時間が RTT の影響を強く受ける
**改善案:**
- namespace/database の bootstrap は 1 接続だけで先に済ませ、残りは接続確認だけにする
- もしくは初期化を 1 本目の成功後に `allFutures` 系で並列化する
- あわせて `Authorization` の Base64 生成や URL 生成もループ外へ寄せる
### 3. `dbFormat(queryString, args: JsonNode)` が中間文字列を多量に生成する
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_lib.nim` 77-118行
```nim
var strArgs: seq[string]
...
strArgs.add(&"LET ${numToAlphabet(i)} = {$arg.getInt}; ")
...
result = strArgs.join() & queryPart
```
**問題:**
- 各引数ごとに `&` で新しい文字列を作る
- `strArgs` に積んだ後で `join()`、最後に `& queryPart` でもう一度連結する
- プレースホルダ数が多い bulk insert / update 系ほどアロケーションが増える
- MariaDB / PostgreSQL の `dbFormat` 改善と違い、Surreal の `JsonNode` 経路はまだ単一パス化されていない
**改善案:**
- `newStringOfCap` で見積もり確保し、`LET $a = ...; ``result` へ直接 `add` する
- `strArgs: seq[string]``join()` を廃止し、最後に `queryPart``add` する
- placeholder 名は毎回 `numToAlphabet` で再計算せず、ローカルキャッシュか直接 append する
### 4. `query` / `exec` / `info` の HTTP 実行ロジックが 6 箇所に重複している
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_impl.nim` 47-112行
```nim
let query = dbFormat(query, args)
let resp = db.client.post(&"{db.host}:{db.port}/sql", query).await
let body = resp.body().await.parseJson()
```
**問題:**
- `seq[string]` / `JsonNode` の 2 系統で `query`, `exec`, `info` がほぼ同一実装
- timeout 対応、HTTP status 判定、JSON parse、Surreal エラー整形を入れたいときに 6 箇所修正になる
- 毎回 `&"{db.host}:{db.port}/sql"` を再生成しており、ホットパスに細かな再アロケーションが残る
**改善案:**
- `runSql(db, sql: string): Future[JsonNode]` のような共通 proc を切り出す
- `dbFormat` だけ呼び出し側オーバーロードで吸収し、HTTP 送信・JSON parse・エラー判定は 1 箇所へ寄せる
- `SurrealConn``sqlUrl` を保持し、ホットパスの `strformat` をなくす
---
## P2: 軽微
### 5. `numToAlphabet` が placeholder ごとに前置連結と `toLower()` を行う
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_lib.nim` 44-55行
```nim
result = ""
while n > 0:
...
result = chr(int('A') + remainder) & result
...
return result.toLower()
```
**問題:**
- `& result` による前置連結で、文字数に応じた再アロケーションが発生する
- 最後に `toLower()` でもう 1 回文字列を作る
- 1 回のコストは小さいが、`dbFormat(JsonNode)``questionToDaller()` の両方から頻繁に呼ばれる
**改善案:**
- 小さなバッファへ末尾追加して最後に reverse する
- もしくは最初から `'a'` 基準で生成して `toLower()` をなくす
### 6. `questionToDaller()` が `numToAlphabet()` と組み合わさって placeholder 数ぶん文字列生成を行う
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_lib.nim` 58-74行
```nim
if s[j] == '?':
...
result.add('$')
result.add(numToAlphabet(i))
```
**問題:**
- 文字列本体はセグメント単位で `add` できており悪くないが、placeholder 名生成が都度アロケーションになる
- `find()` 経路では `surreal_exec.nim` 366-369行で毎回 `questionToDaller` を通る
**改善案:**
- `numToAlphabet` 改善とセットで最適化する
- placeholder 数が少ないケースでは効果は小さいため、P1 の `dbFormat(JsonNode)` ほど優先度は高くない
### 7. `getAllRows()` が `JsonNode` 配列を `seq[JsonNode]` へ変換するため、結果件数ぶん追加走査が入る
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_exec.nim` 121-138行, 184-201行
```nim
let rows = surreal_impl.query(...).await
...
return rows.toSeq
```
**問題:**
- `surreal_impl.query()` はすでに `JsonNode` の配列を返している
- `get()` / `insertId(seq)` のたびに `toSeq()` でもう一度全件を走査する
- 大量行取得では余分なイテレーションと `seq` 生成が発生する
**改善案:**
- 内部表現を `JsonNode` で統一して必要箇所だけ `seq` 化する
- あるいは `surreal_impl.query()` 側で最終的に必要な `seq[JsonNode]` を 1 回だけ組み立てる
---
## すでに良い点
### 1. プール待機は `Deque` + 通知ベースで、PostgreSQL 改善後と同じ方向にそろっている
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_exec.nim` 24-75行
- `waiters``Deque[Future[void]]`
- `wakeOnePoolWaiter()``popFirst()` で O(1)
- `sleepAsync(10)` ポーリングは使っていない
### 2. プール待機の deadline は `MonoTime` ベース
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_exec.nim` 40-69行
- PostgreSQL 改善設計書で問題視していた wall clock 依存は入っていない
- ただし「接続取得待ち」だけで、「HTTP 実行待ち」にはまだ広がっていない
---
## 改善優先度まとめ
| 優先度 | # | 問題 | 影響 | 修正難度 |
|---|---|---|---|---|
| **P0** | 1 | HTTP リクエストで `timeout` 未使用 | ハング時に接続が返却されず、プール全体が詰まる | 中 |
| **P1** | 2 | `dbOpen` が接続数ぶん逐次 bootstrap | 起動時間が `maxConnections` に比例 | 低〜中 |
| **P1** | 3 | `dbFormat(JsonNode)` の中間文字列多発 | bulk 系で CPU / アロケーション増 | 中 |
| **P1** | 4 | HTTP 実行ロジック 6 箇所重複 | 最適化と timeout 対応の実装コスト増 | 低 |
| **P2** | 5 | `numToAlphabet` の前置連結 + `toLower()` | placeholder 多いと微小劣化 | 低 |
| **P2** | 6 | `questionToDaller()` の placeholder 名生成 | 微小 | 低 |
| **P2** | 7 | `rows.toSeq()` の追加走査 | 大量行取得で余分な走査 | 低〜中 |
---
## PostgreSQL / MariaDB との比較
| 項目 | MariaDB / PostgreSQL | Surreal(現状) |
|---|---|---|
| プール待機 | `Deque` + 通知ベース | 同様に改善済み |
| deadline 管理 | `MonoTime` 利用 | 接続取得待ちは利用、HTTP 実行待ちは未適用 |
| 実リクエスト timeout | driver 内で明示設計 | `timeout` 引数が未使用 |
| 初期化コスト | DB 接続確立中心 | `/status` + `DEFINE ...` を接続数ぶん HTTP 実行 |
| フォーマット最適化 | `dbFormat` 改善済み箇所あり | `JsonNode` 経路は未最適化 |
## 次に着手するなら
1. `surreal_impl.nim` に共通 `runSql` ヘルパを作り、timeout・HTTP status・JSON parse・Surreal error 判定を 1 箇所へ寄せる
2. `surreal_open.nim` の bootstrap を 1 回化し、残り接続は軽量に初期化する
3. `surreal_lib.nim``dbFormat(JsonNode)` を単一バッファ書き込みへ変更する