Work in progress replacing sqlite with postgres.
[?]
Jan 20, 2015, 3:29 PM
IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEACDependencies
- [2]
JKMHA2QGSQLite support is now relatively sane. - [3]
NJZ3DKZYTHEY CAN TALK! - [4]
MK7ODWHUSome minor renaming. - [5]
ADMKQQGCInitial empty Snap project. - [6]
5W5M56VJMove library code to 'lib' - [7]
TZQJVHBAAdd auction functions to ADB. - [8]
WFZDMVUXRename ADB -> QDB - [9]
7XN3I3QJAdd 'loggedIntervals' endpoint. - [10]
2Y2QZFVFSwitch to more modern cabal2nix-based workflow. - [11]
GKGVYBZGAdded JSON serialization to TimeLog - [12]
7DBNV3GVInitial, stack-based impl of time log event reduction. - [13]
WO2MINIFAuctions now compile! - [14]
FRPWIKCNAdded log event parsing to Quixotic.Database.SQLite - [15]
W35DDBFYFactor common JSON conversions up into client lib module. - [16]
TLQ72DSJLenses, sqlite-simple - [17]
MWUPXTBFA few steps down a road to be abandoned. - [18]
NTPC7KJETrivial changes, feature scratchpad. - [19]
Z3M53KTLAdrift. - [20]
EMVTF2IWWIP moving back to snap. - [21]
64VI73NPServer now compiles using abstracted SQLite - [22]
TCOAKCGGCompleted conversion to snap. - [23]
LAROLAYUWIP - [24]
64C6AWH6Rename Ananke -> Quixotic, project reboot. - [25]
NVOCQVASInitial failing tests. - [26]
RSEB2NFGReplacing Snap with Scotty. - [27]
75N3UJ4JMore progression toward lenses. - [28]
WZUHEZSBStart of migration back toward snap. - [29]
LUM4VQJIIncrement. - [*]
OBFPJS2GProject successfully builds and tests under nix.
Change contents
- edit in lib/Quixotic/Auction.hs at line 7
import Data.Group - replacement in lib/Quixotic/Auction.hs at line 8
import Quixoticimport Network.Bitcoin - edit in lib/Quixotic/Auction.hs at line 13
makePrisms ''AuctionId - edit in lib/Quixotic/Auction.hs at line 19
- edit in lib/Quixotic/Auction.hs at line 27
- replacement in lib/Quixotic/Auction.hs at line 34
btc bid = toRational $ bid ^. (bidAmount . satoshis)btc bid = toRational $ bid ^. bidAmount - replacement in lib/Quixotic/Auction.hs at line 43
| (total ++ x ^. bidAmount) < (auction ^. raiseAmount) =x : (takeWinningBids (total ++ x ^. bidAmount) xs)| total + (x ^. bidAmount) < (auction ^. raiseAmount) =x : (takeWinningBids (total + (x ^. bidAmount)) xs) - replacement in lib/Quixotic/Auction.hs at line 47
| total < auction ^. raiseAmount =let remainder = (auction ^. raiseAmount) ++ invert totalwinFraction :: RationalwinFraction = (toRational $ remainder ^. satoshis) / (toRational $ x ^. (bidAmount . satoshis))| total < (auction ^. raiseAmount) =let remainder = (auction ^. raiseAmount) - totalwinFraction = toRational $ remainder / (x ^. bidAmount) - replacement in lib/Quixotic/Auction.hs at line 57
in takeWinningBids mempty $ sort bids[5.1147]in takeWinningBids (fromInteger 0) $ sort bids - file addition: PostgreSQL.hs[5.40]
{-# LANGUAGE ScopedTypeVariables, OverloadedStrings, NoImplicitPrelude #-}{-# LANGUAGE TemplateHaskell #-}module Quixotic.Database.PostgreSQL (postgresQDB) whereimport Blaze.ByteString.Builder (fromByteString)import ClassyPreludeimport Control.Lensimport Data.Fixedimport Data.Hourglassimport qualified Data.List as DLimport Database.PostgreSQL.Simpleimport Database.PostgreSQL.Simple.ToFieldimport Database.PostgreSQL.Simple.FromFieldimport Database.PostgreSQL.Simple.ToRowimport Database.PostgreSQL.Simple.FromRowimport Network.Bitcoinimport Quixoticimport Quixotic.Auctionimport Quixotic.Databaseimport Quixotic.TimeLogimport Quixotic.UserseventTypeParser :: FieldParser EventTypeeventTypeParser f v = fromField f v >>= nameEventuidParser :: FieldParser UserIduidParser f v = UserId <$> fromField f vsecondsParser :: FieldParser SecondssecondsParser f v = Seconds <$> fromField f vusernameParser :: FieldParser UserNameusernameParser f v = UserName <$> fromField f vbtcAddrParser :: FieldParser BtcAddrbtcAddrParser f v = BtcAddr <$> fromField f vbtcParser :: FieldParser BTCbtcParser f v = fromField f vworkEventParser :: RowParser WorkEventworkEventParser = WorkEvent <$> fieldWith eventTypeParser <*> fieldlogEntryParser :: RowParser LogEntrylogEntryParser = LogEntry <$> fieldWith btcAddrParser <*> workEventParserauctionRowParser :: RowParser AuctionauctionRowParser = Auction <$> fieldWith btcParser <*> fieldbidRowParser :: RowParser BidbidRowParser = Bid <$> fieldWith uidParser<*> fieldWith secondsParser<*> fieldWith btcParser<*> fielduserRowParser :: RowParser UseruserRowParser = User <$> fieldWith usernameParser<*> fieldWith btcAddrParser<*> field-- Local newtypes to permit field serializationnewtype PBTC = PBTC { pBTC :: BTC }instance ToField PBTC wheretoField (PBTC btc) = Plain . fromByteString . fromString $ showFixed False btc-- Local newtypes to permit row deserialization via-- typeclass. Wish I could just pass the RowParser instancesnewtype PLogEntry = PLogEntry { pLogEntry :: LogEntry }instance FromRow PLogEntry wherefromRow = PLogEntry <$> logEntryParsernewtype PBid = PBid { pBid :: Bid }instance FromRow PBid wherefromRow = PBid <$> bidRowParsernewtype PUser = PUser { pUser :: User }instance FromRow PUser wherefromRow = PUser <$> userRowParsernewtype PAuction = PAuction { pAuction :: Auction }instance FromRow PAuction wherefromRow = PAuction <$> auctionRowParserrecordEvent' :: UserId -> LogEntry -> ReaderT Connection IO ()recordEvent' (UserId uid) (LogEntry a e) = doconn <- askvoid . lift $ execute conn"INSERT INTO work_events (user_id, btc_addr, event_type, event_time) VALUES (?, ?, ?, ?)"( uid, a ^. address, e ^. (eventType . to eventName), e ^. eventTime)readWorkIndex' :: ReaderT Connection IO WorkIndexreadWorkIndex' = doconn <- askrows <- lift $ query_ conn"SELECT btc_addr, event_type, event_time from work_events"pure . workIndex $ fmap pLogEntry rowsnewAuction' :: Auction -> ReaderT Connection IO AuctionIdnewAuction' auc = doconn <- askaucIds <- lift $ query conn"INSERT INTO auctions (raise_amount, end_time) VALUES (?, ?) RETURNING id"(auc ^. (raiseAmount.to PBTC), auc ^. auctionEnd)pure . AuctionId . fromOnly $ DL.head aucIdsreadAuction' :: AuctionId -> ReaderT Connection IO (Maybe Auction)readAuction' aucId = doconn <- askrows <- lift $ query conn"SELECT raise_amount, end_time FROM auctions WHERE ROWID = ?"(Only (aucId ^. _AuctionId))pure . fmap pAuction $ headMay rowsrecordBid' :: AuctionId -> Bid -> ReaderT Connection IO ()recordBid' (AuctionId aucId) bid = doconn <- askvoid . lift $ execute conn"INSERT INTO bids (auction_id, user_id, bid_seconds, bid_amount, bid_time) values (?, ?, ?, ?, ?)"( aucId, bid ^. (bidUser._UserId), case bid ^. bidSeconds of (Seconds i) -> i, bid ^. (bidAmount.to PBTC), bid ^. bidTime)readBids' :: AuctionId -> ReaderT Connection IO [Bid]readBids' aucId = doconn <- askrows <- lift $ query conn"SELECT user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"(Only $ (aucId ^. _AuctionId))pure $ fmap pBid rowscreateUser' :: User -> ReaderT Connection IO UserIdcreateUser' user = doconn <- askuids <- lift $ query conn"INSERT INTO users (handle, btc_addr, email) VALUES (?, ?) RETURNING id"(user ^. (username._UserName), user ^. (userAddress.address), user ^. userEmail)pure . UserId . fromOnly $ DL.head uidsfindUser' :: UserId -> ReaderT Connection IO (Maybe User)findUser' (UserId uid) = doconn <- askusers <- lift $ query conn"SELECT handle, btc_addr, email FROM users WHERE id = ?"(Only uid)pure . fmap pUser $ headMay usersfindUserByHandle' :: Handle -> ReaderT Connection IO (Maybe QDBUser)findUserByHandle' = undefinedpostgresQDB :: QDB (ReaderT Connection IO)postgresQDB = QDB{ recordEvent = recordEvent', readWorkIndex = readWorkIndex', newAuction = newAuction', readAuction = readAuction', recordBid = recordBid', readBids = readBids', createUser = createUser', findUser = findUser', findUserByHandle = findUserByHandle'} - edit in lib/Quixotic/Database/SQLite.hs at line 11
import qualified Text.Read as R - replacement in lib/Quixotic/Database/SQLite.hs at line 37
let auctionParser = Auction <$> (fmap BTC field) <*> fieldlet auctionParser = Auction <$> fmap R.read field <*> field - replacement in lib/Quixotic/Database/SQLite.hs at line 45
let bidParser = Bid <$> fmap UserId field <*> fmap Seconds field <*> fmap BTC field <*> fieldlet bidParser = Bid <$> fmap UserId field <*> fmap Seconds field <*> fmap R.read field <*> field - edit in lib/Quixotic/Database/SQLite.hs at line 62
newtype PBTC = PBTC BTC - replacement in lib/Quixotic/Database/SQLite.hs at line 63[2.657]→[2.657:722](∅→∅),[2.722]→[5.797:798](∅→∅),[5.1498]→[5.797:798](∅→∅),[5.797]→[5.797:798](∅→∅),[5.798]→[5.1499:1580](∅→∅)
instance ToField PBTC wheretoField (PBTC (BTC i)) = toField irecordEvent' :: LogEntry -> ReaderT Connection IO ()recordEvent' logEntry = do-- TODO: Record the user idrecordEvent' :: UserId -> LogEntry -> ReaderT Connection IO ()recordEvent' _ logEntry = do - replacement in lib/Quixotic/Database/SQLite.hs at line 83
(auc ^. (raiseAmount . satoshis), auc ^. auctionEnd)(show $ auc ^. raiseAmount, auc ^. auctionEnd) - replacement in lib/Quixotic/Database/SQLite.hs at line 102
, PBTC $ bid ^. bidAmount, show $ bid ^. bidAmount - replacement in lib/Quixotic/Database/SQLite.hs at line 115
createUser' user = docreateUser' u = do - replacement in lib/Quixotic/Database/SQLite.hs at line 119
(user ^. (userAddress . address), user ^. userEmail)(u ^. (userAddress . address), u ^. userEmail) - replacement in lib/Quixotic/Database/SQLite.hs at line 122
sqliteQDB :: QDB IO ConnectionsqliteQDB :: QDB (ReaderT Connection IO) - edit in lib/Quixotic/Database/SQLite.hs at line 131
, findUser = \_ -> pure Nothing, findUserByHandle = \_ -> pure Nothing - replacement in lib/Quixotic/Database.hs at line 1
{-# LANGUAGE NoImplicitPrelude #-}{-# LANGUAGE NoImplicitPrelude, TemplateHaskell #-} - edit in lib/Quixotic/Database.hs at line 6
import Control.Lens - replacement in lib/Quixotic/Database.hs at line 12[5.5045]→[5.161:181](∅→∅),[5.181]→[5.5568:5614](∅→∅),[5.5065]→[5.5568:5614](∅→∅),[5.5614]→[5.839:882](∅→∅),[5.882]→[5.5674:5725](∅→∅),[5.5674]→[5.5674:5725](∅→∅),[5.5725]→[5.3276:3388](∅→∅),[5.3388]→[2.1444:1491](∅→∅),[2.1491]→[5.5885:5930](∅→∅),[5.5885]→[5.5885:5930](∅→∅)
data QDB m a = QDB{ recordEvent :: LogEntry -> ReaderT a m (), readWorkIndex :: ReaderT a m WorkIndex, newAuction :: Auction -> ReaderT a m AuctionId, readAuction :: AuctionId -> ReaderT a m (Maybe Auction), recordBid :: AuctionId -> Bid -> ReaderT a m (), readBids :: AuctionId -> ReaderT a m [Bid], createUser :: User -> ReaderT a m UserIddata QDBUser = QDBUser{ _userId :: UserId, _user :: User}makeLenses ''QDBUserdata QDB m = QDB{ recordEvent :: UserId -> LogEntry -> m (), readWorkIndex :: m WorkIndex, newAuction :: Auction -> m AuctionId, readAuction :: AuctionId -> m (Maybe Auction), recordBid :: AuctionId -> Bid -> m (), readBids :: AuctionId -> m [Bid], createUser :: User -> m UserId, findUser :: UserId -> m (Maybe User), findUserByHandle :: Handle -> m (Maybe QDBUser) - edit in lib/Quixotic/Users.hs at line 11
makePrisms ''UserId - edit in lib/Quixotic/Users.hs at line 13
newtype UserName = UserName Text deriving (Show, Eq)makePrisms ''UserName - replacement in lib/Quixotic/Users.hs at line 17
{ _userAddress :: BtcAddr{ _username :: UserName, _userAddress :: BtcAddr - edit in lib/Quixotic/Users.hs at line 21
- edit in lib/Quixotic/Users.hs at line 22[2.1670]
- edit in lib/Quixotic.hs at line 9
import Data.Group - edit in lib/Quixotic.hs at line 12[5.1198]→[5.2661:2662](∅→∅),[5.1932]→[5.2661:2662](∅→∅),[5.2661]→[5.2661:2662](∅→∅),[5.2662]→[5.4901:4967](∅→∅),[5.1272]→[5.1996:2013](∅→∅),[5.4967]→[5.1996:2013](∅→∅),[5.1996]→[5.1996:2013](∅→∅),[5.2013]→[5.1354:1384](∅→∅),[5.1354]→[5.1354:1384](∅→∅),[5.1384]→[5.4968:5009](∅→∅),[5.2053]→[5.1427:1514](∅→∅),[5.5009]→[5.1427:1514](∅→∅),[5.1427]→[5.1427:1514](∅→∅),[5.1514]→[5.5010:5046](∅→∅),[5.2091]→[5.1553:1581](∅→∅),[5.5046]→[5.1553:1581](∅→∅),[5.1553]→[5.1553:1581](∅→∅)
newtype BTC = BTC { _satoshis :: Int64 } deriving (Show, Eq, Ord)makeLenses ''BTCinstance Semigroup BTC where(<>) (BTC b1) (BTC b2) = BTC $ b1 + b2instance Monoid BTC wheremempty = BTC 0mappend = (<>)instance Group BTC whereinvert (BTC b) = BTC . negate $ binstance Abelian BTC where - edit in quixotic.cabal at line 23
Quixotic.Database.PostgreSQL - edit in quixotic.cabal at line 40
, postgresql-simple >= 0.4.9 && < 0.5, blaze-builder - edit in quixotic.cabal at line 88[31.3216][5.443]
, postgresql-simple, snap >= 0.13 && < 0.14 - edit in quixotic.cabal at line 92[5.545][31.3217]
, snaplet-postgresql-simple >= 0.6, resource-pool-catchio - replacement in server/Main.hs at line 2
{-# LANGUAGE RecordWildCards #-}{-# LANGUAGE RecordWildCards, TemplateHaskell #-} - edit in server/Main.hs at line 9
import Control.Lens.TH - replacement in server/Main.hs at line 15
import Database.SQLite.Simpleimport Data.Poolimport Database.PostgreSQL.Simple - replacement in server/Main.hs at line 20
import Quixotic.Database.SQLiteimport Quixotic.Database.PostgreSQL - edit in server/Main.hs at line 27
import Snap.Snapletimport Snap.Snaplet.Authimport Snap.Snaplet.Auth.Backends.PostgresqlSimpleimport Snap.Snaplet.PostgresqlSimpleimport Snap.Snaplet.Sessionimport Snap.Snaplet.Session.Backends.CookieSession - edit in server/Main.hs at line 34[5.5493]→[5.1478:1492](∅→∅),[5.1492]→[5.1634:1644](∅→∅),[5.1644]→[5.219:255](∅→∅),[5.255]→[5.257:284](∅→∅),[5.189]→[5.257:284](∅→∅),[5.284]→[5.256:339](∅→∅),[5.246]→[5.2594:2595](∅→∅),[5.304]→[5.2594:2595](∅→∅),[5.334]→[5.2594:2595](∅→∅),[5.339]→[5.2594:2595](∅→∅),[5.1673]→[5.2594:2595](∅→∅),[5.6332]→[5.2594:2595](∅→∅),[5.1492]→[5.2594:2595](∅→∅),[5.2595]→[5.207:245](∅→∅),[5.245]→[5.335:353](∅→∅),[5.353]→[4.2:210](∅→∅),[4.210]→[5.6540:6545](∅→∅),[5.504]→[5.6540:6545](∅→∅),[5.6540]→[5.6540:6545](∅→∅),[5.6545]→[5.1931:1932](∅→∅),[5.1931]→[5.1931:1932](∅→∅)
main :: IO ()main = docfg <- loadQConfig "quixotic.cfg"db <- open $ dbName cfgsconf <- snapConfig cfgsimpleHttpServe sconf $ runReaderT (site sqliteQDB) dbsite :: QDB IO a -> ReaderT a Snap ()site qdb = route[ ("logStart/:btcAddr", logWorkHandler qdb StartWork), ("logEnd/:btcAddr", logWorkHandler qdb StopWork), ("loggedIntervals/:btcAddr", loggedIntervalsHandler qdb), ("payouts", payoutsHandler qdb)] - edit in server/Main.hs at line 37
, authSiteKey :: FilePath, cookieTimeout :: Maybe Int - replacement in server/Main.hs at line 41
, dbName :: String-- , dbName :: String - edit in server/Main.hs at line 43
type PQDB = QDB (ReaderT Connection IO)data App = App{ _qdb :: Snaplet PQDB, _sess :: Snaplet SessionManager, _db :: Snaplet Postgres, _auth :: Snaplet (AuthManager App)}makeLenses ''App - edit in server/Main.hs at line 54
main :: IO ()main = docfg <- loadQConfig "quixotic.cfg"sconf <- snapConfig cfg--simpleHttpServe sconf $ runReaderT (site sqliteQDB) dbserveSnaplet sconf $ appInit cfgappInit :: QConfig -> SnapletInit App AppappInit QConfig{..} = makeSnaplet "quixotic" "Quixotic Time Tracker" Nothing $ doqdbs <- nestSnaplet "qdb" qdb qdbpgSnapletInitsesss <- nestSnaplet "sessions" sess $initCookieSessionManager (fpToString authSiteKey) "quookie" cookieTimeoutpgs <- nestSnaplet "db" db pgsInitauths <- nestSnaplet "auth" auth $ initPostgresAuth sess pgsaddRoutes [ ("logStart/:btcAddr", logWorkHandler StartWork), ("logEnd/:btcAddr", logWorkHandler StopWork)-- , ("loggedIntervals/:btcAddr", loggedIntervalsHandler qdb)-- , ("payouts", payoutsHandler qdb)]return $ App qdbs sesss pgs auths - edit in server/Main.hs at line 84
<*> (fmap fpFromText $ C.require cfg "siteKey")<*> C.lookup cfg "cookieTimeout" - replacement in server/Main.hs at line 88
<*> C.require cfg "db"-- <*> C.require cfg "db" - replacement in server/Main.hs at line 98
snapConfig :: QConfig -> IO (SC.Config Snap ())snapConfig :: QConfig -> IO (SC.Config Snap a) - replacement in server/Main.hs at line 101[5.5696]→[4.211:300](∅→∅),[4.300]→[5.649:669](∅→∅),[5.649]→[5.649:669](∅→∅),[5.669]→[5.6623:6664](∅→∅),[5.620]→[5.6623:6664](∅→∅),[5.6664]→[5.670:884](∅→∅)
logWorkHandler :: QDB IO a -> EventType -> ReaderT a Snap ()logWorkHandler qdb ev = dolet QDB{..} = qdbaddrBytes <- lift $ getParam "btcAddr"timestamp <- lift $ liftIO getCurrentTimemaybe(lift $ snapError 400 "")(\a -> mapReaderT liftIO $ recordEvent (LogEntry a (WorkEvent ev timestamp)))(fmap decodeUtf8 addrBytes >>= parseBtcAddr)qdbpgSnapletInit :: SnapletInit a PQDBqdbpgSnapletInit = makeSnaplet "qdbpg" "QDB on Postgresql" Nothing $ doreturn postgresQDB - replacement in server/Main.hs at line 105
loggedIntervalsHandler :: QDB IO a -> ReaderT a Snap ()loggedIntervalsHandler qdb = dolet QDB{..} = qdbwidx <- mapReaderT liftIO $ readWorkIndexlift . modifyResponse $ addHeader "content-type" "application/json"lift . writeLBS . A.encode $ mapKeys (^. address) widxlogWorkHandler :: EventType -> Handler App App ()logWorkHandler evType = doQDB{..} <- with qdb memptypg <- with db getPostgresStateauthedUser <- with auth currentUserqUid <-addrBytes <- getParam "btcAddr"timestamp <- liftIO getCurrentTimelet workEvent = WorkEvent evType timestampbtcAddr = fmap decodeUtf8 addrBytes >>= parseBtcAddrstoreEv uid addr = runReaderT . recordEvent uid $ LogEntry addr workEventmaybe (snapError 400 "") (liftPG . storeEv) btcAddr - replacement in server/Main.hs at line 118[5.982]→[4.390:463](∅→∅),[4.463]→[5.910:985](∅→∅),[5.910]→[5.910:985](∅→∅),[5.759]→[5.986:1072](∅→∅),[5.1072]→[3.412:482](∅→∅),[3.482]→[5.1072:1144](∅→∅),[5.1072]→[5.1072:1144](∅→∅)
payoutsHandler :: QDB IO a -> ReaderT a Snap ()payoutsHandler qdb = dolet QDB{..} = qdbdep = linearDepreciation (Months 6) (Months 60)ptime <- lift . liftIO $ getCurrentTimewidx <- mapReaderT liftIO $ readWorkIndexlift . modifyResponse $ addHeader "content-type" "application/json"lift . writeLBS . A.encode . PayoutsResponse $ payouts dep ptime widx--loggedIntervalsHandler :: QDB IO a -> ReaderT a Snap ()--loggedIntervalsHandler qdb = do-- let QDB{..} = qdb-- widx <- mapReaderT liftIO $ readWorkIndex-- lift . modifyResponse $ addHeader "content-type" "application/json"-- lift . writeLBS . A.encode $ mapKeys (^. address) widx----payoutsHandler :: QDB IO a -> ReaderT a Snap ()--payoutsHandler qdb = do-- let QDB{..} = qdb-- dep = linearDepreciation (Months 6) (Months 60)-- ptime <- lift . liftIO $ getCurrentTime-- widx <- mapReaderT liftIO $ readWorkIndex-- lift . modifyResponse $ addHeader "content-type" "application/json"-- lift . writeLBS . A.encode . PayoutsResponse $ payouts dep ptime widx