Skip to content

Commit a57a543

Browse files
committed
initial commit
1 parent b9b9ef6 commit a57a543

25 files changed

+2262
-0
lines changed

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Description
2+
A FFI-like bindings for PostgreSQL strored functions.
3+
4+
`postgresql-simple-bind` is an extension for `postgresql-simple`
5+
library that faciliates and automates bindings creation. This is
6+
especially useful in a design pattern where an application
7+
communicates with a database via API hiding the internal structure
8+
of the latter.
9+
10+
# Example
11+
Suppose we have the following functions in our database:
12+
13+
```
14+
function add_num(p_x bigint) returns void
15+
function get_all_nums() returns setof bigint
16+
```
17+
18+
In order to use them in a haskell application we write the following code:
19+
20+
```
21+
import Database.PostgreSQL.Simple.Bind (bindFunction, defaultOptions)
22+
import Database.PostgreSQL.Simple.Bind.Types()
23+
24+
bindFunction defaultOptions "function add_num(p_x bigint) returns void"
25+
bindFunction defaultOptions "function get_all_nums() returns setof bigint"
26+
```
27+
28+
That's it. Now we can stick them wherever we want to:
29+
```
30+
add_many_nums :: Connection -> [Int] -> IO ()
31+
add_many_nums conn xs = sequence_ $ map (add_num conn) xs
32+
33+
get_sum :: Connection -> IO Int
34+
get_sum conn = sum <$> (get_all_nums conn)
35+
```
36+
37+
# Behind the scenes
38+
It worth to mention that type translation from PostrgeSQL language to haskell
39+
is two-step. Firstly, a PostgreSQL type mapped to `PostgresType` instance and
40+
then this type family provides us the final type.
41+
For example `add_num` is translated the following way:
42+
43+
```
44+
-- original PostgreSQL declaration
45+
function add_num(p_x bigint) returns void
46+
47+
-- first step
48+
add_num :: ( PostgresType "bigint" ~ a, ToField a
49+
, PostgresType "void" ~ b, FromField b) => Connection -> a -> IO b
50+
51+
-- second step
52+
add_num :: Connection -> Int -> IO ()
53+
```
54+
55+
where
56+
```
57+
type instance PostgresType "bigint" = Int
58+
type instance PostgresType "void" = ()
59+
```
60+
as they are defined in `Database.PostgreSQL.Simple.Bind.Types`.
61+
62+
What if the provided instances give us unwanted types (e.g. `varchar` is
63+
mapped to `Text` while we want `String`)? This is why all the instances are
64+
defined into a separated module. We just do not import the module and define
65+
our own instances.
66+
67+
68+
# On types
69+
As we mentioned in the previous section there are certain restrictions on the
70+
types that can be used in `PostgresType` instances.
71+
72+
One of them comes naturally: all argument and result types must be instances of
73+
`ToField` and `FromField` respectively.
74+
75+
In case there is an argument with a default value, it's corresponding type
76+
will be wrapped into `Maybe` constructor.
77+
78+
Complex types cannot be specified unless there are corresponding `FromRow`
79+
and/or `ToRow` instances. This means there is no support for `record` return
80+
type as it doesn't disclose any information on it's structure.
81+
82+
Another caveat is about functions returning tables (or sets of composite
83+
types). There is no way to put `not null` constraint on the resulting columns,
84+
so such function can return result with `null` in any column. At the current
85+
moment this behaviour is not supported, so each function returning table is
86+
supposed to return non-`null`-values.
87+
88+
89+
# Customization
90+
There are not so much ways to change behaviour of `bindFunction` (yet).
91+
In the most cases the only required tweak is renaming stored functions.
92+
This can be done by specifying `nameModifier` and `schemaModifier` options.
93+
For example if database and application code adhere snake case and camel case
94+
naming conventions respectively, conversion can be made like this:
95+
96+
```
97+
import Text.CaseConversion
98+
import Database.PostgreSQL.Simple.Bind (Options(..), defaultOptions)
99+
100+
bindOptions :: Options
101+
bindOptions = defaultOptions {
102+
nameModifier = convertCase Snake Camel
103+
}
104+
```
105+
106+
# Automated generation
107+
It can be tedious to manually maintain consistent function declarations
108+
across the codebase. More convenient way is to automatically generate module
109+
during the compilation time. In case of cabal it can be done by using
110+
preBuild hook: set `build-type` to `Custom` in .cabal-file and define
111+
`main` in Setup.hs like this
112+
113+
```
114+
import Database.PostgreSQL.Simple.Bind.Util (generateBindingsModule)
115+
116+
main :: IO ()
117+
main = defaultMainWithHooks $ simpleUserHooks { preBuild = mkBindings }
118+
119+
mkBindings :: Args -> BuildFlags -> IO HookedBuildInfo
120+
mkBindings args buildFlags = do
121+
conn <- connect connectInfo
122+
(generateBindingsModule conn
123+
"Database.PostgreSQL.Simple.Bind.defaultOptions" "Bindings" [
124+
"stored_function_1"
125+
, "stored_function_2"
126+
-- ...
127+
]) >>= (writeFile "./src/Bindings.hs")
128+
close conn
129+
return emptyHookedBuildInfo
130+
```
131+
132+
Every time the build procedure is executed, there will be database
133+
lookup for function signatures.
134+
135+
136+

Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

