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,31 @@
---
alwaysApply: true
---
現在のブランチ名と一致するファイル名のファイルが、リポジトリのルートから見て `.cursor/rules/branch/` 配下にあった場合は、それを読み込んでください。
各ブランチルールは、ブランチで行うべき開発内容、タスクを小さい粒度で分けた進捗、参考資料の一覧を含みます。
ブランチルールファイルの拡張子は `.mdc` です。
エージェントは開発の進度に従って進捗を適時記録してください。
実装する中で `調査結果・設計まとめ` に間違いがあると判明した場合は、その都度、より正しいと考えられる設計方針を書き込んでください。
以下はブランチルールの雛形です
```
---
description: {ブランチ名}ブランチでの開発時に読み込む
alwaysApply: false
---
{ブランチルール名}
===
このブランチで実装することは以下の通りです。
-
## 進捗
- [ ]
## 参考資料
-
## 調査結果・設計まとめ
```
@@ -0,0 +1,130 @@
---
description: 318-bug-incorrect-sqlite-check-constraint-for-columnchar-columnstring-and-columnuuidブランチでの開発時に読み込む
alwaysApply: false
---
SQLite CHECK Constraint 引用誤り修正 ブランチルール
===
このブランチで実装することは以下の通りです。
- SQLite の schema builder において、`Column.char`, `Column.string`, `Column.uuid` が生成する `CHECK` 制約の列参照が文字列リテラル扱いになっている不具合を修正する。
- `CREATE TABLE` と `ALTER TABLE ... ADD COLUMN` の両方で、`length('column_name')` ではなく列値を参照する `length(column_name)` 系の SQL を生成するように整える。
- SQLite driver の修正を公開 API へ漏らさず、driver 内部の SQL 生成責務に閉じ込める。
- 長さ制約の既定値あり・既定値なしの両ケースで、`char` `string` `uuid` の意味論が崩れないことを確認できるテストを整備する。
- 必要に応じて README / `documents/rdb/schema_builder.md` の SQLite 例を同期し、誤った `CHECK (length('email') <= 255)` 表記を残さない。
## 進捗
- [x] `.cursor/rules/project.mdc` と `.cursor/rules/branch.mdc` を確認した
- [x] 現在のブランチ名に対応するブランチルールファイルが未作成であることを確認した
- [x] SQLite の `CHECK` 生成箇所を調査し、`create_column_query.nim` と `add_column_query.nim` の両方に同種の問題があることを確認した
- [x] 対象カラム型が `Column.char`, `Column.string`, `Column.uuid` の 3 系統であることを確認した
- [x] ドキュメントに同じ誤表記が残っていることを確認した
- [x] ブランチルールに改修方針・進捗粒度・参考資料を整理した
- [x] SQLite schema builder の `CHECK` 制約生成を、列名を文字列リテラル化しない形式へ修正する
- [x] `CREATE TABLE` 経路のテストを追加または更新し、生成 SQL が列値長を検査することを確認する
- [x] `ADD COLUMN` 経路のテストを追加または更新し、生成 SQL が列値長を検査することを確認する
- [x] `char` `string` `uuid` の既定長と明示長の両ケースで回帰確認する
- [x] 必要な README / `documents/rdb/schema_builder.md` の記述を同期する
- [ ] 実装途中で設計認識に誤りが見つかった場合は、本ファイルの `調査結果・設計まとめ` を更新する
## 参考資料
- `src/allographer/schema_builder/queries/sqlite/sub/create_column_query.nim`
- `src/allographer/schema_builder/queries/sqlite/sub/add_column_query.nim`
- `tests/sqlite/test_create_schema.nim`
- `tests/sqlite/test_schema.nim`
- `documents/rdb/schema_builder.md`
- `README.md`
- SQLite Documentation: CHECK constraints
- SQLite Documentation: core functions `length(...)`
## 調査結果・設計まとめ
### 背景
SQLite 実装では、文字列長を制限するために `CHECK (length(...) <= N)` を列定義へ付与している。
しかし現状の SQL 生成は `length('{column.name}')` となっており、列値ではなく列名そのものを単一引用符付きの文字列リテラルとして評価している。
その結果、たとえば `name` 列では常に `length('name') == 4` と評価されるため、期待した列値バリデーションにならない。
### 現状の問題
1. SQLite の `CREATE TABLE` 用列生成で `CHECK (length('{column.name}') <= {maxLength})` を出力している
2. SQLite の `ADD COLUMN` 用列生成でも同じ誤りを重複して持っている
3. 影響範囲が `Column.char`, `Column.string`, `Column.uuid` にまたがっている
4. ドキュメント例にも同じ誤表記があり、利用者が誤仕様と誤解する余地がある
5. SQL 生成の不具合であり、公開 API ではなく SQLite driver 内部責務として直すべき問題である
### 設計目標
1. SQLite で生成される長さ `CHECK` 制約が常に列値を参照する
2. `char`, `string`, `uuid` の API 意味論を変えずに不具合のみを修正する
3. `create` と `add column` の両経路で SQL 生成規則を揃える
4. 既存の RDB 向け共通 API を汚さず、SQLite 実装へ変更を閉じ込める
5. テストと文書で回帰防止できる状態にする
### 非目標
1. SQLite 以外の driver の長さ制約仕様を同時に変更すること
2. `Column.char`, `Column.string`, `Column.uuid` の既定長そのものを見直すこと
3. schema builder 全体の quoting 戦略をこのブランチで全面再設計すること
4. 公開 API の追加・削除・引数変更を行うこと
### 実装方針
#### 1. SQLite の長さ `CHECK` 制約は列値参照に統一する
変更対象:
- `src/allographer/schema_builder/queries/sqlite/sub/create_column_query.nim`
- `src/allographer/schema_builder/queries/sqlite/sub/add_column_query.nim`
方針:
- `length('{column.name}')` を列値参照の式へ修正する
- `char`, `string`, `uuid` の 3 系統で同じルールを適用する
- `CREATE TABLE` と `ADD COLUMN` で生成式の差異を作らない
#### 2. 列名の扱いは SQLite 列参照として妥当な形を維持する
要点:
- 単一引用符 `'...'` は文字列リテラルなので使用しない
- 既存の SQLite SQL 生成スタイルに合わせ、列参照として解釈される形を採用する
- 必要なら identifier quoting の既存ルールを確認し、単純な文字列置換ではなく SQLite 方言として自然な表現に寄せる
- 共通 helper を `schema_utils` に置き、`CREATE TABLE` / `ADD COLUMN` の両経路で同じ生成規則を使う
#### 3. テストは SQL 文字列と実動作の両面を意識して追加する
候補:
1. schema 生成テストで `CHECK` 句が列値参照になっていることを確認する
2. `Column.string("name")` の既定長ケースを確認する
3. `Column.char("name", 20)` の明示長ケースを確認する
4. `Column.uuid("uuid")` の既定長ケースを確認する
5. `add()` 経路でも同じ `CHECK` 句になることを確認する
### 受け入れ基準
1. SQLite の `char`, `string`, `uuid` が生成する `CHECK` 制約で列名が単一引用符付き文字列になっていない
2. `CREATE TABLE` と `ADD COLUMN` の両方で同じ修正が反映されている
3. 追加または更新したテストで不具合再発を防止できる
4. 必要なドキュメント例が実装と一致している
5. 公開 API 変更なしで SQLite driver 内に修正が閉じ込められている
### 参照仕様
- SQLite Documentation: CHECK constraints
https://www.sqlite.org/lang_createtable.html#ckconst
- SQLite Documentation: Built-In Scalar SQL Functions
https://www.sqlite.org/lang_corefunc.html#length
### リスク
1. 列名参照の quoting 形式を誤ると、今度は予約語や特殊文字を含む列名で不整合が出る可能性がある
2. `create` と `add column` の片方だけ修正すると、経路差による再発が起きる
3. ドキュメント未更新のままだと、修正後も利用者が誤った SQL 例を参照して混乱する
4. SQL 文字列比較系テストだけだと、実際の制約評価の誤りを見逃す可能性がある
### 段階導入案
1. 影響箇所と既存 SQLite テストを整理する
2. `create` / `add column` の SQL 生成を修正する
3. `char` / `string` / `uuid` の回帰テストを追加または更新する
4. 必要な README / documents を同期する
5. 実装差分が設計とずれたら本ファイルを更新する
@@ -0,0 +1,156 @@
---
description: 320-allographerpostgresql-非同期待機改善-設計書ブランチでの開発時に読み込む
alwaysApply: false
---
allographer(PostgreSQL) 非同期待機改善 ブランチルール
===
このブランチで実装することは以下の通りです。
- allographer の PostgreSQL 実装に残っている `sleepAsync(10)` ベースの非同期待機を、libpq ソケット readiness ベースの待機へ置き換える。
- `query*`, `queryPlain*`, `exec*`, `getColumns*` など、同一問題を持つ待機経路を共通化して修正する。
- `dbOpen` 時に PostgreSQL 接続を non-blocking 化し、libpq 非同期 API の前提を明示的に保証する。
- 接続プール空き待機を `sleepAsync(10)` ポーリングから通知ベースへ移行し、プール枯渇時の待機遅延を抑制する。
- 公開 API 互換を維持したまま、httpbeast/httpx 利用時の DB 完了後の回収遅延を縮小する。
## 進捗
- [x] `src/allographer/query_builder/libs/postgres/postgres_impl.nim` の待機ループを調査し、`sleepAsync(10)` 依存箇所を一覧化する
- [x] `src/allographer/query_builder/libs/postgres/postgres_rdb.nim` を確認し、既存宣言で再利用できる libpq API と不足宣言を整理する(追加宣言不要。`pqsocket` / `pqflush` / `pqsetnonblocking` 等は既存)
- [x] PostgreSQL 待機ヘルパ `waitPgReadable` / `waitPgWritable` / `cancelQuery` の責務と配置を決める(`postgres_impl.nim` 内の非公開プロシージャ)
- [x] `PQsocket` / `PQconsumeInput` / `PQisBusy` / `PQgetResult` / `PQflush` に基づく共通クエリ待機ループを設計する(`pgFlushOutgoing` / `pgNextResult` / `pgEnsureIdle`
- [x] `query*`, `queryPlain*`, `exec*`, `getColumns*` の各経路を共通待機ロジックへ寄せる
- [x] タイムアウト時の `cancelQuery` 実行とエラーメッセージ方針を統一する(タイムアウトは `cancelQuery` 後に `DbError`
- [x] `src/allographer/query_builder/models/postgres/postgres_open.nim` で `pqsetnonblocking(conn, 1)` と検証処理を追加する
- [x] `src/allographer/query_builder/models/postgres/postgres_types.nim` に接続プール waiter キューを追加する(`waiters*: seq[Future[void]]`
- [x] `src/allographer/query_builder/models/postgres/postgres_exec.nim` の `getFreeConn` / `returnConn` を通知ベースへ置換する
- [x] PostgreSQL の単体・結合・回帰テストを追加または更新する(`tests/postgres/test_pool_wait.nim` 追加、既存 `test*.nim` 回帰)
- [x] `sleepAsync(10)` が PostgreSQL クエリ待機とプール空き待機から除去されたことを確認する
- [ ] README / documents / ルール文書に仕様差分が必要か確認し、必要分を同期する(利用者向け公開 API 変更なしのため未着手。必要なら追記)
- [x] MariaDB: `mariadb_rdb.nim` に `mysql_read_query_result``cdecl`)を追加
- [x] MariaDB: `mariadb_impl.nim` は接続ブロッキング前提で `mysql_real_query` / `mysql_read_query_result` / `mysql_fetch_row` / `free_result` を同期利用(`mysql_read_query_result_*` / `fetch_row_*` の async ループは環境でタイムアウト・デッドロックのため不使用)
- [x] MariaDB: `mariadb_open.nim` は `MYSQL_OPT_NONBLOCK` を付与しない(同上)
- [x] MariaDB: `mariadb_types` / `mariadb_exec` のプール空き待ちは通知ベース(`tests/mariadb/test_pool_wait.nim`
## 参考資料
- allographer の PostgreSQL 実装: `src/allographer/query_builder/libs/postgres/postgres_impl.nim`
- allographer の PostgreSQL 実行管理: `src/allographer/query_builder/models/postgres/postgres_exec.nim`
- allographer の PostgreSQL 接続初期化: `src/allographer/query_builder/models/postgres/postgres_open.nim`
- allographer の PostgreSQL 型定義: `src/allographer/query_builder/models/postgres/postgres_types.nim`
- libpq 非同期 API: `PQsocket`, `PQconsumeInput`, `PQisBusy`, `PQgetResult`, `PQsetnonblocking`, `PQflush`, `PQgetCancel`, `PQcancel`, `PQfreeCancel`
- Nim `asyncdispatch` のイベントループ仕様
- httpbeast / httpx の POSIX selector ループ実装
## 調査結果・設計まとめ
### 背景
`httpbeast` / `httpx` 利用時のみ、DB クエリ待機が `asynchttpserver` より大きく悪化する事象がある。
現状の allographer PostgreSQL 実装は、libpq 非同期 API を使いながらも、実際の待機を `await sleepAsync(10)` に依存している。
### 問題の本質
- 現状の PostgreSQL 待機は、ソケット readiness 待ちではなく 10ms 刻みのポーリングである
- Nim の `asyncdispatch` は `poll` / `waitFor` / `runForever` が回らない限りタイマーも進まない
- httpbeast / httpx の POSIX 経路は `selectInto(-1, ...)` による無期限ブロックを中心としており、`asyncdispatch.poll(0)` は限定的にしか回らない
- そのため `sleepAsync(10)` は実時間 10ms で復帰せず、「次に HTTP 側ループがたまたま dispatcher を回すまで」遅延する
- 結果として、DB は完了済みでも allographer 側の結果回収が遅れ、体感レイテンシが悪化する
### 設計目標
1. PostgreSQL クエリ待機から `sleepAsync` ポーリングを除去する
2. libpq ソケットの可読・可書込イベントで待機する
3. 接続プール空き待ちも通知ベースへ移行し、タイマー依存を減らす
4. `dbOpen`, `table`, `select`, `find`, `update` などの公開 API 互換を維持する
### 非目標
1. Query Builder API の破壊的変更
2. MySQL / SQLite への同時展開(MariaDB は PostgreSQL と同方針で非同期待機を実装済み)
3. 初回対応で全 OS 最適化を完了させること
### 実装方針
#### 1. libpq 待機を FD イベント待機へ変更する
変更対象:
- `src/allographer/query_builder/libs/postgres/postgres_impl.nim`
- `src/allographer/query_builder/libs/postgres/postgres_rdb.nim`
追加する内部ヘルパ:
- `waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool]`
- `waitPgWritable(db: PPGconn, timeoutMs: int): Future[bool]`
- `cancelQuery(db: PPGconn)`
要点:
- `pqsocket(db)` で FD を得る
- 必要に応じて dispatcher に `AsyncFD` を登録する
- read / write readiness を Future 完了で待つ
- タイムアウト管理を行う
- 登録リークや未解除ハンドラを残さない
#### 2. クエリ実行ループを共通化する
送信フェーズ:
1. `pqsendQueryParams(...)` を呼ぶ
2. `pqflush == 1` の間は `waitPgWritable` で待機する
受信フェーズ:
1. `pqconsumeInput`
2. `pqisBusy == 1` なら `waitPgReadable` で待機する
3. ready 後に `pqgetResult` を drain する
タイムアウト時は `cancelQuery` を実行し、エラーを返す。
#### 3. `dbOpen` で non-blocking を保証する
変更対象:
- `src/allographer/query_builder/models/postgres/postgres_open.nim`
仕様:
1. `pqsetnonblocking(conn, 1)` を実行する
2. 失敗時は `DbError`
3. `pqisnonblocking(conn) == 1` を確認し、保証できない場合は例外化する
#### 4. 接続プール空き待ちを通知ベースに変更する
変更対象:
- `src/allographer/query_builder/models/postgres/postgres_types.nim`
- `src/allographer/query_builder/models/postgres/postgres_exec.nim`
方針:
- `Connections` に `waiters*: Deque[Future[void]]` を追加する
- `getFreeConn` は空きがなければ waiter を登録して待機する
- `returnConn` は `isBusy = false` にした後、未完了 waiter を 1 つだけ起こす
- 余剰 waiter や完了済み waiter は掃除する
### 失敗時挙動
1. `pqsocket < 0` は `DbError`
2. タイムアウト時は `cancelQuery` 実行後に `DbError`
3. `cancelQuery` 失敗時は追加情報付きで例外化
4. dispatcher 登録・解除エラーは握り潰さず上位へ伝播する
### テスト方針
単体テスト:
1. `waitPgReadable` が read event で復帰する
2. timeout 時に `false` を返す
3. `cancelQuery` が後始末まで行う
4. `getFreeConn` が通知で復帰する
結合テスト:
1. httpbeast / httpx + Postgres のレイテンシ回帰を確認する
2. `asynchttpserver` と比較して極端な乖離がないことを確認する
3. プール枯渇時の待機が 10ms 刻みポーリングに依存しないことを確認する
回帰テスト:
1. 既存 CRUD クエリの結果互換
2. transaction 系 API の動作
3. 例外時メッセージが意味を保っていること
### 受け入れ基準
1. PostgreSQL クエリ待機ループから `await sleepAsync(10)` が除去されている
2. httpbeast / httpx で DB 完了後の回収遅延が顕著に増幅しない
3. プール空き待機で `sleepAsync` ポーリングを使わない
4. 公開 API 変更なしで既存テストと追加テストが成立する
@@ -0,0 +1,240 @@
---
description: 321-mariadb-nonblockingブランチでの開発時に読み込む
alwaysApply: false
---
MariaDB nonblocking API 対応 ブランチルール
===
このブランチで実装することは以下の通りです。
- allographer の MariaDB 実装を、MariaDB Connector/C の non-blocking client API に追従できる構成へ設計・整備する。
- 現在の `sleepAsync(10)` ベースの接続プール待機と、実質ブロッキングなクエリ実行経路を切り分けて改善方針を明確化する。
- `mysql_real_query_start/cont` 系、`mysql_read_query_result_start/cont` 系、`mysql_fetch_row_start/cont` 系を使う待機ループの責務分離を設計する。
- 必要な Connector/C 宣言を `mariadb_rdb.nim` に追加し、将来の実装変更でヘッダ不足が起きないようにする。
- 公開 API 互換を維持しつつ、MariaDB driver 内に差分を閉じ込める。
- 設計メモで整理していた背景・参照仕様・リスク・段階導入案をこのブランチルールへ集約し、参照元を一本化する。
## 進捗
- [x] `.cursor/rules/project.mdc` と `.cursor/rules/branch.mdc` を確認した
- [x] 現行 `src/allographer/query_builder/models/mariadb/` と `src/allographer/query_builder/libs/mariadb/` を調査した
- [x] ローカルの `/usr/include/mariadb/mysql.h` で non-blocking API 宣言と待機フラグを確認した
- [x] MariaDB 公式ドキュメントで non-blocking API の制約と利用手順を確認した
- [x] ブランチルールに設計方針・改修粒度・参考資料を整理した
- [x] `src/allographer/query_builder/libs/mariadb/mariadb_rdb.nim` に必要宣言を追加した
- [x] `mariadb_impl.nim` に共通待機ヘルパと `_start/_cont` ベースの実行ループを実装する
- [x] `mariadb_open.nim` で `MYSQL_OPT_NONBLOCK` を有効化し、接続初期化手順を整理する
- [x] `mariadb_types.nim` / `mariadb_exec.nim` のプール空き待機を通知ベースへ置換する
- [x] `tests/mariadb/` に pool wait と CRUD 回帰を追加する
- [x] 実装中に設計差分が出た場合は本ファイルの `調査結果・設計まとめ` を更新する
- [x] `src/allographer/query_builder/libs/surreal/` と `models/surreal/` の効率改善余地を調査し、`documents/rdb/surreal_efficiency_report.md` に整理した
- [x] `documents/rdb/surreal_efficiency_report.md` の P0/P1 項目に沿って、`surreal_impl.nim` の HTTP timeout 共通化、`surreal_open.nim` の bootstrap 1 回化、`surreal_lib.nim` の文字列生成改善、`surreal_exec.nim` の `getElems()` 化を実装した
## 参考資料
- `documents/rdb/mariadb_nonblocking_design.md` の内容を統合済み
- `src/allographer/query_builder/models/mariadb/mariadb_open.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_exec.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_types.nim`
- `src/allographer/query_builder/libs/mariadb/mariadb_impl.nim`
- `src/allographer/query_builder/libs/mariadb/mariadb_rdb.nim`
- `documents/rdb/surreal_efficiency_report.md`
- `/usr/include/mariadb/mysql.h`
- MariaDB Documentation: Non-Blocking API Reference
- MariaDB Documentation: Using the Non-Blocking Library
- `src/allographer/query_builder/models/postgres/postgres_open.nim`
- `src/allographer/query_builder/models/postgres/postgres_exec.nim`
## 調査結果・設計まとめ
### 背景
現行の MariaDB 実装は `Future` を返しているが、実際の MariaDB Connector/C 呼び出しは `mysql_real_query()` / `mysql_use_result()` / `mysql_fetch_row()` を同期的に使っている。
そのため、Nim 側では async API に見えても、ソケット I/O 待機は Connector/C 内でブロックされうる。さらに接続プール枯渇時は `sleepAsync(10)` ポーリングで待っており、待機遅延が大きい。
### 現状の問題
1. `mariadb_exec.nim` の `getFreeConn` が `sleepAsync(10)` ポーリングで、プール返却の即時通知がない
2. `mariadb_impl.nim` の `query*` / `exec*` / `getColumns*` は non-blocking API を使っていない
3. `timeout` 引数はあるが、Connector/C の I/O 待機そのものをアプリ側 event loop で制御できていない
4. `mysql_use_result()` を使う都合上、行フェッチ途中で新規操作を開始できないという Connector/C 制約を、内部設計として明示できていない
5. 設計メモが別ファイルに分散すると、ブランチ進捗と参照資料の整合性が崩れやすい
### 設計目標
1. MariaDB クエリ送受信を Connector/C の `_start/_cont` API へ置き換える
2. ソケット待機は `mysql_get_socket()` と `MYSQL_WAIT_*` に従って event loop で処理する
3. `mysql_get_timeout_value_ms()` に従って timeout を扱い、ライブラリ内部 timeout と allographer の接続 timeout を両立させる
4. 公開 API は維持し、driver 内部だけで差分を吸収する
5. 接続プール空き待機は PostgreSQL と同様の通知ベースへ寄せる
### 非目標
1. Query Builder の公開 API 変更
2. MySQL driver まで同時に全面改修すること
3. 初回対応で prepared statement 系まで non-blocking 化すること
4. このブランチの責務を MariaDB driver の範囲から広げすぎること
### 実装方針
#### 1. `mariadb_rdb.nim` に non-blocking API 宣言を揃える
追加または有効化する対象:
- `mysql_options`
- `mysql_get_socket`
- `mysql_get_timeout_value`
- `mysql_get_timeout_value_ms`
- `mysql_real_connect_start/cont`
- `mysql_real_query_start/cont`
- `mysql_store_result_start/cont`
- `mysql_read_query_result_start/cont`
- `mysql_fetch_row_start/cont`
- `mysql_free_result_start/cont`
- `MYSQL_WAIT_READ/WRITE/EXCEPT/TIMEOUT`
- 必要なら `mysql_optionsv` など周辺宣言も不足なく追加する
#### 2. `mariadb_open.nim` で non-blocking を明示的に有効化する
手順:
1. `mysql_init()` 後、`mysql_options(conn, MYSQL_OPT_NONBLOCK, nil)` を呼ぶ
2. 接続自体も `mysql_real_connect_start/cont` に寄せるか、初手は接続のみ同期で残すかを選ぶ
3. 設計上は接続も non-blocking に揃えるのが一貫するため、最終的には `_start/_cont` 化を目標とする
補足:
- MariaDB 公式ドキュメントでは、`MYSQL_OPT_NONBLOCK` を有効化せずに non-blocking API を使うとクラッシュしうる
- DNS 解決は非同期化されないため、ホスト名利用時は依然として初回接続でブロックしうる
- 接続初期化の最終形は `_start/_cont` へ寄せるが、既存動作を壊さない段階導入を優先する
#### 3. `mariadb_impl.nim` に共通待機ヘルパを追加する
候補ヘルパ:
- `waitMariadb(conn: PMySQL, waitStatus: cint, deadline: MonoTime): Future[cint]`
- `runMariadbOp[T](...)` のような `_start/_cont` 反復ヘルパ
- `readAllRows(...)` のような result drain ヘルパ
待機仕様:
- `waitStatus` の `MYSQL_WAIT_READ/WRITE/EXCEPT` を `AsyncFD` で待つ
- `MYSQL_WAIT_TIMEOUT` が立っているときは `mysql_get_timeout_value_ms()` を優先する
- allographer 側 `timeout` を超えたら `DbError` を返す
- 待機ループは API ごとに重複させず、共通の進行関数へ寄せる
#### 4. クエリ実行は「送信」「結果確定」「結果取得」「行フェッチ」に分ける
`SELECT` 系:
1. `mysql_real_query_start/cont`
2. `mysql_use_result()` または `mysql_store_result_start/cont` のどちらを使うか判断
3. 既存実装との互換を優先するなら、まずは `mysql_use_result()` を維持しつつ `mysql_fetch_row_start/cont` で逐次取得する
4. 行取得後に `mysql_free_result_start/cont` または `mysql_free_result()` で解放する
`INSERT/UPDATE/DELETE` 系:
1. `mysql_real_query_start/cont`
2. `mysql_real_query_start/cont` の完了を実行完了とみなし、結果セットがない経路では追加の `read_query_result` 呼び出しを行わない
3. `mysql_errno()` / `mysql_error()` を確認して `DbError`
補足:
- MariaDB 公式ドキュメントでは、`mysql_use_result()` 自体はブロックしないが、そこからの `mysql_fetch_row_start/cont` は `use_result` 由来なら待機を伴う
- `store_result` を選ぶとフェッチ自体は即時化しやすい一方、結果全件受信を先に待つ必要がある
- 現行の逐次処理に近いのは `use_result + fetch_row_start/cont`
- 実装上の都合で `store_result` を選ぶ場合でも、公開 API の見え方は変えない
- 実装検証では、`mysql_real_query_start/cont` 直後に `mysql_read_query_result_start/cont` を追加すると DML 経路でタイムアウトが発生したため、`exec*` 系は `real_query` 完了をもって終了判定する方針へ更新した
#### 4.1 実装差分メモ(2026-03-23
- `mariadb_rdb.nim` の `MYSQL_OPT_NONBLOCK` は MariaDB ヘッダ定義に合わせて `6000` を明示指定した
- `mariadb_impl.nim` では `waitMariadb` を共通化し、`mysql_real_query_start/cont` と `mysql_fetch_row_start/cont` / `mysql_free_result_start/cont` を統一ループで実行する実装にした
- `mariadb_open.nim` で `mysql_options(conn, MYSQL_OPT_NONBLOCK, nil)` を全接続で有効化した
- `mariadb_types.nim` / `mariadb_exec.nim` で接続プール待機を `waiters` ベースへ置換し、`sleepAsync(10)` ポーリングを撤廃した
- `tests/mariadb/test_pool_wait.nim` を追加し、`tests/mariadb/test_query.nim` / `tests/mariadb/test_transaction.nim` と合わせて回帰確認した
#### 4.2 効率改善差分メモ(2026-03-23
- `mariadb_impl.nim`: `setColumnInfo` を `while true` フェッチループの外に移動し、行ごとに `fetch_field_direct` の FFI 呼び出しと文字列アロケーションが発生する問題を解消した。各行では `baseColumns` のコピーに対して NULL 上書きのみ行う
- `mariadb_impl.nim`: 全 proc から `assert db.ping == 0` を除去した。`mysql_ping` は同期ブロッキングでイベントループを停止するため。接続異常は `real_query_start` のエラーハンドリングで検出する
- `mariadb_impl.nim`: `epochTime()` ベースのデッドライン管理を `std/monotimes` の `MonoTime` + `Duration` に置き換え、NTP 補正による巻き戻りリスクを排除した
- `mariadb_impl.nim`: `MariadbWaitState` を `ref object` から `object`(値型)に変更し、`waitMariadb` 呼び出しあたり 1 回のヒープアロケーションを削減した
- `mariadb_impl.nim`: JSON→string 変換ロジックを `jsonObjValuesToStrSeq` / `jsonFlatToStrSeq` の 2 proc に集約し、4 箇所の重複を解消した
- `mariadb_impl.nim`: `getColumnTypes` の SQL を文字列補間から `?` プレースホルダに変更し、SQL インジェクションリスクを排除した
- `mariadb_exec.nim`: `exec` / `insertId` が DML のたびに `INFORMATION_SCHEMA.COLUMNS` を問い合わせていた問題を、`Connections.columnTypeCache` によるテーブル単位キャッシュで解消した
- `mariadb_types.nim`: `waiters` を `seq[Future[void]]` から `Deque[Future[void]]` に変更し、`wakeOnePoolWaiter` の先頭削除を O(n) → O(1) に改善した
- `mariadb_lib.nim`: `dbFormat` の char-by-char 連結を、`?` 位置間のサブ文字列を一括 `add` する方式に変更した
- SurrealDB: `surreal_impl.nim` に `runSurrealSql` / `awaitWithTimeout` を追加し、`query` / `exec` / `info` の HTTP timeout と JSON error 処理を共通化した
- SurrealDB: `surreal_open.nim` で `/status` と `/sql` の bootstrap を timeout 対応で実行し、namespace/database 定義を初回接続へ集約した
- SurrealDB: `surreal_lib.nim` の `numToAlphabet` / `questionToDaller` / `dbFormat(JsonNode)` を単一バッファ志向へ整理した
- SurrealDB: `surreal_exec.nim` の `getAllRows` は `rows.getElems()` を使い、`toSeq()` の追加走査を避けた
#### 5. 接続プール待機は通知ベースへ置換する
`mariadb_types.nim`:
- `Connections.waiters*: seq[Future[void]]` を追加する
`mariadb_exec.nim`:
- `getFreeConn` は waiter を積んで `withTimeout` で待つ
- `returnConn` は接続返却後に 1 件だけ waiter を起こす
- PostgreSQL 実装と同じ責務分割に揃える
- waiters の起床順やキャンセル時の取り扱いは、既存 PostgreSQL 実装の方針に合わせて整理する
#### 6. 段階導入する
第1段階:
- ヘッダ宣言追加
- ブランチルール・設計書追加
第2段階:
- プール待機の通知化
- `exec*` 系の non-blocking 化
第3段階:
- `query*` / `rawQuery*` / `getColumns*` / `insertId` の非同期 result drain 実装
- テスト拡充
- 段階ごとに実装差分が出たら本ファイルへ追記し、ブランチ内の唯一の設計記録にする
### 受け入れ基準
1. MariaDB driver で `sleepAsync(10)` によるプール待機がなくなっている
2. `mariadb_impl.nim` のクエリ待機が `mysql_real_query_start/cont` などの non-blocking API を利用している
3. 同一接続上で未完了 non-blocking 操作を残したまま次の操作を始めない
4. 既存 CRUD / transaction / schema テストが回帰しない
5. 公開 API 変更なしで実装差分が driver 内に閉じ込められている
### 参照仕様
- MariaDB Documentation: Non-Blocking API Reference
https://mariadb.com/docs/server/reference/product-development/mariadb-internals/using-mariadb-with-your-programs-api/non-blocking-client-library/non-blocking-api-reference
- MariaDB Documentation: Using the Non-Blocking Library
https://mariadb.com/docs/server/reference/product-development/mariadb-internals/using-mariadb-with-your-programs-api/non-blocking-client-library/using-the-non-blocking-library
- `/usr/include/mariadb/mysql.h`
確認済み項目:
- `MYSQL_OPT_NONBLOCK`
- `MYSQL_WAIT_READ`, `MYSQL_WAIT_WRITE`, `MYSQL_WAIT_EXCEPT`, `MYSQL_WAIT_TIMEOUT`
- `mysql_options`, `mysql_optionsv`
- `mysql_get_socket`
- `mysql_get_timeout_value`, `mysql_get_timeout_value_ms`
- `mysql_real_connect_start/cont`
- `mysql_real_query_start/cont`
- `mysql_store_result_start/cont`
- `mysql_read_query_result_start/cont`
- `mysql_fetch_row_start/cont`
- `mysql_free_result_start/cont`
### リスク
1. `mysql_use_result()` は結果を最後まで読み切る前に次操作へ進めないため、clean up 漏れがあると接続を壊しやすい
2. MariaDB non-blocking API は `_cont()` を最後まで呼び切る必要があり、途中中断設計を誤ると不整合が出る
3. DNS 解決はブロックしうるので、接続初期化だけは期待したほど改善しない可能性がある
4. MySQL driver と共通化したくなっても、まずは MariaDB driver 内に閉じ込めてから判断すべき
5. 設計記録を別ファイルに残さないことで、進捗と方針の乖離を抑える
### 段階導入案
1. 設計書とブランチルールを追加
2. `mariadb_rdb.nim` に宣言追加
3. プール待機通知化
4. `exec*` 系を non-blocking 化
5. `query*` / `rawQuery*` / `getColumns*` を non-blocking 化
6. 必要なら `mysql_real_connect_start/cont` を導入
### 完了条件
1. MariaDB driver の内部待機が Connector/C non-blocking API ベースになっている
2. プール空き待機が通知ベースである
3. 既存公開 API を壊していない
4. MariaDB の主要テストが通る
@@ -0,0 +1,174 @@
---
description: 323-database-url-for-connectionブランチでの開発時に読み込む
alwaysApply: false
---
dbOpen Database URL 対応 ブランチルール
===
このブランチで実装することは以下の通りです。
- `dbOpen` に database URL を使って接続できる新しいオーバーロードを追加し、既存の引数シグネチャは後方互換のため残す。
- SQLite / MySQL / MariaDB / PostgreSQL は一般的な database URL 仕様に従い、scheme・認証情報・host・port・database path・query を解釈できる設計に整える。
- URL 解析責務を各 driver の `dbOpen` 実装にベタ書きせず、共通化できる層へ寄せて公開 API を汚さない。
- 既存の URL 文字列 1 引数版 `dbOpen` と、新しく追加する引数デザインの役割分担を明確にし、曖昧なオーバーロード解決を避ける。
- 既存テスト・example・README・`documents/rdb/query_builder.md` を、最終仕様と齟齬がない状態へ同期する。
## 進捗
- [x] `.cursor/rules/project.mdc` と `.cursor/rules/branch.mdc` を確認した
- [x] 現在のブランチ名に対応するブランチルールファイルが未作成であることを確認した
- [x] 既存 `dbOpen` 実装が SQLite / MySQL / MariaDB / PostgreSQL / SurrealDB でどう分かれているか確認した
- [x] MySQL / MariaDB / PostgreSQL にはすでに URL 文字列 1 引数版 `dbOpen` があり、SQLite には未整備であることを確認した
- [x] 既存 URL 実装が `split` ベースの簡易パーサであり、一般的な URL 仕様を十分に扱えていないことを確認した
- [x] 今回のブランチでは SurrealDB を対象外とする方針を反映した
- [x] ブランチルールに改修方針・進捗粒度・参考資料を整理した
- [x] 一般的な database URL 仕様として採用する範囲を整理する
- [x] 新しい `dbOpen` オーバーロードの引数デザインを確定する
- [x] URL 解析の共通 helper / 型配置を決め、driver 差分の閉じ込め方を整理する
- [x] SQLite の URL 受け口を追加し、ファイルパス指定との意味論を衝突させない
- [x] MySQL / MariaDB / PostgreSQL の URL 解析を一般仕様に合わせて強化する
- [x] 正常系・異常系・後方互換のテストを driver ごとに追加または更新する
- [x] example / README / documents を同期する
- [x] 実装途中で設計認識に誤りが見つかった場合は、本ファイルの `調査結果・設計まとめ` を更新する
## 参考資料
- `src/allographer/connection.nim`
- `src/allographer/query_builder/models/sqlite/sqlite_open.nim`
- `src/allographer/query_builder/models/mysql/mysql_open.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_open.nim`
- `src/allographer/query_builder/models/postgres/postgres_open.nim`
- `src/allographer/query_builder/models/surreal/surreal_open.nim`
- `tests/mysql/test_open.nim`
- `tests/mariadb/test_open.nim`
- `tests/postgres/test_open.nim`
- `tests/sqlite/connections.nim`
- `tests/postgres/connections.nim`
- `tests/mysql/connections.nim`
- `tests/mariadb/connections.nim`
- `example/connections.nim`
- `README.md`
- `documents/rdb/query_builder.md`
- RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
- PostgreSQL Documentation: Connection URIs
- MySQL / MariaDB ecosystem documentation for connection URI / DSN conventions
## 調査結果・設計まとめ
### 背景
現状の `dbOpen` は、RDB 系では「分解済みの接続情報を個別引数で渡す形」が基本で、一部 driver にだけ URL 文字列 1 引数版が追加されている。
ただし既存の URL 版は `split` を重ねた簡易実装で、一般的な URL で登場する予約文字・query・省略可能項目・scheme 差異に弱い。API としては URL 対応を名乗りつつ、仕様が狭く不均一な状態である。
今回の目的は、既存 API を壊さずに、database URL を正式な接続入力として扱える `dbOpen` オーバーロード群を整備し、RDB driver 間の設計を揃えることである。SurrealDB は今回の対象外とする。
### 現状の問題
1. SQLite はファイルパス引数のみで、URL ベース接続の入口がない
2. MySQL / MariaDB / PostgreSQL の URL 版は実装されているが、一般的な URL 仕様を十分に扱えない
3. driver ごとに URL の扱いがバラバラで、共通 API として学習コストが高い
4. 既存の URL 文字列 1 引数版と、新たな「database url を使う関数」の住み分けを設計しないと、将来の拡張で曖昧さが増える
### 設計目標
1. 既存 `dbOpen` シグネチャを残しつつ、新しいオーバーロードで URL ベース接続を自然に表現できる
2. SQLite / MySQL / MariaDB / PostgreSQL は一般的な URI 記法を優先する
3. URL 解析と driver ごとの接続情報変換を責務分離し、公開 API へ場当たり的分岐を持ち込まない
4. 後方互換・テスト・文書更新まで含めて、利用者が迷わない状態を作る
### 非目標
1. 既存の個別引数版 `dbOpen` を削除または非推奨化すること
2. 接続 URL と無関係な query builder / schema builder API を同時に変更すること
3. SurrealDB の接続モデルや URL 設計をこのブランチで見直すこと
4. このブランチで ORM 的な接続設定 abstraction を全面導入すること
### API 方針
#### 1. 新しいオーバーロードは「URL であることが呼び出し側から明確に分かる」形を優先する
候補:
1. `dbOpen(MySQL, url = "...", maxConnections = ..., timeout = ...)`
2. `dbOpen(MySQL, databaseUrl = "...", maxConnections = ..., timeout = ...)`
3. URL 専用の薄い型を導入して `dbOpen(MySQL, DatabaseUrl("..."), ...)`
採用した設計:
- URL 専用の薄い型 `DatabaseUrl` を導入し、`dbOpen(driver, databaseUrl = asDatabaseUrl("..."), ...)` で呼べるようにした
- 既存の `dbOpen(driver, url: string, ...)` は後方互換のため残し、内部では共通 parser に委譲する
- SQLite の既存 `dbOpen(SQLite3, path: string, ...)` はそのまま維持し、URL 文字列が渡された場合のみ URL ルートへ寄せる
理由:
- SQLite では単なる `string` がファイルパスと URL の両方になり得る
- `DatabaseUrl` に分けることで、既存の string overload と overload 解決が衝突しない
- 将来の拡張でも、URL 専用の入口を安全に追加しやすい
#### 2. RDB の URL 仕様は一般的な形式を優先する
最低限そろえる対象:
1. `sqlite:///absolute/path/to/db.sqlite3`
2. `sqlite://relative/path.db` またはこの扱いを不採用とするなら明文化する
3. `mysql://user:pass@host:3306/database`
4. `mariadb://user:pass@host:3306/database`
5. `postgresql://user:pass@host:5432/database`
設計メモ:
- `postgres://...` は `postgresql://...` の alias として受ける
- percent-encoding の decode 要否を明確にする
- query parameter は現段階で未使用でも、少なくとも破壊的に誤解釈しない方針を取る
- database 名や password に `:` `/` `@` が含まれるケースを、単純 `split` で壊さない
### 実装方針
#### 1. URL 解析は共通 helper 化を優先する
方針:
- `scheme`, `user`, `password`, `host`, `port`, `path`, `query` を扱える共通 parser を用意する
- URL 専用の `DatabaseUrl` を導入し、driver ごとの差分は「解析結果からどう接続情報へ落とすか」に限定する
- 既存 driver ファイルの `split` ベース処理は置き換え対象とする
#### 2. SQLite は URL とファイルパスの意味論を分ける
要点:
- 既存 `dbOpen(SQLite3, "/path/to/file.db")` はそのまま維持する
- URL 版は `asDatabaseUrl(...)` で「これは URL」と明示できる形を優先する
- `:memory:` や相対パスとの整合を壊さない
#### 3. 既存 URL 1 引数版は互換維持しつつ内部実装を新 parser に寄せる
要点:
- MySQL / MariaDB / PostgreSQL の既存テストは維持する
- 既存の `ValueError` / `DbError` の期待が妥当なら保つ
- 公開 API は残しつつ、仕様の狭さだけを改善する
#### 4. テストは「構文解析」と「実接続」を分けて考える
候補:
1. URL から接続情報へ正しく分解されることを確認する軽量テスト
2. 既存の integration 系 `dbOpen(...).isConnected()` テスト
3. 不正 scheme・不足情報・不正 port・予約文字を含む入力の異常系テスト
4. SQLite の URL 指定とファイルパス指定の後方互換テスト
5. SurrealDB の namespace / database 解決規則テスト
### 受け入れ基準
1. 既存の `dbOpen` 呼び出しを壊さずに、新しい URL ベースのオーバーロードを追加できている
2. SQLite / MySQL / MariaDB / PostgreSQL の URL 設計が一般的な仕様と整合している
3. SurrealDB の URL 設計が独自要件を満たしつつ、他 driver と読後感が大きく乖離していない
4. 既存の簡易 `split` 実装を置き換え、予約文字や query を誤解釈しにくい構造になっている
5. driver ごとの正常系・異常系・後方互換テストが追加または更新されている
6. example / README / documents / ブランチルールが最終仕様と一致している
### リスク
1. Nim 標準ライブラリだけで URL 解析を行う場合、RFC 3986 の細部を十分に扱えない可能性がある
2. SQLite の URL 仕様を曖昧にすると、既存のファイルパス API と混同されやすい
3. SurrealDB の namespace / database の載せ方を誤ると、RDB に寄せたつもりで逆に分かりにくくなる
4. 既存の `dbOpen(driver, urlString)` と新オーバーロードが衝突すると、Nim の overload 解決で利用者体験が悪化する
5. 文字列解析だけでなく実接続テストも必要なため、driver 横断で確認コストが上がる
### 段階導入案
1. 採用する URL 仕様と新オーバーロードの表面 API を確定する
2. 共通 URL parser / 接続情報変換レイヤを用意する
3. MySQL / MariaDB / PostgreSQL の既存 URL 実装を載せ替える
4. SQLite の URL 入口を追加する
5. テストを整備して後方互換と異常系を固める
6. example / README / documents を同期する
@@ -0,0 +1,167 @@
---
description: 333-prepared-statementブランチでの開発時に読み込む
alwaysApply: false
---
333-prepared-statement ブランチでの高速化内容
===
このブランチで実装することは以下の通りです。
- prepared statement を、高頻度な同一 SQL 実行で通常経路より速くなるように改善する
- とくに PostgreSQL で、prepare/deallocate の無駄と接続拡散を減らす
- benchmark が prepared statement の warm 状態を正しく測れるようにする
## 進捗
- [x] `example/benchmark.nim` の PostgreSQL 計測で prepared update が遅く見える理由を調査した
- [x] prepared statement 高速化の設計方針を整理した
- [ ] PostgreSQL にプール単位の prepared statement cache を追加する
- [ ] `close()` を論理 close 化し、物理破棄は別 API に分離する
- [ ] 接続固定で prepared statement を連続実行できる API を追加する
- [ ] `example/benchmark.nim` を cold/warm 計測に分離し、warm 経路で同一 statement を再利用する
- [ ] prepared cache と接続固定 API のテストを追加する
## 参考資料
- PostgreSQL libpq prepared statements: https://www.postgresql.jp/document/12/html/libpq-exec.html
- `PQsendPrepare`
- `PQsendQueryPrepared`
- `DEALLOCATE`
- PostgreSQL 非同期 libpq: https://www.postgresql.jp/document/12/html/libpq-async.html
- 対象実装
- `src/allographer/query_builder/models/postgres/postgres_exec.nim`
- `src/allographer/query_builder/models/postgres/postgres_types.nim`
- `src/allographer/query_builder/libs/postgres/postgres_impl.nim`
- `example/benchmark.nim`
## 調査結果・設計まとめ
### 現状の問題
PostgreSQL で prepared statement が遅く見える主因は、prepared statement 自体の性質ではなく、現在の実装と benchmark の計測条件にある。
- `benchmark.nim` は計測のたびに `prepare` と `close` を行っており、steady-state の再利用性能ではなくライフサイクル全体を測っている
- PostgreSQL 実装は prepared statement をコネクションごとに lazy prepare するため、プールが大きいと `PREPARE` が広く拡散する
- `first()` と `exec()` が毎回 `getFreeConn()` するため、1 つの業務操作でも接続が変わり、prepared state の局所性が弱い
- 通常経路も `PQsendQueryParams` による bind 実行なので、prepared の利得は parse/plan 再利用ぶんに限られる
### 高速化の基本方針
prepared statement を速くするには、単発 API を追加するだけでは足りない。以下をまとめて入れる必要がある。
- prepare/deallocate の回数を減らす
- 同じ logical operation を同じ接続に寄せる
- warm 済み prepared state をプール内で継続利用する
### 設計方針 1: プール単位の prepared cache
`PreparedStatement` ハンドルごとに物理 state を持つのではなく、`Connections` が SQL ごとの prepared state を保持する。
概念設計:
```nim
type PostgresPreparedEntry = ref object
sql*: string
nArgs*: int
stmtBaseName*: string
stmtNames*: seq[string] ## connI ごとの物理 prepared 名
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
waiters*: Deque[Future[void]]
columnTypeCache*: Table[string, seq[Row]]
preparedCache*: Table[string, PostgresPreparedEntry]
```
期待効果:
- 同じ SQL に対する `prepare()` の重複を避けられる
- 同一プール内では physical prepare を接続ごとに 1 回へ寄せられる
- 短命な prepared handle を繰り返し作っても warm state を失いにくくなる
### 設計方針 2: `close()` の論理 close 化
現在の `close()` は、その handle が使った接続すべてに `DEALLOCATE` を送る。高速化の観点ではこれは不利なので、意味論を分ける。
- `close()`: handle の参照を閉じるだけ
- `clearPreparedCache()` または `flushPrepared(sql)`: 物理 `DEALLOCATE`
- 接続 close 時: その接続に紐づく prepared state を一括破棄
期待効果:
- 利用者が毎回 `prepare` / `close` しても、プール側は hot SQL を保持できる
- benchmark の warm 計測でも、毎回の物理破棄コストを避けられる
### 設計方針 3: 接続固定 API の追加
prepared statement の locality を高めるため、同じ接続上で複数回の prepared 実行を束ねる API を追加する。
候補 API:
```nim
proc withPreparedConnection*(
self: PostgresConnections,
body: proc (ctx: PostgresPreparedContext): Future[void]
): Future[void]
```
```nim
type PostgresPreparedContext* = ref object
owner*: PostgresConnections
connI*: int
```
prepared statement 側には `ctx` 付き overload を足す。
```nim
proc first*(self: PostgresPreparedStatement, ctx: PostgresPreparedContext, args: seq[string]): Future[Option[JsonNode]]
proc exec*(self: PostgresPreparedStatement, ctx: PostgresPreparedContext, args: seq[string]): Future[void]
```
期待効果:
- `SELECT` と `UPDATE` が別接続に飛ぶのを防げる
- lazy prepare の発生先が必要な接続に限定される
- transaction 中の `transactionConn` と整合しやすい
### benchmark の変更方針
`example/benchmark.nim` は prepared statement の高速化効果を正しく測れるように変更する。
- `selectStmt` / `updateStmt` を `timeProcess` の外で 1 回だけ作る
- 繰り返し計測では同じ prepared statement を再利用する
- `close()` は全計測の最後に 1 回だけ呼ぶ
- benchmark 名を `cold` と `warm` に分ける
- warm 計測では `withPreparedConnection` を使って接続固定経路を測る
### 実装順序
#### Phase 1
- `Connections.preparedCache` を追加する
- `prepare()` を cache lookup ベースに変更する
- `close()` を論理 close 化する
- `clearPreparedCache()` を追加する
#### Phase 2
- `PostgresPreparedContext` を追加する
- `withPreparedConnection` を追加する
- prepared `get/first/exec` に `ctx` overload を追加する
- `benchmark.nim` を warm/cold 計測へ更新する
#### Phase 3
- `preparedCacheMaxEntries` を追加する
- `refCount == 0` の entry を対象に LRU eviction を入れる
- eviction 時に `DEALLOCATE` を発行する
### テスト方針
- 同じ SQL を複数回 `prepare()` しても、同一接続で physical prepare が 1 回に収まること
- `close()` 後に再 `prepare()` しても、cache が残る設計なら physical prepare が増えないこと
- `withPreparedConnection` の block 内で同じ `connI` が使われること
- transaction 中は `transactionConn` が優先されること
- cache eviction 時に `DEALLOCATE` が正しく走ること
@@ -0,0 +1,206 @@
---
description: 336-update-prepared-statementブランチでの開発時に読み込む
alwaysApply: false
---
336-update-prepared-statement ブランチでの prepared statement 横展開
===
このブランチで実装することは以下の通りです。
- 既存の SQLite / PostgreSQL / MySQL / MariaDB の prepared statement API を参照しつつ、SurrealDB 向け prepared statement API を追加する
- SurrealDB v3 の `/sql` API と SurrealQL の `LET` / parameter 仕様を前提に、server-side prepare ではなく client-side template reuse と 1 リクエスト多文実行で prepared statement 的な使い勝手を実現する
- SurrealDB 固有事情は `models/surreal` と `libs/surreal` に閉じ込め、公開 API は既存 driver と可能な限り揃える
- このターンでは実装は行わず、調査結果と実装方針の設計書を固める
## 進捗
- [x] `.cursor/rules/project.mdc` / `.cursor/rules/branch.mdc` / `.cursor/rules/branch/336-update-prepared-statement.mdc` を確認した
- [x] 既存 prepared statement API の到達点を SQLite / PostgreSQL / MariaDB / MySQL で確認した
- [x] 現在の SurrealDB 実装と `/sql` 利用箇所を確認した
- [x] SurrealDB v3 公式ドキュメントで parameter / HTTP `/sql` / transaction の仕様を確認した
- [x] SurrealDB prepared statement の設計と利用例を `documents/surrealdb/prepared_statement.md` に整理した
- [x] `models/surreal` に prepared statement 用の型と公開 API を追加した
- [x] `libs/surreal` に multi-statement 実行向けの共通処理を追加した
- [x] SurrealDB prepared statement のテストを追加した
- [x] `documents/surrealdb/query_builder.md` / `documents/rdb/query_builder.md` を更新した
## 参考資料
- 既存 prepared statement API
- `src/allographer/query_builder/models/sqlite/sqlite_types.nim`
- `src/allographer/query_builder/models/sqlite/sqlite_exec.nim`
- `src/allographer/query_builder/models/postgres/postgres_types.nim`
- `src/allographer/query_builder/models/postgres/postgres_exec.nim`
- `src/allographer/query_builder/models/mysql/mysql_exec.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_exec.nim`
- 現在の SurrealDB 実装
- `src/allographer/query_builder/models/surreal/surreal_types.nim`
- `src/allographer/query_builder/models/surreal/surreal_exec.nim`
- `src/allographer/query_builder/models/surreal/surreal_open.nim`
- `src/allographer/query_builder/libs/surreal/surreal_impl.nim`
- `src/allographer/query_builder/libs/surreal/surreal_lib.nim`
- `src/allographer/schema_builder/queries/surreal/schema_utils.nim`
- 既存ドキュメント
- `documents/surrealdb/query_builder.md`
- `documents/surrealdb/schema_builder.md`
- SurrealDB v3 公式ドキュメント
- Parameters: https://surrealdb.com/docs/surrealql/parameters
- HTTP Protocol (`POST /sql`): https://surrealdb.com/docs/surrealdb/integration/http
- Transactions: https://surrealdb.com/docs/surrealql/transactions
- Security summary (parameterized query): https://surrealdb.com/docs/surrealdb/security/summary
## 調査結果・設計まとめ
### 1. 前提整理
SurrealDB v3 には PostgreSQL や MySQL のような server-side prepared statement を、そのまま同じ意味で `/sql` HTTP 経由に載せる公開 API は見当たらない。一方で、以下は公式仕様として確認できる。
- `/sql` は 1 回の POST で複数の SurrealQL 文を `;` 区切りで送れる
- parameter は `LET $x = ...;` で定義して後続文から参照できる
- SurrealDB 3.x では `$x = ...` だけの旧記法は deprecated で、`LET` を使う必要がある
- statement はデフォルトで文ごとに transaction 境界を持つ。複数文を原子的に扱いたい場合は `BEGIN TRANSACTION; ...; COMMIT TRANSACTION;` が必要
したがって、このブランチでの `prepare()` は「DB が statement を事前コンパイルして保持する API」ではなく、「SQL template の正規化・再利用・安全な値埋め込み・単一 request 化を行う client-side prepared statement」として設計する。
### 2. 公開 API 方針
利用者体験は既存 driver に寄せる。第一候補は次の API である。
```nim
let stmt = surreal.prepare("SELECT * FROM user WHERE id = ?")
let rows = await stmt.get(@["user:alice"])
let row = await stmt.first(%*[1])
await stmt.exec(@["value"])
await stmt.close()
await surreal.clearStmtCache()
await surreal.withConn(proc(ctx: SurrealPreparedContext): Future[void] {.async.} =
discard await stmt.first(ctx, @["user:alice"])
)
```
採用方針:
- `prepare`
- `get`
- `first`
- `exec`
- `close`
- `flushStmt`
- `clearStmtCache`
- `withConn`
- `SurrealPreparedContext`
初期スコープでは `getPlain` / `firstPlain` は後回し候補とする。SurrealDB の戻り値は object 中心で列定義が RDB より弱く、列順の意味が薄いため、まずは既存の `JsonNode` 中心 API を優先する。
### 3. 実装モデル
prepared statement 実行時は、毎回次の 3 段階で 1 リクエストを組み立てる。
1. 元 SQL の `?` を `$a`, `$b`, ... に変換する
2. 各引数を `LET $a = ...; LET $b = ...;` に変換する
3. `LET` 群と本体 SQL を `;` で連結して `/sql` に 1 回だけ POST する
概念的には次の形になる。
```sql
LET $a = "user:alice";
LET $b = <datetime>"2026-04-03T00:00:00Z";
SELECT * FROM user WHERE id = $a AND created_at >= $b;
```
この方式なら、すでに `surreal_lib.nim` にある `questionToDaller` と `dbFormat(queryString, args: JsonNode)` の思想をそのまま再利用できる。prepared statement 専用に別の埋め込み規則を増やす必要はない。
### 4. 型設計
既存 driver に合わせ、プール単位 cache を持つ。
```nim
type SurrealPreparedEntry = ref object
sql*: string
normalizedSql*: string
nArgs*: int
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
waiters*: Deque[Future[void]]
preparedCache*: Table[string, SurrealPreparedEntry]
type SurrealPreparedContext* = ref object
owner*: SurrealConnections
connI*: int
type SurrealPreparedStatement* = ref object
owner*: SurrealConnections
entry*: SurrealPreparedEntry
sql*: string
isClosed*: bool
```
ポイント:
- Surreal は HTTP `/sql` なので physical statement handle は持たない
- cache の目的は server resource 再利用ではなく、template 正規化と API 一貫性の維持
- `close()` は logical close とし、cache entry 自体は `flushStmt` / `clearStmtCache` まで残す
### 5. 実行結果の扱い
`/sql` の戻り値は statement ごとの配列なので、prepared statement では「最後の本体 SQL の結果」を利用者へ返す。
- `get`: 最終 statement の `result` を `seq[JsonNode]` で返す
- `first`: `get` の先頭 1 件を `Option[JsonNode]` で返す
- `exec`: 最終 statement の status を確認し、副作用系として扱う
- `LET` statement の result は利用者に見せない
ただし error 判定は最後の statement だけでなく全 statement を走査する。`LET` で失敗した場合でも早期に `DbError` を返す必要がある。
### 6. `withConn` を残す理由
HTTP `/sql` だけを見ると、prepared statement は connection 固定が必須ではない。しかし API 統一のため、`withConn` は残す価値がある。
- 既存 driver と同じ呼び出しパターンを維持できる
- SurrealDB の parameter は connection/session scope を持ちうるため、将来 RPC 対応や session 前提最適化へ進む余地を残せる
- 現行の connection pool 制御と整合する
ただし初期実装では、`withConn` の主目的は性能よりも API 一貫性になる。
### 7. multi-statement 利用の境界
このブランチで使う multi-statement は「`LET` 群 + 本体 SQL」を 1 リクエストに束ねる用途を主眼とする。
- `prepare("UPDATE ...")` は 1 つの本体 statement を持つ
- `LET` は前置 statement
- 複数の業務 statement を 1 個の prepared statement に詰め込む設計は初期スコープ外
理由:
- 既存 prepared statement API も基本は 1 SQL を対象にしている
- SurrealDB では複数 statement が自動で 1 transaction にはならない
- 2 個以上の副作用文を 1 本の prepared statement に許すと、`exec()` の返り値・失敗時意味論・テストが急に複雑になる
もし将来 multi-statement business query を扱うなら、別 API として `prepareBatch` あるいは transaction helper を検討する。
### 8. 実装結果
- `surreal_types.nim` に prepared 関連型を追加し、`Connections` に `preparedCache` を持たせた
- `surreal_lib.nim` に prepared SQL 生成の共通関数を追加した
- `surreal_exec.nim` に `prepare` / `get` / `first` / `exec` / `close` / `flushStmt` / `clearStmtCache` / `withConn` を追加した
- `tests/surrealdb/test_prepared_statement.nim` を追加した
- `documents/surrealdb/prepared_statement.md` と既存ドキュメントを同期した
### 9. テスト方針
- `prepare("SELECT ... WHERE id = ?")` で `get` / `first` が通ること
- `prepare("UPDATE ... SET name = ? WHERE id = ?")` で `exec` が通ること
- `close()` 後に同じ SQL を `prepare()` し直しても再利用できること
- `clearStmtCache()` 後に再度 `prepare()` できること
- `withConn` 内の `ctx` overload が通ること
- `JsonNode` 引数で `JInt` / `JString` / `JNull` / `JArray` / `JObject` が `LET` 経由で正しく扱えること
- record id や datetime 文字列の変換が既存 `dbFormat` 規則と一致すること
### 10. 現時点の結論
SurrealDB v3 版 prepared statement は「server-side prepare の移植」ではなく、「既存 prepared statement API と揃った公開インターフェースを、SurrealDB の `LET` と `/sql` multi-statement で安全に再現する」方針が最も自然である。
この方針なら `SYW-ARCH-001` に従って公開 API を共通化しつつ、SurrealDB 固有差分は `libs/surreal` と `models/surreal` に閉じ込められる。
@@ -0,0 +1,271 @@
---
description: 340-aged-connectionブランチでの開発時に読み込む
alwaysApply: false
---
340-aged-connection コネクションプール改善調査
===
このブランチで実装することは以下の通りです。
- 既存の SQLite / PostgreSQL / MySQL / MariaDB のコネクションプール実装を調査し、長寿命化した接続や壊れた接続を再利用し続けるリスクを洗い出す
- https://please-sleep.cou929.nu/go-sql-db-connection-pool.html の `sql.DB` の考え方を参考に、allographer のプールへ取り込める改善点を整理する
- 「使えないコネクションは破棄してから張り直す」方針を、公開 API を壊さず driver 内へ閉じ込めて実装できる設計に落とし込む
- prepared statement cache / columnTypeCache / transaction 中の接続固定など、再接続時に影響する内部状態の整合条件を明確化する
- 調査結果を踏まえて、各 driver の pool slot を差し替える実装へ進める
## 進捗
- [x] `.cursor/rules/project.mdc` / `.cursor/rules/branch.mdc` を確認した
- [x] 現在のブランチ名 `340-aged-connection` に対応するブランチルールを新規作成した
- [x] `src/allographer/query_builder/models/*/*_open.nim` を確認し、各 driver の接続生成方式を調査した
- [x] `src/allographer/query_builder/models/*/*_exec.nim` を確認し、接続取得・返却・待機方式を調査した
- [x] `src/allographer/query_builder/libs/*/*_impl.nim` を確認し、接続異常時の現在の振る舞いを調査した
- [x] 参考記事の `sql.DB` の設計要点を読み、allographer へ適用できる論点を整理した
- [x] 各 driver 共通で使える `ConnectionHealth` / `ConnectionFactory` 相当の内部構造を設計し、`Connections` に lifetime / factory 情報を持たせた
- [x] 接続借用時の寿命判定・再生成手順を driver ごとに実装した
- [x] prepared statement cache の slot 単位無効化を実装し、transaction 中は既存の固定接続をそのまま使う方針を維持した
- [x] 影響範囲に応じた回帰テストを追加した
## 参考資料
- 参考記事
- https://please-sleep.cou929.nu/go-sql-db-connection-pool.html
- 現在の接続初期化
- `src/allographer/query_builder/models/postgres/postgres_open.nim`
- `src/allographer/query_builder/models/sqlite/sqlite_open.nim`
- `src/allographer/query_builder/models/mysql/mysql_open.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_open.nim`
- 現在の接続プール管理
- `src/allographer/query_builder/models/postgres/postgres_exec.nim`
- `src/allographer/query_builder/models/sqlite/sqlite_exec.nim`
- `src/allographer/query_builder/models/mysql/mysql_exec.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_exec.nim`
- 現在の型定義
- `src/allographer/query_builder/models/postgres/postgres_types.nim`
- `src/allographer/query_builder/models/sqlite/sqlite_types.nim`
- `src/allographer/query_builder/models/mysql/mysql_types.nim`
- `src/allographer/query_builder/models/mariadb/mariadb_types.nim`
- 接続利用時の低レベル実装
- `src/allographer/query_builder/libs/postgres/postgres_impl.nim`
- `src/allographer/query_builder/libs/sqlite/sqlite_impl.nim`
- `src/allographer/query_builder/libs/mysql/mysql_impl.nim`
- `src/allographer/query_builder/libs/mariadb/mariadb_impl.nim`
## 調査結果・設計まとめ
### 1. 現在の実装で見えたこと
allographer のプールは、全 driver とも `dbOpen` 時に `maxConnections` 本を先に開いて配列へ保持する方式になっている。
- PostgreSQL: `postgres_open.nim` で全接続を eager に生成し、`createdAt` を持つが、寿命判定にはまだ使っていない
- SQLite: `sqlite_open.nim` でも同様に eager open で、`createdAt` は保持しているが再接続用途には未使用
- MySQL: `mysql_open.nim` で eager open。`ConnectionInfo` はあるため再接続情報は保持しやすい
- MariaDB: `mariadb_open.nim` も eager open。`ConnectionInfo` はある
接続取得は、PostgreSQL / SQLite / MariaDB は waiter キューで通知ベース、MySQL はまだ `sleepAsync(10)` ポーリングで待っている。
一方で、どの driver でも「借り出す前にその接続が寿命切れか」「壊れていないか」を判定して、不要なら捨てて張り直す仕組みはまだない。
### 2. 参考記事から取り込める設計要点
参考記事の `sql.DB` で特に重要だった論点は次の 5 点。
1. idle 接続と利用中接続を分けて扱う
2. 接続取得時に idle 接続を再利用し、必要なら新規接続する
3. 待ち行列を持ち、上限到達時は空き通知で待つ
4. `SetConnMaxLifetime` / `SetConnMaxIdleTime` 相当で、古い接続を使い回しすぎない
5. 使えない接続は `ErrBadConn` 相当として捨て、再試行できるようにする
allographer は 1. と 3. は一部できているが、4. と 5. が弱い。今回のブランチの主眼はここに置くのが自然。
### 3. 今のプールで改善余地が大きい箇所
#### 3.1 aged connection を判定する仕組みがない
各 driver の `Connection` には `createdAt` があるが、現在は接続寿命の上限判定に使っていない。
そのため、
- DB サーバ側 `wait_timeout`
- TCP 切断
- 長時間放置された idle 接続
- LB / proxy 配下でのコネクション寿命超過
といった要因で「見た目はプール内にあるが、実際にはもう使えない接続」を抱え続ける可能性がある。
特に MySQL は `mysql_impl.nim` の各所で `assert conn.ping == 0` を前提にしており、壊れた接続に対して graceful に「捨てて張り直す」制御へ入る前に異常化しやすい。
#### 3.2 bad connection を pool slot 単位で交換できない
現状は `self.pools.conns[i].conn` をそのまま実行系へ渡しており、途中で「この接続は再利用不可」と判断しても、その slot の中身を閉じて新しい接続に差し替える共通処理がない。
必要なのは次のような流れである。
1. 接続借用前に寿命・健全性を判定する
2. 異常ならその slot の接続を close する
3. 同じ slot に新しい接続を張り直す
4. その slot にぶら下がる prepared statement 状態を無効化する
5. その後でクエリ本体へ進む
今は 2. から 4. の共通導線が存在しない。
#### 3.3 reconnect に必要な情報が driver ごとに不揃い
MySQL / MariaDB は `ConnectionInfo` を持つが、PostgreSQL / SQLite の `Connections` には再接続用の接続情報が保存されていない。
したがって共通の「壊れた slot を再接続する」設計を入れるには、少なくとも内部的には各 driver の `Connections` に factory 情報を保持させる必要がある。
例:
- PostgreSQL: database / user / password / host / port / non-blocking 設定
- SQLite: database path
- MySQL / MariaDB: 既存 `ConnectionInfo` を `Connections` から参照できる形へ統一
#### 3.4 prepared statement cache が再接続で壊れうる
PostgreSQL / MySQL / MariaDB / SQLite は SQL 単位 cache を持ちつつ、物理 statement handle は conn index ごとに保持している。
このため slot `i` の接続を捨てて張り直すなら、少なくとも slot `i` に紐づく prepared 情報はクリアしないといけない。
- PostgreSQL: `stmtNames[i]` を空に戻す必要がある
- MySQL / MariaDB: `stmts[i]` を close して nil に戻す必要がある
- SQLite: `stmts[i]` を finalize して nil に戻す必要がある
再接続だけして cache を残すと、「古い接続に prepare されたハンドルを新しい接続で再利用しようとする」不整合になる。
#### 3.5 transaction 中は自動再接続の扱いを厳格に分ける必要がある
通常クエリでは bad connection を破棄して retry できる余地があるが、transaction 中は話が変わる。
- `BEGIN` 後の接続が死んだら、その transaction 文脈は失われる
- 同じ API 呼び出しを別接続へ載せ替えても、意味上は同じ transaction ではない
したがって transaction 中は、
- 自動再接続して継続してよい場面
- 明示的に `DbError` を返して transaction abort を促す場面
を分ける必要がある。初期方針としては「transaction 中は bad connection を検知したら再接続はしても自動リトライしない」が安全。
### 4. このブランチで採るべき基本方針
#### 方針A: pool slot を生かし、接続だけ差し替える
公開 API を崩さないため、接続プール自体を大きく作り変えるより、既存の `conns[i]` 配列を維持したまま「slot 内の接続実体だけ交換する」方がよい。
これにより、
- `getFreeConn` / `returnConn` の外形は維持できる
- prepared cache の添字も維持できる
- driver 差分を `*_open.nim` / `*_exec.nim` / `*_types.nim` に閉じ込めやすい
#### 方針B: 借用時に寿命判定、実行失敗時に bad connection 判定を入れる
初回実装は、次の 2 箇所で接続再生成を行うのがよい。
1. `getFreeConn` で idle 接続を貸し出す直前
2. クエリ / prepare 実行中に「接続断」と判断できる例外を受けた直後
1 は proactive、2 は reactive な対策である。両方あると「古い接続をなるべく使わない」と「想定外に死んだ接続をすぐ交換する」の両立ができる。
#### 方針C: ping 常用よりも age/lifetime と error 分類を優先する
毎回 ping してから貸し出す設計は単純だが、MySQL/MariaDB では同期的な `ping` が待機コストや event loop 停止要因になりうる。
そのため初手は、
- `createdAt` ベースの `maxLifetime`
- 必要なら `lastUsedAt` ベースの `maxIdleTime`
- driver 固有の bad connection エラー分類
を優先し、health check は「寿命超過時のみ」または「失敗後の再接続確認時のみ」に寄せるほうがよい。
### 5. 具体的な内部設計案
#### 5.1 `Connections` に再接続用メタデータを持たせる
各 driver の `Connections` に、少なくとも次を持たせる。
- 接続生成に必要な情報
- `maxLifetimeSeconds`
- `maxIdleSeconds`
- `lastUsedAt` または `returnedAt`
- 必要なら `isBroken`
既存 `createdAt` はそのまま使う。
#### 5.2 `replaceConn(i)` 相当の内部 proc を driver ごとに用意する
責務:
1. slot `i` の古い接続を close
2. slot `i` に紐づく prepared state を破棄
3. 新しい物理接続を開く
4. `createdAt` / `isBusy` / `lastUsedAt` を初期化
この proc は driver ごとの差分が大きいので、共通化しすぎず各 driver 実装へ置く。
#### 5.3 `getFreeConn` に「借り出し前の再生成」フックを入れる
通知ベース待機の有無に関係なく、空き slot を見つけたタイミングで次を行う。
1. `isBusy = true`
2. `shouldRecycle(conn)` を判定
3. true なら `replaceConn(i)`
4. 成功した slot を返す
ここで失敗した場合は、その slot を unusable のまま返さず、別 slot 探索または例外へ進める。
#### 5.4 実行ラッパで bad connection を捕捉して slot を再初期化する
`query`, `exec`, `prepare` など接続を直接使う箇所は多いので、driver ごとに
- `withBorrowedConn`
- `runWithConnRecovery`
のような内部ラッパへ寄せる余地がある。
このラッパは、
1. 実行
2. bad connection 例外なら slot を破棄
3. 非 transaction 時のみ 1 回だけ再接続して再試行
を担当する。
### 6. 影響の大きさから見た優先順位
優先度 P0:
- MySQL / MariaDB / PostgreSQL / SQLite で「壊れた接続を破棄して張り直す」最小導線を作る
- prepared statement の conn index 単位 invalidation を入れる
- transaction 中は自動リトライしない
優先度 P1:
- `maxLifetime` を導入して aged connection の再利用を減らす
- `maxIdleTime` を導入する
- MySQL の pool wait を通知ベースへ寄せる
優先度 P2:
- idle cleaner の別タスク化
- lazy open 化
- 統計情報の公開
今回のブランチ名と依頼内容からすると、まずは P0 と P1 の前半までを対象にするのが適切。
### 7. 現時点の結論
現在のコネクションプールで最も改善効果が大きいのは、「接続をただ保持する」のではなく、「貸し出し時と失敗時に、その接続がまだ使えるかを判断し、ダメなら slot ごと再初期化する」層を追加することだと判断した。
参考記事の `sql.DB` のうち、allographer にいま不足しているのは主に次の部分である。
- 接続寿命管理
- bad connection の廃棄と再試行
- idle connection の掃除
### 8. 実装後の方針
- pool ごとに `maxConnectionLifetime` / `maxConnectionIdleTime` を持たせ、既定値は 300 秒にした
- 借用時に `createdAt` / `lastUsedAt` を見て aged connection を `refreshConn` で差し替える
- 差し替え時は prepared statement cache を slot 単位で無効化し、古い statement handle を新しい physical connection に持ち込まない
- transaction 中は既存の接続固定を優先し、途中で別 slot に載せ替える振る舞いは入れない
特に今回の要望である「コネクションが使えない時は破棄してから、新しくコネクションを張り直す」は、driver ごとの `replaceConn(i)` と、prepared cache invalidation を軸に設計するのが最も自然である。
一方で transaction 中の自動再接続は意味論を壊しやすいため、通常クエリと同列には扱わない。ここは安全側に倒し、まずは「通常クエリのみ 1 回まで回復可能、transaction 中は即エラー」を初期設計とする。
@@ -0,0 +1,35 @@
---
description: fixブランチでの開発時に読み込む
alwaysApply: false
---
fix ブランチルール
===
allographer自体の改善
- `getFreeConn` がタイムアウトすると `errorConnectionNum` を返すが、その後の `getRowPlain` / `exec` / `getAllRows` は例外ではなくそのまま `return` している。結果として、更新できていなくてもアプリ側では成功レスポンスを返せてしまう。
- prepared statement は接続ごとに lazy prepare されるが、プール単位の cache がない。高頻度 SQL が複数接続に散ると、prepare の局所性が弱い。
- `close()` は使った接続全てに `DEALLOCATE` を送る実装なので、短寿命の prepared statement を多用すると warm 状態を毎回捨てる。
- DML 実行時の列型取得は現在は table 単位 cache になっているが、bulk update 用の raw SQL / prepared SQL を多用する場合は、そもそも query builder を通さない高速経路の価値が高い。
改善方針:
- `errorConnectionNum` に到達したときは no-op ではなく `DbError` を投げる。silent failure は benchmark だけでなく通常アプリでも危険。
- `Connections` に SQL 単位の prepared cache を持たせ、同一 SQL の physical prepare を接続ごとに 1 回へ寄せる。
- `close()` は論理 close に寄せ、物理 `DEALLOCATE` は明示 API に分離する。steady-state の benchmark では prepared state を維持できるようにする。
- 接続固定 API、あるいは transaction / prepared context を足し、同一 logical operation の複数クエリを同じ接続に寄せられるようにする。
- bulk update 用に `raw(...).exec()` だけでなく、prepared raw SQL を再利用できる経路を整える。今回のベンチマーク用途では query builder の汎用性よりも固定 SQL の再利用性が効く。
## 進捗
- [x] `errorConnectionNum` 到達時の silent return を `DbError` へ置換(PostgreSQL / MySQL / MariaDB / SQLite
- [x] PostgreSQL に SQL 単位の prepared cache (`Connections.preparedCache`) を追加
- [x] PostgreSQL prepared `close()` を論理 close 化し、`flushStmt(sql)` / `clearStmtCache()` を追加
- [x] PostgreSQL に `withConn` / `PostgresPreparedContext` を追加し、prepared `first/get/exec` の `ctx` overload を追加
- [x] `example/benchmark.nim` を prepared cold/warm 計測へ分離し、warm 側で statement 再利用経路を追加
- [x] PostgreSQL prepared statement テストに論理 close / cache clear / prepared context ケースを追加
- [x] pool wait テストに pool size 0 時の `DbError` 検証を追加(PostgreSQL / MariaDB / SQLite
## 調査結果・設計まとめ
- `errorConnectionNum` の no-op return は、DML 失敗を呼び出し側が検知できないため、全 RDB driver で同じ失敗モードを発生させていた。今回 `DbError` へ統一し、失敗を上位へ伝播させる。
- prepared cache はまず PostgreSQL に導入した。理由は `PREPARE/DEALLOCATE` のネットワーク往復コストがベンチで支配的だったため。
- `close()` の意味を「ハンドルの寿命」と「物理 prepared state の寿命」で分離し、warm 計測で state を保持できるようにした。
@@ -0,0 +1,201 @@
---
alwaysApply: true
---
プロジェクトルール
===
## 0. 規範表現(RFC 2119)と Lint方針
このドキュメントの規範キーワードは以下の意味で扱う。
- **MUST**: 必須。違反はレビューで差し戻し対象
- **SHOULD**: 強い推奨。逸脱時は理由を記録する
- **MAY**: 任意。採用時は周辺整合性を優先する
- **MUST NOT**: 禁止。明示的な合意なしに導入してはならない
各ルールには Rule ID を付与する。
- 形式: `SYW-<DOMAIN>-<NNN>`
- 併記形式: `[ID][LEVEL][CHECK]`
- `CHECK` の意味:
- `grep`: 文字列や配置で確認しやすい
- `manual`: レビューで確認する
- `test`: テストで確認する
- `doc`: 文書整合の確認
本文とルール一覧が矛盾した場合は、ルール一覧を優先する。
### 0.1 ルール一覧(要約)
| ID | LEVEL | CHECK | 対象 | 要件(要約) |
|---|---|---|---|---|
| SYW-GOAL-001 | MUST | manual | 全体 | 本リポジトリは Nim による RDB 向け query builder / migration / seeder を中核成果物とする |
| SYW-GOAL-002 | MUST | manual | API | Laravel Query Builder / Orator に学んだ直感的で連鎖可能な API を保つ |
| SYW-ARCH-001 | MUST | manual | 設計 | DB差分は driver 実装へ閉じ込め、公開 API は可能な限り共通化する |
| SYW-ARCH-002 | MUST NOT | manual | 設計 | 一時的な場当たり分岐を公開 API に持ち込まない |
| SYW-QB-001 | MUST | test | Query Builder | SQL 生成は決定的で、同じ入力に対し同じクエリ文字列・束縛順序を返す |
| SYW-QB-002 | MUST | manual | Query Builder | メソッドチェーンは可読性を優先し、Laravel/Orator 利用者が推測可能な命名を採用する |
| SYW-QB-003 | MUST NOT | manual | Query Builder | RDB 向け API に暗黙の破壊的副作用を持たせない |
| SYW-SCHEMA-001 | MUST | test | Migration | migration は冪等に再実行判定でき、履歴テーブルと整合する |
| SYW-SCHEMA-002 | MUST | test | Schema Builder | create / alter / drop は DB ごとの差異を吸収しつつ意味論を維持する |
| SYW-SEED-001 | MUST | test | Seeder | seeder は空判定または条件判定に基づき安全に再実行できる |
| SYW-COMPAT-001 | MUST | manual | 互換性 | 既存公開 API の破壊的変更は、必要性・移行方針・影響範囲を文書化する |
| SYW-TEST-001 | MUST | test | テスト | 修正した機能レイヤに対応するテストを追加または更新する |
| SYW-TEST-002 | SHOULD | test | テスト | DB 横断の共通仕様は複数 driver で検証する |
| SYW-DOC-001 | MUST | doc | ドキュメント | README / documents / ルール文書は、公開仕様変更時に同期する |
| SYW-SCOPE-001 | MUST NOT | manual | スコープ | ORM 全面実装を前提にせず、query builder・schema builder の完成度を優先する |
## 1. 目的・スコープ
### 1.1 達成目標
[SYW-GOAL-001][MUST][manual]
このプロジェクトの達成目標は、**Nim 言語で Laravel の Query Builder / ORM 的な記述感と Python Orator の使いやすさを参考にした、RDB 向け query builder を実装・改善・保守すること**である。
[SYW-GOAL-002][MUST][manual]
中核機能は以下の 3 系統である。
- Query Builder
- Migration / Schema Builder
- Seeder
[SYW-GOAL-003][MUST][manual]
利用者は「生 SQL を毎回手で組み立てなくても、読みやすいメソッドチェーンで安全に RDB を扱える」ことを期待している。実装判断は常にこの期待を優先する。
### 1.2 主要スコープ
[SYW-SCOPE-001][MUST][manual]
主要スコープは以下である。
- SQLite / PostgreSQL / MySQL / MariaDB などの RDB 向け query builder
- create / alter / drop を扱う schema builder
- migration 履歴管理
- 初期データ投入用 seeder
- 非同期実行を前提とした接続・実行モデル
[SYW-SCOPE-002][SHOULD][manual]
既存コードベースに SurrealDB 実装が存在していても、判断に迷った場合はまず RDB 向け API 品質を優先する。
### 1.3 非スコープ
[SYW-SCOPE-003][MUST NOT][manual]
以下は明示的な要件がない限り優先しない。
- Active Record 的な重い ORM 機能の全面拡張
- マジックに依存した暗黙変換の追加
- driver ごとに大きく振る舞いが異なる独自 API の増殖
## 2. 設計原則
### 2.1 API 原則
[SYW-ARCH-001][MUST][manual]
公開 API は driver 差分より先に、利用者の記述体験を基準に設計する。各 driver の都合は内部実装で吸収する。
[SYW-QB-002][MUST][manual]
メソッド名・引数順・返り値は、Laravel Query Builder / Orator を知る利用者が高い確率で推測できる形を優先する。
[SYW-QB-003][MUST NOT][manual]
`select`, `where`, `join`, `insert`, `update`, `delete`, `first`, `find`, `get` などの基本操作に、利用者が予期しない破壊的副作用を持ち込んではならない。
[SYW-COMPAT-001][MUST][manual]
公開 API の破壊的変更を行う場合は、少なくとも以下を変更箇所に残す。
- 何が壊れるか
- なぜ必要か
- 既存利用者の移行方法
### 2.2 実装分離
[SYW-ARCH-002][MUST NOT][manual]
「特定 driver だけ通すための場当たり的 if 文」を上位 API 層へ散在させてはならない。分岐は generator / lib / model など責務のある層へ閉じ込める。
[SYW-ARCH-003][MUST][manual]
責務分離の目安は以下とする。
- 接続管理: `connection` 系
- API 入口と共通操作: `query_builder` / `schema_builder`
- DB 方言差分: `models/*/query/*`, `libs/*`
- 実行・変換: `*_exec`, `*_types`, `*_query`
[SYW-ARCH-004][SHOULD][manual]
新機能は、まず共通仕様を言語化してから driver 差分へ展開する。最初に 1 driver だけ特殊実装へ逃がさない。
## 3. Query Builder ルール
[SYW-QB-001][MUST][test]
同じ builder 入力は、同じ SQL 文字列と同じ bind 順序を生成しなければならない。出力が非決定的になる変更は禁止する。
[SYW-QB-004][MUST][manual]
where 句、join、group by、order by、limit / offset、paginate などの句の結合順は、利用者の記述順と SQL としての自然順を壊さない。
[SYW-QB-005][SHOULD][manual]
値の埋め込みよりも、既存のプレースホルダ・エスケープ・型変換の仕組みを優先して使う。
[SYW-QB-006][MUST][test]
`first` / `find` / `get` の空結果時の振る舞いは既存仕様を維持する。`Option` や空シーケンスの扱いを無言で変えてはならない。
[SYW-QB-007][SHOULD][manual]
ログ出力やデバッグ補助を追加する場合は、通常経路の性能や API 純度を汚さない。
## 4. Migration / Schema Builder ルール
[SYW-SCHEMA-001][MUST][test]
migration は履歴テーブルにより再実行可否を判定できなければならない。二重適用や履歴不整合を起こす変更は禁止する。
[SYW-SCHEMA-002][MUST][test]
`create`, `alter`, `drop`, `rename`, `add column`, `change column`, `drop column` は、DB ごとの差異があっても利用者にとって一貫した意味を維持する。
[SYW-SCHEMA-003][MUST][manual]
破壊的な schema 操作を追加・変更する場合は、ロールバック不能性やデータ移行影響を必ず確認する。
[SYW-SCHEMA-004][SHOULD][manual]
column 定義 API は Laravel ライクな表現を優先し、命名やオプションの意味が直感に反しないようにする。
## 5. Seeder ルール
[SYW-SEED-001][MUST][test]
seeder は再実行時に重複データを無制限に増やさないよう、空テーブル判定または列条件判定を用いる既存思想を維持する。
[SYW-SEED-002][SHOULD][manual]
サンプル seeder と実装が乖離した場合は、利用者が最初に触れる example を優先して同期する。
## 6. テスト・品質保証
[SYW-TEST-001][MUST][test]
コード変更時は、影響する最小レイヤのテストを追加または更新する。
- query 生成を変えたら query テスト
- schema 操作を変えたら schema / create_schema テスト
- トランザクションを変えたら transaction テスト
- utility を変えたら utils テスト
[SYW-TEST-002][SHOULD][test]
共通 API の変更は、可能な限り SQLite だけでなく PostgreSQL / MySQL / MariaDB 系にも同種の検証を広げる。
[SYW-TEST-003][MUST][manual]
テスト追加が難しい場合は、その理由と未検証リスクを作業記録へ残す。
## 7. ドキュメント・例・後方互換
[SYW-DOC-001][MUST][doc]
公開仕様を変えたら、必要に応じて以下を同期する。
- `README.md`
- `documents/rdb/*.md`
- `example/`
- `.cursor/rules/*.mdc`
[SYW-DOC-002][SHOULD][doc]
サンプルコードは「最短で使い方が伝わる」ことを優先し、実装とズレた古い記法を放置しない。
## 8. 実装判断の優先順位
[SYW-PRIO-001][MUST][manual]
判断に迷った場合の優先順位は以下とする。
1. 利用者にとって API が自然か
2. 既存公開仕様を壊さないか
3. 複数 RDB で意味が揃うか
4. 内部実装が責務分離されているか
5. テストと文書で保守できるか
## 9. このプロジェクトで避けること
[SYW-ANTI-001][MUST NOT][manual]
以下は避ける。
- 一部 driver だけを通すための公開 API 汚染
- テストなしの SQL 生成変更
- migration 履歴や seeder 冪等性を壊す変更
- README と実装の乖離放置
- Nim らしさよりも複雑なマジックを優先すること