Delete and ignore tmp folders created by ./verify-package/.

This commit is contained in:
Andreas Ländle 2023-06-24 05:18:19 +02:00
parent 0700c9110f
commit 4dcebea5e5
37 changed files with 1 additions and 7703 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
/builds/
/logs/
/tmp.*/
nightly-*.yaml
lts-*.yaml
*.swp

View File

@ -1,25 +0,0 @@
Copyright (c) 2014-2017, Dylan Simon
Portions Copyright (c) 2010, 2011, Chris Forno
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of postgresql-typed nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL CHRIS FORNO BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,218 +0,0 @@
-- Copyright 2010, 2011, 2012, 2013 Chris Forno
-- Copyright 2014-2015 Dylan Simon
module Database.PostgreSQL.Typed
(
-- *Introduction
-- $intro
PGError(..)
-- *Usage
-- $usage
-- **Connections
-- $connect
, PGDatabase(..)
, defaultPGDatabase
, PGConnection
, pgConnect
, pgDisconnect
, useTPGDatabase
-- **Queries
-- $query
-- ***Compile time
-- $compile
, pgSQL
-- ***Runtime
-- $run
, pgQuery
, pgExecute
, pgTransaction
-- **TemplatePG compatibility
-- $templatepg
-- *Advanced usage
-- **Types
-- $types
-- **A Note About NULL
-- $nulls
-- *Caveats
-- $caveats
-- **Tips
-- $tips
) where
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.TH
import Database.PostgreSQL.Typed.Query
-- $intro
-- PostgreSQL-Typed is designed with 2 goals in mind: safety and performance. The
-- primary focus is on safety.
--
-- To help ensure safety, it uses the PostgreSQL server to parse every query
-- and statement in your code to infer types at compile-time. This means that
-- in theory you cannot get a syntax error at runtime. Getting proper types at
-- compile time has the nice side-effect that it eliminates run-time type
-- casting and usually results in less code. This approach was inspired by
-- MetaHDBC (<http://haskell.org/haskellwiki/MetaHDBC>) and PG'OCaml
-- (<http://pgocaml.berlios.de/>).
--
-- While compile-time query analysis eliminates many errors, it doesn't
-- eliminate all of them. If you modify the database without recompilation or
-- have an error in a trigger or function, for example, you can still trigger a
-- 'PGError' or other failure (if types change). Also, nullable result fields resulting from outer joins are not
-- detected and need to be handled explicitly.
--
-- Based originally on Chris Forno's TemplatePG library.
-- A compatibility interface for that library is provided by "Database.PostgreSQL.Typed.TemplatePG" which can basically function as a drop-in replacement (and also provides an alternative interface with some additional features).
-- $usage
-- Basic usage consists of calling 'pgConnect', 'pgSQL' (Template Haskell quasi-quotation), 'pgQuery', and 'pgDisconnect':
-- You must enable TemplateHaskell and/or QuasiQuotes language extensions.
--
-- > c <- pgConnect
-- > let name = "Joe"
-- > people :: [Int32] <- pgQuery c [pgSQL|SELECT id FROM people WHERE name = ${name}|]
-- > pgDisconnect c
-- $connect
-- All database access requires a 'PGConnection' that is created at runtime using 'pgConnect', and should be explicitly be closed with 'pgDisconnect' when finished.
--
-- However, at compile time, PostgreSQL-Typed needs to make its own connection to the database in order to describe queries.
-- By default, it will use the following environment variables:
--
-- [@TPG_DB@] the database name to use (default: same as user)
--
-- [@TPG_USER@] the username to connect as (default: @$USER@ or @postgres@)
--
-- [@TPG_PASS@] the password to use (default: /empty/)
--
-- [@TPG_HOST@] the host to connect to (default: @localhost@)
--
-- [@TPG_PORT@ or @TPG_SOCK@] the port number or local socket path to connect on (default: @5432@)
--
-- If you'd like to specify what connection to use directly, use 'useTPGDatabase' at the top level:
--
-- > useTPGDatabase PGDatabase{ ... }
--
-- Note that due to TH limitations, the database must be in-line or in a different module. This call must be processed by the compiler before (above) any other TH calls.
--
-- You can set @TPG_DEBUG@ at compile or runtime to get a protocol-level trace.
-- $query
-- There are two steps to running a query: a Template Haskell quasiquoter to perform type-inference at compile time and create a 'PGQuery'; and a run-time function to execute the query ('pgRunQuery', 'pgQuery', 'pgExecute').
-- $compile
-- Both TH functions take a single SQL string, which may contain in-line placeholders of the form @${expr}@ (where @expr@ is any valid Haskell expression) and/or PostgreSQL placeholders of the form @$1@, @$2@, etc.
--
-- > let q = [pgSQL|SELECT id, name, address FROM people WHERE name LIKE ${query++"%"} OR email LIKE $1|] :: PGSimpleQuery [(Int32, String, Maybe String)]
--
-- Expression placeholders are substituted with PostgreSQL ones in left-to-right order starting with 1, so must be in places that PostgreSQL allows them (e.g., not identifiers, table names, column names, operators, etc.)
-- However, this does mean that you can repeat expressions using the corresponding PostgreSQL placeholder as above.
-- If there are extra PostgreSQL parameters the may be passed as arguments:
--
-- > [pgSQL|SELECT id FROM people WHERE name = $1|] :: String -> PGSimpleQuery [Int32]
--
-- To produce 'PGPreparedQuery' objects instead of 'PGSimpleQuery', put a single @$@ at the beginning of the query.
-- You can also create queries at run-time using 'rawPGSimpleQuery' or 'rawPGPreparedQuery'.
-- $run
-- There are multiple ways to run a 'PGQuery' once it's created ('pgQuery', 'pgExecute'), and you can also write your own, but they all reduce to 'pgRunQuery'.
-- These all take a 'PGConnection' and a 'PGQuery', and return results.
-- How they work depends on the type of query.
--
-- 'PGSimpleQuery' simply substitutes the placeholder values literally into into the SQL statement. This should be safe for all currently-supported types.
--
-- 'PGPreparedQuery' is a bit more complex: the first time any given prepared query is run on a given connection, the query is prepared. Every subsequent time, the previously-prepared query is re-used and the new placeholder values are bound to it.
-- Queries are identified by the text of the SQL statement with PostgreSQL placeholders in-place, so the exact parameter values do not matter (but the exact SQL statement, whitespace, etc. does).
-- (Prepared queries are released automatically at 'pgDisconnect', but may be closed early using 'Database.PostgreSQL.Typed.Protocol.pgCloseQuery'.)
-- $templatepg
-- There is also an older, simpler interface based on TemplatePG that combines both the compile and runtime steps.
-- 'Database.PostgreSQL.Typed.TemplatePG.queryTuples' does all the work ('Database.PostgreSQL.Typed.TemplatePG.queryTuple' and 'Database.PostgreSQL.Typed.TemplatePG.execute' are convenience
-- functions).
--
-- It's a Template Haskell function, so you need to splice it into your program
-- with @$()@. It requires a 'PGConnection' to a PostgreSQL server, but can't be
-- given one at compile-time, so you need to pass it after the splice:
--
-- > h <- pgConnect ...
-- > tuples <- $(queryTuples "SELECT * FROM pg_database") h
--
-- To pass parameters to a query, include them in the string with {}. Most
-- Haskell expressions should work. For example:
--
-- > let owner = 33 :: Int32
-- > tuples <- $(queryTuples "SELECT * FROM pg_database WHERE datdba = {owner} LIMIT {2 * 3 :: Int64}") h
--
-- TemplatePG provides 'Database.PostgreSQL.Typed.TemplatePG.withTransaction', 'Database.PostgreSQL.Typed.TemplatePG.rollback', and 'Database.PostgreSQL.Typed.TemplatePG.insertIgnore', but they've
-- not been thoroughly tested, so use them at your own risk.
-- $types
-- Most builtin types are already supported.
-- For the most part, exactly equivalent types are all supported (e.g., 'Int32' for int4) as well as other safe equivalents, but you cannot, for example, pass an 'Integer' as a @smallint@.
-- To achieve this flexibility, the exact types of all parameters and results must be fully known (e.g., numeric literals will not work).
--
-- However you can add support for your own types or add flexibility to existing types by creating new instances of 'Database.PostgreSQL.Typed.Types.PGParameter' (for encoding) and 'Database.PostgreSQL.Typed.Types.PGColumn' (for decoding).
--
-- > instance PGType "mytype"
-- > instance PGParameter "mytype" MyType where
-- > pgEncode _ (v :: MyType) = ... :: ByteString
-- > instance PGColumn "mytype" MyType where
-- > pgDecode _ (s :: ByteString) = ... :: MyType
--
-- You can make as many 'PGParameter' and 'PGColumn' instances as you want if you want to support different representations of your type.
-- If you want to use any of the functions in "Database.PostgreSQL.Typed.Dynamic", however, such as 'Database.PostgreSQL.Typed.Dynamic.pgSafeLiteral', you must define a default representation:
--
-- > instance PGRep MyType where type PGRepType MyType = "mytype"
--
-- If you want to support arrays of your new type, you should also provide a 'Database.PostgreSQL.Typed.Array.PGArrayType' instance (or 'Database.PostgreSQL.Typed.Range.PGRangeType' for new ranges).
-- Currently only 1-dimensional arrays are supported.
--
-- > instance PGType "mytype[]"
-- > instance PGArrayType "mytype[]" "mytype"
--
-- Required language extensions: FlexibleInstances, MultiParamTypeClasses, DataKinds
-- $nulls
-- Sometimes PostgreSQL cannot automatically determine whether or not a result field can
-- potentially be @NULL@. In those cases it will assume that it can. Basically,
-- any time a result field is not immediately traceable to an originating table
-- and column (such as when a function is applied to a result column), it's
-- assumed to be nullable and will be returned as a 'Maybe' value. Other values may be decoded without the 'Maybe' wrapper.
--
-- You can use @NULL@ values in parameters as well by using 'Maybe'.
-- $caveats
-- The types of all parameters and results must be fully known. This may
-- require explicit casts in some cases (especially with numeric literals).
--
-- You cannot construct queries at run-time, since they
-- wouldn't be available to be analyzed at compile time (but you can construct them at compile time by writing your own TH functions).
--
-- Because of how PostgreSQL handles placeholders, they cannot be used in place of lists (such as @IN (?)@). You must replace such cases with equivalent arrays (@= ANY (?)@).
--
-- For the most part, any code must be compiled and run against databases that are at least structurally identical.
-- Furthermore, prepared queries also store OIDs for user types, so the generated 'PGPreparedQuery' can only be run on the exact same database or one restored from a dump with OIDs (@pg_dump -o@). If this is a concern, only use built-in types in prepared queries.
-- (This requirement could be weakened with some work, if there were need.)
-- $tips
-- If you find yourself pattern matching on result tuples just to pass them on
-- to functions, you can use @uncurryN@ from the tuple package. The following
-- examples are equivalent.
--
-- > (a, b, c) <- $(queryTuple "SELECT a, b, c FROM table LIMIT 1")
-- > someFunction a b c
-- > uncurryN someFunction `liftM` $(queryTuple "SELECT a, b, c FROM table LIMIT 1")

View File

@ -1,336 +0,0 @@
{-# LANGUAGE CPP, MultiParamTypeClasses, FlexibleContexts, FlexibleInstances, UndecidableInstances, DataKinds, OverloadedStrings, TypeFamilies #-}
#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE UndecidableSuperClasses #-}
#endif
{-# OPTIONS_GHC -fno-warn-orphans #-}
#if __GLASGOW_HASKELL__ >= 800
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
#endif
-- |
-- Module: Database.PostgreSQL.Typed.Array
-- Copyright: 2015 Dylan Simon
--
-- Representaion of PostgreSQL's array type.
-- Currently this only supports one-dimensional arrays.
-- PostgreSQL arrays in theory can dynamically be any (rectangular) shape.
module Database.PostgreSQL.Typed.Array where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (*>), (<*))
#endif
import qualified Data.Attoparsec.ByteString.Char8 as P
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Char8 as BSC
import Data.Char (toLower)
import Data.List (intersperse)
import Data.Monoid ((<>))
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (mconcat)
#endif
import GHC.TypeLits (Symbol)
import Database.PostgreSQL.Typed.Types
-- |The cannonical representation of a PostgreSQL array of any type, which may always contain NULLs.
-- Currenly only one-dimetional arrays are supported, although in PostgreSQL, any array may be of any dimentionality.
type PGArray a = [Maybe a]
-- |Class indicating that the first PostgreSQL type is an array of the second.
-- This implies 'PGParameter' and 'PGColumn' instances that will work for any type using comma as a delimiter (i.e., anything but @box@).
-- This will only work with 1-dimensional arrays.
class (PGType t, PGType (PGElemType t)) => PGArrayType t where
type PGElemType t :: Symbol
pgArrayElementType :: PGTypeID t -> PGTypeID (PGElemType t)
pgArrayElementType PGTypeProxy = PGTypeProxy
-- |The character used as a delimeter. The default @,@ is correct for all standard types (except @box@).
pgArrayDelim :: PGTypeID t -> Char
pgArrayDelim _ = ','
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPING #-}
#endif
(PGArrayType t, PGParameter (PGElemType t) a) => PGParameter t (PGArray a) where
pgEncode ta l = buildPGValue $ BSB.char7 '{' <> mconcat (intersperse (BSB.char7 $ pgArrayDelim ta) $ map el l) <> BSB.char7 '}' where
el Nothing = BSB.string7 "null"
el (Just e) = pgDQuoteFrom (pgArrayDelim ta : "{}") $ pgEncode (pgArrayElementType ta) e
#if __GLASGOW_HASKELL__ >= 710
-- |Allow entirely non-null arrays as parameter inputs only.
-- (Only supported on ghc >= 7.10 due to instance overlap.)
instance {-# OVERLAPPABLE #-} (PGArrayType t, PGParameter (PGElemType t) a) => PGParameter t [a] where
pgEncode ta = pgEncode ta . map Just
#endif
instance (PGArrayType t, PGColumn (PGElemType t) a) => PGColumn t (PGArray a) where
pgDecode ta a = either (error . ("pgDecode array (" ++) . (++ ("): " ++ BSC.unpack a))) id $ P.parseOnly pa a where
pa = P.char '{' *> P.sepBy (P.skipSpace *> el <* P.skipSpace) (P.char (pgArrayDelim ta)) <* P.char '}' <* P.endOfInput
el = fmap (pgDecode (pgArrayElementType ta)) <$>
parsePGDQuote False (pgArrayDelim ta : "{}") (("null" ==) . BSC.map toLower)
-- Just a dump of pg_type:
instance PGType "boolean" => PGType "boolean[]" where
type PGVal "boolean[]" = PGArray (PGVal "boolean")
instance PGType "boolean" => PGArrayType "boolean[]" where
type PGElemType "boolean[]" = "boolean"
instance PGType "bytea" => PGType "bytea[]" where
type PGVal "bytea[]" = PGArray (PGVal "bytea")
instance PGType "bytea" => PGArrayType "bytea[]" where
type PGElemType "bytea[]" = "bytea"
instance PGType "\"char\"" => PGType "\"char\"[]" where
type PGVal "\"char\"[]" = PGArray (PGVal "\"char\"")
instance PGType "\"char\"" => PGArrayType "\"char\"[]" where
type PGElemType "\"char\"[]" = "\"char\""
instance PGType "name" => PGType "name[]" where
type PGVal "name[]" = PGArray (PGVal "name")
instance PGType "name" => PGArrayType "name[]" where
type PGElemType "name[]" = "name"
instance PGType "bigint" => PGType "bigint[]" where
type PGVal "bigint[]" = PGArray (PGVal "bigint")
instance PGType "bigint" => PGArrayType "bigint[]" where
type PGElemType "bigint[]" = "bigint"
instance PGType "smallint" => PGType "smallint[]" where
type PGVal "smallint[]" = PGArray (PGVal "smallint")
instance PGType "smallint" => PGArrayType "smallint[]" where
type PGElemType "smallint[]" = "smallint"
instance PGType "int2vector" => PGType "int2vector[]" where
type PGVal "int2vector[]" = PGArray (PGVal "int2vector")
instance PGType "int2vector" => PGArrayType "int2vector[]" where
type PGElemType "int2vector[]" = "int2vector"
instance PGType "integer" => PGType "integer[]" where
type PGVal "integer[]" = PGArray (PGVal "integer")
instance PGType "integer" => PGArrayType "integer[]" where
type PGElemType "integer[]" = "integer"
instance PGType "regproc" => PGType "regproc[]" where
type PGVal "regproc[]" = PGArray (PGVal "regproc")
instance PGType "regproc" => PGArrayType "regproc[]" where
type PGElemType "regproc[]" = "regproc"
instance PGType "text" => PGType "text[]" where
type PGVal "text[]" = PGArray (PGVal "text")
instance PGType "text" => PGArrayType "text[]" where
type PGElemType "text[]" = "text"
instance PGType "oid" => PGType "oid[]" where
type PGVal "oid[]" = PGArray (PGVal "oid")
instance PGType "oid" => PGArrayType "oid[]" where
type PGElemType "oid[]" = "oid"
instance PGType "tid" => PGType "tid[]" where
type PGVal "tid[]" = PGArray (PGVal "tid")
instance PGType "tid" => PGArrayType "tid[]" where
type PGElemType "tid[]" = "tid"
instance PGType "xid" => PGType "xid[]" where
type PGVal "xid[]" = PGArray (PGVal "xid")
instance PGType "xid" => PGArrayType "xid[]" where
type PGElemType "xid[]" = "xid"
instance PGType "cid" => PGType "cid[]" where
type PGVal "cid[]" = PGArray (PGVal "cid")
instance PGType "cid" => PGArrayType "cid[]" where
type PGElemType "cid[]" = "cid"
instance PGType "oidvector" => PGType "oidvector[]" where
type PGVal "oidvector[]" = PGArray (PGVal "oidvector")
instance PGType "oidvector" => PGArrayType "oidvector[]" where
type PGElemType "oidvector[]" = "oidvector"
instance PGType "json" => PGType "json[]" where
type PGVal "json[]" = PGArray (PGVal "json")
instance PGType "json" => PGArrayType "json[]" where
type PGElemType "json[]" = "json"
instance PGType "xml" => PGType "xml[]" where
type PGVal "xml[]" = PGArray (PGVal "xml")
instance PGType "xml" => PGArrayType "xml[]" where
type PGElemType "xml[]" = "xml"
instance PGType "point" => PGType "point[]" where
type PGVal "point[]" = PGArray (PGVal "point")
instance PGType "point" => PGArrayType "point[]" where
type PGElemType "point[]" = "point"
instance PGType "lseg" => PGType "lseg[]" where
type PGVal "lseg[]" = PGArray (PGVal "lseg")
instance PGType "lseg" => PGArrayType "lseg[]" where
type PGElemType "lseg[]" = "lseg"
instance PGType "path" => PGType "path[]" where
type PGVal "path[]" = PGArray (PGVal "path")
instance PGType "path" => PGArrayType "path[]" where
type PGElemType "path[]" = "path"
instance PGType "box" => PGType "box[]" where
type PGVal "box[]" = PGArray (PGVal "box")
instance PGType "box" => PGArrayType "box[]" where
type PGElemType "box[]" = "box"
pgArrayDelim _ = ';'
instance PGType "polygon" => PGType "polygon[]" where
type PGVal "polygon[]" = PGArray (PGVal "polygon")
instance PGType "polygon" => PGArrayType "polygon[]" where
type PGElemType "polygon[]" = "polygon"
instance PGType "line" => PGType "line[]" where
type PGVal "line[]" = PGArray (PGVal "line")
instance PGType "line" => PGArrayType "line[]" where
type PGElemType "line[]" = "line"
instance PGType "cidr" => PGType "cidr[]" where
type PGVal "cidr[]" = PGArray (PGVal "cidr")
instance PGType "cidr" => PGArrayType "cidr[]" where
type PGElemType "cidr[]" = "cidr"
instance PGType "real" => PGType "real[]" where
type PGVal "real[]" = PGArray (PGVal "real")
instance PGType "real" => PGArrayType "real[]" where
type PGElemType "real[]" = "real"
instance PGType "double precision" => PGType "double precision[]" where
type PGVal "double precision[]" = PGArray (PGVal "double precision")
instance PGType "double precision" => PGArrayType "double precision[]" where
type PGElemType "double precision[]" = "double precision"
instance PGType "abstime" => PGType "abstime[]" where
type PGVal "abstime[]" = PGArray (PGVal "abstime")
instance PGType "abstime" => PGArrayType "abstime[]" where
type PGElemType "abstime[]" = "abstime"
instance PGType "reltime" => PGType "reltime[]" where
type PGVal "reltime[]" = PGArray (PGVal "reltime")
instance PGType "reltime" => PGArrayType "reltime[]" where
type PGElemType "reltime[]" = "reltime"
instance PGType "tinterval" => PGType "tinterval[]" where
type PGVal "tinterval[]" = PGArray (PGVal "tinterval")
instance PGType "tinterval" => PGArrayType "tinterval[]" where
type PGElemType "tinterval[]" = "tinterval"
instance PGType "circle" => PGType "circle[]" where
type PGVal "circle[]" = PGArray (PGVal "circle")
instance PGType "circle" => PGArrayType "circle[]" where
type PGElemType "circle[]" = "circle"
instance PGType "money" => PGType "money[]" where
type PGVal "money[]" = PGArray (PGVal "money")
instance PGType "money" => PGArrayType "money[]" where
type PGElemType "money[]" = "money"
instance PGType "macaddr" => PGType "macaddr[]" where
type PGVal "macaddr[]" = PGArray (PGVal "macaddr")
instance PGType "macaddr" => PGArrayType "macaddr[]" where
type PGElemType "macaddr[]" = "macaddr"
instance PGType "inet" => PGType "inet[]" where
type PGVal "inet[]" = PGArray (PGVal "inet")
instance PGType "inet" => PGArrayType "inet[]" where
type PGElemType "inet[]" = "inet"
instance PGType "aclitem" => PGType "aclitem[]" where
type PGVal "aclitem[]" = PGArray (PGVal "aclitem")
instance PGType "aclitem" => PGArrayType "aclitem[]" where
type PGElemType "aclitem[]" = "aclitem"
instance PGType "bpchar" => PGType "bpchar[]" where
type PGVal "bpchar[]" = PGArray (PGVal "bpchar")
instance PGType "bpchar" => PGArrayType "bpchar[]" where
type PGElemType "bpchar[]" = "bpchar"
instance PGType "character varying" => PGType "character varying[]" where
type PGVal "character varying[]" = PGArray (PGVal "character varying")
instance PGType "character varying" => PGArrayType "character varying[]" where
type PGElemType "character varying[]" = "character varying"
instance PGType "date" => PGType "date[]" where
type PGVal "date[]" = PGArray (PGVal "date")
instance PGType "date" => PGArrayType "date[]" where
type PGElemType "date[]" = "date"
instance PGType "time without time zone" => PGType "time without time zone[]" where
type PGVal "time without time zone[]" = PGArray (PGVal "time without time zone")
instance PGType "time without time zone" => PGArrayType "time without time zone[]" where
type PGElemType "time without time zone[]" = "time without time zone"
instance PGType "timestamp without time zone" => PGType "timestamp without time zone[]" where
type PGVal "timestamp without time zone[]" = PGArray (PGVal "timestamp without time zone")
instance PGType "timestamp without time zone" => PGArrayType "timestamp without time zone[]" where
type PGElemType "timestamp without time zone[]" = "timestamp without time zone"
instance PGType "timestamp with time zone" => PGType "timestamp with time zone[]" where
type PGVal "timestamp with time zone[]" = PGArray (PGVal "timestamp with time zone")
instance PGType "timestamp with time zone" => PGArrayType "timestamp with time zone[]" where
type PGElemType "timestamp with time zone[]" = "timestamp with time zone"
instance PGType "interval" => PGType "interval[]" where
type PGVal "interval[]" = PGArray (PGVal "interval")
instance PGType "interval" => PGArrayType "interval[]" where
type PGElemType "interval[]" = "interval"
instance PGType "time with time zone" => PGType "time with time zone[]" where
type PGVal "time with time zone[]" = PGArray (PGVal "time with time zone")
instance PGType "time with time zone" => PGArrayType "time with time zone[]" where
type PGElemType "time with time zone[]" = "time with time zone"
instance PGType "bit" => PGType "bit[]" where
type PGVal "bit[]" = PGArray (PGVal "bit")
instance PGType "bit" => PGArrayType "bit[]" where
type PGElemType "bit[]" = "bit"
instance PGType "varbit" => PGType "varbit[]" where
type PGVal "varbit[]" = PGArray (PGVal "varbit")
instance PGType "varbit" => PGArrayType "varbit[]" where
type PGElemType "varbit[]" = "varbit"
instance PGType "numeric" => PGType "numeric[]" where
type PGVal "numeric[]" = PGArray (PGVal "numeric")
instance PGType "numeric" => PGArrayType "numeric[]" where
type PGElemType "numeric[]" = "numeric"
instance PGType "refcursor" => PGType "refcursor[]" where
type PGVal "refcursor[]" = PGArray (PGVal "refcursor")
instance PGType "refcursor" => PGArrayType "refcursor[]" where
type PGElemType "refcursor[]" = "refcursor"
instance PGType "regprocedure" => PGType "regprocedure[]" where
type PGVal "regprocedure[]" = PGArray (PGVal "regprocedure")
instance PGType "regprocedure" => PGArrayType "regprocedure[]" where
type PGElemType "regprocedure[]" = "regprocedure"
instance PGType "regoper" => PGType "regoper[]" where
type PGVal "regoper[]" = PGArray (PGVal "regoper")
instance PGType "regoper" => PGArrayType "regoper[]" where
type PGElemType "regoper[]" = "regoper"
instance PGType "regoperator" => PGType "regoperator[]" where
type PGVal "regoperator[]" = PGArray (PGVal "regoperator")
instance PGType "regoperator" => PGArrayType "regoperator[]" where
type PGElemType "regoperator[]" = "regoperator"
instance PGType "regclass" => PGType "regclass[]" where
type PGVal "regclass[]" = PGArray (PGVal "regclass")
instance PGType "regclass" => PGArrayType "regclass[]" where
type PGElemType "regclass[]" = "regclass"
instance PGType "regtype" => PGType "regtype[]" where
type PGVal "regtype[]" = PGArray (PGVal "regtype")
instance PGType "regtype" => PGArrayType "regtype[]" where
type PGElemType "regtype[]" = "regtype"
instance PGType "record" => PGType "record[]" where
type PGVal "record[]" = PGArray (PGVal "record")
instance PGType "record" => PGArrayType "record[]" where
type PGElemType "record[]" = "record"
instance PGType "cstring" => PGType "cstring[]" where
type PGVal "cstring[]" = PGArray (PGVal "cstring")
instance PGType "cstring" => PGArrayType "cstring[]" where
type PGElemType "cstring[]" = "cstring"
instance PGType "uuid" => PGType "uuid[]" where
type PGVal "uuid[]" = PGArray (PGVal "uuid")
instance PGType "uuid" => PGArrayType "uuid[]" where
type PGElemType "uuid[]" = "uuid"
instance PGType "txid_snapshot" => PGType "txid_snapshot[]" where
type PGVal "txid_snapshot[]" = PGArray (PGVal "txid_snapshot")
instance PGType "txid_snapshot" => PGArrayType "txid_snapshot[]" where
type PGElemType "txid_snapshot[]" = "txid_snapshot"
instance PGType "tsvector" => PGType "tsvector[]" where
type PGVal "tsvector[]" = PGArray (PGVal "tsvector")
instance PGType "tsvector" => PGArrayType "tsvector[]" where
type PGElemType "tsvector[]" = "tsvector"
instance PGType "tsquery" => PGType "tsquery[]" where
type PGVal "tsquery[]" = PGArray (PGVal "tsquery")
instance PGType "tsquery" => PGArrayType "tsquery[]" where
type PGElemType "tsquery[]" = "tsquery"
instance PGType "gtsvector" => PGType "gtsvector[]" where
type PGVal "gtsvector[]" = PGArray (PGVal "gtsvector")
instance PGType "gtsvector" => PGArrayType "gtsvector[]" where
type PGElemType "gtsvector[]" = "gtsvector"
instance PGType "regconfig" => PGType "regconfig[]" where
type PGVal "regconfig[]" = PGArray (PGVal "regconfig")
instance PGType "regconfig" => PGArrayType "regconfig[]" where
type PGElemType "regconfig[]" = "regconfig"
instance PGType "regdictionary" => PGType "regdictionary[]" where
type PGVal "regdictionary[]" = PGArray (PGVal "regdictionary")
instance PGType "regdictionary" => PGArrayType "regdictionary[]" where
type PGElemType "regdictionary[]" = "regdictionary"
instance PGType "int4range" => PGType "int4range[]" where
type PGVal "int4range[]" = PGArray (PGVal "int4range")
instance PGType "int4range" => PGArrayType "int4range[]" where
type PGElemType "int4range[]" = "int4range"
instance PGType "numrange" => PGType "numrange[]" where
type PGVal "numrange[]" = PGArray (PGVal "numrange")
instance PGType "numrange" => PGArrayType "numrange[]" where
type PGElemType "numrange[]" = "numrange"
instance PGType "tsrange" => PGType "tsrange[]" where
type PGVal "tsrange[]" = PGArray (PGVal "tsrange")
instance PGType "tsrange" => PGArrayType "tsrange[]" where
type PGElemType "tsrange[]" = "tsrange"
instance PGType "tstzrange" => PGType "tstzrange[]" where
type PGVal "tstzrange[]" = PGArray (PGVal "tstzrange")
instance PGType "tstzrange" => PGArrayType "tstzrange[]" where
type PGElemType "tstzrange[]" = "tstzrange"
instance PGType "daterange" => PGType "daterange[]" where
type PGVal "daterange[]" = PGArray (PGVal "daterange")
instance PGType "daterange" => PGArrayType "daterange[]" where
type PGElemType "daterange[]" = "daterange"
instance PGType "int8range" => PGType "int8range[]" where
type PGVal "int8range[]" = PGArray (PGVal "int8range")
instance PGType "int8range" => PGArrayType "int8range[]" where
type PGElemType "int8range[]" = "int8range"

View File

@ -1,157 +0,0 @@
{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses, ScopedTypeVariables, DataKinds, DefaultSignatures, TemplateHaskell, TypeFamilies #-}
#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE UndecidableSuperClasses #-}
#endif
-- |
-- Module: Database.PostgreSQL.Typed.Dynamic
-- Copyright: 2015 Dylan Simon
--
-- Automatic (dynamic) marshalling of PostgreSQL values based on Haskell types (not SQL statements).
-- This is intended for direct construction of queries and query data, bypassing the normal SQL type inference.
module Database.PostgreSQL.Typed.Dynamic
( PGRep(..)
, pgTypeOf
, pgTypeOfProxy
, pgEncodeRep
, pgDecodeRep
, pgLiteralRep
, pgLiteralString
, pgSafeLiteral
, pgSafeLiteralString
, pgSubstituteLiterals
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif
#ifdef VERSION_aeson
import qualified Data.Aeson as JSON
#endif
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.ByteString.Internal (w2c)
import qualified Data.ByteString.Lazy as BSL
import Data.Int
import Data.Monoid ((<>))
import Data.Proxy (Proxy)
#ifdef VERSION_scientific
import Data.Scientific (Scientific)
#endif
import Data.String (fromString)
#ifdef VERSION_text
import qualified Data.Text as T
#endif
import qualified Data.Time as Time
#ifdef VERSION_uuid
import qualified Data.UUID as UUID
#endif
import GHC.TypeLits (Symbol)
import Language.Haskell.Meta.Parse (parseExp)
import qualified Language.Haskell.TH as TH
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.SQLToken
-- |Represents canonical/default PostgreSQL representation for various Haskell types, allowing convenient type-driven marshalling.
class (PGParameter (PGRepType a) a, PGColumn (PGRepType a) a) => PGRep a where
-- |The PostgreSOL type that this type should be converted to.
type PGRepType a :: Symbol
pgTypeOf :: a -> PGTypeID (PGRepType a)
pgTypeOf _ = PGTypeProxy
pgTypeOfProxy :: Proxy a -> PGTypeID (PGRepType a)
pgTypeOfProxy _ = PGTypeProxy
-- |Encode a value using 'pgEncodeValue'.
pgEncodeRep :: PGRep a => a -> PGValue
pgEncodeRep x = pgEncodeValue unknownPGTypeEnv (pgTypeOf x) x
-- |Produce a literal value for interpolation in a SQL statement using 'pgLiteral'. Using 'pgSafeLiteral' is usually safer as it includes type cast.
pgLiteralRep :: PGRep a => a -> BS.ByteString
pgLiteralRep x = pgLiteral (pgTypeOf x) x
-- |Decode a value using 'pgDecodeValue'.
pgDecodeRep :: forall a . PGRep a => PGValue -> a
pgDecodeRep = pgDecodeValue unknownPGTypeEnv (PGTypeProxy :: PGTypeID (PGRepType a))
-- |Produce a raw SQL literal from a value. Using 'pgSafeLiteral' is usually safer when interpolating in a SQL statement.
pgLiteralString :: PGRep a => a -> String
pgLiteralString = BSC.unpack . pgLiteralRep
-- |Produce a safely type-cast literal value for interpolation in a SQL statement, e.g., "'123'::integer".
pgSafeLiteral :: PGRep a => a -> BS.ByteString
pgSafeLiteral x = pgLiteralRep x <> BSC.pack "::" <> pgNameBS (pgTypeName (pgTypeOf x))
-- |Identical to @'BSC.unpack' . 'pgSafeLiteral'@ but more efficient.
pgSafeLiteralString :: PGRep a => a -> String
pgSafeLiteralString x = pgLiteralString x ++ "::" ++ map w2c (pgNameBytes (pgTypeName (pgTypeOf x)))
instance PGRep a => PGRep (Maybe a) where
type PGRepType (Maybe a) = PGRepType a
instance PGRep () where
type PGRepType () = "void"
instance PGRep Bool where
type PGRepType Bool = "boolean"
instance PGRep OID where
type PGRepType OID = "oid"
instance PGRep Int16 where
type PGRepType Int16 = "smallint"
instance PGRep Int32 where
type PGRepType Int32 = "integer"
instance PGRep Int64 where
type PGRepType Int64 = "bigint"
instance PGRep Float where
type PGRepType Float = "real"
instance PGRep Double where
type PGRepType Double = "double precision"
instance PGRep Char where
type PGRepType Char = "\"char\""
instance PGRep String where
type PGRepType String = "text"
instance PGRep BS.ByteString where
type PGRepType BS.ByteString = "text"
instance PGRep PGName where
type PGRepType PGName = "text" -- superset of "name"
#ifdef VERSION_text
instance PGRep T.Text where
type PGRepType T.Text = "text"
#endif
instance PGRep Time.Day where
type PGRepType Time.Day = "date"
instance PGRep Time.TimeOfDay where
type PGRepType Time.TimeOfDay = "time without time zone"
instance PGRep (Time.TimeOfDay, Time.TimeZone) where
type PGRepType (Time.TimeOfDay, Time.TimeZone) = "time with time zone"
instance PGRep Time.LocalTime where
type PGRepType Time.LocalTime = "timestamp without time zone"
instance PGRep Time.UTCTime where
type PGRepType Time.UTCTime = "timestamp with time zone"
instance PGRep Time.DiffTime where
type PGRepType Time.DiffTime = "interval"
instance PGRep Rational where
type PGRepType Rational = "numeric"
#ifdef VERSION_scientific
instance PGRep Scientific where
type PGRepType Scientific = "numeric"
#endif
#ifdef VERSION_uuid
instance PGRep UUID.UUID where
type PGRepType UUID.UUID = "uuid"
#endif
#ifdef VERSION_aeson
instance PGRep JSON.Value where
type PGRepType JSON.Value = "jsonb"
#endif
-- |Create an expression that literally substitutes each instance of @${expr}@ for the result of @pgSafeLiteral expr@, producing a lazy 'BSL.ByteString'.
-- This lets you do safe, type-driven literal substitution into SQL fragments without needing a full query, bypassing placeholder inference and any prepared queries, for example when using 'Database.PostgreSQL.Typed.Protocol.pgSimpleQuery' or 'Database.PostgreSQL.Typed.Protocol.pgSimpleQueries_'.
-- Unlike most other TH functions, this does not require any database connection.
pgSubstituteLiterals :: String -> TH.ExpQ
pgSubstituteLiterals sql = TH.AppE (TH.VarE 'BSL.fromChunks) . TH.ListE <$> mapM sst (sqlTokens sql) where
sst (SQLExpr e) = do
v <- either (fail . (++) ("Failed to parse expression {" ++ e ++ "}: ")) return $ parseExp e
return $ TH.VarE 'pgSafeLiteral `TH.AppE` v
sst t = return $ TH.VarE 'fromString `TH.AppE` TH.LitE (TH.StringL $ show t)

View File

@ -1,150 +0,0 @@
{-# LANGUAGE CPP, TemplateHaskell, FlexibleInstances, MultiParamTypeClasses, DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE UndecidableSuperClasses #-}
#endif
-- |
-- Module: Database.PostgreSQL.Typed.Enum
-- Copyright: 2015 Dylan Simon
--
-- Support for PostgreSQL enums.
module Database.PostgreSQL.Typed.Enum
( PGEnum(..)
, dataPGEnum
) where
import Control.Arrow ((&&&))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as BSL
import Data.Ix (Ix)
import Data.Maybe (fromJust, fromMaybe)
import Data.Tuple (swap)
import Data.Typeable (Typeable)
import qualified Language.Haskell.TH as TH
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Dynamic
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.TypeCache
import Database.PostgreSQL.Typed.TH
-- |A type based on a PostgreSQL enum. Automatically instantiated by 'dataPGEnum'.
class (Eq a, Ord a, Enum a, Bounded a, PGRep a) => PGEnum a where
{-# MINIMAL pgEnumName | pgEnumValues #-}
-- |The database name of a value.
pgEnumName :: a -> PGName
pgEnumName a = fromJust $ lookup a pgEnumValues
-- |Lookup a value matching the given database name.
pgEnumValue :: PGName -> Maybe a
pgEnumValue n = lookup n $ map swap pgEnumValues
-- |List of all the values in the enum along with their database names.
pgEnumValues :: [(a, PGName)]
pgEnumValues = map (id &&& pgEnumName) $ enumFromTo minBound maxBound
-- |Create a new enum type corresponding to the given PostgreSQL enum type.
-- For example, if you have @CREATE TYPE foo AS ENUM (\'abc\', \'DEF\')@, then
-- @dataPGEnum \"Foo\" \"foo\" (\"Foo_\"++)@ will be equivalent to:
--
-- > data Foo = Foo_abc | Foo_DEF deriving (Eq, Ord, Enum, Bounded, Typeable)
-- > instance PGType "foo" where PGVal "foo" = Foo
-- > instance PGParameter "foo" Foo where ...
-- > instance PGColumn "foo" Foo where ...
-- > instance PGRep Foo where PGRepType = "foo"
-- > instance PGEnum Foo where pgEnumValues = [(Foo_abc, "abc"), (Foo_DEF, "DEF")]
--
-- Requires language extensions: TemplateHaskell, FlexibleInstances, MultiParamTypeClasses, DeriveDataTypeable, DataKinds, TypeFamilies
dataPGEnum :: String -- ^ Haskell type to create
-> PGName -- ^ PostgreSQL enum type name
-> (String -> String) -- ^ How to generate constructor names from enum values, e.g. @(\"Type_\"++)@ (input is 'pgNameString')
-> TH.DecsQ
dataPGEnum typs pgenum valnf = do
(pgid, vals) <- TH.runIO $ withTPGTypeConnection $ \tpg -> do
vals <- map (\([eo, v]) -> (pgDecodeRep eo, pgDecodeRep v)) . snd
<$> pgSimpleQuery (pgConnection tpg) (BSL.fromChunks
[ "SELECT enumtypid, enumlabel"
, " FROM pg_catalog.pg_enum"
, " WHERE enumtypid = ", pgLiteralRep pgenum, "::regtype"
, " ORDER BY enumsortorder"
])
case vals of
[] -> fail $ "dataPGEnum " ++ typs ++ " = " ++ show pgenum ++ ": no values found"
(eo, _):_ -> do
et <- maybe (fail $ "dataPGEnum " ++ typs ++ " = " ++ show pgenum ++ ": enum type not found (you may need to use reloadTPGTypes or adjust search_path)") return
=<< lookupPGType tpg eo
return (et, map snd vals)
let valn = map (TH.mkName . valnf . pgNameString &&& map (TH.IntegerL . fromIntegral) . pgNameBytes) vals
typl = TH.LitT (TH.StrTyLit $ pgNameString pgid)
dv <- TH.newName "x"
return $
[ TH.DataD [] typn []
#if MIN_VERSION_template_haskell(2,11,0)
Nothing
#endif
(map (\(n, _) -> TH.NormalC n []) valn) $
#if MIN_VERSION_template_haskell(2,11,0)
#if MIN_VERSION_template_haskell(2,12,0)
return $ TH.DerivClause Nothing $
#endif
map TH.ConT
#endif
[''Eq, ''Ord, ''Enum, ''Ix, ''Bounded, ''Typeable]
, instanceD [] (TH.ConT ''PGType `TH.AppT` typl)
[ tySynInstD ''PGVal typl typt
]
, instanceD [] (TH.ConT ''PGParameter `TH.AppT` typl `TH.AppT` typt)
[ TH.FunD 'pgEncode [TH.Clause [TH.WildP, TH.VarP dv]
(TH.NormalB $ TH.VarE 'pgNameBS `TH.AppE` (TH.VarE 'pgEnumName `TH.AppE` TH.VarE dv))
[]]
]
, instanceD [] (TH.ConT ''PGColumn `TH.AppT` typl `TH.AppT` typt)
[ TH.FunD 'pgDecode [TH.Clause [TH.WildP, TH.VarP dv]
(TH.NormalB $ TH.VarE 'fromMaybe
`TH.AppE` (TH.AppE (TH.VarE 'error) $
TH.InfixE (Just $ TH.LitE (TH.StringL ("pgEnumValue " ++ show pgid ++ ": "))) (TH.VarE '(++)) (Just $ TH.VarE 'BSC.unpack `TH.AppE` TH.VarE dv))
`TH.AppE` (TH.VarE 'pgEnumValue `TH.AppE` (TH.ConE 'PGName
`TH.AppE` (TH.VarE 'BS.unpack `TH.AppE` TH.VarE dv))))
[]]
]
, instanceD [] (TH.ConT ''PGRep `TH.AppT` typt)
[ tySynInstD ''PGRepType typt typl
]
, instanceD [] (TH.ConT ''PGEnum `TH.AppT` typt)
[ TH.FunD 'pgEnumName $ map (\(n, l) -> TH.Clause [conP n []]
(TH.NormalB $ namelit l)
[]) valn
, TH.FunD 'pgEnumValue $ map (\(n, l) ->
TH.Clause [conP 'PGName [TH.ListP (map TH.LitP l)]]
(TH.NormalB $ TH.ConE 'Just `TH.AppE` TH.ConE n)
[]) valn
++ [TH.Clause [TH.WildP] (TH.NormalB $ TH.ConE 'Nothing) []]
, TH.FunD 'pgEnumValues [TH.Clause []
(TH.NormalB $ TH.ListE $ map (\(n, l) ->
TH.ConE '(,) `TH.AppE` TH.ConE n `TH.AppE` namelit l) valn)
[]]
]
, TH.PragmaD $ TH.AnnP (TH.TypeAnnotation typn) $ namelit $ map (TH.IntegerL . fromIntegral) $ pgNameBytes pgid
]
++ map (\(n, l) ->
TH.PragmaD $ TH.AnnP (TH.ValueAnnotation n) $ namelit l) valn
where
typn = TH.mkName typs
typt = TH.ConT typn
instanceD = TH.InstanceD
#if MIN_VERSION_template_haskell(2,11,0)
Nothing
#endif
tySynInstD c l t = TH.TySynInstD
#if MIN_VERSION_template_haskell(2,15,0)
$ TH.TySynEqn Nothing (TH.AppT (TH.ConT c) l)
#else
c $ TH.TySynEqn [l]
#endif
t
namelit l = TH.ConE 'PGName `TH.AppE` TH.ListE (map TH.LitE l)
conP n p = TH.ConP n
#if MIN_VERSION_template_haskell(2,18,0)
[]
#endif
p

View File

@ -1,347 +0,0 @@
-- |
-- Module: Database.PostgreSQL.Typed.HDBC
-- Copyright: 2016 Dylan Simon
--
-- Use postgresql-typed as a backend for HDBC.
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Database.PostgreSQL.Typed.HDBC
( Connection
, connect
, fromPGConnection
, withPGConnection
, reloadTypes
, connectionFetchSize
, setFetchSize
) where
import Control.Arrow ((&&&))
import Control.Concurrent.MVar (MVar, newMVar, withMVar)
import Control.Exception (handle, throwIO)
import Control.Monad (void, guard)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy.Char8 as BSLC
import Data.IORef (newIORef, readIORef, writeIORef, modifyIORef')
import Data.Int (Int16)
import qualified Data.IntMap.Lazy as IntMap
import Data.List (uncons)
import qualified Data.Map.Lazy as Map
import Data.Maybe (fromMaybe, isNothing)
import Data.Time.Clock (DiffTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Time.LocalTime (zonedTimeToUTC)
import Data.Word (Word32)
import qualified Database.HDBC.Types as HDBC
import qualified Database.HDBC.ColTypes as HDBC
import System.Mem.Weak (addFinalizer)
import Text.Read (readMaybe)
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.Dynamic
import Database.PostgreSQL.Typed.TypeCache
import Database.PostgreSQL.Typed.SQLToken
import Paths_postgresql_typed (version)
-- |A wrapped 'PGConnection'.
-- This differs from a bare 'PGConnection' in a few ways:
--
-- 1. It always has exactly one active transaction (with 'pgBegin')
-- 2. It automatically disconnects on GC
-- 3. It provides a mutex around the underlying 'PGConnection' for thread-safety
--
data Connection = Connection
{ connectionPG :: MVar PGConnection
, connectionServerVer :: String
, connectionTypes :: IntMap.IntMap SqlType
, connectionFetchSize :: Word32 -- ^Number of rows to fetch (and cache) with 'HDBC.execute' and each time 'HDBC.fetchRow' requires more rows. A higher value will result in fewer round-trips to the database but potentially more wasted data. Defaults to 1. 0 means fetch all rows.
}
sqlError :: IO a -> IO a
sqlError = handle $ \(PGError m) ->
let f c = BSC.unpack $ Map.findWithDefault BSC.empty c m
fC = f 'C'
fD = f 'D' in
throwIO HDBC.SqlError
{ HDBC.seState = fC
, HDBC.seNativeError = if null fC then -1 else fromMaybe 0 $ readMaybe (f 'P')
, HDBC.seErrorMsg = f 'S' ++ ": " ++ f 'M' ++ if null fD then fD else '\n':fD
}
-- |Use the underlying 'PGConnection' directly. You must be careful to ensure that the first invariant is preserved: you should not call 'pgBegin', 'pgCommit', or 'pgRollback' on it. All other operations should be safe.
withPGConnection :: Connection -> (PGConnection -> IO a) -> IO a
withPGConnection c = sqlError . withMVar (connectionPG c)
takePGConnection :: PGConnection -> IO (MVar PGConnection)
takePGConnection pg = do
addFinalizer pg (pgDisconnectOnce pg)
pgBegin pg
newMVar pg
-- |Convert an existing 'PGConnection' to an HDBC-compatible 'Connection'.
-- The caveats under 'connectionPG' apply if you plan to continue using the original 'PGConnection'.
fromPGConnection :: PGConnection -> IO Connection
fromPGConnection pg = do
pgv <- takePGConnection pg
reloadTypes Connection
{ connectionPG = pgv
, connectionServerVer = maybe "" BSC.unpack $ pgServerVersion $ pgTypeEnv pg
, connectionTypes = mempty
, connectionFetchSize = 1
}
-- |Connect to a database for HDBC use (equivalent to 'pgConnect' and 'pgBegin').
connect :: PGDatabase -> IO Connection
connect d = sqlError $ do
pg <- pgConnect d
fromPGConnection pg
-- |Reload the table of all types from the database.
-- This may be needed if you make structural changes to the database.
reloadTypes :: Connection -> IO Connection
reloadTypes c = withPGConnection c $ \pg -> do
t <- pgGetTypes pg
return c{ connectionTypes = IntMap.map (sqlType (pgTypeEnv pg) . pgNameString) t }
-- |Change the 'connectionFetchSize' for new 'HDBC.Statement's created with 'HDBC.prepare'.
-- Ideally this could be set with each call to 'HDBC.execute' and 'HDBC.fetchRow', but the HDBC interface provides no way to do this.
setFetchSize :: Word32 -> Connection -> Connection
setFetchSize i c = c{ connectionFetchSize = i }
sqls :: String -> BSLC.ByteString
sqls = BSLC.pack
placeholders :: Int -> [SQLToken] -> [SQLToken]
placeholders n (SQLQMark False : l) = SQLParam n : placeholders (succ n) l
placeholders n (SQLQMark True : l) = SQLQMark False : placeholders n l
placeholders n (t : l) = t : placeholders n l
placeholders _ [] = []
data ColDesc = ColDesc
{ colDescName :: String
, colDesc :: HDBC.SqlColDesc
, colDescDecode :: PGValue -> HDBC.SqlValue
}
data Cursor = Cursor
{ cursorDesc :: [ColDesc]
, cursorRow :: [PGValues]
, cursorActive :: Bool
, _cursorStatement :: HDBC.Statement -- keep a handle to prevent GC
}
noCursor :: HDBC.Statement -> Cursor
noCursor = Cursor [] [] False
getType :: Connection -> PGConnection -> Maybe Bool -> PGColDescription -> ColDesc
getType c pg nul PGColDescription{..} = ColDesc
{ colDescName = BSC.unpack pgColName
, colDesc = HDBC.SqlColDesc
{ HDBC.colType = sqlTypeId t
, HDBC.colSize = fromIntegral pgColModifier <$ guard (pgColModifier >= 0)
, HDBC.colOctetLength = fromIntegral pgColSize <$ guard (pgColSize >= 0)
, HDBC.colDecDigits = Nothing
, HDBC.colNullable = nul
}
, colDescDecode = sqlTypeDecode t
} where t = IntMap.findWithDefault (sqlType (pgTypeEnv pg) $ show pgColType) (fromIntegral pgColType) (connectionTypes c)
instance HDBC.IConnection Connection where
disconnect c = withPGConnection c
pgDisconnectOnce
commit c = withPGConnection c $ \pg -> do
pgCommitAll pg
pgBegin pg
rollback c = withPGConnection c $ \pg -> do
pgRollbackAll pg
pgBegin pg
runRaw c q = withPGConnection c $ \pg ->
pgSimpleQueries_ pg $ sqls q
run c q v = withPGConnection c $ \pg -> do
let q' = sqls $ show $ placeholders 1 $ sqlTokens q
v' = map encode v
fromMaybe 0 <$> pgRun pg q' [] v'
prepare c q = do
let q' = sqls $ show $ placeholders 1 $ sqlTokens q
n <- withPGConnection c $ \pg -> pgPrepare pg q' []
cr <- newIORef $ error "Cursor"
let
execute v = withPGConnection c $ \pg -> do
d <- pgBind pg n (map encode v)
(r, e) <- pgFetch pg n (connectionFetchSize c)
modifyIORef' cr $ \p -> p
{ cursorDesc = map (getType c pg Nothing) d
, cursorRow = r
, cursorActive = isNothing e
}
return $ fromMaybe 0 e
stmt = HDBC.Statement
{ HDBC.execute = execute
, HDBC.executeRaw = void $ execute []
, HDBC.executeMany = mapM_ execute
, HDBC.finish = withPGConnection c $ \pg -> do
writeIORef cr $ noCursor stmt
pgClose pg n
, HDBC.fetchRow = withPGConnection c $ \pg -> do
p <- readIORef cr
fmap (zipWith colDescDecode (cursorDesc p)) <$> case cursorRow p of
[] | cursorActive p -> do
(rl, e) <- pgFetch pg n (connectionFetchSize c)
let rl' = uncons rl
writeIORef cr p
{ cursorRow = maybe [] snd rl'
, cursorActive = isNothing e
}
return $ fst <$> rl'
| otherwise ->
return Nothing
(r:l) -> do
writeIORef cr p{ cursorRow = l }
return $ Just r
, HDBC.getColumnNames =
map colDescName . cursorDesc <$> readIORef cr
, HDBC.originalQuery = q
, HDBC.describeResult =
map (colDescName &&& colDesc) . cursorDesc <$> readIORef cr
}
writeIORef cr $ noCursor stmt
addFinalizer stmt $ withPGConnection c $ \pg -> pgClose pg n
return stmt
clone c = withPGConnection c $ \pg -> do
pg' <- pgConnect $ pgConnectionDatabase pg
pgv <- takePGConnection pg'
return c{ connectionPG = pgv }
hdbcDriverName _ = "postgresql-typed"
hdbcClientVer _ = show version
proxiedClientName = HDBC.hdbcDriverName
proxiedClientVer = HDBC.hdbcClientVer
dbServerVer = connectionServerVer
dbTransactionSupport _ = True
getTables c = withPGConnection c $ \pg ->
map (pgDecodeRep . head) . snd <$> pgSimpleQuery pg (BSLC.fromChunks
[ "SELECT relname"
, " FROM pg_catalog.pg_class"
, " JOIN pg_catalog.pg_namespace ON relnamespace = pg_namespace.oid"
, " WHERE nspname = ANY (current_schemas(false))"
, " AND relkind IN ('r','v','m','f')"
])
describeTable c t = withPGConnection c $ \pg ->
map (\[attname, attrelid, attnum, atttypid, attlen, atttypmod, attnotnull] ->
colDescName &&& colDesc $ getType c pg (Just $ not $ pgDecodeRep attnotnull) PGColDescription
{ pgColName = pgDecodeRep attname
, pgColTable = pgDecodeRep attrelid
, pgColNumber = pgDecodeRep attnum
, pgColType = pgDecodeRep atttypid
, pgColSize = pgDecodeRep attlen
, pgColModifier = pgDecodeRep atttypmod
, pgColBinary = False
})
. snd <$> pgSimpleQuery pg (BSLC.fromChunks
[ "SELECT attname, attrelid, attnum, atttypid, attlen, atttypmod, attnotnull"
, " FROM pg_catalog.pg_attribute"
, " WHERE attrelid = ", pgLiteralRep t, "::regclass"
, " AND attnum > 0 AND NOT attisdropped"
, " ORDER BY attrelid, attnum"
])
encodeRep :: PGRep a => a -> PGValue
encodeRep x = PGTextValue $ pgEncode (pgTypeOf x) x
encode :: HDBC.SqlValue -> PGValue
encode (HDBC.SqlString x) = encodeRep x
encode (HDBC.SqlByteString x) = encodeRep x
encode (HDBC.SqlWord32 x) = encodeRep x
encode (HDBC.SqlWord64 x) = encodeRep (fromIntegral x :: Rational)
encode (HDBC.SqlInt32 x) = encodeRep x
encode (HDBC.SqlInt64 x) = encodeRep x
encode (HDBC.SqlInteger x) = encodeRep (fromInteger x :: Rational)
encode (HDBC.SqlChar x) = encodeRep x
encode (HDBC.SqlBool x) = encodeRep x
encode (HDBC.SqlDouble x) = encodeRep x
encode (HDBC.SqlRational x) = encodeRep x
encode (HDBC.SqlLocalDate x) = encodeRep x
encode (HDBC.SqlLocalTimeOfDay x) = encodeRep x
encode (HDBC.SqlZonedLocalTimeOfDay t z) = encodeRep (t, z)
encode (HDBC.SqlLocalTime x) = encodeRep x
encode (HDBC.SqlZonedTime x) = encodeRep (zonedTimeToUTC x)
encode (HDBC.SqlUTCTime x) = encodeRep x
encode (HDBC.SqlDiffTime x) = encodeRep (realToFrac x :: DiffTime)
encode (HDBC.SqlPOSIXTime x) = encodeRep (realToFrac x :: Rational) -- (posixSecondsToUTCTime x)
encode (HDBC.SqlEpochTime x) = encodeRep (posixSecondsToUTCTime (fromInteger x))
encode (HDBC.SqlTimeDiff x) = encodeRep (fromIntegral x :: DiffTime)
encode HDBC.SqlNull = PGNullValue
data SqlType = SqlType
{ sqlTypeId :: HDBC.SqlTypeId
, sqlTypeDecode :: PGValue -> HDBC.SqlValue
}
sqlType :: PGTypeEnv -> String -> SqlType
sqlType e t = SqlType
{ sqlTypeId = typeId t
, sqlTypeDecode = decode t e
}
typeId :: String -> HDBC.SqlTypeId
typeId "boolean" = HDBC.SqlBitT
typeId "bytea" = HDBC.SqlVarBinaryT
typeId "\"char\"" = HDBC.SqlCharT
typeId "name" = HDBC.SqlVarCharT
typeId "bigint" = HDBC.SqlBigIntT
typeId "smallint" = HDBC.SqlSmallIntT
typeId "integer" = HDBC.SqlIntegerT
typeId "text" = HDBC.SqlLongVarCharT
typeId "oid" = HDBC.SqlIntegerT
typeId "real" = HDBC.SqlFloatT
typeId "double precision" = HDBC.SqlDoubleT
typeId "abstime" = HDBC.SqlUTCDateTimeT
typeId "reltime" = HDBC.SqlIntervalT HDBC.SqlIntervalSecondT
typeId "tinterval" = HDBC.SqlIntervalT HDBC.SqlIntervalDayToSecondT
typeId "bpchar" = HDBC.SqlVarCharT
typeId "character varying" = HDBC.SqlVarCharT
typeId "date" = HDBC.SqlDateT
typeId "time without time zone" = HDBC.SqlTimeT
typeId "timestamp without time zone" = HDBC.SqlTimestampT
typeId "timestamp with time zone" = HDBC.SqlTimestampWithZoneT -- XXX really SQLUTCDateTimeT
typeId "interval" = HDBC.SqlIntervalT HDBC.SqlIntervalDayToSecondT
typeId "time with time zone" = HDBC.SqlTimeWithZoneT
typeId "numeric" = HDBC.SqlDecimalT
typeId "uuid" = HDBC.SqlGUIDT
typeId t = HDBC.SqlUnknownT t
decodeRep :: PGColumn t a => PGTypeID t -> PGTypeEnv -> (a -> HDBC.SqlValue) -> PGValue -> HDBC.SqlValue
decodeRep t e f (PGBinaryValue v) = f $ pgDecodeBinary e t v
decodeRep t _ f (PGTextValue v) = f $ pgDecode t v
decodeRep _ _ _ PGNullValue = HDBC.SqlNull
#define DECODE(T) \
decode T e = decodeRep (PGTypeProxy :: PGTypeID T) e
decode :: String -> PGTypeEnv -> PGValue -> HDBC.SqlValue
DECODE("boolean") HDBC.SqlBool
DECODE("\"char\"") HDBC.SqlChar
DECODE("name") HDBC.SqlString
DECODE("bigint") HDBC.SqlInt64
DECODE("smallint") (HDBC.SqlInt32 . fromIntegral :: Int16 -> HDBC.SqlValue)
DECODE("integer") HDBC.SqlInt32
DECODE("text") HDBC.SqlString
DECODE("oid") HDBC.SqlWord32
DECODE("real") HDBC.SqlDouble
DECODE("double precision") HDBC.SqlDouble
DECODE("bpchar") HDBC.SqlString
DECODE("character varying") HDBC.SqlString
DECODE("date") HDBC.SqlLocalDate
DECODE("time without time zone") HDBC.SqlLocalTimeOfDay
DECODE("time with time zone") (uncurry HDBC.SqlZonedLocalTimeOfDay)
DECODE("timestamp without time zone") HDBC.SqlLocalTime
DECODE("timestamp with time zone") HDBC.SqlUTCTime
DECODE("interval") (HDBC.SqlDiffTime . realToFrac :: DiffTime -> HDBC.SqlValue)
DECODE("numeric") HDBC.SqlRational
decode _ _ = decodeRaw where
decodeRaw (PGBinaryValue v) = HDBC.SqlByteString v
decodeRaw (PGTextValue v) = HDBC.SqlByteString v
decodeRaw PGNullValue = HDBC.SqlNull

View File

@ -1,135 +0,0 @@
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies, DataKinds, TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Module: Database.PostgreSQL.Typed.Inet
-- Copyright: 2015 Dylan Simon
--
-- Representaion of PostgreSQL's inet/cidr types using "Network.Socket".
-- We don't (yet) supply PGColumn (parsing) instances.
module Database.PostgreSQL.Typed.Inet where
import Control.Monad (void, guard, liftM2)
import qualified Data.ByteString.Char8 as BSC
import Data.Bits (shiftL, (.|.))
import Data.Maybe (fromJust)
import Data.Word (Word8, Word16, Word32)
import Foreign.Marshal.Array (withArray)
import Foreign.Ptr (castPtr)
import Foreign.Storable (peek)
import qualified Network.Socket as Net
import Numeric (readDec, readHex)
import System.IO.Unsafe (unsafeDupablePerformIO)
import qualified Text.ParserCombinators.ReadP as RP
import qualified Text.ParserCombinators.ReadPrec as RP (lift)
import Text.Read (Read(readPrec))
import Database.PostgreSQL.Typed.Types
data PGInet
= PGInet
{ pgInetAddr :: !Net.HostAddress
, pgInetMask :: !Word8
}
| PGInet6
{ pgInetAddr6 :: !Net.HostAddress6
, pgInetMask :: !Word8
}
deriving (Eq)
sockAddrPGInet :: Net.SockAddr -> Maybe PGInet
sockAddrPGInet (Net.SockAddrInet _ a) = Just $ PGInet a 32
sockAddrPGInet (Net.SockAddrInet6 _ _ a _) = Just $ PGInet6 a 128
sockAddrPGInet _ = Nothing
-- |Convert four bytes to network byte order, using unsafe casting.
-- 'Data.Word.byteSwap32' would be better, but I couldn't find a good way to determine host byte order.
bton32 :: (Word8, Word8, Word8, Word8) -> Word32
bton32 (b1, b2, b3, b4) = unsafeDupablePerformIO $
withArray [b1, b2, b3, b4] (peek . castPtr)
instance Show PGInet where
-- This is how Network.Socket's Show SockAddr does it:
show (PGInet a 32) = fromJust $ fst $ unsafeDupablePerformIO $
Net.getNameInfo [Net.NI_NUMERICHOST] True False (Net.SockAddrInet 0 a)
show (PGInet a m) = show (PGInet a 32) ++ '/' : show m
show (PGInet6 a 128) = fromJust $ fst $ unsafeDupablePerformIO $
Net.getNameInfo [Net.NI_NUMERICHOST] True False (Net.SockAddrInet6 0 0 a 0)
show (PGInet6 a m) = show (PGInet6 a 128) ++ '/' : show m
instance Read PGInet where
-- This is even less pleasant, but we only have to deal with representations pg generates
-- Not at all efficient, since in ReadP, but should get us by
readPrec = RP.lift $ r4 RP.+++ r6 where
r4i = do
o1 <- rdec
_ <- RP.char '.'
o2 <- rdec
_ <- RP.char '.'
o3 <- rdec
_ <- RP.char '.'
o4 <- rdec
return (o1, o2, o3, o4)
-- ipv4
r4 = do
q <- r4i
m <- mask 32
return $ PGInet (bton32 q) m
-- trailing ipv4 in ipv6
r64 = do
(b1, b2, b3, b4) <- r4i
return [jb b1 b2, jb b3 b4]
-- ipv6 pre-double-colon
r6l 0 = return []
r6l 2 = colon >> r6lc 2 RP.+++ r64
r6l n = colon >> r6lc n
r6lc n = r6lp n RP.+++ r6b n
r6lp n = r6w (r6l (pred n))
-- ipv6 double-colon
r6b n = do
colon
r <- r6rp (pred n) RP.<++ return []
let l = length r
return $ replicate (n - l) 0 ++ r
-- ipv6 post-double-colon
r6r 0 = return []
r6r n = (colon >> r6rp n) RP.<++ return []
r6rp n
| n >= 2 = r6rc n RP.+++ r64
| otherwise = r6rc n
r6rc n = r6w (r6r (pred n))
r6w = liftM2 (:) rhex
-- ipv6
r6 = do
[w1, w2, w3, w4, w5, w6, w7, w8] <- r6lp 8 RP.<++ (colon >> r6b 8)
m <- mask 128
return $ PGInet6 (jw w1 w2, jw w3 w4, jw w5 w6, jw w7 w8) m
colon = void $ RP.char ':'
mask m = RP.option m $ do
_ <- RP.char '/'
n <- rdec
guard (n <= m)
return n
rdec :: RP.ReadP Word8
rdec = RP.readS_to_P readDec
rhex :: RP.ReadP Word16
rhex = RP.readS_to_P readHex
jw :: Word16 -> Word16 -> Word32
jw x y = fromIntegral x `shiftL` 16 .|. fromIntegral y
jb :: Word8 -> Word8 -> Word16
jb x y = fromIntegral x `shiftL` 8 .|. fromIntegral y
instance PGType "inet" where
type PGVal "inet" = PGInet
instance PGType "cidr" where
type PGVal "cidr" = PGInet
instance PGParameter "inet" PGInet where
pgEncode _ = BSC.pack . show
instance PGParameter "cidr" PGInet where
pgEncode _ = BSC.pack . show
instance PGColumn "inet" PGInet where
pgDecode _ = read . BSC.unpack
instance PGColumn "cidr" PGInet where
pgDecode _ = read . BSC.unpack

View File

@ -1,292 +0,0 @@
{-# LANGUAGE CPP, PatternGuards, MultiParamTypeClasses, FunctionalDependencies, TypeSynonymInstances, FlexibleInstances, FlexibleContexts, GADTs, DataKinds, TemplateHaskell #-}
module Database.PostgreSQL.Typed.Query
( PGQuery(..)
, PGSimpleQuery
, PGPreparedQuery
, rawPGSimpleQuery
, rawPGPreparedQuery
, QueryFlags(..)
, simpleQueryFlags
, parseQueryFlags
, makePGQuery
, pgSQL
, pgExecute
, pgQuery
, pgLazyQuery
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif
import Control.Arrow ((***), first, second)
import Control.Exception (try)
import Control.Monad (void, when, mapAndUnzipM)
import Data.Array (listArray, (!), inRange)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.UTF8 as BSLU
import qualified Data.ByteString.UTF8 as BSU
import Data.Char (isSpace, isAlphaNum)
import qualified Data.Foldable as Fold
import Data.List (dropWhileEnd)
import Data.Maybe (fromMaybe, isNothing)
import Data.String (IsString(..))
import Data.Word (Word32)
import Language.Haskell.Meta.Parse (parseExp)
import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Dynamic
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.TH
import Database.PostgreSQL.Typed.SQLToken
class PGQuery q a | q -> a where
-- |Execute a query and return the number of rows affected (or -1 if not known) and a list of results.
pgRunQuery :: PGConnection -> q -> IO (Int, [a])
-- |Change the raw SQL query stored within this query.
-- This is unsafe because the query has already been type-checked, so any change must not change the number or type of results or placeholders (so adding additional static WHERE or ORDER BY clauses is generally safe).
-- This is useful in cases where you need to construct some part of the query dynamically, but still want to infer the result types.
-- If you want to add dynamic values to the query, it's best to use 'Database.PostgreSQL.Typed.Dynamic.pgSafeLiteral'.
-- For example:
--
-- > [pgSQL|SELECT a FROM t|] `unsafeModifyQuery` (<> (" WHERE a = " <> pgSafeLiteral x))
unsafeModifyQuery :: q -> (BS.ByteString -> BS.ByteString) -> q
getQueryString :: PGTypeEnv -> q -> BS.ByteString
class PGQuery q PGValues => PGRawQuery q
-- |Execute a query that does not return results.
-- Return the number of rows affected (or -1 if not known).
pgExecute :: PGQuery q () => PGConnection -> q -> IO Int
pgExecute c q = fst <$> pgRunQuery c q
-- |Run a query and return a list of row results.
pgQuery :: PGQuery q a => PGConnection -> q -> IO [a]
pgQuery c q = snd <$> pgRunQuery c q
instance PGQuery BS.ByteString PGValues where
pgRunQuery c sql = pgSimpleQuery c (BSL.fromStrict sql)
unsafeModifyQuery q f = f q
getQueryString _ = id
newtype SimpleQuery = SimpleQuery BS.ByteString
deriving (Show)
instance PGQuery SimpleQuery PGValues where
pgRunQuery c (SimpleQuery sql) = pgSimpleQuery c (BSL.fromStrict sql)
unsafeModifyQuery (SimpleQuery sql) f = SimpleQuery $ f sql
getQueryString _ (SimpleQuery q) = q
instance PGRawQuery SimpleQuery
data PreparedQuery = PreparedQuery BS.ByteString [OID] PGValues [Bool]
deriving (Show)
instance PGQuery PreparedQuery PGValues where
pgRunQuery c (PreparedQuery sql types bind bc) = pgPreparedQuery c sql types bind bc
unsafeModifyQuery (PreparedQuery sql types bind bc) f = PreparedQuery (f sql) types bind bc
getQueryString _ (PreparedQuery q _ _ _) = q
instance PGRawQuery PreparedQuery
data QueryParser q a = QueryParser (PGTypeEnv -> q) (PGTypeEnv -> PGValues -> a)
instance PGRawQuery q => PGQuery (QueryParser q a) a where
pgRunQuery c (QueryParser q p) = second (fmap $ p e) <$> pgRunQuery c (q e) where e = pgTypeEnv c
unsafeModifyQuery (QueryParser q p) f = QueryParser (\e -> unsafeModifyQuery (q e) f) p
getQueryString e (QueryParser q _) = getQueryString e $ q e
instance Functor (QueryParser q) where
fmap f (QueryParser q p) = QueryParser q (\e -> f . p e)
instance Show q => Show (QueryParser q a) where
showsPrec p (QueryParser q _) = showParen (p > 10) $
showString "QueryParser " . showsPrec 11 (q unknownPGTypeEnv)
rawParser :: q -> QueryParser q PGValues
rawParser q = QueryParser (const q) (const id)
-- |A simple one-shot query that simply substitutes literal representations of parameters for placeholders.
type PGSimpleQuery = QueryParser SimpleQuery
-- |A prepared query that automatically is prepared in the database the first time it is run and bound with new parameters each subsequent time.
type PGPreparedQuery = QueryParser PreparedQuery
-- |Make a simple query directly from a query string, with no type inference
rawPGSimpleQuery :: BS.ByteString -> PGSimpleQuery PGValues
rawPGSimpleQuery = rawParser . SimpleQuery
instance IsString (PGSimpleQuery PGValues) where
fromString = rawPGSimpleQuery . fromString
instance IsString (PGSimpleQuery ()) where
fromString = void . rawPGSimpleQuery . fromString
-- |Make a prepared query directly from a query string and bind parameters, with no type inference
rawPGPreparedQuery :: BS.ByteString -> PGValues -> PGPreparedQuery PGValues
rawPGPreparedQuery sql bind = rawParser $ PreparedQuery sql [] bind []
-- |Run a prepared query in lazy mode, where only chunk size rows are requested at a time.
-- If you eventually retrieve all the rows this way, it will be far less efficient than using @pgQuery@, since every chunk requires an additional round-trip.
-- Although you may safely stop consuming rows early, currently you may not interleave any other database operation while reading rows. (This limitation could theoretically be lifted if required.)
pgLazyQuery :: PGConnection -> PGPreparedQuery a -> Word32 -- ^ Chunk size (1 is common, 0 is all-or-nothing)
-> IO [a]
pgLazyQuery c (QueryParser q p) count =
fmap (p e) <$> pgPreparedLazyQuery c sql types bind bc count where
e = pgTypeEnv c
PreparedQuery sql types bind bc = q e
-- |Given a SQL statement with placeholders of the form @${expr}@, return a (hopefully) valid SQL statement with @$N@ placeholders and the list of expressions.
-- This does its best to understand SQL syntax, so placeholders are only interpreted in places postgres would understand them (i.e., not in quoted strings). Since this is not valid SQL otherwise, there is never reason to escape a literal @${@.
-- You can use @$N@ placeholders in the query otherwise to refer to the N-th index placeholder expression.
sqlPlaceholders :: String -> (String, [String])
sqlPlaceholders = sst (1 :: Int) . sqlTokens where
sst n (SQLExpr e : l) = (('$':show n) ++) *** (e :) $ sst (succ n) l
sst n (t : l) = first (show t ++) $ sst n l
sst _ [] = ("", [])
-- |Given a SQL statement with placeholders of the form @$N@ and a list of TH 'ByteString' expressions, return a new 'ByteString' expression that substitutes the expressions for the placeholders.
sqlSubstitute :: String -> [TH.Exp] -> TH.Exp
sqlSubstitute sql exprl = TH.AppE (TH.VarE 'BS.concat) $ TH.ListE $ map sst $ sqlTokens sql where
bnds = (1, length exprl)
exprs = listArray bnds exprl
expr n
| inRange bnds n = exprs ! n
| otherwise = error $ "SQL placeholder '$" ++ show n ++ "' out of range (not recognized by PostgreSQL)"
sst (SQLParam n) = expr n
sst t = TH.VarE 'BSU.fromString `TH.AppE` TH.LitE (TH.StringL $ show t)
splitCommas :: String -> [String]
splitCommas = spl where
spl [] = []
spl [c] = [[c]]
spl (',':s) = "":spl s
spl (c:s) = (c:h):t where h:t = spl s
trim :: String -> String
trim = dropWhileEnd isSpace . dropWhile isSpace
-- |Flags affecting how and what type of query to build with 'makePGQuery'.
data QueryFlags = QueryFlags
{ flagQuery :: Bool -- ^ Create a query -- otherwise just call 'pgSubstituteLiterals' to create a string (SQL fragment).
, flagNullable :: Maybe Bool -- ^ Disable nullability inference, treating all values as nullable (if 'True') or not (if 'False').
, flagPrepare :: Maybe [String] -- ^ Prepare and re-use query, binding parameters of the given types (inferring the rest, like PREPARE).
}
-- |'QueryFlags' for a default (simple) query.
simpleQueryFlags :: QueryFlags
simpleQueryFlags = QueryFlags True Nothing Nothing
newName :: Char -> BS.ByteString -> TH.Q TH.Name
newName pre = TH.newName . ('_':) . (pre:) . filter (\c -> isAlphaNum c || c == '_') . BSC.unpack
-- |Construct a 'PGQuery' from a SQL string.
-- This is the underlying template function for 'pgSQL' which you can use in largely the same way when you want to construct query strings from other variables.
-- For example:
--
-- > selectQuery = "SELECT * FROM"
-- > selectFoo = $(makePGQuery simpleQueryFlags (selectQuery ++ " foo"))
--
-- The only caveat is that variables or functions like @selectQuery@ need to be defined in a different module (due to TH stage restrictions).
makePGQuery :: QueryFlags -> String -> TH.ExpQ
makePGQuery QueryFlags{ flagQuery = False } sqle = pgSubstituteLiterals sqle
makePGQuery QueryFlags{ flagNullable = nulls, flagPrepare = prep } sqle = do
(pt, rt) <- TH.runIO $ tpgDescribe (BSU.fromString sqlp) (fromMaybe [] prep) (isNothing nulls)
when (length pt < length exprs) $ fail "Not all expression placeholders were recognized by PostgreSQL"
e <- TH.newName "_tenv"
l <- TH.newName "l"
(vars, vals) <- mapAndUnzipM (\t -> do
v <- newName 'p' $ tpgValueName t
return
( TH.VarP v
, tpgTypeEncoder (isNothing prep) t e `TH.AppE` TH.VarE v
)) pt
(pats, conv, bins) <- unzip3 <$> mapM (\t -> do
v <- newName 'c' $ tpgValueName t
return
( TH.VarP v
, tpgTypeDecoder (Fold.and nulls) t e `TH.AppE` TH.VarE v
, tpgTypeBinary t e
)) rt
foldl TH.AppE (TH.LamE vars $ TH.ConE 'QueryParser
`TH.AppE` TH.LamE [TH.VarP e] (maybe
(TH.ConE 'SimpleQuery
`TH.AppE` sqlSubstitute sqlp vals)
(\p -> TH.ConE 'PreparedQuery
`TH.AppE` (TH.VarE 'BSU.fromString `TH.AppE` TH.LitE (TH.StringL sqlp))
`TH.AppE` TH.ListE (map (TH.LitE . TH.IntegerL . toInteger . tpgValueTypeOID . snd) $ zip p pt)
`TH.AppE` TH.ListE vals
`TH.AppE` TH.ListE
#ifdef VERSION_postgresql_binary
bins
#else
[]
#endif
)
prep)
`TH.AppE` TH.LamE [TH.VarP e, TH.VarP l] (TH.CaseE (TH.VarE l)
[ TH.Match (TH.ListP pats) (TH.NormalB $ case conv of
[x] -> x
_ -> TH.TupE
#if MIN_VERSION_template_haskell(2,16,0)
$ map Just
#endif
conv) []
, TH.Match TH.WildP (TH.NormalB $ TH.VarE 'error `TH.AppE` TH.LitE (TH.StringL "pgSQL: result arity mismatch")) []
]))
<$> mapM parse exprs
where
(sqlp, exprs) = sqlPlaceholders sqle
parse e = either (fail . (++) ("Failed to parse expression {" ++ e ++ "}: ")) return $ parseExp e
-- |Parse flags off the beginning of a query string, returning the flags and the remaining string.
parseQueryFlags :: String -> (QueryFlags, String)
parseQueryFlags = pqf simpleQueryFlags where
pqf f@QueryFlags{ flagQuery = True, flagPrepare = Nothing } ('#':q) = pqf f{ flagQuery = False } q
pqf f@QueryFlags{ flagQuery = True, flagNullable = Nothing } ('?':q) = pqf f{ flagNullable = Just True } q
pqf f@QueryFlags{ flagQuery = True, flagNullable = Nothing } ('!':q) = pqf f{ flagNullable = Just False } q
pqf f@QueryFlags{ flagQuery = True, flagPrepare = Nothing } ('$':q) = pqf f{ flagPrepare = Just [] } q
pqf f@QueryFlags{ flagQuery = True, flagPrepare = Just [] } ('(':s) = pqf f{ flagPrepare = Just args } (sql r) where
args = map trim $ splitCommas arg
(arg, r) = break (')' ==) s
sql (')':q) = q
sql _ = error "pgSQL: unterminated argument list"
pqf f q = (f, q)
qqQuery :: String -> TH.ExpQ
qqQuery = uncurry makePGQuery . parseQueryFlags
qqTop :: Bool -> String -> TH.DecsQ
qqTop True ('!':sql) = qqTop False sql
qqTop err sql = do
r <- TH.runIO $ try $ withTPGConnection $ \c ->
pgSimpleQuery c (BSLU.fromString sql)
either ((if err then TH.reportError else TH.reportWarning) . (show :: PGError -> String)) (const $ return ()) r
return []
-- |A quasi-quoter for PGSQL queries.
--
-- Used in expression context, it may contain any SQL statement @[pgSQL|SELECT ...|]@.
-- The statement may contain PostgreSQL-style placeholders (@$1@, @$2@, ...) or in-line placeholders (@${1+1}@) containing any valid Haskell expression (except @{}@).
-- It will be replaced by a 'PGQuery' object that can be used to perform the SQL statement.
-- If there are more @$N@ placeholders than expressions, it will instead be a function accepting the additional parameters and returning a 'PGQuery'.
--
-- Ideally, this mimics postgres' SQL parsing, so that placeholders and expressions will only be expanded when they are in valid positions (i.e., not inside quoted strings).
-- Since @${@ is not valid SQL otherwise, there should be no need to escape it.
--
-- The statement may start with one of more special flags affecting the interpretation:
--
-- [@?@] To disable nullability inference, treating all result values as nullable, thus returning 'Maybe' values regardless of inferred nullability. This makes unexpected NULL errors impossible.
-- [@!@] To disable nullability inference, treating all result values as /not/ nullable, thus only returning 'Maybe' where requested. This is makes unexpected NULL errors more likely.
-- [@$@] To create a 'PGPreparedQuery' (using placeholder parameters) rather than the default 'PGSimpleQuery' (using literal substitution).
-- [@$(type,...)@] To specify specific types for a prepared query (see <http://www.postgresql.org/docs/current/static/sql-prepare.html> for details), rather than inferring parameter types by default.
-- [@#@] Only do literal @${}@ substitution using 'pgSubstituteLiterals' and return a string, not a query.
--
-- 'pgSQL' can also be used at the top-level to execute SQL statements at compile-time (without any parameters and ignoring results).
-- Here the query can only be prefixed with @!@ to make errors non-fatal.
--
-- If you want to construct queries out of string variables rather than quasi-quoted strings, you can use the lower-level 'makePGQuery' instead.
pgSQL :: QuasiQuoter
pgSQL = QuasiQuoter
{ quoteExp = qqQuery
, quoteType = const $ fail "pgSQL not supported in types"
, quotePat = const $ fail "pgSQL not supported in patterns"
, quoteDec = qqTop True
}

View File

@ -1,290 +0,0 @@
{-# LANGUAGE CPP, MultiParamTypeClasses, FlexibleContexts, FlexibleInstances, UndecidableInstances, DataKinds, GeneralizedNewtypeDeriving, PatternGuards, OverloadedStrings, TypeFamilies #-}
#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE UndecidableSuperClasses #-}
#endif
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Module: Database.PostgreSQL.Typed.Range
-- Copyright: 2015 Dylan Simon
--
-- Representaion of PostgreSQL's range type.
-- There are a number of existing range data types, but PostgreSQL's is rather particular.
-- This tries to provide a one-to-one mapping.
module Database.PostgreSQL.Typed.Range where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<$))
#endif
import Control.Monad (guard)
import qualified Data.Attoparsec.ByteString.Char8 as P
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Char8 as BSC
#if MIN_VERSION_base(4,9,0)
import Data.Semigroup (Semigroup(..))
#else
import Data.Monoid ((<>))
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (Monoid(..))
#endif
#endif
import GHC.TypeLits (Symbol)
import Database.PostgreSQL.Typed.Types
-- |A end-point for a range, which may be nothing (infinity, NULL in PostgreSQL), open (inclusive), or closed (exclusive)
data Bound a
= Unbounded -- ^ Equivalent to @Bounded False ±Infinity@
| Bounded
{ _boundClosed :: Bool -- ^ @True@ if the range includes this bound
, _bound :: a
}
deriving (Eq)
instance Functor Bound where
fmap _ Unbounded = Unbounded
fmap f (Bounded c a) = Bounded c (f a)
newtype LowerBound a = Lower { boundLower :: Bound a } deriving (Eq, Functor)
-- |Takes into account open vs. closed (but does not understand equivalent discrete bounds)
instance Ord a => Ord (LowerBound a) where
compare (Lower Unbounded) (Lower Unbounded) = EQ
compare (Lower Unbounded) _ = LT
compare _ (Lower Unbounded) = GT
compare (Lower (Bounded ac a)) (Lower (Bounded bc b)) = compare a b <> compare bc ac
-- |The constraint is only necessary for @maxBound@, unfortunately
instance Bounded a => Bounded (LowerBound a) where
minBound = Lower Unbounded
maxBound = Lower (Bounded False maxBound)
newtype UpperBound a = Upper { boundUpper :: Bound a } deriving (Eq, Functor)
-- |Takes into account open vs. closed (but does not understand equivalent discrete bounds)
instance Ord a => Ord (UpperBound a) where
compare (Upper Unbounded) (Upper Unbounded) = EQ
compare (Upper Unbounded) _ = GT
compare _ (Upper Unbounded) = LT
compare (Upper (Bounded ac a)) (Upper (Bounded bc b)) = compare a b <> compare ac bc
-- |The constraint is only necessary for @minBound@, unfortunately
instance Bounded a => Bounded (UpperBound a) where
minBound = Upper (Bounded False minBound)
maxBound = Upper Unbounded
compareBounds :: Ord a => LowerBound a -> UpperBound a -> Bound Bool
compareBounds (Lower (Bounded lc l)) (Upper (Bounded uc u)) =
case compare l u of
LT -> Bounded True True
EQ -> Bounded (lc /= uc) (lc && uc)
GT -> Bounded False False
compareBounds _ _ = Unbounded
data Range a
= Empty
| Range
{ lower :: LowerBound a
, upper :: UpperBound a
}
deriving (Eq, Ord)
instance Functor Range where
fmap _ Empty = Empty
fmap f (Range l u) = Range (fmap f l) (fmap f u)
instance Show a => Show (Range a) where
showsPrec _ Empty = showString "empty"
showsPrec _ (Range (Lower l) (Upper u)) =
sc '[' '(' l . sb l . showChar ',' . sb u . sc ']' ')' u where
sc c o b = showChar $ if boundClosed b then c else o
sb = maybe id (showsPrec 10) . bound
bound :: Bound a -> Maybe a
bound Unbounded = Nothing
bound (Bounded _ b) = Just b
-- |Unbounded endpoints are always open.
boundClosed :: Bound a -> Bool
boundClosed Unbounded = False
boundClosed (Bounded c _) = c
-- |Construct from parts: @makeBound (boundClosed b) (bound b) == b@
makeBound :: Bool -> Maybe a -> Bound a
makeBound c (Just a) = Bounded c a
makeBound False Nothing = Unbounded
makeBound True Nothing = error "makeBound: unbounded may not be closed"
-- |Empty ranges treated as 'Unbounded'
lowerBound :: Range a -> Bound a
lowerBound Empty = Unbounded
lowerBound (Range (Lower b) _) = b
-- |Empty ranges treated as 'Unbounded'
upperBound :: Range a -> Bound a
upperBound Empty = Unbounded
upperBound (Range _ (Upper b)) = b
-- |Equivalent to @boundClosed . lowerBound@
lowerClosed :: Range a -> Bool
lowerClosed Empty = False
lowerClosed (Range (Lower b) _) = boundClosed b
-- |Equivalent to @boundClosed . upperBound@
upperClosed :: Range a -> Bool
upperClosed Empty = False
upperClosed (Range _ (Upper b)) = boundClosed b
empty :: Range a
empty = Empty
isEmpty :: Ord a => Range a -> Bool
isEmpty Empty = True
isEmpty (Range l u)
| Bounded _ n <- compareBounds l u = not n
| otherwise = False
full :: Range a
full = Range (Lower Unbounded) (Upper Unbounded)
isFull :: Range a -> Bool
isFull (Range (Lower Unbounded) (Upper Unbounded)) = True
isFull _ = False
-- |Create a point range @[x,x]@
point :: a -> Range a
point a = Range (Lower (Bounded True a)) (Upper (Bounded True a))
-- |Extract a point: @getPoint (point x) == Just x@
getPoint :: Eq a => Range a -> Maybe a
getPoint (Range (Lower (Bounded True l)) (Upper (Bounded True u))) = u <$ guard (u == l)
getPoint _ = Nothing
-- Construct a range from endpoints and normalize it.
range :: Ord a => Bound a -> Bound a -> Range a
range l u = normalize $ Range (Lower l) (Upper u)
-- Construct a standard range (@[l,u)@ or 'point') from bounds (like 'bound') and normalize it.
normal :: Ord a => Maybe a -> Maybe a -> Range a
normal l u = range (mb True l) (mb (l == u) u) where
mb = maybe Unbounded . Bounded
-- Construct a bounded range like 'normal'.
bounded :: Ord a => a -> a -> Range a
bounded l u = normal (Just l) (Just u)
-- Fold empty ranges to 'Empty'.
normalize :: Ord a => Range a -> Range a
normalize r
| isEmpty r = Empty
| otherwise = r
-- |'normalize' for discrete (non-continuous) range types, using the 'Enum' instance
normalize' :: (Ord a, Enum a) => Range a -> Range a
normalize' Empty = Empty
normalize' (Range (Lower l) (Upper u)) = normalize $ range l' u'
where
l' = case l of
Bounded False b -> Bounded True (succ b)
_ -> l
u' = case u of
Bounded True b -> Bounded False (succ b)
_ -> u
-- |Contains range
(@>), (<@) :: Ord a => Range a -> Range a -> Bool
_ @> Empty = True
Empty @> r = isEmpty r
Range la ua @> Range lb ub = la <= lb && ua >= ub
a <@ b = b @> a
-- |Contains element
(@>.) :: Ord a => Range a -> a -> Bool
r @>. a = r @> point a
overlaps :: Ord a => Range a -> Range a -> Bool
overlaps a b = intersect a b /= Empty
intersect :: Ord a => Range a -> Range a -> Range a
intersect (Range la ua) (Range lb ub) = normalize $ Range (max la lb) (min ua ub)
intersect _ _ = Empty
-- |Union ranges. Fails if ranges are disjoint.
union :: Ord a => Range a -> Range a -> Range a
union Empty r = r
union r Empty = r
union _ra@(Range la ua) _rb@(Range lb ub)
-- isEmpty _ra = _rb
-- isEmpty _rb = _ra
| Bounded False False <- compareBounds lb ua = error "union: disjoint Ranges"
| Bounded False False <- compareBounds la ub = error "union: disjoint Ranges"
| otherwise = Range (min la lb) (max ua ub)
#if MIN_VERSION_base(4,9,0)
instance Ord a => Semigroup (Range a) where
(<>) = union
#endif
instance Ord a => Monoid (Range a) where
mempty = Empty
mappend = union
-- |Class indicating that the first PostgreSQL type is a range of the second.
-- This implies 'PGParameter' and 'PGColumn' instances that will work for any type.
class (PGType t, PGType (PGSubType t)) => PGRangeType t where
type PGSubType t :: Symbol
pgRangeElementType :: PGTypeID t -> PGTypeID (PGSubType t)
pgRangeElementType PGTypeProxy = PGTypeProxy
instance (PGRangeType t, PGParameter (PGSubType t) a) => PGParameter t (Range a) where
pgEncode _ Empty = BSC.pack "empty"
pgEncode tr (Range (Lower l) (Upper u)) = buildPGValue $
pc '[' '(' l
<> pb (bound l)
<> BSB.char7 ','
<> pb (bound u)
<> pc ']' ')' u
where
pb Nothing = mempty
pb (Just b) = pgDQuoteFrom "(),[]" $ pgEncode (pgRangeElementType tr) b
pc c o b = BSB.char7 $ if boundClosed b then c else o
instance (PGRangeType t, PGColumn (PGSubType t) a) => PGColumn t (Range a) where
pgDecode tr a = either (error . ("pgDecode range (" ++) . (++ ("): " ++ BSC.unpack a))) id $ P.parseOnly per a where
per = (Empty <$ pe) <> pr
pe = P.stringCI "empty"
pb = fmap (pgDecode (pgRangeElementType tr)) <$> parsePGDQuote True "(),[]" BSC.null
pc c o = (True <$ P.char c) <> (False <$ P.char o)
mb = maybe Unbounded . Bounded
pr = do
lc <- pc '[' '('
lb <- pb
_ <- P.char ','
ub <- pb
uc <- pc ']' ')'
return $ Range (Lower (mb lc lb)) (Upper (mb uc ub))
instance PGType "int4range" where
type PGVal "int4range" = Range (PGVal (PGSubType "int4range"))
instance PGRangeType "int4range" where
type PGSubType "int4range" = "integer"
instance PGType "numrange" where
type PGVal "numrange" = Range (PGVal (PGSubType "numrange"))
instance PGRangeType "numrange" where
type PGSubType "numrange" = "numeric"
instance PGType "tsrange" where
type PGVal "tsrange" = Range (PGVal (PGSubType "tsrange"))
instance PGRangeType "tsrange" where
type PGSubType "tsrange" = "timestamp without time zone"
instance PGType "tstzrange" where
type PGVal "tstzrange" = Range (PGVal (PGSubType "tstzrange"))
instance PGRangeType "tstzrange" where
type PGSubType "tstzrange" = "timestamp with time zone"
instance PGType "daterange" where
type PGVal "daterange" = Range (PGVal (PGSubType "daterange"))
instance PGRangeType "daterange" where
type PGSubType "daterange" = "date"
instance PGType "int8range" where
type PGVal "int8range" = Range (PGVal (PGSubType "int8range"))
instance PGRangeType "int8range" where
type PGSubType "int8range" = "bigint"

View File

@ -1,217 +0,0 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE UndecidableSuperClasses #-}
#endif
-- |
-- Module: Database.PostgreSQL.Typed.Relation
-- Copyright: 2016 Dylan Simon
--
-- Automatically create data types based on tables and other relations.
module Database.PostgreSQL.Typed.Relation
( dataPGRelation
) where
import qualified Data.ByteString.Lazy as BSL
import Data.Proxy (Proxy(..))
import qualified Language.Haskell.TH as TH
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Dynamic
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.TypeCache
import Database.PostgreSQL.Typed.TH
-- |Data types that are based on database relations.
-- Normally these instances are created using 'dataPGRelation'.
class (PGRep a, PGRecordType (PGRepType a)) => PGRelation a where
-- |Database name of table/relation (i.e., second argument to 'dataPGRelation'). Normally this is the same as @'pgTypeID' . 'pgTypeOfProxy'@, but this preserves any specified schema qualification.
pgRelationName :: Proxy a -> PGName
pgRelationName = pgTypeName . pgTypeOfProxy
-- |Database names of columns.
pgColumnNames :: Proxy a -> [PGName]
-- |Create a new data type corresponding to the given PostgreSQL relation.
-- For example, if you have @CREATE TABLE foo (abc integer NOT NULL, def text)@, then
-- @dataPGRelation \"Foo\" \"foo\" (\"foo_\"++)@ will be equivalent to:
--
-- > data Foo = Foo{ foo_abc :: PGVal "integer", foo_def :: Maybe (PGVal "text") }
-- > instance PGType "foo" where PGVal "foo" = Foo
-- > instance PGParameter "foo" Foo where ...
-- > instance PGColumn "foo" Foo where ...
-- > instance PGColumn "foo" (Maybe Foo) where ... -- to handle NULL in not null columns
-- > instance PGRep Foo where PGRepType = "foo"
-- > instance PGRecordType "foo"
-- > instance PGRelation Foo where pgColumnNames _ = ["abc", "def"]
-- > uncurryFoo :: (PGVal "integer", Maybe (PGVal "text")) -> Foo
--
-- (Note that @PGVal "integer" = Int32@ and @PGVal "text" = Text@ by default.)
-- This provides instances for marshalling the corresponding composite/record types, e.g., using @SELECT foo.*::foo FROM foo@.
-- If you want any derived instances, you'll need to create them yourself using StandaloneDeriving.
--
-- Requires language extensions: TemplateHaskell, FlexibleInstances, MultiParamTypeClasses, DataKinds, TypeFamilies, PatternGuards
dataPGRelation :: String -- ^ Haskell type and constructor to create
-> PGName -- ^ PostgreSQL table/relation name
-> (String -> String) -- ^ How to generate field names from column names, e.g. @("table_"++)@ (input is 'pgNameString')
-> TH.DecsQ
dataPGRelation typs pgtab colf = do
(pgid, cold) <- TH.runIO $ withTPGTypeConnection $ \tpg -> do
cl <- mapM (\[to, cn, ct, cnn] -> do
let c = pgDecodeRep cn :: PGName
n = TH.mkName $ colf $ pgNameString c
o = pgDecodeRep ct :: OID
t <- maybe (fail $ "dataPGRelation " ++ typs ++ " = " ++ show pgtab ++ ": column '" ++ show c ++ "' has unknown type " ++ show o) return
=<< lookupPGType tpg o
return (pgDecodeRep to, (c, n, TH.LitT (TH.StrTyLit $ pgNameString t), not $ pgDecodeRep cnn)))
. snd =<< pgSimpleQuery (pgConnection tpg) (BSL.fromChunks
[ "SELECT reltype, attname, atttypid, attnotnull"
, " FROM pg_catalog.pg_attribute"
, " JOIN pg_catalog.pg_class ON attrelid = pg_class.oid"
, " WHERE attrelid = ", pgLiteralRep pgtab, "::regclass"
, " AND attnum > 0 AND NOT attisdropped"
, " ORDER BY attnum"
])
case cl of
[] -> fail $ "dataPGRelation " ++ typs ++ " = " ++ show pgtab ++ ": no columns found"
(to, _):_ -> do
tt <- maybe (fail $ "dataPGRelation " ++ typs ++ " = " ++ show pgtab ++ ": table type not found (you may need to use reloadTPGTypes or adjust search_path)") return
=<< lookupPGType tpg to
return (tt, map snd cl)
cols <- mapM (\(c, _, t, nn) -> do
v <- TH.newName $ pgNameString c
return (v, t, nn))
cold
let typl = TH.LitT (TH.StrTyLit $ pgNameString pgid)
encfun f = TH.FunD f [TH.Clause [TH.WildP, conP typn (map (\(v, _, _) -> TH.VarP v) cols)]
(TH.NormalB $ pgcall f rect `TH.AppE`
(TH.ConE 'PGRecord `TH.AppE` TH.ListE (map (colenc f) cols)))
[] ]
dv <- TH.newName "x"
tv <- TH.newName "t"
ev <- TH.newName "e"
return $
[ TH.DataD
[]
typn
[]
#if MIN_VERSION_template_haskell(2,11,0)
Nothing
#endif
[ TH.RecC typn $ map (\(_, n, t, nn) ->
( n
#if MIN_VERSION_template_haskell(2,11,0)
, TH.Bang TH.NoSourceUnpackedness TH.NoSourceStrictness
#else
, TH.NotStrict
#endif
, (if nn then (TH.ConT ''Maybe `TH.AppT`) else id)
(TH.ConT ''PGVal `TH.AppT` t)))
cold
]
[]
, instanceD [] (TH.ConT ''PGType `TH.AppT` typl)
[ tySynInstD ''PGVal typl typt
]
, instanceD [] (TH.ConT ''PGParameter `TH.AppT` typl `TH.AppT` typt)
[ encfun 'pgEncode
, encfun 'pgLiteral
]
, instanceD [] (TH.ConT ''PGColumn `TH.AppT` typl `TH.AppT` typt)
[ TH.FunD 'pgDecode [TH.Clause [TH.WildP, TH.VarP dv]
(TH.GuardedB
[ (TH.PatG [TH.BindS
(conP 'PGRecord [TH.ListP $ map colpat cols])
(pgcall 'pgDecode rect `TH.AppE` TH.VarE dv)]
, foldl (\f -> TH.AppE f . coldec) (TH.ConE typn) cols)
, (TH.NormalG (TH.ConE 'True)
, TH.VarE 'error `TH.AppE` TH.LitE (TH.StringL $ "pgDecode " ++ typs ++ ": NULL in not null record column"))
])
[] ]
]
#if MIN_VERSION_template_haskell(2,11,0)
, TH.InstanceD (Just TH.Overlapping) [] (TH.ConT ''PGColumn `TH.AppT` typl `TH.AppT` (TH.ConT ''Maybe `TH.AppT` typt))
[ TH.FunD 'pgDecode [TH.Clause [TH.WildP, TH.VarP dv]
(TH.GuardedB
[ (TH.PatG [TH.BindS
(conP 'PGRecord [TH.ListP $ map colpat cols])
(pgcall 'pgDecode rect `TH.AppE` TH.VarE dv)]
, TH.ConE 'Just `TH.AppE` foldl (\f -> TH.AppE f . coldec) (TH.ConE typn) cols)
, (TH.NormalG (TH.ConE 'True)
, TH.ConE 'Nothing)
])
[] ]
, TH.FunD 'pgDecodeValue
[ TH.Clause [TH.WildP, TH.WildP, conP 'PGNullValue []]
(TH.NormalB $ TH.ConE 'Nothing)
[]
, TH.Clause [TH.WildP, TH.VarP tv, conP 'PGTextValue [TH.VarP dv]]
(TH.NormalB $ TH.VarE 'pgDecode `TH.AppE` TH.VarE tv `TH.AppE` TH.VarE dv)
[]
, TH.Clause [TH.VarP ev, TH.VarP tv, conP 'PGBinaryValue [TH.VarP dv]]
(TH.NormalB $ TH.VarE 'pgDecodeBinary `TH.AppE` TH.VarE ev `TH.AppE` TH.VarE tv `TH.AppE` TH.VarE dv)
[]
]
]
#endif
, instanceD [] (TH.ConT ''PGRep `TH.AppT` typt)
[ tySynInstD ''PGRepType typt typl
]
, instanceD [] (TH.ConT ''PGRecordType `TH.AppT` typl) []
, instanceD [] (TH.ConT ''PGRelation `TH.AppT` typt)
[ TH.FunD 'pgRelationName [TH.Clause [TH.WildP]
(TH.NormalB $ namelit pgtab)
[] ]
, TH.FunD 'pgColumnNames [TH.Clause [TH.WildP]
(TH.NormalB $ TH.ListE $ map (\(c, _, _, _) -> namelit c) cold)
[] ]
]
, TH.SigD (TH.mkName ("uncurry" ++ typs)) $ TH.ArrowT `TH.AppT`
foldl (\f (_, t, n) -> f `TH.AppT`
(if n then (TH.ConT ''Maybe `TH.AppT`) else id)
(TH.ConT ''PGVal `TH.AppT` t))
(TH.ConT (TH.tupleTypeName (length cols)))
cols `TH.AppT` typt
, TH.FunD (TH.mkName ("uncurry" ++ typs))
[ TH.Clause [conP (TH.tupleDataName (length cols)) (map (\(v, _, _) -> TH.VarP v) cols)]
(TH.NormalB $ foldl (\f (v, _, _) -> f `TH.AppE` TH.VarE v) (TH.ConE typn) cols)
[]
]
, TH.PragmaD $ TH.AnnP (TH.TypeAnnotation typn) $ namelit pgid
, TH.PragmaD $ TH.AnnP (TH.ValueAnnotation typn) $ namelit pgid
] ++ map (\(c, n, _, _) ->
TH.PragmaD $ TH.AnnP (TH.ValueAnnotation n) $ namelit c) cold
where
typn = TH.mkName typs
typt = TH.ConT typn
instanceD = TH.InstanceD
#if MIN_VERSION_template_haskell(2,11,0)
Nothing
#endif
tySynInstD c l t = TH.TySynInstD
#if MIN_VERSION_template_haskell(2,15,0)
$ TH.TySynEqn Nothing (TH.AppT (TH.ConT c) l)
#else
c $ TH.TySynEqn [l]
#endif
t
pgcall f t = TH.VarE f `TH.AppE`
(TH.ConE 'PGTypeProxy `TH.SigE`
(TH.ConT ''PGTypeID `TH.AppT` t))
colenc f (v, t, False) = TH.ConE 'Just `TH.AppE` (pgcall f t `TH.AppE` TH.VarE v)
colenc f (v, t, True) = TH.VarE 'fmap `TH.AppE` pgcall f t `TH.AppE` TH.VarE v
colpat (v, _, False) = conP 'Just [TH.VarP v]
colpat (v, _, True) = TH.VarP v
coldec (v, t, False) = pgcall 'pgDecode t `TH.AppE` TH.VarE v
coldec (v, t, True) = TH.VarE 'fmap `TH.AppE` pgcall 'pgDecode t `TH.AppE` TH.VarE v
rect = TH.LitT $ TH.StrTyLit "record"
namelit n = TH.ConE 'PGName `TH.AppE`
TH.ListE (map (TH.LitE . TH.IntegerL . fromIntegral) $ pgNameBytes n)
conP n p = TH.ConP n
#if MIN_VERSION_template_haskell(2,18,0)
[]
#endif
p

View File

@ -1,131 +0,0 @@
-- |
-- Module: Database.PostgreSQL.Typed.SQLToken
-- Copyright: 2016 Dylan Simon
--
-- Parsing of SQL statements to safely identify placeholders.
-- Supports both dollar-placeholders and question marks for HDBC.
{-# LANGUAGE PatternGuards #-}
module Database.PostgreSQL.Typed.SQLToken
( SQLToken(..)
, sqlTokens
) where
import Control.Arrow (first)
import Data.Char (isDigit, isAsciiUpper, isAsciiLower)
import Data.List (stripPrefix)
import Data.String (IsString(..))
-- |A parsed SQL token.
data SQLToken
= SQLToken String -- ^Raw (non-markup) SQL string
| SQLParam Int -- ^A \"$N\" parameter placeholder (this is the only non-string-preserving token: \"$012\" becomes \"$12\")
| SQLExpr String -- ^A \"${expr}\" expression placeholder
| SQLQMark Bool -- ^A possibly-escaped question-mark: False for \"?\" or True for \"\\?\"
deriving (Eq)
-- |Produces the original SQL string
instance Show SQLToken where
showsPrec _ (SQLToken s) = showString s
showsPrec _ (SQLParam p) = showChar '$' . shows p
showsPrec _ (SQLExpr e) = showString "${" . showString e . showChar '}'
showsPrec _ (SQLQMark False) = showChar '?'
showsPrec _ (SQLQMark True) = showString "\\?"
showList = flip $ foldr shows
instance IsString SQLToken where
fromString = SQLToken
type PH = String -> [SQLToken]
infixr 4 ++:, +:
(++:) :: String -> [SQLToken] -> [SQLToken]
p ++: (SQLToken q : l) = SQLToken (p ++ q) : l
p ++: l = SQLToken p : l
(+:) :: Char -> [SQLToken] -> [SQLToken]
p +: (SQLToken q : l) = SQLToken (p : q) : l
p +: l = SQLToken [p] : l
x :: PH
x ('-':'-':s) = "--" ++: comment s
x ('e':'\'':s) = "e'" ++: xe s
x ('E':'\'':s) = "E'" ++: xe s
x ('\'':s) = '\'' +: xq s
x ('$':'{':s) = expr s
x ('$':'$':s) = "$$" ++: xdolq "" s
x ('$':c:s)
| dolqStart c
, (t,'$':r) <- span dolqCont s
= '$' : c : t ++: '$' +: xdolq (c:t) r
| isDigit c
, (i,r) <- span isDigit s
= SQLParam (read $ c:i) : x r
x ('"':s) = '"' +: xd s
x ('/':'*':s) = "/*" ++: xc 1 s
x (c:s)
| identStart c
, (i,r) <- span identCont s
= c : i ++: x r
x ('\\':'?':s) = SQLQMark True : x s
x ('?':s) = SQLQMark False : x s
x (c:s) = c +: x s
x [] = []
xthru :: (Char -> Bool) -> PH
xthru f s = case break f s of
(p, c:r) -> p ++ [c] ++: x r
(p, []) -> [SQLToken p]
comment :: PH
comment = xthru (\n -> '\n' == n || '\r' == n)
xe :: PH
xe ('\\':c:s) = '\\' +: c +: xe s
xe ('\'':s) = '\'' +: x s
xe (c:s) = c +: xe s
xe [] = []
xq :: PH
xq = xthru ('\'' ==)
-- no need to handle xqdouble
xd :: PH
xd = xthru ('\"' ==)
-- no need to handle xddouble
identStart, identCont, dolqStart, dolqCont :: Char -> Bool
identStart c = isAsciiUpper c || isAsciiLower c || c >= '\128' && c <= '\255' || c == '_'
dolqStart = identStart
dolqCont c = dolqStart c || isDigit c
identCont c = dolqCont c || c == '$'
xdolq :: String -> PH
xdolq t = dolq where
dolq ('$':s)
| Just r <- stripPrefix t' s = '$':t' ++: x r
dolq (c:s) = c +: dolq s
dolq [] = []
t' = t ++ "$"
xc :: Int -> PH
xc 0 s = x s
xc n ('/':'*':s) = "/*" ++: xc (succ n) s
xc n ('*':'/':s) = "*/" ++: xc (pred n) s
xc n (c:s) = c +: xc n s
xc _ [] = []
expr :: PH
expr = pr . ex (0 :: Int) where
pr (e, Nothing) = [SQLToken ("${" ++ e)]
pr (e, Just r) = SQLExpr e : r
ex 0 ('}':s) = ("", Just $ x s)
ex n ('}':s) = first ('}':) $ ex (pred n) s
ex n ('{':s) = first ('{':) $ ex (succ n) s
ex n (c:s) = first (c:) $ ex n s
ex _ [] = ("", Nothing)
-- |Parse a SQL string into a series of tokens.
-- The 'showList' implementation for 'SQLToken' inverts this sequence back to a SQL string.
sqlTokens :: String -> [SQLToken]
sqlTokens = x

View File

@ -1,198 +0,0 @@
{-# LANGUAGE CPP, PatternGuards, ScopedTypeVariables, FlexibleContexts, TemplateHaskell, DataKinds #-}
-- |
-- Module: Database.PostgreSQL.Typed.TH
-- Copyright: 2015 Dylan Simon
--
-- Support functions for compile-time PostgreSQL connection and state management.
-- You can use these to build your own Template Haskell functions using the PostgreSQL connection.
module Database.PostgreSQL.Typed.TH
( getTPGDatabase
, withTPGTypeConnection
, withTPGConnection
, useTPGDatabase
, reloadTPGTypes
, TPGValueInfo(..)
, tpgDescribe
, tpgTypeEncoder
, tpgTypeDecoder
, tpgTypeBinary
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<$))
#endif
import Control.Applicative ((<|>))
import Control.Concurrent.MVar (MVar, newMVar, takeMVar, putMVar, withMVar)
import Control.Exception (onException, finally)
#ifdef VERSION_tls
import Control.Exception (throwIO)
#endif
import Control.Monad (liftM2)
import qualified Data.ByteString as BS
#ifdef VERSION_tls
import qualified Data.ByteString.Char8 as BSC
#endif
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.UTF8 as BSU
import qualified Data.Foldable as Fold
import Data.Maybe (isJust, fromMaybe)
import Data.String (fromString)
import qualified Data.Traversable as Tv
import qualified Language.Haskell.TH as TH
import qualified Network.Socket as Net
import System.Environment (lookupEnv)
import System.IO.Unsafe (unsafePerformIO, unsafeInterleaveIO)
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.TypeCache
-- |Generate a 'PGDatabase' based on the environment variables:
-- @TPG_HOST@ (localhost); @TPG_SOCK@ or @TPG_PORT@ (5432); @TPG_DB@ or user; @TPG_USER@ or @USER@ (postgres); @TPG_PASS@ ()
getTPGDatabase :: IO PGDatabase
getTPGDatabase = do
user <- fromMaybe "postgres" <$> liftM2 (<|>) (lookupEnv "TPG_USER") (lookupEnv "USER")
db <- fromMaybe user <$> lookupEnv "TPG_DB"
host <- fromMaybe "localhost" <$> lookupEnv "TPG_HOST"
pnum <- fromMaybe "5432" <$> lookupEnv "TPG_PORT"
#ifdef mingw32_HOST_OS
let port = Right pnum
#else
port <- maybe (Right pnum) Left <$> lookupEnv "TPG_SOCK"
#endif
pass <- fromMaybe "" <$> lookupEnv "TPG_PASS"
debug <- isJust <$> lookupEnv "TPG_DEBUG"
#ifdef VERSION_tls
tlsEnabled <- isJust <$> lookupEnv "TPG_TLS"
tlsVerifyMode <- lookupEnv "TPG_TLS_MODE" >>= \modeStr ->
case modeStr of
Just "full" -> pure TlsValidateFull
Just "ca" -> pure TlsValidateCA
Just other -> throwIO (userError ("Unknown verify mode: " ++ other))
Nothing -> pure TlsValidateCA
mTlsCertPem <- lookupEnv "TPG_TLS_ROOT_CERT"
dbTls <- case mTlsCertPem of
Just certPem ->
case pgTlsValidate tlsVerifyMode (BSC.pack certPem) of
Right x -> pure x
Left err -> throwIO (userError err)
Nothing | tlsEnabled -> pure TlsNoValidate
Nothing -> pure TlsDisabled
#endif
return $ defaultPGDatabase
{ pgDBAddr = either (Right . Net.SockAddrUnix) (Left . (,) host) port
, pgDBName = BSU.fromString db
, pgDBUser = BSU.fromString user
, pgDBPass = BSU.fromString pass
, pgDBDebug = debug
#ifdef VERSION_tls
, pgDBTLS = dbTls
#endif
}
{-# NOINLINE tpgState #-}
tpgState :: MVar (PGDatabase, Maybe PGTypeConnection)
tpgState = unsafePerformIO $ do
db <- unsafeInterleaveIO getTPGDatabase
newMVar (db, Nothing)
-- |Run an action using the Template Haskell state.
withTPGTypeConnection :: (PGTypeConnection -> IO a) -> IO a
withTPGTypeConnection f = do
(db, tpg') <- takeMVar tpgState
tpg <- maybe (newPGTypeConnection =<< pgConnect db) return tpg'
`onException` putMVar tpgState (db, Nothing) -- might leave connection open
f tpg `finally` putMVar tpgState (db, Just tpg)
-- |Run an action using the Template Haskell PostgreSQL connection.
withTPGConnection :: (PGConnection -> IO a) -> IO a
withTPGConnection f = withTPGTypeConnection (f . pgConnection)
-- |Specify an alternative database to use during compilation.
-- This lets you override the default connection parameters that are based on TPG environment variables.
-- This should be called as a top-level declaration and produces no code.
-- It uses 'pgReconnect' so is safe to call multiple times with the same database.
useTPGDatabase :: PGDatabase -> TH.DecsQ
useTPGDatabase db = TH.runIO $ do
(db', tpg') <- takeMVar tpgState
putMVar tpgState . (,) db =<<
(if db == db'
then Tv.mapM (\t -> do
c <- pgReconnect (pgConnection t) db
return t{ pgConnection = c }) tpg'
else Nothing <$ Fold.mapM_ (pgDisconnect . pgConnection) tpg')
`onException` putMVar tpgState (db, Nothing)
return []
-- |Force reloading of all types from the database.
-- This may be needed if you make structural changes to the database during compile-time.
reloadTPGTypes :: TH.DecsQ
reloadTPGTypes = TH.runIO $ [] <$ withMVar tpgState (mapM_ flushPGTypeConnection . snd)
-- |Lookup a type name by OID.
-- Error if not found.
tpgType :: PGTypeConnection -> OID -> IO PGName
tpgType c o =
maybe (fail $ "Unknown PostgreSQL type: " ++ show o ++ "\nYou may need to use reloadTPGTypes or adjust search_path, or your postgresql-typed application may need to be rebuilt.") return =<< lookupPGType c o
-- |Lookup a type OID by type name.
-- This is less common and thus less efficient than going the other way.
-- Fail if not found.
getTPGTypeOID :: PGTypeConnection -> PGName -> IO OID
getTPGTypeOID c t =
maybe (fail $ "Unknown PostgreSQL type: " ++ show t ++ "; be sure to use the exact type name from \\dTS") return =<< findPGType c t
data TPGValueInfo = TPGValueInfo
{ tpgValueName :: BS.ByteString
, tpgValueTypeOID :: !OID
, tpgValueType :: PGName
, tpgValueNullable :: Bool
}
-- |A type-aware wrapper to 'pgDescribe'
tpgDescribe :: BS.ByteString -> [String] -> Bool -> IO ([TPGValueInfo], [TPGValueInfo])
tpgDescribe sql types nulls = withTPGTypeConnection $ \tpg -> do
at <- mapM (getTPGTypeOID tpg . fromString) types
(pt, rt) <- pgDescribe (pgConnection tpg) (BSL.fromStrict sql) at nulls
(,)
<$> mapM (\o -> do
ot <- tpgType tpg o
return TPGValueInfo
{ tpgValueName = BS.empty
, tpgValueTypeOID = o
, tpgValueType = ot
, tpgValueNullable = True
}) pt
<*> mapM (\(c, o, n) -> do
ot <- tpgType tpg o
return TPGValueInfo
{ tpgValueName = c
, tpgValueTypeOID = o
, tpgValueType = ot
, tpgValueNullable = n && o /= 2278 -- "void"
}) rt
typeApply :: PGName -> TH.Name -> TH.Name -> TH.Exp
typeApply t f e =
TH.VarE f `TH.AppE` TH.VarE e
`TH.AppE` (TH.ConE 'PGTypeProxy `TH.SigE` (TH.ConT ''PGTypeID `TH.AppT` TH.LitT (TH.StrTyLit $ pgNameString $ t)))
-- |TH expression to encode a 'PGParameter' value to a 'Maybe' 'L.ByteString'.
tpgTypeEncoder :: Bool -> TPGValueInfo -> TH.Name -> TH.Exp
tpgTypeEncoder lit v = typeApply (tpgValueType v) $
if lit
then 'pgEscapeParameter
else 'pgEncodeParameter
-- |TH expression to decode a 'Maybe' 'L.ByteString' to a ('Maybe') 'PGColumn' value.
tpgTypeDecoder :: Bool -> TPGValueInfo -> TH.Name -> TH.Exp
tpgTypeDecoder nulls v = typeApply (tpgValueType v) $
if nulls && tpgValueNullable v
then 'pgDecodeColumn
else 'pgDecodeColumnNotNull
-- |TH expression calling 'pgBinaryColumn'.
tpgTypeBinary :: TPGValueInfo -> TH.Name -> TH.Exp
tpgTypeBinary v = typeApply (tpgValueType v) 'pgBinaryColumn

View File

@ -1,133 +0,0 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-deprecations #-}
-- Copyright 2010, 2011, 2012, 2013 Chris Forno
-- |This module exposes the high-level Template Haskell interface for querying
-- and manipulating the PostgreSQL server.
--
-- All SQL string arguments support expression interpolation. Just enclose your
-- expression in @{}@ in the SQL string.
--
-- Note that transactions are messy and untested. Attempt to use them at your
-- own risk.
module Database.PostgreSQL.Typed.TemplatePG
( queryTuples
, queryTuple
, execute
, insertIgnore
, withTransaction
, rollback
, PGException
, pgConnect
#if !MIN_VERSION_network(2,7,0)
, PortID(..)
#endif
, PG.pgDisconnect
) where
import Control.Exception (catchJust)
import Control.Monad (liftM, void, guard)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy.Char8 as BSLC
import Data.Maybe (listToMaybe, isJust)
import qualified Language.Haskell.TH as TH
#if MIN_VERSION_network(2,7,0)
import Data.Word (Word16)
#else
import Network (PortID(..))
#endif
#if !defined(mingw32_HOST_OS)
import qualified Network.Socket as Net
#endif
import System.Environment (lookupEnv)
import qualified Database.PostgreSQL.Typed.Protocol as PG
import Database.PostgreSQL.Typed.Query
-- |Convert a 'queryTuple'-style string with placeholders into a new style SQL string.
querySQL :: String -> String
querySQL ('{':s) = '$':'{':querySQL s
querySQL (c:s) = c:querySQL s
querySQL "" = ""
-- |@queryTuples :: String -> (PGConnection -> IO [(column1, column2, ...)])@
--
-- Query a PostgreSQL server and return the results as a list of tuples.
--
-- Example (where @h@ is a handle from 'pgConnect'):
--
-- > $(queryTuples "SELECT usesysid, usename FROM pg_user") h :: IO [(Maybe String, Maybe Integer)]
queryTuples :: String -> TH.ExpQ
queryTuples sql = [| \c -> pgQuery c $(makePGQuery simpleQueryFlags $ querySQL sql) |]
-- |@queryTuple :: String -> (PGConnection -> IO (Maybe (column1, column2, ...)))@
--
-- Convenience function to query a PostgreSQL server and return the first
-- result as a tuple. If the query produces no results, return 'Nothing'.
--
-- Example (where @h@ is a handle from 'pgConnect'):
--
-- > let sysid = 10::Integer;
-- > $(queryTuple "SELECT usesysid, usename FROM pg_user WHERE usesysid = {sysid}") h :: IO (Maybe (Maybe String, Maybe Integer))
queryTuple :: String -> TH.ExpQ
queryTuple sql = [| liftM listToMaybe . $(queryTuples sql) |]
-- |@execute :: String -> (PGConnection -> IO ())@
--
-- Convenience function to execute a statement on the PostgreSQL server.
--
-- Example (where @h@ is a handle from 'pgConnect'):
execute :: String -> TH.ExpQ
execute sql = [| \c -> void $ pgExecute c $(makePGQuery simpleQueryFlags $ querySQL sql) |]
-- |Run a sequence of IO actions (presumably SQL statements) wrapped in a
-- transaction. Unfortunately you're restricted to using this in the 'IO'
-- Monad for now due to the use of 'onException'. I'm debating adding a
-- 'MonadPeelIO' version.
withTransaction :: PG.PGConnection -> IO a -> IO a
withTransaction = PG.pgTransaction
-- |Roll back a transaction.
rollback :: PG.PGConnection -> IO ()
rollback h = void $ PG.pgSimpleQuery h $ BSLC.pack "ROLLBACK"
-- |Ignore duplicate key errors. This is also limited to the 'IO' Monad.
insertIgnore :: IO () -> IO ()
insertIgnore q = catchJust uniquenessError q (\ _ -> return ()) where
uniquenessError e = guard (PG.pgErrorCode e == BSC.pack "23505")
type PGException = PG.PGError
#if MIN_VERSION_network(2,7,0)
-- |For backwards compatibility with old network package.
data PortID
= Service String
| PortNumber Word16
#if !defined(mingw32_HOST_OS)
| UnixSocket String
#endif
#endif
pgConnect :: String -- ^ the host to connect to
-> PortID -- ^ the port to connect on
-> ByteString -- ^ the database to connect to
-> ByteString -- ^ the username to connect as
-> ByteString -- ^ the password to connect with
-> IO PG.PGConnection -- ^ a handle to communicate with the PostgreSQL server on
pgConnect h n d u p = do
debug <- isJust `liftM` lookupEnv "TPG_DEBUG"
PG.pgConnect $ PG.defaultPGDatabase
{ PG.pgDBAddr = case n of
PortNumber s -> Left (h, show s)
Service s -> Left (h, s)
#if !defined(mingw32_HOST_OS)
UnixSocket s -> Right (Net.SockAddrUnix s)
#endif
, PG.pgDBName = d
, PG.pgDBUser = u
, PG.pgDBPass = p
, PG.pgDBDebug = debug
}

View File

@ -1,67 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Database.PostgreSQL.Typed.TypeCache
( PGTypes
, pgGetTypes
, PGTypeConnection
, pgConnection
, newPGTypeConnection
, flushPGTypeConnection
, lookupPGType
, findPGType
) where
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import qualified Data.IntMap as IntMap
import Data.List (find)
import Database.PostgreSQL.Typed.Types (PGName, OID)
import Database.PostgreSQL.Typed.Dynamic
import Database.PostgreSQL.Typed.Protocol
-- |Map keyed on fromIntegral OID.
type PGTypes = IntMap.IntMap PGName
-- |A 'PGConnection' along with cached information about types.
data PGTypeConnection = PGTypeConnection
{ pgConnection :: !PGConnection
, pgTypes :: IORef (Maybe PGTypes)
}
-- |Create a 'PGTypeConnection'.
newPGTypeConnection :: PGConnection -> IO PGTypeConnection
newPGTypeConnection c = do
t <- newIORef Nothing
return $ PGTypeConnection c t
-- |Flush the cached type list, forcing it to be reloaded.
flushPGTypeConnection :: PGTypeConnection -> IO ()
flushPGTypeConnection c =
writeIORef (pgTypes c) Nothing
-- |Get a map of types from the database.
pgGetTypes :: PGConnection -> IO PGTypes
pgGetTypes c =
IntMap.fromAscList . map (\[to, tn] -> (fromIntegral (pgDecodeRep to :: OID), pgDecodeRep tn)) .
snd <$> pgSimpleQuery c "SELECT oid, format_type(CASE WHEN typtype = 'd' THEN typbasetype ELSE oid END, -1) FROM pg_catalog.pg_type ORDER BY oid"
-- |Get a cached map of types.
getPGTypes :: PGTypeConnection -> IO PGTypes
getPGTypes (PGTypeConnection c tr) =
maybe (do
t <- pgGetTypes c
writeIORef tr $ Just t
return t)
return
=<< readIORef tr
-- |Lookup a type name by OID.
-- This is an efficient, often pure operation.
lookupPGType :: PGTypeConnection -> OID -> IO (Maybe PGName)
lookupPGType c o =
IntMap.lookup (fromIntegral o) <$> getPGTypes c
-- |Lookup a type OID by type name.
-- This is less common and thus less efficient than going the other way.
findPGType :: PGTypeConnection -> PGName -> IO (Maybe OID)
findPGType c t =
fmap (fromIntegral . fst) . find ((==) t . snd) . IntMap.toList <$> getPGTypes c

View File

@ -1,827 +0,0 @@
{-# LANGUAGE CPP, FlexibleInstances, ScopedTypeVariables, MultiParamTypeClasses, FlexibleContexts, DataKinds, KindSignatures, TypeFamilies, DeriveDataTypeable #-}
#if __GLASGOW_HASKELL__ < 710
{-# LANGUAGE OverlappingInstances #-}
#endif
#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE UndecidableSuperClasses #-}
#endif
-- |
-- Module: Database.PostgreSQL.Typed.Types
-- Copyright: 2015 Dylan Simon
--
-- Classes to support type inference, value encoding/decoding, and instances to support built-in PostgreSQL types.
module Database.PostgreSQL.Typed.Types
(
-- * Basic types
OID
, PGValue(..)
, PGValues
, PGTypeID(..)
, PGTypeEnv(..), unknownPGTypeEnv
, PGName(..), pgNameBS, pgNameString
, PGRecord(..)
-- * Marshalling classes
, PGType(..)
, PGParameter(..)
, PGColumn(..)
, PGStringType
, PGRecordType
-- * Marshalling interface
, pgEncodeParameter
, pgEscapeParameter
, pgDecodeColumn
, pgDecodeColumnNotNull
-- * Conversion utilities
, pgQuote
, pgDQuote
, pgDQuoteFrom
, parsePGDQuote
, buildPGValue
) where
import qualified Codec.Binary.UTF8.String as UTF8
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<$), (<*), (*>))
#endif
import Control.Arrow ((&&&))
#ifdef VERSION_aeson
import qualified Data.Aeson as JSON
#endif
import qualified Data.Attoparsec.ByteString as P (anyWord8)
import qualified Data.Attoparsec.ByteString.Char8 as P
import Data.Bits (shiftL, (.|.))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Builder.Prim as BSBP
import qualified Data.ByteString.Char8 as BSC
import Data.ByteString.Internal (c2w, w2c)
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.UTF8 as BSU
import Data.Char (isSpace, isDigit, digitToInt, intToDigit, toLower)
import Data.Data (Data)
import Data.Int
import Data.List (intersperse)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (mempty, mconcat)
#endif
import Data.Ratio ((%), numerator, denominator)
#ifdef VERSION_scientific
import Data.Scientific (Scientific)
#endif
import Data.String (IsString(..))
#ifdef VERSION_text
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
#endif
import qualified Data.Time as Time
#if MIN_VERSION_time(1,5,0)
import Data.Time (defaultTimeLocale)
#else
import System.Locale (defaultTimeLocale)
#endif
import Data.Typeable (Typeable)
#ifdef VERSION_uuid
import qualified Data.UUID as UUID
#endif
import Data.Word (Word8, Word32)
import GHC.TypeLits (Symbol, symbolVal, KnownSymbol)
import Numeric (readFloat)
#ifdef VERSION_postgresql_binary
#if MIN_VERSION_postgresql_binary(0,12,0)
import qualified PostgreSQL.Binary.Decoding as BinD
import qualified PostgreSQL.Binary.Encoding as BinE
#else
import qualified PostgreSQL.Binary.Decoder as BinD
import qualified PostgreSQL.Binary.Encoder as BinE
#endif
#endif
type PGTextValue = BS.ByteString
type PGBinaryValue = BS.ByteString
-- |A value passed to or from PostgreSQL in raw format.
data PGValue
= PGNullValue
| PGTextValue { pgTextValue :: PGTextValue } -- ^ The standard text encoding format (also used for unknown formats)
| PGBinaryValue { pgBinaryValue :: PGBinaryValue } -- ^ Special binary-encoded data. Not supported in all cases.
deriving (Show, Eq)
-- |A list of (nullable) data values, e.g. a single row or query parameters.
type PGValues = [PGValue]
-- |Parameters that affect how marshalling happens.
-- Currenly we force all other relevant parameters at connect time.
-- Nothing values represent unknown.
data PGTypeEnv = PGTypeEnv
{ pgIntegerDatetimes :: Maybe Bool -- ^ If @integer_datetimes@ is @on@; only relevant for binary encoding.
, pgServerVersion :: Maybe BS.ByteString -- ^ The @server_version@ parameter
} deriving (Show)
unknownPGTypeEnv :: PGTypeEnv
unknownPGTypeEnv = PGTypeEnv
{ pgIntegerDatetimes = Nothing
, pgServerVersion = Nothing
}
-- |A PostgreSQL literal identifier, generally corresponding to the \"name\" type (63-byte strings), but as it would be entered in a query, so may include double-quoting for special characters or schema-qualification.
newtype PGName = PGName
{ pgNameBytes :: [Word8] -- ^Raw bytes of the identifier (should really be a 'BS.ByteString', but we need a working 'Data' instance for annotations).
}
deriving (Eq, Ord, Typeable, Data)
-- |The literal identifier as used in a query.
pgNameBS :: PGName -> BS.ByteString
pgNameBS = BS.pack . pgNameBytes
-- |Applies utf-8 encoding.
instance IsString PGName where
fromString = PGName . UTF8.encode
-- |Unquoted 'pgNameString'.
instance Show PGName where
show = pgNameString
-- |Reverses the 'IsString' instantce.
pgNameString :: PGName -> String
pgNameString = UTF8.decode . pgNameBytes
-- |A proxy type for PostgreSQL types. The type argument should be an (internal) name of a database type, as per @format_type(OID)@ (usually the same as @\\dT+@).
-- When the type's namespace (schema) is not in @search_path@, this will be explicitly qualified, so you should be sure to have a consistent @search_path@ for all database connections.
-- The underlying 'Symbol' should be considered a lifted 'PGName'.
data PGTypeID (t :: Symbol) = PGTypeProxy
-- |A valid PostgreSQL type, its metadata, and corresponding Haskell representation.
-- For conversion the other way (from Haskell type to PostgreSQL), see 'Database.PostgreSQL.Typed.Dynamic.PGRep'.
-- Unfortunately any instances of this will be orphans.
class (KnownSymbol t
#if __GLASGOW_HASKELL__ >= 800
, PGParameter t (PGVal t), PGColumn t (PGVal t)
#endif
) => PGType t where
-- |The default, native Haskell representation of this type, which should be as close as possible to the PostgreSQL representation.
type PGVal t :: *
-- |The string name of this type: specialized version of 'symbolVal'.
pgTypeName :: PGTypeID t -> PGName
pgTypeName = fromString . symbolVal
-- |Does this type support binary decoding?
-- If so, 'pgDecodeBinary' must be implemented for every 'PGColumn' instance of this type.
pgBinaryColumn :: PGTypeEnv -> PGTypeID t -> Bool
pgBinaryColumn _ _ = False
-- |A @PGParameter t a@ instance describes how to encode a PostgreSQL type @t@ from @a@.
class PGType t => PGParameter t a where
-- |Encode a value to a PostgreSQL text representation.
pgEncode :: PGTypeID t -> a -> PGTextValue
-- |Encode a value to a (quoted) literal value for use in SQL statements.
-- Defaults to a quoted version of 'pgEncode'
pgLiteral :: PGTypeID t -> a -> BS.ByteString
pgLiteral t = pgQuote . pgEncode t
-- |Encode a value to a PostgreSQL representation.
-- Defaults to the text representation by pgEncode
pgEncodeValue :: PGTypeEnv -> PGTypeID t -> a -> PGValue
pgEncodeValue _ t = PGTextValue . pgEncode t
-- |A @PGColumn t a@ instance describes how te decode a PostgreSQL type @t@ to @a@.
class PGType t => PGColumn t a where
-- |Decode the PostgreSQL text representation into a value.
pgDecode :: PGTypeID t -> PGTextValue -> a
-- |Decode the PostgreSQL binary representation into a value.
-- Only needs to be implemented if 'pgBinaryColumn' is true.
pgDecodeBinary :: PGTypeEnv -> PGTypeID t -> PGBinaryValue -> a
pgDecodeBinary _ t _ = error $ "pgDecodeBinary " ++ show (pgTypeName t) ++ ": not supported"
pgDecodeValue :: PGTypeEnv -> PGTypeID t -> PGValue -> a
pgDecodeValue _ t (PGTextValue v) = pgDecode t v
pgDecodeValue e t (PGBinaryValue v) = pgDecodeBinary e t v
pgDecodeValue _ t PGNullValue = error $ "NULL in " ++ show (pgTypeName t) ++ " column (use Maybe or COALESCE)"
instance PGParameter t a => PGParameter t (Maybe a) where
pgEncode t = maybe (error $ "pgEncode " ++ show (pgTypeName t) ++ ": Nothing") (pgEncode t)
pgLiteral = maybe (BSC.pack "NULL") . pgLiteral
pgEncodeValue e = maybe PGNullValue . pgEncodeValue e
instance PGColumn t a => PGColumn t (Maybe a) where
pgDecode t = Just . pgDecode t
pgDecodeBinary e t = Just . pgDecodeBinary e t
pgDecodeValue _ _ PGNullValue = Nothing
pgDecodeValue e t v = Just $ pgDecodeValue e t v
-- |Final parameter encoding function used when a (nullable) parameter is passed to a prepared query.
pgEncodeParameter :: PGParameter t a => PGTypeEnv -> PGTypeID t -> a -> PGValue
pgEncodeParameter = pgEncodeValue
-- |Final parameter escaping function used when a (nullable) parameter is passed to be substituted into a simple query.
pgEscapeParameter :: PGParameter t a => PGTypeEnv -> PGTypeID t -> a -> BS.ByteString
pgEscapeParameter _ = pgLiteral
-- |Final column decoding function used for a nullable result value.
pgDecodeColumn :: PGColumn t (Maybe a) => PGTypeEnv -> PGTypeID t -> PGValue -> Maybe a
pgDecodeColumn = pgDecodeValue
-- |Final column decoding function used for a non-nullable result value.
pgDecodeColumnNotNull :: PGColumn t a => PGTypeEnv -> PGTypeID t -> PGValue -> a
pgDecodeColumnNotNull = pgDecodeValue
pgQuoteUnsafe :: BS.ByteString -> BS.ByteString
pgQuoteUnsafe = (`BSC.snoc` '\'') . BSC.cons '\''
-- |Produce a SQL string literal by wrapping (and escaping) a string with single quotes.
pgQuote :: BS.ByteString -> BS.ByteString
pgQuote s
| '\0' `BSC.elem` s = error "pgQuote: unhandled null in literal"
| otherwise = pgQuoteUnsafe $ BSC.intercalate (BSC.pack "''") $ BSC.split '\'' s
-- |Shorthand for @'BSL.toStrict' . 'BSB.toLazyByteString'@
buildPGValue :: BSB.Builder -> BS.ByteString
buildPGValue = BSL.toStrict . BSB.toLazyByteString
-- |Double-quote a value (e.g., as an identifier).
-- Does not properly handle unicode escaping (yet).
pgDQuote :: BS.ByteString -> BSB.Builder
pgDQuote s = dq <> BSBP.primMapByteStringBounded ec s <> dq where
dq = BSB.char7 '"'
ec = BSBP.condB (\c -> c == c2w '"' || c == c2w '\\') bs (BSBP.liftFixedToBounded BSBP.word8)
bs = BSBP.liftFixedToBounded $ ((,) '\\') BSBP.>$< (BSBP.char7 BSBP.>*< BSBP.word8)
-- |Double-quote a value if it's \"\", \"null\", or contains any whitespace, \'\"\', \'\\\', or the characters given in the first argument.
pgDQuoteFrom :: [Char] -> BS.ByteString -> BSB.Builder
pgDQuoteFrom unsafe s
| BS.null s || BSC.any (\c -> isSpace c || c == '"' || c == '\\' || c `elem` unsafe) s || BSC.map toLower s == BSC.pack "null" = pgDQuote s
| otherwise = BSB.byteString s
-- |Parse double-quoted values ala 'pgDQuote'.
parsePGDQuote :: Bool -> [Char] -> (BS.ByteString -> Bool) -> P.Parser (Maybe BS.ByteString)
parsePGDQuote blank unsafe isnul = (Just <$> q) <> (mnul <$> uq) where
q = P.char '"' *> (BS.concat <$> qs)
qs = do
p <- P.takeTill (\c -> c == '"' || c == '\\')
e <- P.anyChar
if e == '"'
then return [p]
else do
c <- P.anyWord8
(p :) . (BS.singleton c :) <$> qs
uq = (if blank then P.takeWhile else P.takeWhile1) (`notElem` ('"':'\\':unsafe))
mnul s
| isnul s = Nothing
| otherwise = Just s
#ifdef VERSION_postgresql_binary
binEnc :: BinEncoder a -> a -> BS.ByteString
binEnc = (.)
#if MIN_VERSION_postgresql_binary(0,12,0)
BinE.encodingBytes
type BinDecoder = BinD.Value
type BinEncoder a = a -> BinE.Encoding
#else
buildPGValue
type BinDecoder = BinD.Decoder
type BinEncoder a = BinE.Encoder a
#endif
binDec :: PGType t => BinDecoder a -> PGTypeID t -> PGBinaryValue -> a
binDec d t = either (\e -> error $ "pgDecodeBinary " ++ show (pgTypeName t) ++ ": " ++ show e) id .
#if MIN_VERSION_postgresql_binary(0,12,0)
BinD.valueParser
#else
BinD.run
#endif
d
#define BIN_COL pgBinaryColumn _ _ = True
#define BIN_ENC(F) pgEncodeValue _ _ = PGBinaryValue . binEnc (F)
#define BIN_DEC(F) pgDecodeBinary _ = binDec (F)
#else
#define BIN_COL
#define BIN_ENC(F)
#define BIN_DEC(F)
#endif
instance PGType "any" where
type PGVal "any" = PGValue
instance PGType t => PGColumn t PGValue where
pgDecode _ = PGTextValue
pgDecodeBinary _ _ = PGBinaryValue
pgDecodeValue _ _ = id
instance PGParameter "any" PGValue where
pgEncode _ (PGTextValue v) = v
pgEncode _ PGNullValue = error "pgEncode any: NULL"
pgEncode _ (PGBinaryValue _) = error "pgEncode any: binary"
pgEncodeValue _ _ = id
instance PGType "void" where
type PGVal "void" = ()
instance PGParameter "void" () where
pgEncode _ _ = BSC.empty
instance PGColumn "void" () where
pgDecode _ _ = ()
pgDecodeBinary _ _ _ = ()
pgDecodeValue _ _ _ = ()
instance PGType "boolean" where
type PGVal "boolean" = Bool
BIN_COL
instance PGParameter "boolean" Bool where
pgEncode _ False = BSC.singleton 'f'
pgEncode _ True = BSC.singleton 't'
pgLiteral _ False = BSC.pack "false"
pgLiteral _ True = BSC.pack "true"
BIN_ENC(BinE.bool)
instance PGColumn "boolean" Bool where
pgDecode _ s = case BSC.head s of
'f' -> False
't' -> True
c -> error $ "pgDecode boolean: " ++ [c]
BIN_DEC(BinD.bool)
type OID = Word32
instance PGType "oid" where
type PGVal "oid" = OID
BIN_COL
instance PGParameter "oid" OID where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.int4_word32)
instance PGColumn "oid" OID where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.int)
instance PGType "smallint" where
type PGVal "smallint" = Int16
BIN_COL
instance PGParameter "smallint" Int16 where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.int2_int16)
instance PGColumn "smallint" Int16 where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.int)
instance PGType "integer" where
type PGVal "integer" = Int32
BIN_COL
instance PGParameter "integer" Int32 where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.int4_int32)
instance PGColumn "integer" Int32 where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.int)
instance PGType "bigint" where
type PGVal "bigint" = Int64
BIN_COL
instance PGParameter "bigint" Int64 where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.int8_int64)
instance PGColumn "bigint" Int64 where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.int)
instance PGType "real" where
type PGVal "real" = Float
BIN_COL
instance PGParameter "real" Float where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.float4)
instance PGColumn "real" Float where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.float4)
instance PGColumn "real" Double where
pgDecode _ = read . BSC.unpack
BIN_DEC(realToFrac <$> BinD.float4)
instance PGType "double precision" where
type PGVal "double precision" = Double
BIN_COL
instance PGParameter "double precision" Double where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.float8)
instance PGParameter "double precision" Float where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.float8 . realToFrac)
instance PGColumn "double precision" Double where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.float8)
-- XXX need real encoding as text
-- but then no one should be using this type really...
instance PGType "\"char\"" where
type PGVal "\"char\"" = Word8
BIN_COL
instance PGParameter "\"char\"" Word8 where
pgEncode _ = BS.singleton
pgEncodeValue _ _ = PGBinaryValue . BS.singleton
instance PGColumn "\"char\"" Word8 where
pgDecode _ = BS.head
pgDecodeBinary _ _ = BS.head
instance PGParameter "\"char\"" Char where
pgEncode _ = BSC.singleton
pgEncodeValue _ _ = PGBinaryValue . BSC.singleton
instance PGColumn "\"char\"" Char where
pgDecode _ = BSC.head
pgDecodeBinary _ _ = BSC.head
class PGType t => PGStringType t
instance PGStringType t => PGParameter t String where
pgEncode _ = BSU.fromString
BIN_ENC(BinE.text_strict . T.pack)
instance PGStringType t => PGColumn t String where
pgDecode _ = BSU.toString
BIN_DEC(T.unpack <$> BinD.text_strict)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPABLE #-}
#endif
PGStringType t => PGParameter t BS.ByteString where
pgEncode _ = id
BIN_ENC(BinE.text_strict . TE.decodeUtf8)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPABLE #-}
#endif
PGStringType t => PGColumn t BS.ByteString where
pgDecode _ = id
BIN_DEC(TE.encodeUtf8 <$> BinD.text_strict)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPABLE #-}
#endif
PGStringType t => PGParameter t PGName where
pgEncode _ = pgNameBS
BIN_ENC(BinE.text_strict . TE.decodeUtf8 . pgNameBS)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPABLE #-}
#endif
PGStringType t => PGColumn t PGName where
pgDecode _ = PGName . BS.unpack
BIN_DEC(PGName . BS.unpack . TE.encodeUtf8 <$> BinD.text_strict)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPABLE #-}
#endif
PGStringType t => PGParameter t BSL.ByteString where
pgEncode _ = BSL.toStrict
BIN_ENC(BinE.text_lazy . TLE.decodeUtf8)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPABLE #-}
#endif
PGStringType t => PGColumn t BSL.ByteString where
pgDecode _ = BSL.fromStrict
BIN_DEC(TLE.encodeUtf8 <$> BinD.text_lazy)
#ifdef VERSION_text
instance PGStringType t => PGParameter t T.Text where
pgEncode _ = TE.encodeUtf8
BIN_ENC(BinE.text_strict)
instance PGStringType t => PGColumn t T.Text where
pgDecode _ = TE.decodeUtf8
BIN_DEC(BinD.text_strict)
instance PGStringType t => PGParameter t TL.Text where
pgEncode _ = BSL.toStrict . TLE.encodeUtf8
BIN_ENC(BinE.text_lazy)
instance PGStringType t => PGColumn t TL.Text where
pgDecode _ = TL.fromStrict . TE.decodeUtf8
BIN_DEC(BinD.text_lazy)
#define PGVALSTRING T.Text
#else
#define PGVALSTRING String
#endif
instance PGType "text" where
type PGVal "text" = PGVALSTRING
BIN_COL
instance PGType "character varying" where
type PGVal "character varying" = PGVALSTRING
BIN_COL
instance PGType "name" where
type PGVal "name" = PGVALSTRING
BIN_COL
instance PGType "bpchar" where
type PGVal "bpchar" = PGVALSTRING
BIN_COL
instance PGStringType "text"
instance PGStringType "character varying"
instance PGStringType "name" -- limit 63 characters; not strictly textsend but essentially the same
instance PGStringType "bpchar" -- blank padded
encodeBytea :: BSB.Builder -> PGTextValue
encodeBytea h = buildPGValue $ BSB.string7 "\\x" <> h
decodeBytea :: PGTextValue -> [Word8]
decodeBytea s
| sm /= "\\x" = error $ "pgDecode bytea: " ++ sm
| otherwise = pd $ BS.unpack d where
(m, d) = BS.splitAt 2 s
sm = BSC.unpack m
pd [] = []
pd (h:l:r) = (shiftL (unhex h) 4 .|. unhex l) : pd r
pd [x] = error $ "pgDecode bytea: " ++ show x
unhex = fromIntegral . digitToInt . w2c
instance PGType "bytea" where
type PGVal "bytea" = BS.ByteString
BIN_COL
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPING #-}
#endif
PGParameter "bytea" BSL.ByteString where
pgEncode _ = encodeBytea . BSB.lazyByteStringHex
pgLiteral t = pgQuoteUnsafe . pgEncode t
BIN_ENC(BinE.bytea_lazy)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPING #-}
#endif
PGColumn "bytea" BSL.ByteString where
pgDecode _ = BSL.pack . decodeBytea
BIN_DEC(BinD.bytea_lazy)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPING #-}
#endif
PGParameter "bytea" BS.ByteString where
pgEncode _ = encodeBytea . BSB.byteStringHex
pgLiteral t = pgQuoteUnsafe . pgEncode t
BIN_ENC(BinE.bytea_strict)
instance
#if __GLASGOW_HASKELL__ >= 710
{-# OVERLAPPING #-}
#endif
PGColumn "bytea" BS.ByteString where
pgDecode _ = BS.pack . decodeBytea
BIN_DEC(BinD.bytea_strict)
readTime :: Time.ParseTime t => String -> String -> t
readTime =
#if MIN_VERSION_time(1,5,0)
Time.parseTimeOrError False
#else
Time.readTime
#endif
defaultTimeLocale
instance PGType "date" where
type PGVal "date" = Time.Day
BIN_COL
instance PGParameter "date" Time.Day where
pgEncode _ = BSC.pack . Time.showGregorian
pgLiteral t = pgQuoteUnsafe . pgEncode t
BIN_ENC(BinE.date)
instance PGColumn "date" Time.Day where
pgDecode _ = readTime "%F" . BSC.unpack
BIN_DEC(BinD.date)
binColDatetime :: PGTypeEnv -> PGTypeID t -> Bool
#ifdef VERSION_postgresql_binary
binColDatetime PGTypeEnv{ pgIntegerDatetimes = Just _ } _ = True
#endif
binColDatetime _ _ = False
#ifdef VERSION_postgresql_binary
binEncDatetime :: PGParameter t a => BinEncoder a -> BinEncoder a -> PGTypeEnv -> PGTypeID t -> a -> PGValue
binEncDatetime _ ff PGTypeEnv{ pgIntegerDatetimes = Just False } _ = PGBinaryValue . binEnc ff
binEncDatetime fi _ PGTypeEnv{ pgIntegerDatetimes = Just True } _ = PGBinaryValue . binEnc fi
binEncDatetime _ _ PGTypeEnv{ pgIntegerDatetimes = Nothing } t = PGTextValue . pgEncode t
binDecDatetime :: PGColumn t a => BinDecoder a -> BinDecoder a -> PGTypeEnv -> PGTypeID t -> PGBinaryValue -> a
binDecDatetime _ ff PGTypeEnv{ pgIntegerDatetimes = Just False } = binDec ff
binDecDatetime fi _ PGTypeEnv{ pgIntegerDatetimes = Just True } = binDec fi
binDecDatetime _ _ PGTypeEnv{ pgIntegerDatetimes = Nothing } = error "pgDecodeBinary: unknown integer_datetimes value"
#endif
-- PostgreSQL uses "[+-]HH[:MM]" timezone offsets, while "%z" uses "+HHMM" by default.
-- readTime can successfully parse both formats, but PostgreSQL needs the colon.
fixTZ :: String -> String
fixTZ "" = ""
fixTZ ['+',h1,h2] | isDigit h1 && isDigit h2 = ['+',h1,h2,':','0','0']
fixTZ ['-',h1,h2] | isDigit h1 && isDigit h2 = ['-',h1,h2,':','0','0']
fixTZ ['+',h1,h2,m1,m2] | isDigit h1 && isDigit h2 && isDigit m1 && isDigit m2 = ['+',h1,h2,':',m1,m2]
fixTZ ['-',h1,h2,m1,m2] | isDigit h1 && isDigit h2 && isDigit m1 && isDigit m2 = ['-',h1,h2,':',m1,m2]
fixTZ (c:s) = c:fixTZ s
instance PGType "time without time zone" where
type PGVal "time without time zone" = Time.TimeOfDay
pgBinaryColumn = binColDatetime
instance PGParameter "time without time zone" Time.TimeOfDay where
pgEncode _ = BSC.pack . Time.formatTime defaultTimeLocale "%T%Q"
pgLiteral t = pgQuoteUnsafe . pgEncode t
#ifdef VERSION_postgresql_binary
pgEncodeValue = binEncDatetime BinE.time_int BinE.time_float
#endif
instance PGColumn "time without time zone" Time.TimeOfDay where
pgDecode _ = readTime "%T%Q" . BSC.unpack
#ifdef VERSION_postgresql_binary
pgDecodeBinary = binDecDatetime BinD.time_int BinD.time_float
#endif
instance PGType "time with time zone" where
type PGVal "time with time zone" = (Time.TimeOfDay, Time.TimeZone)
pgBinaryColumn = binColDatetime
instance PGParameter "time with time zone" (Time.TimeOfDay, Time.TimeZone) where
pgEncode _ (t, z) = BSC.pack $ Time.formatTime defaultTimeLocale "%T%Q" t ++ fixTZ (Time.formatTime defaultTimeLocale "%z" z)
pgLiteral t = pgQuoteUnsafe . pgEncode t
#ifdef VERSION_postgresql_binary
pgEncodeValue = binEncDatetime BinE.timetz_int BinE.timetz_float
#endif
instance PGColumn "time with time zone" (Time.TimeOfDay, Time.TimeZone) where
pgDecode _ = (Time.localTimeOfDay . Time.zonedTimeToLocalTime &&& Time.zonedTimeZone) . readTime "%T%Q%z" . fixTZ . BSC.unpack
#ifdef VERSION_postgresql_binary
pgDecodeBinary = binDecDatetime BinD.timetz_int BinD.timetz_float
#endif
instance PGType "timestamp without time zone" where
type PGVal "timestamp without time zone" = Time.LocalTime
pgBinaryColumn = binColDatetime
instance PGParameter "timestamp without time zone" Time.LocalTime where
pgEncode _ = BSC.pack . Time.formatTime defaultTimeLocale "%F %T%Q"
pgLiteral t = pgQuoteUnsafe . pgEncode t
#ifdef VERSION_postgresql_binary
pgEncodeValue = binEncDatetime BinE.timestamp_int BinE.timestamp_float
#endif
instance PGColumn "timestamp without time zone" Time.LocalTime where
pgDecode _ = readTime "%F %T%Q" . BSC.unpack
#ifdef VERSION_postgresql_binary
pgDecodeBinary = binDecDatetime BinD.timestamp_int BinD.timestamp_float
#endif
instance PGType "timestamp with time zone" where
type PGVal "timestamp with time zone" = Time.UTCTime
pgBinaryColumn = binColDatetime
instance PGParameter "timestamp with time zone" Time.UTCTime where
pgEncode _ = BSC.pack . fixTZ . Time.formatTime defaultTimeLocale "%F %T%Q%z"
-- pgLiteral t = pgQuoteUnsafe . pgEncode t
#ifdef VERSION_postgresql_binary
pgEncodeValue = binEncDatetime BinE.timestamptz_int BinE.timestamptz_float
#endif
instance PGColumn "timestamp with time zone" Time.UTCTime where
pgDecode _ = readTime "%F %T%Q%z" . fixTZ . BSC.unpack
#ifdef VERSION_postgresql_binary
pgDecodeBinary = binDecDatetime BinD.timestamptz_int BinD.timestamptz_float
#endif
instance PGType "interval" where
type PGVal "interval" = Time.DiffTime
pgBinaryColumn = binColDatetime
instance PGParameter "interval" Time.DiffTime where
pgEncode _ = BSC.pack . show
pgLiteral t = pgQuoteUnsafe . pgEncode t
#ifdef VERSION_postgresql_binary
pgEncodeValue = binEncDatetime BinE.interval_int BinE.interval_float
#endif
-- |Representation of DiffTime as interval.
-- PostgreSQL stores months and days separately in intervals, but DiffTime does not.
-- We collapse all interval fields into seconds
instance PGColumn "interval" Time.DiffTime where
pgDecode _ a = either (error . ("pgDecode interval (" ++) . (++ ("): " ++ BSC.unpack a))) realToFrac $ P.parseOnly ps a where
ps = do
_ <- P.char 'P'
d <- units [('Y', 12*month), ('M', month), ('W', 7*day), ('D', day)]
((d +) <$> pt) <> (d <$ P.endOfInput)
pt = do
_ <- P.char 'T'
t <- units [('H', 3600), ('M', 60), ('S', 1)]
P.endOfInput
return t
units l = fmap sum $ P.many' $ do
x <- P.signed P.scientific
u <- P.choice $ map (\(c, u) -> u <$ P.char c) l
return $ x * u
day = 86400
month = 2629746
#ifdef VERSION_postgresql_binary
pgDecodeBinary = binDecDatetime BinD.interval_int BinD.interval_float
#endif
instance PGType "numeric" where
type PGVal "numeric" =
#ifdef VERSION_scientific
Scientific
#else
Rational
#endif
BIN_COL
instance PGParameter "numeric" Rational where
pgEncode _ r
| denominator r == 0 = BSC.pack "NaN" -- this can't happen
| otherwise = BSC.pack $ take 30 (showRational (r / (10 ^^ e))) ++ 'e' : show e where
e = floor $ logBase (10 :: Double) $ fromRational $ abs r :: Int -- not great, and arbitrarily truncate somewhere
pgLiteral _ r
| denominator r == 0 = BSC.pack "'NaN'" -- this can't happen
| otherwise = BSC.pack $ '(' : show (numerator r) ++ '/' : show (denominator r) ++ "::numeric)"
BIN_ENC(BinE.numeric . realToFrac)
-- |High-precision representation of Rational as numeric.
-- Unfortunately, numeric has an NaN, while Rational does not.
-- NaN numeric values will produce exceptions.
instance PGColumn "numeric" Rational where
pgDecode _ bs
| s == "NaN" = 0 % 0 -- this won't work
| otherwise = ur $ readFloat s where
ur [(x,"")] = x
ur _ = error $ "pgDecode numeric: " ++ s
s = BSC.unpack bs
BIN_DEC(realToFrac <$> BinD.numeric)
-- This will produce infinite(-precision) strings
showRational :: Rational -> String
showRational r = show (ri :: Integer) ++ '.' : frac (abs rf) where
(ri, rf) = properFraction r
frac 0 = ""
frac f = intToDigit i : frac f' where (i, f') = properFraction (10 * f)
#ifdef VERSION_scientific
instance PGParameter "numeric" Scientific where
pgEncode _ = BSC.pack . show
pgLiteral = pgEncode
BIN_ENC(BinE.numeric)
instance PGColumn "numeric" Scientific where
pgDecode _ = read . BSC.unpack
BIN_DEC(BinD.numeric)
#endif
#ifdef VERSION_uuid
instance PGType "uuid" where
type PGVal "uuid" = UUID.UUID
BIN_COL
instance PGParameter "uuid" UUID.UUID where
pgEncode _ = UUID.toASCIIBytes
pgLiteral t = pgQuoteUnsafe . pgEncode t
BIN_ENC(BinE.uuid)
instance PGColumn "uuid" UUID.UUID where
pgDecode _ u = fromMaybe (error $ "pgDecode uuid: " ++ BSC.unpack u) $ UUID.fromASCIIBytes u
BIN_DEC(BinD.uuid)
#endif
-- |Generic class of composite (row or record) types.
newtype PGRecord = PGRecord [Maybe PGTextValue]
class PGType t => PGRecordType t
instance PGRecordType t => PGParameter t PGRecord where
pgEncode _ (PGRecord l) =
buildPGValue $ BSB.char7 '(' <> mconcat (intersperse (BSB.char7 ',') $ map (maybe mempty (pgDQuoteFrom "(),")) l) <> BSB.char7 ')'
pgLiteral _ (PGRecord l) =
BSC.pack "ROW(" <> BS.intercalate (BSC.singleton ',') (map (maybe (BSC.pack "NULL") pgQuote) l) `BSC.snoc` ')'
instance PGRecordType t => PGColumn t PGRecord where
pgDecode _ a = either (error . ("pgDecode record (" ++) . (++ ("): " ++ BSC.unpack a))) PGRecord $ P.parseOnly pa a where
pa = P.char '(' *> P.sepBy el (P.char ',') <* P.char ')' <* P.endOfInput
el = parsePGDQuote True "()," BS.null
instance PGType "record" where
type PGVal "record" = PGRecord
-- |The generic anonymous record type, as created by @ROW@.
-- In this case we can not know the types, and in fact, PostgreSQL does not accept values of this type regardless (except as literals).
instance PGRecordType "record"
#ifdef VERSION_aeson
instance PGType "json" where
type PGVal "json" = JSON.Value
BIN_COL
instance PGParameter "json" JSON.Value where
pgEncode _ = BSL.toStrict . JSON.encode
BIN_ENC(BinE.json_ast)
instance PGColumn "json" JSON.Value where
pgDecode _ j = either (error . ("pgDecode json (" ++) . (++ ("): " ++ BSC.unpack j))) id $ P.parseOnly JSON.json j
BIN_DEC(BinD.json_ast)
instance PGType "jsonb" where
type PGVal "jsonb" = JSON.Value
BIN_COL
instance PGParameter "jsonb" JSON.Value where
pgEncode _ = BSL.toStrict . JSON.encode
BIN_ENC(BinE.jsonb_ast)
instance PGColumn "jsonb" JSON.Value where
pgDecode _ j = either (error . ("pgDecode jsonb (" ++) . (++ ("): " ++ BSC.unpack j))) id $ P.parseOnly JSON.json j
BIN_DEC(BinD.jsonb_ast)
#endif
{-
--, ( 142, 143, "xml", ?)
--, ( 600, 1017, "point", ?)
--, ( 650, 651, "cidr", ?)
--, ( 790, 791, "money", Centi? Fixed?)
--, ( 829, 1040, "macaddr", ?)
--, ( 869, 1041, "inet", ?)
--, (1266, 1270, "timetz", ?)
--, (1560, 1561, "bit", Bool?)
--, (1562, 1563, "varbit", ?)
-}

View File

@ -1,107 +0,0 @@
# Haskell PostgreSQL-typed
A Haskell PostgreSQL interface that provides type-safety through compile-time (template Haskell) database access.
See the [Haddock](http://hackage.haskell.org/package/postgresql-typed) documentation in [Database.PostgreSQL.Typed](http://hackage.haskell.org/package/postgresql-typed/docs/Database-PostgreSQL-Typed.html) or the [test cases](test/Main.hs) for simple examples.
## Getting started
### Installation
Use your preferred package manager to install or add to your package dependencies:
- `stack install postgresql-typed` or
- `cabal install postgresql-typed`
You'll also likely need to add `network` as a dependency.
### Enable ghc extensions
Make sure you enable `TemplateHaskell`, `QuasiQuotes`, and `DataKinds` language extensions, either in your cabal `default-extensions` or in a `{-# LANGUAGE TemplateHaskell, QuasiQuotes, DataKinds #-}` pragma in your source.
### Setup compile-time database connection
Either set the following environment variables:
- `TPG_DB` the database name to use (default: same as user)
- `TPG_USER` the username to connect as (default: `$USER` or `postgres`)
- `TPG_PASS` the password to use (default: *empty*)
- `TPG_HOST` the host to connect to (default: `localhost`)
- `TPG_PORT` or `TPG_SOCK` the port number or local socket path to connect on (default port: `5432`)
*Or* in your code call `Database.PostgreSQL.Typed.useTPGDatabase` with a database config as a top-level quote in each code file where you have SQL queries.
It's often helpful to make your own utility function to do this:
```haskell
-- |Call this at top-level at the beginning of every file (rather than 'useTPGDatabase')
useMyTPGConfig :: Language.Haskell.TH.DecsQ
useMyTPGConfig = useTPGDatabase PGDatabase{ ... } -- or load config from file
```
### Setup your database schema
Your tables and other schema need to be created in your development (compile-time) database before you compile your code.
No queries will actually be executed, so there does not need to be any data, but it will do query parsing with the database (prepare queries) so any referenced objects must exist.
### Setup run-time database connection
Use `pgConnect` to connect to your database using a `PGDatabase` configuration.
The run-time database does not need to be the same as the build-time database (though it can be), but it *must* have the same schema.
It's recommended to use `bracket (pgConnect PGDatabase{..}) pgDisconnect`.
If you need a pool of connections, consider `resource-pool` (while `PGConnection`s are mostly thread-safe, they can't be used for multiple queries simultaneously).
### Complete example
schema.sql:
```sql
CREATE TABLE thing (id SERIAL PRIMARY KEY, name TEXT NOT NULL);
```
DBConfig.hs:
```haskell
{-# LANGUAGE OverloadedStrings #-}
module DBConfig where
import qualified Database.PostgreSQL.Typed as PG
import Network.Socket (SockAddr(SockAddrUnix))
myPGDatabase :: PG.PGDatabase
myPGDatabase = PG.defaultPGDatabase
{ PG.pgDBAddr = if tcp then Left ("localhost", "5432") else Right (SockAddrUnix "/run/postgresql/.s.PGSQL.5432")
, PG.pgDBUser = "user"
, PG.pgDBName = "db"
} where tcp = False
```
Main.hs:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
import Control.Exception (bracket)
import Control.Monad (void, unless)
import Data.Int (Int32)
import Data.Maybe (listToMaybe)
import qualified Database.PostgreSQL.Typed as PG
import DBConfig
PG.useTPGDatabase myPGDatabase
data Thing = Thing Int32 String
deriving (Eq)
createThing :: PG.PGConnection -> Thing -> IO ()
createThing pg (Thing tid tname) =
void $ PG.pgExecute pg [PG.pgSQL|INSERT INTO thing (id, name) VALUES (${tid}, ${tname})|]
lookupThing :: PG.PGConnection -> Int32 -> IO (Maybe Thing)
lookupThing pg tid = fmap (uncurry Thing) . listToMaybe <$>
PG.pgQuery pg [PG.pgSQL|SELECT id, name FROM thing WHERE id = ${tid}|]
main = bracket (PG.pgConnect myPGDatabase) PG.pgDisconnect $ \pg -> do
let myt = Thing 1 "cat"
createThing pg myt
t <- lookupThing pg 1
unless (t == Just myt) $ fail "wrong thing!"
```

View File

@ -1,2 +0,0 @@
import Distribution.Simple
main = defaultMain

View File

@ -1,174 +0,0 @@
Name: postgresql-typed
Version: 0.6.2.3
Cabal-Version: >= 1.10
License: BSD3
License-File: COPYING
Copyright: 2010-2013 Chris Forno, 2014-2019 Dylan Simon
Author: Dylan Simon
Maintainer: Dylan Simon <dylan-pgtyped@dylex.net>
Stability: provisional
Bug-Reports: https://github.com/dylex/postgresql-typed/issues
Homepage: https://github.com/dylex/postgresql-typed
Category: Database
Synopsis: PostgreSQL interface with compile-time SQL type checking, optional HDBC backend
Description: Automatically type-check SQL statements at compile time.
Uses Template Haskell and the raw PostgreSQL protocol to describe SQL statements at compile time and provide appropriate type marshalling for both parameters and results.
Allows not only syntax verification of your SQL but also full type safety between your SQL and Haskell.
Supports many built-in PostgreSQL types already, including arrays and ranges, and can be easily extended in user code to support any other types.
.
Also includes an optional HDBC backend that, since it uses the raw PostgreSQL protocol, may be more efficient than the normal libpq backend in some cases (though provides no more type safety than HDBC-postgresql when used without templates).
.
Originally based on Chris Forno's templatepg library.
Tested-With: GHC == 7.10.3, GHC == 8.0.1
Build-Type: Simple
extra-source-files: README.md
source-repository head
type: git
location: git://github.com/dylex/postgresql-typed
Flag md5
Description: Enable md5 password authentication method.
Default: True
Flag binary
Description: Use binary protocol encoding via postgresql-binary. This may put additional restrictions on supported PostgreSQL server versions.
Default: True
Flag text
Description: Support Text string values via text (implied by binary).
Default: True
Flag uuid
Description: Support the UUID type via uuid (implied by binary).
Default: True
Flag scientific
Description: Support decoding numeric via scientific (implied by binary).
Default: True
Flag aeson
Description: Support decoding json via aeson.
Default: True
Flag HDBC
Description: Provide an HDBC driver backend using the raw PostgreSQL protocol.
Flag tls
Description: Enable TLS (SSL) support in PostgreSQL server connections.
Default: True
Flag crypton
Description: Use crypton rather than cryptonite.
Default: True
Library
default-language: Haskell2010
Build-Depends:
base >= 4.8 && < 5,
array,
binary,
containers,
old-locale,
time,
bytestring >= 0.10.2,
template-haskell,
haskell-src-meta,
network,
attoparsec >= 0.12 && < 0.15,
utf8-string
Exposed-Modules:
Database.PostgreSQL.Typed
Database.PostgreSQL.Typed.Protocol
Database.PostgreSQL.Typed.Types
Database.PostgreSQL.Typed.TH
Database.PostgreSQL.Typed.Query
Database.PostgreSQL.Typed.Enum
Database.PostgreSQL.Typed.Array
Database.PostgreSQL.Typed.Range
Database.PostgreSQL.Typed.Inet
Database.PostgreSQL.Typed.Dynamic
Database.PostgreSQL.Typed.TemplatePG
Database.PostgreSQL.Typed.SQLToken
Database.PostgreSQL.Typed.ErrCodes
Database.PostgreSQL.Typed.Relation
Other-Modules:
Paths_postgresql_typed
Database.PostgreSQL.Typed.TypeCache
GHC-Options: -Wall
if flag(md5)
Build-Depends: memory >= 0.5
if flag(crypton)
Build-Depends: crypton
else
Build-Depends: cryptonite >= 0.5
if flag(binary)
Build-Depends: postgresql-binary >= 0.8, text >= 1, uuid >= 1.3, scientific >= 0.3
else
if flag(text)
Build-Depends: text >= 1
if flag(uuid)
Build-Depends: uuid >= 1.3
if flag(scientific)
Build-Depends: scientific >= 0.3
if flag(aeson)
Build-Depends: aeson >= 0.7
if flag(HDBC)
Build-Depends: HDBC >= 2.2
Exposed-Modules:
Database.PostgreSQL.Typed.HDBC
if flag(tls)
Build-Depends: data-default
if flag(crypton)
Build-Depends: tls >= 1.7, crypton-x509, crypton-x509-store, crypton-x509-validation
else
Build-Depends: tls < 1.7, x509, x509-store, x509-validation
test-suite test
default-language: Haskell2010
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Main.hs
Other-Modules: Connect
default-Extensions: TemplateHaskell, QuasiQuotes
build-depends: base, network, time, bytestring, postgresql-typed, QuickCheck
GHC-Options: -Wall
if flag(tls)
Build-Depends: tls
test-suite hdbc
default-language: Haskell2010
type: exitcode-stdio-1.0
hs-source-dirs: test/hdbc, test
main-is: runtests.hs
other-modules:
Connect
SpecificDB
TestMisc
TestSbasics
TestTime
TestUtils
Testbasics
Tests
if flag(HDBC)
build-depends: base, bytestring, network, time, containers, convertible, postgresql-typed, HDBC, HUnit
else
buildable: False
if flag(tls)
Build-Depends: tls
benchmark bench
default-language: Haskell2010
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Bench.hs
other-modules: Connect
build-depends:
base,
bytestring,
time,
network,
criterion,
postgresql-typed
if flag(tls)
Build-Depends: tls

View File

@ -1,67 +0,0 @@
# This file was automatically generated by 'stack init'
#
# Some commonly used options have been documented as comments in this file.
# For advanced use and comprehensive documentation of the format, please see:
# https://docs.haskellstack.org/en/stable/yaml_configuration/
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
# A snapshot resolver dictates the compiler version and the set of packages
# to be used for project dependencies. For example:
#
# resolver: lts-3.5
# resolver: nightly-2015-09-21
# resolver: ghc-7.10.2
#
# The location of a snapshot can be provided as a file or url. Stack assumes
# a snapshot provided as a file might change, whereas a url resource does not.
#
# resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml
resolver:
filepath: /home/andreas/stackage/stackage/snapshot.yaml
# User packages to be built.
# Various formats can be used as shown in the example below.
#
# packages:
# - some-directory
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
# subdirs:
# - auto-update
# - wai
packages:
- .
# Dependency packages to be pulled from upstream that are not in the resolver.
# These entries can reference officially published versions as well as
# forks / in-progress versions pinned to a git hash. For example:
#
# extra-deps:
# - acme-missiles-0.3
# - git: https://github.com/commercialhaskell/stack.git
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#
# extra-deps: []
# Override default flag values for local packages and extra-deps
# flags: {}
# Extra package databases containing global packages
# extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
#
# Require a specific version of Stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: ">=2.11"
#
# Override the architecture used by Stack, especially useful on Windows
# arch: i386
# arch: x86_64
#
# Extra directories used by Stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
#
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

View File

@ -1,42 +0,0 @@
{-# LANGUAGE TemplateHaskell, QuasiQuotes, DataKinds #-}
module Main (main) where
import qualified Data.ByteString as BS
import Data.Int (Int16, Int32, Int64)
import qualified Data.Time as Time
import qualified Criterion.Main as C
import System.Exit (exitSuccess, exitFailure)
import Database.PostgreSQL.Typed
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Query
import Connect
useTPGDatabase db
selectTypes :: PGConnection -> IO [(String, OID, Int16, Bool, Maybe BS.ByteString)]
selectTypes c = pgQuery c [pgSQL|SELECT typname, typnamespace, typlen, typbyval, typdefault FROM pg_catalog.pg_type|]
selectTypesLazy :: PGConnection -> IO [(String, OID, Int16, Bool, Maybe BS.ByteString)]
selectTypesLazy c = pgLazyQuery c [pgSQL|$SELECT typname, typnamespace, typlen, typbyval, typdefault FROM pg_catalog.pg_type|] 1
selectParams :: PGConnection -> IO [(Maybe String, Maybe Int64, Maybe Double, Maybe BS.ByteString, Maybe Bool)]
selectParams c = pgQuery c [pgSQL|$SELECT ${"hello"}::text, ${123::Int64}::bigint, ${123.4::Double}::float, ${BS.pack [120..220]}::bytea, ${Nothing::Maybe Bool}::boolean|]
selectValues :: PGConnection -> IO [(Int32, Time.UTCTime)]
selectValues c = pgQuery c [pgSQL|!SELECT generate_series, now() FROM generate_series(8,256)|]
selectValuesLazy :: PGConnection -> IO [(Int32, Time.UTCTime)]
selectValuesLazy c = pgLazyQuery c [pgSQL|$!SELECT generate_series, now() FROM generate_series(8,256)|] 5
main :: IO ()
main = do
c <- pgConnect db
C.defaultMain
[ C.bench "types" $ C.nfIO $ selectTypes c
, C.bench "types lazy" $ C.nfIO $ selectTypesLazy c
, C.bench "params" $ C.nfIO $ selectParams c
, C.bench "values" $ C.nfIO $ selectValues c
, C.bench "values lazy" $ C.nfIO $ selectValuesLazy c
]

View File

@ -1,58 +0,0 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Connect where
#ifdef VERSION_tls
import Control.Exception (throwIO)
#endif
import qualified Data.ByteString.Char8 as BSC
import Data.Maybe (fromMaybe, isJust)
import Database.PostgreSQL.Typed (PGDatabase (..),
defaultPGDatabase)
#ifdef VERSION_tls
import Database.PostgreSQL.Typed.Protocol (PGTlsMode (..),
PGTlsValidateMode (..),
pgTlsValidate)
#endif
import Network.Socket (SockAddr (SockAddrUnix))
import System.Environment (lookupEnv)
import System.IO.Unsafe (unsafePerformIO)
db :: PGDatabase
db = unsafePerformIO $ do
mPort <- lookupEnv "PGPORT"
pgDBAddr <- case mPort of
Nothing ->
#ifndef mingw32_HOST_OS
Right . SockAddrUnix . fromMaybe "/tmp/.s.PGSQL.5432" <$> lookupEnv "PGSOCK"
#else
pure $ pgDBAddr defaultPGDatabase
#endif
Just port -> pure $ Left ("localhost", port)
#ifdef VERSION_tls
pgDBTLS <- do
enabled <- isJust <$> lookupEnv "PGTLS"
validateFull <- isJust <$> lookupEnv "PGTLS_VALIDATEFULL"
rootcert <- fmap BSC.pack <$> lookupEnv "PGTLS_ROOTCERT"
case (enabled,validateFull,rootcert) of
(False,_,_) -> pure TlsDisabled
(True,False,Nothing) -> pure TlsNoValidate
(True,True,Just cert) -> either (throwIO . userError) pure $ pgTlsValidate TlsValidateFull cert
(True,True,Nothing) -> throwIO $ userError "Need to pass the root certificate on the PGTLS_ROOTCERT environment variable to validate FQHN"
(True,False,Just cert) -> either (throwIO . userError) pure $ pgTlsValidate TlsValidateCA cert
#endif
pgDBPass <- maybe BSC.empty BSC.pack <$> lookupEnv "PG_PASS"
pgDBDebug <- isJust <$> lookupEnv "PG_DEBUG"
pure $ defaultPGDatabase
{ pgDBName = "templatepg"
, pgDBUser = "templatepg"
, pgDBParams = [("TimeZone", "UTC")]
, pgDBDebug
#ifdef VERSION_tls
, pgDBTLS
#endif
, pgDBAddr
, pgDBPass
}
{-# NOINLINE db #-}

View File

@ -1,220 +0,0 @@
{-# LANGUAGE OverloadedStrings, FlexibleInstances, MultiParamTypeClasses, DataKinds, DeriveDataTypeable, TypeFamilies, PatternGuards, StandaloneDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans -Wincomplete-uni-patterns #-}
--{-# OPTIONS_GHC -ddump-splices #-}
module Main (main) where
import Control.Exception (try)
import Control.Monad (unless)
import Data.Char (isDigit, toUpper)
import Data.Int (Int32)
import qualified Data.Time as Time
import Data.Word (Word8)
import System.Exit (exitSuccess, exitFailure)
import qualified Test.QuickCheck as Q
import Test.QuickCheck.Test (isSuccess)
import Database.PostgreSQL.Typed
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.Query (PGSimpleQuery, getQueryString)
import Database.PostgreSQL.Typed.Array ()
import qualified Database.PostgreSQL.Typed.Range as Range
import Database.PostgreSQL.Typed.Enum
import Database.PostgreSQL.Typed.Inet
import Database.PostgreSQL.Typed.SQLToken
import Database.PostgreSQL.Typed.Relation
import qualified Database.PostgreSQL.Typed.ErrCodes as PGErr
import Connect
assert :: Bool -> IO ()
assert False = exitFailure
assert True = return ()
useTPGDatabase db
-- This runs at compile-time:
[pgSQL|!CREATE TYPE myenum AS enum ('abc', 'DEF', 'XX_ye')|]
[pgSQL|!DROP TABLE myfoo|]
[pgSQL|!CREATE TABLE myfoo (id serial primary key, adé myenum, bar float)|]
dataPGEnum "MyEnum" "myenum" ("MyEnum_" ++)
deriving instance Show MyEnum
dataPGRelation "MyFoo" "myfoo" (\(c:s) -> "foo" ++ toUpper c : s)
instance Q.Arbitrary MyEnum where
arbitrary = Q.arbitraryBoundedEnum
instance Q.Arbitrary MyFoo where
arbitrary = MyFoo 0 <$> Q.arbitrary <*> Q.arbitrary
instance Eq MyFoo where
MyFoo _ a b == MyFoo _ a' b' = a == a' && b == b'
deriving instance Show MyFoo
instance Q.Arbitrary Time.Day where
arbitrary = Time.ModifiedJulianDay <$> Q.arbitrary
instance Q.Arbitrary Time.DiffTime where
arbitrary = Time.picosecondsToDiffTime . (1000000 *) <$> Q.arbitrary
instance Q.Arbitrary Time.UTCTime where
arbitrary = Time.UTCTime <$> Q.arbitrary <*> ((Time.picosecondsToDiffTime . (1000000 *)) <$> Q.choose (0,86399999999))
instance Q.Arbitrary Time.LocalTime where
arbitrary = Time.utcToLocalTime Time.utc <$> Q.arbitrary
instance Q.Arbitrary a => Q.Arbitrary (Range.Bound a) where
arbitrary = do
u <- Q.arbitrary
if u
then return $ Range.Unbounded
else Range.Bounded <$> Q.arbitrary <*> Q.arbitrary
instance (Ord a, Q.Arbitrary a) => Q.Arbitrary (Range.Range a) where
arbitrary = Range.range <$> Q.arbitrary <*> Q.arbitrary
instance Q.Arbitrary PGInet where
arbitrary = do
v6 <- Q.arbitrary
if v6
then PGInet6 <$> Q.arbitrary <*> ((`mod` 129) <$> Q.arbitrary)
else PGInet <$> Q.arbitrary <*> ((`mod` 33) <$> Q.arbitrary)
instance Q.Arbitrary SQLToken where
arbitrary = Q.oneof
[ SQLToken <$> Q.arbitrary
, SQLParam <$> Q.arbitrary
, SQLExpr <$> Q.arbitrary
, SQLQMark <$> Q.arbitrary
]
newtype SafeString = SafeString Q.UnicodeString
deriving (Eq, Ord, Show)
instance Q.Arbitrary SafeString where
arbitrary = SafeString <$> Q.suchThat Q.arbitrary (notElem '\0' . Q.getUnicodeString)
getSafeString :: SafeString -> String
getSafeString (SafeString s) = Q.getUnicodeString s
simple :: PGConnection -> OID -> IO [String]
simple c t = pgQuery c [pgSQL|SELECT typname FROM pg_catalog.pg_type WHERE oid = ${t} AND oid = $1|]
simpleApply :: PGConnection -> OID -> IO [Maybe String]
simpleApply c = pgQuery c . [pgSQL|?SELECT typname FROM pg_catalog.pg_type WHERE oid = $1|]
prepared :: PGConnection -> OID -> String -> IO [Maybe String]
prepared c t = pgQuery c . [pgSQL|?$SELECT typname FROM pg_catalog.pg_type WHERE oid = ${t} AND typname = $2|]
preparedApply :: PGConnection -> Int32 -> IO [String]
preparedApply c = pgQuery c . [pgSQL|$(integer)SELECT typname FROM pg_catalog.pg_type WHERE oid = $1|]
selectProp :: PGConnection -> Bool -> Word8 -> Int32 -> Float -> Time.LocalTime -> Time.UTCTime -> Time.Day -> Time.DiffTime -> SafeString -> [Maybe SafeString] -> Range.Range Int32 -> MyEnum -> PGInet -> Q.Property
selectProp pgc b c i f t z d p s l r e a = Q.ioProperty $ do
[(Just b', Just c', Just i', Just f', Just s', Just d', Just t', Just z', Just p', Just l', Just r', Just e', Just a')] <- pgQuery pgc
[pgSQL|$SELECT ${b}::bool, ${c}::"char", ${Just i}::int, ${f}::float4, ${getSafeString s}::varchar, ${Just d}::date, ${t}::timestamp, ${z}::timestamptz, ${p}::interval, ${map (fmap getSafeString) l}::text[], ${r}::int4range, ${e}::myenum, ${a}::inet|]
return $ Q.conjoin
[ i Q.=== i'
, c Q.=== c'
, b Q.=== b'
, getSafeString s Q.=== s'
, f Q.=== f'
, d Q.=== d'
, t Q.=== t'
, z Q.=== z'
, p Q.=== p'
, map (fmap getSafeString) l Q.=== l'
, Range.normalize' r Q.=== r'
, e Q.=== e'
, a Q.=== a'
]
selectProp' :: PGConnection -> Bool -> Int32 -> Float -> Time.LocalTime -> Time.UTCTime -> Time.Day -> Time.DiffTime -> SafeString -> [Maybe SafeString] -> Range.Range Int32 -> MyEnum -> PGInet -> Q.Property
selectProp' pgc b i f t z d p s l r e a = Q.ioProperty $ do
[(Just b', Just i', Just f', Just s', Just d', Just t', Just z', Just p', Just l', Just r', Just e', Just a')] <- pgQuery pgc
[pgSQL|SELECT ${b}::bool, ${Just i}::int, ${f}::float4, ${getSafeString s}::varchar, ${Just d}::date, ${t}::timestamp, ${z}::timestamptz, ${p}::interval, ${map (fmap getSafeString) l}::text[], ${r}::int4range, ${e}::myenum, ${a}::inet|]
return $ Q.conjoin
[ i Q.=== i'
, b Q.=== b'
, getSafeString s Q.=== s'
, f Q.=== f'
, d Q.=== d'
, t Q.=== t'
, z Q.=== z'
, p Q.=== p'
, map (fmap getSafeString) l Q.=== l'
, Range.normalize' r Q.=== r'
, e Q.=== e'
, a Q.=== a'
]
selectFoo :: PGConnection -> [MyFoo] -> Q.Property
selectFoo pgc l = Q.ioProperty $ do
_ <- pgExecute pgc [pgSQL|TRUNCATE myfoo|]
let loop [] = return ()
loop [x] = do
1 <- pgExecute pgc [pgSQL|INSERT INTO myfoo (bar, adé) VALUES (${fooBar x}, ${fooAdé x})|]
return ()
loop (x:y:r) = do
1 <- pgExecute pgc [pgSQL|INSERT INTO myfoo (adé, bar) VALUES (${fooAdé x}, ${fooBar x})|]
1 <- pgExecute pgc [pgSQL|$INSERT INTO myfoo (adé, bar) VALUES (${fooAdé y}, ${fooBar y})|]
loop r
loop l
r <- pgQuery pgc [pgSQL|SELECT * FROM myfoo ORDER BY id|]
return $ l Q.=== map (\(i,a,b) -> MyFoo i a b) r
tokenProp :: String -> Q.Property
tokenProp s =
not (has0 s) Q.==> s Q.=== show (sqlTokens s) where
has0 ('$':'0':c:_) | isDigit c = True
has0 (_:r) = has0 r
has0 [] = False
main :: IO ()
main = do
c <- pgConnect db
r <- Q.quickCheckResult
$ selectProp c
Q..&&. selectProp' c
Q..&&. selectFoo c
Q..&&. tokenProp
Q..&&. [pgSQL|#abc ${3.14::Float} def $f$ $$ ${1} $f$${2::Int32}|] Q.=== "abc 3.14::real def $f$ $$ ${1} $f$2::integer"
Q..&&. getQueryString (pgTypeEnv c) ([pgSQL|SELECT ${"ab'cd"::String}::text, ${3.14::Float}::float4|] :: PGSimpleQuery (Maybe String, Maybe Float)) Q.=== "SELECT 'ab''cd'::text, 3.14::float4"
Q..&&. pgEnumValues Q.=== [(MyEnum_abc, "abc"), (MyEnum_DEF, "DEF"), (MyEnum_XX_ye, "XX_ye")]
Q..&&. Q.conjoin (map (\(s, t) -> sqlTokens s Q.=== t)
[ ("",
[])
, ( "SELECT a from b WHERE c = ?"
, ["SELECT a from b WHERE c = ", SQLQMark False])
, ( "INSERT INTO foo VALUES (?,?)"
, ["INSERT INTO foo VALUES (", SQLQMark False, ",", SQLQMark False, ")"])
, ( "INSERT INTO foo VALUES ('?','''?')"
, ["INSERT INTO foo VALUES ('?','''?')"])
, ( "-- really?\n-- yes'?\nINSERT INTO ? VALUES ('', ?, \"?asd\", e'?\\'?', '?''?', /* foo? */ /* foo /* bar */ ? */ ?)"
, ["-- really?\n-- yes'?\nINSERT INTO ", SQLQMark False, " VALUES ('', ", SQLQMark False, ", \"?asd\", e'?\\'?', '?''?', /* foo? */ /* foo /* bar */ ? */ ", SQLQMark False, ")"])
, ( "some ${things? {don't}} change$1 $1\\?"
, ["some ", SQLExpr "things? {don't}", " change$1 ", SQLParam 1, SQLQMark True])
])
assert $ isSuccess r
["box"] <- simple c 603
[Just "box"] <- simpleApply c 603
[Just "box"] <- prepared c 603 "box"
["box"] <- preparedApply c 603
[Just "line"] <- prepared c 628 "line"
["line"] <- preparedApply c 628
pgSimpleQueries_ c "LISTEN channame; NOTIFY channame, 'oh hello'; SELECT pg_notify('channame', 'there')"
PGNotification _ "channame" "oh hello" <- pgGetNotification c
(-1, []) <- pgSimpleQuery c "NOTIFY channame"
pgTransaction c $ do
(1, [[PGTextValue "1"]]) <- pgSimpleQuery c "SELECT 1"
(-1, []) <- pgSimpleQuery c "NOTIFY channame, 'nope'"
Left e1 <- try $ pgSimpleQuery c "SYNTAX_ERROR"
assert $ pgErrorCode e1 == PGErr.syntax_error
Left e2 <- try $ pgSimpleQuery c "SELECT 1"
assert $ pgErrorCode e2 == PGErr.in_failed_sql_transaction
unless (pgSupportsTls c) $ do
[PGNotification _ "channame" "there", PGNotification _ "channame" ""] <- pgGetNotifications c
[] <- pgGetNotifications c
pure ()
pgDisconnect c
exitSuccess

View File

@ -1,27 +0,0 @@
module SpecificDB where
import Database.HDBC
import Database.PostgreSQL.Typed.HDBC
import Connect
connectDB :: IO Connection
connectDB =
handleSqlError (do dbh <- connect db
_ <- run dbh "SET client_min_messages=WARNING" []
return dbh)
dateTimeTypeOfSqlValue :: SqlValue -> String
dateTimeTypeOfSqlValue (SqlLocalDate _) = "date"
dateTimeTypeOfSqlValue (SqlLocalTimeOfDay _) = "time without time zone"
dateTimeTypeOfSqlValue (SqlZonedLocalTimeOfDay _ _) = "time with time zone"
dateTimeTypeOfSqlValue (SqlLocalTime _) = "timestamp without time zone"
dateTimeTypeOfSqlValue (SqlZonedTime _) = "timestamp with time zone"
dateTimeTypeOfSqlValue (SqlUTCTime _) = "timestamp with time zone"
dateTimeTypeOfSqlValue (SqlDiffTime _) = "interval"
dateTimeTypeOfSqlValue (SqlPOSIXTime _) = "numeric"
dateTimeTypeOfSqlValue (SqlEpochTime _) = "integer"
dateTimeTypeOfSqlValue (SqlTimeDiff _) = "interval"
dateTimeTypeOfSqlValue _ = "text"
supportsFracTime :: Bool
supportsFracTime = True

View File

@ -1,181 +0,0 @@
module TestMisc(tests, setup) where
import Test.HUnit
import Database.HDBC
import TestUtils
import System.IO
import Control.Exception
import Data.Char
import Control.Monad
import qualified Data.Map as Map
rowdata =
[[SqlInt32 0, toSql "Testing", SqlNull],
[SqlInt32 1, toSql "Foo", SqlInt32 5],
[SqlInt32 2, toSql "Bar", SqlInt32 9]]
colnames = ["testid", "teststring", "testint"]
alrows :: [[(String, SqlValue)]]
alrows = map (zip colnames) rowdata
setup f = dbTestCase $ \dbh ->
do run dbh "CREATE TABLE hdbctest2 (testid INTEGER PRIMARY KEY NOT NULL, teststring TEXT, testint INTEGER)" []
sth <- prepare dbh "INSERT INTO hdbctest2 VALUES (?, ?, ?)"
executeMany sth rowdata
finish sth
commit dbh
finally (f dbh)
(do run dbh "DROP TABLE hdbctest2" []
commit dbh
)
cloneTest dbh a =
do dbh2 <- clone dbh
finally (handleSqlError (a dbh2))
(handleSqlError (disconnect dbh2))
testgetColumnNames = setup $ \dbh ->
do sth <- prepare dbh "SELECT * from hdbctest2"
execute sth []
cols <- getColumnNames sth
finish sth
["testid", "teststring", "testint"] @=? map (map toLower) cols
testdescribeResult = setup $ \dbh -> when (not ((hdbcDriverName dbh) `elem`
["sqlite3"])) $
do sth <- prepare dbh "SELECT * from hdbctest2"
execute sth []
cols <- describeResult sth
["testid", "teststring", "testint"] @=? map (map toLower . fst) cols
let coldata = map snd cols
assertBool "r0 type" (colType (coldata !! 0) `elem`
[SqlBigIntT, SqlIntegerT])
assertBool "r1 type" (colType (coldata !! 1) `elem`
[SqlVarCharT, SqlLongVarCharT])
assertBool "r2 type" (colType (coldata !! 2) `elem`
[SqlBigIntT, SqlIntegerT])
finish sth
testdescribeTable = setup $ \dbh -> when (not ((hdbcDriverName dbh) `elem`
["sqlite3"])) $
do cols <- describeTable dbh "hdbctest2"
["testid", "teststring", "testint"] @=? map (map toLower . fst) cols
let coldata = map snd cols
assertBool "r0 type" (colType (coldata !! 0) `elem`
[SqlBigIntT, SqlIntegerT])
assertEqual "r0 nullable" (Just False) (colNullable (coldata !! 0))
assertBool "r1 type" (colType (coldata !! 1) `elem`
[SqlVarCharT, SqlLongVarCharT])
assertEqual "r1 nullable" (Just True) (colNullable (coldata !! 1))
assertBool "r2 type" (colType (coldata !! 2) `elem`
[SqlBigIntT, SqlIntegerT])
assertEqual "r2 nullable" (Just True) (colNullable (coldata !! 2))
testquickQuery = setup $ \dbh ->
do results <- quickQuery dbh "SELECT * from hdbctest2 ORDER BY testid" []
rowdata @=? results
testfetchRowAL = setup $ \dbh ->
do sth <- prepare dbh "SELECT * from hdbctest2 ORDER BY testid"
execute sth []
fetchRowAL sth >>= (Just (head alrows) @=?)
fetchRowAL sth >>= (Just (alrows !! 1) @=?)
fetchRowAL sth >>= (Just (alrows !! 2) @=?)
fetchRowAL sth >>= (Nothing @=?)
finish sth
testfetchRowMap = setup $ \dbh ->
do sth <- prepare dbh "SELECT * from hdbctest2 ORDER BY testid"
execute sth []
fetchRowMap sth >>= (Just (Map.fromList $ head alrows) @=?)
fetchRowMap sth >>= (Just (Map.fromList $ alrows !! 1) @=?)
fetchRowMap sth >>= (Just (Map.fromList $ alrows !! 2) @=?)
fetchRowMap sth >>= (Nothing @=?)
finish sth
testfetchAllRowsAL = setup $ \dbh ->
do sth <- prepare dbh "SELECT * from hdbctest2 ORDER BY testid"
execute sth []
fetchAllRowsAL sth >>= (alrows @=?)
testfetchAllRowsMap = setup $ \dbh ->
do sth <- prepare dbh "SELECT * from hdbctest2 ORDER BY testid"
execute sth []
fetchAllRowsMap sth >>= (map (Map.fromList) alrows @=?)
testexception = setup $ \dbh ->
catchSql (do sth <- prepare dbh "SELECT invalidcol FROM hdbctest2"
execute sth []
assertFailure "No exception was raised"
)
(\e -> commit dbh)
testrowcount = setup $ \dbh ->
do r <- run dbh "UPDATE hdbctest2 SET testint = 25 WHERE testid = 20" []
assertEqual "UPDATE with no change" 0 r
r <- run dbh "UPDATE hdbctest2 SET testint = 26 WHERE testid = 0" []
assertEqual "UPDATE with 1 change" 1 r
r <- run dbh "UPDATE hdbctest2 SET testint = 27 WHERE testid <> 0" []
assertEqual "UPDATE with 2 changes" 2 r
commit dbh
res <- quickQuery dbh "SELECT * from hdbctest2 ORDER BY testid" []
assertEqual "final results"
[[SqlInt32 0, toSql "Testing", SqlInt32 26],
[SqlInt32 1, toSql "Foo", SqlInt32 27],
[SqlInt32 2, toSql "Bar", SqlInt32 27]] res
{- Since we might be running against a live DB, we can't look at a specific
list here (though a SpecificDB test case may be able to). We can ensure
that our test table is, or is not, present, as appropriate. -}
testgetTables1 = setup $ \dbh ->
do r <- getTables dbh
True @=? "hdbctest2" `elem` r
testgetTables2 = dbTestCase $ \dbh ->
do r <- getTables dbh
False @=? "hdbctest2" `elem` r
testclone = setup $ \dbho -> cloneTest dbho $ \dbh ->
do results <- quickQuery dbh "SELECT * from hdbctest2 ORDER BY testid" []
rowdata @=? results
testnulls = setup $ \dbh ->
do let dn = hdbcDriverName dbh
when (not (dn `elem` ["postgresql", "odbc", "postgresql-typed"])) (
do sth <- prepare dbh "INSERT INTO hdbctest2 VALUES (?, ?, ?)"
executeMany sth rows
finish sth
res <- quickQuery dbh "SELECT * from hdbctest2 WHERE testid > 99 ORDER BY testid" []
seq (length res) rows @=? res
)
where rows = [[SqlInt32 100, SqlString "foo\NULbar", SqlNull],
[SqlInt32 101, SqlString "bar\NUL", SqlNull],
[SqlInt32 102, SqlString "\NUL", SqlNull],
[SqlInt32 103, SqlString "\xFF", SqlNull],
[SqlInt32 104, SqlString "regular", SqlNull]]
testunicode = setup $ \dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest2 VALUES (?, ?, ?)"
executeMany sth rows
finish sth
res <- quickQuery dbh "SELECT * from hdbctest2 WHERE testid > 99 ORDER BY testid" []
seq (length res) rows @=? res
where rows = [[SqlInt32 100, SqlString "foo\x263a", SqlNull],
[SqlInt32 101, SqlString "bar\x00A3", SqlNull],
[SqlInt32 102, SqlString (take 263 (repeat 'a')), SqlNull]]
tests = TestList [TestLabel "getColumnNames" testgetColumnNames,
TestLabel "describeResult" testdescribeResult,
TestLabel "describeTable" testdescribeTable,
TestLabel "quickQuery" testquickQuery,
TestLabel "fetchRowAL" testfetchRowAL,
TestLabel "fetchRowMap" testfetchRowMap,
TestLabel "fetchAllRowsAL" testfetchAllRowsAL,
TestLabel "fetchAllRowsMap" testfetchAllRowsMap,
TestLabel "sql exception" testexception,
TestLabel "clone" testclone,
TestLabel "update rowcount" testrowcount,
TestLabel "get tables1" testgetTables1,
TestLabel "get tables2" testgetTables2,
TestLabel "nulls" testnulls,
TestLabel "unicode" testunicode]

View File

@ -1,170 +0,0 @@
module TestSbasics(tests) where
import Test.HUnit
import Data.List
import Database.HDBC
import TestUtils
import Control.Exception
openClosedb = sqlTestCase $
do dbh <- connectDB
disconnect dbh
multiFinish = dbTestCase (\dbh ->
do sth <- prepare dbh "SELECT 1 + 1"
sExecute sth []
finish sth
finish sth
finish sth
)
runRawTest = dbTestCase (\dbh ->
do runRaw dbh "CREATE TABLE valid1 (a int); CREATE TABLE valid2 (a int)"
tables <- getTables dbh
assertBool "valid1 table not created!" ("valid1" `elem` tables)
assertBool "valid2 table not created!" ("valid2" `elem` tables)
)
runRawErrorTest = dbTestCase (\dbh ->
let expected = "ERROR: syntax error at or near \"INVALID\""
in do err <- (runRaw dbh "CREATE TABLE valid1 (a int); INVALID" >> return "No error") `catchSql`
(return . seErrorMsg)
assertBool "Error message inappropriate" (expected `isPrefixOf` err)
rollback dbh
tables <- getTables dbh
assertBool "valid1 table created!" (not $ "valid1" `elem` tables)
)
basicQueries = dbTestCase (\dbh ->
do sth <- prepare dbh "SELECT 1 + 1"
sExecute sth []
sFetchRow sth >>= (assertEqual "row 1" (Just [Just "2"]))
sFetchRow sth >>= (assertEqual "last row" Nothing)
)
createTable = dbTestCase (\dbh ->
do sRun dbh "CREATE TABLE hdbctest1 (testname VARCHAR(20), testid INTEGER, testint INTEGER, testtext TEXT)" []
commit dbh
)
dropTable = dbTestCase (\dbh ->
do sRun dbh "DROP TABLE hdbctest1" []
commit dbh
)
runReplace = dbTestCase (\dbh ->
do sRun dbh "INSERT INTO hdbctest1 VALUES (?, ?, ?, ?)" r1
sRun dbh "INSERT INTO hdbctest1 VALUES (?, ?, 2, ?)" r2
commit dbh
sth <- prepare dbh "SELECT * FROM hdbctest1 WHERE testname = 'runReplace' ORDER BY testid"
sExecute sth []
sFetchRow sth >>= (assertEqual "r1" (Just r1))
sFetchRow sth >>= (assertEqual "r2" (Just [Just "runReplace", Just "2",
Just "2", Nothing]))
sFetchRow sth >>= (assertEqual "lastrow" Nothing)
)
where r1 = [Just "runReplace", Just "1", Just "1234", Just "testdata"]
r2 = [Just "runReplace", Just "2", Nothing]
executeReplace = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('executeReplace',?,?,?)"
sExecute sth [Just "1", Just "1234", Just "Foo"]
sExecute sth [Just "2", Nothing, Just "Bar"]
commit dbh
sth <- prepare dbh "SELECT * FROM hdbctest1 WHERE testname = ? ORDER BY testid"
sExecute sth [Just "executeReplace"]
sFetchRow sth >>= (assertEqual "r1"
(Just $ map Just ["executeReplace", "1", "1234",
"Foo"]))
sFetchRow sth >>= (assertEqual "r2"
(Just [Just "executeReplace", Just "2", Nothing,
Just "Bar"]))
sFetchRow sth >>= (assertEqual "lastrow" Nothing)
)
testExecuteMany = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('multi',?,?,?)"
sExecuteMany sth rows
commit dbh
sth <- prepare dbh "SELECT testid, testint, testtext FROM hdbctest1 WHERE testname = 'multi'"
sExecute sth []
mapM_ (\r -> sFetchRow sth >>= (assertEqual "" (Just r))) rows
sFetchRow sth >>= (assertEqual "lastrow" Nothing)
)
where rows = [map Just ["1", "1234", "foo"],
map Just ["2", "1341", "bar"],
[Just "3", Nothing, Nothing]]
testsFetchAllRows = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('sFetchAllRows', ?, NULL, NULL)"
sExecuteMany sth rows
commit dbh
sth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'sFetchAllRows' ORDER BY testid"
sExecute sth []
results <- sFetchAllRows sth
assertEqual "" rows results
)
where rows = map (\x -> [Just . show $ x]) [1..9]
basicTransactions = dbTestCase (\dbh ->
do assertBool "Connected database does not support transactions; skipping transaction test" (dbTransactionSupport dbh)
sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('basicTransactions', ?, NULL, NULL)"
sExecute sth [Just "0"]
commit dbh
qrysth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'basicTransactions' ORDER BY testid"
sExecute qrysth []
sFetchAllRows qrysth >>= (assertEqual "initial commit" [[Just "0"]])
-- Now try a rollback
sExecuteMany sth rows
rollback dbh
sExecute qrysth []
sFetchAllRows qrysth >>= (assertEqual "rollback" [[Just "0"]])
-- Now try another commit
sExecuteMany sth rows
commit dbh
sExecute qrysth []
sFetchAllRows qrysth >>= (assertEqual "final commit" ([Just "0"]:rows))
)
where rows = map (\x -> [Just . show $ x]) [1..9]
testWithTransaction = dbTestCase (\dbh ->
do assertBool "Connected database does not support transactions; skipping transaction test" (dbTransactionSupport dbh)
sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('withTransaction', ?, NULL, NULL)"
sExecute sth [Just "0"]
commit dbh
qrysth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'withTransaction' ORDER BY testid"
sExecute qrysth []
sFetchAllRows qrysth >>= (assertEqual "initial commit" [[Just "0"]])
-- Let's try a rollback.
catch (withTransaction dbh (\_ -> do sExecuteMany sth rows
fail "Foo"))
(\SomeException{} -> return ())
sExecute qrysth []
sFetchAllRows qrysth >>= (assertEqual "rollback" [[Just "0"]])
-- And now a commit.
withTransaction dbh (\_ -> sExecuteMany sth rows)
sExecute qrysth []
sFetchAllRows qrysth >>= (assertEqual "final commit" ([Just "0"]:rows))
)
where rows = map (\x -> [Just . show $ x]) [1..9]
tests = TestList
[
TestLabel "openClosedb" openClosedb,
TestLabel "multiFinish" multiFinish,
TestLabel "runRawTest" runRawTest,
TestLabel "runRawErrorTest" runRawErrorTest,
TestLabel "basicQueries" basicQueries,
TestLabel "createTable" createTable,
TestLabel "runReplace" runReplace,
TestLabel "executeReplace" executeReplace,
TestLabel "executeMany" testExecuteMany,
TestLabel "sFetchAllRows" testsFetchAllRows,
TestLabel "basicTransactions" basicTransactions,
TestLabel "withTransaction" testWithTransaction,
TestLabel "dropTable" dropTable
]

View File

@ -1,97 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
module TestTime(tests) where
import Test.HUnit
import Database.HDBC
import TestUtils
import Control.Exception
import Data.Time (UTCTime, Day, NominalDiffTime)
import Data.Time.LocalTime
import Data.Time.Clock.POSIX
import Data.Maybe
import Data.Convertible
import SpecificDB
import Data.Time (parseTimeM, defaultTimeLocale, TimeLocale)
import Database.HDBC.Locale (iso8601DateFormat)
instance Eq ZonedTime where
a == b = zonedTimeToUTC a == zonedTimeToUTC b
testZonedTime :: ZonedTime
testZonedTime = fromJust $ parseTime' defaultTimeLocale (iso8601DateFormat (Just "%T %z"))
"1989-08-01 15:33:01 -0500"
testZonedTimeFrac :: ZonedTime
testZonedTimeFrac = fromJust $ parseTime' defaultTimeLocale (iso8601DateFormat (Just "%T%Q %z"))
"1989-08-01 15:33:01.536 -0500"
testDTType :: (Convertible SqlValue a, Show b, Eq b) =>
a
-> (a -> SqlValue)
-> (a -> b)
-> Test
testDTType inputdata convToSqlValue toComparable = dbTestCase $ \dbh ->
do _ <- run dbh ("CREATE TABLE hdbctesttime (testid INTEGER PRIMARY KEY NOT NULL, testvalue " ++ dateTimeTypeOfSqlValue value ++ ")") []
commit dbh
finally (testDT dbh) (do commit dbh
_ <- run dbh "DROP TABLE hdbctesttime" []
commit dbh
)
where testDT dbh =
do _ <- run dbh "INSERT INTO hdbctesttime (testid, testvalue) VALUES (?, ?)"
[iToSql 5, value]
commit dbh
r <- quickQuery' dbh "SELECT testid, testvalue FROM hdbctesttime" []
case r of
~[[testidsv, testvaluesv]] ->
do assertEqual "testid" (5::Int) (fromSql testidsv)
assertEqual "testvalue"
(toComparable inputdata)
(toComparable$ fromSql testvaluesv)
value = convToSqlValue inputdata
mkTest :: (Eq b, Show b, Convertible SqlValue a) => String -> a -> (a -> SqlValue) -> (a -> b) -> Test
mkTest label inputdata convfunc toComparable =
TestLabel label (testDTType inputdata convfunc toComparable)
tests :: Test
tests = TestList $
((TestLabel "Non-frac" $ testIt testZonedTime) :
if supportsFracTime then [TestLabel "Frac" $ testIt testZonedTimeFrac] else [])
testIt :: ZonedTime -> Test
testIt baseZonedTime =
TestList [ mkTest "Day" baseDay toSql id
, mkTest "TimeOfDay" baseTimeOfDay toSql id
, mkTest "ZonedTimeOfDay" baseZonedTimeOfDay toSql id
, mkTest "LocalTime" baseLocalTime toSql id
, mkTest "ZonedTime" baseZonedTime toSql id
, mkTest "UTCTime" baseUTCTime toSql id
, mkTest "DiffTime" baseDiffTime toSql id
, mkTest "POSIXTime" basePOSIXTime posixToSql id
]
where
baseDay :: Day
baseDay = localDay baseLocalTime
baseTimeOfDay :: TimeOfDay
baseTimeOfDay = localTimeOfDay baseLocalTime
baseZonedTimeOfDay :: (TimeOfDay, TimeZone)
baseZonedTimeOfDay = fromSql (SqlZonedTime baseZonedTime)
baseLocalTime :: LocalTime
baseLocalTime = zonedTimeToLocalTime baseZonedTime
baseUTCTime :: UTCTime
baseUTCTime = convert baseZonedTime
baseDiffTime :: NominalDiffTime
baseDiffTime = basePOSIXTime
basePOSIXTime :: POSIXTime
basePOSIXTime = convert baseZonedTime
parseTime' :: TimeLocale -> String -> String -> Maybe ZonedTime
parseTime' = parseTimeM True

View File

@ -1,29 +0,0 @@
module TestUtils(connectDB, sqlTestCase, dbTestCase, printDBInfo) where
import Database.HDBC
import Database.PostgreSQL.Typed.HDBC
import Test.HUnit
import Control.Exception
import SpecificDB(connectDB)
sqlTestCase :: IO () -> Test
sqlTestCase a =
TestCase (handleSqlError a)
dbTestCase :: (Connection -> IO ()) -> Test
dbTestCase a =
TestCase (do dbh <- connectDB
finally (handleSqlError (a dbh))
(handleSqlError (disconnect dbh))
)
printDBInfo :: IO ()
printDBInfo = handleSqlError $
do dbh <- connectDB
putStrLn "+-------------------------------------------------------------------------"
putStrLn $ "| Testing HDBC database module: " ++ hdbcDriverName dbh ++
", bound to client: " ++ hdbcClientVer dbh
putStrLn $ "| Proxied driver: " ++ proxiedClientName dbh ++
", bound to version: " ++ proxiedClientVer dbh
putStrLn $ "| Connected to server version: " ++ dbServerVer dbh
putStrLn "+-------------------------------------------------------------------------\n"
disconnect dbh

View File

@ -1,168 +0,0 @@
module Testbasics(tests) where
import Test.HUnit
import Database.HDBC
import TestUtils
import System.IO
import Control.Exception
openClosedb = sqlTestCase $
do dbh <- connectDB
disconnect dbh
multiFinish = dbTestCase (\dbh ->
do sth <- prepare dbh "SELECT 1 + 1"
r <- execute sth []
assertEqual "basic count" 0 r
finish sth
finish sth
finish sth
)
basicQueries = dbTestCase (\dbh ->
do sth <- prepare dbh "SELECT 1 + 1"
execute sth [] >>= (0 @=?)
r <- fetchAllRows sth
assertEqual "converted from" [["2"]] (map (map fromSql) r)
assertEqual "int32 compare" [[SqlInt32 2]] r
assertEqual "iToSql compare" [[iToSql 2]] r
assertEqual "num compare" [[toSql (2::Int)]] r
assertEqual "nToSql compare" [[nToSql (2::Int)]] r
assertEqual "string compare" [[SqlString "2"]] r
)
createTable = dbTestCase (\dbh ->
do run dbh "CREATE TABLE hdbctest1 (testname VARCHAR(20), testid INTEGER, testint INTEGER, testtext TEXT)" []
commit dbh
)
dropTable = dbTestCase (\dbh ->
do run dbh "DROP TABLE hdbctest1" []
commit dbh
)
runReplace = dbTestCase (\dbh ->
do r <- run dbh "INSERT INTO hdbctest1 VALUES (?, ?, ?, ?)" r1
assertEqual "insert retval" 1 r
run dbh "INSERT INTO hdbctest1 VALUES (?, ?, ?, ?)" r2
commit dbh
sth <- prepare dbh "SELECT * FROM hdbctest1 WHERE testname = 'runReplace' ORDER BY testid"
rv2 <- execute sth []
assertEqual "select retval" 0 rv2
r <- fetchAllRows sth
assertEqual "" [r1, r2] r
)
where r1 = [toSql "runReplace", iToSql 1, iToSql 1234, SqlString "testdata"]
r2 = [toSql "runReplace", iToSql 2, iToSql 2, SqlNull]
executeReplace = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('executeReplace',?,?,?)"
execute sth [iToSql 1, iToSql 1234, toSql "Foo"]
execute sth [SqlInt32 2, SqlNull, toSql "Bar"]
commit dbh
sth <- prepare dbh "SELECT * FROM hdbctest1 WHERE testname = ? ORDER BY testid"
execute sth [SqlString "executeReplace"]
r <- fetchAllRows sth
assertEqual "result"
[[toSql "executeReplace", iToSql 1, toSql "1234",
toSql "Foo"],
[toSql "executeReplace", iToSql 2, SqlNull,
toSql "Bar"]]
r
)
testExecuteMany = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('multi',?,?,?)"
executeMany sth rows
commit dbh
sth <- prepare dbh "SELECT testid, testint, testtext FROM hdbctest1 WHERE testname = 'multi'"
execute sth []
r <- fetchAllRows sth
assertEqual "" rows r
)
where rows = [map toSql ["1", "1234", "foo"],
map toSql ["2", "1341", "bar"],
[toSql "3", SqlNull, SqlNull]]
testFetchAllRows = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('sFetchAllRows', ?, NULL, NULL)"
executeMany sth rows
commit dbh
sth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'sFetchAllRows' ORDER BY testid"
execute sth []
results <- fetchAllRows sth
assertEqual "" rows results
)
where rows = map (\x -> [iToSql x]) [1..9]
testFetchAllRows' = dbTestCase (\dbh ->
do sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('sFetchAllRows2', ?, NULL, NULL)"
executeMany sth rows
commit dbh
sth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'sFetchAllRows2' ORDER BY testid"
execute sth []
results <- fetchAllRows' sth
assertEqual "" rows results
)
where rows = map (\x -> [iToSql x]) [1..9]
basicTransactions = dbTestCase (\dbh ->
do assertBool "Connected database does not support transactions; skipping transaction test" (dbTransactionSupport dbh)
sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('basicTransactions', ?, NULL, NULL)"
execute sth [iToSql 0]
commit dbh
qrysth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'basicTransactions' ORDER BY testid"
execute qrysth []
fetchAllRows qrysth >>= (assertEqual "initial commit" [[toSql "0"]])
-- Now try a rollback
executeMany sth rows
rollback dbh
execute qrysth []
fetchAllRows qrysth >>= (assertEqual "rollback" [[toSql "0"]])
-- Now try another commit
executeMany sth rows
commit dbh
execute qrysth []
fetchAllRows qrysth >>= (assertEqual "final commit" ([SqlString "0"]:rows))
)
where rows = map (\x -> [iToSql $ x]) [1..9]
testWithTransaction = dbTestCase (\dbh ->
do assertBool "Connected database does not support transactions; skipping transaction test" (dbTransactionSupport dbh)
sth <- prepare dbh "INSERT INTO hdbctest1 VALUES ('withTransaction', ?, NULL, NULL)"
execute sth [toSql "0"]
commit dbh
qrysth <- prepare dbh "SELECT testid FROM hdbctest1 WHERE testname = 'withTransaction' ORDER BY testid"
execute qrysth []
fetchAllRows qrysth >>= (assertEqual "initial commit" [[toSql "0"]])
-- Let's try a rollback.
catch (withTransaction dbh (\_ -> do executeMany sth rows
fail "Foo"))
(\SomeException{} -> return ())
execute qrysth []
fetchAllRows qrysth >>= (assertEqual "rollback" [[SqlString "0"]])
-- And now a commit.
withTransaction dbh (\_ -> executeMany sth rows)
execute qrysth []
fetchAllRows qrysth >>= (assertEqual "final commit" ([iToSql 0]:rows))
)
where rows = map (\x -> [iToSql x]) [1..9]
tests = TestList
[
TestLabel "openClosedb" openClosedb,
TestLabel "multiFinish" multiFinish,
TestLabel "basicQueries" basicQueries,
TestLabel "createTable" createTable,
TestLabel "runReplace" runReplace,
TestLabel "executeReplace" executeReplace,
TestLabel "executeMany" testExecuteMany,
TestLabel "fetchAllRows" testFetchAllRows,
TestLabel "fetchAllRows'" testFetchAllRows',
TestLabel "basicTransactions" basicTransactions,
TestLabel "withTransaction" testWithTransaction,
TestLabel "dropTable" dropTable
]

View File

@ -1,19 +0,0 @@
{- arch-tag: Tests main file
-}
module Tests(tests) where
import Test.HUnit
import qualified Testbasics
import qualified TestSbasics
import qualified TestMisc
import qualified TestTime
test1 = TestCase ("x" @=? "x")
tests = TestList
[ TestLabel "test1" test1
, TestLabel "String basics" TestSbasics.tests
, TestLabel "SqlValue basics" Testbasics.tests
, TestLabel "Misc tests" TestMisc.tests
, TestLabel "Time tests" TestTime.tests
]

View File

@ -1,16 +0,0 @@
{- arch-tag: Test runner
-}
module Main where
import Test.HUnit
import System.Exit
import Tests
import TestUtils
main = do
printDBInfo
r <- runTestTT tests
if errors r == 0 && failures r == 0
then exitSuccess
else exitFailure