default.nix

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{ mkDerivation, attoparsec, base, bytestring, case-conversion
2+
, heredoc, HUnit, postgresql-simple, stdenv, template-haskell, text
3+
, time
4+
}:
5+
mkDerivation {
6+
pname = "postgresql-simple-bind";
7+
version = "0.1.0.0";
8+
src = ./.;
9+
libraryHaskellDepends = [
10+
attoparsec base bytestring heredoc postgresql-simple
11+
template-haskell text time
12+
];
13+
testHaskellDepends = [
14+
base bytestring case-conversion HUnit postgresql-simple text
15+
];
16+
description = "A FFI-like bindings to stored functions";
17+
license = stdenv.lib.licenses.gpl3;
18+
}

examples/Common.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module Common (
2+
TestEnv(..)
3+
, mkTest
4+
, include
5+
, bindOptions
6+
) where
7+
8+
import Test.HUnit
9+
import Control.Exception.Base
10+
import Text.CaseConversion
11+
import Database.PostgreSQL.Simple
12+
import Database.PostgreSQL.Simple.Bind (Options(..), defaultOptions)
13+
import Database.PostgreSQL.Simple.Types
14+
import qualified Data.ByteString.Char8 as BS
15+
16+
17+
data TestEnv = TestEnv {
18+
connectInfo :: ConnectInfo
19+
}
20+
21+
bindOptions :: Options
22+
bindOptions = defaultOptions {
23+
nameModifier = convertCase Snake Camel . ("sql_" ++)
24+
}
25+
26+
withConn :: ConnectInfo -> (Connection -> IO a) -> IO a
27+
withConn connectInfo = bracket (connect connectInfo) close
28+
29+
mkTest :: (Connection -> IO ()) -> (Connection -> IO()) -> TestEnv -> Test
30+
mkTest setup run env = TestCase $ withConn (connectInfo env)
31+
(\conn -> mapM_ ($ conn) [begin, setup, run, rollback])
32+
33+
include :: Connection -> String -> IO ()
34+
include conn fn = readFile fn >>= (execute_ conn . Query . BS.pack) >> return ()

