Use Haskell to Access Two Popular Databases

0
58
haskell
Haskell, named after logician Haskell Curry, is a standardised, general-purpose, purely functional programming language, with non-strict semantics and strong static typing. In this ninth article on Haskell, we shall cover access to Sqlite and MySQL databases using Haskell modules

A number of packages are available from https://hackage.haskell.org/packages/#cat:Database, but, I will illustrate a few of them with examples. You first need to install the cabal-install tool on Fedora, for example, using the following command:

$ sudo yum install cabal-install

You can then install HDBC.Sqlite3 using the
following command:

$ cabal install HDBC-sqlite3

This installs the latest 2.3.3.0 version from https://hackage.haskell.org/package/HDBCsqlite3. You can also install the Sqlite3 package on Fedora for testing, as follows:

$ sudo yum install sqlite

To initiate a connection to a database, you can test it out in the GHCi prompt using the connectSqlite3 function, as shown below:

$ ghci

GHCi, version 7. 6. 3: http://www. haskell. org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.

ghci> : module Database.HDBC Database.HDBC.Sqlite3
ghci> conn <- connectSqlite3 “students.db”
Loading package array-0. 4. 0. 1 ... linking ... done.
Loading package deepseq-1. 3. 0. 1 ... linking ... done.
Loading package old-locale-1. 0. 0. 5 ... linking ... done.
Loading package time-1. 4. 0. 1 ... linking ... done.
Loading package transformers-0. 3. 0. 0 ... linking ... done.
Loading package bytestring-0. 10. 0. 2 ... linking ... done.
Loading package text-0. 11. 3. 1 ... linking ... done.
Loading package old-time-1. 1. 0. 1 ... linking ... done.
Loading package containers-0. 5. 0. 0 ... linking ... done.
Loading package utf8-string-0. 3. 7 ... linking ... done.
Loading package mtl-2. 1. 2 ... linking ... done.
Loading package convertible-1. 1. 0. 0 ... linking ... done.
Loading package HDBC-2. 4. 0. 0 ... linking ... done.
Loading package HDBC-sqlite3-2. 3. 3. 0 ... linking ... done

The signature of the connectSqlite3 function is as follows:

ghci> : t connectSqlite3
connectSqlite3 :: FilePath -> IO Connection

The type of conn is a Connection.

ghci> : t conn
conn :: Connection

If you already have an existing Sqlite3 database, you can give the full path to the database and connect to it, or else you can create a table using the Sqlite CREATE TABLE syntax as shown below:

ghci> run conn “CREATE TABLE names (id INTEGER NOT NULL, fname VARCHAR(80), lname VARCHAR(80))” []
0

The type signature of run is as follows:

ghci> : t run
run
:: IConnection conn => conn -> String -> [SqlValue] -> IO Integer

It takes three arguments as input and performs an IO computation that returns an integer indicating the status of the execution. The first argument to run is the connection, the second argument is the Sqlite command to be executed, and finally there is the array of SqlValues that provide a mapping between Haskell values and SQL databases.
Both Haskell and SQL databases have types, and different databases may have different representations of the types. In order to provide a consistent mapping between the two, each HDBC driver implements the relation using SqlValue.
You can now insert a record into the database using the following command:

ghci> run conn “INSERT INTO names (id, fname, lname) VALUES(1, ’Edwin’, ’Brady’)” []
1
ghci> commit conn

The type signature of commit is given here:

ghci> : t commit
commit :: IConnection conn => conn -> IO ()

It takes a connection and completes the pending IO actions. To read the result from Haskell you can use the quickQuery function from the GHCi prompt, as follows:

ghci> quickQuery conn “SELECT * from names” []
[[SqlByteString “1”, SqlByteString “Edwin”, SqlByteString “Brady”]]

The type signature of the quickQuery function is as follows:

quickQuery
:: IConnection conn =>
conn -> String -> [SqlValue] -> IO [[SqlValue]]

You can also verify the result of the above actions using the Sqlite3 executable in the command prompt as illustrated below:

$ sqlite3 students.db
SQLite version 3.8.4.3 2014-04-03 16:53:12
Enter “.help” for usage hints.
sqlite> .schema
CREATE TABLE names (id INTEGER NOT NULL, fname VARCHAR(80), lname VARCHAR(80));
sqlite> select * from names;
1|Edwin|Brady

You can also do batch processing for inserts by preparing the statements and executing them:

ghci> batch <- prepare conn “INSERT INTO names VALUES (?, ?, ?)”
ghci> execute batch [toSql (2 :: Int), toSql “Simon”, toSql “Marlow”]
1
ghci> execute batch [toSql (3 :: Int), toSql “Ulf”, toSql “Norell”]
1
ghci> commit conn

The type signatures of the prepare and execute functions are given below:

ghci> : t prepare
prepare :: IConnection conn => conn -> String -> IO Statement
ghci> : t execute
execute :: Statement -> [SqlValue] -> IO Integer

