packages feed

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