examples/ExMessages.hs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE TemplateHaskell #-}
4+
{-# LANGUAGE TypeFamilies #-}
5+
6+
-- Legend:
7+
-- API for reading and sending messages.
8+
-- We apply database patches that modify internal structure yet do not
9+
-- break the API.
10+
--
11+
-- This is a demonstration of the pattern where the database and the
12+
-- application communicate via API. All three times we run the same test
13+
-- against different editions of the database.
14+
15+
16+
module ExMessages (
17+
messages
18+
) where
19+
20+
import Test.HUnit
21+
import Database.PostgreSQL.Simple.Bind (bindFunction)
22+
import Database.PostgreSQL.Simple.Bind.Types()
23+
import Database.PostgreSQL.Simple (Connection)
24+
import Prelude hiding (getContents)
25+
import Common (bindOptions, TestEnv, mkTest, include)
26+
27+
concat <$> mapM (bindFunction bindOptions) [
28+
"function send_message(p_receiver varchar, p_contents varchar) returns bigint"
29+
, "function get_new_messages(p_receiver varchar) returns table (message_id bigint, sender varchar, contents varchar)"
30+
, "function mark_as_read(p_receiver varchar, p_message_id bigint) returns void"
31+
]
32+
33+
34+
runTests :: Int -> Connection -> IO ()
35+
runTests n conn = do
36+
let getId (x, _, _) = x
37+
38+
msg1 <- sqlSendMessage conn "mr_foo" "hello!"
39+
msg2 <- sqlSendMessage conn "mr_bar" "hello!"
40+
msg3 <- sqlSendMessage conn "mr_bar" "hello again!"
41+
42+
sqlGetNewMessages conn "mr_foo" >>= \xs ->
43+
assertEqual ("check get_new_messages " ++ (show n) ++ ".1") [msg1] (map getId xs)
44+
45+
sqlGetNewMessages conn "mr_bar" >>= \xs ->
46+
assertEqual ("check get_new_messages " ++ (show n) ++ ".2") [msg2, msg3] (map getId xs)
47+
48+
sqlMarkAsRead conn "mr_bar" msg2
49+
50+
sqlGetNewMessages conn "mr_bar" >>= \xs ->
51+
assertEqual ("check get_new_messages " ++ (show n) ++ ".3") [msg3] (map getId xs)
52+
53+
sqlMarkAsRead conn "mr_foo" msg1
54+
sqlMarkAsRead conn "mr_bar" msg3
55+
56+
57+
messages :: TestEnv -> Test
58+
messages = mkTest (flip include "./examples/sql/messages.sql")
59+
(\conn -> do
60+
runTests 1 conn
61+
include conn "./examples/sql/messages-patch-1.sql"
62+
runTests 2 conn
63+
include conn "./examples/sql/messages-patch-2.sql"
64+
runTests 3 conn)
65+

examples/ExNumDumpster.hs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE TemplateHaskell #-}
4+
{-# LANGUAGE TypeFamilies #-}
5+
{-# LANGUAGE ExistentialQuantification #-}
6+
7+
-- Legend:
8+
-- NumDumpster is a collection of numbers stored in a database.
9+
-- API provides some basic functions to manipulate the collection.
10+
--
11+
-- This is an example of using basic features of the library
12+
13+
14+
module ExNumDumpster (
15+
numDumpster
16+
) where
17+
18+
import Test.HUnit
19+
import Database.PostgreSQL.Simple
20+
21+
import Database.PostgreSQL.Simple.Bind (bindFunction)
22+
import Database.PostgreSQL.Simple.Bind.Types()
23+
24+
import Common (bindOptions, TestEnv, mkTest, include)
25+
26+
27+
concat <$> mapM (bindFunction bindOptions) [
28+
"function add_num(p_x bigint) returns void"
29+
, "function get_last_num() returns bigint"
30+
, "function get_range(p_range_min bigint default null, p_range_max bigint default null) returns setof bigint"
31+
, "function get_all_nums() returns setof bigint"
32+
, "function clear() returns void"
33+
]
34+
35+
36+
addManyNums :: Connection -> [Int] -> IO ()
37+
addManyNums conn xs = sequence_ $ map (sqlAddNum conn) xs
38+
39+
getSum :: Connection -> IO Int
40+
getSum conn = sum <$> (sqlGetAllNums conn)
41+
42+
iterFib :: Connection -> IO Int
43+
iterFib conn = do
44+
x <- sqlGetLastNum conn
45+
x' <- getSum conn
46+
sqlClear conn
47+
addManyNums conn [x, x']
48+
return x'
49+
50+
51+
numDumpster :: TestEnv -> Test
52+
numDumpster = mkTest (flip include "./examples/sql/numdumpster.sql")
53+
(\conn -> do
54+
sqlAddNum conn 1
55+
sqlGetLastNum conn >>= \x -> assertEqual "check get_last_num" 1 x
56+
57+
sqlClear conn
58+
addManyNums conn [1, 2, 3, 4]
59+
sqlGetAllNums conn >>= \xs -> assertEqual "check get_all_nums" [1, 2, 3, 4] xs
60+
61+
sqlGetRange conn Nothing Nothing >>= \xs -> assertEqual "check get_range" [1, 2, 3, 4] xs
62+
sqlGetRange conn (Just 2) (Just 3) >>= \xs -> assertEqual "check get_range" [2, 3] xs
63+
sqlGetRange conn Nothing (Just 3) >>= \xs -> assertEqual "check get_range" [1, 2, 3] xs
64+
sqlGetRange conn (Just 2) Nothing >>= \xs -> assertEqual "check get_range" [2, 3, 4] xs
65+
66+
sqlClear conn
67+
addManyNums conn [0, 1]
68+
((head . reverse) <$> (sequence $ replicate 11 (iterFib conn))) >>=
69+
\x -> assertEqual "check 11th fibonacci number" 144 x)
70+

0 commit comments

Comments
 (0)