Skip to content

Commit cbfc6fa

Browse files
vrom911Dmitrii Kovanikov
authored andcommitted
Initial implementation (#1)
* Initial implementation * Update src/PgNamed.hs Co-Authored-By: Dmitrii Kovanikov <[email protected]> * Make fields strict * Fix stack
1 parent 206d0e3 commit cbfc6fa

File tree

6 files changed

+188
-52
lines changed

6 files changed

+188
-52
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ cabal.project.local
2323
.HTF/
2424
# Stack
2525
.stack-work/
26+
stack.yaml.lock
2627

2728
### IDE/support
2829
# Vim

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ cache:
1414

1515
matrix:
1616
include:
17-
- ghc: 8.6.4
18-
19-
- ghc: 8.6.4
17+
- ghc: 8.6.5
18+
19+
- ghc: 8.6.5
2020
env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
2121

2222
install:

postgresql-simple-named.cabal

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1-
cabal-version: 2.0
1+
cabal-version: 2.4
22
name: postgresql-simple-named
33
version: 0.0.0.0
44
synopsis: Implementation of named parameters for `postgresql-simple` library
5-
description: Implementation of named parameters for `postgresql-simple` library
5+
description:
6+
Implementation of named parameters for `postgresql-simple` library.
7+
.
8+
Here is an exaple of how it could be used in your code:
9+
.
10+
> queryNamed [sql|
11+
> SELECT *
12+
> FROM table
13+
> WHERE foo = ?foo
14+
> AND bar = ?bar
15+
> AND baz = ?foo
16+
> |] [ "foo" =? "fooBar"
17+
> , "bar" =? "barVar"
18+
> ]
19+
620
homepage: https://github.com/Holmusk/postgresql-simple-named
721
bug-reports: https://github.com/Holmusk/postgresql-simple-named/issues
822
license: MPL-2.0
@@ -14,19 +28,14 @@ category: Database
1428
build-type: Simple
1529
extra-doc-files: README.md
1630
, CHANGELOG.md
17-
tested-with: GHC == 8.6.4
31+
tested-with: GHC == 8.6.5
1832

1933
source-repository head
2034
type: git
2135
location: https://github.com/Holmusk/postgresql-simple-named.git
2236

23-
library
24-
hs-source-dirs: src
25-
exposed-modules: PostgresqlSimpleNamed
26-
27-
37+
common common-options
2838
build-depends: base ^>= 4.12.0.0
29-
3039

3140
ghc-options: -Wall
3241
-Wincomplete-uni-patterns
@@ -53,39 +62,21 @@ library
5362
TypeApplications
5463
ViewPatterns
5564

65+
library
66+
import: common-options
67+
hs-source-dirs: src
68+
exposed-modules: PgNamed
69+
build-depends: bytestring ^>= 0.10.8
70+
, mtl ^>= 2.2
71+
, postgresql-simple ^>= 0.6.2
72+
, text ^>= 1.2
73+
5674
test-suite postgresql-simple-named-test
75+
import: common-options
5776
type: exitcode-stdio-1.0
5877
hs-source-dirs: test
5978
main-is: Spec.hs
6079

61-
build-depends: base ^>= 4.12.0.0
62-
, postgresql-simple-named
63-
64-
65-
ghc-options: -Wall
66-
-threaded
67-
-rtsopts
68-
-with-rtsopts=-N
69-
-Wincomplete-uni-patterns
70-
-Wincomplete-record-updates
71-
-Wcompat
72-
-Widentities
73-
-Wredundant-constraints
74-
-fhide-source-paths
75-
-Wmissing-export-lists
76-
-Wpartial-fields
80+
build-depends: postgresql-simple-named
7781

78-
default-language: Haskell2010
79-
default-extensions: ConstraintKinds
80-
DeriveGeneric
81-
GeneralizedNewtypeDeriving
82-
InstanceSigs
83-
KindSignatures
84-
LambdaCase
85-
OverloadedStrings
86-
RecordWildCards
87-
ScopedTypeVariables
88-
StandaloneDeriving
89-
TupleSections
90-
TypeApplications
91-
ViewPatterns
82+
ghc-options: -threaded -rtsopts -with-rtsopts=-N

src/PgNamed.hs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{-# LANGUAGE DerivingStrategies #-}
2+
{-# LANGUAGE ExistentialQuantification #-}
3+
{-# LANGUAGE FlexibleContexts #-}
4+
5+
{- | Introduces named parameters for @postgresql-simple@ library.
6+
It uses @?@ question mark symbol as the indicator of the named parameter which
7+
is replaced with the standard syntax with question marks. Check out the example
8+
of usage:
9+
10+
@
11+
queryNamed [sql|
12+
SELECT *
13+
FROM users
14+
WHERE foo = ?foo
15+
AND bar = ?bar
16+
AND baz = ?foo
17+
|] [ "foo" =? "fooBar"
18+
, "bar" =? "barVar"
19+
]
20+
@
21+
-}
22+
23+
module PgNamed
24+
( NamedParam (..)
25+
, Name (..)
26+
27+
, extractNames
28+
, namesToRow
29+
, (=?)
30+
) where
31+
32+
import Control.Monad.Except (MonadError (throwError))
33+
import Data.Bifunctor (bimap)
34+
import Data.ByteString (ByteString)
35+
import Data.Char (isAlphaNum)
36+
import Data.List (lookup)
37+
import Data.List.NonEmpty (NonEmpty (..))
38+
import Data.Text (Text)
39+
import Data.Text.Encoding (decodeUtf8)
40+
import GHC.Exts (IsString)
41+
42+
import qualified Data.ByteString.Char8 as BS
43+
import qualified Database.PostgreSQL.Simple as PG
44+
import qualified Database.PostgreSQL.Simple.ToField as PG
45+
import qualified Database.PostgreSQL.Simple.Types as PG
46+
47+
-- | Wrapper over name of the argument.
48+
newtype Name = Name
49+
{ unName :: Text
50+
} deriving newtype (Show, Eq, Ord, IsString)
51+
52+
-- | Data type to represent each named parameter.
53+
data NamedParam = NamedParam
54+
{ namedParamName :: !Name
55+
, namedParamParam :: !PG.Action
56+
} deriving (Show)
57+
58+
-- | @PostgreSQL@ error type for named parameters.
59+
data PgNamedError
60+
-- | Named parameter is not specified.
61+
= PgNamedParam Name
62+
-- | Query has no names inside but was called with named functions,
63+
| PgNoNames PG.Query
64+
-- | Query contains an empty name.
65+
| PgEmptyName PG.Query
66+
67+
68+
-- | Type alias for monads that can throw errors of the 'PgNamedError' type.
69+
type WithError = MonadError PgNamedError
70+
71+
instance Show PgNamedError where
72+
show e = "PostgreSQL named parameter error: " ++ case e of
73+
PgNamedParam n -> "Named parameter '" ++ show n ++ "' is not specified"
74+
PgNoNames (PG.Query q) ->
75+
"Query has no names but was called with named functions: " ++ BS.unpack q
76+
PgEmptyName (PG.Query q) ->
77+
"Query contains an empty name: " ++ BS.unpack q
78+
79+
-- | Checks whether the 'Name' is in the list and returns its parameter.
80+
lookupName :: Name -> [NamedParam] -> Maybe PG.Action
81+
lookupName n = lookup n . map (\NamedParam{..} -> (namedParamName, namedParamParam))
82+
83+
{- | This function takes query with named parameters specified like this:
84+
85+
@
86+
SELECT name, user FROM users WHERE id = ?id
87+
@
88+
89+
and returns either the error or query with all all names replaced by
90+
questiosn marks @?@ with list of the names in the order of their appearance.
91+
92+
For example:
93+
94+
>>> extractNames "SELECT * FROM users WHERE foo = ?foo AND bar = ?bar AND baz = ?foo"
95+
Right ("SELECT * FROM users WHERE foo = ? AND bar = ? AND baz = ?","foo" :| ["bar","foo"])
96+
-}
97+
extractNames
98+
:: PG.Query
99+
-> Either PgNamedError (PG.Query, NonEmpty Name)
100+
extractNames qr = go (PG.fromQuery qr) >>= \case
101+
(_, []) -> Left $ PgNoNames qr
102+
(q, name:names) -> Right (PG.Query q, name :| names)
103+
where
104+
go :: ByteString -> Either PgNamedError (ByteString, [Name])
105+
go str
106+
| BS.null str = Right ("", [])
107+
| otherwise = let (before, after) = BS.break (== '?') str in
108+
case BS.uncons after of
109+
Nothing -> Right (before, [])
110+
Just ('?', nameStart) ->
111+
let (name, remainingQuery) = BS.span isNameChar nameStart
112+
in if BS.null name
113+
then Left $ PgEmptyName qr
114+
else fmap (bimap ((before <> "?") <>) (Name (decodeUtf8 name) :))
115+
(go remainingQuery)
116+
Just _ -> error "'break (== '?')' doesn't return string started with the question mark"
117+
118+
isNameChar :: Char -> Bool
119+
isNameChar c = isAlphaNum c || c == '_'
120+
121+
122+
-- | Returns the list of values to use in query by given list of 'Name's.
123+
namesToRow
124+
:: forall m . WithError m
125+
=> NonEmpty Name -- ^ List of the names used in query
126+
-> [NamedParam] -- ^ List of the named parameters
127+
-> m (NonEmpty PG.Action)
128+
namesToRow names params = traverse magicLookup names
129+
where
130+
magicLookup :: Name -> m PG.Action
131+
magicLookup n = case lookupName n params of
132+
Just x -> pure x
133+
Nothing -> throwError $ PgNamedParam n
134+
135+
{- | Operator to create 'NamedParam's.
136+
137+
>>> "foo" =? (1 :: Int)
138+
NamedParam {namedParamName = "foo", namedParamParam = 1}
139+
140+
So it can be used in creating the list of the named arguments:
141+
142+
@
143+
queryNamed [sql|
144+
SELECT * FROM users WHERE foo = ?foo AND bar = ?bar AND baz = ?foo"
145+
|] [ "foo" =? "fooBar"
146+
, "bar" =? "barVar"
147+
]
148+
@
149+
-}
150+
infix 7 =?
151+
(=?) :: (PG.ToField a) => Name -> a -> NamedParam
152+
n =? a = NamedParam n $ PG.toField a
153+
{-# INLINE (=?) #-}

src/PostgresqlSimpleNamed.hs

Lines changed: 0 additions & 6 deletions
This file was deleted.

stack.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
resolver: lts-13.16
2-
3-
ghc-options:
4-
"$locals": -fhide-source-paths
1+
resolver: lts-13.27

0 commit comments

Comments
 (0)