{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import Control.Exception
import Data.Int
import Data.Text (Text)
import qualified Database.Postgres.Temp as Tmp
import GHC.Generics (Generic)
import qualified Hasql.Connection as Hasql
import Hasql.Decoders (column)
import Hasql.Interpolate
import Hasql.Interpolate.Internal.TH
import qualified Hasql.Session as Hasql
import Language.Haskell.TH
import Test.Tasty
import Test.Tasty.HUnit
import Prelude
main :: IO ()
main = defaultMain tests
tests :: TestTree
tests =
testGroup
"Tests"
[ parserTests,
withResource (either (error . show) pure =<< Tmp.startConfig Tmp.defaultConfig) Tmp.stop executionTests
]
parserTests :: TestTree
parserTests =
testGroup
"parser"
[ testCase "quote" testParseQuotes,
testCase "comment" testParseComment,
testCase "param" testParseParam
]
executionTests :: IO Tmp.DB -> TestTree
executionTests getDb =
testGroup
"execution"
( ($ getDb)
<$> [ testCase "basic" . testBasic,
testCase "composite test" . testComposite,
testCase "row" . testRow,
testCase "row generic" . testRowGeneric,
testCase "snippet" . testSnippet
]
)
testParseQuotes :: IO ()
testParseQuotes = do
let expected = SqlExpr expectedSqlExpr [] [] 0
expectedSqlExpr =
[ Sbe'Quote "#{bonk}",
Sbe'Sql " ",
Sbe'Quote "^{z''onk}",
Sbe'Sql " ",
Sbe'Ident "#{k\"\"onk}",
Sbe'Sql " ",
Sbe'DollarQuote "tag" "#{kiplonk}",
Sbe'Sql " ",
Sbe'Cquote "newline \\n escaped \\'string\\'"
]
parseSqlExpr "'#{bonk}' '^{z''onk}' \"#{k\"\"onk}\" $tag$#{kiplonk}$tag$ E'newline \\n escaped \\'string\\''" @?= Right expected
testParseComment :: IO ()
testParseComment = do
let expected = SqlExpr expectedSqlExpr [] [] 0
expectedSqlExpr =
[ Sbe'Sql "content ",
Sbe'Sql "\nhello ",
Sbe'Sql " world\n",
Sbe'Sql " end\n"
]
inputStr =
unlines
[ "content -- trailing comment",
"hello /* / comment * */ world",
"/* comment",
"blerg /* nested comment */",
"*/ end"
]
parseSqlExpr inputStr @?= Right expected
testParseParam :: IO ()
testParseParam = do
let expected =
SqlExpr
[Sbe'Param, Sbe'Sql " ", Sbe'Param]
[Pe'Exp (VarE (mkName "x")), Pe'Exp (LitE (IntegerL 2))]
[]
0
parseSqlExpr "#{x} #{2}" @?= Right expected
testBasic :: IO Tmp.DB -> IO ()
testBasic getDb = do
withLocalTransaction getDb \conn -> do
let relation :: [(Int64, Bool, Int64)]
relation =
[ (0, True, 5),
(1, True, 6),
(2, False, 7)
]
createRes <- run conn [sql| create table hasql_interpolate_test(x int8, y boolean, z int8) |]
createRes @?= ()
RowsAffected insertRes <- run conn [sql| insert into hasql_interpolate_test (x,y,z) select * from ^{toTable relation} |]
insertRes @?= 3
selectRes <- run conn [sql| select x, y, z from hasql_interpolate_test where x > #{0 :: Int64} order by x |]
selectRes @?= filter (\(x, _, _) -> x > 0) relation
testComposite :: IO Tmp.DB -> IO ()
testComposite getDb = do
withLocalTransaction getDb \conn -> do
let expected = [Point 0 0, Point 1 1]
res <- run conn [sql| select * from (values (row(0,0)), (row(1,1)) ) as t |]
res @?= map OneColumn expected
data T = T Int64 Bool Text deriving stock (Eq, Show)
instance DecodeRow T where
decodeRow =
T
<$> column decodeField
<*> column decodeField
<*> column decodeField
testRow :: IO Tmp.DB -> IO ()
testRow getDb = do
withLocalTransaction getDb \conn -> do
let expected = [T 0 True "foo", T 1 False "bar"]
res <- run conn [sql| select * from (values (0,true,'foo'), (1,false,'bar') ) as t |]
res @?= expected
testRowGeneric :: IO Tmp.DB -> IO ()
testRowGeneric getDb = do
withLocalTransaction getDb \conn -> do
let expected = [Point 0 0, Point 1 1]
res <- run conn [sql| select * from (values (0,0), (1,1) ) as t |]
res @?= expected
testSnippet :: IO Tmp.DB -> IO ()
testSnippet getDb = do
withLocalTransaction getDb \conn -> do
let expected = [Point 0 0]
let snippet = [sql| t.y = 0 |]
let xVal :: Int64 = 0
res <- run conn [sql| select * from (values (0,0), (1,1) ) as t(x,y) where t.x = #{xVal} and ^{snippet} |]
res @?= expected
withLocalTransaction :: IO Tmp.DB -> (Hasql.Connection -> IO a) -> IO a
withLocalTransaction getDb k =
getDb >>= \db -> bracket (either (fail . show) pure =<< Hasql.acquire (Tmp.toConnectionString db)) Hasql.release \conn -> do
let beginTrans = do
Hasql.run (Hasql.statement () (interp False [sql| begin |])) conn >>= \case
Left err -> fail (show err)
Right () -> pure ()
rollbackTrans = do
Hasql.run (Hasql.statement () (interp False [sql| rollback |])) conn >>= \case
Left err -> fail (show err)
Right () -> pure ()
bracket beginTrans (\() -> rollbackTrans) \() -> k conn
run :: DecodeResult a => Hasql.Connection -> Sql -> IO a
run conn stmt = do
Hasql.run (Hasql.statement () (interp False stmt)) conn >>= \case
Left err -> assertFailure ("Hasql statement unexpectedly failed with error: " <> show err)
Right x -> pure x
data Point = Point Int64 Int64
deriving stock (Generic, Eq, Show)
deriving (DecodeValue) via CompositeValue Point
deriving anyclass (DecodeRow)