Merge branch 'jcristovao-nullsFirst'
This commit is contained in:
@@ -3,6 +3,10 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
### Added
|
||||||
|
- Option to specify nulls first or last, eg /people?order=age.desc.nullsfirst
|
||||||
|
|
||||||
## [0.2.7.0] - 2015-03-03
|
## [0.2.7.0] - 2015-03-03
|
||||||
### Added
|
### Added
|
||||||
- Server response logging
|
- Server response logging
|
||||||
|
|||||||
+13
-5
@@ -1,4 +1,4 @@
|
|||||||
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
|
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiWayIf #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module PgQuery where
|
module PgQuery where
|
||||||
@@ -39,6 +39,7 @@ data QualifiedTable = QualifiedTable {
|
|||||||
data OrderTerm = OrderTerm {
|
data OrderTerm = OrderTerm {
|
||||||
otTerm :: T.Text
|
otTerm :: T.Text
|
||||||
, otDirection :: BS.ByteString
|
, otDirection :: BS.ByteString
|
||||||
|
, otNullOrder :: Maybe BS.ByteString
|
||||||
}
|
}
|
||||||
|
|
||||||
limitT :: Maybe NonnegRange -> StatementT
|
limitT :: Maybe NonnegRange -> StatementT
|
||||||
@@ -67,7 +68,8 @@ orderT ts q =
|
|||||||
queryTerm :: OrderTerm -> PStmt
|
queryTerm :: OrderTerm -> PStmt
|
||||||
queryTerm t = B.Stmt
|
queryTerm t = B.Stmt
|
||||||
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
|
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
|
||||||
<> cs (otDirection t) <> " ")
|
<> cs (otDirection t) <> " "
|
||||||
|
<> maybe "" cs (otNullOrder t) <> " ")
|
||||||
empty True
|
empty True
|
||||||
|
|
||||||
parentheticT :: StatementT
|
parentheticT :: StatementT
|
||||||
@@ -175,10 +177,16 @@ orderParse q =
|
|||||||
orderParseTerm :: T.Text -> Maybe OrderTerm
|
orderParseTerm :: T.Text -> Maybe OrderTerm
|
||||||
orderParseTerm s =
|
orderParseTerm s =
|
||||||
case T.split (=='.') s of
|
case T.split (=='.') s of
|
||||||
[c,d] ->
|
(c:d:nls) ->
|
||||||
if d `elem` ["asc", "desc"]
|
if d `elem` ["asc", "desc"]
|
||||||
then Just $ OrderTerm c $
|
then Just $ OrderTerm c
|
||||||
if d == "asc" then "asc" else "desc"
|
( if d == "asc" then "asc" else "desc" )
|
||||||
|
( case nls of
|
||||||
|
[n] -> if | n == "nullsfirst" -> Just "nulls first"
|
||||||
|
| n == "nullslast" -> Just "nulls last"
|
||||||
|
| otherwise -> Nothing
|
||||||
|
_ -> Nothing
|
||||||
|
)
|
||||||
else Nothing
|
else Nothing
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
|||||||
+52
-35
@@ -3,28 +3,16 @@ module Feature.QuerySpec where
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Hasql as H
|
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||||
import Hasql.Postgres as H
|
|
||||||
import Control.Monad (void)
|
|
||||||
import Data.Text(Text)
|
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
testSet :: IO ()
|
|
||||||
testSet = do
|
|
||||||
clearTable "items" >> clearTable "no_pk"
|
|
||||||
createItems 15
|
|
||||||
pool <- H.acquirePool pgSettings testPoolOpts
|
|
||||||
void . liftIO $ H.session pool $ H.tx Nothing $ do
|
|
||||||
H.unitEx $ insertNoPk "xyyx" "u"
|
|
||||||
H.unitEx $ insertNoPk "xYYx" "v"
|
|
||||||
|
|
||||||
where
|
|
||||||
insertNoPk :: Text -> Text -> H.Stmt H.Postgres
|
|
||||||
insertNoPk = [H.stmt|insert into "1".no_pk (a, b) values (?,?)|]
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
spec =
|
||||||
|
beforeAll (clearTable "items" >> createItems 15)
|
||||||
|
. beforeAll (clearTable "no_pk" >> createNulls 2 >> createLikableStrings)
|
||||||
|
. afterAll_ (clearTable "items" >> clearTable "no_pk" >> clearTable "simple_pk")
|
||||||
|
. around withApp $ do
|
||||||
describe "Querying a nonexistent table" $
|
describe "Querying a nonexistent table" $
|
||||||
it "causes a 404" $
|
it "causes a 404" $
|
||||||
get "/faketable" `shouldRespondWith` 404
|
get "/faketable" `shouldRespondWith` 404
|
||||||
@@ -47,18 +35,18 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "matches with like" $ do
|
it "matches with like" $ do
|
||||||
get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
|
get "/simple_pk?k=like.*yx" `shouldRespondWith`
|
||||||
[{"a":"xyyx","b":"u"}]|]
|
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
|
||||||
get "/no_pk?a=like.xy*" `shouldRespondWith` [json|
|
get "/simple_pk?k=like.xy*" `shouldRespondWith`
|
||||||
[{"a":"xyyx","b":"u"}]|]
|
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
|
||||||
get "/no_pk?a=like.*YY*" `shouldRespondWith` [json|
|
get "/simple_pk?k=like.*YY*" `shouldRespondWith`
|
||||||
[{"a":"xYYx","b":"v"}]|]
|
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||||
|
|
||||||
it "matches with ilike" $ do
|
it "matches with ilike" $ do
|
||||||
get "/no_pk?a=ilike.xy*&order=b.asc" `shouldRespondWith` [json|
|
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
|
||||||
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
|
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||||
get "/no_pk?a=ilike.*YY*&order=b.asc" `shouldRespondWith` [json|
|
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
|
||||||
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
|
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||||
|
|
||||||
describe "ordering response" $ do
|
describe "ordering response" $ do
|
||||||
it "by a column asc" $
|
it "by a column asc" $
|
||||||
@@ -76,6 +64,36 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
|||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "by a column asc with nulls last" $
|
||||||
|
get "/no_pk?order=a.asc.nullslast"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"a":"1","b":"0"},
|
||||||
|
{"a":"2","b":"0"},
|
||||||
|
{"a":null,"b":null}] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "by a column desc with nulls first" $
|
||||||
|
get "/no_pk?order=a.desc.nullsfirst"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"a":null,"b":null},
|
||||||
|
{"a":"2","b":"0"},
|
||||||
|
{"a":"1","b":"0"}] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "by a column desc with nulls last" $
|
||||||
|
get "/no_pk?order=a.desc.nullslast"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"a":"2","b":"0"},
|
||||||
|
{"a":"1","b":"0"},
|
||||||
|
{"a":null,"b":null}] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||||
|
}
|
||||||
|
|
||||||
it "without other constraints" $
|
it "without other constraints" $
|
||||||
get "/items?order=asc.id" `shouldRespondWith` 200
|
get "/items?order=asc.id" `shouldRespondWith` 200
|
||||||
|
|
||||||
@@ -88,10 +106,9 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
|||||||
, matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]
|
, matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "Omits question mark when there are no params" $
|
it "Omits question mark when there are no params" $ do
|
||||||
get "/simple_pk"
|
r <- get "/simple_pk"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
liftIO $ do
|
||||||
matchBody = Just "[]"
|
let respHeaders = simpleHeaders r
|
||||||
, matchStatus = 200
|
respHeaders `shouldSatisfy` matchHeader
|
||||||
, matchHeaders = ["Content-Location" <:> "/simple_pk"]
|
"Content-Location" "/simple_pk"
|
||||||
}
|
|
||||||
|
|||||||
+22
-1
@@ -109,9 +109,30 @@ createItems n = do
|
|||||||
<- H.acquirePool pgSettings testPoolOpts
|
<- H.acquirePool pgSettings testPoolOpts
|
||||||
void . liftIO $ H.session pool $ H.tx Nothing txn
|
void . liftIO $ H.session pool $ H.tx Nothing txn
|
||||||
where
|
where
|
||||||
txn = sequence_ $ map H.unitEx stmts
|
txn = mapM_ H.unitEx stmts
|
||||||
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
|
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
|
||||||
|
|
||||||
|
createNulls :: Int -> IO ()
|
||||||
|
createNulls n = do
|
||||||
|
pool :: H.Pool H.Postgres
|
||||||
|
<- H.acquirePool pgSettings testPoolOpts
|
||||||
|
void . liftIO $ H.session pool $ H.tx Nothing txn
|
||||||
|
where
|
||||||
|
txn = mapM_ H.unitEx (stmt':stmts)
|
||||||
|
stmt' = [H.stmt|insert into "1".no_pk (a,b) values (null,null)|]
|
||||||
|
stmts = map [H.stmt|insert into "1".no_pk (a,b) values (?,0)|] [1..n]
|
||||||
|
|
||||||
|
createLikableStrings :: IO ()
|
||||||
|
createLikableStrings = do
|
||||||
|
pool <- H.acquirePool pgSettings testPoolOpts
|
||||||
|
void . liftIO $ H.session pool $ H.tx Nothing $ do
|
||||||
|
H.unitEx $ insertSimplePk "xyyx" "u"
|
||||||
|
H.unitEx $ insertSimplePk "xYYx" "v"
|
||||||
|
where
|
||||||
|
insertSimplePk :: Text -> Text -> H.Stmt H.Postgres
|
||||||
|
insertSimplePk = [H.stmt|insert into "1".simple_pk (k, extra) values (?,?)|]
|
||||||
|
|
||||||
|
|
||||||
-- for hspec-wai
|
-- for hspec-wai
|
||||||
pending_ :: WaiSession ()
|
pending_ :: WaiSession ()
|
||||||
pending_ = liftIO Test.Hspec.pending
|
pending_ = liftIO Test.Hspec.pending
|
||||||
|
|||||||
Reference in New Issue
Block a user