You can once again check the records in the database using the quickQuery function:

ghci> quickQuery’ conn “SELECT * from names” []
[[SqlByteString “1”, SqlByteString “Edwin”, SqlByteString “Brady”],[SqlByteString “2”, SqlByteString “Simon”, SqlByteString “Marlow”],[SqlByteString “3”, SqlByteString “Ulf”, SqlByteString “Norell”]]

You can also run an update query to the database. For example:

ghci> run conn “UPDATE names set lname = ’Peyton Jones’ WHERE fname = ’Simon’” []
1
ghci> commit conn

Verifying the output from the Sqlite3 command prompt, you get:

sqlite> select * from names;
1|Edwin|Brady
2|Simon|Peyton Jones
3|Ulf|Norell

The HDBC driver provides many functions to retrieve information about the database and the drivers. A few examples are illustrated below:

ghci> hdbcDriverName conn
“sqlite3”
ghci> hdbcClientVer conn
“3.8.4.3”
ghci> dbTransactionSupport conn
True
ghci> getTables conn
[“names”]
ghci> describeTable conn “names”
[(“id”, SqlColDesc {colType = SqlIntegerT, colSize = Nothing, colOctetLength = Nothing, colDecDigits = Nothing, colNullable = Nothing}),(“fname”, SqlColDesc {colType = SqlUnknownT “varchar(80)”, colSize = Nothing, colOctetLength = Nothing, colDecDigits = Nothing, colNullable = Nothing}),(“lname”, SqlColDesc {colType = SqlUnknownT “varchar(80)”, colSize = Nothing, colOctetLength = Nothing, colDecDigits = Nothing, colNullable = Nothing})]

It is considered good practice to use handleSqlError before running any HDBC commands to catch errors that may arise during the database transactions. For example:

ghci> handleSqlError $ quickQuery conn “SELECT * from namesaaa” []
*** Exception: user error (SQL error: SqlError {seState = “”, seNativeError = 1, seErrorMsg = “prepare 23: SELECT * from namesaaa: no such table: namesaaa”})

To disconnect from the database, you can use the disconnect function provided by HDBC as shown below:

ghci> disconnect conn

Let’s now look at how to access a MySQL database using the mysql-simple package. You will also need to create or grant privileges for an existing user to use the MySQL database server. Please follow your GNU/Linux distribution manual on how to install and configure a MySQL server. On Fedora, for example, you must have a mysql and mysql-server installed:

$ sudo yum install community-mysql community-mysql-server

You can install the mysql-simple Haskell package using the following command:

$ cabal install mysql-simple

Create a test database using the mysql command line tool as shown below:

$ mysql -u user -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.5.38-log MySQL Community Server (GPL)
Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.
Type ’help;’ or ’\h’ for help. Type ’\c’ to clear the current input statement.
mysql> create database test;
Query OK, 1 row affected (0.03 sec)

Let us create a users’ table that has an ID as well as first name and last name fields, using the mysql-simple package, as follows:

{-# LANGUAGE OverloadedStrings #-}
import Database.MySQL.Simple
main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “user”
, connectPassword = “password”
, connectDatabase = “test”
}
execute conn “create table users (id INT, fname VARCHAR(80), lname VARCHAR(80))” ()
close conn

The OverloadedStrings extension allows string literals to be polymorphic for the IsString class. The defaultConnectInfo is of type ConnectInfo:

ghci> : t defaultConnectInfo
defaultConnectInfo :: ConnectInfo

ConnectInfo can take many parameters to describe the connectivity to the MySQL server. For example:

connectInfo :: ConnectInfo
connectInfo = ConnectInfo { connectHost = “localhost”,
connectPort = 3306,
connectUser = “user”,
connectPassword = “password”,
connectDatabase = “test”,
connectOptions = [],
connectPath = “”,
connectSSL = Nothing }

The above code to create a table can be compiled directly in GHCi and the main function can be executed as given below:

$ ghci create. hs
GHCi, version 7. 6. 3: http://www. haskell. org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( create. hs, interpreted )
Ok, modules loaded: Main.
ghci> main
Loading package array-0. 4. 0. 1 ... linking ... done.
Loading package deepseq-1. 3. 0. 1 ... linking ... done.
Loading package old-locale-1. 0. 0. 5 ... linking ... done.
Loading package time-1. 4. 0. 1 ... linking ... done.
Loading package bytestring-0. 10. 0. 2 ... linking ... done.
Loading package text-0. 11. 3. 1 ... linking ... done.
Loading package blaze-builder-0. 3. 1. 1 ...linking ... done.
Loading package primitive-0. 5. 0. 1 ... linking ... done.
Loading package vector-0. 10. 0. 1 ... linking ... done.
Loading package pcre-light-0. 4 ... linking ... done.
Loading package containers-0. 5. 0. 0 ... linking ... done.
Loading package attoparsec-0. 10. 4. 0 ... linking ... done.
Loading package blaze-textual-0. 2. 0. 8 ... linking... done.
Loading package base16-bytestring-0. 1. 1. 6 ...linking... done.
Loading package mysql-0. 1. 1. 7 ... linking ... done.
Loading package mysql-simple-0. 2. 2. 4 ... linking ... done.

