SQLite PoolとTransaction
std.db.sqliteはNagi 0.2.0に含まれ、0.1.11には含まれません。SF05で旧Db/db_*を廃止したため、更新前に移行を参照してください。
Rustからcompilerの標準module/resource/operation metadata enumを網羅matchする組込み利用者は、新しいQuery/literal variantへの対応が必要です。
このページの目次
何ができるか
Pool は複数のSQLite接続を管理し、Tx は一つの接続を専有します。SQL構造はopaque Query、実値はtyped owned Parametersで渡します。旧Db/db_*、runtime public Db/Sql、動的文字列factoryは削除し、旧Nagi入口はSF05 migration診断にします。旧APIとの併走fallbackはありません。
まず動かす
必要なもの
- Rust toolchain(
rustcとcargo)とRust crateをビルドできるC toolchain。 - このページと
examples/sqlite_pool.nagi、Nagi 0.2.0のソースcheckout(取得したソースの作業用フォルダー)。手順はrepository rootで実行します。 - SQLiteのコマンドラインツールは不要です。SQLiteはNagi runtimeに同梱されます。
ソースの取得方法はソースからcompilerをbuildする手順を参照してください。この手順は0.2.0のソースとcompilerを使います。インストール済み0.2.0を使う場合は、下記の./target/release/nagicをnagicに置き換え、compilerのbuildを省略できます。
cargo build がnative dependencyで止まる場合は、Rust toolchainに加えてOS向けC compiler/linkerが使えることを確認してください。nagic が見つからない場合は、下のrelease binaryを直接実行するか target/release をPATHへ追加します。
コンパイラとサンプルを実行する
repository root(Cargo.toml があるdirectory)で、compilerをbuildし、サンプルをcheckしてから実行します。
cargo build --release --locked -p nagic
./target/release/nagic check examples/sqlite_pool.nagi
./target/release/nagic run examples/sqlite_pool.nagi
check が成功すると診断なしで終了し、run は次を標準出力へ表示します。
7
closed
Windowsでは同じrepository rootでrelease executableを指定します。PATHに追加済みなら nagic.exe だけを使えます。
cargo build --release --locked -p nagic
./target/release/nagic.exe check examples/sqlite_pool.nagi
./target/release/nagic.exe run examples/sqlite_pool.nagi
このsampleは :memory: DBを一接続で開き、tableを作り、7 を保存して読み直し、最後にpoolをcloseします。query は0行なら None、1行以上なら先頭行を Some にします。sampleの main が成功したときの出力は 7 と closed です。失敗時に nagic run がどのerror文面を表示するかはAPI契約ではありません。
sampleの std.result.map_error は、異なるerror型をアプリの AppFailure に明示変換します。options と bind_f64 は通常の Error を返し、DB操作は sqlite.Failure を返します。Failure は Error へ暗黙変換されません。
import std.db.sqlite as sqlite
import std.result as result
class Total:
amount: i64
enum AppFailure:
Configuration(cause: Error)
Database(cause: sqlite.Failure)
def configuration_error(cause: Error) -> AppFailure:
return AppFailure.Configuration(cause)
def database_error(cause: sqlite.Failure) -> AppFailure:
return AppFailure.Database(cause)
async def write_and_read(pool: view[sqlite.Pool]) -> Result[i64, sqlite.Failure]:
tx = try await sqlite.begin(pool, sqlite.BeginMode.DEFERRED)
try await sqlite.exec(tx, sqlite.literal("CREATE TABLE IF NOT EXISTS amounts(amount INTEGER NOT NULL)"), sqlite.parameters())
values = sqlite.bind_i64(sqlite.parameters(), 7)
try await sqlite.exec(tx, sqlite.literal("INSERT INTO amounts VALUES (?)"), values)
try await sqlite.commit(tx)
tx = try await sqlite.begin(pool, sqlite.BeginMode.DEFERRED)
row = try await sqlite.query[Total](tx, sqlite.literal("SELECT amount FROM amounts"), sqlite.parameters())
try await sqlite.rollback(tx)
match row:
case Some(total):
return ok(total.amount)
case None:
return ok(0)
async def main() -> Result[unit, AppFailure]:
config = try result.map_error(sqlite.options(1, 2, 1000, 0), configuration_error)
pool = try result.map_error(await sqlite.open(":memory:", config), database_error)
work = await write_and_read(view(pool))
ending = await sqlite.close(pool, 1000)
# Observe close even when the transaction failed; preserve the work error first.
amount = try result.map_error(work, database_error)
try result.map_error(ending, database_error)
print(amount)
return ok(print("closed"))
write_and_read が失敗しても main はcloseをawaitしてからworkのErrを返します。workとcloseが両方失敗した場合、このsampleはwork側を優先しclose側のErrを返しません。業務アプリでは両方をログや独自errorに記録してください。
手順が失敗したら
| 症状 | 確認すること |
|---|---|
cargo build がC/native dependencyで失敗 |
repository rootか、Rust toolchain・C compiler/linkerが使えるか |
nagic: command not found |
target/release/nagic(Windowsは target/release/nagic.exe)を直接指定するかPATHを確認 |
check が未対応module/APIを示す |
実行中compilerがこのworktreeのソースからbuildされたか。インストール済み0.1.11には本APIがない |
check がrow型を拒否 |
query[T] / all[T] の T が標準行readerを生成できるclassか、必要なfield型か |
run がSQLite Failureで終了 |
APIが返す kind と message を確認する。lazy openのため、pathやnative接続の失敗が begin で初めて現れることがある |
用語と値の所有
| 型 | 役割・主な条件 |
|---|---|
Options |
接続数、各workerのqueue capacity、acquire timeout、SQLite busy timeoutを保持する。constructorで全値を指定する |
Query |
直接literal由来のopaque Copy値。保存・共有・Debug可、Eq/JSON不可 |
Pool |
接続の取得とcloseを管理する。clone_pool が同じclose/failure状態を共有するhandleを作る |
Tx |
1接続のtransaction session。非Copyで、同じtaskの中だけで使う。fieldやshared値に格納できない |
Parameters |
SQL bind値のowned list。各bind builderは受け取った値をconsumeして新しい Parameters を返す |
BeginMode |
DEFERRED、IMMEDIATE、EXCLUSIVE |
Failure |
DB操作の失敗。kind、outcome、retired、borrowed message fieldを持つ |
FailureKind |
failureの分類 |
Outcome |
failure時に確認できたtransaction結果 |
Pool はnon-Copyです。共有handleを増やすときは sqlite.clone_pool(pool) を使います。各cloneはclose開始・failureを共有します。Tx はnon-Copy/non-Clone/non-sharedで、task境界を越えず、class fieldへ保存できません。同じtask内のlocal、Option / Result、owned関数委譲では使えます。通常のSQLite operationは view[Tx] として暗黙に借ります。自作関数が view[Tx] を受け取る場合は、呼出し側で view(tx) を明示します。
commit(tx) と rollback(tx) は引数の Tx をconsumeします。Futureを作った時点で所有権が移るため、await後に同じhandleを使えません。読み取りを終えた後でtransactionを続けるなら、古いTxを再利用せず新しく begin します。
設定とpath
options(connections, queue_capacity, acquire_ms, busy_ms) の4引数は省略できません。数値のdefaultはありません。
| 引数 | 条件と意味 |
|---|---|
connections |
正数で、usize とTokio Semaphoreのpermit上限に入ること |
queue_capacity |
正数で、同じpermit上限に入ること。各workerのcommand inboxの容量で、Pool全体の待機task数やheap使用量の上限ではない |
acquire_ms |
0以上でnative Duration / deadlineへ変換できること。Poolの論理slot待ちとnative slot登録を合わせた取得予算 |
busy_ms |
0以上でnative時間範囲内、かつSQLiteの i32 millisecond範囲内。SQLiteがdatabase lockの解除を待つ時間 |
capacity超過、負数、時間変換範囲外は options の Error です。0msは「必ずtimeout」ではなく待たない指定です。要求時点で条件が満たされていれば成功します。
open(path, options) は空pathと file: URIを拒否します。:memory: はconnectionごとに別DBになるため、connections は1だけです。それ以外は通常のfilesystem pathを使い、相対pathは実行時current working directoryを基準にします。open はpathをFuture作成時に所有しますが、native connectionはlazyに begin 時に作ります。したがってfilesystem/native openの失敗が open ではなく最初の begin の WORKER failureとして返る場合があります。自動WAL、URI解釈、共有memory URIはありません。
sqlite.literal("SELECT id FROM items WHERE id = ?") はcanonical identityにより直接literalだけを受理します。変数・view・連結・format・関数返却strは元位置付きで拒否します。同名のユーザー関数は占有しません。QueryはCopyでlocal/field/返却値に使えますが、SQL文字列を取り出せず、JSON・Eq・直接構築はできません。query/all/execはQuery必須です。通常checkはSQL parserを持たず、SQLのshapeはruntimeまたは明示SQL事前検査が確認します。
型付き引数とSQLの形
parameters()で空の値列を作り、builderの戻り値を次のbuilderへ渡します。追加した順番が、SQLの匿名placeholder ? に対応します。
| builder | 追加する値 | 条件 |
|---|---|---|
bind_i64(parameters, value) |
i64 |
整数 |
bind_f64(parameters, value) |
f64 |
有限値のみ。NaN/InfinityはError |
bind_text(parameters, value) |
str |
所有する文字列を渡す |
bind_bytes(parameters, value) |
bytes |
所有するbyte列を渡す |
bind_null(parameters) |
SQLiteのNULL | 型情報を持つNULLではない |
新APIは一度に一つのSQL文と匿名?だけを受け付けます。名前付きの:name / @nameや番号付きの?1は使えません。bind数、値型、NULLの読み取りはruntimeで検査します。通常のcheckが実DBの値を使ってSQLを実行することはありません。値はSQL文字列へ連結せず、Parametersで渡します。
query・all・execの使い分け
| 操作 | 受け付けるSQL | 戻り値 |
|---|---|---|
query[T] |
SQLiteがreadonlyと判定し、列を返す一文。SELECTという単語だけで判定しない |
Result[Option[T], Failure]。0行ならNone、それ以外は先頭行のSome |
all[T] |
queryと同じreadonlyな行を返すSQL | 全行のResult[List[T], Failure] |
exec |
結果の列を返さない一文。通常のtable/index/view/trigger DDLも利用可 | その文の直接変更件数を表すResult[i64, Failure] |
RETURNING付きの書き込みは、readonlyでもcolumnなしでもないため、上のどの操作も受け付けません。利用者SQLにBEGIN / COMMIT / ROLLBACKとそのvariant、SAVEPOINT、PRAGMA、ATTACH、DETACHを書かず、moduleの操作でtransactionを開始・終了します。virtual table、extension、raw connectionの公開はありません。
Tは標準FromRow生成が対応するfieldを持つclassです。scalarのi8/i16/i32/i64/u8/u16/u32/f32/f64/bool/str/bytes、そのOption、対応するowned[...] wrapperを使えます。field名を返却列名に合わせ、NULLを受け取るfieldにはT?を使います。enum、resource、任意generic、未対応fieldはcheckで拒否します。実データの値型・NULL・整数範囲はruntimeのResultで扱います。手書きRustのFromRowを用意しても、新APIのchecker制約は回避できません。
Public typesと操作一覧
このmoduleは9型と19操作を公開します。view[T] は値をborrowする型、Future[T] はawaitする非同期処理、Result[T, E] は成功値かerrorのどちらかです。tryは現在のfunctionからErrを返します。より詳しい言語規則はview、async、Resultを参照してください。
| 型 | Copy / field保存 / shared / debug | 意味 |
|---|---|---|
Query |
Copy / storage・shared・Debug可、Eq/JSON不可 | 直接literal由来の固定構造 |
Pool |
non-Copy / storage可 / shared可 / Debug可 | clone_poolで同じpoolのhandleを増やす |
Tx |
non-Copy / storage不可 / shared不可 / Debug不可 | 同じtaskでのみ使うaffine transaction handle |
Parameters |
non-Copy / storage可 / shared不可 / Debug不可 | owned bind値をbuilderで積む |
Options |
non-Copy / storage可 / shared不可 / Debug可 | optionsで検証して作る設定値 |
BeginMode |
Copy・equality可 / storage・shared・Debug可 | DEFERRED / IMMEDIATE / EXCLUSIVE |
Failure |
non-Copy / storage・shared・Debug可 | DB error、結果とretirement metadata |
FailureKind |
Copy・equality可 / storage・shared・Debug可 | 下表の13分類 |
Outcome |
Copy・equality可 / storage・shared・Debug可 | 下表の5状態 |
| 操作 | signature | 使い方と戻り値 |
|---|---|---|
literal |
(sql: str) -> Query |
直接文字列literalのみをcheckerで受理 |
options |
(connections: i64, queue_capacity: i64, acquire_ms: i64, busy_ms: i64) -> Result[Options, Error] |
全値を明示し範囲を検証 |
open |
(path: view[str], options: Options) -> Future[Result[Pool, Failure]] |
pathを所有しPoolを作る。native connectionはlazy |
clone_pool |
(pool: view[Pool]) -> Pool |
close/failure状態を共有するhandle |
begin |
(pool: view[Pool], mode: BeginMode) -> Future[Result[Tx, Failure]] |
logical slotを取得してtransactionを始める |
parameters |
() -> Parameters |
空のbind list |
bind_i64 |
(parameters: Parameters, value: i64) -> Parameters |
integerを追加 |
bind_f64 |
(parameters: Parameters, value: f64) -> Result[Parameters, Error] |
finite floatだけ追加 |
bind_text |
(parameters: Parameters, value: str) -> Parameters |
owned textを追加 |
bind_bytes |
(parameters: Parameters, value: bytes) -> Parameters |
owned bytesを追加 |
bind_null |
(parameters: Parameters) -> Parameters |
plain SQLite NULLを追加 |
query[T] |
[T](tx: view[Tx], sql: Query, parameters: Parameters) -> Future[Result[Option[T], Failure]] |
row-producing readonly SQLの先頭行 |
all[T] |
[T](tx: view[Tx], sql: Query, parameters: Parameters) -> Future[Result[List[T], Failure]] |
row-producing readonly SQLの全行 |
exec |
(tx: view[Tx], sql: Query, parameters: Parameters) -> Future[Result[i64, Failure]] |
columnを返さない一文 |
commit |
(tx: Tx) -> Future[Result[unit, Failure]] |
transactionを終端してcommit |
rollback |
(tx: Tx) -> Future[Result[unit, Failure]] |
transactionを終端してrollback |
close |
(pool: view[Pool], timeout_ms: i64) -> Future[Result[unit, Failure]] |
closingを開始し、worker joinを待つ |
copy_primary_error |
(problem: view[Failure]) -> Option[Error] |
Failureのprimary causeを明示コピー |
copy_cleanup_error |
(problem: view[Failure]) -> Option[Error] |
cleanup causeを明示コピー |
async operationではawait後にResultをtryまたはmatchで処理します。optionsとbind_f64の失敗はError、残りのDB operationはFailureを返します。
Parametersの使い方
bind_i64などは引数のParametersをmoveして返します。次のbindへ返却値を渡します。query / all / exec もParametersをconsumeするので、同じ値を二回渡せません。
def make_values() -> sqlite.Parameters:
values = sqlite.parameters()
values = sqlite.bind_i64(values, 4)
values = sqlite.bind_text(values, "Nagi")
return values
この値はSQLの匿名?へ順番にbindされます。queryに渡したあと別文にも値を渡したい場合は、文ごとにParametersを作るか、値を再構築してください。copyでParametersを複製することはできません。
複数行を読む
all[T]はrow classを指定し、全行を返します。例えば現在のTotal classでamountを読む処理は次の形です。
async def read_all(tx: view[sqlite.Tx]) -> Result[List[Total], sqlite.Failure]:
return await sqlite.all[Total](tx, sqlite.literal("SELECT amount FROM amounts ORDER BY amount"), sqlite.parameters())
queryは最初の1行だけ、allは全行です。readonly SQLの結果を1件だけ扱うならquery、複数件を走査するならallを選びます。execはrow結果を返さないDDLやwrite向けです。operation形の誤り、複数文、named/numbered placeholderはruntimeで拒否されます。
間違いと直し方
| 間違い | 段階と理由 | 修正 |
|---|---|---|
sqlite.query[i64](tx, ...) |
checkで拒否。新APIは標準FromRow生成を行うclassが必要 |
amount: i64 fieldを持つclassを作ってquery[Amount]を使う |
sqlite.exec(tx, sqlite.literal("DELETE FROM items WHERE id = ?1"), params) |
実行時にSQL failure。新APIは匿名?だけ |
?を使い、Parametersを同じ順にbuilderで作る |
sqlite.all[Row](tx, sql, values)の後もvaluesを使う |
allがParametersをconsumeする |
各SQL用に別のParametersを作る |
try await sqlite.commit(tx)の後で同じtxを再利用 |
commitはFuture作成時からTxをconsume |
次のSQLは新しいbegin後のTxで実行 |
Txをclass fieldへ保存、またはchild taskへmove |
checkで拒否。Txはstorage/shared/task境界を越えない |
task内で終端するか、child task内で新しくbeginする |
def read(tx: sqlite.Tx)へ呼出側のtxを渡す |
owned parameterへTxをmoveし元から使えなくなる | 引数をview[sqlite.Tx]にし、呼出し側でread(view(tx))とする |
自作helperは次のようにborrowしたTxを受け取り、同じtask内で使えます。
async def read_total(tx: view[sqlite.Tx]) -> Result[Total?, sqlite.Failure]:
return await sqlite.query[Total](tx, sqlite.literal("SELECT amount FROM amounts"), sqlite.parameters())
async def call_read_total(tx: sqlite.Tx) -> Result[Total?, sqlite.Failure]:
return await read_total(view(tx))
SQLite authorizerはSQL内の BEGIN / COMMIT / ROLLBACK、SAVEPOINT、PRAGMA、ATTACH / DETACH も拒否します。transaction境界にはmodule operationを使います。
transactionと並行処理の境界
Poolのlogical slot待ちはTokioのFIFO semaphoreへ委譲されます。FIFOの説明はこのlogical permit waitに限ります。native workerの起動、native slot登録後のadmission、SQLite BEGIN 完了までの順序は保証しません。
acquire_ms はlogical permit待ちからnative record登録まで同じ取得予算を使います。connection startupと登録後のready/BEGIN SQL busy waitはこの予算に含みません。busy_ms がSQLite busy waitを制限します。SQL実行・commitに別のinterrupt deadlineはありません。
一つの Tx が一接続を専有します。Pool cloneは共有しても、Txをchild taskへmove/captureしたり、別taskから同じTxを使ったりできません。必要ならchild task内でPoolから独自に begin します。外側taskの TaskFailure(子自体の失敗)と、そのtaskの戻り値に入る Result[..., sqlite.Failure](DB操作の失敗)は別の層です。
普通のSQL、bind、decode errorでもnative transactionがactiveだと確認できればTxは継続できます。ただし失敗したstatementが変更を一切していないとは限らず、同じtransaction内の先行効果が残る場合があります。SQLiteが自動rollbackした場合は ABORTED / ROLLED_BACK として後続操作を拒否します。cancelは受理済みSQLや外部副作用をundoしません。retryやrollbackが安全だと推測せず、業務のidempotencyと明示rollbackを設計してください。
Failureを読む
Failureの分類とtransaction結果を分けて読みます。outcome は観測できた状態であり、再実行の安全性を表しません。
FailureKind |
意味 |
|---|---|
INVALID |
path/optionsやtimeout設定が不正 |
CLOSED |
Poolがclosingまたはclosed |
ACQUIRE_TIMEOUT |
取得予算内にlogical/native slotを確保できない |
BUSY |
SQLite database lockがbusy/locked |
SQL |
SQLのprepare/実行に失敗 |
BIND |
placeholder/value bindingの失敗 |
DECODE |
SQLite rowからclass fieldへの変換失敗 |
ABORTED |
SQLite native transactionが既に終了した |
CLEANUP |
transaction/connection cleanupの失敗 |
WORKER |
native connection起動またはworker処理の失敗 |
REPLY_LOST |
workerから結果を受け取れず、結果が不明 |
CLOSE_TIMEOUT |
close期限までに全worker joinを確認できない |
ALLOCATION |
fallible ledger/idle reservationに失敗 |
ALLOCATION は明示的なfallible reservation失敗を分類します。すべてのOOMやallocator abortから回復する保証ではありません。
Outcome |
意味 |
|---|---|
NOT_APPLICABLE |
このfailureに関連するactive Txがない、またはまだ存在しない |
ACTIVE |
failure時点でnative Txがactiveだと確認できた |
COMMITTED |
commit完了を確認した |
ROLLED_BACK |
rollbackまたはSQLiteのautomatic rollbackを確認した |
UNKNOWN |
完了状態を確定できない |
retired はそのFailureで接続のretirementが確認されたかを表します。false は「このfailureからretirementを確認できていない」だけで、workerやPoolが健全だと証明しません。特に REPLY_LOST + UNKNOWN から、rollback済み・connection再利用可能・retry安全を推論しないでください。Debug表示は kind / outcome / retired に限定されますが、公開 message の文字列は安定したprotocolではありません。
Failure.message は Failure ownerを借りる view[str] です。Failureより長く保持するなら copy(problem.message) で所有文字列を作ります。native causeをerrorとして保持したいときは copy_primary_error(problem) と copy_cleanup_error(problem) を明示的に使います。どちらも Option[Error] で、causeがなければ None、ある場合はkind/messageをコピーします。commitが完了してcleanupが失敗するなど、primaryとcleanupの情報が別々に存在するfailureがあります。
Poolを閉じる
close(pool, timeout_ms) はcloneすべてで共有するclosingを開始し、新しい begin を止め、active Txの終了とworkerのnative close/actual joinを待ちます。timeoutが0でも既に終了を確認済みなら成功できます。timeoutやclose Futureのcancel後もclosingは戻りません。active Txを強制終了せず、後の close でも完了を待てます。
Pool の最後のhandleをDropしても、非同期cleanupやworker joinの完了確認にはなりません。Dropは閉鎖を要求するだけです。終了を確認する必要がある処理では、所有者が close をawaitしてください。close成功はnative closeとworker threadのactual joinを確認した結果です。無期限にblockするSQL、process kill、allocator abortからcloseが必ず戻る保証はありません。
SQLの事前検査
通常checkはQueryのliteral境界、型・所有権・行fieldを検査し、SQLiteエンジンを起動しません。明示schemaによるopt-in検査は直接Query constructorのSQL形・schema・必要列をprepare-onlyで確認します。直接Parameters builder列のbind個数は照合し、変数/関数経由のQuery構造やParameters個数はruntime検査として表示します。実値型・NULL・数値範囲・DB内データはruntimeで確認します。SQL事前検査を参照してください。