packages feed

hasql-generate-1.0.0: src/Hasql/Generate/Connection.hs

module Hasql.Generate.Connection
    ( ConnectionInfo (..)
    , libpqDefaults
    , toConnString
    , withCompileTimeConnection
    ) where

----------------------------------------------------------------------------------------------------

import           Control.Exception         ( bracket, throwIO )

import           Data.Bool                 ( Bool (..), otherwise, (||) )
import qualified Data.ByteString           as BS
import qualified Data.ByteString.Char8     as BS8
import           Data.Char                 ( Char )
import           Data.Default.Class        ( Default (..) )
import           Data.Eq                   ( (/=), (==) )
import           Data.Functor              ( (<$>) )
import           Data.List
    ( any
    , concatMap
    , null
    , unwords
    , (++)
    )
import           Data.Maybe                ( Maybe (..), catMaybes, maybe )
import           Data.Semigroup            ( (<>) )
import           Data.String               ( String )

import qualified Database.PostgreSQL.LibPQ as PQ

import           Prelude                   ( Applicative (pure), userError )

import           System.IO                 ( IO )

----------------------------------------------------------------------------------------------------

{-  Acquire a libpq connection for compile-time schema introspection, run the
    provided action, and ensure the connection is closed afterward.

    The 'Data.ByteString.ByteString' argument is passed directly to
    @PQ.connectdb@. An empty string causes libpq to read connection
    parameters from standard PostgreSQL environment variables (@PGHOST@,
    @PGPORT@, @PGUSER@, @PGPASSWORD@, @PGDATABASE@, etc.).

    Throws an 'IOError' if the connection cannot be established.
-}
withCompileTimeConnection :: BS8.ByteString -> (PQ.Connection -> IO a) -> IO a
withCompileTimeConnection connStr =
  bracket acquire release
  where
    acquire :: IO PQ.Connection
    acquire = do
      conn <- PQ.connectdb connStr
      status <- PQ.status conn
      if status /= PQ.ConnectionOk
        then do
          msg <- maybe "unknown error" BS8.unpack <$> PQ.errorMessage conn
          PQ.finish conn
          throwIO (userError ("hasql-generate: compile-time DB connection failed: " <> msg))
        else
          pure conn

    release :: PQ.Connection -> IO ()
    release = PQ.finish

----------------------------------------------------------------------------------------------------

{-  Connection configuration for compile-time PostgreSQL introspection.

    @PgSimpleInfo@ lets you specify individual connection fields. Any field
    left as @Nothing@ is omitted from the connection string, so libpq falls
    back to its standard environment variables (@PGHOST@, @PGPORT@, etc.).

    @PgConnectionString@ passes a raw libpq connection string through as-is,
    useful for advanced options like @sslmode=verify-full@.
-}
data ConnectionInfo
    = PgSimpleInfo
      { pgHost     :: Maybe String
      , pgPort     :: Maybe String
      , pgUser     :: Maybe String
      , pgPassword :: Maybe String
      , pgDatabase :: Maybe String
      }
    | PgConnectionString String

{-  All @Nothing@ — produces an empty connection string so libpq reads
    standard environment variables.
-}
instance Default ConnectionInfo where
  def :: ConnectionInfo
  def = libpqDefaults

libpqDefaults :: ConnectionInfo
libpqDefaults = PgSimpleInfo Nothing Nothing Nothing Nothing Nothing

{-  Assemble a 'ConnectionInfo' into a libpq connection 'BS.ByteString'.

    For @PgSimpleInfo@, builds a @key=value@ string from the fields that
    are @Just@, omitting @Nothing@ fields. For @PgConnectionString@, passes
    the raw string through.
-}
toConnString :: ConnectionInfo -> BS.ByteString
toConnString (PgConnectionString s) = BS8.pack s
toConnString (PgSimpleInfo host port user pass db) =
  let pairs =
        catMaybes
          [ kvPair "host" host
          , kvPair "port" port
          , kvPair "user" user
          , kvPair "password" pass
          , kvPair "dbname" db
          ]
   in if null pairs
        then BS.empty
        else BS8.pack (unwords pairs)
  where
    kvPair :: String -> Maybe String -> Maybe String
    kvPair _key Nothing   = Nothing
    kvPair key (Just val) = Just (key <> "=" <> escapeValue val)

    escapeValue :: String -> String
    escapeValue v
      | needsQuoting v = "'" ++ concatMap escapeChar v ++ "'"
      | otherwise = v

    needsQuoting :: String -> Bool
    needsQuoting = any (\c -> c == ' ' || c == '\'' || c == '\\')

    escapeChar :: Char -> String
    escapeChar '\'' = "\\'"
    escapeChar '\\' = "\\\\"
    escapeChar c    = [c]