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
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:
@@ -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.nim(query* 内)
|
||||
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 の除去は不要。
|
||||
Reference in New Issue
Block a user