packages feed

hpqtypes-extras 1.10.3.0 → 1.10.4.0

raw patch · 7 files changed

+400/−82 lines, 7 filesdep +QuickCheckdep +deepseqdep +extradep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, deepseq, extra, tasty-bench, time

Dependency ranges changed: base

API changes (from Hackage documentation)

+ Database.PostgreSQL.PQTypes.Deriving: SQLEnum :: a -> SQLEnum a
+ Database.PostgreSQL.PQTypes.Deriving: SQLEnumAsText :: a -> SQLEnumAsText a
+ Database.PostgreSQL.PQTypes.Deriving: class (Enum a, Bounded a) => EnumAsTextEncoding a
+ Database.PostgreSQL.PQTypes.Deriving: class (Enum a, Bounded a, Enum (EnumBase a), Ord (EnumBase a)) => EnumEncoding a where {
+ Database.PostgreSQL.PQTypes.Deriving: decodeEnum :: EnumEncoding a => EnumBase a -> Either [(EnumBase a, EnumBase a)] a
+ Database.PostgreSQL.PQTypes.Deriving: decodeEnumAsText :: EnumAsTextEncoding a => Text -> Either [Text] a
+ Database.PostgreSQL.PQTypes.Deriving: decodeEnumAsTextMap :: EnumAsTextEncoding a => Map Text a
+ Database.PostgreSQL.PQTypes.Deriving: decodeEnumMap :: EnumEncoding a => Map (EnumBase a) a
+ Database.PostgreSQL.PQTypes.Deriving: encodeEnum :: EnumEncoding a => a -> EnumBase a
+ Database.PostgreSQL.PQTypes.Deriving: encodeEnumAsText :: EnumAsTextEncoding a => a -> Text
+ Database.PostgreSQL.PQTypes.Deriving: instance (Database.PostgreSQL.PQTypes.Deriving.EnumEncoding a, Database.PostgreSQL.PQTypes.Format.PQFormat (Database.PostgreSQL.PQTypes.Deriving.EnumBase a), Database.PostgreSQL.PQTypes.FromSQL.FromSQL (Database.PostgreSQL.PQTypes.Deriving.EnumBase a), GHC.Show.Show (Database.PostgreSQL.PQTypes.Deriving.EnumBase a), Data.Typeable.Internal.Typeable (Database.PostgreSQL.PQTypes.Deriving.EnumBase a)) => Database.PostgreSQL.PQTypes.FromSQL.FromSQL (Database.PostgreSQL.PQTypes.Deriving.SQLEnum a)
+ Database.PostgreSQL.PQTypes.Deriving: instance (Database.PostgreSQL.PQTypes.Deriving.EnumEncoding a, Database.PostgreSQL.PQTypes.Format.PQFormat (Database.PostgreSQL.PQTypes.Deriving.EnumBase a), Database.PostgreSQL.PQTypes.ToSQL.ToSQL (Database.PostgreSQL.PQTypes.Deriving.EnumBase a)) => Database.PostgreSQL.PQTypes.ToSQL.ToSQL (Database.PostgreSQL.PQTypes.Deriving.SQLEnum a)
+ Database.PostgreSQL.PQTypes.Deriving: instance Database.PostgreSQL.PQTypes.Deriving.EnumAsTextEncoding a => Database.PostgreSQL.PQTypes.Format.PQFormat (Database.PostgreSQL.PQTypes.Deriving.SQLEnumAsText a)
+ Database.PostgreSQL.PQTypes.Deriving: instance Database.PostgreSQL.PQTypes.Deriving.EnumAsTextEncoding a => Database.PostgreSQL.PQTypes.FromSQL.FromSQL (Database.PostgreSQL.PQTypes.Deriving.SQLEnumAsText a)
+ Database.PostgreSQL.PQTypes.Deriving: instance Database.PostgreSQL.PQTypes.Deriving.EnumAsTextEncoding a => Database.PostgreSQL.PQTypes.ToSQL.ToSQL (Database.PostgreSQL.PQTypes.Deriving.SQLEnumAsText a)
+ Database.PostgreSQL.PQTypes.Deriving: instance Database.PostgreSQL.PQTypes.Format.PQFormat (Database.PostgreSQL.PQTypes.Deriving.EnumBase a) => Database.PostgreSQL.PQTypes.Format.PQFormat (Database.PostgreSQL.PQTypes.Deriving.SQLEnum a)
+ Database.PostgreSQL.PQTypes.Deriving: isInjective :: (Enum a, Bounded a, Eq a, Eq b) => (a -> b) -> Bool
+ Database.PostgreSQL.PQTypes.Deriving: newtype SQLEnum a
+ Database.PostgreSQL.PQTypes.Deriving: newtype SQLEnumAsText a
+ Database.PostgreSQL.PQTypes.Deriving: type family EnumBase a;
+ Database.PostgreSQL.PQTypes.Deriving: }

