ENH ensure tx sort order is (kinda) stable

This commit is contained in:
Nathan Dwarshuis 2023-07-21 19:57:54 -04:00
parent bd94afd30f
commit e9772e6516
6 changed files with 97 additions and 76 deletions

View File

@ -153,13 +153,9 @@ readIncome
} }
return $ return $
Tx Tx
{ txCommit = key { txMeta = TxMeta day incPriority (TxDesc "") key
, txDate = day
, txPrimary = Left primary , txPrimary = Left primary
, txOther = [] , txOther = []
, txDesc = TxDesc ""
, -- , txBudget = name
txPriority = incPriority
} }
periodScaler periodScaler
@ -358,25 +354,28 @@ fromShadow tx ShadowTransfer {stFrom, stTo, stDesc, stRatio, stCurrency, stMatch
shaRes = liftExcept $ shadowMatches stMatch tx shaRes = liftExcept $ shadowMatches stMatch tx
shadowMatches :: TransferMatcher -> Tx CommitR -> AppExcept Bool shadowMatches :: TransferMatcher -> Tx CommitR -> AppExcept Bool
shadowMatches TransferMatcher {tmFrom, tmTo, tmDate, tmVal} Tx {txPrimary, txDate} = do shadowMatches
-- NOTE this will only match against the primary entry set since those TransferMatcher {tmFrom, tmTo, tmDate, tmVal}
-- are what are guaranteed to exist from a transfer Tx {txPrimary, txMeta = TxMeta {txmDate}} =
valRes <- case txPrimary of do
Left es -> valMatches tmVal $ toRational $ esTotalValue es -- NOTE this will only match against the primary entry set since those
Right _ -> return True -- are what are guaranteed to exist from a transfer
return $ valRes <- case txPrimary of
memberMaybe fa tmFrom Left es -> valMatches tmVal $ toRational $ esTotalValue es
&& memberMaybe ta tmTo Right _ -> return True
&& maybe True (`dateMatches` txDate) tmDate return $
&& valRes memberMaybe fa tmFrom
where && memberMaybe ta tmTo
fa = either getAcntFrom getAcntFrom txPrimary && maybe True (`dateMatches` txmDate) tmDate
ta = either getAcntTo getAcntTo txPrimary && valRes
getAcntFrom = getAcnt esFrom where
getAcntTo = getAcnt esTo fa = either getAcntFrom getAcntFrom txPrimary
getAcnt f = eAcnt . hesPrimary . f ta = either getAcntTo getAcntTo txPrimary
memberMaybe x AcntSet {asList, asInclude} = getAcntFrom = getAcnt esFrom
(if asInclude then id else not) $ x `elem` (AcntID <$> asList) getAcntTo = getAcnt esTo
getAcnt f = eAcnt . hesPrimary . f
memberMaybe x AcntSet {asList, asInclude} =
(if asInclude then id else not) $ x `elem` (AcntID <$> asList)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- random -- random

View File

@ -469,6 +469,7 @@ updateCD (CRUDOps cs () () ds) = do
mapM_ deleteKeyE ds mapM_ deleteKeyE ds
insertEntityManyE cs insertEntityManyE cs
-- TODO defer foreign keys so I don't need to confusingly reverse this stuff
deleteTxs :: MonadSqlQuery m => DeleteTxs -> m () deleteTxs :: MonadSqlQuery m => DeleteTxs -> m ()
deleteTxs DeleteTxs {dtTxs, dtEntrySets, dtEntries, dtTagRelations, dtCommits} = do deleteTxs DeleteTxs {dtTxs, dtEntrySets, dtEntries, dtTagRelations, dtCommits} = do
mapM_ deleteKeyE dtTagRelations mapM_ deleteKeyE dtTagRelations
@ -566,8 +567,10 @@ readUpdates hashes = do
, ,
( (
( entrysets ^. EntrySetRId ( entrysets ^. EntrySetRId
, entrysets ^. EntrySetRIndex
, txs ^. TransactionRDate , txs ^. TransactionRDate
, txs ^. TransactionRPriority , txs ^. TransactionRPriority
, txs ^. TransactionRDescription
, ,
( entrysets ^. EntrySetRCurrency ( entrysets ^. EntrySetRCurrency
, currencies ^. CurrencyRPrecision , currencies ^. CurrencyRPrecision
@ -577,11 +580,12 @@ readUpdates hashes = do
) )
) )
let (toUpdate, toRead) = L.partition (E.unValue . fst) xs let (toUpdate, toRead) = L.partition (E.unValue . fst) xs
toUpdate' <- liftExcept $ mapErrors makeUES $ groupKey (\(i, _, _, _) -> i) (snd <$> toUpdate) toUpdate' <- liftExcept $ mapErrors makeUES $ groupKey (\(i, _, _, _, _, _) -> i) (snd <$> toUpdate)
let toRead' = fmap (makeRE . snd) toRead let toRead' = fmap (makeRE . snd) toRead
return (toRead', toUpdate') return (toRead', toUpdate')
where where
makeUES ((_, day, pri, (curID, prec)), es) = do makeUES ((_, esi, day, pri, desc, (curID, prec)), es) = do
let sk = TxSortKey (E.unValue day) (E.unValue pri) (E.unValue desc)
let prec' = fromIntegral $ E.unValue prec let prec' = fromIntegral $ E.unValue prec
let cur = E.unValue curID let cur = E.unValue curID
let res = let res =
@ -599,8 +603,7 @@ readUpdates hashes = do
Left x -> Left x ->
Left $ Left $
UpdateEntrySet UpdateEntrySet
{ utDate = E.unValue day { utCurrency = cur
, utCurrency = cur
, utFrom0 = x , utFrom0 = x
, utTo0 = to0 , utTo0 = to0
, utFromRO = fromRO , utFromRO = fromRO
@ -608,13 +611,13 @@ readUpdates hashes = do
, utFromUnk = fromUnk , utFromUnk = fromUnk
, utToUnk = toUnk , utToUnk = toUnk
, utTotalValue = realFracToDecimalP prec' tot , utTotalValue = realFracToDecimalP prec' tot
, utPriority = E.unValue pri , utSortKey = sk
, utIndex = E.unValue esi
} }
Right x -> Right x ->
Right $ Right $
UpdateEntrySet UpdateEntrySet
{ utDate = E.unValue day { utCurrency = cur
, utCurrency = cur
, utFrom0 = x , utFrom0 = x
, utTo0 = to0 , utTo0 = to0
, utFromRO = fromRO , utFromRO = fromRO
@ -622,18 +625,20 @@ readUpdates hashes = do
, utFromUnk = fromUnk , utFromUnk = fromUnk
, utToUnk = toUnk , utToUnk = toUnk
, utTotalValue = () , utTotalValue = ()
, utPriority = E.unValue pri , utSortKey = sk
, utIndex = E.unValue esi
} }
-- TODO this error is lame -- TODO this error is lame
_ -> throwAppError $ DBError DBUpdateUnbalanced _ -> throwAppError $ DBError DBUpdateUnbalanced
makeRE ((_, day, pri, (curID, prec)), entry) = do makeRE ((_, esi, day, pri, desc, (curID, prec)), entry) = do
let e = entityVal entry let e = entityVal entry
in ReadEntry in ReadEntry
{ reDate = E.unValue day { reCurrency = E.unValue curID
, reCurrency = E.unValue curID
, reAcnt = entryRAccount e , reAcnt = entryRAccount e
, reValue = realFracToDecimal (fromIntegral $ E.unValue prec) (entryRValue e) , reValue = realFracToDecimal (fromIntegral $ E.unValue prec) (entryRValue e)
, rePriority = E.unValue pri , reSortKey = TxSortKey (E.unValue day) (E.unValue pri) (E.unValue desc)
, reESIndex = E.unValue esi
, reIndex = entryRIndex e
} }
splitFrom splitFrom
@ -792,7 +797,7 @@ insertBudgets (CRUDOps bs () () ds) = do
-- TODO useless overhead? -- TODO useless overhead?
(toUpdate, toInsert) <- balanceTxs (ToInsert <$> cs) (toUpdate, toInsert) <- balanceTxs (ToInsert <$> cs)
mapM_ updateTx toUpdate mapM_ updateTx toUpdate
forM_ (groupWith itxCommit toInsert) $ forM_ (groupWith (txmCommit . itxMeta) toInsert) $
\(c, ts) -> do \(c, ts) -> do
ck <- insert c ck <- insert c
mapM_ (insertTx name ck) ts mapM_ (insertTx name ck) ts
@ -804,7 +809,7 @@ insertHistory
insertHistory (CRUDOps cs rs us ds) = do insertHistory (CRUDOps cs rs us ds) = do
(toUpdate, toInsert) <- balanceTxs $ (ToInsert <$> cs) ++ (ToRead <$> rs) ++ (ToUpdate <$> us) (toUpdate, toInsert) <- balanceTxs $ (ToInsert <$> cs) ++ (ToRead <$> rs) ++ (ToUpdate <$> us)
mapM_ updateTx toUpdate mapM_ updateTx toUpdate
forM_ (groupWith itxCommit toInsert) $ forM_ (groupWith (txmCommit . itxMeta) toInsert) $
\(c, ts) -> do \(c, ts) -> do
ck <- insert c ck <- insert c
mapM_ (insertTx historyName ck) ts mapM_ (insertTx historyName ck) ts
@ -829,8 +834,8 @@ insertHistory (CRUDOps cs rs us ds) = do
-- deleteTxs ds -- deleteTxs ds
insertTx :: MonadSqlQuery m => BudgetName -> CommitRId -> InsertTx -> m () insertTx :: MonadSqlQuery m => BudgetName -> CommitRId -> InsertTx -> m ()
insertTx b c InsertTx {itxDate, itxDesc, itxEntrySets, itxPriority} = do insertTx b c InsertTx {itxMeta = TxMeta {txmDate, txmPriority, txmDesc}, itxEntrySets} = do
k <- insert $ TransactionR c itxDate b itxDesc itxPriority k <- insert $ TransactionR c txmDate b txmDesc txmPriority
mapM_ (uncurry (insertEntrySet k)) $ zip [0 ..] (NE.toList itxEntrySets) mapM_ (uncurry (insertEntrySet k)) $ zip [0 ..] (NE.toList itxEntrySets)
where where
insertEntrySet tk i InsertEntrySet {iesCurrency, iesFromEntries, iesToEntries} = do insertEntrySet tk i InsertEntrySet {iesCurrency, iesFromEntries, iesToEntries} = do

View File

@ -72,9 +72,10 @@ readHistStmt
readHistStmt root i = do readHistStmt root i = do
bounds <- asks (unHSpan . tsHistoryScope) bounds <- asks (unHSpan . tsHistoryScope)
bs <- readImport root i bs <- readImport root i
return $ filter (inDaySpan bounds . txDate) . fmap (\t -> t {txCommit = c}) <$> bs return $ filter (inDaySpan bounds . txmDate . txMeta) . fmap go <$> bs
where where
c = CommitR (CommitHash $ hash i) CTHistoryStatement go t@Tx {txMeta = m} =
t {txMeta = m {txmCommit = CommitR (CommitHash $ hash i) CTHistoryStatement}}
-- TODO this probably won't scale well (pipes?) -- TODO this probably won't scale well (pipes?)
readImport readImport
@ -319,9 +320,7 @@ toTx
r@TxRecord {trAmount, trDate, trDesc} = do r@TxRecord {trAmount, trDate, trDesc} = do
combineError curRes subRes $ \(cur, f, t) ss -> combineError curRes subRes $ \(cur, f, t) ss ->
Tx Tx
{ txDate = trDate { txMeta = TxMeta trDate priority trDesc ()
, txDesc = trDesc
, txCommit = ()
, txPrimary = , txPrimary =
Left $ Left $
EntrySet EntrySet
@ -331,7 +330,6 @@ toTx
, esTo = t , esTo = t
} }
, txOther = Left <$> ss , txOther = Left <$> ss
, txPriority = priority
} }
where where
curRes = do curRes = do

View File

@ -24,10 +24,12 @@ CommitR sql=commits
type ConfigType type ConfigType
UniqueCommitHash hash UniqueCommitHash hash
deriving Show Eq Ord deriving Show Eq Ord
ConfigStateR sql=config_state ConfigStateR sql=config_state
historySpan HistorySpan historySpan HistorySpan
budgetSpan BudgetSpan budgetSpan BudgetSpan
deriving Show deriving Show
CurrencyR sql=currencies CurrencyR sql=currencies
symbol CurID symbol CurID
fullname T.Text fullname T.Text
@ -35,12 +37,14 @@ CurrencyR sql=currencies
UniqueCurrencySymbol symbol UniqueCurrencySymbol symbol
UniqueCurrencyFullname fullname UniqueCurrencyFullname fullname
deriving Show Eq Ord deriving Show Eq Ord
TagR sql=tags TagR sql=tags
symbol TagID symbol TagID
fullname T.Text fullname T.Text
UniqueTagSymbol symbol UniqueTagSymbol symbol
UniqueTagFullname fullname UniqueTagFullname fullname
deriving Show Eq Ord deriving Show Eq Ord
AccountR sql=accounts AccountR sql=accounts
name T.Text name T.Text
fullpath AcntPath fullpath AcntPath
@ -49,11 +53,13 @@ AccountR sql=accounts
leaf Bool leaf Bool
UniqueAccountFullpath fullpath UniqueAccountFullpath fullpath
deriving Show Eq Ord deriving Show Eq Ord
AccountPathR sql=account_paths AccountPathR sql=account_paths
parent AccountRId parent AccountRId
child AccountRId child AccountRId
depth Int depth Int
deriving Show Eq Ord deriving Show Eq Ord
TransactionR sql=transactions TransactionR sql=transactions
commit CommitRId commit CommitRId
date Day date Day
@ -61,12 +67,14 @@ TransactionR sql=transactions
description TxDesc description TxDesc
priority Int priority Int
deriving Show Eq deriving Show Eq
EntrySetR sql=entry_sets EntrySetR sql=entry_sets
transaction TransactionRId transaction TransactionRId
currency CurrencyRId currency CurrencyRId
index EntrySetIndex index EntrySetIndex
rebalance Bool rebalance Bool
deriving Show Eq deriving Show Eq
EntryR sql=entries EntryR sql=entries
entryset EntrySetRId entryset EntrySetRId
account AccountRId account AccountRId
@ -77,12 +85,16 @@ EntryR sql=entries
cachedType (Maybe TransferType) cachedType (Maybe TransferType)
cachedLink (Maybe EntryIndex) cachedLink (Maybe EntryIndex)
deriving Show Eq deriving Show Eq
TagRelationR sql=tag_relations TagRelationR sql=tag_relations
entry EntryRId entry EntryRId
tag TagRId tag TagRId
deriving Show Eq deriving Show Eq
|] |]
newtype TxIndex = TxIndex {unTxIndex :: Int}
deriving newtype (Show, Eq, Ord, Num, Real, Enum, Integral, PersistField, PersistFieldSql)
newtype EntrySetIndex = EntrySetIndex {unEntrySetIndex :: Int} newtype EntrySetIndex = EntrySetIndex {unEntrySetIndex :: Int}
deriving newtype (Show, Eq, Ord, Num, Real, Enum, Integral, PersistField, PersistFieldSql) deriving newtype (Show, Eq, Ord, Num, Real, Enum, Integral, PersistField, PersistFieldSql)