You can check with the mysql command line utility for the created table:

mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> desc users;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | YES | | NULL | |
| fname | varchar(80) | YES | | NULL | |
| lname | varchar(80) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

You can now list the databases available in the MySQL server using the query_ function as illustrated below:

{-# LANGUAGE OverloadedStrings #-}
import Database.MySQL.Simple
main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “user”
, connectPassword = “password”
, connectDatabase = “test”
}
databases <- query_ conn “SHOW databases”
print (databases :: [Only String])
close conn

You can compile the above code directly with GHCi and execute the main function, as follows:

$ ghci show-databases. hs
GHCi, version 7. 6. 3: http://www. haskell. org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( show-databases. hs, interpreted )
Ok, modules loaded: Main.
ghci> main
Loading package array-0. 4. 0. 1 ... linking ... done.
Loading package deepseq-1. 3. 0. 1 ...linking... done.
[Only {fromOnly = “information_schema”}, Only {fromOnly = “mysql”}, Only {fromOnly = “performance_schema”}, Only {fromOnly = “test”}]

You can try inserting a record into the database using the execute function:

execute conn “insert into users (id, fname, lname) values (?, ?, ?)” [“1” :: String, “Edwin” :: String, “Brady” :: String]

After running the code, you can check the database entry using the mysql client program as shown below:

mysql> select * from users;
+------+-------+-------+
| id | fname | lname |
+------+-------+-------+
| 1 | Edwin | Brady |
+------+-------+-------+
1 row in set (0.00 sec)

You can also do batch inserts using the executeMany function. For example:

executeMany conn “insert into users (id, fname, lname) values (?, ?, ?)” [(“2” :: String, “Simon” :: String, “Marlow” :: String), (“3” :: String, “Ulf” :: String, “Norell” :: String)]

You can verify the execution of the code from the mysql utility:

mysql> select * from users;
+------+-------+--------+
| id | fname | lname |
+------+-------+--------+
| 1 | Edwin | Brady |
| 2 | Simon | Marlow |
| 3 | Ulf | Norell |
+------+-------+--------+
3 rows in set (0.01 sec)

You can change a record entry using the UPDATE MySQL command:

execute conn “update users SET lname = ’Peyton Jones’ where fname = ’Simon’” ()

Executing the code in GHCi, and checking the results with the mysql prompt gives the following changed output:

mysql> select * from users;
+------+-------+--------------+
| id | fname | lname |
+------+-------+--------------+
| 1 | Edwin | Brady |
| 2 | Simon | Peyton Jones |
| 3 | Ulf | Norell |
+------+-------+--------------+
3 rows in set (0.00 sec)

It is important to catch any exceptions that may arise on executing the database commands. Consider the following example, where the number of arguments passed does not match with what’s expected:

{-# LANGUAGE OverloadedStrings #-}
import Database.MySQL.Simple
import Control.Exception
import GHC.Int
main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “root”
, connectPassword = “si-adm1n#q”
, connectDatabase = “test”
}
result <- try (execute conn “insert into users (id, fname, lname) values (?, ?, ?)” [“4” :: String, “Laurel” :: String]) :: IO (Either SomeException Int64)
case result of
Left ex -> putStrLn $ “Caught exception: “ ++ show ex
Right val -> putStrLn $ “The answer was: “ ++ show val
close conn

The error is caught when the main function is executed inside GHCi:

ghci> main
Caught exception: FormatError {fmtMessage = “3 ’?’ characters, but 2 parameters”, fmtQuery = “insert into users (id, fname, lname) values (?, ?, ?)”, fmtParams = [“4”, “Laurel”]}

You can also map through the results returned from the database and use them for your needs. The following is an illustration of the same:

{-# LANGUAGE OverloadedStrings #-}

import Database.MySQL.Simple
import Control.Monad
import Data.Text as Text
main :: IO ()
main = do
conn <- connect defaultConnectInfo
{ connectUser = “user”
, connectPassword = “password”
, connectDatabase = “test”
}
users <- query_ conn “SELECT fname, lname FROM users”
forM_ users $ \(fname, lname) ->
putStrLn $ Text.unpack fname ++ “ “ ++ Text.unpack lname
close conn

The resultant output when executing the main function in GHCi is given below:

ghci> main

Edwin Brady
Simon Peyton Jones
Ulf Norell

You are encouraged to read Database.MySQL.Simple documentation from https://hackage.haskell.org/package/mysql-simple-0.2.2.4/docs/DatabaseMySQL-Simple.html for more information.

LEAVE A REPLY

Please enter your comment!
Please enter your name here