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:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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"}|]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Vendored
+29
@@ -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: -
|
||||||
--
|
--
|
||||||
|
|||||||
Vendored
+5
-4
@@ -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
|
||||||
|
|||||||
Vendored
+20
-2
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user