View File

@ -102,14 +102,22 @@ data CachedEntry
| CachedBalance Decimal | CachedBalance Decimal
| CachedPercent Double | CachedPercent Double
data TxSortKey = TxSortKey
{ tskDate :: !Day
, tskPriority :: !Int
, tskDesc :: !TxDesc
}
deriving (Show, Eq, Ord)
-- TODO this should actually be a ReadTx since it will be compared with other -- TODO this should actually be a ReadTx since it will be compared with other
-- Tx's to get the insert/update order correct -- Tx's to get the insert/update order correct
data ReadEntry = ReadEntry data ReadEntry = ReadEntry
{ reCurrency :: !CurrencyRId { reCurrency :: !CurrencyRId
, reAcnt :: !AccountRId , reAcnt :: !AccountRId
, reValue :: !Decimal , reValue :: !Decimal
, reDate :: !Day , reIndex :: !EntryIndex
, rePriority :: !Int , reESIndex :: !EntrySetIndex
, reSortKey :: !TxSortKey
} }
deriving (Show) deriving (Show)
@ -149,9 +157,9 @@ data UpdateEntrySet f t = UpdateEntrySet
, utFromRO :: ![UE_RO] , utFromRO :: ![UE_RO]
, utToRO :: ![UE_RO] , utToRO :: ![UE_RO]
, utCurrency :: !CurrencyRId , utCurrency :: !CurrencyRId
, utDate :: !Day
, utTotalValue :: !t , utTotalValue :: !t
, utPriority :: !Int , utIndex :: !EntrySetIndex
, utSortKey :: !TxSortKey
} }
deriving (Show) deriving (Show)
@ -214,13 +222,18 @@ type ShadowEntrySet = TotalEntrySet Double EntryValue EntryLink
data TxCommit = HistoryCommit CommitR | BudgetCommit CommitR T.Text data TxCommit = HistoryCommit CommitR | BudgetCommit CommitR T.Text
deriving (Eq, Ord, Show) deriving (Eq, Ord, Show)
data TxMeta k = TxMeta
{ txmDate :: !Day
, txmPriority :: !Int
, txmDesc :: !TxDesc
, txmCommit :: !k
}
deriving (Show, Eq, Ord)
data Tx k = Tx data Tx k = Tx
{ txDesc :: !TxDesc { txMeta :: !(TxMeta k)
, txDate :: !Day
, txPriority :: !Int
, txPrimary :: !(Either PrimaryEntrySet TransferEntrySet) , txPrimary :: !(Either PrimaryEntrySet TransferEntrySet)
, txOther :: ![Either SecondayEntrySet ShadowEntrySet] , txOther :: ![Either SecondayEntrySet ShadowEntrySet]
, txCommit :: !k
} }
deriving (Generic, Show) deriving (Generic, Show)
@ -236,11 +249,8 @@ data InsertEntrySet = InsertEntrySet
} }
data InsertTx = InsertTx data InsertTx = InsertTx
{ itxDesc :: !TxDesc { itxMeta :: !(TxMeta CommitR)
, itxDate :: !Day
, itxPriority :: !Int
, itxEntrySets :: !(NonEmpty InsertEntrySet) , itxEntrySets :: !(NonEmpty InsertEntrySet)
, itxCommit :: !CommitR
} }
deriving (Generic) deriving (Generic)

