Add support for Prefer: count=planned/estimated on GET /table (#1386)
This commit is contained in:
@@ -13,7 +13,7 @@ import SpecHelper
|
||||
|
||||
spec :: SpecWith Application
|
||||
spec =
|
||||
describe "Requesting many items with server limits enabled" $ do
|
||||
describe "Requesting many items with server limits(max-rows) enabled" $ do
|
||||
it "restricts results" $
|
||||
get "/items"
|
||||
`shouldRespondWith` [json| [{"id":1},{"id":2}] |]
|
||||
@@ -29,16 +29,38 @@ spec =
|
||||
matchHeader "Content-Range" "0-0/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
it "limit works on all levels" $
|
||||
it "works on all levels" $
|
||||
get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc"
|
||||
`shouldRespondWith` [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "limit is not applied to parent embeds" $
|
||||
it "is not applied to parent embeds" $
|
||||
get "/tasks?select=id,project(id)&id=gt.5"
|
||||
`shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
context "count=estimated" $ do
|
||||
it "uses the query planner guess when query rows > maxRows" $
|
||||
request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
|
||||
}
|
||||
|
||||
it "gives exact count when query rows <= maxRows" $
|
||||
request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
}
|
||||
|
||||
it "only uses the query planner guess if it's indeed greater than the exact count" $
|
||||
request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
||||
}
|
||||
|
||||
@@ -197,6 +197,65 @@ spec = do
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
context "when count=planned" $ do
|
||||
it "obtains a filtered range" $ do
|
||||
request methodGet "/items?select=id&id=gt.8" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-6/8"]
|
||||
}
|
||||
request methodGet "/child_entities?select=id&id=gt.3" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` [json|[{"id":4}, {"id":5}, {"id":6}]|]
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/4"]
|
||||
}
|
||||
request methodGet "/getallprojects_view?select=id&id=lt.3" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` [json|[{"id":1}, {"id":2}]|]
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/673"]
|
||||
}
|
||||
|
||||
it "obtains the full range" $ do
|
||||
request methodHead "/items" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-14/15"]
|
||||
}
|
||||
request methodHead "/child_entities" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-5/6"]
|
||||
}
|
||||
request methodHead "/getallprojects_view" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-4/2019"]
|
||||
}
|
||||
|
||||
it "ignores limit/offset on the planned count" $ do
|
||||
request methodHead "/items?limit=2&offset=3" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "3-4/15"]
|
||||
}
|
||||
request methodHead "/child_entities?limit=2" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/6"]
|
||||
}
|
||||
request methodHead "/getallprojects_view?limit=2" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
|
||||
}
|
||||
|
||||
it "works with two levels" $
|
||||
request methodHead "/child_entities?select=*,entities(*)" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-5/6"]
|
||||
}
|
||||
|
||||
context "with range headers" $ do
|
||||
context "of acceptable range" $ do
|
||||
it "succeeds with partial content" $ do
|
||||
|
||||
+10
-4
@@ -65,7 +65,7 @@ main = do
|
||||
refDbStructure <- newIORef $ Just dbStructure
|
||||
|
||||
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
maxRowsApp = return $ postgrest (testMaxRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
@@ -78,8 +78,11 @@ main = do
|
||||
rootSpecApp = return $ postgrest (testCfgRootSpec testDbConn) refDbStructure pool getTime $ pure ()
|
||||
htmlRawOutputApp = return $ postgrest (testCfgHtmlRawOutput testDbConn) refDbStructure pool getTime $ pure ()
|
||||
|
||||
let reset :: IO ()
|
||||
let reset, analyze :: IO ()
|
||||
reset = resetDb testDbConn
|
||||
analyze = do
|
||||
analyzeTable testDbConn "items"
|
||||
analyzeTable testDbConn "child_entities"
|
||||
|
||||
actualPgVersion = pgVersion dbStructure
|
||||
extraSpecs =
|
||||
@@ -95,7 +98,6 @@ main = do
|
||||
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
|
||||
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
|
||||
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
|
||||
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
||||
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
|
||||
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion)
|
||||
] ++ extraSpecs
|
||||
@@ -112,12 +114,16 @@ main = do
|
||||
|
||||
mapM_ (before withApp) specs
|
||||
|
||||
-- we analyze to get accurate results from EXPLAIN
|
||||
beforeAll_ analyze . before withApp $
|
||||
describe "Feature.RangeSpec" Feature.RangeSpec.spec
|
||||
|
||||
-- this test runs with a raw-output-media-types set to text/html
|
||||
before htmlRawOutputApp $
|
||||
describe "Feature.HtmlRawOutputSpec" Feature.HtmlRawOutputSpec.spec
|
||||
|
||||
-- this test runs with a different server flag
|
||||
before ltdApp $
|
||||
before maxRowsApp $
|
||||
describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec
|
||||
|
||||
-- this test runs with a different schema
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ main = do
|
||||
cost <- exec pool mempty $
|
||||
requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing
|
||||
liftIO $
|
||||
cost `shouldSatisfy` (< Just 20)
|
||||
cost `shouldSatisfy` (< Just 30)
|
||||
|
||||
it "should not exceed cost when calling scalar proc" $ do
|
||||
cost <- exec pool [str| {"a": 3, "b": 4} |] $
|
||||
|
||||
+6
-2
@@ -93,8 +93,8 @@ testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing }
|
||||
testUnicodeCfg :: Text -> AppConfig
|
||||
testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchema = "تست" }
|
||||
|
||||
testLtdRowsCfg :: Text -> AppConfig
|
||||
testLtdRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 }
|
||||
testMaxRowsCfg :: Text -> AppConfig
|
||||
testMaxRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 }
|
||||
|
||||
testProxyCfg :: Text -> AppConfig
|
||||
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
|
||||
@@ -149,6 +149,10 @@ setupDb dbConn = do
|
||||
resetDb :: Text -> IO ()
|
||||
resetDb dbConn = loadFixture dbConn "data"
|
||||
|
||||
analyzeTable :: Text -> Text -> IO ()
|
||||
analyzeTable dbConn tableName =
|
||||
void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []
|
||||
|
||||
loadFixture :: Text -> FilePath -> IO()
|
||||
loadFixture dbConn name =
|
||||
void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", toS dbConn, "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
|
||||
|
||||
Vendored
+2
@@ -101,6 +101,8 @@ GRANT ALL ON TABLE
|
||||
, pgrst_reserved_chars
|
||||
, authors_w_entities
|
||||
, openapi_types
|
||||
, getallprojects_view
|
||||
, get_projects_above_view
|
||||
TO postgrest_test_anonymous;
|
||||
|
||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||
|
||||
Vendored
+13
-1
@@ -1096,11 +1096,17 @@ CREATE FUNCTION get_projects_below(id int) RETURNS SETOF projects
|
||||
SELECT * FROM test.projects WHERE id < $1;
|
||||
$_$;
|
||||
|
||||
CREATE FUNCTION get_projects_above(id int) RETURNS SETOF projects
|
||||
LANGUAGE sql
|
||||
AS $_$
|
||||
SELECT * FROM test.projects WHERE id > $1;
|
||||
$_$ ROWS 1;
|
||||
|
||||
CREATE FUNCTION getallprojects() RETURNS SETOF projects
|
||||
LANGUAGE sql
|
||||
AS $_$
|
||||
SELECT * FROM test.projects;
|
||||
$_$;
|
||||
$_$ ROWS 2019;
|
||||
|
||||
CREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF projects
|
||||
LANGUAGE sql
|
||||
@@ -1730,3 +1736,9 @@ select $$
|
||||
</html>
|
||||
$$::text;
|
||||
$_$ language sql;
|
||||
|
||||
create view getallprojects_view as
|
||||
select * from getallprojects();
|
||||
|
||||
create view get_projects_above_view as
|
||||
select * from get_projects_above(1);
|
||||
|
||||
Reference in New Issue
Block a user