imm-2.1.2.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 TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UnicodeSyntax #-}
module Database.SQLite (
DatabaseSQLite (..),
defaultDatabase,
mkHandle,
)
where
import Data.Aeson (decode, encode)
import Database.Beam
import Database.Beam.Backend.SQL (BeamSqlBackend, HasSqlValueSyntax (..))
import Database.Beam.Backend.SQL.BeamExtensions
-- import Database.Beam.Migrate
import Database.Beam.Sqlite (SqliteM, runBeamSqlite)
import Database.Handle hiding (deleteFeed, insertFeed, insertItem, purge)
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 ∷ SqliteM [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 → SqliteM (FeedRecord Inserted)
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 → SqliteM (FeedItemRecord Inserted)
fetchItem uid =
feedItemsTable
& all_
& filter_ (\i → primaryKey i ==. val_ (FeedItemKey $ fromIntegral uid))
& select
& runSelectReturningOne
>>= maybe (fail $ "Item not found: " <> show uid) return
<&> asFeedItemRecord
fetchAllItems ∷ SqliteM [FeedItemRecord Inserted]
fetchAllItems =
select (feedItemsTable & all_)
& runSelectReturningList
<&> map asFeedItemRecord
fetchItems ∷ UID → SqliteM [FeedItemRecord Inserted]
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 → SqliteM ()
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 → SqliteM (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 → SqliteM (FeedLocationT Identity)
insertFeedLocation location =
insertExpressions [FeedLocationT default_ (val_ location)]
& insert feedLocationsTable
& runInsertReturningList
>>= headFail ("Unable to insert feed location " <> show location)
insertItem ∷ FeedItemRecord NotInserted → SqliteM (FeedItemRecord Inserted)
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 ∷ SqliteM ()
purge = runDelete $ delete feedLocationsTable $ \_ → val_ True
updateItemStatus ∷ FeedItemRecord Inserted → SqliteM ()
updateItemStatus record =
runUpdate $
update
feedItemsTable
(\item → _itemStatusT item <-. val_ (_itemStatus record))
(\item → primaryKey item ==. val_ (FeedItemKey $ fromIntegral $ _itemKey record))
updateFeedDefinition ∷ FeedRecord Inserted → SqliteM ()
updateFeedDefinition record =
runUpdate $
update
feedsTable
(\feed → _feedDefinitionT feed <-. val_ (_feedDefinition record))
(\feed → primaryKey feed ==. val_ (FeedKey $ FeedLocationKey $ fromIntegral $ _feedKey record))
updateFeedStatus ∷ FeedRecord Inserted → SqliteM ()
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