pwncash/lib/Internal/Database/Ops.hs

426 lines
13 KiB
Haskell
Raw Normal View History

2022-12-11 17:51:11 -05:00
module Internal.Database.Ops
2023-05-07 20:29:33 -04:00
( runDB
2022-12-11 17:51:11 -05:00
, nukeTables
, updateHashes
2023-05-13 13:53:43 -04:00
, updateDBState
2022-12-11 17:51:11 -05:00
, getDBState
, tree2Records
, flattenAcntRoot
, paths2IDs
2023-05-13 13:53:43 -04:00
, mkPool
2023-05-29 15:56:15 -04:00
, whenHash
, whenHash_
, insertSplit
, resolveSplit
)
where
import Conduit
2023-05-07 20:29:33 -04:00
import Control.Monad.Except
import Control.Monad.Logger
import Data.Hashable
2023-05-07 20:29:33 -04:00
import Database.Esqueleto.Experimental ((==.), (^.))
import qualified Database.Esqueleto.Experimental as E
import Database.Esqueleto.Internal.Internal (SqlSelect)
import Database.Persist.Monad
2023-05-29 13:09:17 -04:00
import Database.Persist.Sqlite hiding
( delete
, deleteWhere
, insert
, insertKey
2023-05-29 15:56:15 -04:00
, insert_
2023-05-29 13:09:17 -04:00
, runMigration
, (==.)
, (||.)
)
2023-01-28 22:58:05 -05:00
import GHC.Err
import Internal.Types.Main
import Internal.Utils
import RIO hiding (LogFunc, isNothing, on, (^.))
import RIO.List ((\\))
import qualified RIO.List as L
import qualified RIO.Map as M
2023-01-27 20:54:25 -05:00
import qualified RIO.NonEmpty as N
import qualified RIO.Text as T
2022-12-11 17:51:11 -05:00
2023-05-07 20:29:33 -04:00
runDB
2023-01-05 22:23:22 -05:00
:: MonadUnliftIO m
=> SqlConfig
2023-05-07 20:29:33 -04:00
-> SqlQueryT (NoLoggingT m) a
-> m a
runDB c more =
runNoLoggingT $ do
pool <- mkPool c
runSqlQueryT pool $ do
_ <- lift askLoggerIO
runMigration migrateAll
more
mkPool :: (MonadLoggerIO m, MonadUnliftIO m) => SqlConfig -> m ConnectionPool
mkPool c = case c of
Sqlite p -> createSqlitePool p 10
-- conn <- open p
-- wrapConnection conn logfn
2022-12-11 18:53:54 -05:00
Postgres -> error "postgres not implemented"
2022-12-11 17:51:11 -05:00
2023-05-07 20:29:33 -04:00
nukeTables :: MonadSqlQuery m => m ()
2022-12-11 17:51:11 -05:00
nukeTables = do
deleteWhere ([] :: [Filter CommitR])
deleteWhere ([] :: [Filter CurrencyR])
deleteWhere ([] :: [Filter AccountR])
deleteWhere ([] :: [Filter TransactionR])
2023-01-28 22:58:05 -05:00
-- showBalances :: MonadUnliftIO m => SqlPersistT m ()
-- showBalances = do
-- xs <- select $ do
-- (accounts :& splits :& txs) <-
-- from
-- $ table @AccountR
-- `innerJoin` table @SplitR
-- `on` (\(a :& s) -> a ^. AccountRId ==. s ^. SplitRAccount)
-- `innerJoin` table @TransactionR
-- `on` (\(_ :& s :& t) -> s ^. SplitRTransaction ==. t ^. TransactionRId)
-- where_ $
-- isNothing (txs ^. TransactionRBucket)
-- &&. ( (accounts ^. AccountRFullpath `like` val "asset" ++. (%))
-- ||. (accounts ^. AccountRFullpath `like` val "liability" ++. (%))
-- )
-- groupBy (accounts ^. AccountRFullpath, accounts ^. AccountRName)
-- return
-- ( accounts ^. AccountRFullpath
-- , accounts ^. AccountRName
-- , sum_ $ splits ^. SplitRValue
-- )
-- -- TODO super stetchy table printing thingy
-- liftIO $ do
-- putStrLn $ T.unpack $ fmt "Account" "Balance"
-- putStrLn $ T.unpack $ fmt (T.replicate 60 "-") (T.replicate 15 "-")
-- mapM_ (putStrLn . T.unpack . fmtBalance) xs
-- where
-- fmtBalance (path, name, bal) = fmt (toFullPath path name) (toBal bal)
-- fmt a b = T.unwords ["| ", pad 60 a, " | ", pad 15 b, " |"]
-- pad n xs = T.append xs $ T.replicate (n - T.length xs) " "
-- toFullPath path name = T.unwords [unValue @T.Text path, "/", unValue @T.Text name]
-- toBal = maybe "???" (fmtRational 2) . unValue
2022-12-11 17:51:11 -05:00
hashConfig :: Config -> [Int]
hashConfig
Config_
2023-02-05 18:45:56 -05:00
{ budget = bs
, statements = ss
} = (hash <$> bs) ++ (hash <$> ms) ++ (hash <$> ps)
where
(ms, ps) = partitionEithers $ fmap go ss
2023-04-30 23:28:16 -04:00
go (HistTransfer x) = Left x
go (HistStatement x) = Right x
2022-12-11 17:51:11 -05:00
setDiff :: Eq a => [a] -> [a] -> ([a], [a])
-- setDiff = setDiff' (==)
setDiff as bs = (as \\ bs, bs \\ as)
-- setDiff' :: Eq a => (a -> b -> Bool) -> [a] -> [b] -> ([a], [b])
-- setDiff' f = go []
-- where
-- go inA [] bs = (inA, bs)
-- go inA as [] = (as ++ inA, [])
-- go inA (a:as) bs = case inB a bs of
-- Just bs' -> go inA as bs'
-- Nothing -> go (a:inA) as bs
-- inB _ [] = Nothing
-- inB a (b:bs)
-- | f a b = Just bs
-- | otherwise = inB a bs
2023-05-07 20:29:33 -04:00
getDBHashes :: MonadSqlQuery m => m [Int]
2022-12-11 17:51:11 -05:00
getDBHashes = fmap (commitRHash . entityVal) <$> dumpTbl
2023-05-07 20:29:33 -04:00
nukeDBHash :: MonadSqlQuery m => Int -> m ()
nukeDBHash h = deleteE $ do
c <- E.from E.table
E.where_ (c ^. CommitRHash ==. E.val h)
2022-12-11 17:51:11 -05:00
2023-05-07 20:29:33 -04:00
nukeDBHashes :: MonadSqlQuery m => [Int] -> m ()
2022-12-11 17:51:11 -05:00
nukeDBHashes = mapM_ nukeDBHash
2023-05-07 20:29:33 -04:00
getConfigHashes :: MonadSqlQuery m => Config -> m ([Int], [Int])
2022-12-11 17:51:11 -05:00
getConfigHashes c = do
let ch = hashConfig c
dh <- getDBHashes
return $ setDiff dh ch
2023-05-07 20:29:33 -04:00
dumpTbl :: (MonadSqlQuery m, PersistEntity r) => m [Entity r]
dumpTbl = selectE $ E.from E.table
2022-12-11 17:51:11 -05:00
2023-05-07 20:29:33 -04:00
deleteAccount :: MonadSqlQuery m => Entity AccountR -> m ()
deleteAccount e = deleteE $ do
c <- E.from $ E.table @AccountR
E.where_ (c ^. AccountRId ==. E.val k)
2022-12-11 17:51:11 -05:00
where
k = entityKey e
2023-05-07 20:29:33 -04:00
deleteCurrency :: MonadSqlQuery m => Entity CurrencyR -> m ()
deleteCurrency e = deleteE $ do
c <- E.from $ E.table @CurrencyR
E.where_ (c ^. CurrencyRId ==. E.val k)
2022-12-11 17:51:11 -05:00
where
k = entityKey e
2023-05-07 20:29:33 -04:00
deleteTag :: MonadSqlQuery m => Entity TagR -> m ()
deleteTag e = deleteE $ do
c <- E.from $ E.table @TagR
E.where_ (c ^. TagRId ==. E.val k)
2023-02-26 22:53:12 -05:00
where
k = entityKey e
-- TODO slip-n-slide code...
insertFull
2023-05-07 20:29:33 -04:00
:: (PersistRecordBackend r SqlBackend, Typeable r, MonadSqlQuery m)
=> Entity r
2023-05-07 20:29:33 -04:00
-> m ()
2022-12-11 17:51:11 -05:00
insertFull (Entity k v) = insertKey k v
currency2Record :: Currency -> Entity CurrencyR
2023-05-04 21:48:21 -04:00
currency2Record c@Currency {curSymbol, curFullname, curPrecision} =
Entity (toKey c) $ CurrencyR curSymbol curFullname (fromIntegral curPrecision)
2022-12-11 17:51:11 -05:00
currencyMap :: [Entity CurrencyR] -> CurrencyMap
2023-05-04 21:48:21 -04:00
currencyMap =
M.fromList
. fmap
( \e ->
( currencyRSymbol $ entityVal e
, (entityKey e, fromIntegral $ currencyRPrecision $ entityVal e)
)
)
2022-12-11 17:51:11 -05:00
toKey :: (ToBackendKey SqlBackend b, Hashable a) => a -> Key b
toKey = toSqlKey . fromIntegral . hash
tree2Entity :: AcntType -> [T.Text] -> T.Text -> T.Text -> Entity AccountR
tree2Entity t parents name des =
Entity (toSqlKey $ fromIntegral h) $
AccountR name (toPath parents) des
2022-12-11 17:51:11 -05:00
where
p = AcntPath t (reverse (name : parents))
2022-12-11 17:51:11 -05:00
h = hash p
toPath = T.intercalate "/" . (atName t :) . reverse
2022-12-11 17:51:11 -05:00
tree2Records
:: AcntType
-> AccountTree
2023-02-12 21:52:41 -05:00
-> ([Entity AccountR], [AccountPathR], [(AcntPath, (AccountRId, AcntSign, AcntType))])
2022-12-11 17:51:11 -05:00
tree2Records t = go []
where
go ps (Placeholder d n cs) =
let e = tree2Entity t (fmap snd ps) n d
2022-12-11 17:51:11 -05:00
k = entityKey e
2023-01-28 22:58:05 -05:00
(as, aps, ms) = L.unzip3 $ fmap (go ((k, n) : ps)) cs
2022-12-11 17:51:11 -05:00
a0 = acnt k n (fmap snd ps) d
paths = expand k $ fmap fst ps
in (a0 : concat as, paths ++ concat aps, concat ms)
go ps (Account d n) =
2022-12-11 17:51:11 -05:00
let e = tree2Entity t (fmap snd ps) n d
k = entityKey e
in ( [acnt k n (fmap snd ps) d]
, expand k $ fmap fst ps
2023-02-12 21:52:41 -05:00
, [(AcntPath t $ reverse $ n : fmap snd ps, (k, sign, t))]
)
toPath = T.intercalate "/" . (atName t :) . reverse
2022-12-11 17:51:11 -05:00
acnt k n ps = Entity k . AccountR n (toPath ps)
expand h0 hs = (\(h, d) -> AccountPathR h h0 d) <$> zip (h0 : hs) [0 ..]
2022-12-11 17:51:11 -05:00
sign = accountSign t
paths2IDs :: [(AcntPath, a)] -> [(AcntID, a)]
paths2IDs =
uncurry zip
. first trimNames
2023-01-28 22:58:05 -05:00
. L.unzip
. L.sortOn fst
. fmap (first pathList)
2022-12-11 17:51:11 -05:00
where
2023-01-27 20:54:25 -05:00
pathList (AcntPath t []) = atName t :| []
pathList (AcntPath t ns) = N.reverse $ atName t :| ns
2022-12-11 17:51:11 -05:00
2023-01-27 20:54:25 -05:00
-- none of these errors should fire assuming that input is sorted and unique
trimNames :: [N.NonEmpty T.Text] -> [AcntID]
trimNames = fmap (T.intercalate "_" . reverse) . trimAll 0
2022-12-11 17:51:11 -05:00
where
trimAll _ [] = []
trimAll i (y : ys) = case L.foldl' (matchPre i) (y, [], []) ys of
(a, [], bs) -> reverse $ trim i a : bs
(a, as, bs) -> reverse bs ++ trimAll (i + 1) (reverse $ a : as)
2022-12-11 17:51:11 -05:00
matchPre i (y, ys, old) new = case (y !? i, new !? i) of
(Nothing, Just _) ->
case ys of
[] -> (new, [], trim i y : old)
_ -> err "unsorted input"
2022-12-11 17:51:11 -05:00
(Just _, Nothing) -> err "unsorted input"
(Nothing, Nothing) -> err "duplicated inputs"
(Just a, Just b)
| a == b -> (new, y : ys, old)
2022-12-11 17:51:11 -05:00
| otherwise ->
let next = case ys of
[] -> [trim i y]
_ -> trimAll (i + 1) (reverse $ y : ys)
in (new, [], reverse next ++ old)
2023-01-27 20:54:25 -05:00
trim i = N.take (i + 1)
2022-12-11 17:51:11 -05:00
err msg = errorWithoutStackTrace $ "Import.Database.Ops.hs: " ++ msg
2023-01-27 20:54:25 -05:00
(!?) :: N.NonEmpty a -> Int -> Maybe a
2022-12-11 17:51:11 -05:00
xs !? n
| n < 0 = Nothing
-- Definition adapted from GHC.List
| otherwise =
foldr
( \x r k -> case k of
0 -> Just x
_ -> r (k - 1)
)
(const Nothing)
xs
n
2022-12-11 17:51:11 -05:00
flattenAcntRoot :: AccountRoot -> [(AcntType, AccountTree)]
2023-02-12 16:23:32 -05:00
flattenAcntRoot AccountRoot_ {arIncome, arExpenses, arLiabilities, arAssets, arEquity} =
2022-12-11 17:51:11 -05:00
((IncomeT,) <$> arIncome)
++ ((ExpenseT,) <$> arExpenses)
++ ((LiabilityT,) <$> arLiabilities)
++ ((AssetT,) <$> arAssets)
++ ((EquityT,) <$> arEquity)
2022-12-11 17:51:11 -05:00
indexAcntRoot :: AccountRoot -> ([Entity AccountR], [AccountPathR], AccountMap)
indexAcntRoot r =
( concat ars
, concat aprs
, M.fromList $ paths2IDs $ concat ms
)
where
2023-01-28 22:58:05 -05:00
(ars, aprs, ms) = L.unzip3 $ uncurry tree2Records <$> flattenAcntRoot r
2022-12-11 17:51:11 -05:00
2023-01-25 23:04:54 -05:00
getDBState
2023-05-07 20:29:33 -04:00
:: (MonadInsertError m, MonadSqlQuery m)
2023-01-25 23:04:54 -05:00
=> Config
2023-05-07 20:29:33 -04:00
-> m (FilePath -> DBState)
2023-01-28 19:32:56 -05:00
getDBState c = do
2023-05-13 13:53:43 -04:00
(del, new) <- getConfigHashes c
2023-01-28 19:32:56 -05:00
-- TODO not sure how I feel about this, probably will change this struct alot
-- in the future so whatever...for now
2023-05-07 20:29:33 -04:00
combineError bi si $ \b s f ->
2023-05-13 13:53:43 -04:00
-- TODO this can be cleaned up, half of it is meant to be queried when
-- determining how to insert budgets/history and the rest is just
-- holdover data to delete upon successful insertion
2023-01-28 19:32:56 -05:00
DBState
2023-05-13 13:53:43 -04:00
{ kmCurrency = currencyMap cs
2023-01-28 19:32:56 -05:00
, kmAccount = am
, kmBudgetInterval = b
, kmStatementInterval = s
2023-05-13 13:53:43 -04:00
, kmNewCommits = new
, kmOldCommits = del
2023-01-28 19:32:56 -05:00
, kmConfigDir = f
2023-05-13 13:53:43 -04:00
, kmTag = tagMap ts
, kmTagAll = ts
, kmAcntPaths = paths
, kmAcntsOld = acnts
, kmCurrenciesOld = cs
2023-01-28 19:32:56 -05:00
}
where
2023-05-29 15:56:15 -04:00
bi = liftExcept $ resolveDaySpan $ budgetInterval $ global c
si = liftExcept $ resolveDaySpan $ statementInterval $ global c
2023-05-13 13:53:43 -04:00
(acnts, paths, am) = indexAcntRoot $ accounts c
cs = currency2Record <$> currencies c
ts = toRecord <$> tags c
toRecord t@Tag {tagID, tagDesc} = Entity (toKey t) $ TagR tagID tagDesc
tagMap = M.fromList . fmap (\e -> (tagRSymbol $ entityVal e, entityKey e))
updateHashes :: (MonadFinance m, MonadSqlQuery m) => m ()
updateHashes = do
old <- askDBState kmOldCommits
nukeDBHashes old
updateTags :: (MonadFinance m, MonadSqlQuery m) => m ()
updateTags = do
tags <- askDBState kmTagAll
tags' <- selectE $ E.from $ E.table @TagR
let (toIns, toDel) = setDiff tags tags'
mapM_ deleteTag toDel
mapM_ insertFull toIns
updateAccounts :: (MonadFinance m, MonadSqlQuery m) => m ()
updateAccounts = do
acnts <- askDBState kmAcntsOld
paths <- askDBState kmAcntPaths
acnts' <- dumpTbl
let (toIns, toDel) = setDiff acnts acnts'
deleteWhere ([] :: [Filter AccountPathR])
mapM_ deleteAccount toDel
mapM_ insertFull toIns
mapM_ insert paths
updateCurrencies :: (MonadFinance m, MonadSqlQuery m) => m ()
updateCurrencies = do
curs <- askDBState kmCurrenciesOld
curs' <- selectE $ E.from $ E.table @CurrencyR
let (toIns, toDel) = setDiff curs curs'
mapM_ deleteCurrency toDel
mapM_ insertFull toIns
updateDBState :: (MonadFinance m, MonadSqlQuery m) => m ()
updateDBState = do
updateHashes
updateTags
updateAccounts
updateCurrencies
2023-05-07 20:29:33 -04:00
deleteE :: (MonadSqlQuery m) => E.SqlQuery () -> m ()
deleteE q = unsafeLiftSql "esqueleto-delete" (E.delete q)
selectE :: (MonadSqlQuery m, SqlSelect a r) => E.SqlQuery a -> m [r]
selectE q = unsafeLiftSql "esqueleto-select" (E.select q)
2023-05-29 15:56:15 -04:00
whenHash
:: (Hashable a, MonadFinance m, MonadSqlQuery m)
=> ConfigType
-> a
-> b
-> (CommitRId -> m b)
-> m b
whenHash t o def f = do
let h = hash o
hs <- askDBState kmNewCommits
if h `elem` hs then f =<< insert (CommitR h t) else return def
whenHash_
:: (Hashable a, MonadFinance m)
=> ConfigType
-> a
-> m b
-> m (Maybe (CommitR, b))
whenHash_ t o f = do
let h = hash o
let c = CommitR h t
hs <- askDBState kmNewCommits
if h `elem` hs then Just . (c,) <$> f else return Nothing
insertSplit :: MonadSqlQuery m => TransactionRId -> KeySplit -> m SplitRId
insertSplit t Entry {eAcnt, eCurrency, eValue, eComment, eTags} = do
k <- insert $ SplitR t eCurrency eAcnt eComment eValue
mapM_ (insert_ . TagRelationR k) eTags
return k
resolveSplit :: (MonadInsertError m, MonadFinance m) => BalSplit -> m KeySplit
resolveSplit s@Entry {eAcnt, eCurrency, eValue, eTags} = do
let aRes = lookupAccountKey eAcnt
let cRes = lookupCurrencyKey eCurrency
let sRes = lookupAccountSign eAcnt
let tagRes = combineErrors $ fmap lookupTag eTags
-- TODO correct sign here?
-- TODO lenses would be nice here
combineError (combineError3 aRes cRes sRes (,,)) tagRes $
\(aid, cid, sign) tags ->
s
{ eAcnt = aid
, eCurrency = cid
, eValue = eValue * fromIntegral (sign2Int sign)
, eTags = tags
}