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,288 @@
# PostgreSQL driver 効率改善レポート
MariaDB driver に対して実施した効率改善と同様の観点で、PostgreSQL driver`src/allographer/query_builder/libs/postgres/` および `models/postgres/`)を調査した結果をまとめる。
---
## P0: 重大(即時修正推奨)
### 1. `exec` / `insertId` が DML のたびに `information_schema.columns` を問い合わせている
**ファイル:** `models/postgres/postgres_exec.nim` 196-211行, 214-229行
```nim
proc exec(self:PostgresQuery, queryString:string) {.async.} =
...
let table = self.query["table"].getStr
let columnGetQuery = &"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table}'"
let (columns, _) = postgres_impl.query(..., columnGetQuery, ...).await
postgres_impl.exec(..., queryString, ..., columns, ...).await
```
**問題:**
- INSERT / UPDATE / DELETE のたびに追加で1回 SELECT が発生し、DML 性能が実質2倍遅くなる
- `'{table}'` の文字列補間は SQL インジェクションリスクがある(`insertId` にも同様の問題)
**改善案:**
MariaDB で実施済みの `columnTypeCache: Table[string, seq[seq[string]]]``Connections` に追加し、テーブルごとに初回のみ取得・キャッシュする。SQL はパラメータ化クエリに変更する。
---
## P1: 中程度
### 2. `waiters` が `seq[Future[void]]` で先頭削除が O(n)
**ファイル:** `models/postgres/postgres_types.nim` 20行, `models/postgres/postgres_exec.nim` 28-35行
```nim
# postgres_types.nim
waiters*: seq[Future[void]]
# postgres_exec.nim
proc wakeOnePoolWaiter(pools: Connections) =
while pools.waiters.len > 0:
let w = pools.waiters[0]
pools.waiters.delete(0) # ← O(n) のシフト操作
...
```
**問題:** `seq` の先頭削除は全要素シフトを伴う O(n) 操作。waiter が多い高負荷時に性能劣化する。`removePoolWaiter` も同じくリニアスキャン+シフト。
**改善案:** `std/deques``Deque[Future[void]]` に変更。`popFirst()` が O(1)。MariaDB で実施済み。
### 3. `PgWaitState` が `ref object`(ヒープアロケーション)
**ファイル:** `libs/postgres/postgres_impl.nim` 14行
```nim
type
PgWaitState = ref object
cancelled: bool
```
**問題:** `waitPgReadable` / `waitPgWritable` が呼ばれるたびに `PgWaitState` のヒープアロケーションが発生する。
**改善案:** `object`(値型)に変更。async proc のクロージャ環境に格納されるため、値型でも正しく動作する。MariaDB で実施済み。
### 4. `waitPgReadable` と `waitPgWritable` がほぼ重複している
**ファイル:** `libs/postgres/postgres_impl.nim` 37-81行
```nim
proc waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
...
addRead(fd, readCb) # ← 違いはここだけ
...
proc waitPgWritable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
...
addWrite(fd, writeCb) # ← 違いはここだけ
...
```
**問題:** 2つの proc の差は `addRead` vs `addWrite` だけ。20行以上の完全な重複。
**改善案:** `waitPgIo(db, timeoutMs, forRead: bool)` のような統一 proc にまとめる。
### 5. `waitPgReadable` / `waitPgWritable` が `pqsocket` を二重に呼んでいる
**ファイル:** `libs/postgres/postgres_impl.nim` 37-58行
```nim
proc waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
...
ensurePgSocketRegistered(db) # 内部で pqsocket(db) を呼ぶ
let sock = pqsocket(db) # ← 再度 pqsocket を呼んでいる
if sock < 0:
dbError(db)
let fd = AsyncFD(cint(sock))
...
```
**問題:** `ensurePgSocketRegistered` 内で `pqsocket` を呼んで fd を得ているのに、その戻り値を使わず直後にもう一度 `pqsocket` を呼んでいる。FFI 呼び出しの無駄。
**改善案:** `ensurePgSocketRegistered``AsyncFD` を返す proc にする(MariaDB 版 `ensureMariadbSocketRegistered` と同じ設計)。
---
## P2: 軽微
### 6. `getTime().toUnix()` によるデッドライン計算(秒精度)
**ファイル:** `libs/postgres/postgres_impl.nim` 83-89行, 149-150行 他多数
```nim
proc pgRemainingMs(deadline: int64): int =
let leftSec = deadline - getTime().toUnix()
...
result = int(leftSec * 1000)
# deadline の生成(秒精度)
let calledAt = getTime().toUnix()
let deadline = calledAt + timeout.int64
```
**問題:**
- `getTime().toUnix()` は秒精度のため、ミリ秒単位のタイムアウト制御ができない
- NTP 補正によるクロック巻き戻りの影響を受ける
- `timeout = 1` のケースでは、0.9秒経過後に残り 0ms と判定される可能性がある
**改善案:** `std/monotimes``MonoTime` + `Duration` に統一する。MariaDB で実施済み。
### 7. `setColumnInfo` が行ごとに全カラム分の FFI 呼び出しを行っている
**ファイル:** `libs/postgres/postgres_lib.nim` 159-167行, `libs/postgres/postgres_impl.nim` 160-163行 他
```nim
# postgres_lib.nim
proc setColumnInfo*(res: PPGresult; dbRows: var DbRows; line, cols: int32) =
var columns: DbColumns
setLen(columns, cols)
for col in 0'i32..cols-1:
columns[col].name = $pqfname(res, col) # 行に依存しない
columns[col].typ = getColumnType(res, line, col) # NULL判定のみ行依存
columns[col].tableName = $(pqftable(res, col)) # 行に依存しない
dbRows.add(columns)
# postgres_impl.nimquery* 内)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
setColumnInfo(pqresult, dbRows, i, cols) # ← 毎行で全カラム FFI 呼び出し
rows.add(row)
```
**問題:** `pqfname`, `pqftable`, `pqftype` はカラムの OID・名前・テーブル名を返す関数で、行によって結果が変わらない。行に依存するのは `pqgetisnull(res, line, col)` による NULL 判定のみ。1000行×10カラムでは、カラム名・テーブル名だけで2万回の不要な FFI 呼び出し+文字列アロケーションが発生する。
**改善案:** カラムの基本情報(name, tableName, OID)はループ外で1回だけ取得し、行ごとには NULL 判定のみ行う。
```nim
# 基本情報を1回だけ取得
var baseColumns: DbColumns
setLen(baseColumns, cols)
for col in 0'i32..cols-1:
baseColumns[col].name = $pqfname(res, col)
baseColumns[col].typ = getBaseColumnType(res, col) # NULL判定なし
baseColumns[col].tableName = $(pqftable(res, col))
# 行ごとにNULLだけ上書き
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
var rowColumns = baseColumns
for col in 0'i32..cols-1:
if pqgetisnull(res, i, col) == 1:
rowColumns[col].typ = DbType(kind: dbNull, name: "null")
dbRows.add(rowColumns)
rows.add(row)
```
### 8. `dbFormat` / `questionToDaller` の char-by-char 連結
**ファイル:** `libs/postgres/postgres_lib.nim` 203-216行, 219-229行
```nim
proc dbFormat*(formatstr: string, args: varargs[string]): string =
result = ""
...
for c in items(formatstr):
if c == '?':
add(result, dbQuote(args[a]))
inc(a)
else:
add(result, c) # ← 1文字ずつ add
proc questionToDaller*(s:string):string =
var i = 1
for c in s:
if c == '?':
result.add(&"${i}")
i += 1
else:
result.add(c) # ← 1文字ずつ add
```
**問題:** `?` が出現するまでの連続部分を1文字ずつ `add` しており、長いクエリ文字列で非効率。
**改善案:** `?` 位置間のサブ文字列を一括 `add` する。MariaDB の `dbFormat` で実施済みの手法。
### 9. `query*` / `rawQuery*` / `execGetValue*` のパラメータ送信パターンが重複
**ファイル:** `libs/postgres/postgres_impl.nim` 134-166行, 193-225行, 228-260行
```nim
# query*, execGetValue*, rawQuery* の冒頭がほぼ同一:
let status =
if pgParams.nParams > 0:
pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, pgParams.values, ...)
else:
pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, nil, nil, nil, 0)
defer:
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
if status != 1: dbError(db)
```
**問題:** 4箇所にわたるクエリ送信ボイラープレート。保守コストが高い。
**改善案:** `pgSendQuery(db, pgParams)` のような共通 proc を抽出する。
### 10. `fromObjArray` の2つのオーバーロードが大部分重複
**ファイル:** `libs/postgres/postgres_lib.nim` 239-291行, 293-337行
**問題:** `columns` パラメータの有無のみが異なり、JSON→string 変換ロジックは同一。
**改善案:** 共通の内部 proc を抽出し、`columns` が空の場合はバイナリ判定をスキップする。
---
## P3: 微小
### 11. `dbQuote` の容量見積もりなし
**ファイル:** `libs/postgres/postgres_lib.nim` 182-201行
```nim
proc dbQuote(s: string): string =
if s == "null":
return "NULL"
result = "'"
...
```
**問題:** `newStringOfCap` による事前容量確保がない。特殊文字が多い文字列で再アロケーションが発生する。
**改善案:** `result = newStringOfCap(s.len + 2)` を先頭に追加。
---
## 改善優先度まとめ
| 優先度 | # | 問題 | 影響 | 修正難度 |
|---|---|---|---|---|
| **P0** | 1 | `exec`/`insertId` が毎回 `information_schema` を問い合わせ + SQL インジェクション | DML ごとに追加1クエリ | 中(キャッシュ + パラメータ化) |
| **P1** | 2 | `waiters``seq.delete(0)` が O(n) | 高負荷時の性能劣化 | 低(`Deque` に変更) |
| **P1** | 3 | `PgWaitState``ref object` | 毎回のヒープアロケーション | 低(`object` に変更) |
| **P1** | 4 | `waitPgReadable`/`waitPgWritable` が重複 | 保守コスト | 低(統一) |
| **P1** | 5 | `pqsocket` 二重呼び出し | 不要な FFI 呼び出し | 低 |
| **P2** | 6 | `getTime().toUnix()` の秒精度デッドライン | タイムアウト精度 | 低(`MonoTime` 化) |
| **P2** | 7 | `setColumnInfo` が行ごとに全カラム FFI 呼び出し | 行数×カラム数の不要な FFI + アロケーション | 中(構造変更) |
| **P2** | 8 | `dbFormat`/`questionToDaller` の char-by-char | 長いクエリで微小な差 | 低 |
| **P2** | 9 | クエリ送信ボイラープレート重複 | 保守コスト | 低 |
| **P2** | 10 | `fromObjArray` の重複 | 保守コスト | 低 |
| **P3** | 11 | `dbQuote` の容量見積もりなし | 微小 | 低 |
---
## MariaDB との比較
| 項目 | MariaDB(修正済み) | PostgreSQL(現状) |
|---|---|---|
| カラム型キャッシュ | `columnTypeCache` 導入済み | 毎回 `information_schema` を問い合わせ |
| ブロッキング ping | 除去済み | `assert db.status == CONNECTION_OK`FFI なし、問題小) |
| `setColumnInfo` 位置 | ループ外に移動済み | ループ内で毎行呼び出し |
| waiter データ構造 | `Deque` に変更済み | `seq`(先頭削除 O(n) |
| 待機状態オブジェクト | 値型(`object`)に変更済み | `ref object`(ヒープ割当) |
| デッドライン | `MonoTime` に変更済み | `getTime().toUnix()`(秒精度) |
| `dbFormat` | セグメント一括 add | char-by-char |
| SQL インジェクション | プレースホルダ化済み | 文字列補間のまま |
**注意:** PostgreSQL の `assert db.status == CONNECTION_OK``pqStatus()` がローカルのフィールド読み取りであり、MariaDB の `mysql_ping()` のようなネットワークラウンドトリップは発生しない。そのため、PostgreSQL 側はこの assert の除去は不要。