imm-2.0.0.0: src/main/Database/SQLite.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.SQLite
( DatabaseSQLite(..)
, defaultDatabase
, mkHandle
) where
import Database.Handle hiding (deleteFeed, insertFeed, insertItem, purge)
import Data.Aeson
import Database.Beam
import Database.Beam.Backend.SQL (BeamSqlBackend, HasSqlValueSyntax (..))
import Database.Beam.Backend.SQL.BeamExtensions
-- import Database.Beam.Migrate
import Database.Beam.Sqlite (runBeamSqlite)
import Database.SQLite.Simple
import Imm.Feed
import Imm.Pretty
import System.Directory
-- * Schema
data FeedLocationT f = FeedLocationT
{ _locationID :: Columnar f Int32
, _locationValue :: Columnar f FeedLocation
} deriving(Generic, Beamable)
instance Table FeedLocationT where
data PrimaryKey FeedLocationT f = FeedLocationKey (Columnar f Int32)
deriving(Generic, Beamable)
primaryKey = FeedLocationKey . _locationID
data FeedT f = Feed
{ _feedKeyT :: PrimaryKey FeedLocationT f
, _feedDefinitionT :: Columnar f FeedDefinition
, _feedStatusT :: Columnar f FeedStatus
} deriving(Generic, Beamable)
instance Table FeedT where
data PrimaryKey FeedT f = FeedKey (PrimaryKey FeedLocationT f)
deriving(Generic, Beamable)
primaryKey = FeedKey . _feedKeyT
data FeedItemT f = FeedItemT
{ _itemKeyT :: Columnar f Int32
, _itemFeedKeyT :: PrimaryKey FeedLocationT f
, _itemDefinitionT :: Columnar f FeedItem
, _itemStatusT :: Columnar f FeedItemStatus
} deriving(Generic, Beamable)
instance Table FeedItemT where
data PrimaryKey FeedItemT f = FeedItemKey (Columnar f Int32)
deriving(Generic, Beamable)
primaryKey = FeedItemKey . _itemKeyT
data FeedDatabase f = FeedDatabase
{ _feedLocations :: f (TableEntity FeedLocationT)
, _feeds :: f (TableEntity FeedT)
, _feedItems :: f (TableEntity FeedItemT)
} deriving (Generic, Database be)
feedDatabase :: DatabaseSettings be FeedDatabase
feedDatabase = defaultDbSettings `withDbModification`
dbModification
{ _feedItems = modifyTableFields tableModification
{ _itemKeyT = fieldNamed "key"
, _itemFeedKeyT = FeedLocationKey $ fieldNamed "feed_key"
, _itemDefinitionT = fieldNamed "definition"
, _itemStatusT = fieldNamed "status"
}
, _feeds = modifyTableFields tableModification
{ _feedKeyT = FeedLocationKey $ fieldNamed "key"
, _feedDefinitionT = fieldNamed "definition"
, _feedStatusT = fieldNamed "status"
}
, _feedLocations = modifyTableFields tableModification
{ _locationID = fieldNamed "key"
, _locationValue = fieldNamed "value"
}
}
feedLocationsTable :: DatabaseEntity be FeedDatabase (TableEntity FeedLocationT)
feedLocationsTable = _feedLocations feedDatabase
feedsTable :: DatabaseEntity be FeedDatabase (TableEntity FeedT)
feedsTable = _feeds feedDatabase
feedItemsTable :: DatabaseEntity be FeedDatabase (TableEntity FeedItemT)
feedItemsTable = _feedItems feedDatabase
-- * Queries
fetchAllFeeds :: _ [FeedRecord Inserted]
fetchAllFeeds = fmap (map asFeedRecord) $ runSelectReturningList $ select $ do
feed <- feedsTable & all_
location <- feedLocationsTable & all_
guard_ $ _feedKeyT feed `references_` location
pure (location, feed)
asFeedRecord :: f ~ Identity => (FeedLocationT f, FeedT f) -> FeedRecord Inserted
asFeedRecord (locationT, feed) = FeedRecord
(fromIntegral $ _locationID locationT)
(_locationValue locationT)
(_feedDefinitionT feed)
(_feedStatusT feed)
fetchFeed :: UID -> _
fetchFeed uid = select (queryFeed uid) & runSelectReturningOne
>>= maybe (fail $ "Feed not found: " <> show uid) return
<&> asFeedRecord
queryFeed :: UID -> _
queryFeed uid = do
feed <- feedsTable & all_
location <- feedLocationsTable & all_
guard_ $ _feedKeyT feed `references_` location
guard_ $ _locationID location ==. val_ (fromIntegral uid)
pure (location, feed)
fetchItem :: UID -> _
fetchItem uid = feedItemsTable
& all_
& filter_ (\i -> primaryKey i ==. val_ (FeedItemKey $ fromIntegral uid))
& select
& runSelectReturningOne
>>= maybe (fail $ "Item not found: " <> show uid) return
<&> asFeedItemRecord
fetchAllItems :: _ [FeedItemRecord Inserted]
fetchAllItems = select (feedItemsTable & all_)
& runSelectReturningList
<&> map asFeedItemRecord
fetchItems :: UID -> _
fetchItems uid = select (queryItems uid) & runSelectReturningList <&> map asFeedItemRecord
queryItems :: UID -> _
queryItems uid = do
item <- feedItemsTable & all_
guard_ $ _itemFeedKeyT item ==. val_ (FeedLocationKey $ fromIntegral uid)
pure item
asFeedItemRecord :: FeedItemT Identity -> FeedItemRecord Inserted
asFeedItemRecord item = FeedItemRecord
(fromIntegral $ _itemKeyT item)
(fromIntegral feedKey)
(_itemDefinitionT item)
(_itemStatusT item)
where FeedLocationKey feedKey = _itemFeedKeyT item
deleteFeed :: UID -> _ ()
deleteFeed uid = do
-- runDelete $ delete feedItemsTable $ \item -> _itemFeedKeyT item ==. val_ (FeedLocationKey $ fromIntegral uid)
-- runDelete $ delete feedsTable $ \feed -> _feedKeyT feed ==. val_ (FeedLocationKey $ fromIntegral uid)
runDelete $ delete feedLocationsTable $ \location -> _locationID location ==. val_ (fromIntegral uid)
insertFeed :: FeedRecord NotInserted -> _ (FeedRecord Inserted)
insertFeed record = do
location <- insertFeedLocation $ _feedLocation record
let value = Feed (primaryKey location) (_feedDefinition record) (_feedStatus record)
feed <- insertValues [value]
& insert feedsTable
& runInsertReturningList
>>= headFail (displayException $ FeedsNotInserted [record])
return $ asFeedRecord (location, feed)
insertFeedLocation :: FeedLocation -> _
insertFeedLocation location = insertExpressions [FeedLocationT default_ (val_ location)]
& insert feedLocationsTable
& runInsertReturningList
>>= headFail ("Unable to insert feed location " <> show location)
insertItem :: FeedItemRecord NotInserted -> _
insertItem record = do
inserted <- insertExpressions [FeedItemT default_ (val_ $ FeedLocationKey $ fromIntegral $ _itemFeedKey record) (val_ $ _itemDefinition record) (val_ $ _itemStatus record)]
& insert feedItemsTable
& runInsertReturningList
>>= headFail (displayException $ ItemsNotInserted [record])
return $ FeedItemRecord (fromIntegral $ _itemKeyT inserted) (_itemFeedKey record) (_itemDefinition record) (_itemStatus record)
purge = runDelete $ delete feedLocationsTable $ const $ val_ True
updateItemStatus :: FeedItemRecord Inserted -> _
updateItemStatus record = runUpdate $ update feedItemsTable
(\item -> _itemStatusT item <-. val_ (_itemStatus record))
(\item -> primaryKey item ==. val_ (FeedItemKey $ fromIntegral $ _itemKey record))
updateFeedDefinition :: FeedRecord Inserted -> _
updateFeedDefinition record = runUpdate $ update feedsTable
(\feed -> _feedDefinitionT feed <-. val_ (_feedDefinition record))
(\feed -> primaryKey feed ==. val_ (FeedKey $ FeedLocationKey $ fromIntegral $ _feedKey record))
updateFeedStatus :: FeedRecord Inserted -> _
updateFeedStatus record = runUpdate $ update feedsTable
(\feed -> _feedStatusT feed <-. val_ (_feedStatus record))
(\feed -> primaryKey feed ==. val_ (FeedKey $ FeedLocationKey $ fromIntegral $ _feedKey record))
-- * Handle
newtype DatabaseSQLite = DatabaseSQLite
{ _sqliteFile :: FilePath
}
instance Pretty DatabaseSQLite where
pretty db = "SQLite database: " <+> pretty (_sqliteFile db)
-- | Default database is stored in @$XDG_CONFIG_HOME\/imm\/database.sqlite@
defaultDatabase :: IO DatabaseSQLite
defaultDatabase = DatabaseSQLite <$> getXdgDirectory XdgConfig "imm/database.sqlite"
createTables :: Connection -> IO ()
createTables conn = do
execute_ conn "CREATE TABLE IF NOT EXISTS locations (key INTEGER NOT NULL PRIMARY KEY, value BLOB NOT NULL UNIQUE);"
execute_ conn "CREATE TABLE IF NOT EXISTS feeds ( \
\ key INTEGER NOT NULL PRIMARY KEY, definition BLOB NOT NULL, status BLOB NOT NULL, \
\ CONSTRAINT fk_feeds FOREIGN KEY (key) REFERENCES locations(key) ON DELETE CASCADE \
\ );"
execute_ conn "CREATE TABLE IF NOT EXISTS items ( \
\ key INTEGER NOT NULL PRIMARY KEY, feed_key INTEGER NOT NULL, definition BLOB NOT NULL, status BLOB NOT NULL, \
\ CONSTRAINT fk_feeds FOREIGN KEY (feed_key) REFERENCES locations(key) ON DELETE CASCADE \
\ );"
mkHandle :: DatabaseSQLite -> IO (Handle IO)
mkHandle database = do
conn <- open $ _sqliteFile database
createTables conn
execute_ conn "PRAGMA foreign_keys = ON;"
return $ Handle
{ _describeDatabase = return $ pretty database
, _fetchAllFeeds = runBeamSqlite conn fetchAllFeeds
, _fetchFeed = runBeamSqlite conn . fetchFeed
, _fetchItems = runBeamSqlite conn . fetchItems
, _fetchAllItems = runBeamSqlite conn fetchAllItems
, _fetchItem = runBeamSqlite conn . fetchItem
, _updateFeedDefinition = runBeamSqlite conn . updateFeedDefinition
, _updateFeedStatus = runBeamSqlite conn . updateFeedStatus
, _updateItemStatus = runBeamSqlite conn . updateItemStatus
, _insertFeed = runBeamSqlite conn . insertFeed
, _insertItem = runBeamSqlite conn . insertItem
, _deleteFeed = runBeamSqlite conn . deleteFeed
, _purge = runBeamSqlite conn purge
, _commit = return ()
}
instance (BeamSqlBackend be, FromBackendRow be LByteString) => FromBackendRow be FeedLocation where
fromBackendRow = fromBackendRow <&> decode >>= maybe empty pure
instance HasSqlValueSyntax be ByteString => HasSqlValueSyntax be FeedLocation where
sqlValueSyntax = sqlValueSyntax . toStrict . encode
instance (BeamSqlBackend be, FromBackendRow be LByteString) => FromBackendRow be FeedItem where
fromBackendRow = fromBackendRow <&> decode >>= maybe empty pure
instance HasSqlValueSyntax be ByteString => HasSqlValueSyntax be FeedItem where
sqlValueSyntax = sqlValueSyntax . toStrict . encode
instance (BeamSqlBackend be, FromBackendRow be LByteString) => FromBackendRow be FeedDefinition where
fromBackendRow = fromBackendRow <&> decode >>= maybe empty pure
instance HasSqlValueSyntax be ByteString => HasSqlValueSyntax be FeedDefinition where
sqlValueSyntax = sqlValueSyntax . toStrict . encode
instance (BeamSqlBackend be, FromBackendRow be LByteString) => FromBackendRow be FeedStatus where
fromBackendRow = fromBackendRow <&> decode >>= maybe empty pure
instance HasSqlValueSyntax be ByteString => HasSqlValueSyntax be FeedStatus where
sqlValueSyntax = sqlValueSyntax . toStrict . encode
instance (BeamSqlBackend be, FromBackendRow be LByteString) => FromBackendRow be FeedItemStatus where
fromBackendRow = fromBackendRow <&> decode >>= maybe empty pure
instance HasSqlValueSyntax be ByteString => HasSqlValueSyntax be FeedItemStatus where
sqlValueSyntax = sqlValueSyntax . toStrict . encode