| Module : Database.PostgreSQL.Test.Enumerator Copyright : (c) 2004 Oleg Kiselyov, Alistair Bayley License : BSD-style Maintainer : oleg@pobox.com, alistair@abayley.org Stability : experimental Portability : non-portable Can we support database functions that return/create multiple result-sets? Oracle, MS Sql Server, and Sybase have them, and we can simulate them in Postgres with the code below. CREATE TRUSTED PROCEDURAL LANGUAGE 'plpgsql' HANDLER plpgsql_call_handler; CREATE OR REPLACE VIEW t_whole as select 0 as n union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9 ; CREATE OR REPLACE VIEW t_natural as select n from ( select t1.n + 10 * t10.n + 100 * t100.n as n from t_whole t1, t_whole t10, t_whole t100 ) t where n > 0 order by 1 ; DROP FUNCTION takusenTestFunc() ; CREATE OR REPLACE FUNCTION takusenTestFunc() RETURNS SETOF refcursor AS \$\$ DECLARE refc1 refcursor; refc2 refcursor; BEGIN OPEN refc1 FOR SELECT n*n from t_natural where n < 10 order by 1; RETURN NEXT refc1; OPEN refc2 FOR SELECT n, n*n, n*n*n from t_natural where n < 10 order by 1; RETURN NEXT refc2; END;\$\$ LANGUAGE plpgsql; -- this select returns two values (rows), both strings (well, refcursors), -- which are the cursor names. select * from myfunc(); fetch all from ""; fetch all from ""; commit; Another example: CREATE OR REPLACE FUNCTION takusenTestFunc(lim int4) RETURNS refcursor AS \$\$ DECLARE refc refcursor; BEGIN OPEN refc FOR SELECT n, takusenTestFunc2(n) from t_natural where n < lim order by n; RETURN refc; END; \$\$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION takusenTestFunc2(lim int4) RETURNS refcursor AS \$\$ DECLARE refc refcursor; BEGIN OPEN refc FOR SELECT n from t_natural where n < lim order by n; RETURN refc; END; \$\$ LANGUAGE plpgsql; SELECT n, takusenTestFunc(n) from t_natural where n < 10 order by n; > {-# LANGUAGE OverlappingInstances #-} > module Database.PostgreSQL.Test.Enumerator (runTest) where > import qualified Database.PostgreSQL.Test.PGFunctions as Low > import Database.PostgreSQL.Enumerator > import Database.Test.Performance as Perf > import Database.Test.Enumerator > import Control.Monad (when) > import Test.MiniUnit > import Data.Int > import Data.List > import System.Time > runTest :: Perf.ShouldRunTests -> [String] -> IO () > runTest runPerf args = do > putStrLn "PostgreSQL tests" > let (user:pswd:dbname:_) = args > Low.runTest user > flip catchDB basicDBExceptionReporter $ do > (r, conn1) <- withContinuedSession (connect [CAuser user]) (testBody runPerf) > withSession conn1 testPartTwo > testBody runPerf = do > runFixture PGSqlFunctions > when (runPerf == Perf.RunTests) runPerformanceTests > testPartTwo = do > makeFixture execDrop execDDL_ > destroyFixture execDDL_ > runPerformanceTests = do > makeFixture execDrop execDDL_ > beginTransaction RepeatableRead > runTestTT "PostgreSQL performance tests" (map (flip catchDB reportRethrow) > [ timedSelect (prefetch 1000 sqlRows2Power20 []) 40 (2^20) > , timedSelect (prefetch 1 sqlRows2Power17 []) 40 (2^17) > , timedSelect (prefetch 1000 sqlRows2Power17 []) 5 (2^17) > , timedCursor (prefetch 1 sqlRows2Power17 []) 40 (2^17) > , timedCursor (prefetch 1000 sqlRows2Power17 []) 5 (2^17) > ] > ) > commit > destroyFixture execDDL_ > runFixture :: PGSqlFunctions -> DBM mark Session () > runFixture fns = do > makeFixture execDrop execDDL_ > execDDL_ makeFixtureNestedMultiResultSet1 > execDDL_ makeFixtureNestedMultiResultSet2 > execDDL_ makeFixtureNestedMultiResultSet3 > execDDL_ makeFixtureNestedMultiResultSet4 > execDDL_ makeFixtureMultiResultSet1 > runTestTT "Postgres tests" (map (runOneTest fns) testList) > execDDL_ dropFixtureMultiResultSet1 > execDDL_ dropFixtureNestedMultiResultSet4 > execDDL_ dropFixtureNestedMultiResultSet3 > execDDL_ dropFixtureNestedMultiResultSet2 > execDDL_ dropFixtureNestedMultiResultSet1 > destroyFixture execDDL_ > runOneTest fns t = catchDB (t fns) reportRethrow ----------------------------------------------------------- > selectNoRows _ = selectTest sqlNoRows iterNoRows expectNoRows > selectTerminatesEarly _ = selectTest sqlTermEarly iterTermEarly expectTermEarly > selectFloatsAndInts fns = selectTest (sqlFloatsAndInts fns) iterFloatsAndInts expectFloatsAndInts > selectNullString _ = selectTest sqlNullString iterNullString expectNullString > selectEmptyString _ = selectTest sqlEmptyString iterEmptyString expectEmptyString > selectUnhandledNull _ = catchDB ( do > selectTest sqlUnhandledNull iterUnhandledNull expectUnhandledNull > assertFailure sqlUnhandledNull > ) (\e -> return () ) > selectNullDate dateFn = selectTest (sqlNullDate dateFn) iterNullDate expectNullDate > selectDate dateFn = selectTest (sqlDate dateFn) iterDate expectDate > selectCalDate dateFn = selectTest (sqlDate dateFn) iterCalDate expectCalDate > selectBoundaryDates dateFn = selectTest (sqlBoundaryDates dateFn) iterBoundaryDates expectBoundaryDates > selectCursor fns = actionCursor (sqlCursor fns) > selectExhaustCursor fns = actionExhaustCursor (sqlCursor fns) Note that these two tests use the same prepared statement name. This tests that the statement is properly deallocated, through use of withPreparedStatement. > selectBindString _ = actionBindString > (prepareQuery "1" (sql sqlBindString) [bindType "", bindType ""]) > [bindP "a2", bindP "b1"] > selectBindInt _ = actionBindInt > (prepareQuery "1" (sql sqlBindInt) (map bindType expectBindInt)) > [bindP (1::Int), bindP (2::Int)] > selectBindIntDoubleString _ = actionBindIntDoubleString > (prefetch 0 sqlBindIntDoubleString [bindP (1::Int), bindP (2.2::Double), bindP "row 1", bindP (3::Int), bindP (4.4::Double), bindP "row 2"]) > selectBindDate _ = actionBindDate > (prefetch 1 sqlBindDate (map bindP expectBindDate)) > selectBindBoundaryDates _ = actionBindBoundaryDates > (prefetch 1 sqlBindBoundaryDates (map bindP expectBoundaryDates)) > selectRebindStmt _ = actionRebind > (prepareQuery "1" (sql sqlRebind) [bindType (0::Int)]) > [bindP (1::Int)] [bindP (2::Int)] > boundStmtDML _ = actionBoundStmtDML > (prepareCommand "boundStmtDML" (sql sqlBoundStmtDML) [bindType (0::Int), bindType ""]) > boundStmtDML2 _ = do > beginTransaction RepeatableRead > count <- execDML (cmdbind sqlBoundStmtDML [bindP (100::Int), bindP "100"]) > rollback > assertEqual sqlBoundStmtDML 1 count > polymorphicFetchTest _ = actionPolymorphicFetch > (prefetch 0 sqlPolymorphicFetch [bindP expectPolymorphicFetch]) > polymorphicFetchTestNull _ = withTransaction RepeatableRead $ > actionPolymorphicFetchNull (prefetch 1 sqlPolymorphicFetchNull []) For the exceptionRollback test we have to specify the count result is int4; if we don't specify the type then it defaults to a Postgres numeric, which we can't yet marshal. > exceptionRollback _ = actionExceptionRollback sqlInsertTest4 > ("select count(*)::int4 from " ++ testTable) Ensure we get an exception if the result-set columns don't match the output buffers (at present we only check that there aren't too many buffers i.e. not enough columns). > iterateeMatchesResultSet _ = actionIterateeMatchesResultSet > (prefetch 0 sqlIterateeMatchesResultSet [bindP (1::Int), bindP (2.2::Double), bindP "row 1", bindP (3::Int)]) > selectUTF8Text _ = do > let iter (s::String) (_::String) = result s > -- GREEK SMALL LETTER PHI : CF86 UTF8, 03C6 UTF16, 966 decimal > let expect = ['\966'] > result <- doQuery (sql ("select '" ++ expect ++ "'")) iter "" > assertEqual "selectUTF8Text" expect result > dropFixtureMultiResultSet1 = "DROP FUNCTION takusenTestFunc()" > makeFixtureMultiResultSet1 = > "CREATE OR REPLACE FUNCTION takusenTestFunc() RETURNS SETOF refcursor AS $$" > ++ "DECLARE refc1 refcursor; refc2 refcursor; BEGIN" > ++ " OPEN refc1 FOR SELECT n*n from t_natural where n < 10 order by 1;" > ++ " RETURN NEXT refc1;" > ++ " OPEN refc2 FOR SELECT n, n*n, n*n*n from t_natural where n < 10 order by 1;" > ++ " RETURN NEXT refc2;" > ++ "END;$$ LANGUAGE plpgsql;\n" > selectMultiResultSet _ = do > withTransaction RepeatableRead $ do > withPreparedStatement (prepareLargeQuery 2 "stmt1" (sql "select * from takusenTestFunc()") []) $ \pstmt -> do > withBoundStatement pstmt [] $ \bstmt -> do > dummy <- doQuery bstmt iterMain [] > result1 <- doQuery (NextResultSet pstmt) iterRS1 [] > assertEqual "selectMultiResultSet: RS1" [1,4,9,16,25,36,49,64,81] result1 > result2 <- doQuery (NextResultSet pstmt) iterRS2 [] > let expect = [(1,1,1),(2,4,8),(3,9,27),(4,16,64),(5,25,125),(6,36,216) > ,(7,49,343),(8,64,512),(9,81,729)] > assertEqual "selectMultiResultSet: RS2" expect result2 > return () > where > iterMain :: (Monad m) => (RefCursor String) -> IterAct m [RefCursor String] > iterMain c acc = result (acc ++ [c]) > iterRS1 :: (Monad m) => Int -> IterAct m [Int] > iterRS1 i acc = result (acc ++ [i]) > iterRS2 :: (Monad m) => Int -> Int -> Int -> IterAct m [(Int, Int, Int)] > iterRS2 i i2 i3 acc = result (acc ++ [(i, i2, i3)]) > dropFixtureNestedMultiResultSet1 = "DROP VIEW t_whole" > makeFixtureNestedMultiResultSet1 = "CREATE OR REPLACE VIEW t_whole as" > ++ " select 0 as n union select 1 union select 2 union select 3 union select 4" > ++ " union select 5 union select 6 union select 7 union select 8 union select 9" > dropFixtureNestedMultiResultSet2 = "DROP VIEW t_natural" > makeFixtureNestedMultiResultSet2 = "CREATE OR REPLACE VIEW t_natural as" > ++ " select n from" > ++ " ( select t1.n + 10 * t10.n + 100 * t100.n as n" > ++ " from t_whole t1, t_whole t10, t_whole t100" > ++ " ) t where n > 0 order by 1" > dropFixtureNestedMultiResultSet3 = "DROP FUNCTION takusenTestFunc(int4)" > makeFixtureNestedMultiResultSet3 = > "CREATE OR REPLACE FUNCTION takusenTestFunc(lim int4) RETURNS refcursor AS $$" > ++ " DECLARE refc refcursor; BEGIN" > ++ " OPEN refc FOR SELECT n, takusenTestFunc2(n) from t_natural where n < lim order by n;" > ++ " RETURN refc; END; $$ LANGUAGE plpgsql;" > dropFixtureNestedMultiResultSet4 = "DROP FUNCTION takusenTestFunc2(int4)" > makeFixtureNestedMultiResultSet4 = > "CREATE OR REPLACE FUNCTION takusenTestFunc2(lim int4) RETURNS refcursor AS $$" > ++ " DECLARE refc refcursor; BEGIN" > ++ " OPEN refc FOR SELECT n from t_natural where n < lim order by n;" > ++ " RETURN refc; END; $$ LANGUAGE plpgsql;" > selectNestedMultiResultSet _ = do > let > q = "SELECT n, takusenTestFunc(n) from t_natural where n < 10 order by n" > iterMain (i::Int) (c::RefCursor String) acc = result' ((i,c):acc) > iterInner (i::Int) (c::RefCursor String) acc = result' ((i,c):acc) > iterInner2 (i::Int) acc = result' (i:acc) > withTransaction RepeatableRead $ do > rs <- doQuery (sql q) iterMain [] > assertEqual "selectNestedMultiResultSet" [9,8,7,6,5,4,3,2,1] (map fst rs) > --print_ "" > flip mapM_ rs $ \(outer, c) -> do > rs <- doQuery c iterInner [] > let expect = drop (9-outer) [8,7,6,5,4,3,2,1] > assertEqual "processOuter" expect (map fst rs) > flip mapM_ rs $ \(inner, c) -> do > rs <- doQuery c iterInner2 [] > let expect = drop (9-inner) [8,7,6,5,4,3,2,1] > assertEqual "processInner" expect rs > flip mapM_ rs $ \i -> do > --print_ (show outer ++ " " ++ show inner ++ " " ++ show i) > assertBool "processInner2" (i < inner) > generateErrorMessageTest _ = do > catchDB ( do > doQuery (sql "select * from nonExistantObject") iterNoRows [] > return () > ) (\e -> do > let msg = formatDBException e > -- uncomment this to view error message > --liftIO (putStrLn msg) > let expect = "42P01 7: ERROR: 42P01: relation" > assertEqual "generateErrorMessageTest" expect (take (length expect) msg) > ) > testList :: [PGSqlFunctions -> DBM mark Session ()] > testList = > [ selectNoRows, selectTerminatesEarly, selectFloatsAndInts > , selectNullString, selectEmptyString, selectUnhandledNull > , selectNullDate, selectDate, selectCalDate, selectBoundaryDates > , selectCursor, selectExhaustCursor > , selectBindString, selectBindInt, selectBindIntDoubleString > , selectBindDate, selectBindBoundaryDates, selectRebindStmt > , boundStmtDML, boundStmtDML2 > , polymorphicFetchTest, polymorphicFetchTestNull, exceptionRollback > , selectMultiResultSet, selectNestedMultiResultSet > , generateErrorMessageTest, selectUTF8Text, iterateeMatchesResultSet > ]