2015-11-10 21:19:41 +03:00
module Main where
2022-06-20 13:54:54 +03:00
import Contravariant.Extras
2024-04-19 07:38:30 +03:00
import Hasql.Decoders qualified as Decoders
import Hasql.Encoders qualified as Encoders
import Hasql.Session qualified as Session
import Hasql.Statement qualified as Statement
2024-04-20 13:59:11 +03:00
import Hasql.TestingUtils.TestingDsl qualified as Session
2024-04-19 07:38:30 +03:00
import Main.Connection qualified as Connection
2016-01-22 18:43:15 +03:00
import Main.Prelude hiding ( assert )
2024-04-19 07:38:30 +03:00
import Main.Statements qualified as Statements
2023-10-16 03:54:25 +03:00
import Test.QuickCheck.Instances ( )
2015-11-10 21:19:41 +03:00
import Test.Tasty
2015-12-06 07:06:31 +03:00
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
2022-06-20 13:54:54 +03:00
import Test.Tasty.Runners
2015-11-10 21:19:41 +03:00
2023-10-16 03:54:25 +03:00
main :: IO ()
2015-11-10 21:19:41 +03:00
main =
defaultMain tree
2023-10-16 03:54:25 +03:00
tree :: TestTree
2015-11-10 21:19:41 +03:00
tree =
2023-10-13 02:24:12 +03:00
localOption ( NumThreads 1 )
$ testGroup
2022-06-20 13:54:54 +03:00
" All tests "
2023-10-13 02:24:12 +03:00
[ testGroup " Roundtrips "
$ let roundtrip encoder decoder input =
let session =
let statement = Statement . Statement " select $1 " encoder decoder True
in Session . statement input statement
in unsafePerformIO $ do
x <- Connection . with ( Session . run session )
return ( Right ( Right input ) === x )
in [ testProperty " Array "
$ let encoder = Encoders . param ( Encoders . nonNullable ( Encoders . array ( Encoders . dimension foldl' ( Encoders . element ( Encoders . nonNullable Encoders . int8 ) ) ) ) )
decoder = Decoders . singleRow ( Decoders . column ( Decoders . nonNullable ( Decoders . array ( Decoders . dimension replicateM ( Decoders . element ( Decoders . nonNullable Decoders . int8 ) ) ) ) ) )
in roundtrip encoder decoder ,
testProperty " 2D Array "
$ let encoder = Encoders . param ( Encoders . nonNullable ( Encoders . array ( Encoders . dimension foldl' ( Encoders . dimension foldl' ( Encoders . element ( Encoders . nonNullable Encoders . int8 ) ) ) ) ) )
decoder = Decoders . singleRow ( Decoders . column ( Decoders . nonNullable ( Decoders . array ( Decoders . dimension replicateM ( Decoders . dimension replicateM ( Decoders . element ( Decoders . nonNullable Decoders . int8 ) ) ) ) ) ) )
in \ list -> list /= [] ==> roundtrip encoder decoder ( replicate 3 list )
] ,
testCase " Failed query "
$ let statement =
Statement . Statement " select true where 1 = any ($1) and $2 " encoder decoder True
where
encoder =
contrazip2
( Encoders . param ( Encoders . nonNullable ( Encoders . array ( Encoders . dimension foldl' ( Encoders . element ( Encoders . nonNullable Encoders . int8 ) ) ) ) ) )
( Encoders . param ( Encoders . nonNullable ( Encoders . text ) ) )
decoder =
fmap ( maybe False ( const True ) ) ( Decoders . rowMaybe ( ( Decoders . column . Decoders . nonNullable ) Decoders . bool ) )
session =
Session . statement ( [ 3 , 7 ] , " a " ) statement
in do
x <- Connection . with ( Session . run session )
assertBool ( show x ) $ case x of
Right ( Left ( Session . QueryError " select true where 1 = any ($1) and $2 " [ " [3, 7] " , " \ " a \ " " ] _ ) ) -> True
_ -> False ,
testCase " IN simulation "
$ let statement =
Statement . Statement " select true where 1 = any ($1) " encoder decoder True
where
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . array ( Encoders . dimension foldl' ( Encoders . element ( Encoders . nonNullable Encoders . int8 ) ) ) ) )
decoder =
fmap ( maybe False ( const True ) ) ( Decoders . rowMaybe ( ( Decoders . column . Decoders . nonNullable ) Decoders . bool ) )
session =
do
result1 <- Session . statement [ 1 , 2 ] statement
result2 <- Session . statement [ 2 , 3 ] statement
return ( result1 , result2 )
in do
x <- Connection . with ( Session . run session )
assertEqual ( show x ) ( Right ( Right ( True , False ) ) ) x ,
testCase " NOT IN simulation "
$ let statement =
Statement . Statement " select true where 3 <> all ($1) " encoder decoder True
where
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . array ( Encoders . dimension foldl' ( Encoders . element ( Encoders . nonNullable Encoders . int8 ) ) ) ) )
decoder =
fmap ( maybe False ( const True ) ) ( Decoders . rowMaybe ( ( Decoders . column . Decoders . nonNullable ) Decoders . bool ) )
session =
do
result1 <- Session . statement [ 1 , 2 ] statement
result2 <- Session . statement [ 2 , 3 ] statement
return ( result1 , result2 )
in do
x <- Connection . with ( Session . run session )
assertEqual ( show x ) ( Right ( Right ( True , False ) ) ) x ,
testCase " Composite decoding "
$ let statement =
2022-10-15 12:54:25 +03:00
Statement . Statement sql encoder decoder True
where
sql =
2023-10-13 02:24:12 +03:00
" select (1, true) "
2022-10-15 12:54:25 +03:00
encoder =
2023-10-13 02:24:12 +03:00
mempty
2022-10-15 12:54:25 +03:00
decoder =
2023-10-13 02:24:12 +03:00
Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . composite ( ( , ) <$> ( Decoders . field . Decoders . nonNullable ) Decoders . int8 <*> ( Decoders . field . Decoders . nonNullable ) Decoders . bool ) ) )
session =
Session . statement () statement
in do
2022-06-20 13:54:54 +03:00
x <- Connection . with ( Session . run session )
2023-10-13 02:24:12 +03:00
assertEqual ( show x ) ( Right ( Right ( 1 , True ) ) ) x ,
testCase " Complex composite decoding "
$ let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select (1, true) as entity1, ('hello', 3) as entity2 "
encoder =
mempty
decoder =
Decoders . singleRow
$ ( , )
<$> ( Decoders . column . Decoders . nonNullable ) entity1
<*> ( Decoders . column . Decoders . nonNullable ) entity2
where
entity1 =
Decoders . composite
$ ( , )
<$> ( Decoders . field . Decoders . nonNullable ) Decoders . int8
<*> ( Decoders . field . Decoders . nonNullable ) Decoders . bool
entity2 =
Decoders . composite
$ ( , )
<$> ( Decoders . field . Decoders . nonNullable ) Decoders . text
<*> ( Decoders . field . Decoders . nonNullable ) Decoders . int8
session =
Session . statement () statement
in do
x <- Connection . with ( Session . run session )
assertEqual ( show x ) ( Right ( Right ( ( 1 , True ) , ( " hello " , 3 ) ) ) ) x ,
testGroup " unknownEnum "
$ [ testCase " " $ do
2024-04-20 14:01:58 +03:00
res <- Session . runSessionOnLocalDb $ do
2022-06-20 13:54:54 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" drop type if exists mood "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2022-06-20 13:54:54 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" create type mood as enum ('sad', 'ok', 'happy') "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2022-06-20 13:54:54 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
2023-10-13 02:24:12 +03:00
" select $1 "
2022-06-20 13:54:54 +03:00
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . enum ( Just . id ) ) ) )
encoder =
2023-10-13 02:24:12 +03:00
Encoders . param ( Encoders . nonNullable ( Encoders . unknownEnum id ) )
2024-04-20 13:46:51 +03:00
in Session . statement " ok " statement
2023-10-13 02:24:12 +03:00
assertEqual " " ( Right " ok " ) res
] ,
testCase " Composite encoding " $ do
let value =
( 123 , 456 , 789 , " abc " )
res <-
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 :: pg_enum "
encoder =
Encoders . param
. Encoders . nonNullable
. Encoders . composite
. mconcat
$ [ contramap ( \ ( a , _ , _ , _ ) -> a ) . Encoders . field . Encoders . nonNullable $ Encoders . oid ,
contramap ( \ ( _ , a , _ , _ ) -> a ) . Encoders . field . Encoders . nonNullable $ Encoders . oid ,
contramap ( \ ( _ , _ , a , _ ) -> a ) . Encoders . field . Encoders . nonNullable $ Encoders . float4 ,
contramap ( \ ( _ , _ , _ , a ) -> a ) . Encoders . field . Encoders . nonNullable $ Encoders . name
]
decoder =
Decoders . singleRow
$ ( Decoders . column . Decoders . nonNullable . Decoders . composite )
( ( , , , )
<$> ( Decoders . field . Decoders . nonNullable ) Decoders . int4
<*> ( Decoders . field . Decoders . nonNullable ) Decoders . int4
<*> ( Decoders . field . Decoders . nonNullable ) Decoders . float4
<*> ( Decoders . field . Decoders . nonNullable ) Decoders . text
)
in Connection . with $ Session . run $ Session . statement value statement
assertEqual " " ( Right ( Right value ) ) res ,
testCase " Empty array "
$ let io =
do
x <- Connection . with ( Session . run session )
assertEqual ( show x ) ( Right ( Right [] ) ) x
where
session =
Session . statement () statement
where
statement =
Statement . Statement sql encoder decoder True
where
sql =
" select array[]::int8[] "
encoder =
mempty
decoder =
Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . array ( Decoders . dimension replicateM ( Decoders . element ( Decoders . nonNullable Decoders . int8 ) ) ) ) )
in io ,
testCase " Failing prepared statements "
$ let io =
Connection . with ( Session . run session )
>>= ( assertBool <$> show <*> resultTest )
where
resultTest =
\ case
Right ( Left ( Session . QueryError _ _ ( Session . ResultError ( Session . ServerError " 26000 " _ _ _ _ ) ) ) ) -> False
_ -> True
session =
catchError session ( const ( pure () ) ) *> session
where
session =
Session . statement () statement
where
statement =
Statement . Statement sql encoder decoder True
where
sql =
" absurd "
encoder =
mempty
decoder =
Decoders . noResult
in io ,
testCase " Prepared statements after error "
$ let io =
Connection . with ( Session . run session )
>>= \ x -> assertBool ( show x ) ( either ( const False ) isRight x )
where
session =
try *> fail *> try
where
try =
Session . statement 1 statement
where
statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 :: int8 "
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . int8 ) )
decoder =
Decoders . singleRow $ ( Decoders . column . Decoders . nonNullable ) Decoders . int8
fail =
catchError ( Session . sql " absurd " ) ( const ( pure () ) )
in io ,
testCase " \ " in progress after error \ " bugfix "
$ let sumStatement :: Statement . Statement ( Int64 , Int64 ) Int64
sumStatement =
Statement . Statement sql encoder decoder True
where
sql =
" select ($1 + $2) "
encoder =
contramap fst ( Encoders . param ( Encoders . nonNullable ( Encoders . int8 ) ) )
<> contramap snd ( Encoders . param ( Encoders . nonNullable ( Encoders . int8 ) ) )
decoder =
Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) Decoders . int8 )
sumSession :: Session . Session Int64
sumSession =
Session . sql " begin " *> Session . statement ( 1 , 1 ) sumStatement <* Session . sql " end "
errorSession :: Session . Session ()
errorSession =
Session . sql " asldfjsldk "
io =
Connection . with $ \ c -> do
Session . run errorSession c
Session . run sumSession c
in io >>= \ x -> assertBool ( show x ) ( either ( const False ) isRight x ) ,
testCase " \ " another command is already in progress \ " bugfix "
$ let sumStatement :: Statement . Statement ( Int64 , Int64 ) Int64
sumStatement =
Statement . Statement sql encoder decoder True
where
sql =
" select ($1 + $2) "
encoder =
contramap fst ( Encoders . param ( Encoders . nonNullable ( Encoders . int8 ) ) )
<> contramap snd ( Encoders . param ( Encoders . nonNullable ( Encoders . int8 ) ) )
decoder =
Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) Decoders . int8 )
session :: Session . Session Int64
session =
do
Session . sql " begin; "
s <- Session . statement ( 1 , 1 ) sumStatement
Session . sql " end; "
return s
2024-04-20 14:01:58 +03:00
in Session . runSessionOnLocalDb session >>= \ x -> assertEqual ( show x ) ( Right 2 ) x ,
2023-10-13 02:24:12 +03:00
testCase " Executing the same query twice "
$ pure () ,
testCase " Interval Encoding "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 = interval '10 seconds' "
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . bool ) ) )
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . interval ) )
2024-04-20 13:46:51 +03:00
in Session . statement ( 10 :: DiffTime ) statement
2023-10-13 02:24:12 +03:00
in actualIO >>= \ x -> assertEqual ( show x ) ( Right True ) x ,
testCase " Interval Decoding "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select interval '10 seconds' "
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . interval ) ) )
encoder =
Encoders . noParams
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
in actualIO >>= \ x -> assertEqual ( show x ) ( Right ( 10 :: DiffTime ) ) x ,
testCase " Interval Encoding/Decoding "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 "
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . interval ) ) )
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . interval ) )
2024-04-20 13:46:51 +03:00
in Session . statement ( 10 :: DiffTime ) statement
2023-10-13 02:24:12 +03:00
in actualIO >>= \ x -> assertEqual ( show x ) ( Right ( 10 :: DiffTime ) ) x ,
testCase " Unknown "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" drop type if exists mood "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" create type mood as enum ('sad', 'ok', 'happy') "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 = ('ok' :: mood) "
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . bool ) ) )
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . unknown ) )
2024-04-20 13:46:51 +03:00
in Session . statement " ok " statement
2023-10-13 02:24:12 +03:00
in actualIO >>= assertEqual " " ( Right True ) ,
testCase " Textual Unknown "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" create or replace function overloaded(a int, b int) returns int as $$ select a + b $$ language sql; "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" create or replace function overloaded(a text, b text, c text) returns text as $$ select a || b || c $$ language sql; "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select overloaded($1, $2) || overloaded($3, $4, $5) "
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . text ) ) )
encoder =
contramany ( Encoders . param ( Encoders . nonNullable ( Encoders . unknown ) ) )
2024-04-20 13:46:51 +03:00
in Session . statement [ " 1 " , " 2 " , " 4 " , " 5 " , " 6 " ] statement
2023-10-13 02:24:12 +03:00
in actualIO >>= assertEqual " " ( Right " 3456 " ) ,
testCase " Enum "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" drop type if exists mood "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql mempty Decoders . noResult True
where
sql =
" create type mood as enum ('sad', 'ok', 'happy') "
2024-04-20 13:46:51 +03:00
in Session . statement () statement
2023-10-13 02:24:12 +03:00
let statement =
Statement . Statement sql encoder decoder True
where
sql =
" select ($1 :: mood) "
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . enum ( Just . id ) ) ) )
encoder =
Encoders . param ( Encoders . nonNullable ( ( Encoders . enum id ) ) )
2024-04-20 13:46:51 +03:00
in Session . statement " ok " statement
2023-10-13 02:24:12 +03:00
in actualIO >>= assertEqual " " ( Right " ok " ) ,
testCase " The same prepared statement used on different types "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2023-10-13 02:24:12 +03:00
let effect1 =
2024-04-20 13:46:51 +03:00
Session . statement " ok " statement
2023-10-13 02:24:12 +03:00
where
statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 "
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . text ) )
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) ( Decoders . text ) ) )
effect2 =
2024-04-20 13:46:51 +03:00
Session . statement 1 statement
2023-10-13 02:24:12 +03:00
where
statement =
Statement . Statement sql encoder decoder True
where
sql =
" select $1 "
encoder =
Encoders . param ( Encoders . nonNullable ( Encoders . int8 ) )
decoder =
( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) Decoders . int8 ) )
in ( , ) <$> effect1 <*> effect2
in actualIO >>= assertEqual " " ( Right ( " ok " , 1 ) ) ,
testCase " Affected rows counting "
$ replicateM_ 13
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2022-06-20 13:54:54 +03:00
dropTable
createTable
replicateM_ 100 insertRow
deleteRows <* dropTable
2015-11-15 12:13:45 +03:00
where
2022-06-20 13:54:54 +03:00
dropTable =
2024-04-20 13:46:51 +03:00
Session . statement ()
2023-10-13 02:24:12 +03:00
$ Statements . plain
$ " drop table if exists a "
2022-06-20 13:54:54 +03:00
createTable =
2024-04-20 13:46:51 +03:00
Session . statement ()
2023-10-13 02:24:12 +03:00
$ Statements . plain
$ " create table a (id bigserial not null, name varchar not null, primary key (id)) "
2022-06-20 13:54:54 +03:00
insertRow =
2024-04-20 13:46:51 +03:00
Session . statement ()
2023-10-13 02:24:12 +03:00
$ Statements . plain
$ " insert into a (name) values ('a') "
2022-06-20 13:54:54 +03:00
deleteRows =
2024-04-20 13:46:51 +03:00
Session . statement () $ Statement . Statement sql mempty decoder False
2022-06-20 13:54:54 +03:00
where
sql =
" delete from a "
decoder =
Decoders . rowsAffected
in actualIO >>= assertEqual " " ( Right 100 ) ,
2023-10-13 02:24:12 +03:00
testCase " Result of an auto-incremented column "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ do
2024-04-20 13:46:51 +03:00
Session . statement () $ Statements . plain $ " drop table if exists a "
Session . statement () $ Statements . plain $ " create table a (id serial not null, v char not null, primary key (id)) "
id1 <- Session . statement () $ Statement . Statement " insert into a (v) values ('a') returning id " mempty ( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) Decoders . int4 ) ) False
id2 <- Session . statement () $ Statement . Statement " insert into a (v) values ('b') returning id " mempty ( Decoders . singleRow ( ( Decoders . column . Decoders . nonNullable ) Decoders . int4 ) ) False
Session . statement () $ Statements . plain $ " drop table if exists a "
2023-10-13 02:24:12 +03:00
pure ( id1 , id2 )
in assertEqual " " ( Right ( 1 , 2 ) ) =<< actualIO ,
testCase " List decoding "
$ let actualIO =
2024-04-20 14:01:58 +03:00
Session . runSessionOnLocalDb $ Session . statement () $ Statements . selectList
2023-10-13 02:24:12 +03:00
in assertEqual " " ( Right [ ( 1 , 2 ) , ( 3 , 4 ) , ( 5 , 6 ) ] ) =<< actualIO
2022-06-20 13:54:54 +03:00
]