Faster queries using json_agg

This commit is contained in:
Ruslan Talpa
2017-12-12 15:44:50 +02:00
committed by GitHub
parent e4183780a9
commit effbec234f
8 changed files with 81 additions and 49 deletions
+1
View File
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #917, Add ability to map RAISE errorcode/message to http status - @steve-chavez - #917, Add ability to map RAISE errorcode/message to http status - @steve-chavez
- #940, Add ability to map GUC to http response headers - @steve-chavez - #940, Add ability to map GUC to http response headers - @steve-chavez
- #1022, Include git sha in version report - @begriffs - #1022, Include git sha in version report - @begriffs
- Faster queries using json_agg - @ruslantalpa
### Fixed ### Fixed
+3 -3
View File
@@ -249,7 +249,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}}, alias, _)) forst) (j,s) = (j,sel:s) getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}}, alias, _)) forst) (j,s) = (j,sel:s)
where where
sel = "COALESCE((" sel = "COALESCE(("
<> "SELECT array_to_json(array_agg(row_to_json(" <> pgFmtIdent table <> ".*))) " <> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table <> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = requestToQuery schema False (DbRead (Node n forst)) where subquery = requestToQuery schema False (DbRead (Node n forst))
@@ -263,7 +263,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias, _)) forst) (j,s) = (j,sel:s) getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias, _)) forst) (j,s) = (j,sel:s)
where where
sel = "COALESCE ((" sel = "COALESCE (("
<> "SELECT array_to_json(array_agg(row_to_json(" <> pgFmtIdent table <> ".*))) " <> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table <> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = requestToQuery schema False (DbRead (Node n forst)) where subquery = requestToQuery schema False (DbRead (Node n forst))
@@ -336,7 +336,7 @@ asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')" asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
asJsonF :: SqlFragment asJsonF :: SqlFragment
asJsonF = "coalesce(array_to_json(array_agg(row_to_json(_postgrest_t))), '[]')::character varying" asJsonF = "coalesce(json_agg(_postgrest_t), '[]')::character varying"
asJsonSingleF :: SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element asJsonSingleF :: SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element
asJsonSingleF = "coalesce(string_agg(row_to_json(_postgrest_t)::text, ','), '')::character varying " asJsonSingleF = "coalesce(string_agg(row_to_json(_postgrest_t)::text, ','), '')::character varying "
+6 -6
View File
@@ -423,9 +423,9 @@ spec = do
matchStatus = 204, matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "*/*"]
} }
get "/items" `shouldRespondWith`
g <- get "/items" [json|[{"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},{id:16},{"id":2},{"id":1}]|]
liftIO $ simpleBody g `shouldBe` [json| [{"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},{id:16},{"id":2},{"id":1}] |] { matchHeaders = [matchContentTypeJson] }
it "makes no updates and and returns 200, when patching with an empty json object and return=rep" $ do it "makes no updates and and returns 200, when patching with an empty json object and return=rep" $ do
request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |] request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]
@@ -434,9 +434,9 @@ spec = do
matchStatus = 200, matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "*/*"]
} }
get "/items" `shouldRespondWith`
g <- get "/items" [json| [{"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},{id:16},{"id":2},{"id":1}] |]
liftIO $ simpleBody g `shouldBe` [json| [{"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},{id:16},{"id":2},{"id":1}] |] { matchHeaders = [matchContentTypeJson] }
context "with unicode values" $ context "with unicode values" $
it "succeeds and returns values intact" $ do it "succeeds and returns values intact" $ do
+2 -3
View File
@@ -5,7 +5,6 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus)) import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
import Text.Heredoc
import SpecHelper import SpecHelper
import Network.Wai (Application) import Network.Wai (Application)
@@ -31,14 +30,14 @@ spec =
it "limit works on all levels" $ it "limit works on all levels" $
get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc" get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc"
`shouldRespondWith` [str|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|] `shouldRespondWith` [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
it "limit is not applied to parent embeds" $ it "limit is not applied to parent embeds" $
get "/tasks?select=id,project{id}&id=gt.5" get "/tasks?select=id,project{id}&id=gt.5"
`shouldRespondWith` [str|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|] `shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
+57 -27
View File
@@ -85,9 +85,11 @@ spec = do
it "matches with ilike" $ do it "matches with ilike" $ do
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith` get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|] [json|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith` get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|] [json|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "matches with ilike using not operator" $ it "matches with ilike using not operator" $
get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]" get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"
@@ -178,21 +180,26 @@ spec = do
it "matches filtering nested items" $ it "matches filtering nested items" $
get "/clients?select=id,projects{id,tasks{id,name}}&projects.tasks.name=like.Design*" `shouldRespondWith` get "/clients?select=id,projects{id,tasks{id,name}}&projects.tasks.name=like.Design*" `shouldRespondWith`
[str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|] [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "matches with cs operator" $ do it "matches with cs operator" $ do
get "/complex_items?select=id&arr_data=cs.{2}" `shouldRespondWith` get "/complex_items?select=id&arr_data=cs.{2}" `shouldRespondWith`
[str|[{"id":2},{"id":3}]|] [json|[{"id":2},{"id":3}]|]
{ matchHeaders = [matchContentTypeJson] }
-- TODO: remove in 0.5.0 as deprecated -- TODO: remove in 0.5.0 as deprecated
get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith` get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith`
[str|[{"id":2},{"id":3}]|] [json|[{"id":2},{"id":3}]|]
{ matchHeaders = [matchContentTypeJson] }
it "matches with cd operator" $ do it "matches with cd operator" $ do
get "/complex_items?select=id&arr_data=cd.{1,2,4}" `shouldRespondWith` get "/complex_items?select=id&arr_data=cd.{1,2,4}" `shouldRespondWith`
[str|[{"id":1},{"id":2}]|] [json|[{"id":1},{"id":2}]|]
{ matchHeaders = [matchContentTypeJson] }
-- TODO: remove in 0.5.0 as deprecated -- TODO: remove in 0.5.0 as deprecated
get "/complex_items?select=id&arr_data=<@.{1,2,4}" `shouldRespondWith` get "/complex_items?select=id&arr_data=<@.{1,2,4}" `shouldRespondWith`
[str|[{"id":1},{"id":2}]|] [json|[{"id":1},{"id":2}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "Shaping response with select parameter" $ do describe "Shaping response with select parameter" $ do
@@ -279,7 +286,8 @@ spec = do
it "requesting parents and children" $ it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parent without specifying primary key" $ do it "requesting parent without specifying primary key" $ do
get "/projects?select=name,client{name}" `shouldRespondWith` get "/projects?select=name,client{name}" `shouldRespondWith`
@@ -321,7 +329,8 @@ spec = do
it "requesting parents and children while renaming them" $ it "requesting parents and children while renaming them" $
get "/projects?id=eq.1&select=myId:id, name, project_client:client_id{*}, project_tasks:tasks{id, name}" `shouldRespondWith` get "/projects?id=eq.1&select=myId:id, name, project_client:client_id{*}, project_tasks:tasks{id, name}" `shouldRespondWith`
[str|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parents two levels up while using FK to specify the link" $ it "requesting parents two levels up while using FK to specify the link" $
get "/tasks?id=eq.1&select=id,name,project:project_id{id,name,client:client_id{id,name}}" `shouldRespondWith` get "/tasks?id=eq.1&select=id,name,project:project_id{id,name,client:client_id{id,name}}" `shouldRespondWith`
@@ -338,7 +347,8 @@ spec = do
it "rows with missing parents are included" $ it "rows with missing parents are included" $
get "/projects?id=in.1,5&select=id,clients{id}" `shouldRespondWith` get "/projects?id=in.1,5&select=id,clients{id}" `shouldRespondWith`
[str|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|] [json|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|]
{ matchHeaders = [matchContentTypeJson] }
it "rows with no children return [] instead of null" $ it "rows with no children return [] instead of null" $
get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith` get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith`
@@ -346,53 +356,70 @@ spec = do
it "requesting children 2 levels" $ it "requesting children 2 levels" $
get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith` get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith`
[str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|] [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting children 2 levels (with relation path fixed)" $ it "requesting children 2 levels (with relation path fixed)" $
get "/clients?id=eq.1&select=id,projects:projects.client_id{id,tasks{id}}" `shouldRespondWith` get "/clients?id=eq.1&select=id,projects:projects.client_id{id,tasks{id}}" `shouldRespondWith`
[str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|] [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting many<->many relation" $ it "requesting many<->many relation" $
get "/tasks?select=id,users{id}" `shouldRespondWith` get "/tasks?select=id,users{id}" `shouldRespondWith`
[str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] [json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting many<->many relation (with relation path fixed)" $ it "requesting many<->many relation (with relation path fixed)" $
get "/tasks?select=id,users:users.users_tasks{id}" `shouldRespondWith` get "/tasks?select=id,users:users.users_tasks{id}" `shouldRespondWith`
[str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] [json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting many<->many relation with rename" $ it "requesting many<->many relation with rename" $
get "/tasks?id=eq.1&select=id,theUsers:users{id}" `shouldRespondWith` get "/tasks?id=eq.1&select=id,theUsers:users{id}" `shouldRespondWith`
[str|[{"id":1,"theUsers":[{"id":1},{"id":3}]}]|] [json|[{"id":1,"theUsers":[{"id":1},{"id":3}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting many<->many relation reverse" $ it "requesting many<->many relation reverse" $
get "/users?select=id,tasks{id}" `shouldRespondWith` get "/users?select=id,tasks{id}" `shouldRespondWith`
[str|[{"id":1,"tasks":[{"id":1},{"id":2},{"id":3},{"id":4}]},{"id":2,"tasks":[{"id":5},{"id":6},{"id":7}]},{"id":3,"tasks":[{"id":1},{"id":5}]}]|] [json|[{"id":1,"tasks":[{"id":1},{"id":2},{"id":3},{"id":4}]},{"id":2,"tasks":[{"id":5},{"id":6},{"id":7}]},{"id":3,"tasks":[{"id":1},{"id":5}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parents and children on views" $ it "requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parents and children on views with renamed keys" $ it "requesting parents and children on views with renamed keys" $
get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients{*}, tasks{id, name}" `shouldRespondWith` get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
[str|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting children with composite key" $ it "requesting children with composite key" $
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments{content}" `shouldRespondWith` get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments{content}" `shouldRespondWith`
[str|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|] [json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $ it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
get "/articles?id=eq.1&select=id,articleStars{users{*}}" `shouldRespondWith` get "/articles?id=eq.1&select=id,articleStars{users{*}}" `shouldRespondWith`
[str|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|] [json|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can select by column name" $ it "can embed by FK column name" $
get "/projects?id=in.1,3&select=id,name,client_id,client_id{id,name}" `shouldRespondWith` get "/projects?id=in.1,3&select=id,name,client_id{id,name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1,"client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client_id":{"id":2,"name":"Apple"}}]|] [json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by FK column name and select the FK value at the same time, if aliased" $
get "/projects?id=in.1,3&select=id,name,client_id,client:client_id{id,name}" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can select by column name sans id" $ it "can select by column name sans id" $
get "/projects?id=in.1,3&select=id,name,client_id,client{id,name}" `shouldRespondWith` get "/projects?id=in.1,3&select=id,name,client_id,client{id,name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|] [json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can detect fk relations through views to tables in the public schema" $ it "can detect fk relations through views to tables in the public schema" $
get "/consumers_view?select=*,orders_view{*}" `shouldRespondWith` 200 get "/consumers_view?select=*,orders_view{*}" `shouldRespondWith` 200
@@ -464,15 +491,18 @@ spec = do
it "ordering embeded entities" $ it "ordering embeded entities" $
get "/projects?id=eq.1&select=id, name, tasks{id, name}&tasks.order=name.asc" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, tasks{id, name}&tasks.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|] [json|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "ordering embeded entities with alias" $ it "ordering embeded entities with alias" $
get "/projects?id=eq.1&select=id, name, the_tasks:tasks{id, name}&tasks.order=name.asc" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, the_tasks:tasks{id, name}&tasks.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|] [json|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"},{"id":1,"name":"Design w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "ordering embeded entities, two levels" $ it "ordering embeded entities, two levels" $
get "/projects?id=eq.1&select=id, name, tasks{id, name, users{id, name}}&tasks.order=name.asc&tasks.users.order=name.desc" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, tasks{id, name, users{id, name}}&tasks.order=name.asc&tasks.users.order=name.desc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7","users":[{"id":1,"name":"Angela Martin"}]},{"id":1,"name":"Design w7","users":[{"id":3,"name":"Dwight Schrute"},{"id":1,"name":"Angela Martin"}]}]}]|] [json|[{"id":1,"name":"Windows 7","tasks":[{"id":2,"name":"Code w7","users":[{"id":1,"name":"Angela Martin"}]},{"id":1,"name":"Design w7","users":[{"id":3,"name":"Dwight Schrute"},{"id":1,"name":"Angela Martin"}]}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "ordering embeded parents does not break things" $ it "ordering embeded parents does not break things" $
get "/projects?id=eq.1&select=id, name, clients{id, name}&clients.order=name.asc" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, clients{id, name}&clients.order=name.asc" `shouldRespondWith`
+5 -6
View File
@@ -9,7 +9,6 @@ import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import SpecHelper import SpecHelper
import Text.Heredoc
import Network.Wai (Application) import Network.Wai (Application)
import Protolude hiding (get) import Protolude hiding (get)
@@ -131,20 +130,20 @@ spec = do
it "no parameters return everything" $ it "no parameters return everything" $
get "/items?select=id&order=id.asc" get "/items?select=id&order=id.asc"
`shouldRespondWith` `shouldRespondWith`
[str|[{"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}]|] [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 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"] , matchHeaders = ["Content-Range" <:> "0-14/*"]
} }
it "top level limit with parameter" $ it "top level limit with parameter" $
get "/items?select=id&order=id.asc&limit=3" get "/items?select=id&order=id.asc&limit=3"
`shouldRespondWith` [str|[{"id":1},{"id":2},{"id":3}]|] `shouldRespondWith` [json|[{"id":1},{"id":2},{"id":3}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"] , matchHeaders = ["Content-Range" <:> "0-2/*"]
} }
it "headers override get parameters" $ it "headers override get parameters" $
request methodGet "/items?select=id&order=id.asc&limit=3" request methodGet "/items?select=id&order=id.asc&limit=3"
(rangeHdrs $ ByteRangeFromTo 0 1) "" (rangeHdrs $ ByteRangeFromTo 0 1) ""
`shouldRespondWith` [str|[{"id":1},{"id":2}]|] `shouldRespondWith` [json|[{"id":1},{"id":2}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"] , matchHeaders = ["Content-Range" <:> "0-1/*"]
} }
@@ -152,7 +151,7 @@ spec = do
it "limit works on all levels" $ it "limit works on all levels" $
get "/clients?select=id,projects{id,tasks{id}}&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1" get "/clients?select=id,projects{id,tasks{id}}&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1"
`shouldRespondWith` `shouldRespondWith`
[str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|] [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
@@ -160,7 +159,7 @@ spec = do
it "limit and offset works on first level" $ it "limit and offset works on first level" $
get "/items?select=id&order=id.asc&limit=3&offset=2" get "/items?select=id&order=id.asc&limit=3&offset=2"
`shouldRespondWith` [str|[{"id":3},{"id":4},{"id":5}]|] `shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "2-4/*"] , matchHeaders = ["Content-Range" <:> "2-4/*"]
} }
+4 -2
View File
@@ -112,9 +112,11 @@ spec =
context "foreign entities embedding" $ do context "foreign entities embedding" $ do
it "can embed if related tables are in the exposed schema" $ do it "can embed if related tables are in the exposed schema" $ do
post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith` post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/getproject?id=1&select=id,name,client{id},tasks{id}" `shouldRespondWith` get "/rpc/getproject?id=1&select=id,name,client{id},tasks{id}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "cannot embed if the related table is not in the exposed schema" $ do it "cannot embed if the related table is not in the exposed schema" $ do
post "/rpc/single_article?select=*,article_stars{*}" [json|{ "id": 1}|] post "/rpc/single_article?select=*,article_stars{*}" [json|{ "id": 1}|]
+2 -1
View File
@@ -39,7 +39,8 @@ spec =
it "can shape plurality singular object routes" $ it "can shape plurality singular object routes" $
request methodGet "/projects_view?id=eq.1&select=id,name,clients{*},tasks{id,name}" [singular] "" request methodGet "/projects_view?id=eq.1&select=id,name,clients{*},tasks{id,name}" [singular] ""
`shouldRespondWith` `shouldRespondWith`
[str|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|] [json|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|]
{ matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"] }
context "when updating rows" $ do context "when updating rows" $ do