Instruction file imported from dumblepy/nim-allographer (
.cursor/rules/branch/336-update-prepared-statement.mdc). Copyright stays with the author.
336-update-prepared-statement ブランチでの prepared statement 横展開
このブランチで実装することは以下の通りです。
- 既存の SQLite / PostgreSQL / MySQL / MariaDB の prepared statement API を参照しつつ、SurrealDB 向け prepared statement API を追加する
- SurrealDB v3 の
/sqlAPI と SurrealQL のLET/ parameter 仕様を前提に、server-side prepare ではなく client-side template reuse と 1 リクエスト多文実行で prepared statement 的な使い勝手を実現する - SurrealDB 固有事情は
models/surrealとlibs/surrealに閉じ込め、公開 API は既存 driver と可能な限り揃える - このターンでは実装は行わず、調査結果と実装方針の設計書を固める
進捗
-
.cursor/rules/project.mdc/.cursor/rules/branch.mdc/.cursor/rules/branch/336-update-prepared-statement.mdcを確認した - 既存 prepared statement API の到達点を SQLite / PostgreSQL / MariaDB / MySQL で確認した
- 現在の SurrealDB 実装と
/sql利用箇所を確認した - SurrealDB v3 公式ドキュメントで parameter / HTTP
/sql/ transaction の仕様を確認した - SurrealDB prepared statement の設計と利用例を
documents/surrealdb/prepared_statement.mdに整理した -
models/surrealに prepared statement 用の型と公開 API を追加した -
libs/surrealに multi-statement 実行向けの共通処理を追加した - SurrealDB prepared statement のテストを追加した
-
documents/surrealdb/query_builder.md/documents/rdb/query_builder.mdを更新した
参考資料
- 既存 prepared statement API
src/allographer/query_builder/models/sqlite/sqlite_types.nimsrc/allographer/query_builder/models/sqlite/sqlite_exec.nimsrc/allographer/query_builder/models/postgres/postgres_types.nimsrc/allographer/query_builder/models/postgres/postgres_exec.nimsrc/allographer/query_builder/models/mysql/mysql_exec.nimsrc/allographer/query_builder/models/mariadb/mariadb_exec.nim
- 現在の SurrealDB 実装
src/allographer/query_builder/models/surreal/surreal_types.nimsrc/allographer/query_builder/models/surreal/surreal_exec.nimsrc/allographer/query_builder/models/surreal/surreal_open.nimsrc/allographer/query_builder/libs/surreal/surreal_impl.nimsrc/allographer/query_builder/libs/surreal/surreal_lib.nimsrc/allographer/schema_builder/queries/surreal/schema_utils.nim
- 既存ドキュメント
documents/surrealdb/query_builder.mddocuments/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 である。
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"])
)
採用方針:
preparegetfirstexeccloseflushStmtclearStmtCachewithConnSurrealPreparedContext
初期スコープでは getPlain / firstPlain は後回し候補とする。SurrealDB の戻り値は object 中心で列定義が RDB より弱く、列順の意味が薄いため、まずは既存の JsonNode 中心 API を優先する。
3. 実装モデル
prepared statement 実行時は、毎回次の 3 段階で 1 リクエストを組み立てる。
- 元 SQL の
?を$a,$b, ... に変換する - 各引数を
LET $a = ...; LET $b = ...;に変換する LET群と本体 SQL を;で連結して/sqlに 1 回だけ POST する
概念的には次の形になる。
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 を持つ。
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 を確認し、副作用系として扱うLETstatement の 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内のctxoverload が通ること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 に閉じ込められる。