Fix overloaded computed columns on RPC (#1473)

* fix some typos and spelling

* fix pg_source CTE name should be prefixed with pgrst_

* added tests for overloaded computed columns on patch calls
This commit is contained in:
Wolfgang Walther
2020-04-06 14:04:55 -05:00
committed by GitHub
parent 82fa1d8812
commit c524531784
14 changed files with 184 additions and 78 deletions
+9 -9
View File
@@ -324,20 +324,20 @@ spec =
]|]
{ matchHeaders = [matchContentTypeJson] }
it "embeds childs recursively" $
get "/family_tree?id=eq.1&select=id,name, childs:family_tree!parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`
it "embeds children recursively" $
get "/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{
"id": "1", "name": "Parental Unit", "childs": [
{ "id": "2", "name": "Kid One", "childs": [ { "id": "4", "name": "Grandkid One" } ] },
{ "id": "3", "name": "Kid Two", "childs": [ { "id": "5", "name": "Grandkid Two" } ] }
"id": "1", "name": "Parental Unit", "children": [
{ "id": "2", "name": "Kid One", "children": [ { "id": "4", "name": "Grandkid One" } ] },
{ "id": "3", "name": "Kid Two", "children": [ { "id": "5", "name": "Grandkid Two" } ] }
]
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds parent and then embeds childs" $
get "/family_tree?id=eq.2&select=id,name,parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`
it "embeds parent and then embeds children" $
get "/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{
"id": "2", "name": "Kid One", "parent": {
"id": "1", "name": "Parental Unit", "childs": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]
"id": "1", "name": "Parental Unit", "children": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]
}
}]|] { matchHeaders = [matchContentTypeJson] }
@@ -356,7 +356,7 @@ spec =
}
}]|] { matchHeaders = [matchContentTypeJson] }
it "embeds childs" $ do
it "embeds children" $ do
get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`
[json|[{
"id": 1, "name": "Referee Org",
+37 -14
View File
@@ -464,17 +464,40 @@ spec actualPgVersion = do
matchHeaders = []
}
it "can provide a representation" $ do
_ <- post "/items"
[json| { id: 1 } |]
request methodPatch
"/items?id=eq.1"
[("Prefer", "return=representation")]
[json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |]
{ matchHeaders = [matchContentTypeJson] }
-- put value back for other tests
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
context "with representation requested" $ do
it "can provide a representation" $ do
_ <- post "/items"
[json| { id: 1 } |]
request methodPatch
"/items?id=eq.1"
[("Prefer", "return=representation")]
[json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |]
{ matchHeaders = [matchContentTypeJson] }
-- put value back for other tests
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
it "can return computed columns" $
request methodPatch
"/items?id=eq.1&select=id,always_true"
[("Prefer", "return=representation")]
[json| { id: 1 } |]
`shouldRespondWith` [json| [{ id: 1, always_true: true }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can select overloaded computed columns" $ do
request methodPatch
"/items?id=eq.1&select=id,computed_overload"
[("Prefer", "return=representation")]
[json| { id: 1 } |]
`shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]
{ matchHeaders = [matchContentTypeJson] }
request methodPatch
"/items2?id=eq.1&select=id,computed_overload"
[("Prefer", "return=representation")]
[json| { id: 1 } |]
`shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]
{ matchHeaders = [matchContentTypeJson] }
it "makes no updates and returns 204, when patching with an empty json object/array" $ do
request methodPatch "/items" [] [json| {} |]
@@ -561,7 +584,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ]
}
it "embeds childs after update" $
it "embeds children after update" $
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"
[("Prefer", "return=representation")]
[json|{"name": "tardis-patched"}|]
@@ -573,7 +596,7 @@ spec actualPgVersion = do
matchHeaders = [matchContentTypeJson]
}
it "embeds parent, childs and grandchilds after update" $
it "embeds parent, children and grandchildren after update" $
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"
[("Prefer", "return=representation")]
[json|{"name": "tardis-patched-2"}|]
@@ -594,7 +617,7 @@ spec actualPgVersion = do
matchHeaders = [matchContentTypeJson]
}
it "embeds childs after update without explicitly including the id in the ?select" $
it "embeds children after update without explicitly including the id in the ?select" $
request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"
[("Prefer", "return=representation")]
[json|{"name": "tardis-patched"}|]
+15 -15
View File
@@ -77,7 +77,7 @@ spec actualPgVersion =
context "Inserting tables on different schemas" $ do
it "succeeds inserting on default schema and returning it" $
request methodPost "/childs" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|]
request methodPost "/children" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|]
`shouldRespondWith`
[json|[{"id":1, "name": "child v1-1", "parent_id": 1}]|]
{
@@ -86,7 +86,7 @@ spec actualPgVersion =
}
it "succeeds inserting on the v1 schema and returning its parent" $
request methodPost "/childs?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")]
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")]
[json|{"name": "child v1-2", "parent_id": 2}|]
`shouldRespondWith`
[json|[{"id":2, "parent": {"id": 2, "name": "parent v1-2"}}]|]
@@ -96,7 +96,7 @@ spec actualPgVersion =
}
it "succeeds inserting on the v2 schema and returning its parent" $
request methodPost "/childs?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")]
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")]
[json|{"name": "child v2-3", "parent_id": 3}|]
`shouldRespondWith`
[json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|]
@@ -106,7 +106,7 @@ spec actualPgVersion =
}
it "fails when inserting on an unknown schema" $
request methodPost "/childs" [("Content-Profile", "unknown")]
request methodPost "/children" [("Content-Profile", "unknown")]
[json|{"name": "child 4", "parent_id": 4}|]
`shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|]
@@ -125,22 +125,22 @@ spec actualPgVersion =
}
it "succeeds in calling the v1 schema proc and embedding" $
request methodGet "/rpc/get_parents_below?id=6&select=id,name,childs(id,name)" [("Accept-Profile", "v1")] ""
request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v1")] ""
`shouldRespondWith`
[json| [
{"id":1,"name":"parent v1-1","childs":[{"id":1,"name":"child v1-1"}]},
{"id":2,"name":"parent v1-2","childs":[{"id":2,"name":"child v1-2"}]}] |]
{"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},
{"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
}
it "succeeds in calling the v2 schema proc and embedding" $
request methodGet "/rpc/get_parents_below?id=6&select=id,name,childs(id,name)" [("Accept-Profile", "v2")] ""
request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v2")] ""
`shouldRespondWith`
[json| [
{"id":3,"name":"parent v2-3","childs":[{"id":1,"name":"child v2-3"}]},
{"id":4,"name":"parent v2-4","childs":[]}] |]
{"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},
{"id":4,"name":"parent v2-4","children":[]}] |]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
@@ -148,7 +148,7 @@ spec actualPgVersion =
context "Modifying tables on different schemas" $ do
it "succeeds in patching on the v1 schema and returning its parent" $
request methodPatch "/childs?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]
request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]
[json|{"name": "child v1-1 updated"}|]
`shouldRespondWith`
[json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|]
@@ -158,7 +158,7 @@ spec actualPgVersion =
}
it "succeeds in patching on the v2 schema and returning its parent" $
request methodPatch "/childs?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
[json|{"name": "child v2-1 updated"}|]
`shouldRespondWith`
[json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|]
@@ -168,13 +168,13 @@ spec actualPgVersion =
}
it "succeeds on deleting on the v2 schema" $ do
request methodDelete "/childs?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] ""
request methodDelete "/children?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] ""
`shouldRespondWith` [json|[{"id": 1, "name": "child v2-1 updated", "parent_id": 3}]|]
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
request methodGet "/childs?id=eq.1" [("Accept-Profile", "v2")] ""
request methodGet "/children?id=eq.1" [("Accept-Profile", "v2")] ""
`shouldRespondWith` "[]"
{
matchStatus = 200
@@ -183,7 +183,7 @@ spec actualPgVersion =
when (actualPgVersion >= pgVersion96) $
it "succeeds on PUT on the v2 schema" $
request methodPut "/childs?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
[json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]
`shouldRespondWith`
[json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
+33 -10
View File
@@ -237,7 +237,6 @@ spec actualPgVersion = do
[json| [{"myId":1}] |]
{ matchHeaders = [matchContentTypeJson] }
it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
@@ -327,6 +326,30 @@ spec actualPgVersion = do
[json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "computed columns" $ do
it "computed column on table" $
get "/items?id=eq.1&select=id,always_true" `shouldRespondWith`
[json|[{"id":1,"always_true":true}]|]
{ matchHeaders = [matchContentTypeJson] }
it "computed column on rpc" $
get "/rpc/search?id=1&select=id,always_true" `shouldRespondWith`
[json|[{"id":1,"always_true":true}]|]
{ matchHeaders = [matchContentTypeJson] }
it "overloaded computed columns on both tables" $ do
get "/items?id=eq.1&select=id,computed_overload" `shouldRespondWith`
[json|[{"id":1,"computed_overload":true}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/items2?id=eq.1&select=id,computed_overload" `shouldRespondWith`
[json|[{"id":1,"computed_overload":true}]|]
{ matchHeaders = [matchContentTypeJson] }
it "overloaded computed column on rpc" $
get "/rpc/search?id=1&select=id,computed_overload" `shouldRespondWith`
[json|[{"id":1,"computed_overload":true}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "view embedding" $ do
it "can detect fk relations through views to tables in the public schema" $
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
@@ -482,18 +505,18 @@ spec actualPgVersion = do
"designTasks":[ ] }
]|] { matchHeaders = [matchContentTypeJson] }
it "works with two aliased childs embeds plus and/or" $
get "/entities?select=id,childs:child_entities(id,gChilds:grandchild_entities(id))&childs.and=(id.in.(1,2,3))&childs.gChilds.or=(id.eq.1,id.eq.2)" `shouldRespondWith`
it "works with two aliased children embeds plus and/or" $
get "/entities?select=id,children:child_entities(id,gChildren:grandchild_entities(id))&children.and=(id.in.(1,2,3))&children.gChildren.or=(id.eq.1,id.eq.2)" `shouldRespondWith`
[json|[
{ "id":1,
"childs":[
{"id":1,"gChilds":[{"id":1}, {"id":2}]},
{"id":2,"gChilds":[]}]},
"children":[
{"id":1,"gChildren":[{"id":1}, {"id":2}]},
{"id":2,"gChildren":[]}]},
{ "id":2,
"childs":[
{"id":3,"gChilds":[]}]},
{ "id":3,"childs":[]},
{ "id":4,"childs":[]}
"children":[
{"id":3,"gChildren":[]}]},
{ "id":3,"children":[]},
{ "id":4,"children":[]}
]|] { matchHeaders = [matchContentTypeJson] }
describe "ordering response" $ do
+5 -5
View File
@@ -29,33 +29,33 @@ main = do
context "call proc query" $ do
it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool [str| {"id": 3} |] $
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False Nothing
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False Nothing []
liftIO $
cost `shouldSatisfy` (< Just 40)
it "should not exceed cost when calling setof composite proc with empty params" $ do
cost <- exec pool mempty $
requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing
requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing []
liftIO $
cost `shouldSatisfy` (< Just 30)
it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool [str| {"a": 3, "b": 4} |] $
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing []
liftIO $
cost `shouldSatisfy` (< Just 10)
context "params=multiple-objects" $ do
it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool [str| [{"id": 1}, {"id": 4}] |] $
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False (Just MultipleObjects)
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False (Just MultipleObjects) []
liftIO $ do
cost `shouldSatisfy` (> Just 2000)
cost `shouldSatisfy` (< Just 2100)
it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |] $
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing []
liftIO $
cost `shouldSatisfy` (< Just 10)
+29
View File
@@ -207,6 +207,35 @@ INSERT INTO items VALUES (15);
SELECT pg_catalog.setval('items_id_seq', 15, true);
--
-- Data for Name: items2; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE items2 CASCADE;
INSERT INTO items2 VALUES (1);
INSERT INTO items2 VALUES (2);
INSERT INTO items2 VALUES (3);
INSERT INTO items2 VALUES (4);
INSERT INTO items2 VALUES (5);
INSERT INTO items2 VALUES (6);
INSERT INTO items2 VALUES (7);
INSERT INTO items2 VALUES (8);
INSERT INTO items2 VALUES (9);
INSERT INTO items2 VALUES (10);
INSERT INTO items2 VALUES (11);
INSERT INTO items2 VALUES (12);
INSERT INTO items2 VALUES (13);
INSERT INTO items2 VALUES (14);
INSERT INTO items2 VALUES (15);
--
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('items2_id_seq', 15, true);
--
-- Data for Name: json; Type: TABLE DATA; Schema: test; Owner: -
--
+5 -4
View File
@@ -15,6 +15,7 @@ SET search_path = test, "تست", pg_catalog;
GRANT ALL ON TABLE
items
, items2
, "articleStars"
, articles
, auto_incrementing_pk
@@ -127,8 +128,8 @@ GRANT ALL ON TABLE
, v1.parents
, v2.parents
, v2.another_table
, v1.childs
, v2.childs
, v1.children
, v2.children
TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
@@ -138,8 +139,8 @@ GRANT USAGE ON SEQUENCE
, items_id_seq
, callcounter_count
, leak_id_seq
, v1.childs_id_seq
, v2.childs_id_seq
, v1.children_id_seq
, v2.children_id_seq
TO postgrest_test_anonymous;
-- Privileges for non anonymous users
+20 -2
View File
@@ -109,10 +109,28 @@ CREATE TABLE items (
id bigserial primary key
);
CREATE TABLE items2 (
id bigserial primary key
);
CREATE FUNCTION search(id BIGINT) RETURNS SETOF items
LANGUAGE plpgsql
AS $$BEGIN
RETURN QUERY SELECT items.id FROM items WHERE items.id=search.id;
END$$;
CREATE FUNCTION always_true(test.items) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT true $$;
CREATE FUNCTION computed_overload(test.items) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT true $$;
CREATE FUNCTION computed_overload(test.items2) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT true $$;
CREATE FUNCTION is_first(test.items) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT $1.id = 1 $$;
@@ -1702,7 +1720,7 @@ create table v1.parents (
, name text
);
create table v1.childs (
create table v1.children (
id serial primary key
, name text
, parent_id int
@@ -1720,7 +1738,7 @@ create table v2.parents (
, name text
);
create table v2.childs (
create table v2.children (
id serial primary key
, name text
, parent_id int