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
+2
View File
@@ -9,6 +9,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- #1473, Fix overloaded computed columns on RPC - @wolfgangwalther
## [7.0.0] - 2020-04-03 ## [7.0.0] - 2020-04-03
### Added ### Added
+10 -6
View File
@@ -46,7 +46,8 @@ import PostgREST.ApiRequest (Action (..), ApiRequest (..),
import PostgREST.Auth (containsRole, jwtClaims, import PostgREST.Auth (containsRole, jwtClaims,
parseSecret) parseSecret)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.DbRequestBuilder (mutateRequest, readRequest) import PostgREST.DbRequestBuilder (mutateRequest, readRequest,
returningCols)
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.Error (PgError (..), SimpleError (..), import PostgREST.Error (PgError (..), SimpleError (..),
errorResponseFor, singularityError) errorResponseFor, singularityError)
@@ -127,7 +128,7 @@ app dbStructure proc cols conf apiRequest =
(ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) -> (ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
case readSqlParts tSchema tName of case readSqlParts tSchema tName of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right (q, cq, bField) -> do Right (q, cq, bField, _) -> do
let cQuery = if estimatedCount let cQuery = if estimatedCount
then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
else cq else cq
@@ -293,10 +294,10 @@ app dbStructure proc cols conf apiRequest =
let tName = fromMaybe pName $ procTableName =<< proc in let tName = fromMaybe pName $ procTableName =<< proc in
case readSqlParts tSchema tName of case readSqlParts tSchema tName of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right (q, cq, bField) -> do Right (q, cq, bField, returning) -> do
let let
preferParams = iPreferParameters apiRequest preferParams = iPreferParameters apiRequest
pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams returning
stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON) stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON)
(contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects) (contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects)
bField pgVer bField pgVer
@@ -351,11 +352,14 @@ app dbStructure proc cols conf apiRequest =
readSqlParts s t = readSqlParts s t =
let let
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
returnings :: ReadRequest -> Either Response [FieldName]
returnings rr = Right (returningCols rr)
in in
(,,) <$> (,,,) <$>
(readRequestToQuery <$> readReq) <*> (readRequestToQuery <$> readReq) <*>
(readRequestToCountQuery <$> readReq) <*> (readRequestToCountQuery <$> readReq) <*>
(binaryField contentType rawContentTypes returnsScalar =<< readReq) (binaryField contentType rawContentTypes returnsScalar =<< readReq) <*>
(returnings =<< readReq)
mutateSqlParts s t = mutateSqlParts s t =
let let
+4 -3
View File
@@ -14,6 +14,7 @@ A query tree is built in case of resource embedding. By inferring the relationsh
module PostgREST.DbRequestBuilder ( module PostgREST.DbRequestBuilder (
readRequest readRequest
, mutateRequest , mutateRequest
, returningCols
) where ) where
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
@@ -40,7 +41,7 @@ readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest
readRequest schema rootTableName maxRows allRels apiRequest = readRequest schema rootTableName maxRows allRels apiRequest =
mapLeft errorResponseFor $ mapLeft errorResponseFor $
treeRestrictRange maxRows =<< treeRestrictRange maxRows =<<
augumentRequestWithJoin schema rootRels =<< augmentRequestWithJoin schema rootRels =<<
addFiltersOrdersRanges apiRequest <*> addFiltersOrdersRanges apiRequest <*>
(initReadRequest rootName <$> pRequestSelect sel) (initReadRequest rootName <$> pRequestSelect sel)
where where
@@ -89,8 +90,8 @@ treeRestrictRange maxRows request = pure $ nodeRestrictRange maxRows <$> request
nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode
nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i) nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i)
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest augmentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
augumentRequestWithJoin schema allRels request = augmentRequestWithJoin schema allRels request =
addRels schema allRels Nothing request addRels schema allRels Nothing request
>>= addJoinConditions Nothing >>= addJoinConditions Nothing
+2 -2
View File
@@ -186,8 +186,8 @@ countF :: SqlQuery -> Bool -> (SqlFragment, SqlFragment)
countF countQuery shouldCount = countF countQuery shouldCount =
if shouldCount if shouldCount
then ( then (
", pg_source_count AS (" <> countQuery <> ")" ", pgrst_source_count AS (" <> countQuery <> ")"
, "(SELECT pg_catalog.count(*) FROM pg_source_count)" ) , "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
else ( else (
mempty mempty
, "null::bigint") , "null::bigint")
+12 -7
View File
@@ -113,15 +113,15 @@ mutateRequestToQuery (Delete mainQi logicForest returnings) =
returningF mainQi returnings returningF mainQi returnings
] ]
requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Bool -> Maybe PreferParameters -> SqlQuery requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Bool -> Maybe PreferParameters -> [FieldName] -> SqlQuery
requestToCallProcQuery qi pgArgs returnsScalar preferParams = requestToCallProcQuery qi pgArgs returnsScalar preferParams returnings =
unwords [ unwords [
"WITH", "WITH",
argsCTE, argsCTE,
sourceBody ] sourceBody ]
where where
paramsAsSingleObject = preferParams == Just SingleObject paramsAsSingleObject = preferParams == Just SingleObject
paramsAsMulitpleObjects = preferParams == Just MultipleObjects paramsAsMultipleObjects = preferParams == Just MultipleObjects
(argsCTE, args) (argsCTE, args)
| null pgArgs = (ignoredBody, "") | null pgArgs = (ignoredBody, "")
@@ -132,7 +132,7 @@ requestToCallProcQuery qi pgArgs returnsScalar preferParams =
"pgrst_args AS (", "pgrst_args AS (",
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtArgs (\a -> " " <> pgaType a) <> ")", "SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtArgs (\a -> " " <> pgaType a) <> ")",
")"] ")"]
, if paramsAsMulitpleObjects , if paramsAsMultipleObjects
then fmtArgs (\a -> " := pgrst_args." <> pgFmtIdent (pgaName a)) then fmtArgs (\a -> " := pgrst_args." <> pgFmtIdent (pgaName a))
else fmtArgs (\a -> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM pgrst_args LIMIT 1)") else fmtArgs (\a -> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM pgrst_args LIMIT 1)")
) )
@@ -142,20 +142,25 @@ requestToCallProcQuery qi pgArgs returnsScalar preferParams =
sourceBody :: SqlFragment sourceBody :: SqlFragment
sourceBody sourceBody
| paramsAsMulitpleObjects = | paramsAsMultipleObjects =
if returnsScalar if returnsScalar
then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args" then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args"
else unwords [ "SELECT pgrst_lat_args.*" else unwords [ "SELECT pgrst_lat_args.*"
, "FROM pgrst_args," , "FROM pgrst_args,"
, "LATERAL ( SELECT * FROM " <> callIt <> " ) pgrst_lat_args" ] , "LATERAL ( SELECT " <> returned_columns <> " FROM " <> callIt <> " ) pgrst_lat_args" ]
| otherwise = | otherwise =
if returnsScalar if returnsScalar
then "SELECT " <> callIt <> " AS pgrst_scalar" then "SELECT " <> callIt <> " AS pgrst_scalar"
else "SELECT * FROM " <> callIt else "SELECT " <> returned_columns <> " FROM " <> callIt
callIt :: SqlFragment callIt :: SqlFragment
callIt = fromQi qi <> "(" <> args <> ")" callIt = fromQi qi <> "(" <> args <> ")"
returned_columns :: SqlFragment
returned_columns
| null returnings = "*"
| otherwise = intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName qi) <$> returnings)
-- | SQL query meant for COUNTing the root node of the Tree. -- | SQL query meant for COUNTing the root node of the Tree.
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT. -- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
+1 -1
View File
@@ -522,7 +522,7 @@ pgVersion121 :: PgVersion
pgVersion121 = PgVersion 120001 "12.1" pgVersion121 = PgVersion 120001 "12.1"
sourceCTEName :: SqlFragment sourceCTEName :: SqlFragment
sourceCTEName = "pg_source" sourceCTEName = "pgrst_source"
-- | full jspath, e.g. .property[0].attr.detail -- | full jspath, e.g. .property[0].attr.detail
type JSPath = [JSPathExp] type JSPath = [JSPathExp]
+9 -9
View File
@@ -324,20 +324,20 @@ spec =
]|] ]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "embeds childs recursively" $ it "embeds children recursively" $
get "/family_tree?id=eq.1&select=id,name, childs:family_tree!parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith` get "/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{ [json|[{
"id": "1", "name": "Parental Unit", "childs": [ "id": "1", "name": "Parental Unit", "children": [
{ "id": "2", "name": "Kid One", "childs": [ { "id": "4", "name": "Grandkid One" } ] }, { "id": "2", "name": "Kid One", "children": [ { "id": "4", "name": "Grandkid One" } ] },
{ "id": "3", "name": "Kid Two", "childs": [ { "id": "5", "name": "Grandkid Two" } ] } { "id": "3", "name": "Kid Two", "children": [ { "id": "5", "name": "Grandkid Two" } ] }
] ]
}]|] { matchHeaders = [matchContentTypeJson] } }]|] { matchHeaders = [matchContentTypeJson] }
it "embeds parent and then embeds childs" $ it "embeds parent and then embeds children" $
get "/family_tree?id=eq.2&select=id,name,parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith` get "/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`
[json|[{ [json|[{
"id": "2", "name": "Kid One", "parent": { "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] } }]|] { matchHeaders = [matchContentTypeJson] }
@@ -356,7 +356,7 @@ spec =
} }
}]|] { matchHeaders = [matchContentTypeJson] } }]|] { matchHeaders = [matchContentTypeJson] }
it "embeds childs" $ do it "embeds children" $ do
get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith` get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`
[json|[{ [json|[{
"id": 1, "name": "Referee Org", "id": 1, "name": "Referee Org",
+37 -14
View File
@@ -464,17 +464,40 @@ spec actualPgVersion = do
matchHeaders = [] matchHeaders = []
} }
it "can provide a representation" $ do context "with representation requested" $ do
_ <- post "/items" it "can provide a representation" $ do
[json| { id: 1 } |] _ <- post "/items"
request methodPatch [json| { id: 1 } |]
"/items?id=eq.1" request methodPatch
[("Prefer", "return=representation")] "/items?id=eq.1"
[json| { id: 99 } |] [("Prefer", "return=representation")]
`shouldRespondWith` [json| [{id:99}] |] [json| { id: 99 } |]
{ matchHeaders = [matchContentTypeJson] } `shouldRespondWith` [json| [{id:99}] |]
-- put value back for other tests { matchHeaders = [matchContentTypeJson] }
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |] -- 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 it "makes no updates and returns 204, when patching with an empty json object/array" $ do
request methodPatch "/items" [] [json| {} |] request methodPatch "/items" [] [json| {} |]
@@ -561,7 +584,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ] , 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)" request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json|{"name": "tardis-patched"}|] [json|{"name": "tardis-patched"}|]
@@ -573,7 +596,7 @@ spec actualPgVersion = do
matchHeaders = [matchContentTypeJson] 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)" 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")] [("Prefer", "return=representation")]
[json|{"name": "tardis-patched-2"}|] [json|{"name": "tardis-patched-2"}|]
@@ -594,7 +617,7 @@ spec actualPgVersion = do
matchHeaders = [matchContentTypeJson] 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)" request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json|{"name": "tardis-patched"}|] [json|{"name": "tardis-patched"}|]
+15 -15
View File
@@ -77,7 +77,7 @@ spec actualPgVersion =
context "Inserting tables on different schemas" $ do context "Inserting tables on different schemas" $ do
it "succeeds inserting on default schema and returning it" $ 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` `shouldRespondWith`
[json|[{"id":1, "name": "child v1-1", "parent_id": 1}]|] [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" $ 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}|] [json|{"name": "child v1-2", "parent_id": 2}|]
`shouldRespondWith` `shouldRespondWith`
[json|[{"id":2, "parent": {"id": 2, "name": "parent v1-2"}}]|] [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" $ 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}|] [json|{"name": "child v2-3", "parent_id": 3}|]
`shouldRespondWith` `shouldRespondWith`
[json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|] [json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|]
@@ -106,7 +106,7 @@ spec actualPgVersion =
} }
it "fails when inserting on an unknown schema" $ 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}|] [json|{"name": "child 4", "parent_id": 4}|]
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"The schema must be one of the following: v1, v2"}|] [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" $ 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` `shouldRespondWith`
[json| [ [json| [
{"id":1,"name":"parent v1-1","childs":[{"id":1,"name":"child v1-1"}]}, {"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},
{"id":2,"name":"parent v1-2","childs":[{"id":2,"name":"child v1-2"}]}] |] {"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]
{ {
matchStatus = 200 matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"] , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
} }
it "succeeds in calling the v2 schema proc and embedding" $ 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` `shouldRespondWith`
[json| [ [json| [
{"id":3,"name":"parent v2-3","childs":[{"id":1,"name":"child v2-3"}]}, {"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},
{"id":4,"name":"parent v2-4","childs":[]}] |] {"id":4,"name":"parent v2-4","children":[]}] |]
{ {
matchStatus = 200 matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"] , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
@@ -148,7 +148,7 @@ spec actualPgVersion =
context "Modifying tables on different schemas" $ do context "Modifying tables on different schemas" $ do
it "succeeds in patching on the v1 schema and returning its parent" $ 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"}|] [json|{"name": "child v1-1 updated"}|]
`shouldRespondWith` `shouldRespondWith`
[json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|] [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" $ 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"}|] [json|{"name": "child v2-1 updated"}|]
`shouldRespondWith` `shouldRespondWith`
[json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|] [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 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}]|] `shouldRespondWith` [json|[{"id": 1, "name": "child v2-1 updated", "parent_id": 3}]|]
{ {
matchStatus = 200 matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"] , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
} }
request methodGet "/childs?id=eq.1" [("Accept-Profile", "v2")] "" request methodGet "/children?id=eq.1" [("Accept-Profile", "v2")] ""
`shouldRespondWith` "[]" `shouldRespondWith` "[]"
{ {
matchStatus = 200 matchStatus = 200
@@ -183,7 +183,7 @@ spec actualPgVersion =
when (actualPgVersion >= pgVersion96) $ when (actualPgVersion >= pgVersion96) $
it "succeeds on PUT on the v2 schema" $ 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 } ]|] [json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]
`shouldRespondWith` `shouldRespondWith`
[json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|] [json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
+33 -10
View File
@@ -237,7 +237,6 @@ spec actualPgVersion = do
[json| [{"myId":1}] |] [json| [{"myId":1}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "one simple column with casting (text)" $ it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith` get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |] [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"}]}]|] [json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { 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 describe "view embedding" $ do
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
@@ -482,18 +505,18 @@ spec actualPgVersion = do
"designTasks":[ ] } "designTasks":[ ] }
]|] { matchHeaders = [matchContentTypeJson] } ]|] { matchHeaders = [matchContentTypeJson] }
it "works with two aliased childs embeds plus and/or" $ it "works with two aliased children 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` 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|[ [json|[
{ "id":1, { "id":1,
"childs":[ "children":[
{"id":1,"gChilds":[{"id":1}, {"id":2}]}, {"id":1,"gChildren":[{"id":1}, {"id":2}]},
{"id":2,"gChilds":[]}]}, {"id":2,"gChildren":[]}]},
{ "id":2, { "id":2,
"childs":[ "children":[
{"id":3,"gChilds":[]}]}, {"id":3,"gChildren":[]}]},
{ "id":3,"childs":[]}, { "id":3,"children":[]},
{ "id":4,"childs":[]} { "id":4,"children":[]}
]|] { matchHeaders = [matchContentTypeJson] } ]|] { matchHeaders = [matchContentTypeJson] }
describe "ordering response" $ do describe "ordering response" $ do
+5 -5
View File
@@ -29,33 +29,33 @@ main = do
context "call proc query" $ do context "call proc query" $ do
it "should not exceed cost when calling setof composite proc" $ do it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool [str| {"id": 3} |] $ 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 $ liftIO $
cost `shouldSatisfy` (< Just 40) cost `shouldSatisfy` (< Just 40)
it "should not exceed cost when calling setof composite proc with empty params" $ do it "should not exceed cost when calling setof composite proc with empty params" $ do
cost <- exec pool mempty $ cost <- exec pool mempty $
requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing []
liftIO $ liftIO $
cost `shouldSatisfy` (< Just 30) cost `shouldSatisfy` (< Just 30)
it "should not exceed cost when calling scalar proc" $ do it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool [str| {"a": 3, "b": 4} |] $ 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 $ liftIO $
cost `shouldSatisfy` (< Just 10) cost `shouldSatisfy` (< Just 10)
context "params=multiple-objects" $ do context "params=multiple-objects" $ do
it "should not exceed cost when calling setof composite proc" $ do it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool [str| [{"id": 1}, {"id": 4}] |] $ 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 liftIO $ do
cost `shouldSatisfy` (> Just 2000) cost `shouldSatisfy` (> Just 2000)
cost `shouldSatisfy` (< Just 2100) cost `shouldSatisfy` (< Just 2100)
it "should not exceed cost when calling scalar proc" $ do 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}] |] $ 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 $ liftIO $
cost `shouldSatisfy` (< Just 10) 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); 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: - -- 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 GRANT ALL ON TABLE
items items
, items2
, "articleStars" , "articleStars"
, articles , articles
, auto_incrementing_pk , auto_incrementing_pk
@@ -127,8 +128,8 @@ GRANT ALL ON TABLE
, v1.parents , v1.parents
, v2.parents , v2.parents
, v2.another_table , v2.another_table
, v1.childs , v1.children
, v2.childs , v2.children
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
@@ -138,8 +139,8 @@ GRANT USAGE ON SEQUENCE
, items_id_seq , items_id_seq
, callcounter_count , callcounter_count
, leak_id_seq , leak_id_seq
, v1.childs_id_seq , v1.children_id_seq
, v2.childs_id_seq , v2.children_id_seq
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
-- Privileges for non anonymous users -- Privileges for non anonymous users
+20 -2
View File
@@ -109,10 +109,28 @@ CREATE TABLE items (
id bigserial primary key 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 CREATE FUNCTION always_true(test.items) RETURNS boolean
LANGUAGE sql STABLE LANGUAGE sql STABLE
AS $$ SELECT true $$; 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 CREATE FUNCTION is_first(test.items) RETURNS boolean
LANGUAGE sql STABLE LANGUAGE sql STABLE
AS $$ SELECT $1.id = 1 $$; AS $$ SELECT $1.id = 1 $$;
@@ -1702,7 +1720,7 @@ create table v1.parents (
, name text , name text
); );
create table v1.childs ( create table v1.children (
id serial primary key id serial primary key
, name text , name text
, parent_id int , parent_id int
@@ -1720,7 +1738,7 @@ create table v2.parents (
, name text , name text
); );
create table v2.childs ( create table v2.children (
id serial primary key id serial primary key
, name text , name text
, parent_id int , parent_id int