Merge pull request #404 from begriffs/max-rows

Add --max-rows option
This commit is contained in:
Joe Nelson
2015-12-06 15:57:37 -08:00
17 changed files with 98 additions and 37 deletions
+1
View File
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added ### Added
- Allow order by computed columns - @diogob - Allow order by computed columns - @diogob
- Set max rows in response with --max-rows - @begriffs
## [0.3.0.1] - 2015-11-27 ## [0.3.0.1] - 2015-11-27
+4
View File
@@ -92,6 +92,10 @@ The possible flags are:
<dd>Max connections to use in db pool. Defaults to to 10, but you <dd>Max connections to use in db pool. Defaults to to 10, but you
should find an optimal value for your db by running the SQL should find an optimal value for your db by running the SQL
command <code>show max_connections;</code></dd> command <code>show max_connections;</code></dd>
<dt>-m, --max-rows</dt>
<dd>Max number of rows to return in a read request. The default is
no limit.</dd>
</dl> </dl>
<div class="admonition note"> <div class="admonition note">
+3
View File
@@ -45,6 +45,7 @@ executable postgrest
, parsec , parsec
, postgrest , postgrest
, regex-tdfa , regex-tdfa
, safe >= 0.3 && < 0.4
, scientific , scientific
, string-conversions , string-conversions
, text , text
@@ -98,6 +99,7 @@ library
, optparse-applicative , optparse-applicative
, parsec , parsec
, regex-tdfa , regex-tdfa
, safe
, scientific , scientific
, string-conversions , string-conversions
, text , text
@@ -181,6 +183,7 @@ Test-Suite spec
, parsec , parsec
, process , process
, regex-tdfa , regex-tdfa
, safe
, scientific , scientific
, string-conversions , string-conversions
, text , text
+2 -2
View File
@@ -51,7 +51,7 @@ data ApiRequest = ApiRequest {
-- | Set to Nothing for unknown HTTP verbs -- | Set to Nothing for unknown HTTP verbs
iAction :: Action iAction :: Action
-- | Set to Nothing for malformed range -- | Set to Nothing for malformed range
, iRange :: Maybe NonnegRange , iRange :: NonnegRange
-- | Set to Nothing for strangely nested urls -- | Set to Nothing for strangely nested urls
, iTarget :: Target , iTarget :: Target
-- | The content type the client most desires (or JSON if undecided) -- | The content type the client most desires (or JSON if undecided)
@@ -115,7 +115,7 @@ userApiRequest schema req reqBody =
ApiRequest { ApiRequest {
iAction = action iAction = action
, iRange = if singular then Just (singletonRange 0) else rangeRequested hdrs , iRange = if singular then singletonRange 0 else rangeRequested hdrs
, iTarget = target , iTarget = target
, iAccepts = pickContentType $ lookupHeader "accept" , iAccepts = pickContentType $ lookupHeader "accept"
, iPayload = relevantPayload , iPayload = relevantPayload
+3 -3
View File
@@ -73,11 +73,11 @@ app dbStructure conf reqBody req =
case selectQuery of case selectQuery of
Left e -> return $ responseLBS status400 [jsonH] $ cs e Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right q -> do Right q -> do
let range = iRange apiRequest let range = restrictRange (configMaxRows conf) $ iRange apiRequest
singular = iPreferSingular apiRequest singular = iPreferSingular apiRequest
stm = createReadStatement q range singular stm = createReadStatement q range singular
(iPreferCount apiRequest) (contentType == TextCSV) (iPreferCount apiRequest) (contentType == TextCSV)
if range == Just emptyRange if range == emptyRange
then return $ errResponse status416 "HTTP Range error" then return $ errResponse status416 "HTTP Range error"
else do else do
row <- H.maybeEx stm row <- H.maybeEx stm
@@ -87,7 +87,7 @@ app dbStructure conf reqBody req =
then responseLBS status404 [] "" then responseLBS status404 [] ""
else responseLBS status200 [contentTypeH] (fromMaybe "{}" body) else responseLBS status200 [contentTypeH] (fromMaybe "{}" body)
else do else do
let frm = fromMaybe 0 $ rangeOffset <$> range let frm = rangeOffset range
to = frm+queryTotal-1 to = frm+queryTotal-1
contentRange = contentRangeH frm to tableTotal contentRange = contentRangeH frm to tableTotal
status = rangeStatus frm to tableTotal status = rangeStatus frm to tableTotal
+3
View File
@@ -30,6 +30,7 @@ import Network.Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..)) import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative import Options.Applicative
import Paths_postgrest (version) import Paths_postgrest (version)
import Safe (readMay)
import Web.JWT (Secret, secret) import Web.JWT (Secret, secret)
import Prelude import Prelude
@@ -41,6 +42,7 @@ data AppConfig = AppConfig {
, configSchema :: String , configSchema :: String
, configJwtSecret :: Secret , configJwtSecret :: Secret
, configPool :: Int , configPool :: Int
, configMaxRows :: Maybe Int
} }
argParser :: Parser AppConfig argParser :: Parser AppConfig
@@ -53,6 +55,7 @@ argParser = AppConfig
<*> (secret . cs <$> <*> (secret . cs <$>
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault)) strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault) <*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
defaultCorsPolicy :: CorsResourcePolicy defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing defaultCorsPolicy = CorsResourcePolicy Nothing
+16 -9
View File
@@ -34,7 +34,6 @@ import qualified Data.Aeson as JSON
import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset) import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset)
import Control.Error (note, fromMaybe, mapMaybe) import Control.Error (note, fromMaybe, mapMaybe)
import Control.Monad (join)
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.List (find) import Data.List (find)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
@@ -61,10 +60,10 @@ instance Monoid PStmt where
mempty = B.Stmt "" empty True mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt type StatementT = PStmt -> PStmt
createReadStatement :: SqlQuery -> Maybe NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres createReadStatement :: SqlQuery -> NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
createReadStatement selectQuery range isSingle countTable asCsv = createReadStatement selectQuery range isSingle countTable asCsv =
B.Stmt ( B.Stmt (
wrapQuery selectQuery [ wrapLimitedQuery selectQuery [
if countTable then countAllF else countNoneF, if countTable then countAllF else countNoneF,
countF, countF,
"null", -- location header can not be calucalted "null", -- location header can not be calucalted
@@ -91,7 +90,7 @@ createWriteStatement selectQuery mutateQuery isSingle echoRequested
else if isSingle then asJsonSingleF else asJsonF else if isSingle then asJsonSingleF else asJsonF
else "null" else "null"
] selectQuery Nothing ] selectQuery
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True ) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest
@@ -426,19 +425,27 @@ withSourceF s = "WITH " <> sourceSubqueryName <> " AS (" <> s <>")"
fromF :: SqlFragment -> SqlFragment -> SqlFragment fromF :: SqlFragment -> SqlFragment -> SqlFragment
fromF sel limit = "FROM (" <> sel <> " " <> limit <> ") t" fromF sel limit = "FROM (" <> sel <> " " <> limit <> ") t"
limitF :: Maybe NonnegRange -> SqlFragment limitF :: NonnegRange -> SqlFragment
limitF r = "LIMIT " <> limit <> " OFFSET " <> offset limitF r = "LIMIT " <> limit <> " OFFSET " <> offset
where where
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r limit = maybe "ALL" (cs . show) $ rangeLimit r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r offset = cs . show $ rangeOffset r
selectStarF :: SqlFragment selectStarF :: SqlFragment
selectStarF = "SELECT * FROM " <> sourceSubqueryName selectStarF = "SELECT * FROM " <> sourceSubqueryName
wrapQuery :: SqlQuery -> [Text] -> Text -> Maybe NonnegRange -> SqlQuery wrapLimitedQuery :: SqlQuery -> [Text] -> Text -> NonnegRange -> SqlQuery
wrapQuery source selectColumns returnSelect range = wrapLimitedQuery source selectColumns returnSelect range =
withSourceF source <> withSourceF source <>
" SELECT " <> " SELECT " <>
intercalate ", " selectColumns <> intercalate ", " selectColumns <>
" " <> " " <>
fromF returnSelect ( limitF range ) fromF returnSelect ( limitF range )
wrapQuery :: SqlQuery -> [Text] -> Text -> SqlQuery
wrapQuery source selectColumns returnSelect =
withSourceF source <>
" SELECT " <>
intercalate ", " selectColumns <>
" " <>
fromF returnSelect ""
+15 -8
View File
@@ -3,6 +3,7 @@ module PostgREST.RangeQuery (
, rangeRequested , rangeRequested
, rangeLimit , rangeLimit
, rangeOffset , rangeOffset
, restrictRange
, NonnegRange , NonnegRange
) where ) where
@@ -25,20 +26,26 @@ import Prelude
type NonnegRange = Range Int type NonnegRange = Range Int
rangeParse :: BS.ByteString -> Maybe NonnegRange rangeParse :: BS.ByteString -> NonnegRange
rangeParse range = do rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
parsedRange <- listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
Just parsedRange ->
let [_, from, to] = readMaybe . cs <$> parsedRange let [_, from, to] = readMaybe . cs <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from) lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to) upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to) in
rangeIntersection lower upper
Nothing -> rangeGeq 0
return $ rangeIntersection lower upper rangeRequested :: RequestHeaders -> NonnegRange
rangeRequested = rangeParse . fromMaybe "" . lookup hRange
rangeRequested :: RequestHeaders -> Maybe NonnegRange restrictRange :: Maybe Int -> NonnegRange -> NonnegRange
rangeRequested = (rangeParse =<<) . lookup hRange restrictRange Nothing r = r
restrictRange (Just limit) r =
rangeIntersection r $
Range BoundaryBelowAll (BoundaryAbove $ rangeOffset r + limit - 1)
rangeLimit :: NonnegRange -> Maybe Int rangeLimit :: NonnegRange -> Maybe Int
rangeLimit range = rangeLimit range =
+1 -1
View File
@@ -12,7 +12,7 @@ import SpecHelper
spec :: Spec spec :: Spec
spec = beforeAll spec = beforeAll
(clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth") (clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth")
$ around withApp $ around (withApp cfgDefault)
$ describe "authorization" $ do $ describe "authorization" $ do
it "hides tables that anonymous does not own" $ it "hides tables that anonymous does not own" $
+1 -1
View File
@@ -12,7 +12,7 @@ import Network.HTTP.Types
-- }}} -- }}}
spec :: Spec spec :: Spec
spec = around withApp $ describe "CORS" $ do spec = around (withApp cfgDefault) $ describe "CORS" $ do
let preflightHeaders = [ let preflightHeaders = [
("Accept", "*/*"), ("Accept", "*/*"),
("Origin", "http://example.com"), ("Origin", "http://example.com"),
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
spec :: Spec spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items") spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $ . around (withApp cfgDefault) $
describe "Deleting" $ do describe "Deleting" $ do
context "existing record" $ do context "existing record" $ do
it "succeeds with 204 and deletion count" $ it "succeeds with 204 and deletion count" $
+1 -1
View File
@@ -17,7 +17,7 @@ import Control.Monad (replicateM_)
import TestTypes(IncPK(..), CompoundPK(..)) import TestTypes(IncPK(..), CompoundPK(..))
spec :: Spec spec :: Spec
spec = afterAll_ resetDb $ around withApp $ do spec = afterAll_ resetDb $ around (withApp cfgDefault) $ do
describe "Posting new record" $ do describe "Posting new record" $ do
after_ (clearTable "menagerie") . context "disparate csv types" $ do after_ (clearTable "menagerie") . context "disparate csv types" $ do
it "accepts disparate json types" $ do it "accepts disparate json types" $ do
+31
View File
@@ -0,0 +1,31 @@
module Feature.QueryLimitedSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
import SpecHelper
spec :: Spec
spec =
beforeAll (clearTable "items" >> createItems 15)
. afterAll_ (clearTable "items")
. around (withApp $ cfgLimitRows 3) $
describe "Requesting many items with server limits enabled" $ do
it "restricts results" $
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3}] |]
, matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-2/15"]
}
it "respects additional client limiting" $ do
r <- request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 0 1) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/15"
simpleStatus r `shouldBe` partialContent206
+1 -1
View File
@@ -22,7 +22,7 @@ spec =
createLikableStrings >> createLikableStrings >>
createJsonData) createJsonData)
. afterAll_ (clearTable "items" >> clearTable "complex_items" >> clearTable "no_pk" >> clearTable "simple_pk") . afterAll_ (clearTable "items" >> clearTable "complex_items" >> clearTable "no_pk" >> clearTable "simple_pk")
. around withApp $ do . around (withApp cfgDefault) $ do
describe "Querying a table with a column called count" $ describe "Querying a table with a column called count" $
it "should not confuse count column with pg_catalog.count aggregate" $ it "should not confuse count column with pg_catalog.count aggregate" $
+1 -1
View File
@@ -10,7 +10,7 @@ import SpecHelper
spec :: Spec spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items") spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $ . around (withApp cfgDefault) $
describe "GET /items" $ do describe "GET /items" $ do
context "without range headers" $ do context "without range headers" $ do
+1 -1
View File
@@ -9,7 +9,7 @@ import SpecHelper
import Network.HTTP.Types import Network.HTTP.Types
spec :: Spec spec :: Spec
spec = around withApp $ do spec = around (withApp cfgDefault) $ do
describe "GET /" $ do describe "GET /" $ do
it "lists views in schema" $ it "lists views in schema" $
request methodGet "/" [] "" request methodGet "/" [] ""
+11 -6
View File
@@ -41,8 +41,13 @@ isLeft :: Either a b -> Bool
isLeft (Left _ ) = True isLeft (Left _ ) = True
isLeft _ = False isLeft _ = False
cfg :: AppConfig cfgDefault :: AppConfig
cfg = AppConfig dbString 3000 "postgrest_anonymous" "test" (secret "safe") 10 cfgDefault = AppConfig dbString 3000 "postgrest_anonymous" "test" (secret "safe") 10 Nothing
cfgLimitRows :: Int -> AppConfig
cfgLimitRows limit =
AppConfig dbString 3000 "postgrest_anonymous" "test"
(secret "safe") 10 (Just limit)
testPoolOpts :: PoolSettings testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30 testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
@@ -50,20 +55,20 @@ testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
pgSettings :: P.Settings pgSettings :: P.Settings
pgSettings = P.StringSettings $ cs dbString pgSettings = P.StringSettings $ cs dbString
withApp :: ActionWith Application -> IO () withApp :: AppConfig -> ActionWith Application -> IO ()
withApp perform = do withApp config perform = do
pool :: H.Pool P.Postgres pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings testPoolOpts <- H.acquirePool pgSettings testPoolOpts
let txSettings = Just (H.ReadCommitted, Just True) let txSettings = Just (H.ReadCommitted, Just True)
dbOrError <- H.session pool $ H.tx txSettings $ getDbStructure (cs $ configSchema cfg) dbOrError <- H.session pool $ H.tx txSettings $ getDbStructure (cs $ configSchema config)
db <- either (fail . show) return dbOrError db <- either (fail . show) return dbOrError
perform $ middle $ \req resp -> do perform $ middle $ \req resp -> do
time <- getPOSIXTime time <- getPOSIXTime
body <- strictRequestBody req body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx txSettings result <- liftIO $ H.session pool $ H.tx txSettings
$ runWithClaims cfg time (app db cfg body) req $ runWithClaims config time (app db config body) req
either (resp . pgErrResponse) resp result either (resp . pgErrResponse) resp result
where middle = defaultMiddle where middle = defaultMiddle