-
Notifications
You must be signed in to change notification settings - Fork 8
/
dataimporter.hs
149 lines (136 loc) · 4.76 KB
/
dataimporter.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Csv as Csv
import Data.Int (Int64)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Vector (Vector)
import qualified Database.PostgreSQL.Simple as Psql
import GHC.Generics (Generic)
import Options.Applicative (Parser, ParserInfo, argument, auto,
execParser, fullDesc, header, help,
helper, info, long, metavar,
option, progDesc, str, strOption,
(<*>), (<>))
import System.Exit (exitFailure, exitSuccess)
import System.IO (hFlush, hSetEcho, stdin, stdout)
-- structure to hold cli arguments
data Options = Options
{ inputFilename :: String
, dbHost :: String
, dbPort :: Int
, dbName :: String
, dbUser :: String
}
-- Parser for cli arguments
optsParser :: Parser Options
optsParser = Options
<$> argument str
( metavar "FILE"
<> help "input file"
)
<*> strOption
( long "dbhost"
<> metavar "HOSTNAME"
<> help "The hostname of the database"
)
<*> option auto
( long "dbport"
<> metavar "NUMBER"
<> help "The port of the database"
)
<*> strOption
( long "dbname"
<> metavar "NAME"
<> help "The name of the database"
)
<*> strOption
( long "dbuser"
<> metavar "USERNAME"
<> help "The user of the database"
)
-- Adding program help text to the parser
optsParserInfo :: ParserInfo Options
optsParserInfo = info (helper <*> optsParser)
( fullDesc
<> progDesc "A csv importer"
<> header "dataimporter - a csv importer"
)
-- data importer
main :: IO ()
main = do
-- run the options parser over the cli arguments
opts <- execParser optsParserInfo
-- read in the users password
dbPass <- getPassword
-- read the input files contents
fileContents <- BSL.readFile $ inputFilename opts
-- parse the file contents
let csvContents = decodeCsv fileContents
-- deal with the csv failing to parse
case csvContents of Left e -> do
-- if it failed, exit like a dirty script
putStrLn e
exitFailure
Right (_, saleRecords) -> do
-- connect to the database
conn <- makeConnection opts dbPass
-- insert the sale records into the db
_ <- insertSaleRecords conn saleRecords
exitSuccess
-- capture the password with a blind input
getPassword :: IO String
getPassword = do
-- output some prompt text
putStr "password: "
-- flush stdout to make sure the prompt appears
-- normally only flushes on new lines or buffer being filled
hFlush stdout
-- turn off stdin echoing
hSetEcho stdin False
-- capture 1 line of input, the password
dbPass <- getLine
-- turn stdin echoing back on
hSetEcho stdin True
-- print a new line to give the user some feedback
putStrLn ""
return dbPass
-- data type representing our csv rows
data SaleRecord = SaleRecord
{ item :: Text
, quantity :: Int
, price :: Double
} deriving (Generic)
-- FromNamedRecord lets cassava read a row matching the column titles to
-- record element names
instance Csv.FromNamedRecord SaleRecord
-- decode the csv files contents
decodeCsv :: BSL.ByteString -> Either String (Csv.Header, Vector SaleRecord)
-- decodeByName uses the column headers and FromNamedRecord instance to create
-- the SaleRecords
decodeCsv fileContents = Csv.decodeByName fileContents
-- creates a ConnectInfo from our options and then connects to the db with it
makeConnection :: Options -> String -> IO Psql.Connection
makeConnection opts pass = Psql.connect $ Psql.ConnectInfo
(dbHost opts)
(fromIntegral $ dbPort opts)
(dbUser opts)
pass
(dbName opts)
insertSaleRecords :: Psql.Connection -> Vector SaleRecord -> IO (Vector Int64)
insertSaleRecords conn srs = do
-- run inside of a transaction
rets <- Psql.withTransaction conn $
-- runs an insert statement for each SaleRecord
mapM (insertSaleRecord conn) srs
return rets
insertSaleRecord :: Psql.Connection -> SaleRecord -> IO Int64
insertSaleRecord conn sr =
-- execute a raw sql statement with a list of parameters
Psql.execute conn "insert into sales (item, quantity, price) values (?, ?, ?)"
[ item sr
, T.pack . show $ quantity sr
, T.pack . show $ price sr
]