feat: RPC POST for function w/single unnamed param

For POST on RPC, allows:

* passing a json object without using `Prefer: params=single-object`
  The function must be defined with a single unnamed json param and
  `Content-Type: application/json` must be specified.

* uploading binary to a function
  The function must be defined with a single unnamed bytea param and
  `Content-Type: application/octet-stream` must be specified.

* uploading raw text to a function
  The function must be defined with a single unnamed text param and
  `Content-Type: text/plain` must be specified.

BREAKING CHANGE If there's a function "my_func" having a single
unnamed json param and other overloaded pairs(with any number of
params), PostgREST won't be able to resolve a POST request to
"my_func". For solving this, you can name the unnamed json param.

my_func(json) -> my_func(prm json)
This commit is contained in:
steve-chavez
2021-08-30 18:17:59 -05:00
committed by Steve Chavez
parent caaa34b5de
commit d4c6abbaec
11 changed files with 209 additions and 76 deletions
+8 -1
View File
@@ -9,10 +9,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla
- #1878, Add Retry-After hint header when in recovery mode - @gautam1168
- #1735, Allow calling function with single unnamed param through RPC POST. - @steve-chavez
+ Enables calling a function with a single json parameter without using `Prefer: params=single-object`
+ Enables uploading bytea to a function with `Content-Type: application/octet-stream`
+ Enables uploading raw text to a function with `Content-Type: text/plain`
### Fixed
- #1871, Fix OpenAPI missing default values for String types and identify Array types as "array" instead of "string" - @laurenceisla
### Changed
- #1927, Overloaded Functions: If there's a function "my_func" having a single unnamed json param and other overloaded pairs(with any number of params), PostgREST won't be able to resolve a POST request to "my_func". For solving this, you can name the unnamed json param `my_func(json) -> my_func(prm json)`.
## [8.0.0] - 2021-07-25
+10 -6
View File
@@ -60,7 +60,7 @@ data ApiRequestError
| NoRelBetween Text Text
| AmbiguousRelBetween Text Text [Relationship]
| AmbiguousRpc [ProcDescription]
| NoRpc Text Text [Text] Bool
| NoRpc Text Text [Text] Bool ContentType Bool
| InvalidFilters
| UnacceptableSchema [Text]
| ContentTypeError [ByteString]
@@ -99,14 +99,18 @@ instance JSON.ToJSON ApiRequestError where
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
"details" .= (compressedRel <$> rels) ]
toJSON (AmbiguousRpc procs) = JSON.object [
"hint" .= ("Overloaded functions with the same parameter name but different types are not supported" :: Text),
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text),
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])]
toJSON (NoRpc schema procName payloadKeys hasPreferSingleObject) = JSON.object [
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost) =
let prms = "(" <> T.intercalate ", " argumentKeys <> ")" in JSON.object [
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text),
"message" .= ("Could not find the " <> schema <> "." <> procName <>
(if hasPreferSingleObject
then " function with a single json or jsonb parameter"
else "(" <> T.intercalate ", " payloadKeys <> ")" <> " function") <>
(case (hasPreferSingleObject, isInvPost, contentType) of
(True, _, _) -> " function with a single json or jsonb parameter"
(_, True, CTTextPlain) -> " function with a single unnamed text parameter"
(_, True, CTOctetStream) -> " function with a single unnamed bytea parameter"
(_, True, CTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
_ -> prms <> " function") <>
" in the schema cache")]
toJSON UnsupportedVerb = JSON.object [
"message" .= ("Unsupported HTTP verb" :: Text)]
+11 -10
View File
@@ -116,26 +116,27 @@ mutateRequestToQuery (Delete mainQi logicForest returnings) =
H.sql (returningF mainQi returnings)
requestToCallProcQuery :: CallRequest -> H.Snippet
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall singleParam returnings) =
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall returnings) =
prmsCTE <> argsBody
where
(prmsCTE, argFrag)
| null params = (mempty, mempty)
| singleParam = ("WITH pgrst_args AS (SELECT NULL)", jsonPlaceHolder args)
| otherwise = (
(prmsCTE, argFrag) = case params of
OnePosParam prm -> ("WITH pgrst_args AS (SELECT NULL)", singleParameter args (encodeUtf8 $ ppType prm))
KeyParams [] -> (mempty, mempty)
KeyParams prms -> (
"WITH " <> normalizedBody args <> ", " <>
H.sql (
BS.unwords [
"pgrst_args AS (",
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtParams (const mempty) (\a -> " " <> encodeUtf8 (ppType a)) <> ")",
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtParams prms (const mempty) (\a -> " " <> encodeUtf8 (ppType a)) <> ")",
")"])
, H.sql $ if multipleCall
then fmtParams varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
else fmtParams varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
then fmtParams prms varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
else fmtParams prms varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
)
fmtParams :: (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
fmtParams prmFragPre prmFragSuf = BS.intercalate ", " ((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> params)
fmtParams :: [ProcParam] -> (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
fmtParams prms prmFragPre prmFragSuf = BS.intercalate ", "
((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> prms)
varadicPrefix :: ProcParam -> SqlFragment
varadicPrefix a = if ppVar a then "VARIADIC " else mempty
+10 -7
View File
@@ -16,7 +16,6 @@ module PostgREST.Query.SqlFragment
, countF
, fromQi
, ftsOperators
, jsonPlaceHolder
, limitOffsetF
, locationF
, normalizedBody
@@ -31,6 +30,7 @@ module PostgREST.Query.SqlFragment
, responseStatusF
, returningF
, selectBody
, singleParameter
, sourceCTEName
, unknownEncoder
, intercalateSnippet
@@ -105,9 +105,10 @@ ftsOperators = HM.fromList [
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
-- We do this in SQL to avoid processing the JSON in application code
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
normalizedBody :: Maybe BL.ByteString -> H.Snippet
normalizedBody body =
"pgrst_payload AS (SELECT " <> jsonPlaceHolder body <> " AS json_data), " <>
"pgrst_payload AS (SELECT " <> jsonPlaceHolder <> " AS json_data), " <>
H.sql (BS.unwords [
"pgrst_body AS (",
"SELECT",
@@ -116,12 +117,14 @@ normalizedBody body =
"ELSE json_build_array(json_data)",
"END AS val",
"FROM pgrst_payload)"])
where
jsonPlaceHolder = H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
-- | Equivalent to "$1::json"
-- | TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
jsonPlaceHolder :: Maybe BL.ByteString -> H.Snippet
jsonPlaceHolder body =
H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
singleParameter :: Maybe BL.ByteString -> ByteString -> H.Snippet
singleParameter body typ =
if typ == "bytea"
then H.encoderAndParam (HE.nullable HE.bytea) (toS <$> body) -- needed because bytea fails with HE.unknown(pg tries to utf8 encode)
else H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::" <> H.sql typ
selectBody :: SqlFragment
selectBody = "(SELECT val FROM pgrst_body)"
+38 -25
View File
@@ -13,7 +13,7 @@ module PostgREST.Request.ApiRequest
, ContentType(..)
, Action(..)
, Target(..)
, PayloadJSON(..)
, Payload(..)
, userApiRequest
) where
@@ -74,18 +74,19 @@ import Protolude.Conv (toS)
type RequestBody = BL.ByteString
data PayloadJSON
data Payload
= ProcessedJSON -- ^ Cached attributes of a JSON payload
{ pjRaw :: BL.ByteString
{ payRaw :: BL.ByteString
-- ^ This is the raw ByteString that comes from the request body. We
-- cache this instead of an Aeson Value because it was detected that for
-- large payloads the encoding had high memory usage, see
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
, pjKeys :: S.Set Text
, payKeys :: S.Set Text
-- ^ Keys of the object or if it's an array these keys are guaranteed to
-- be the same across all its objects
}
| RawJSON { pjRaw :: BL.ByteString }
| RawJSON { payRaw :: BL.ByteString }
| RawPay { payRaw :: BL.ByteString }
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
-- | Types of things a user wants to do to tables/views/procs
@@ -124,7 +125,7 @@ toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
prmIsVariadic prm = isJust $ find (\ProcParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> PayloadJSON
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> Payload
jsonRpcParams proc prms =
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
@@ -136,7 +137,7 @@ jsonRpcParams proc prms =
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe PayloadJSON
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe Payload
targetToJsonRpcParams target params =
case target of
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
@@ -154,7 +155,7 @@ data ApiRequest = ApiRequest {
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
, iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
@@ -253,7 +254,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
isTargetingDefaultSpec = case path of
PathInfo{pIsDefaultSpec=True} -> True
_ -> False
contentType = ContentType.decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
contentType = maybe CTApplicationJSON ContentType.decodeContentType $ lookupHeader "content-type"
columns
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
| otherwise = Nothing
@@ -264,9 +265,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
(CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
(Just RawJSON{}, Just cls) -> cls
_ -> S.empty
(Just ProcessedJSON{payKeys}, _) -> payKeys
(Just RawJSON{}, Just cls) -> cls
_ -> S.empty
payload = case contentType of
CTApplicationJSON ->
if isJust columns
@@ -282,7 +283,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> parseSimpleQuery (toS reqBody) in
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
ct ->
Left $ toS $ "Content-Type not acceptable: " <> ContentType.toMime ct
if isTargetingProc && ct `elem` [CTTextPlain, CTOctetStream]
then Right $ RawPay reqBody
else Left $ toS $ "Content-Type not acceptable: " <> ContentType.toMime ct
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
action =
case method of
@@ -321,7 +324,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
schema = fromMaybe defaultSchema profile
target =
let
callFindProc procSch procNam = findProc (QualifiedIdentifier procSch procNam) payloadColumns (hasPrefer (show SingleObject)) $ dbProcs dbStructure
callFindProc procSch procNam = findProc
(QualifiedIdentifier procSch procNam) payloadColumns (hasPrefer (show SingleObject)) (dbProcs dbStructure)
contentType (action == ActionInvoke InvPost)
in
case path of
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
@@ -426,7 +431,7 @@ csvToJson (_, vals) =
else JSON.String $ toS str
)
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys
case json of
@@ -482,26 +487,34 @@ rawContentTypes AppConfig{..} =
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
-}
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> Either ApiRequestError ProcDescription
findProc qi argumentsKeys paramsAsSingleObject allProcs =
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ContentType -> Bool -> Either ApiRequestError ProcDescription
findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
case matchProc of
[] -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject
[] -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentType isInvPost
[proc] -> Right proc
procs -> Left $ AmbiguousRpc (toList procs)
where
matchProc = filter matchesParams $ M.lookupDefault mempty qi allProcs -- first find the proc by name
matchesParams proc =
let params = pdParams proc in
-- here we don't match by argument key(there isn't one) but by the single parameter type
if paramsAsSingleObject then
case params of
[prm] -> ppType prm `elem` ["json", "jsonb"]
_ -> False
-- exceptional case for Prefer: params=single-object
if paramsAsSingleObject
then length params == 1 && (ppType <$> headMay params) `elem` [Just "json", Just "jsonb"]
-- If the function has no parameters, the arguments keys must be empty as well
else if null params
then null argumentsKeys
-- If the function is called with post and has a single unnamed parameter
-- it can be called depending on content type and the parameter type
else if isInvPost && length params == 1 && (ppName <$> headMay params) == Just mempty
then case headMay params of
Just prm | contentType == CTApplicationJSON -> ppType prm `elem` ["json", "jsonb"]
| contentType == CTTextPlain -> ppType prm == "text"
| contentType == CTOctetStream -> ppType prm == "bytea"
| otherwise -> False
Nothing -> False
-- A function has optional and required parameters. Optional parameters have a default value and
-- don't require arguments for the function to be executed, required parameters must have an argument present.
else case L.partition ppReq params of
-- If the function has no parameters, the arguments keys must be empty as well
([], []) -> null argumentsKeys
-- If the function only has required parameters, the arguments keys must match those parameters
(reqParams, []) -> argumentsKeys == S.fromList (ppName <$> reqParams)
-- If the function only has optional parameters, the arguments keys can match none or any of them(a subset)
+10 -9
View File
@@ -48,7 +48,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
restrictRange)
import PostgREST.Request.ApiRequest (Action (..),
ApiRequest (..),
PayloadJSON (..))
Payload (..))
import PostgREST.Request.Parsers
import PostgREST.Request.Preferences
@@ -344,24 +344,25 @@ mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
-- update/delete filters can be only on the root table
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
body = pjRaw <$> iPayload apiRequest
body = payRaw <$> iPayload apiRequest -- the body is assumed to be json at this stage(ApiRequest validates)
callRequest :: ProcDescription -> ApiRequest -> ReadRequest -> CallRequest
callRequest proc apiReq readReq = FunctionCall {
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
, funCParams = specifiedParams
, funCArgs = pjRaw <$> iPayload apiReq
, funCParams = callParams
, funCArgs = payRaw <$> iPayload apiReq
, funCScalar = procReturnsScalar proc
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
, funCSingleParam = paramsAsSingleObject
, funCReturning = returningCols readReq []
}
where
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
specifiedParams =
if paramsAsSingleObject
then pdParams proc
else filter (\x -> ppName x `S.member` iColumns apiReq) $ pdParams proc
callParams = case pdParams proc of
[prm] | paramsAsSingleObject -> OnePosParam prm
| ppName prm == mempty -> OnePosParam prm
| otherwise -> KeyParams $ specifiedParams [prm]
prms -> KeyParams $ specifiedParams prms
specifiedParams params = filter (\x -> ppName x `S.member` iColumns apiReq) params
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
returningCols rr@(Node _ forest) pkCols
+7 -3
View File
@@ -7,6 +7,7 @@ module PostgREST.Request.Types
, Field
, Filter(..)
, CallQuery(..)
, CallParams(..)
, CallRequest
, JoinCondition(..)
, JsonOperand(..)
@@ -40,7 +41,7 @@ import qualified GHC.Show (show)
import PostgREST.DbStructure.Identifiers (FieldName,
QualifiedIdentifier)
import PostgREST.DbStructure.Proc (ProcParam)
import PostgREST.DbStructure.Proc (ProcParam (..))
import PostgREST.DbStructure.Relationship (Relationship)
import PostgREST.RangeQuery (NonnegRange)
import PostgREST.Request.Preferences (PreferResolution)
@@ -127,14 +128,17 @@ data MutateQuery
data CallQuery = FunctionCall
{ funCQi :: QualifiedIdentifier
, funCParams :: [ProcParam]
, funCParams :: CallParams
, funCArgs :: Maybe BL.ByteString
, funCScalar :: Bool
, funCMultipleCall :: Bool
, funCSingleParam :: Bool
, funCReturning :: [FieldName]
}
data CallParams
= KeyParams [ProcParam] -- ^ Call with key params: func(a := val1, b:= val2)
| OnePosParam ProcParam -- ^ Call with positional params(only one supported): func(val)
-- | The select value in `/tbl?select=alias:field::cast`
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

+77 -3
View File
@@ -1,11 +1,12 @@
module Feature.RpcSpec where
import qualified Data.ByteString.Lazy as BL (empty)
import qualified Data.ByteString.Lazy as BL (empty, readFile)
import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
import Network.HTTP.Types
import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
@@ -145,8 +146,8 @@ spec actualPgVersion =
it "should fail with 300 Multiple Choices without explicit type casts" $
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
[json| {
"hint":"Overloaded functions with the same parameter name but different types are not supported",
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)" } |]
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)"}|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
@@ -1059,3 +1060,76 @@ spec actualPgVersion =
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
}
context "single unnamed param" $ do
it "can insert json directly" $
post "/rpc/unnamed_json_param"
[json|{"A": 1, "B": 2, "C": 3}|]
`shouldRespondWith`
[json|{"A": 1, "B": 2, "C": 3}|]
it "can insert text directly" $
request methodPost "/rpc/unnamed_text_param"
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
[str|unnamed text arg|]
`shouldRespondWith`
[str|unnamed text arg|]
it "can insert bytea directly" $ do
let file = unsafePerformIO $ BL.readFile "test/C.png"
r <- request methodPost "/rpc/unnamed_bytea_param"
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
file
liftIO $ do
let respBody = simpleBody r
respBody `shouldBe` file
it "will err when no function with single unnamed json parameter exists and application/json is specified" $
request methodPost "/rpc/unnamed_int_param" [("Content-Type", "application/json")]
[json|{"x": 1, "y": 2}|]
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache"
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
}
it "will err when no function with single unnamed text parameter exists and text/plain is specified" $
request methodPost "/rpc/unnamed_int_param"
[("Content-Type", "text/plain")]
[str|a simple text|]
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache"
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
}
it "will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified" $
let file = unsafePerformIO $ BL.readFile "test/C.png" in
request methodPost "/rpc/unnamed_int_param"
[("Content-Type", "application/octet-stream")]
file
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache"
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
}
it "will not be able to resolve when a single unnamed json parameter exists and other overloaded functions exist" $
request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]
[json|{"x": 1, "y": 2}|]
`shouldRespondWith`
[json| {
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
"message":"Could not choose the best candidate function between: test.overloaded_unnamed_param( => json), test.overloaded_unnamed_param(x => integer, y => integer)"}|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
+12 -10
View File
@@ -15,7 +15,7 @@ import Protolude hiding (get, toS)
import Protolude.Conv (toS)
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
import PostgREST.Request.Types (CallQuery (..))
import PostgREST.Request.Types
import PostgREST.DbStructure.Identifiers
import PostgREST.DbStructure.Proc
@@ -33,30 +33,32 @@ main = do
context "call proc query" $ do
it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False]
(Just [str| {"id": 3} |]) False False False [])
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
(KeyParams [ProcParam "id" "int" True False])
(Just [str| {"id": 3} |]) False False [])
liftIO $
cost `shouldSatisfy` (< Just 40)
it "should not exceed cost when calling setof composite proc with empty params" $ do
cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") [] Nothing False False False [])
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
liftIO $
cost `shouldSatisfy` (< Just 30)
it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
[ProcParam "a" "int" True False, ProcParam "b" "int" True False]
(Just [str| {"a": 3, "b": 4} |]) True False False [])
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
(Just [str| {"a": 3, "b": 4} |]) True False [])
liftIO $
cost `shouldSatisfy` (< Just 10)
context "params=multiple-objects" $ do
it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False]
(Just [str| [{"id": 1}, {"id": 4}] |]) False True False [])
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
(KeyParams [ProcParam "id" "int" True False])
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
liftIO $ do
-- lower bound needed for now to make sure that cost is not Nothing
cost `shouldSatisfy` (> Just 2000)
@@ -65,8 +67,8 @@ main = do
it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
[ProcParam "a" "int" True False, ProcParam "b" "int" True False]
(Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False False [])
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
(Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])
liftIO $
cost `shouldSatisfy` (< Just 10)
+26 -2
View File
@@ -1050,7 +1050,7 @@ create function test.ret_point_overloaded(x int, y int) returns test.point_2d as
select row(x, y)::test.point_2d;
$$ language sql;
create function test.ret_point_overloaded(json) returns json as $$
create function test.ret_point_overloaded(x json) returns json as $$
select $1;
$$ language sql;
@@ -2251,4 +2251,28 @@ A test for partitioned tables$$;
foreign key (id_a, name_a) references test.partitioned_a (id, name)
);
end if;
end$do$;
end$do$;
create or replace function test.unnamed_json_param(json) returns json as $$
select $1;
$$ language sql;
create or replace function test.unnamed_text_param(text) returns text as $$
select $1;
$$ language sql ;
create or replace function test.unnamed_bytea_param(bytea) returns bytea as $$
select $1::bytea;
$$ language sql ;
create or replace function test.unnamed_int_param(int) returns int as $$
select $1;
$$ language sql;
create or replace function test.overloaded_unnamed_param(json) returns int as $$
select $1;
$$ language sql;
create or replace function test.overloaded_unnamed_param(x int, y int) returns int as $$
select x + y;
$$ language sql;