-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathInternal.hs
664 lines (588 loc) · 26.6 KB
/
Internal.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
{-# LANGUAGE CPP, BangPatterns, DoAndIfThenElse, RecordWildCards #-}
{-# LANGUAGE DeriveDataTypeable, DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
------------------------------------------------------------------------------
-- |
-- Module: Database.PostgreSQL.Simple.Internal
-- Copyright: (c) 2011-2015 Leon P Smith
-- License: BSD3
-- Maintainer: Leon P Smith <[email protected]>
-- Stability: experimental
--
-- Internal bits. This interface is less stable and can change at any time.
-- In particular this means that while the rest of the postgresql-simple
-- package endeavors to follow the package versioning policy, this module
-- does not. Also, at the moment there are things in here that aren't
-- particularly internal and are exported elsewhere; these will eventually
-- disappear from this module.
--
------------------------------------------------------------------------------
module Database.PostgreSQL.Simple.Internal where
import Control.Applicative
import Control.Exception
import Control.Concurrent.MVar
import Control.Monad(MonadPlus(..), when)
import Data.ByteString(ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Data.ByteString.Builder ( Builder, byteString )
import Data.Char (ord)
import Data.Int (Int64)
import qualified Data.IntMap as IntMap
import Data.IORef
import Data.Maybe(fromMaybe)
import Data.Monoid
import Data.String
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Typeable
import Data.Word
import Database.PostgreSQL.LibPQ(Oid(..))
import qualified Database.PostgreSQL.LibPQ as PQ
import Database.PostgreSQL.LibPQ(ExecStatus(..))
import Database.PostgreSQL.Simple.Compat ( toByteString )
import Database.PostgreSQL.Simple.Ok
import Database.PostgreSQL.Simple.ToField (Action(..), inQuotes)
import Database.PostgreSQL.Simple.Types (Query(..))
import Database.PostgreSQL.Simple.TypeInfo.Types(TypeInfo)
import Control.Monad.Trans.State.Strict
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Class
import GHC.Generics
import GHC.IO.Exception
#if !defined(mingw32_HOST_OS)
import Control.Concurrent(threadWaitRead, threadWaitWrite)
#endif
-- | A Field represents metadata about a particular field
--
-- You don't particularly want to retain these structures for a long
-- period of time, as they will retain the entire query result, not
-- just the field metadata
data Field = Field {
result :: !PQ.Result
, column :: {-# UNPACK #-} !PQ.Column
, typeOid :: {-# UNPACK #-} !PQ.Oid
-- ^ This returns the type oid associated with the column. Analogous
-- to libpq's @PQftype@.
}
type TypeInfoCache = IntMap.IntMap TypeInfo
data Connection = Connection {
connectionHandle :: {-# UNPACK #-} !(MVar PQ.Connection)
, connectionObjects :: {-# UNPACK #-} !(MVar TypeInfoCache)
, connectionTempNameCounter :: {-# UNPACK #-} !(IORef Int64)
, connectionMayHaveOrphanedStatement :: {-# UNPACK #-} !(IORef Bool)
-- ^ True if there could be a statement running in postgres in this connection, but
-- postgresql-simple is not waiting for results from it. This can happen when
-- postgresql-simple is interrupted by asynchronous exceptions.
} deriving (Typeable)
instance Eq Connection where
x == y = connectionHandle x == connectionHandle y
data SqlError = SqlError {
sqlState :: ByteString
, sqlExecStatus :: ExecStatus
, sqlErrorMsg :: ByteString
, sqlErrorDetail :: ByteString
, sqlErrorHint :: ByteString
} deriving (Eq, Show, Typeable)
fatalError :: ByteString -> SqlError
fatalError msg = SqlError "" FatalError msg "" ""
instance Exception SqlError
-- | Exception thrown if 'query' is used to perform an @INSERT@-like
-- operation, or 'execute' is used to perform a @SELECT@-like operation.
data QueryError = QueryError {
qeMessage :: String
, qeQuery :: Query
} deriving (Eq, Show, Typeable)
instance Exception QueryError
-- | Exception thrown if a 'Query' could not be formatted correctly.
-- This may occur if the number of \'@?@\' characters in the query
-- string does not match the number of parameters provided.
data FormatError = FormatError {
fmtMessage :: String
, fmtQuery :: Query
, fmtParams :: [ByteString]
} deriving (Eq, Show, Typeable)
instance Exception FormatError
data ConnectInfo = ConnectInfo {
connectHost :: String
, connectPort :: Word16
, connectUser :: String
, connectPassword :: String
, connectDatabase :: String
} deriving (Generic,Eq,Read,Show,Typeable)
-- | Default information for setting up a connection.
--
-- Defaults are as follows:
--
-- * Server on @localhost@
--
-- * Port on @5432@
--
-- * User @postgres@
--
-- * No password
--
-- * Database @postgres@
--
-- Use as in the following example:
--
-- > connect defaultConnectInfo { connectHost = "db.example.com" }
defaultConnectInfo :: ConnectInfo
defaultConnectInfo = ConnectInfo {
connectHost = "127.0.0.1"
, connectPort = 5432
, connectUser = "postgres"
, connectPassword = ""
, connectDatabase = ""
}
-- | Connect with the given username to the given database. Will throw
-- an exception if it cannot connect.
connect :: ConnectInfo -> IO Connection
connect = connectPostgreSQL . postgreSQLConnectionString
-- | Memory bracket around 'connect' and 'close'.
withConnect :: ConnectInfo -> (Connection -> IO c) -> IO c
withConnect connInfo = bracket (connect connInfo) close
-- | Attempt to make a connection based on a libpq connection string.
-- See <https://www.postgresql.org/docs/9.5/static/libpq-connect.html#LIBPQ-CONNSTRING>
-- for more information. Also note that environment variables also affect
-- parameters not provided, parameters provided as the empty string, and a
-- few other things; see
-- <https://www.postgresql.org/docs/9.5/static/libpq-envars.html>
-- for details. Here is an example with some of the most commonly used
-- parameters:
--
-- > host='db.somedomain.com' port=5432 ...
--
-- This attempts to connect to @db.somedomain.com:5432@. Omitting the port
-- will normally default to 5432.
--
-- On systems that provide unix domain sockets, omitting the host parameter
-- will cause libpq to attempt to connect via unix domain sockets.
-- The default filesystem path to the socket is constructed from the
-- port number and the @DEFAULT_PGSOCKET_DIR@ constant defined in the
-- @pg_config_manual.h@ header file. Connecting via unix sockets tends
-- to use the @peer@ authentication method, which is very secure and
-- does not require a password.
--
-- On Windows and other systems without unix domain sockets, omitting
-- the host will default to @localhost@.
--
-- > ... dbname='postgres' user='postgres' password='secret \' \\ pw'
--
-- This attempts to connect to a database named @postgres@ with
-- user @postgres@ and password @secret \' \\ pw@. Backslash
-- characters will have to be double-quoted in literal Haskell strings,
-- of course. Omitting @dbname@ and @user@ will both default to the
-- system username that the client process is running as.
--
-- Omitting @password@ will default to an appropriate password found
-- in the @pgpass@ file, or no password at all if a matching line is
-- not found. The path of the @pgpass@ file may be specified by setting
-- the @PGPASSFILE@ environment variable. See
-- <https://www.postgresql.org/docs/9.5/static/libpq-pgpass.html> for
-- more information regarding this file.
--
-- As all parameters are optional and the defaults are sensible, the
-- empty connection string can be useful for development and
-- exploratory use, assuming your system is set up appropriately.
--
-- On Unix, such a setup would typically consist of a local
-- postgresql server listening on port 5432, as well as a system user,
-- database user, and database sharing a common name, with permissions
-- granted to the user on the database.
--
-- On Windows, in addition you will either need @pg_hba.conf@
-- to specify the use of the @trust@ authentication method for
-- the connection, which may not be appropriate for multiuser
-- or production machines, or you will need to use a @pgpass@ file
-- with the @password@ or @md5@ authentication methods.
--
-- See <https://www.postgresql.org/docs/9.5/static/client-authentication.html>
-- for more information regarding the authentication process.
--
-- SSL/TLS will typically "just work" if your postgresql server supports or
-- requires it. However, note that libpq is trivially vulnerable to a MITM
-- attack without setting additional SSL connection parameters. In
-- particular, @sslmode@ needs to be set to @require@, @verify-ca@, or
-- @verify-full@ in order to perform certificate validation. When @sslmode@
-- is @require@, then you will also need to specify a @sslrootcert@ file,
-- otherwise no validation of the server's identity will be performed.
-- Client authentication via certificates is also possible via the
-- @sslcert@ and @sslkey@ parameters. See
-- <https://www.postgresql.org/docs/9.5/static/libpq-ssl.html>
-- for detailed information regarding libpq and SSL.
connectPostgreSQL :: ByteString -> IO Connection
connectPostgreSQL connstr = do
conn <- connectdb connstr
stat <- PQ.status conn
case stat of
PQ.ConnectionOk -> do
connectionHandle <- newMVar conn
connectionObjects <- newMVar (IntMap.empty)
connectionTempNameCounter <- newIORef 0
connectionMayHaveOrphanedStatement <- newIORef False
let wconn = Connection{..}
version <- PQ.serverVersion conn
let settings
| version < 80200 = "SET datestyle TO ISO;SET client_encoding TO UTF8"
| otherwise = "SET datestyle TO ISO;SET client_encoding TO UTF8;SET standard_conforming_strings TO on"
_ <- execute_ wconn settings
return wconn
_ -> do
msg <- maybe "connectPostgreSQL error" id <$> PQ.errorMessage conn
throwIO $ fatalError msg
connectdb :: ByteString -> IO PQ.Connection
#if defined(mingw32_HOST_OS)
connectdb = PQ.connectdb
#else
connectdb conninfo = do
conn <- PQ.connectStart conninfo
loop conn
where
funcName = "Database.PostgreSQL.Simple.connectPostgreSQL"
loop conn = do
status <- PQ.connectPoll conn
case status of
PQ.PollingFailed -> throwLibPQError conn "connection failed"
PQ.PollingReading -> do
mfd <- PQ.socket conn
case mfd of
Nothing -> throwIO $! fdError funcName
Just fd -> do
threadWaitRead fd
loop conn
PQ.PollingWriting -> do
mfd <- PQ.socket conn
case mfd of
Nothing -> throwIO $! fdError funcName
Just fd -> do
threadWaitWrite fd
loop conn
PQ.PollingOk -> return conn
#endif
-- | Turns a 'ConnectInfo' data structure into a libpq connection string.
postgreSQLConnectionString :: ConnectInfo -> ByteString
postgreSQLConnectionString connectInfo = fromString connstr
where
connstr = str "host=" connectHost
$ num "port=" connectPort
$ str "user=" connectUser
$ str "password=" connectPassword
$ str "dbname=" connectDatabase
$ []
str name field
| null value = id
| otherwise = showString name . addQuotes value . space
where value = field connectInfo
num name field
| value <= 0 = id
| otherwise = showString name . shows value . space
where value = field connectInfo
addQuotes s rest = '\'' : foldr delta ('\'' : rest) s
where
delta c cs = case c of
'\\' -> '\\' : '\\' : cs
'\'' -> '\\' : '\'' : cs
_ -> c : cs
space [] = []
space xs = ' ':xs
oid2int :: Oid -> Int
oid2int (Oid x) = fromIntegral x
{-# INLINE oid2int #-}
exec :: Connection
-> ByteString
-> IO PQ.Result
#if defined(mingw32_HOST_OS)
exec conn sql =
withConnection conn $ \h -> do
mres <- PQ.exec h sql
case mres of
Nothing -> throwLibPQError h "PQexec returned no results"
Just res -> return res
#else
exec conn sql =
withConnection conn $ \h -> withSocket h $ \socket-> uninterruptibleMask $ \restore -> do
-- 1. If postgresql-simple was interrupted when waiting for query results
-- before, cancel that query (it may even have completed by now, but that's fine)
-- before issuing a new one.
restore $ do
needsToCancel <- readIORef (connectionMayHaveOrphanedStatement conn)
when needsToCancel $ do
cancelRunningQuery h socket
writeIORef (connectionMayHaveOrphanedStatement conn) False
-- 2. Ideally, the code that issues the query and waits for results
-- should not throw exceptions. That way we know an exception means
-- postgresql-simple was interrupted and the query might still be running.
-- Still, even if the code throws exceptions for other reasons, it means
-- we'll try to cancel a running query later once, which is fairly inocuous
-- as long as such exceptions are rare (which they should be).
restore (sendQueryAndWaitForResults h socket)
`onException` writeIORef (connectionMayHaveOrphanedStatement conn) True
where
withSocket h f = do
mfd <- PQ.socket h
case mfd of
Nothing -> throwIO $! fdError "Database.PostgreSQL.Simple.Internal.exec"
Just socket -> f socket
sendQueryAndWaitForResults h socket = do
success <- PQ.sendQuery h sql
if success then do
consumeUntilNotBusy h socket
getResult h Nothing
else throwLibPQError h "PQsendQuery failed"
cancelRunningQuery h socket = do
mcncl <- PQ.getCancel h
case mcncl of
Nothing -> pure ()
Just cncl -> do
cancelStatus <- PQ.cancel cncl
case cancelStatus of
Left _ -> PQ.errorMessage h >>= \mmsg -> throwLibPQError h ("Database.PostgreSQL.Simple.Internal.cancelRunningQuery: " <> fromMaybe "Unknown error" mmsg
<> "\nIt looks like postgresql-simple was previously interrupted by an exception while waiting for query results."
<> " Because of that, before issuing a new query, we tried to cancel that previous query that was interrupted, but failed to do so.")
Right () -> do
consumeUntilNotBusy h socket
waitForNullResult h
waitForNullResult h = do
mres <- PQ.getResult h
case mres of
Nothing -> pure ()
Just _ -> waitForNullResult h
-- | Waits until results are ready to be fetched.
consumeUntilNotBusy h socket = do
-- According to https://www.postgresql.org/docs/current/libpq-async.html :
-- 1. The isBusy status only changes by calling PQConsumeInput
-- 2. In case of errors, "PQgetResult should be called until it returns a null pointer, to allow libpq to process the error information completely"
-- 3. Also, "A typical application using these functions will have a main loop that uses select() or poll() ... When the main loop detects input ready, it should call PQconsumeInput to read the input. It can then call PQisBusy, followed by PQgetResult if PQisBusy returns false (0)"
busy <- PQ.isBusy h
when busy $ do
threadWaitRead socket
someError <- not <$> PQ.consumeInput h
when someError $ PQ.errorMessage h >>= \mmsg -> throwLibPQError h ("Database.PostgreSQL.Simple.Internal.consumeUntilNotBusy: " <> fromMaybe "Unknown error" mmsg)
consumeUntilNotBusy h socket
getResult h mres = do
mres' <- PQ.getResult h
case mres' of
Nothing -> case mres of
Nothing -> throwLibPQError h "PQgetResult returned no results"
Just res -> return res
Just res -> do
status <- PQ.resultStatus res
case status of
-- FIXME: handle PQ.CopyBoth and PQ.SingleTuple
PQ.EmptyQuery -> getResult h mres'
PQ.CommandOk -> getResult h mres'
PQ.TuplesOk -> getResult h mres'
PQ.CopyOut -> return res
PQ.CopyIn -> return res
PQ.BadResponse -> getResult h mres'
PQ.NonfatalError -> getResult h mres'
PQ.FatalError -> getResult h mres'
#endif
-- | A version of 'execute' that does not perform query substitution.
execute_ :: Connection -> Query -> IO Int64
execute_ conn q@(Query stmt) = do
result <- exec conn stmt
finishExecute conn q result
finishExecute :: Connection -> Query -> PQ.Result -> IO Int64
finishExecute _conn q result = do
status <- PQ.resultStatus result
case status of
-- FIXME: handle PQ.CopyBoth and PQ.SingleTuple
PQ.EmptyQuery -> throwIO $ QueryError "execute: Empty query" q
PQ.CommandOk -> do
ncols <- PQ.nfields result
if ncols /= 0
then throwIO $ QueryError ("execute resulted in " ++ show ncols ++
"-column result") q
else do
nstr <- PQ.cmdTuples result
return $ case nstr of
Nothing -> 0 -- is this appropriate?
Just str -> mkInteger str
PQ.TuplesOk -> do
ncols <- PQ.nfields result
throwIO $ QueryError ("execute resulted in " ++ show ncols ++
"-column result") q
PQ.CopyOut ->
throwIO $ QueryError "execute: COPY TO is not supported" q
PQ.CopyIn ->
throwIO $ QueryError "execute: COPY FROM is not supported" q
PQ.BadResponse -> throwResultError "execute" result status
PQ.NonfatalError -> throwResultError "execute" result status
PQ.FatalError -> throwResultError "execute" result status
where
mkInteger str = B8.foldl' delta 0 str
where
delta acc c =
if '0' <= c && c <= '9'
then 10 * acc + fromIntegral (ord c - ord '0')
else error ("finishExecute: not an int: " ++ B8.unpack str)
throwResultError :: ByteString -> PQ.Result -> PQ.ExecStatus -> IO a
throwResultError _ result status = do
errormsg <- fromMaybe "" <$>
PQ.resultErrorField result PQ.DiagMessagePrimary
detail <- fromMaybe "" <$>
PQ.resultErrorField result PQ.DiagMessageDetail
hint <- fromMaybe "" <$>
PQ.resultErrorField result PQ.DiagMessageHint
state' <- maybe "" id <$> PQ.resultErrorField result PQ.DiagSqlstate
throwIO $ SqlError { sqlState = state'
, sqlExecStatus = status
, sqlErrorMsg = errormsg
, sqlErrorDetail = detail
, sqlErrorHint = hint }
disconnectedError :: SqlError
disconnectedError = fatalError "connection disconnected"
-- | Atomically perform an action with the database handle, if there is one.
withConnection :: Connection -> (PQ.Connection -> IO a) -> IO a
withConnection Connection{..} m = do
withMVar connectionHandle $ \conn -> do
if PQ.isNullConnection conn
then throwIO disconnectedError
else m conn
close :: Connection -> IO ()
close Connection{..} =
mask $ \restore -> (do
conn <- takeMVar connectionHandle
restore (PQ.finish conn)
`finally` do
putMVar connectionHandle =<< PQ.newNullConnection
)
newNullConnection :: IO Connection
newNullConnection = do
connectionHandle <- newMVar =<< PQ.newNullConnection
connectionObjects <- newMVar IntMap.empty
connectionTempNameCounter <- newIORef 0
connectionMayHaveOrphanedStatement <- newIORef False
return Connection{..}
data Row = Row {
row :: {-# UNPACK #-} !PQ.Row
, rowresult :: !PQ.Result
}
newtype RowParser a = RP { unRP :: ReaderT Row (StateT PQ.Column Conversion) a }
deriving ( Functor, Applicative, Alternative, Monad )
liftRowParser :: IO a -> RowParser a
liftRowParser = RP . lift . lift . liftConversion
newtype Conversion a = Conversion { runConversion :: Connection -> IO (Ok a) }
liftConversion :: IO a -> Conversion a
liftConversion m = Conversion (\_ -> Ok <$> m)
instance Functor Conversion where
fmap f m = Conversion $ \conn -> (fmap . fmap) f (runConversion m conn)
instance Applicative Conversion where
pure a = Conversion $ \_conn -> pure (pure a)
mf <*> ma = Conversion $ \conn -> do
okf <- runConversion mf conn
case okf of
Ok f -> (fmap . fmap) f (runConversion ma conn)
Errors errs -> return (Errors errs)
instance Alternative Conversion where
empty = Conversion $ \_conn -> pure empty
ma <|> mb = Conversion $ \conn -> do
oka <- runConversion ma conn
case oka of
Ok _ -> return oka
Errors _ -> (oka <|>) <$> runConversion mb conn
instance Monad Conversion where
#if !(MIN_VERSION_base(4,8,0))
return = pure
#endif
m >>= f = Conversion $ \conn -> do
oka <- runConversion m conn
case oka of
Ok a -> runConversion (f a) conn
Errors err -> return (Errors err)
instance MonadPlus Conversion where
mzero = empty
mplus = (<|>)
conversionMap :: (Ok a -> Ok b) -> Conversion a -> Conversion b
conversionMap f m = Conversion $ \conn -> f <$> runConversion m conn
conversionError :: Exception err => err -> Conversion a
conversionError err = Conversion $ \_ -> return (Errors [toException err])
newTempName :: Connection -> IO Query
newTempName Connection{..} = do
!n <- atomicModifyIORef connectionTempNameCounter
(\n -> let !n' = n+1 in (n', n'))
return $! Query $ B8.pack $ "temp" ++ show n
-- FIXME? What error should getNotification and getCopyData throw?
fdError :: ByteString -> IOError
fdError funcName = IOError {
ioe_handle = Nothing,
ioe_type = ResourceVanished,
ioe_location = B8.unpack funcName,
ioe_description = "failed to fetch file descriptor",
ioe_errno = Nothing,
ioe_filename = Nothing
}
libPQError :: ByteString -> IOError
libPQError desc = IOError {
ioe_handle = Nothing,
ioe_type = OtherError,
ioe_location = "libpq",
ioe_description = B8.unpack desc,
ioe_errno = Nothing,
ioe_filename = Nothing
}
throwLibPQError :: PQ.Connection -> ByteString -> IO a
throwLibPQError conn default_desc = do
msg <- maybe default_desc id <$> PQ.errorMessage conn
throwIO $! libPQError msg
fmtError :: String -> Query -> [Action] -> a
fmtError msg q xs = throw FormatError {
fmtMessage = msg
, fmtQuery = q
, fmtParams = map twiddle xs
}
where twiddle (Plain b) = toByteString b
twiddle (Escape s) = s
twiddle (EscapeByteA s) = s
twiddle (EscapeIdentifier s) = s
twiddle (Many ys) = B.concat (map twiddle ys)
fmtErrorBs :: Query -> [Action] -> ByteString -> a
fmtErrorBs q xs msg = fmtError (T.unpack $ TE.decodeUtf8 msg) q xs
-- | Quote bytestring or throw 'FormatError'
quote :: Query -> [Action] -> Either ByteString ByteString -> Builder
quote q xs = either (fmtErrorBs q xs) (inQuotes . byteString)
buildAction :: Connection -- ^ Connection for string escaping
-> Query -- ^ Query for message error
-> [Action] -- ^ List of parameters for message error
-> Action -- ^ Action to build
-> IO Builder
buildAction _ _ _ (Plain b) = pure b
buildAction conn q xs (Escape s) = quote q xs <$> escapeStringConn conn s
buildAction conn q xs (EscapeByteA s) = quote q xs <$> escapeByteaConn conn s
buildAction conn q xs (EscapeIdentifier s) =
either (fmtErrorBs q xs) byteString <$> escapeIdentifier conn s
buildAction conn q xs (Many ys) =
mconcat <$> mapM (buildAction conn q xs) ys
checkError :: PQ.Connection -> Maybe a -> IO (Either ByteString a)
checkError _ (Just x) = return $ Right x
checkError c Nothing = Left . maybe "" id <$> PQ.errorMessage c
escapeWrap :: (PQ.Connection -> ByteString -> IO (Maybe ByteString))
-> Connection
-> ByteString
-> IO (Either ByteString ByteString)
escapeWrap f conn s =
withConnection conn $ \c ->
f c s >>= checkError c
escapeStringConn :: Connection -> ByteString -> IO (Either ByteString ByteString)
escapeStringConn = escapeWrap PQ.escapeStringConn
escapeIdentifier :: Connection -> ByteString -> IO (Either ByteString ByteString)
escapeIdentifier = escapeWrap PQ.escapeIdentifier
escapeByteaConn :: Connection -> ByteString -> IO (Either ByteString ByteString)
escapeByteaConn = escapeWrap PQ.escapeByteaConn
breakOnSingleQuestionMark :: ByteString -> (ByteString, ByteString)
breakOnSingleQuestionMark b = go (B8.empty, b)
where go (x,bs) = (x `B8.append` x',bs')
-- seperate from first QM
where tup@(noQ, restWithQ) = B8.break (=='?') bs
-- if end of query, just return
-- else check for second QM in 'go2'
(x', bs') = maybe tup go2 $
-- drop found QM and peek at next char
B8.uncons restWithQ >>= B8.uncons . snd
-- another QM after the first means:
-- take literal QM and keep going.
go2 ('?', t2) = go (noQ `B8.snoc` '?',t2)
-- Anything else means
go2 _ = tup