66
77module DataSource.DB
88 ( DBConnPool
9+ , DBException (.. )
10+ , DBLayerException (.. )
11+ , createSchema
12+ , deleteAllData
13+ , insertCommentAttachments
14+ , insertTicketComments
15+ , insertTicketInfo
916 , withDatabase
1017 , withProdDatabase
1118 -- * Empty layer
@@ -22,13 +29,16 @@ module DataSource.DB
2229
2330import Universum
2431
32+ import Control.Exception.Safe (Handler (.. ), catches )
2533import Control.Monad.Trans.Control (MonadBaseControl )
2634
2735import Data.Pool (Pool , createPool , withResource )
2836import Data.Text (split )
37+ import Prelude as P (Show (.. ))
2938
30- import Database.SQLite.Simple (FromRow (.. ), NamedParam (.. ), SQLData (.. ), close ,
31- executeNamed , execute_ , field , open , queryNamed , query_ )
39+ import Database.SQLite.Simple (FormatError (.. ), FromRow (.. ), NamedParam (.. ),
40+ Query (.. ), SQLData (.. ), close , executeNamed , execute_ ,
41+ field , open , queryNamed , query_ )
3242import Database.SQLite.Simple.FromField (FromField (.. ), ResultError (.. ), returnError )
3343import Database.SQLite.Simple.Internal (Connection , Field (.. ))
3444import Database.SQLite.Simple.Ok (Ok (.. ))
@@ -37,10 +47,10 @@ import Database.SQLite.Simple.ToField (ToField (..))
3747import DataSource.Http (basicDataLayer )
3848import DataSource.Types (Attachment (.. ), AttachmentContent (.. ), AttachmentId (.. ),
3949 Comment (.. ), CommentBody (.. ), CommentId (.. ), Config ,
40- DBLayer (.. ), TicketField (.. ), TicketFieldId (.. ),
41- TicketFieldValue (.. ), TicketId (.. ), TicketInfo (.. ),
42- TicketStatus (.. ), TicketTags (.. ), TicketURL ( .. ), UserId (.. ),
43- DataLayer (.. ))
50+ DBLayer (.. ), DataLayer (.. ), TicketField (.. ),
51+ TicketFieldId (.. ), TicketFieldValue (.. ), TicketId (.. ),
52+ TicketInfo (.. ), TicketStatus (.. ), TicketTags (.. ),
53+ TicketURL ( .. ), UserId (.. ))
4454
4555------------------------------------------------------------
4656-- Single connection, simple
@@ -147,7 +157,10 @@ connDBLayer = DBLayer
147157
148158-- | The connection pooled Zendesk layer. Used for database querying.
149159-- We need to sync occasionaly.
150- connPoolDataLayer :: forall m . (MonadBaseControl IO m , MonadIO m , MonadReader Config m ) => DBConnPool -> DataLayer m
160+ connPoolDataLayer
161+ :: forall m . (MonadBaseControl IO m , MonadIO m , MonadReader Config m )
162+ => DBConnPool
163+ -> DataLayer m
151164connPoolDataLayer connPool = DataLayer
152165 { zlGetTicketInfo = \ tId -> withConnPool connPool $ \ conn -> getTicketInfoByTicketId conn tId
153166 , zlListDeletedTickets = zlListDeletedTickets basicDataLayer
@@ -163,7 +176,10 @@ connPoolDataLayer connPool = DataLayer
163176
164177
165178-- | The connection pooled database layer. Used for database modification.
166- connPoolDBLayer :: forall m . (MonadBaseControl IO m , MonadIO m , MonadReader Config m ) => DBConnPool -> DBLayer m
179+ connPoolDBLayer
180+ :: forall m . ( MonadBaseControl IO m , MonadIO m , MonadReader Config m , MonadCatch m )
181+ => DBConnPool
182+ -> DBLayer m
167183connPoolDBLayer connPool = DBLayer
168184 { dlInsertTicketInfo = \ tIn -> withConnPool connPool $ \ conn -> insertTicketInfo conn tIn
169185 , dlInsertTicketComments = \ tId comm -> withConnPool connPool $ \ conn -> insertTicketComments conn tId comm
@@ -252,6 +268,9 @@ instance FromRow Attachment where
252268instance FromRow AttachmentContent where
253269 fromRow = AttachmentContent <$> field
254270
271+ instance FromRow TicketId where
272+ fromRow = TicketId <$> field
273+
255274instance ToField TicketId where
256275 toField (TicketId tId) = SQLInteger . fromIntegral $ tId
257276
@@ -360,47 +379,58 @@ getAttachmentContent conn Attachment{..} =
360379-- TODO(ks): withTransaction
361380
362381createSchema :: forall m . (MonadIO m ) => Connection -> m ()
363- createSchema conn = liftIO $ execute_ conn
364- " CREATE TABLE `ticket_info` ( \
365- \ `tiId` INTEGER, \
366- \ `tiRequesterId` INTEGER NOT NULL, \
367- \ `tiAssigneeId` INTEGER, \
368- \ `tiUrl` TEXT NOT NULL, \
369- \ `tiTags` TEXT NOT NULL, \
370- \ `tiStatus` TEXT NOT NULL, \
371- \ PRIMARY KEY(tiId) \
372- \ ) WITHOUT ROWID; \
373- \ \
374- \ CREATE TABLE `ticket_comment` ( \
375- \ `id` INTEGER, \
376- \ `ticket_id` INTEGER NOT NULL, \
377- \ `body` TEXT NOT NULL, \
378- \ `is_public` INTEGER NOT NULL, \
379- \ `author_id` INTEGER NOT NULL, \
380- \ PRIMARY KEY(id), \
381- \ FOREIGN KEY(`ticket_id`) REFERENCES ticket_info(tId) \
382- \ ) WITHOUT ROWID; \
383- \ \
384- \ CREATE TABLE `comment_attachment` ( \
385- \ `aId` INTEGER, \
386- \ `comment_id` INTEGER NOT NULL, \
387- \ `aURL` TEXT NOT NULL, \
388- \ `aContentType` TEXT NOT NULL, \
389- \ `aSize` INTEGER NOT NULL, \
390- \ PRIMARY KEY(aId), \
391- \ FOREIGN KEY(`comment_id`) REFERENCES ticket_comment ( ticket_id ) \
392- \ ) WITHOUT ROWID; \
393- \ \
394- \ CREATE TABLE `attachment_content` ( \
395- \ `attachment_id` INTEGER, \
396- \ `content` BLOB NOT NULL, \
397- \ PRIMARY KEY(attachment_id), \
398- \ FOREIGN KEY(`attachment_id`) REFERENCES comment_attachment(aId) \
399- \ )"
400-
401- insertTicketInfo :: forall m . (MonadIO m ) => Connection -> TicketInfo -> m ()
382+ createSchema conn = do
383+ createTicketInfoTable
384+ createTicketCommentTable
385+ createCommentAttachmentTable
386+ createAttachmentContentTable
387+ where
388+ createTicketInfoTable :: m ()
389+ createTicketInfoTable = liftIO $ execute_ conn
390+ " CREATE TABLE `ticket_info` ( \
391+ \ `tiId` INTEGER, \
392+ \ `tiRequesterId` INTEGER NOT NULL, \
393+ \ `tiAssigneeId` INTEGER, \
394+ \ `tiUrl` TEXT NOT NULL, \
395+ \ `tiTags` TEXT NOT NULL, \
396+ \ `tiStatus` TEXT NOT NULL, \
397+ \ PRIMARY KEY(tiId) \
398+ \ ) WITHOUT ROWID;"
399+ createTicketCommentTable :: m ()
400+ createTicketCommentTable = liftIO $ execute_ conn
401+ " CREATE TABLE `ticket_comment` ( \
402+ \ `id` INTEGER, \
403+ \ `ticket_id` INTEGER NOT NULL, \
404+ \ `body` TEXT NOT NULL, \
405+ \ `is_public` INTEGER NOT NULL, \
406+ \ `author_id` INTEGER NOT NULL, \
407+ \ PRIMARY KEY(id), \
408+ \ FOREIGN KEY(`ticket_id`) REFERENCES ticket_info(tiId) \
409+ \ ) WITHOUT ROWID;"
410+ createCommentAttachmentTable :: m ()
411+ createCommentAttachmentTable = liftIO $ execute_ conn
412+ " CREATE TABLE `comment_attachment` ( \
413+ \ `aId` INTEGER, \
414+ \ `comment_id` INTEGER NOT NULL, \
415+ \ `aURL` TEXT NOT NULL, \
416+ \ `aContentType` TEXT NOT NULL, \
417+ \ `aSize` INTEGER NOT NULL, \
418+ \ PRIMARY KEY(aId), \
419+ \ FOREIGN KEY(`comment_id`) REFERENCES ticket_comment ( ticket_id ) \
420+ \ ) WITHOUT ROWID;"
421+ createAttachmentContentTable :: m ()
422+ createAttachmentContentTable = liftIO $ execute_ conn
423+ " CREATE TABLE `attachment_content` ( \
424+ \ `attachment_id` INTEGER, \
425+ \ `content` BLOB NOT NULL, \
426+ \ PRIMARY KEY(attachment_id), \
427+ \ FOREIGN KEY(`attachment_id`) REFERENCES comment_attachment(aId) \
428+ \ )"
429+
430+ insertTicketInfo :: forall m . (MonadIO m , MonadCatch m ) => Connection -> TicketInfo -> m ()
402431insertTicketInfo conn TicketInfo {.. } =
403- liftIO $ executeNamed conn " INSERT INTO ticket_info (tiId, tiRequesterId, tiAssigneeId, tiUrl, tiTags, tiStatus) \
432+ liftIO $ executeNamedSafe (InsertTicketInfoFailed tiId)
433+ conn " INSERT INTO ticket_info (tiId, tiRequesterId, tiAssigneeId, tiUrl, tiTags, tiStatus) \
404434 \VALUES (:tiId, :tiRequesterId, :tiAssigneeId, :tiUrl, :tiTags, :tiStatus)"
405435 [ " :tiId" := tiId
406436 , " :tiRequesterId" := tiRequesterId
@@ -410,9 +440,15 @@ insertTicketInfo conn TicketInfo{..} =
410440 , " :tiStatus" := tiStatus
411441 ]
412442
413- insertTicketComments :: forall m . (MonadIO m ) => Connection -> TicketId -> Comment -> m ()
443+ insertTicketComments
444+ :: forall m . (MonadIO m , MonadCatch m )
445+ => Connection
446+ -> TicketId
447+ -> Comment
448+ -> m ()
414449insertTicketComments conn ticketId Comment {.. } =
415- liftIO $ executeNamed conn " INSERT INTO ticket_comment (id, ticket_id, body, is_public, author_id) \
450+ liftIO $ executeNamedSafe (InsertTicketCommentsFailed ticketId cId)
451+ conn " INSERT INTO ticket_comment (id, ticket_id, body, is_public, author_id) \
416452 \VALUES (:id, :ticket_id, :body, :is_public, :author_id)"
417453 [ " :id" := cId
418454 , " :ticket_id" := ticketId
@@ -421,9 +457,15 @@ insertTicketComments conn ticketId Comment{..} =
421457 , " :author_id" := cAuthor
422458 ]
423459
424- insertCommentAttachments :: forall m . (MonadIO m ) => Connection -> Comment -> Attachment -> m ()
460+ insertCommentAttachments
461+ :: forall m . (MonadIO m , MonadCatch m )
462+ => Connection
463+ -> Comment
464+ -> Attachment
465+ -> m ()
425466insertCommentAttachments conn Comment {.. } Attachment {.. } =
426- liftIO $ executeNamed conn " INSERT INTO comment_attachment (aId, comment_id, aURL, aContentType, aSize) \
467+ liftIO $ executeNamedSafe (InsertCommentAttachmentFailed cId aId)
468+ conn " INSERT INTO comment_attachment (aId, comment_id, aURL, aContentType, aSize) \
427469 \VALUES (:aId, :comment_id, :aURL, :aContentType, :aSize)"
428470 [ " :aId" := aId
429471 , " :comment_id" := cId
@@ -432,22 +474,97 @@ insertCommentAttachments conn Comment{..} Attachment{..} =
432474 , " :aSize" := aSize
433475 ]
434476
435- deleteCommentAttachments :: forall m . (MonadIO m ) => Connection -> m ()
477+ deleteCommentAttachments :: forall m . (MonadIO m , MonadCatch m ) => Connection -> m ()
436478deleteCommentAttachments conn =
437- liftIO $ execute_ conn " DELETE FROM comment_attachment"
479+ liftIO $ executeSafe_ DBDeleteFailed conn " DELETE FROM comment_attachment"
438480
439- deleteTicketComments :: forall m . (MonadIO m ) => Connection -> m ()
481+ deleteTicketComments :: forall m . (MonadIO m , MonadCatch m ) => Connection -> m ()
440482deleteTicketComments conn =
441- liftIO $ execute_ conn " DELETE FROM ticket_comment"
483+ liftIO $ executeSafe_ DBDeleteFailed conn " DELETE FROM ticket_comment"
442484
443- deleteTickets :: forall m . (MonadIO m ) => Connection -> m ()
485+ deleteTickets :: forall m . (MonadIO m , MonadCatch m ) => Connection -> m ()
444486deleteTickets conn =
445- liftIO $ execute_ conn " DELETE FROM ticket_info"
487+ liftIO $ executeSafe_ DBDeleteFailed conn " DELETE FROM ticket_info"
446488
447489-- | Delete all data.
448- deleteAllData :: forall m . (MonadIO m ) => Connection -> m ()
490+ deleteAllData :: forall m . (MonadIO m , MonadCatch m ) => Connection -> m ()
449491deleteAllData conn = do
450492 deleteCommentAttachments conn
451493 deleteTicketComments conn
452494 deleteTickets conn
453495
496+
497+ ------------------------------------------------------------
498+ -- Exception handling
499+ ------------------------------------------------------------
500+
501+ -- | 'executeNamed' with exception handling
502+ executeNamedSafe :: DBException -> Connection -> Query -> [NamedParam ] -> IO ()
503+ executeNamedSafe err conn query nm = executeNamed conn query nm `catches`
504+ [Handler formatError, Handler $ errorHandler err]
505+
506+ -- | 'execute_' with exception handling
507+ executeSafe_ :: DBException -> Connection -> Query -> IO ()
508+ executeSafe_ err conn query = execute_ conn query `catches`
509+ [Handler formatError, Handler $ errorHandler err]
510+
511+ -- | Exception handling on 'FormatError'
512+ -- For now, we don't do anything with it
513+ formatError :: FormatError -> IO ()
514+ formatError = throwM
515+
516+ -- | Exception handling on 'Error'
517+ -- Since 'Error' from sqlite-simple does not have instance of Exception,
518+ -- the only way of catching it by 'SomeException'
519+ -- We want to throw it with additional info (TicketId, Comment data, etc.)
520+ errorHandler :: DBException -> SomeException -> IO ()
521+ errorHandler dberr someErr = throwM $ DBLayerException dberr someErr
522+
523+ -- | Exceptions that can occur in 'DBLayer'
524+ data DBException
525+ = DBDeleteFailed
526+ -- ^ Exception upon data deletion
527+ | InsertCommentAttachmentFailed CommentId AttachmentId
528+ -- ^ Exception upon inserting 'Comment' and it's associated 'Attachment' to the database
529+ | InsertTicketCommentsFailed TicketId CommentId
530+ -- ^ Exception upon inserting 'TicketId' and it's associated 'Comment' to the database
531+ | InsertTicketInfoFailed TicketId
532+ -- ^ Exception upon inserting 'TicketInfo' to the database
533+
534+ -- | This is used to throw both 'DBLayerException' and 'SomeException'
535+ -- 'Error' from sqlite-simple has no instance of 'Exception'
536+ -- therefore the only way to catching it is by 'SomeException'
537+ -- Both of these exception are need in case the program crashes and we need to
538+ -- find the root cause of it.
539+ data DBLayerException = DBLayerException DBException SomeException
540+
541+ instance Exception DBException
542+
543+ instance Exception DBLayerException
544+
545+ instance Show DBException where
546+ show = \ case
547+ DBDeleteFailed ->
548+ " Exception occured while trying to delete from databse"
549+ InsertCommentAttachmentFailed cid aid -> concat
550+ [ " Exception occured while inserting comment and attachment on commentId: "
551+ , P. show (getCommentId cid)
552+ , " , attachmentId: "
553+ , P. show (getAttachmentId aid)
554+ ]
555+ InsertTicketCommentsFailed tid cid -> concat
556+ [ " Exception occured while inserting ticket and comment on commentId: "
557+ , P. show (getCommentId cid)
558+ , " , ticketId: "
559+ , P. show (getTicketId tid)
560+ ]
561+ InsertTicketInfoFailed tid ->
562+ " Exception occured while inserting TicketInfo with ticketId" <> P. show (getTicketId tid)
563+
564+ instance Show DBLayerException where
565+ show (DBLayerException dberr someErr) = concat
566+ [ " Error occured on DBlayer: "
567+ , P. show dberr
568+ , " with reason: "
569+ , P. show someErr
570+ ]
0 commit comments