diff --git a/CHANGELOG.md b/CHANGELOG.md index 54469fba3..ac5a8f439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Filter columns, e.g. `?select=col1,col2` - @ruslantalpa +- Does not execute the count total if header "Prefer: count=none" - @diogob ## [0.2.11.1] - 2015-09-01 diff --git a/postgrest.cabal b/postgrest.cabal index d623df7aa..bae11bca3 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -40,7 +40,7 @@ executable postgrest , bytestring, text, split, string-conversions , stringsearch , containers, unordered-containers - , optparse-applicative == 0.11.* + , optparse-applicative >= 0.11 && < 0.13 , regex-base, regex-tdfa , Ranged-sets , transformers, MissingH diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index b4df142fd..0ca42482d 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -5,7 +5,7 @@ import Control.Monad (join) import Control.Arrow ((***), second) import Control.Applicative -import Data.Text hiding (map, find) +import Data.Text (Text) import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing) import Text.Regex.TDFA ((=~)) import Data.Ord (comparing) @@ -64,9 +64,12 @@ app conf reqBody req = else do let qt = qualify table from = fromMaybe 0 $ rangeOffset <$> range + count = if hasPrefer "count=none" + then countNone + else countRows qt query = B.Stmt "select " V.empty True <> parentheticT ( - whereT qt qq $ countRows qt + whereT qt qq count ) <> commaq <> ( bodyForAccept contentType qt . limitT range @@ -76,7 +79,7 @@ app conf reqBody req = ) row <- H.maybeEx query let (tableTotal, queryTotal, body) = - fromMaybe (0, 0, Just "" :: Maybe Text) row + fromMaybe (Just 0, 0, Just "" :: Maybe Text) row to = from+queryTotal-1 contentRange = contentRangeH from to tableTotal status = rangeStatus from to tableTotal @@ -129,7 +132,7 @@ app conf reqBody req = ([table], "POST") -> do let qt = qualify table - echoRequested = lookupHeader "Prefer" == Just "return=representation" + echoRequested = hasPrefer "return=representation" parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value)) parsed = if lookupHeader "Content-Type" == Just csvMT then do @@ -215,8 +218,8 @@ app conf reqBody req = row <- H.maybeEx patch let (queryTotal, body) = fromMaybe (0 :: Int, Just "" :: Maybe Text) row - r = contentRangeH 0 (queryTotal-1) queryTotal - echoRequested = lookupHeader "Prefer" == Just "return=representation" + r = contentRangeH 0 (queryTotal-1) (Just queryTotal) + echoRequested = hasPrefer "return=representation" s = case () of _ | queryTotal == 0 -> status404 | echoRequested -> status200 | otherwise -> status204 @@ -244,6 +247,7 @@ app conf reqBody req = qualify = QualifiedIdentifier schema hdrs = requestHeaders req lookupHeader = flip lookup hdrs + hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs accept = lookupHeader hAccept schema = requestedSchema (cs $ configV1Schema conf) accept authenticator = cs $ configDbUser conf @@ -259,21 +263,24 @@ sqlError = undefined isSqlError :: t isSqlError = undefined -rangeStatus :: Int -> Int -> Int -> Status -rangeStatus from to total +rangeStatus :: Int -> Int -> Maybe Int -> Status +rangeStatus _ _ Nothing = status200 +rangeStatus from to (Just total) | from > total = status416 | (1 + to - from) < total = status206 | otherwise = status200 -contentRangeH :: Int -> Int -> Int -> Header +contentRangeH :: Int -> Int -> Maybe Int -> Header contentRangeH from to total = - ("Content-Range", - if total == 0 || from > total - then "*/" <> cs (show total) - else cs (show from) - <> "-" <> cs (show to) - <> "/" <> cs (show total) - ) + ("Content-Range", cs headerValue) + where + headerValue = rangeString <> "/" <> totalString + rangeString + | totalNotZero && fromInRange = show from <> "-" <> cs (show to) + | otherwise = "*" + totalString = fromMaybe "*" (show <$> total) + totalNotZero = fromMaybe True ((/=) 0 <$> total) + fromInRange = from <= to requestedSchema :: Text -> Maybe BS.ByteString -> Text requestedSchema v1schema accept = diff --git a/src/PostgREST/PgQuery.hs b/src/PostgREST/PgQuery.hs index 9e6c8f25b..2ef65af21 100644 --- a/src/PostgREST/PgQuery.hs +++ b/src/PostgREST/PgQuery.hs @@ -101,6 +101,9 @@ countT s = countRows :: QualifiedIdentifier -> PStmt countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQi t) empty True +countNone :: PStmt +countNone = B.Stmt "select null" empty True + asCsvWithCount :: QualifiedIdentifier -> StatementT asCsvWithCount table = withCount . asCsv table diff --git a/test/Feature/RangeSpec.hs b/test/Feature/RangeSpec.hs index 311db4b37..3d3641279 100644 --- a/test/Feature/RangeSpec.hs +++ b/test/Feature/RangeSpec.hs @@ -2,6 +2,7 @@ module Feature.RangeSpec where import Test.Hspec import Test.Hspec.Wai +import Test.Hspec.Wai.JSON import Network.HTTP.Types import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus)) @@ -12,11 +13,30 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable . around withApp $ describe "GET /items" $ do - context "without range headers" $ + context "without range headers" $ do context "with response under server size limit" $ it "returns whole range with status 200" $ get "/items" `shouldRespondWith` 200 + context "when I don't want the count" $ do + it "returns range Content-Range with /*" $ + request methodGet "/menagerie" + [("Prefer", "count=none")] "" + `shouldRespondWith` ResponseMatcher { + matchBody = Just "[]" + , matchStatus = 200 + , matchHeaders = ["Content-Range" <:> "*/*"] + } + + it "returns range Content-Range with range/*" $ + request methodGet "/items?order=id" + [("Prefer", "count=none")] "" + `shouldRespondWith` ResponseMatcher { + matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |] + , matchStatus = 200 + , matchHeaders = ["Content-Range" <:> "0-14/*"] + } + context "with range headers" $ do context "of acceptable range" $ do