View File

@ -642,19 +642,11 @@ balanceTxs ebs =
go (ToRead ReadEntry {reCurrency, reAcnt, reValue}) = do go (ToRead ReadEntry {reCurrency, reAcnt, reValue}) = do
modify $ mapAdd_ (reAcnt, reCurrency) reValue modify $ mapAdd_ (reAcnt, reCurrency) reValue
return Nothing return Nothing
go (ToInsert Tx {txPrimary, txOther, txDesc, txCommit, txDate, txPriority}) = do go (ToInsert Tx {txPrimary, txOther, txMeta}) = do
e <- either balancePrimaryEntrySet balanceSecondaryEntrySet txPrimary e <- either balancePrimaryEntrySet balanceSecondaryEntrySet txPrimary
let tot = sum $ fmap (eValue . ieEntry) $ NE.toList $ iesFromEntries e let tot = sum $ fmap (eValue . ieEntry) $ NE.toList $ iesFromEntries e
es <- mapErrors (goOther tot) txOther es <- mapErrors (goOther tot) txOther
let tx = let tx = InsertTx {itxMeta = txMeta, itxEntrySets = e :| es}
-- TODO this is lame
InsertTx
{ itxDesc = txDesc
, itxDate = txDate
, itxEntrySets = e :| es
, itxCommit = txCommit
, itxPriority = txPriority
}
return $ Just $ Right tx return $ Just $ Right tx
where where
goOther tot = goOther tot =
@ -663,12 +655,20 @@ balanceTxs ebs =
(balancePrimaryEntrySet . fromShadow tot) (balancePrimaryEntrySet . fromShadow tot)
fromShadow tot e@EntrySet {esTotalValue} = e {esTotalValue = tot *. esTotalValue} fromShadow tot e@EntrySet {esTotalValue} = e {esTotalValue = tot *. esTotalValue}
binDate :: EntryCRU -> (Day, Int) -- NOTE this sorting thing is super wonky; I'm basically sorting three different
binDate (ToRead ReadEntry {reDate, rePriority}) = (reDate, rePriority) -- levels of the hierarchy directory and assuming there will be no overlaps.
binDate (ToInsert Tx {txDate, txPriority}) = (txDate, txPriority) -- First, sort at the transaction level by day, priority, and description as
-- tiebreaker. Anything that shares those three keys will have an unstable sort
-- order. Within the entrysets, use the index as it appears in the
-- configuration, and same with the entries. Since we assume no overlap, nothing
-- "bad" should happen if the levels above entries/entrysets sort on 'Nothing'
-- for the indices they don't have at their level.
binDate :: EntryCRU -> (TxSortKey, Maybe EntrySetIndex, Maybe EntryIndex)
binDate (ToRead ReadEntry {reSortKey, reESIndex, reIndex}) = (reSortKey, Just reESIndex, Just reIndex)
binDate (ToInsert Tx {txMeta = (TxMeta t p d _)}) = (TxSortKey t p d, Nothing, Nothing)
binDate (ToUpdate u) = either go go u binDate (ToUpdate u) = either go go u
where where
go UpdateEntrySet {utDate, utPriority} = (utDate, utPriority) go UpdateEntrySet {utSortKey, utIndex} = (utSortKey, Just utIndex, Nothing)
type BCKey = CurrencyRId type BCKey = CurrencyRId
@ -988,12 +988,9 @@ expandTransfer tc bounds Transfer {transAmounts, transTo, transCurrency, transFr
withDates bounds pat $ \day -> withDates bounds pat $ \day ->
return return
Tx Tx
{ txCommit = tc { txMeta = TxMeta day (fromIntegral pri) (TxDesc desc) tc
, txDate = day
, txPrimary = Right $ entryPair transFrom transTo (cpID cp) desc () v'' , txPrimary = Right $ entryPair transFrom transTo (cpID cp) desc () v''
, txOther = [] , txOther = []
, txDesc = TxDesc desc
, txPriority = fromIntegral pri
} }
entryPair entryPair