Files

CHANGELOG.md view
@@ -1,3 +1,7 @@+# hpqtypes-extra-1.10.4.0 (2021-02-04)+* Generate valid INSERT SELECT query with data modifying WITH clauses+* Add DerivingVia helpers for enums+ # hpqtypes-extra-1.10.3.0 (2020-11-16) * Include LIMIT clause in UNION subqueries of the select 
+ benchmark/Main.hs view
@@ -0,0 +1,79 @@+module Main where++import Control.DeepSeq+import Data.Int+import Test.Tasty.Bench++import Database.PostgreSQL.PQTypes.Deriving++main :: IO ()+main = defaultMain+  [ bgroup "enum"+    [ bench "encode" $ nf (encodeEnum @T) T42+    , bench "decode" $ nf (decodeEnum @T) 42+    ]+  , bgroup "enum-text"+    [ bench "encode" $ nf (encodeEnumAsText @S) S42+    , bench "decode" $ nf (decodeEnumAsText @S) "text_42"+    ]+  ]++data T = T01 | T02 | T03 | T04 | T05 | T06 | T07 | T08 | T09 | T10+       | T11 | T12 | T13 | T14 | T15 | T16 | T17 | T18 | T19 | T20+       | T21 | T22 | T23 | T24 | T25 | T26 | T27 | T28 | T29 | T30+       | T31 | T32 | T33 | T34 | T35 | T36 | T37 | T38 | T39 | T40+       | T41 | T42 | T43 | T44 | T45 | T46 | T47 | T48 | T49 | T50+  deriving (Eq, Show, Enum, Bounded)++instance NFData T where+  rnf = (`seq` ())++-- | Enum encoding for 'T'.+--+-- >>> isInjective (encodeEnum @T)+-- True+instance EnumEncoding T where+   type EnumBase T = Int16+   encodeEnum = \case+     T01 -> 1;  T02 -> 2;  T03 -> 3;  T04 -> 4;  T05 -> 5+     T06 -> 6;  T07 -> 7;  T08 -> 8;  T09 -> 9;  T10 -> 10+     T11 -> 11; T12 -> 12; T13 -> 13; T14 -> 14; T15 -> 15+     T16 -> 16; T17 -> 17; T18 -> 18; T19 -> 19; T20 -> 20+     T21 -> 21; T22 -> 22; T23 -> 23; T24 -> 24; T25 -> 25+     T26 -> 26; T27 -> 27; T28 -> 28; T29 -> 29; T30 -> 30+     T31 -> 31; T32 -> 32; T33 -> 33; T34 -> 34; T35 -> 35+     T36 -> 36; T37 -> 37; T38 -> 38; T39 -> 39; T40 -> 40+     T41 -> 41; T42 -> 42; T43 -> 43; T44 -> 44; T45 -> 45+     T46 -> 46; T47 -> 47; T48 -> 48; T49 -> 49; T50 -> 50++----------------------------------------++data S = S01 | S02 | S03 | S04 | S05 | S06 | S07 | S08 | S09 | S10+       | S11 | S12 | S13 | S14 | S15 | S16 | S17 | S18 | S19 | S20+       | S21 | S22 | S23 | S24 | S25 | S26 | S27 | S28 | S29 | S30+       | S31 | S32 | S33 | S34 | S35 | S36 | S37 | S38 | S39 | S40+       | S41 | S42 | S43 | S44 | S45 | S46 | S47 | S48 | S49 | S50+  deriving (Eq, Show, Enum, Bounded)++instance NFData S where+  rnf = (`seq` ())++-- | Enum encoding for 'S'.+--+-- >>> isInjective (encodeEnumAsText @S)+-- True+instance EnumAsTextEncoding S where+  encodeEnumAsText = \case+    S01 -> "text_01"; S02 -> "text_02"; S03 -> "text_03"; S04 -> "text_04"+    S05 -> "text_05"; S06 -> "text_06"; S07 -> "text_07"; S08 -> "text_08"+    S09 -> "text_09"; S10 -> "text_10"; S11 -> "text_11"; S12 -> "text_12"+    S13 -> "text_13"; S14 -> "text_14"; S15 -> "text_15"; S16 -> "text_16"+    S17 -> "text_17"; S18 -> "text_18"; S19 -> "text_19"; S20 -> "text_20"+    S21 -> "text_21"; S22 -> "text_22"; S23 -> "text_23"; S24 -> "text_24"+    S25 -> "text_25"; S26 -> "text_26"; S27 -> "text_27"; S28 -> "text_28"+    S29 -> "text_29"; S30 -> "text_30"; S31 -> "text_31"; S32 -> "text_32"+    S33 -> "text_33"; S34 -> "text_34"; S35 -> "text_35"; S36 -> "text_36"+    S37 -> "text_37"; S38 -> "text_38"; S39 -> "text_39"; S40 -> "text_40"+    S41 -> "text_41"; S42 -> "text_42"; S43 -> "text_43"; S44 -> "text_44"+    S45 -> "text_45"; S46 -> "text_46"; S47 -> "text_47"; S48 -> "text_48"+    S49 -> "text_49"; S50 -> "text_50";
hpqtypes-extras.cabal view
@@ -1,6 +1,6 @@-cabal-version:       1.18+cabal-version:       2.2 name:                hpqtypes-extras-version:             1.10.3.0+version:             1.10.4.0 synopsis:            Extra utilities for hpqtypes library description:         The following extras for hpqtypes library:                      .@@ -10,7 +10,7 @@                        of a database schema.  homepage:            https://github.com/scrive/hpqtypes-extras-license:             BSD3+license:             BSD-3-Clause license-file:        LICENSE extra-source-files:  CHANGELOG.md, README.md author:              Scrive AB@@ -20,18 +20,40 @@ copyright:           Scrive AB category:            Database build-type:          Simple-tested-with:         GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.3 || ==8.10.1+tested-with:         GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.3  Source-repository head   Type:     git   Location: https://github.com/scrive/hpqtypes-extras.git +common common-stanza+  Default-Language: Haskell2010+  Default-Extensions: BangPatterns+                    , DeriveDataTypeable+                    , ExistentialQuantification+                    , FlexibleContexts+                    , GeneralizedNewtypeDeriving+                    , LambdaCase+                    , MultiWayIf+                    , OverloadedStrings+                    , RankNTypes+                    , RecordWildCards+                    , ScopedTypeVariables+                    , StandaloneDeriving+                    , TupleSections+                    , TypeApplications+                    , TypeFamilies+                    , UndecidableInstances+                    , ViewPatterns+ library+  Import: common-stanza   hs-source-dirs: src    ghc-options: -Wall    exposed-modules: Database.PostgreSQL.PQTypes.Checks+                 , Database.PostgreSQL.PQTypes.Deriving                  , Database.PostgreSQL.PQTypes.ExtrasOptions                  , Database.PostgreSQL.PQTypes.Migrate                  , Database.PostgreSQL.PQTypes.Model@@ -52,11 +74,12 @@    build-depends: base              >= 4.9     && < 5                , hpqtypes          >= 1.8.0.0 && < 2.0.0.0-               , base16-bytestring >= 0.1     && < 0.2+               , base16-bytestring >= 0.1     && < 1.0.1.0                , bytestring        >= 0.10    && < 0.11                , containers        >= 0.5     && < 0.7                , cryptohash        >= 0.11    && < 0.12                , exceptions        >= 0.10    && < 0.11+               , extra             >= 1.6.17  && < 1.8.0                , mtl               >= 2.2     && < 2.3                , fields-json       >= 0.4     && < 0.5                , text              >= 1.2     && < 1.3@@ -68,26 +91,11 @@                , safe              >= 0.3     && < 0.4    default-language: Haskell2010-  default-extensions: BangPatterns-                    , DeriveDataTypeable-                    , ExistentialQuantification-                    , FlexibleContexts-                    , GeneralizedNewtypeDeriving-                    , LambdaCase-                    , MultiWayIf-                    , OverloadedStrings-                    , RankNTypes-                    , RecordWildCards-                    , ScopedTypeVariables-                    , StandaloneDeriving-                    , TupleSections-                    , TypeFamilies-                    , UndecidableInstances-                    , ViewPatterns+  default-extensions:   other-extensions:   CPP-                    , TypeApplications  test-suite  hpqtypes-extras-tests+  Import:             common-stanza   type:               exitcode-stdio-1.0   hs-source-dirs:     test   main-is:            Main.hs@@ -100,6 +108,7 @@                     , ScopedTypeVariables   ghc-options:        -Wall   build-depends:      base,+                      QuickCheck,                       exceptions,                       hpqtypes,                       hpqtypes-extras,@@ -109,5 +118,18 @@                       tasty,                       tasty-hunit,                       text,+                      time,                       transformers,                       uuid-types++benchmark bench+  Import:             common-stanza+  type:               exitcode-stdio-1.0+  hs-source-dirs:     benchmark+  main-is:            Main.hs++  ghc-options:        -Wall+  build-depends:      base+                    , deepseq+                    , hpqtypes-extras+                    , tasty-bench
+ src/Database/PostgreSQL/PQTypes/Deriving.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+module Database.PostgreSQL.PQTypes.Deriving (+  -- * Helpers, to be used with @deriving via@ (@-XDerivingVia@).+    SQLEnum(..)+  , EnumEncoding(..)+  , SQLEnumAsText(..)+  , EnumAsTextEncoding(..)+    -- * For use in doctests.+  , isInjective+  ) where++import Control.Exception (SomeException(..), throwIO)+import Data.List.Extra (enumerate, nubSort)+import Data.Map.Strict (Map)+import Data.Text (Text)+import Data.Typeable+import Database.PostgreSQL.PQTypes+import qualified Data.Map.Strict as Map++-- | Helper newtype to be used with @deriving via@ to derive @(PQFormat, ToSQL,+-- FromSQL)@ instances for enums, given an instance of 'EnumEncoding'.+--+-- /Hint:/ non-trivial 'Enum' instances can be derived using the 'generic-data'+-- package!+--+-- >>> :{+-- data Colours = Blue | Black | Red | Mauve | Orange+--   deriving (Eq, Show, Enum, Bounded)+-- instance EnumEncoding Colours where+--   type EnumBase Colours = Int16+--   encodeEnum = \case+--     Blue   -> 1+--     Black  -> 7+--     Red    -> 2+--     Mauve  -> 6+--     Orange -> 3+-- :}+--+-- /Note:/ To get SQL-specific instances use @DerivingVia@:+--+-- @+-- data Colours = ...+--   ...+--   deriving (PQFormat, ToSQL, FromSQL) via SQLEnum Colours+-- @+--+-- >>> isInjective (encodeEnum @Colours)+-- True+--+-- >>> decodeEnum @Colours 7+-- Right Black+--+-- >>> decodeEnum @Colours 42+-- Left [(1,3),(6,7)]+newtype SQLEnum a = SQLEnum a++class+  ( -- The semantic type needs to be finitely enumerable.+    Enum a+  , Bounded a+    -- The base type needs to be enumerable and ordered.+  , Enum (EnumBase a)+  , Ord (EnumBase a)+  ) => EnumEncoding a where+  type EnumBase a+  -- | Encode @a@ as a base type.+  encodeEnum :: a -> EnumBase a++  -- | Decode base type to an @a@. If the conversion fails, a list of valid+  -- ranges is returned instead.+  --+  -- /Note:/ The default implementation looks up values in 'decodeEnumMap' and+  -- can be overwritten for performance if necessary.+  decodeEnum :: EnumBase a -> Either [(EnumBase a, EnumBase a)] a+  decodeEnum b = maybe (Left . intervals $ Map.keys (decodeEnumMap @a)) Right+               $ Map.lookup b (decodeEnumMap @a)++  -- | Include the inverse map as a top-level part of the 'EnumEncoding'+  -- instance to ensure it is only computed once by GHC.+  decodeEnumMap :: Map (EnumBase a) a+  decodeEnumMap = Map.fromList [ (encodeEnum a, a) | a <- enumerate ]++instance PQFormat (EnumBase a) => PQFormat (SQLEnum a) where+  pqFormat = pqFormat @(EnumBase a)++instance+  ( EnumEncoding a+  , PQFormat (EnumBase a)+  , ToSQL (EnumBase a)+  ) => ToSQL (SQLEnum a) where+  type PQDest (SQLEnum a) = PQDest (EnumBase a)+  toSQL (SQLEnum a) = toSQL $ encodeEnum a++instance+  ( EnumEncoding a+  , PQFormat (EnumBase a)+  , FromSQL (EnumBase a)+  , Show (EnumBase a)+  , Typeable (EnumBase a)+  ) => FromSQL (SQLEnum a) where+  type PQBase (SQLEnum a) = PQBase (EnumBase a)+  fromSQL base = do+    b <- fromSQL base+    case decodeEnum b of+      Left validRange -> throwIO $ SomeException RangeError+        { reRange = validRange+        , reValue = b+        }+      Right a -> return $ SQLEnum a++-- | A special case of 'SQLEnum', where the enum is to be encoded as text+-- ('SQLEnum' can't be used because of the 'Enum' constraint on the domain of+-- 'encodeEnum').+--+-- >>> :{+-- data Person = Alfred | Bertrand | Charles+--   deriving (Eq, Show, Enum, Bounded)+-- instance EnumAsTextEncoding Person where+--   encodeEnumAsText = \case+--     Alfred   -> "alfred"+--     Bertrand -> "bertrand"+--     Charles  -> "charles"+-- :}+--+-- /Note:/ To get SQL-specific instances use @DerivingVia@:+--+-- @+-- data Person = ...+--   ...+--   deriving (PQFormat, ToSQL, FromSQL) via SQLEnumAsText Person+-- @+--+-- >>> isInjective (encodeEnumAsText @Person)+-- True+--+-- >>> decodeEnumAsText @Person "bertrand"+-- Right Bertrand+--+-- >>> decodeEnumAsText @Person "batman"+-- Left ["alfred","bertrand","charles"]+newtype SQLEnumAsText a = SQLEnumAsText a++class (Enum a, Bounded a) => EnumAsTextEncoding a where+  -- | Encode @a@ as 'Text'.+  encodeEnumAsText :: a -> Text++  -- | Decode 'Text' to an @a@. If the conversion fails, a list of valid values+  -- is returned instead.+  --+  -- /Note:/ The default implementation looks up values in 'decodeEnumAsTextMap'+  -- and can be overwritten for performance if necessary.+  decodeEnumAsText :: Text -> Either [Text] a+  decodeEnumAsText text = maybe (Left $ Map.keys (decodeEnumAsTextMap @a)) Right+                        $ Map.lookup text (decodeEnumAsTextMap @a)++  -- | Include the inverse map as a top-level part of the 'SQLEnumTextEncoding'+  -- instance to ensure it is only computed once by GHC.+  decodeEnumAsTextMap :: Map Text a+  decodeEnumAsTextMap = Map.fromList [ (encodeEnumAsText a, a) | a <- enumerate ]++instance EnumAsTextEncoding a => PQFormat (SQLEnumAsText a) where+  pqFormat = pqFormat @Text++instance EnumAsTextEncoding a => ToSQL (SQLEnumAsText a) where+  type PQDest (SQLEnumAsText a) = PQDest Text+  toSQL (SQLEnumAsText a) = toSQL $ encodeEnumAsText a++instance EnumAsTextEncoding a => FromSQL (SQLEnumAsText a) where+  type PQBase (SQLEnumAsText a) = PQBase Text+  fromSQL base = do+    text <- fromSQL base+    case decodeEnumAsText text of+      Left validValues -> throwIO $ SomeException InvalidValue+        { ivValue       = text+        , ivValidValues = Just validValues+        }+      Right a -> return $ SQLEnumAsText a++-- | To be used in doctests to prove injectivity of encoding functions.+--+-- >>> isInjective (id :: Bool -> Bool)+-- True+--+-- >>> isInjective (\(_ :: Bool) -> False)+-- False+isInjective :: (Enum a, Bounded a, Eq a, Eq b) => (a -> b) -> Bool+isInjective f = null [ (a, b) | a <- enumerate, b <- enumerate, a /= b, f a == f b ]++-- | Internal helper: given a list of values, decompose it into a list of+-- intervals.+--+-- >>> intervals [42,2,1,0,3,88,-1,43,42]+-- [(-1,3),(42,43),(88,88)]+--+-- prop> nubSort xs == concatMap (\(l,r) -> [l .. r]) (intervals xs)+intervals :: forall  a . (Enum a, Ord a) => [a] -> [(a, a)]+intervals as = case nubSort as of+  [] -> []+  (first : ascendingRest) -> accumIntervals (first, first) ascendingRest+  where+    accumIntervals :: (a, a) -> [a] -> [(a, a)]+    accumIntervals (lower, upper) [] = [(lower, upper)]+    accumIntervals (lower, upper) (first' : ascendingRest') = if succ upper == first'+      then accumIntervals (lower, first') ascendingRest'+      else (lower, upper) : accumIntervals (first', first') ascendingRest'++-- $setup+-- >>> import Data.Int
src/Database/PostgreSQL/PQTypes/Model/ColumnType.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE TypeApplications #-} module Database.PostgreSQL.PQTypes.Model.ColumnType (     ColumnType(..)   , columnTypeToSQL
src/Database/PostgreSQL/PQTypes/SQL/Builder.hs view
@@ -4,27 +4,28 @@ monadic DSL for building SQL statements on the fly. Some examples:  >>> :{->>> | sqlSelect "documents" $ do->>> |  sqlResult "id"->>> |  sqlResult "title"->>> |  sqlResult "mtime"->>> |  sqlOrderBy "documents.mtime DESC"->>> |  sqlWhereILike "documents.title" "%pattern%"->>> :}-SQL " SELECT  id, title, mtime FROM documents WHERE (documents.title ILIKE <\"%pattern%\">)   ORDER BY documents.mtime DESC  "+sqlSelect "documents" $ do+  sqlResult "id"+  sqlResult "title"+  sqlResult "mtime"+  sqlOrderBy "documents.mtime DESC"+  sqlWhereILike "documents.title" "%pattern%"+:}+SQL " SELECT  id, title, mtime FROM documents WHERE (documents.title ILIKE <\"%pattern%\">)    ORDER BY documents.mtime DESC  "  @SQL.Builder@ supports SELECT as 'sqlSelect' and data manipulation using 'sqlInsert', 'sqlInsertSelect', 'sqlDelete' and 'sqlUpdate'. ->>> import Data.Time.Clock->>> now <- getCurrentTime+>>> import Data.Time+>>> let title = "title" :: String+>>> let ctime  = read "2020-01-01 00:00:00 UTC" :: UTCTime >>> :{->>> | sqlInsert "documents" $ do->>> |   sqlSet "title" title->>> |   sqlSet "ctime" now->>> |   sqlResult "id"->>> :}-SQL " INSERT INTO documents (title, ctime) VALUES (<\"title\">, <\"2017-02-01 17:56:20.324894547 UTC\">) RETURNING id"+sqlInsert "documents" $ do+  sqlSet "title" title+  sqlSet "ctime" ctime+  sqlResult "id"+:}+SQL " INSERT INTO documents (title, ctime) VALUES (<\"title\">, <2020-01-01 00:00:00 UTC>)  RETURNING id"  The 'sqlInsertSelect' is particulary interesting as it supports INSERT of values taken from a SELECT clause from same or even different@@ -38,12 +39,12 @@ element.  >>> :{->>> | sqlInsert "documents" $ do->>> |   sqlSet "ctime" now->>> |   sqlSetList "title" ["title1", "title2", "title3"]->>> |   sqlResult "id"->>> :}-SQL " INSERT INTO documents (ctime, title) VALUES (<\"2017-02-01 17:56:20.324894547 UTC\">, <\"title1\">) , (<\"2017-02-01 17:56:20.324894547 UTC\">, <\"title2\">) , (<\"2017-02-01 17:56:20.324894547 UTC\">, <\"title3\">) RETURNING id"+sqlInsert "documents" $ do+  sqlSet "ctime" ctime+  sqlSetList "title" ["title1", "title2", "title3"]+  sqlResult "id"+:}+SQL " INSERT INTO documents (ctime, title) VALUES (<2020-01-01 00:00:00 UTC>, <\"title1\">) , (<2020-01-01 00:00:00 UTC>, <\"title2\">) , (<2020-01-01 00:00:00 UTC>, <\"title3\">)  RETURNING id"  The above will insert 3 new documents. @@ -51,17 +52,17 @@ 'sqlOrderBy', @GROUP BY@ as 'sqlGroupBy'.  >>> :{->>> | sqlSelect "documents" $ do->>> |   sqlResult "id"->>> |   sqlResult "title"->>> |   sqlResult "mtime"->>> |   sqlOrderBy "documents.mtime DESC"->>> |   sqlOrderBy "documents.title"->>> |   sqlGroupBy "documents.status"->>> |   sqlJoinOn "users" "documents.user_id = users.id"->>> |   sqlWhere $ mkSQL "documents.title ILIKE" <?> "%pattern%"->>> :}-SQL " SELECT  id, title, mtime FROM documents  JOIN  users  ON  documents.user_id = users.id WHERE (documents.title ILIKE <\"%pattern%\">) GROUP BY documents.status  ORDER BY documents.mtime DESC, documents.title  "+sqlSelect "documents" $ do+  sqlResult "id"+  sqlResult "title"+  sqlResult "mtime"+  sqlOrderBy "documents.mtime DESC"+  sqlOrderBy "documents.title"+  sqlGroupBy "documents.status"+  sqlJoinOn "users" "documents.user_id = users.id"+  sqlWhere $ mkSQL "documents.title ILIKE" <?> "%pattern%"+:}+SQL " SELECT  id, title, mtime FROM documents  JOIN  users  ON  documents.user_id = users.id WHERE (documents.title ILIKE <\"%pattern%\">)  GROUP BY documents.status  ORDER BY documents.mtime DESC, documents.title  "  Joins are done by 'sqlJoinOn', 'sqlLeftJoinOn', 'sqlRightJoinOn', 'sqlJoinOn', 'sqlFullJoinOn'. If everything fails use 'sqlJoin' and@@ -69,16 +70,16 @@ some kind of abstract syntax data type is lacking.  >>> :{->>> | sqlDelete "mails" $ do->>> |   sqlWhere "id > 67"->>> :}+sqlDelete "mails" $ do+  sqlWhere "id > 67"+:} SQL " DELETE FROM mails  WHERE (id > 67) "  >>> :{->>> | sqlUpdate "document_tags" $ do->>> |   sqlSet "value" (123 :: Int)->>> |   sqlWhere "name = 'abc'"->>> :}+sqlUpdate "document_tags" $ do+  sqlSet "value" (123 :: Int)+  sqlWhere "name = 'abc'"+:} SQL " UPDATE document_tags SET value=<123>  WHERE (name = 'abc') "  Exception returning and 'kWhyNot' are a subsystem for querying why a@@ -123,8 +124,7 @@  -} --- TODO: clean this up and fix the mess with--- "randomly" wrapping stuff in parentheses.+-- TODO: clean this up, add more documentation.  module Database.PostgreSQL.PQTypes.SQL.Builder   ( sqlWhere@@ -484,22 +484,27 @@      makeLongEnough (Many x)   = take longest (x ++ repeat "DEFAULT")  instance Sqlable SqlInsertSelect where-  toSQLCommand cmd =-    "INSERT INTO" <+> sqlInsertSelectWhat cmd <+>-    parenthesize (sqlConcatComma (map fst (sqlInsertSelectSet cmd))) <+>-    parenthesize (toSQLCommand (SqlSelect { sqlSelectFrom    = sqlInsertSelectFrom cmd-                                          , sqlSelectUnion   = []-                                          , sqlSelectDistinct = sqlInsertSelectDistinct cmd-                                          , sqlSelectResult  = fmap snd $ sqlInsertSelectSet cmd-                                          , sqlSelectWhere   = sqlInsertSelectWhere cmd-                                          , sqlSelectOrderBy = sqlInsertSelectOrderBy cmd-                                          , sqlSelectGroupBy = sqlInsertSelectGroupBy cmd-                                          , sqlSelectHaving  = sqlInsertSelectHaving cmd-                                          , sqlSelectOffset  = sqlInsertSelectOffset cmd-                                          , sqlSelectLimit   = sqlInsertSelectLimit cmd-                                          , sqlSelectWith    = sqlInsertSelectWith cmd-                                          })) <+>-    emitClausesSepComma "RETURNING" (sqlInsertSelectResult cmd)+  toSQLCommand cmd = smconcat+    -- WITH clause needs to be at the top level, so we emit it here and not+    -- include it in the SqlSelect below.+    [ emitClausesSepComma "WITH" $+      map (\(name,command) -> name <+> "AS" <+> parenthesize command) (sqlInsertSelectWith cmd)+    , "INSERT INTO" <+> sqlInsertSelectWhat cmd+    , parenthesize . sqlConcatComma . map fst $ sqlInsertSelectSet cmd+    , parenthesize . toSQLCommand $ SqlSelect { sqlSelectFrom    = sqlInsertSelectFrom cmd+                                              , sqlSelectUnion   = []+                                              , sqlSelectDistinct = sqlInsertSelectDistinct cmd+                                              , sqlSelectResult  = fmap snd $ sqlInsertSelectSet cmd+                                              , sqlSelectWhere   = sqlInsertSelectWhere cmd+                                              , sqlSelectOrderBy = sqlInsertSelectOrderBy cmd+                                              , sqlSelectGroupBy = sqlInsertSelectGroupBy cmd+                                              , sqlSelectHaving  = sqlInsertSelectHaving cmd+                                              , sqlSelectOffset  = sqlInsertSelectOffset cmd+                                              , sqlSelectLimit   = sqlInsertSelectLimit cmd+                                              , sqlSelectWith    = []+                                              }+    , emitClausesSepComma "RETURNING" $ sqlInsertSelectResult cmd+    ]  instance Sqlable SqlUpdate where   toSQLCommand cmd =
test/Main.hs view
@@ -34,7 +34,8 @@  instance IsOption ConnectionString where   defaultValue = ConnectionString-                 "postgresql://postgres@localhost/travis_ci_test"+    -- For GitHub Actions CI+    "host=postgres user=postgres password=postgres"   parseValue   = Just . ConnectionString   optionName   = return "connection-string"   optionHelp   = return "Postgres connection string"