serversessionのアップデートで新しいpersistentに対応した時の格闘記録2
serversessionのアップデートで新しいpersistentに対応した時の格闘記録 - ncaq の続き。
この形式はZennとかでよく見るスクラップだなと気がついたので、次からはタイトルはスクラップとかにしたいですね。ここはZennではありませんが。
相も変わらず、 yesodweb/serversession: Secure, modular server-side sessions. の新しいLTS対応を行っていて、そこで障害になるpersistentのアップデートと戦っています。
今回のステータスは、土曜日16時間、日曜日12時間ぐらい寝たので、寝過ぎでは…? 何故かちょっと眠いけどメチャクチャ頭冴えて思いつかなかった正しい方法がわかるぞと思って、数時間バリバリと頭が動いてたんですが、頭が動きすぎて嫌な記憶が沸々と湧いてきたので頓服薬を飲んで対処したのと、ログ見返してみたら今日は4時間しか寝てなかったことが分かって結局ダメです。
前回の後日談
前回PRを出して「メンテナ誰ですか?」って聞いたり、現状のテストがTravis-CI(終わっているorgの方)だったりでちゃんと動いてるのかよく分からなすぎて、実際テストの一部をpendingしてたりしたのでGitHub Actionsに移行してたら、「メンテナ興味ない?やらない?」と言われたのでメンテナをやることになった。前回「無責任かもしれませんがPRを出します」とか書いてたら全ての責任を負うことになってしまった。いやMITライセンスなので無責任無保証ではあるのですが。
QuasiQuoteで自然キー指定するタイプの定義を書いてTHを展開してもらえば新しい構造がわかるのでは
しばらく置いてたらなんで思いつかなかったのかも分からない良いアイデアが浮かんできた。
ざっと見ると普通にpersistentでもPrimary
で自然キーを指定して人工キーを作らない方法はあるらしい。記法のリファレンスは3ステップ飛ばされたけど、今はGitHub WikiでもなくHaddockに全部書きだしてるんですね。
Database.Persist.Quasi
新しいpersistentでPrimary
を設定してもらった。古いLTSの方で行ったら古い形式が出てきた。
git init
しないとHLSが動かないから展開できないのを忘れていた。
このTemplate Haskellを展開したら少しは分かってきそうですね。
展開前。
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE DataKinds #-}
module Lib
( someFunc
) where
import Database.Persist.Quasi
import Database.Persist.TH
someFunc :: IO ()
someFunc = putStrLn "someFunc"
share [mkPersist sqlSettings, mkMigrate "migrateAll"]
[persistLowerCase|
PersistentSession
key String
authId String Maybe
Primary key
|]
展開後。
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE DataKinds #-}
module Lib
( someFunc
) where
import Database.Persist.Quasi
import Database.Persist.TH
someFunc :: IO ()
someFunc = putStrLn "someFunc"
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [(((UnboundEntityDef [])
(NaturalKey ((UnboundCompositeDef [FieldNameHS (pack "key")]) [])))
(((((((((((EntityDef (EntityNameHS (pack "PersistentSession")))
(EntityNameDB (pack "persistent_session")))
(EntityIdField
(((((((((((FieldDef (FieldNameHS (pack "Id")))
(FieldNameDB (pack "id")))
((FTTypeCon Nothing) (pack "PersistentSessionId")))
(SqlOther (pack "Primary Key")))
[])
True)
NoReference)
((FieldCascade Nothing) Nothing))
Nothing)
Nothing)
True)))
[])
[])
[])
[])
[])
(fromList []))
False)
Nothing))
[(((((((UnboundFieldDef (FieldNameHS (pack "key")))
(FieldNameDB (pack "key")))
[])
True)
((FTTypeCon Nothing) (pack "String")))
((FieldCascade Nothing) Nothing))
Nothing)
Nothing,
(((((((UnboundFieldDef (FieldNameHS (pack "authId")))
(FieldNameDB (pack "auth_id")))
[FieldAttrMaybe])
True)
((FTTypeCon Nothing) (pack "String")))
((FieldCascade Nothing) Nothing))
Nothing)
Nothing]]
これLTS-19なので2.14.0.1では無いが大丈夫か?
最新版でも、
ReferenceDef
のCompositeRef
のバリエーションが消されただけなのでこれは気にしなくても大丈夫。
直接UnboundEntityDef
に書き込むのも手の一つだと思いますが、綺麗な方式ではない。
unbindEntityDef
の内部を見るとNatural
かどうかを判断して切り替えているのでヒントを与えてやればうまく生成されるはず。
https://github.com/yesodweb/persistent/blob/c3f057757c8406026b2134b0db3d1ec4a668c874/persistent/Database/Persist/Quasi/Internal.hs#L668
なんかテキスト処理とか行ってるみたいで大変面倒そう。中間形式を吐き出してもらえばわかりやすい? そのような関数は見つかりませんでした。
EntityConstraintDefs
とかいうのがあるらしい。いやこれは作業を行うための中間データだからこちらから参照することはない。
よく分からない。
EntityIdNaturalKey
を生成する方法探しに戻ろう。
https://github.com/yesodweb/persistent/blob/c3f057757c8406026b2134b0db3d1ec4a668c874/persistent/Database/Persist/Types/Base.hs#L188 は既に生成されてるやつから取り出すだけですね。自前でガチャガチャ書いて良いのかなあこれ。
テキトーにガチャガチャ書いたらテスト通ってしまった。警告残ってるけど本当に良いのかこれは。
後は警告消すために、
PersistentSessionId
を消していくわけだけれど、そうなると、
persistIdField = PersistentSessionId
の書き換えが分からない。素直に、
persistIdField = PersistentSessionKey
とかすると型をKey
で包めと怒られるし、
persistIdField = Key PersistentSessionKey
とするとそんなコンストラクタは無いと怒られる。よく考えてみるとKey
は型名でコンストラクタでは無いのだからそれはそうだ。しかしPersistentSessionKey'
の方は生成が難しい。
しばらく色々と試して考えたけど、これも最終的にはTHの展開で定義されるのだからサンプルコードを展開していけば出現するのでは。
メチャクチャなimportの連打を喰らえ。余計な要素は消しました。
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE DataKinds #-}
module Lib
( someFunc
) where
import Data.Map
import Data.Text
import Database.Persist.EntityDef.Internal
import Database.Persist.FieldDef.Internal
import Database.Persist.Names
import Database.Persist.Quasi
import Database.Persist.Quasi.Internal
import Database.Persist.TH
import Database.Persist.Types
someFunc :: IO ()
someFunc = putStrLn "someFunc"
instance PersistField PersistentSession where
toPersistValue = entityToPersistValueHelper
fromPersistValue = entityFromPersistValueHelper ["key"]
instance PersistFieldSql PersistentSession where
sqlType _ = SqlString
data PersistentSession
= PersistentSession {persistentSessionKey :: !String}
type PersistentSessionId = Key PersistentSession
instance PersistEntity PersistentSession where
type PersistEntityBackend PersistentSession = SqlBackend
data Unique PersistentSession
newtype Key PersistentSession
= PersistentSessionKey' {unPersistentSessionKey :: String}
deriving stock Show
deriving stock Read
deriving newtype Eq
deriving newtype Ord
deriving newtype PathPiece
deriving newtype ToHttpApiData
deriving newtype FromHttpApiData
deriving newtype PersistField
deriving newtype PersistFieldSql
deriving newtype ToJSON
deriving newtype FromJSON
data EntityField PersistentSession typ
= (typ ~ PersistentSessionId) => PersistentSessionId |
(typ ~ String) => PersistentSessionKey
keyToValues record_atUg
= [toPersistValue (unPersistentSessionKey record_atUg)]
keyFromValues [x1_atUi]
= PersistentSessionKey'
<$>
(mapLeft ((fieldError (pack "persistent_session")) (pack "key"))
. fromPersistValue)
x1_atUi
keyFromValues x_atUh
= (Left
$ (mappend (pack "PersistentSession: keyFromValues failed on: "))
(pack $ show x_atUh))
keyFromRecordM
= Just
(\ record_atUo
-> PersistentSessionKey' (persistentSessionKey record_atUo))
entityDef _
= ((((((((((EntityDef (EntityNameHS (pack "PersistentSession")))
(EntityNameDB (pack "persistent_session")))
(EntityIdField
(((((((((((FieldDef (FieldNameHS (pack "Id")))
(FieldNameDB (pack "id")))
((FTTypeCon Nothing) (pack "PersistentSessionId")))
(SqlOther (pack "Primary Key")))
[])
True)
NoReference)
((FieldCascade Nothing) Nothing))
Nothing)
Nothing)
True)))
[])
[])
[])
[])
[])
(fromList []))
False)
Nothing
{entityFields = [FieldDef
{fieldHaskell = FieldNameHS (pack "key"),
fieldDB = FieldNameDB (pack "key"),
fieldType = (FTTypeCon Nothing) (pack "String"),
fieldSqlType = sqlType (Proxy :: Proxy String), fieldAttrs = [],
fieldStrict = True, fieldReference = NoReference,
fieldCascade = (FieldCascade Nothing) Nothing,
fieldComments = Nothing, fieldGenerated = Nothing,
fieldIsImplicitIdColumn = False}],
entityId = EntityIdNaturalKey
CompositeDef
{compositeFields = fromList
[(lookupEntityField (Proxy :: Proxy PersistentSession))
(FieldNameHS (pack "key"))],
compositeAttrs = []},
entityForeigns = []}
toPersistFields (PersistentSession x_atUc)
= [SomePersistField x_atUc]
fromPersistValues [x1_atUe]
= PersistentSession
<$>
(mapLeft ((fieldError (pack "persistent_session")) (pack "key"))
. fromPersistValue)
x1_atUe
fromPersistValues x_atUd
= (Left
$ (mappend
(pack "PersistentSession: fromPersistValues failed on: "))
(pack $ show x_atUd))
persistUniqueToFieldNames _
= error "Degenerate case, should never happen"
persistUniqueToValues _
= error "Degenerate case, should never happen"
persistUniqueKeys (PersistentSession _key_atUf) = []
persistFieldDef PersistentSessionId
= stripIdFieldImpl
(EntityIdNaturalKey
CompositeDef
{compositeFields = fromList
[(lookupEntityField (Proxy :: Proxy PersistentSession))
(FieldNameHS (pack "key"))],
compositeAttrs = []})
persistFieldDef PersistentSessionKey
= (lookupEntityField (Proxy :: Proxy PersistentSession))
(FieldNameHS (pack "key"))
persistIdField = PersistentSessionId
fieldLens PersistentSessionId
= (lensPTH entityKey)
(\ (Entity _ value_atUj) key_atUk -> (Entity key_atUk) value_atUj)
fieldLens PersistentSessionKey
= (lensPTH
((\ PersistentSession {persistentSessionKey = x} -> x)
. entityVal))
(\ (Entity key_atUl value_atUm) x_atUn
-> (Entity key_atUl)
(case value_atUm of {
PersistentSession {}
-> PersistentSession {persistentSessionKey = x_atUn} }))
instance TypeError (NoUniqueKeysError PersistentSession) =>
OnlyOneUniqueKey PersistentSession where
onlyUniqueP _ = error "impossible"
instance TypeError (MultipleUniqueKeysError PersistentSession) =>
AtLeastOneUniqueKey PersistentSession where
requireUniquesP _ = error "impossible"
instance SymbolToField "key" PersistentSession String where
symbolToField = PersistentSessionKey
entityDefListFormigrateAll :: [EntityDef]
entityDefListFormigrateAll
= [entityDef (Proxy :: Proxy PersistentSession)]
migrateAll :: Migration
migrateAll = migrateModels entityDefListFormigrateAll
なんか括弧の対応が合わない… HLSのTH展開能力のエミュレート不足なんですかね。
あれpersistIdField
定義されなくない?
と最初は思ったけど内部のQuasiQuoteを先に展開すると無くなってしまい、トップレベルから展開すれば出現するらしい。どういうことかはよく分かりません。
でもPersistentSessionId
を復活させたらpersistFieldDef
の定義が必要になってカラムが増えるのではと思ったけどstripIdFieldImpl
を使えば良いらしい。
https://github.com/yesodweb/persistent/blob/c3f057757c8406026b2134b0db3d1ec4a668c874/persistent/Database/Persist/TH.hs#L2018
非公開関数だから使えないらしい。
THの内部では使えるということですね。まあ内部的には自然キーを探して最初のをセット、
(これ複合キーが自然キーだったらどうするんだ?)
無かったら適当なダミーを返す。
@persistent@ used to assume that an Id was always a single field.
This method preserves as much backwards compatibility as possible.
と書かれていますね。互換性のためかあ、なので実装埋めて無くても一応動いたんですね。
persistIdField
は将来的に非推奨になるのだろうか。
今回は自然キーが単一のキーなのでそれを使いましょう。
非公開関数を読み解く限り要するにこうすれば良い。
persistFieldDef PersistentSessionId = P.persistFieldDef PersistentSessionKey
はずだった。何故か型が合わない。型引数がそれぞれ別のものと見做されているのか呼び出せなくなってるんですね。
ScopedTypeVariables
は関係ないらしい。
まあカンを働かせて別関数にそれぞれの実装を逃したら通りました。型族周り難しすぎてよく分からない。
とりあえずコンポーネントのテストは通るものが実装できた
とりあえずこれで警告も消せたし実装できた?
とりあえず動く実装は出来ましたが、これなら公開インターフェイスのシグネチャを変えなくてもうまく行きそうです。
後これをどうにかしてもサンプルの方が通りませんね。参照先が違うとか?
サンプルは後に回すことにしましょう。
一回は諦めたシグネチャの維持を再度行ってみましょう。とは言ってもpersistent自体の変更で完全に残すのは難しいかもしれませんが。
シグネチャ維持は無理ですね!
これまでProxy
名前が何故かうまく渡ってましたが、それはうまく行かない。
完全に維持するのは諦めることにして、上級ユーザ向けに名前指定出来るAPIを追加しておいて、これまでサンプル通り使ってきたユーザはちょっと書き換えるだけで済むような感じに変えましょう。
そのまま雑に実装してしまうと型シノニムが呼び出し側で閲覧できないから関数一つ呼び出すだけで実装できないのか。普通の関数呼び出しじゃなくてTemplate Haskellだからこその問題ですね。
ハックをやっていくことで解決できるかもしれませんが…
まあこれぐらいの手間は許容範囲として諦めて貰おうと思います。
type
シノニムはコピペしてもらいます。
いや流石にコピペはしなくても良かった。
import
してもらえば問題ない。
exampleの修正
何故かビルドに失敗し続けているexampleを修正します。
参照先がHackageになっていてローカルを見ていないのでは説。
exampleを実際に見てコードに書いてるexampleに色々と無理があるなと気がついてきた。
entityDefs
は[EntityDef]
、しかしmkMigrate
が受け取るのは[UnboundEntityDef]
、これをどうするべきか。
In persistent-2.13.0.0, this was changed to ignore the input entity def list, and instead defer to mkEntityDefList to get the correct entities. This avoids problems where the QuasiQuoter is unable to know what the right reference types are. This sets mkPersist to be the "single source of truth" for entity definitions.
みたいに入力を無視するって書いているけれどどういうことだろう? 引数を無視するわけでは無いだろうし。一応実際に引数を空にして見たら普通にマイグレーションなしでテスト失敗します。
まあunbindEntityDef
で変換すればそりゃ動くんでしょうけど、本当にそれをやって良いのか確信が持てない。何か間違っているのではという気がして仕方がない。
unbindEntityDef
がInternalというのも気になる所。どうせInternalならembedEntityDefs
を使ってしまうか。
まあ当然qualified
でimport
すると名前見つからないエラーになりますよね。コピペしてもらうか少し悩みましたが、修飾付けないでimportしてもらうことにします。
ううむ、文字列でやり取りしてるとこういうところに弱いですね。
なんかあっさりとビルドに成功してしまったな。この前のexampleだけ通らないという悩みは一体何だったんだ。
connEscapeName
の処理前にやらなかったっけ。あーexampleプロジェクト作って試して問題ないことを確認しただけか。
trashかsnapperにデータ残ってるかな。うーん見つからないな。どこで処理方法を知ったのだっけ…
まあ良いか、もう一度やろうか…
Git Stashに残ってた。
connEscapeRawName
で代用。それにしても、これもInternalなので後で公開してくれって送る必要があるなあ。
一応exampleも通るようになったがテストを復活させると通らない
exampleの今残ってたテストは実行して成功するように出来たが、 HomeSpecのテストを復活させて実行するとSQLiteがエラーを返してくるので、本当に問題が起きているのかがよく分からない。
Handler.Home
30/May/2022:17:59:25 +0900 [Error] Error closing database connection in pool: SQLite3 returned ErrorBusy while attempting to perform close: unable to close due to unfinalized statements or unfinished backups @(persistent-2.13.3.5-HvMqaF4atKUIVOnaPK1YB9:Database.Persist.Sql.Run ./Database/Persist/Sql/Run.hs:248:16)
loads the index and checks it looks right FAILED [1]
とりあえず最新版のSQLiteつきのYesodの初期プロジェクトはどうなっているだろうか。あんまりexample信用できないですが…
stack new sqlite-project yesodweb/sqlite
初期exampleだとテスト成功するじゃん。
差分を眺めていく必要がありそうですね。データベーススキーマに関しての一部の事情は異なりますが、ほとんど不要な差分では。初期に戻してみましょう。
うーん一応ビルドと起動は通るんだけど、あまりにもかけ離れている対象をサンプルに取っている気がする…
これのテストは諦めましょう。
Internalのクリーンアップ
今回実装するのにあたってInternalモジュールを使わざるを得ないところがたくさんありました。なるべく使うべきではないと分かってはいたのですが、使わないと実装が不可能だとしか思えなかったので、仕方がありません。
Internalを一つずつ明示的になるようにしましょう。
embedEntityDefs
もモジュールは分かれていませんがInternalですね。
必要になったInternal要素。
EntityDef
UnboundEntityDef
,unbindEntityDef
embedEntityDefs
connEscapeRawName
一つの構造体がそのまま入っていることを除けば、意外とそこまでInternalなものは使っていませんね。
これはpersistentにAPIの公開要望としてissueで送っておきましょう。
今日はここまで
今日はここまでにします。疲れた。次はプロダクトの開発環境でこのバージョンを使っても問題ないか試して、問題なければリリースしたりします。
後一応メンテナなのでメンテナ欄にメールアドレス載せておいた方が良いか。