{-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -- | -- Module: BDCS.DB -- Copyright: (c) 2016-2017 Red Hat, Inc. -- License: LGPL -- -- Maintainer: https://github.com/weldr -- Stability: alpha -- Portability: portable -- -- The metadata database schema and miscellaneous database helper functions module BDCS.DB where import Control.Monad(unless) import Control.Monad.Except(MonadError, throwError) import Control.Monad.IO.Class(MonadIO) import Control.Monad.Logger(NoLoggingT) import Control.Monad.Trans.Resource(MonadBaseControl, ResourceT) import qualified Data.Aeson as Aeson import Data.Aeson((.:), (.:?), (.=)) import Data.ByteString(ByteString) import Data.Int(Int64) import Data.Maybe(listToMaybe) import qualified Data.Text as T import Data.Time(UTCTime) import Database.Esqueleto(Esqueleto, Entity, Key, PersistEntity, PersistField, SqlBackend, SqlPersistT, ToBackendKey, Value, (==.), entityVal, insert, isNothing, val, unValue) import Database.Persist.Sql(rawSql, unSingle) import Database.Persist.Sqlite(runSqlite) import Database.Persist.TH import BDCS.KeyType import BDCS.ReqType {-# ANN module ("HLint: ignore Use module export list" :: String) #-} -- Both esqueleto and maybe export isNothing. I don't want to have to use a qualified import, so -- we'll just compare things directly to Nothing. {-# ANN module ("HLint: ignore Use isNothing" :: String) #-} -- | The database schema version as implemented by this module. This must match the -- PRAGMA user_version value in schema.sql, shipped elsewhere in the source. schemaVersion :: Int64 schemaVersion = 4 -- | Return the version number stored in the database. getDbVersion :: (MonadError String m, MonadIO m) => SqlPersistT m Int64 getDbVersion = rawSql "pragma user_version" [] >>= \case [] -> throwError "Database does not contain a user_version" v:_ -> return $ unSingle v -- | Verify that the version number stored in the database matches the schema version number -- implemented by this module. If there is a version mismatch, throw an error. checkDbVersion :: (MonadError String m, MonadIO m) => SqlPersistT m () checkDbVersion = do -- The change from version 3 to version 4 involves changing the content store, so there -- is no automatic upgrade path. userVersion <- getDbVersion unless (userVersion == schemaVersion) $ throwError $ "Database version " ++ show userVersion ++ " does not match expected version " ++ show schemaVersion ++ ", please re-import your data" -- | Like 'Database.Persist.Sqlite.runSqlite', but first checks that the database's schema version -- matches what is expected. This prevents running against incompatible database versions. checkAndRunSqlite :: (MonadError String m, MonadBaseControl IO m, MonadIO m) => T.Text -> SqlPersistT (NoLoggingT (ResourceT m)) a -> m a checkAndRunSqlite db action = runSqlite db (checkDbVersion >> action) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| Projects name T.Text summary T.Text description T.Text homepage T.Text Maybe upstream_vcs T.Text Maybe NameKey name deriving Eq Show Sources project_id ProjectsId license T.Text version T.Text source_ref T.Text deriving Eq Show Builds source_id SourcesId epoch Int default=0 release T.Text arch T.Text build_time UTCTime changelog ByteString build_config_ref T.Text build_env_ref T.Text deriving Eq Show BuildSignatures build_id BuildsId signature_type T.Text signature_data ByteString deriving Eq Show Files path T.Text file_user T.Text file_group T.Text mtime Int cs_object ByteString Maybe mode Int size Int target T.Text Maybe deriving Eq Show SourceFiles source_id SourcesId file_id FilesId deriving Eq Show BuildFiles build_id BuildsId file_id FilesId deriving Eq Show KeyVal key_value KeyType val_value T.Text Maybe ext_value T.Text Maybe deriving Eq Show ProjectKeyValues package_id ProjectsId key_val_id KeyValId deriving Eq Show SourceKeyValues source_id SourcesId key_val_id KeyValId deriving Eq Show BuildKeyValues build_id BuildsId key_val_id KeyValId deriving Eq Show FileKeyValues file_id FilesId key_val_id KeyValId deriving Eq Show Groups name T.Text group_type T.Text build_id BuildsId Maybe deriving Eq Show GroupFiles group_id GroupsId file_id FilesId deriving Eq Show GroupGroups parent_group_id GroupsId child_group_id GroupsId deriving Eq Show GroupKeyValues group_id GroupsId key_val_id KeyValId deriving Eq Show Requirements req_language ReqLanguage req_context ReqContext req_strength ReqStrength req_expr T.Text deriving Eq Show GroupRequirements group_id GroupsId req_id RequirementsId deriving Eq Show |] -- Implement JSON functions for Projects instance Aeson.ToJSON Projects where toJSON Projects{..} = Aeson.object [ "name" .= projectsName , "summary" .= projectsSummary , "description" .= projectsDescription , "homepage" .= projectsHomepage , "upstream_vcs" .= projectsUpstream_vcs ] instance Aeson.FromJSON Projects where parseJSON = Aeson.withObject "Projects" $ \o -> do projectsName <- o .: "name" projectsSummary <- o .: "summary" projectsDescription <- o .: "description" projectsHomepage <- o .:? "homepage" projectsUpstream_vcs <- o .:? "upstream_vcs" return Projects{..} instance Aeson.ToJSON KeyVal where toJSON kv = let jsonVal :: Maybe Aeson.Value -> Maybe Aeson.Value -> Aeson.Value jsonVal Nothing _ = Aeson.Bool True jsonVal (Just v) Nothing = v jsonVal (Just v) (Just e) = if v == e then v else e in jsonVal (Aeson.toJSON <$> keyValVal_value kv) (Aeson.toJSON <$> keyValExt_value kv) -- | Run an SQL query, returning the first 'Entity' as a Maybe. Use this when you -- want a single row out of the database. firstEntityResult :: Monad m => m [Entity a] -> m (Maybe a) firstEntityResult query = listToMaybe . map entityVal <$> query -- | Run an SQL query, returning the first key as a Maybe. Use this when you want -- a single index out of the database. firstKeyResult :: Monad m => m [Value a] -> m (Maybe a) firstKeyResult query = listToMaybe . map unValue <$> query -- | Run an SQL query, returning the first value from the result list. -- Use this when you want a single index out of the database and it is guaranteed not to be empty. firstListResult :: Monad m => m [Value a] -> m a firstListResult query = head . map unValue <$> query -- | Like 'maybe', but for keys. If the key is nothing, return the default value. Otherwise, -- run the function on the key and return that value. maybeKey :: MonadIO m => m b -- ^ Default value -> (t -> m b) -- ^ A function to run on the key -> m (Maybe t) -- ^ A 'Maybe' key -> m b maybeKey def fn value = value >>= \case Nothing -> def Just v -> fn v -- | Return a query fragment to match a Maybe value. -- If the value is Nothing, this is equivalent to (column is NULL) -- If the value is Just x, this is (value == column) -- Unlike the other Esqueleto operators, the right-hand value is not boxed in a Value, -- since we need to examine it in order to generate the correct SQL. -- -- e.g., with a table like: -- > create table example ( -- > id integer primary key, -- > value text ); -- you could use an esqueleto query like: -- > select $ from $ \example -> do -- > where_ $ maybeVal ==? (example ?. ExampleValue) infix 4 ==? (==?) :: (PersistField typ, Esqueleto query expr backend) => expr (Value (Maybe typ)) -> Maybe typ -> expr (Value Bool) (==?) column Nothing = isNothing column (==?) column value@(Just _) = column ==. val value -- | Attempt to find a record in some table of the database. If it exists, return its key. -- If it doesn't exist, perform some other action and return the key given by that action. orDo :: MonadIO m => m (Maybe b) -> m b -> m b orDo findFn doFn = findFn >>= maybe doFn return -- | Attempt to find a record in some table of the database. If it exists, return its key. -- If it doesn't exist, insert the given object and return its key. orInsert :: (MonadIO m, PersistEntity a, ToBackendKey SqlBackend a) => SqlPersistT m (Maybe (Key a)) -> a -> SqlPersistT m (Key a) orInsert findFn obj = findFn >>= maybe (insert obj) return