@@ -4,18 +4,21 @@
44{-# LANGUAGE DoAndIfThenElse #-}
55{-# LANGUAGE ScopedTypeVariables #-}
66import Common
7+import Database.PostgreSQL.Simple.Copy
78import Database.PostgreSQL.Simple.FromField (FromField)
8−import Database.PostgreSQL.Simple.Types(Query(..),Values(..))
99import Database.PostgreSQL.Simple.HStore
10−import Database.PostgreSQL.Simple.Copy
10+import Database.PostgreSQL.Simple.Internal (breakOnSingleQuestionMark)
11+import Database.PostgreSQL.Simple.Types(Query(..),Values(..), PGArray(..))
1112import qualified Database.PostgreSQL.Simple.Transaction as ST
1213
1314import Control.Applicative
1415import Control.Exception as E
1516import Control.Monad
1617import Data.Char
17−import Data.List (sort)
18+import Data.List (concat, sort)
1819import Data.IORef
20+import Data.Monoid ((<>))
21+import Data.String (fromString)
1922import Data.Typeable
2023import GHC.Generics (Generic)
2124
@@ -44,26 +47,27 @@ tests :: TestEnv -> TestTree
4447tests env = testGroup "tests"
4548 $ map ($ env)
4649 [ testBytea
47− , testCase "ExecuteMany" . testExecuteMany
48− , testCase "Fold" . testFold
49− , testCase "Notify" . testNotify
50− , testCase "Serializable" . testSerializable
51− , testCase "Time" . testTime
52− , testCase "Array" . testArray
53− , testCase "Array of nullables" . testNullableArray
54− , testCase "HStore" . testHStore
55− , testCase "citext" . testCIText
56− , testCase "JSON" . testJSON
57− , testCase "Savepoint" . testSavepoint
58− , testCase "Unicode" . testUnicode
59− , testCase "Values" . testValues
60− , testCase "Copy" . testCopy
50+ , testCase "ExecuteMany" . testExecuteMany
51+ , testCase "Fold" . testFold
52+ , testCase "Notify" . testNotify
53+ , testCase "Serializable" . testSerializable
54+ , testCase "Time" . testTime
55+ , testCase "Array" . testArray
56+ , testCase "Array of nullables" . testNullableArray
57+ , testCase "HStore" . testHStore
58+ , testCase "citext" . testCIText
59+ , testCase "JSON" . testJSON
60+ , testCase "Question mark escape" . testQM
61+ , testCase "Savepoint" . testSavepoint
62+ , testCase "Unicode" . testUnicode
63+ , testCase "Values" . testValues
64+ , testCase "Copy" . testCopy
6165 , testCopyFailures
62− , testCase "Double" . testDouble
63− , testCase "1-ary generic" . testGeneric1
64− , testCase "2-ary generic" . testGeneric2
65− , testCase "3-ary generic" . testGeneric3
66− , testCase "Timeout" . testTimeout
66+ , testCase "Double" . testDouble
67+ , testCase "1-ary generic" . testGeneric1
68+ , testCase "2-ary generic" . testGeneric2
69+ , testCase "3-ary generic" . testGeneric3
70+ , testCase "Timeout" . testTimeout
6771 ]
6872
6973testBytea :: TestEnv -> TestTree
@@ -234,6 +238,56 @@ testJSON TestEnv{..} = do
234238 js' <- query conn "SELECT ?::json" js
235239 [js] @?= js'
236240
241+testQM :: TestEnv -> Assertion
242+testQM TestEnv{..} = do
243+ -- Just test on a single string
244+ let testQuery' b = "testing for ?" <> b <> " and making sure "
245+ testQueryDoubleQM = testQuery' "?"
246+ testQueryRest = "? is substituted"
247+ testQuery = fromString $ testQueryDoubleQM <> testQueryRest
248+ -- expect the entire first part with double QMs replaced with literal '?'
249+ expected = (fromString $ testQuery' "", fromString testQueryRest)
250+ tried = breakOnSingleQuestionMark testQuery
251+ errMsg = concat
252+ [ "Failed to break on single question mark exclusively:\n"
253+ , "expected: ", show expected
254+ , "result: ", show tried
255+ ]
256+ assertBool errMsg $ tried == expected
257+
258+ -- Let's also test the question mark operators in action
259+ -- ? -> Does the string exist as a top-level key within the JSON value?
260+ positiveQuery "SELECT ?::jsonb ?? ?" (testObj, "foo" :: Text)
261+ negativeQuery "SELECT ?::jsonb ?? ?" (testObj, "baz" :: Text)
262+ negativeQuery "SELECT ?::jsonb ?? ?" (toJSON numArray, "1" :: Text)
263+ -- ?| -> Do any of these array strings exist as top-level keys?
264+ positiveQuery "SELECT ?::jsonb ??| ?" (testObj, PGArray ["nope","bar","6" :: Text])
265+ negativeQuery "SELECT ?::jsonb ??| ?" (testObj, PGArray ["nope","6" :: Text])
266+ negativeQuery "SELECT ?::jsonb ??| ?" (toJSON numArray, PGArray ["1","2","6" :: Text])
267+ -- ?& -> Do all of these array strings exist as top-level keys?
268+ positiveQuery "SELECT ?::jsonb ??& ?" (testObj, PGArray ["foo","bar","quux" :: Text])
269+ positiveQuery "SELECT ?::jsonb ??& ?" (testObj, PGArray ["foo","bar" :: Text])
270+ negativeQuery "SELECT ?::jsonb ??& ?" (testObj, PGArray ["foo","bar","baz" :: Text])
271+ negativeQuery "SELECT ?::jsonb ??& ?" (toJSON numArray, PGArray ["1","2","3","4","5" :: Text])
272+ -- Format error for 2 question marks, not 4
273+ True <- expectError (isFormatError 2) $
274+ (query conn "SELECT ?::jsonb ?? ?" $ Only testObj :: IO [Only Bool])
275+ return ()
276+ where positiveQuery :: ToRow a => Query -> a -> Assertion
277+ positiveQuery = boolQuery True
278+ negativeQuery :: ToRow a => Query -> a -> Assertion
279+ negativeQuery = boolQuery False
280+ numArray :: [Int]
281+ numArray = [1,2,3,4,5]
282+ boolQuery :: ToRow a => Bool -> Query -> a -> Assertion
283+ boolQuery b t x = do
284+ a <- query conn t x
285+ [Only b] @?= a
286+ testObj = toJSON (Map.fromList [("foo",toJSON (1 :: Int))
287+ ,("bar",String "baz")
288+ ,("quux",toJSON [1 :: Int,2,3,4,5])] :: Map Text Value
289+ )
290+
237291testSavepoint :: TestEnv -> Assertion
238292testSavepoint TestEnv{..} = do
239293 True <- expectError ST.isNoActiveTransactionError $
@@ -478,6 +532,14 @@ isUniqueViolation SqlError{..} = sqlState == "23505"
478532isSyntaxError :: SqlError -> Bool
479533isSyntaxError SqlError{..} = sqlState == "42601"
480534
535+isFormatError :: Int -> FormatError -> Bool
536+isFormatError i FormatError{..}
537+ | null fmtMessage = False
538+ | otherwise = fmtMessage == concat [ show i
539+ , " single '?' characters, but "
540+ , show (length fmtParams)
541+ , " parameters"
542+ ]
481543------------------------------------------------------------------------
482544
483545-- | Action for connecting to the database that will be used for testing.