Skip to content

Latest commit

 

History

History
72 lines (45 loc) · 7.8 KB

runtime.ja.md

File metadata and controls

72 lines (45 loc) · 7.8 KB

ランタイム

English

文はsqlshapeパッケージで宣言し(Query、One。依存は無い)、使うデータベースのランタイムモジュールで実行する。github.com/kr9ly/sqlshape/postgres/v2はpgxの上(postgres.ja.md)、github.com/kr9ly/sqlshape/mysql/v2はdatabase/sqlの上(mysql.ja.md)。このページは両方が同じにやることを書く。例はpostgresの関数で示すが、mysqlの関数も同じ名前と同じ形をしている。検査器が読むのは宣言だけなので、自前のランタイムで実行してもよい。その場合に得られないものは末尾に書く。

文の実行

var ListOrders = sqlshape.Query[Order, ListParams](`...`)

for o, err := range postgres.Run(ctx, db, ListOrders, p) { ... }   // iter.Seq2[Order, error]、逐次読み出し
orders, err := postgres.Collect(ctx, db, ListOrders, p)            // []Order
first, err  := postgres.First(ctx, db, ListOrders, p)              // 最初の行。無ければ ErrNoRows
tag, err    := postgres.Exec(ctx, db, MarkPaid, p)                 // ドライバの結果(pgconn.CommandTag / sql.Result)。行は捨てる

One[R, P]はSingleを返す。RenderはStmtと同じで、実行方法は3つある:

var UserByEmail = sqlshape.One[User, struct{ Email string }](`...`)
var MarkOrderPaid = sqlshape.One[struct{}, struct{ ID int64 }](`UPDATE orders SET paid_at = now() WHERE id = {{.ID}}`)

u, err     := postgres.Get(ctx, db, UserByEmail, p)       // 無ければ ErrNoRows
u, ok, err := postgres.Find(ctx, db, UserByEmail, p)      // ok が有無を表す
tag, err   := postgres.ExecOne(ctx, db, MarkOrderPaid, q) // 1行も対象にならなければ ErrNoRows

3つとも、2行目が返ってきたらErrManyRowsを返す。検査器は1行以下しか返らないことをスキーマから証明しているので、これが起きるのは、証明の根拠になった一意制約が実際のデータベースでは外れているときである。

行のマッピング

結果列は名前でフィールドに対応づけられる。col:"..."タグ、次にdb:"..."タグ、次にフィールド名をsnake_caseにしたものの順で探す。埋め込み構造体のフィールドは平坦化される。NULLを受けられるフィールド(ポインタ、スライス、マップ、sql.Null*、pgtype.*のようなドライバのNULL可の値型)はNULLをゼロ値として受ける。この展開の結果に列が無いフィールド(一部の分岐だけが選ぶ列)もゼロ値のままになる。Rがスカラーなら1列を直接受ける。numeric / DECIMALはstringで受けると全桁が保たれる。列やパラメータをどのGo型で受けられるかはデータベースごとの表にある(postgres.ja.md、mysql.ja.md)。

Goのenum型にKnown() boolを実装しておくと(Labelledインターフェース)、マッパーはこのビルドが知らないラベルを受け取ったとき、switchで扱えない値をアプリケーションに渡す代わりに*UnknownLabelErrorを返す。

エラー

制約違反はランタイムの*ConstraintErrorとして返る(postgres.ConstraintErrorは*pgconn.PgErrorを、mysql.ConstraintErrorはドライバのエラーを包む)。サーバが報告した内容を持ち、Key()はexpect行に書くのと同じ表記の名前を返す。データベースが制約に付ける名前、またはNOT NULLならtable.columnである(postgres.ja.md、mysql.ja.md)。expect行で名指したSQLSTATE(PostgreSQLのトリガのP0401、MySQLのSIGNAL自身のコード)も同じように包まれる。PostgreSQLはschema.sqlを読まないので-- sqlshape: error注釈のNameを検査器が突き合わせたコードに戻せず、expect行が何か書いてある文が受け取ったカスタムSQLSTATEはすべて包む——検査器がその文について予告済みと確認できたものだけに限らない。

_, err := postgres.First(ctx, db, CreateCustomer, p)
if postgres.Violates(err, "customers_email_key") {
	return ErrEmailTaken
}

この名前は検査器が列挙したものと同じなので、コードが処理していない違反があれば、それはexpect行に書いてあるのに扱っていない違反である(checks.ja.md)。ErrNoRowsはドライバのもの(pgx.ErrNoRows、sql.ErrNoRows)で、IsNoRows(err)で判定できる。

-- sqlshape: error <code> = <Name>注釈のNameは、sqlshape.Error(code)でGoに写し取られる(checks.ja.md)。これはsqlshape.Failure——コードそのものを保持する~string——を返す。ViolatesはFailureも生の文字列も同じに扱い(Violates[K ~string](err error, key K) bool)、渡されたコードだけで判定する。スキーマを読まないので、Nameとたまたま同じ文字列との区別が付かず、両者を解決し合うこともない。OrderTooLargeがsqlshape.Error("P0401")なら、Violates(err, OrderTooLarge)とViolates(err, "P0401")はまったく同じ呼び出しである。

Run / Execの外で走らせた文(pgxを直接、マイグレーションスクリプト、テスト)のエラーはpostgres.WrapError(err)で同じ包み方になり、Violatesで判定できる。そこでは文自身のexpectのコードは分からないので、制約のクラスだけを包む。

検査済みのSQLのみが実行できる

実行のたびに、テンプレートから組み立てたSQLが、検査器が同じ分岐の組み合わせについて検査したSQLと一字一句一致することを確認する。一致しなければクエリは投げずにエラーになる:

sqlshape: rendered SQL differs from the checked expansion [if@64:then]: the runtime evaluator and the checker disagree; please report this

これが出るのはsqlshapeの不具合なので、報告してほしい。通常の使い方で出ることはない。

確認できない場合が2つある。{{range}}が3要素以上のとき(検査は2要素までで行う)と、分岐の組み合わせが256を超えて代表だけが検査されたとき(templates.ja.md)。この2つでは、分岐の形が検査したものと同じであることだけを確認して実行する。

データベースのランタイムが足すもの

上の関数の他に、各ランタイムはそのドライバとデータベースにあって相手に無いものを持つ。pgxではUnprepared、ネストした行とユーザー定義型とLoadUserTypes、Batch、Copy、MatView(postgres.ja.md)。database/sqlではExecOneのRowsAffectedの読み方と、サーバ設定を確かめるmysql.Verify(mysql.ja.md)。

自前のランタイムでは得られないもの

検査器が認識するのは宣言であってランタイムではない(-queryで自前のマーカー関数を登録できる。flags.ja.md)。次の3つの約束はランタイムのもので、sqlshape/postgresかsqlshape/mysqlで実行したときだけ成立する: 送る SQL が検査器の確かめた展開とバイト単位で一致すること(上の節)、違反が expect 行の綴りの名前でConstraintErrorとして返ること、Oneの文が2行目を返したらエラーになること。