-
Notifications
You must be signed in to change notification settings - Fork 215
Explore relational database state storage #1465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
seed the database
Depends on TemplateHaskell
Not really needed since they run in a different directory
So there is no need to override the convention
To have a more controlled migration
To use UserId directly we need to change it to a non machine-dependent size for Beam. MemSize and Pretty are not defined on Int32. But other places start to complain after that
Database.SQLite.Simple.execute_ is not able to run multiple statements at once
Enums are saved using Show and pared with Read They currently abort the request if the data is malformed
danbornside
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not any more familiar with hackage-server codebase than brian, only that we paired on this a bit and i'm more familiar with beam. Even with requested changes i'm not confident enough to approve as-is without some input from mantainers
| , resourceDelete = [ ("", handlerDeleteAdminInfo) ] | ||
| } | ||
|
|
||
| -- handlerWithConnection :: (Database.Connection -> DynamicPath -> ServerPartE Response) -> DynamicPath -> ServerPartE Response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commented out code?
| } madetails | ||
| let cdetails = change adetails | ||
|
|
||
| accountDetailsUpsert AccountDetailsRow { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a bit ormic. this should really use the fine grained syntax to affect only the relevant fields without needing to read it out of the db first.
| accountDetailsFindByUserId :: UserId -> Database.Transaction (Maybe AccountDetailsRow) | ||
| accountDetailsFindByUserId uid = | ||
| Database.runSelectReturningOne $ | ||
| select $ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider using lookup_
| admins <- queryState adminsState GetAdminList | ||
|
|
||
| withTransaction $ do | ||
| forM_ (Users.enumerateAllUsers users) $ \(uid, uinfo) -> do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a very inefficient way to do a large insert, one row at a time. insertValues already takes a list. push the mapping from acidstate to beam under the runInsert
| deriving (Eq, Ord, Read, Show, MemSize) | ||
|
|
||
| instance FromBackendRow Sqlite AuthToken where | ||
| fromBackendRow = AuthToken . BSS.toShort <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to be encoded? the corresponding column in the schema is TEXT; and i rather think that's not a binary-safe column type. sqlite supports BLOB and postgress has BINARY, which probably could work, although for a small value like this, they're pretty inconvenient compared to encoding to hex or base64 or something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned elsewhere, you probably want to implement FromField rather than FromBackendRow instead
| @@ -0,0 +1,47 @@ | |||
| {-# LANGUAGE DeriveAnyClass #-} | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DeriveAnyClass is a bit dodgy
|
|
||
| type AccountDetailsRow = AccountDetailsT Identity | ||
|
|
||
| deriving instance Show AccountDetailsRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instances for type aliases are a bit dodgy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually the idiomatic way to provide instances for Beam table types, for what it's worth
| sqlValueSyntax = autoSqlValueSyntax | ||
|
|
||
| instance FromBackendRow Sqlite AccountDetailsKind where | ||
| fromBackendRow = read . unpack <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
read will throw an exception if there's a parsing problem. either fail pure . Text.Read.readEither would be better.
supplying a meaningfull error message instead of Prelude.read no parse would be better still
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not have to define explicit instances for FromBackendRow. The class BeamBackend ultimately states that reading columns from Sqlite is done via Database.Sqlite.Simple.FromField.FromField.
Therefore, you should define an instance of Database.Sqlite.Simple.FromField.FromField AccountDetailsKind instead
LaurentRDC
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I help maintain Beam. I provided some comments on idiomatic Beam usage. Feel free to summon me in this PR and others if this helps
| sqlValueSyntax = autoSqlValueSyntax | ||
|
|
||
| instance FromBackendRow Sqlite AccountDetailsKind where | ||
| fromBackendRow = read . unpack <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not have to define explicit instances for FromBackendRow. The class BeamBackend ultimately states that reading columns from Sqlite is done via Database.Sqlite.Simple.FromField.FromField.
Therefore, you should define an instance of Database.Sqlite.Simple.FromField.FromField AccountDetailsKind instead
| runSelectReturningOne :: forall a. (FromBackendRow Sqlite a) => SqlSelect Sqlite a -> Transaction (Maybe a) | ||
| runSelectReturningOne q = | ||
| Transaction $ ReaderT $ \(SqlLiteConnection conn) -> runBeamSqlite conn $ Database.Beam.runSelectReturningOne q | ||
|
|
||
| runInsert :: forall (table :: (Type -> Type) -> Type). SqlInsert Sqlite table -> Transaction () | ||
| runInsert q = | ||
| Transaction $ ReaderT $ \(SqlLiteConnection conn) -> runBeamSqlite conn $ Database.Beam.runInsert q | ||
|
|
||
| newtype Transaction a = Transaction {unTransaction :: ReaderT Connection IO a} | ||
| deriving (Functor, Applicative, Monad) | ||
|
|
||
| runTransaction :: Transaction a -> Connection -> IO a | ||
| runTransaction t = runReaderT (unTransaction t) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that the plan is to support both Sqlite and Postgres. I think this can be done by abstracting over the backend instead of using Sqlite specifically here. Then, either via ExistentialQuantification or some typeclass, the only database-specific bit is the runBeamPostgres vs runBeamSqlite functions
| forM_ (Users.enumerateAllUsers users) $ \(uid, uinfo) -> do | ||
| let (status, authInfo) = | ||
| case userStatus uinfo of | ||
| AccountEnabled a -> (Enabled, Just a) | ||
| AccountDisabled ma -> (Disabled, ma) | ||
| AccountDeleted -> (Deleted, Nothing) | ||
|
|
||
| Database.runInsert $ | ||
| insert | ||
| (_tblUsers Database.hackageDb) | ||
| (insertValues [UsersRow { | ||
| _uId = uid, | ||
| _uUsername = userName uinfo, | ||
| _uStatus = status, | ||
| _uAuthInfo = authInfo, | ||
| _uAdmin = Group.member uid admins | ||
| }]) | ||
|
|
||
| forM_ (Map.toList (userTokens uinfo)) $ \(token, desc) -> do | ||
| Database.runInsert $ | ||
| insert | ||
| (_tblUserTokens Database.hackageDb) | ||
| (insertExpressions [UserTokensRow { | ||
| _utId = default_, | ||
| _utUserId = val_ uid, | ||
| _utToken = val_ token, | ||
| _utDescription = val_ desc | ||
| }]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be more performant to execute runInsert fewer times, but each time with more than a single value.
| deriving (Eq, Ord, Read, Show, MemSize) | ||
|
|
||
| instance FromBackendRow Sqlite AuthToken where | ||
| fromBackendRow = AuthToken . BSS.toShort <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned elsewhere, you probably want to implement FromField rather than FromBackendRow instead
| instance FromBackendRow Sqlite UsersStatus where | ||
| fromBackendRow = read . unpack <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should define an instance of FromField rather than FromBackendRow
| instance FromBackendRow Sqlite UserId where | ||
| fromBackendRow = UserId . fromIntegral @Int32 <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should derive an instance of FromField rather than FromBackendRow
| instance FromBackendRow Sqlite UserName where | ||
| fromBackendRow = UserName . unpack <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, FromField instead of FromBackendRow
| instance FromBackendRow Sqlite UserAuth where | ||
| fromBackendRow = UserAuth . PasswdHash . unpack <$> fromBackendRow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FromField instead of FromBackendRow
This is the exploration done during AmeriHac to migrate the acid store to a relational database. I will leave it as draft since it's an exploration probably that will trigger discussions rather than a patch. I also hope to continue soon to keep migrating more state to the database to keep validating the approach.
The goals were
Things not covered
Other general doubts
To keep the exploration shorter we assumed that db backups would probably be done using the usual db backup tools and not via CSV files. It would be simpler to aim for full code migration and no choice but to migrate to the database. If these assumptions are not right then we will need some abstractions to keep reading and writing to the store (acid or db) which increases the effort.
This is the first time using Beam for me, so there might be better ways of doing things.
Thanks to @danbornside for all the pairing on this