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:
committed by
Steve Chavez
parent
caaa34b5de
commit
d4c6abbaec
+8
-1
@@ -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
|
- #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
|
- #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
|
### Fixed
|
||||||
|
|
||||||
- #1871, Fix OpenAPI missing default values for String types and identify Array types as "array" instead of "string" - @laurenceisla
|
- #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
|
## [8.0.0] - 2021-07-25
|
||||||
|
|
||||||
|
|||||||
+10
-6
@@ -60,7 +60,7 @@ data ApiRequestError
|
|||||||
| NoRelBetween Text Text
|
| NoRelBetween Text Text
|
||||||
| AmbiguousRelBetween Text Text [Relationship]
|
| AmbiguousRelBetween Text Text [Relationship]
|
||||||
| AmbiguousRpc [ProcDescription]
|
| AmbiguousRpc [ProcDescription]
|
||||||
| NoRpc Text Text [Text] Bool
|
| NoRpc Text Text [Text] Bool ContentType Bool
|
||||||
| InvalidFilters
|
| InvalidFilters
|
||||||
| UnacceptableSchema [Text]
|
| UnacceptableSchema [Text]
|
||||||
| ContentTypeError [ByteString]
|
| ContentTypeError [ByteString]
|
||||||
@@ -99,14 +99,18 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
|
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
|
||||||
"details" .= (compressedRel <$> rels) ]
|
"details" .= (compressedRel <$> rels) ]
|
||||||
toJSON (AmbiguousRpc procs) = JSON.object [
|
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])]
|
"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),
|
"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 <>
|
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
||||||
(if hasPreferSingleObject
|
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||||
then " function with a single json or jsonb parameter"
|
(True, _, _) -> " function with a single json or jsonb parameter"
|
||||||
else "(" <> T.intercalate ", " payloadKeys <> ")" <> " function") <>
|
(_, 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")]
|
" in the schema cache")]
|
||||||
toJSON UnsupportedVerb = JSON.object [
|
toJSON UnsupportedVerb = JSON.object [
|
||||||
"message" .= ("Unsupported HTTP verb" :: Text)]
|
"message" .= ("Unsupported HTTP verb" :: Text)]
|
||||||
|
|||||||
@@ -116,26 +116,27 @@ mutateRequestToQuery (Delete mainQi logicForest returnings) =
|
|||||||
H.sql (returningF mainQi returnings)
|
H.sql (returningF mainQi returnings)
|
||||||
|
|
||||||
requestToCallProcQuery :: CallRequest -> H.Snippet
|
requestToCallProcQuery :: CallRequest -> H.Snippet
|
||||||
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall singleParam returnings) =
|
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall returnings) =
|
||||||
prmsCTE <> argsBody
|
prmsCTE <> argsBody
|
||||||
where
|
where
|
||||||
(prmsCTE, argFrag)
|
(prmsCTE, argFrag) = case params of
|
||||||
| null params = (mempty, mempty)
|
OnePosParam prm -> ("WITH pgrst_args AS (SELECT NULL)", singleParameter args (encodeUtf8 $ ppType prm))
|
||||||
| singleParam = ("WITH pgrst_args AS (SELECT NULL)", jsonPlaceHolder args)
|
KeyParams [] -> (mempty, mempty)
|
||||||
| otherwise = (
|
KeyParams prms -> (
|
||||||
"WITH " <> normalizedBody args <> ", " <>
|
"WITH " <> normalizedBody args <> ", " <>
|
||||||
H.sql (
|
H.sql (
|
||||||
BS.unwords [
|
BS.unwords [
|
||||||
"pgrst_args AS (",
|
"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
|
, H.sql $ if multipleCall
|
||||||
then fmtParams varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
|
then fmtParams prms varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
|
||||||
else fmtParams varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
|
else fmtParams prms varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
|
||||||
)
|
)
|
||||||
|
|
||||||
fmtParams :: (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
|
fmtParams :: [ProcParam] -> (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
|
||||||
fmtParams prmFragPre prmFragSuf = BS.intercalate ", " ((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> params)
|
fmtParams prms prmFragPre prmFragSuf = BS.intercalate ", "
|
||||||
|
((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> prms)
|
||||||
|
|
||||||
varadicPrefix :: ProcParam -> SqlFragment
|
varadicPrefix :: ProcParam -> SqlFragment
|
||||||
varadicPrefix a = if ppVar a then "VARIADIC " else mempty
|
varadicPrefix a = if ppVar a then "VARIADIC " else mempty
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ module PostgREST.Query.SqlFragment
|
|||||||
, countF
|
, countF
|
||||||
, fromQi
|
, fromQi
|
||||||
, ftsOperators
|
, ftsOperators
|
||||||
, jsonPlaceHolder
|
|
||||||
, limitOffsetF
|
, limitOffsetF
|
||||||
, locationF
|
, locationF
|
||||||
, normalizedBody
|
, normalizedBody
|
||||||
@@ -31,6 +30,7 @@ module PostgREST.Query.SqlFragment
|
|||||||
, responseStatusF
|
, responseStatusF
|
||||||
, returningF
|
, returningF
|
||||||
, selectBody
|
, selectBody
|
||||||
|
, singleParameter
|
||||||
, sourceCTEName
|
, sourceCTEName
|
||||||
, unknownEncoder
|
, unknownEncoder
|
||||||
, intercalateSnippet
|
, 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
|
-- 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
|
-- 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
|
-- 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 :: Maybe BL.ByteString -> H.Snippet
|
||||||
normalizedBody body =
|
normalizedBody body =
|
||||||
"pgrst_payload AS (SELECT " <> jsonPlaceHolder body <> " AS json_data), " <>
|
"pgrst_payload AS (SELECT " <> jsonPlaceHolder <> " AS json_data), " <>
|
||||||
H.sql (BS.unwords [
|
H.sql (BS.unwords [
|
||||||
"pgrst_body AS (",
|
"pgrst_body AS (",
|
||||||
"SELECT",
|
"SELECT",
|
||||||
@@ -116,12 +117,14 @@ normalizedBody body =
|
|||||||
"ELSE json_build_array(json_data)",
|
"ELSE json_build_array(json_data)",
|
||||||
"END AS val",
|
"END AS val",
|
||||||
"FROM pgrst_payload)"])
|
"FROM pgrst_payload)"])
|
||||||
|
where
|
||||||
|
jsonPlaceHolder = H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
|
||||||
|
|
||||||
-- | Equivalent to "$1::json"
|
singleParameter :: Maybe BL.ByteString -> ByteString -> H.Snippet
|
||||||
-- | TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
singleParameter body typ =
|
||||||
jsonPlaceHolder :: Maybe BL.ByteString -> H.Snippet
|
if typ == "bytea"
|
||||||
jsonPlaceHolder body =
|
then H.encoderAndParam (HE.nullable HE.bytea) (toS <$> body) -- needed because bytea fails with HE.unknown(pg tries to utf8 encode)
|
||||||
H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
|
else H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::" <> H.sql typ
|
||||||
|
|
||||||
selectBody :: SqlFragment
|
selectBody :: SqlFragment
|
||||||
selectBody = "(SELECT val FROM pgrst_body)"
|
selectBody = "(SELECT val FROM pgrst_body)"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module PostgREST.Request.ApiRequest
|
|||||||
, ContentType(..)
|
, ContentType(..)
|
||||||
, Action(..)
|
, Action(..)
|
||||||
, Target(..)
|
, Target(..)
|
||||||
, PayloadJSON(..)
|
, Payload(..)
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -74,18 +74,19 @@ import Protolude.Conv (toS)
|
|||||||
|
|
||||||
type RequestBody = BL.ByteString
|
type RequestBody = BL.ByteString
|
||||||
|
|
||||||
data PayloadJSON
|
data Payload
|
||||||
= ProcessedJSON -- ^ Cached attributes of a JSON 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
|
-- ^ 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
|
-- cache this instead of an Aeson Value because it was detected that for
|
||||||
-- large payloads the encoding had high memory usage, see
|
-- large payloads the encoding had high memory usage, see
|
||||||
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
|
-- 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
|
-- ^ Keys of the object or if it's an array these keys are guaranteed to
|
||||||
-- be the same across all its objects
|
-- 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
|
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
||||||
-- | Types of things a user wants to do to tables/views/procs
|
-- | 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
|
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"}
|
-- | 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 =
|
jsonRpcParams proc prms =
|
||||||
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
|
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)
|
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 (Variadic a) (Variadic b) = Variadic $ b ++ a
|
||||||
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
|
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 =
|
targetToJsonRpcParams target params =
|
||||||
case target of
|
case target of
|
||||||
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
|
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
|
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
|
||||||
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
||||||
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
, 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
|
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
||||||
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
||||||
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
||||||
@@ -253,7 +254,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
isTargetingDefaultSpec = case path of
|
isTargetingDefaultSpec = case path of
|
||||||
PathInfo{pIsDefaultSpec=True} -> True
|
PathInfo{pIsDefaultSpec=True} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
contentType = ContentType.decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
contentType = maybe CTApplicationJSON ContentType.decodeContentType $ lookupHeader "content-type"
|
||||||
columns
|
columns
|
||||||
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
@@ -264,9 +265,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
|
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
|
||||||
(CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody
|
(CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody
|
||||||
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of
|
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of
|
||||||
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
|
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||||
(Just RawJSON{}, Just cls) -> cls
|
(Just RawJSON{}, Just cls) -> cls
|
||||||
_ -> S.empty
|
_ -> S.empty
|
||||||
payload = case contentType of
|
payload = case contentType of
|
||||||
CTApplicationJSON ->
|
CTApplicationJSON ->
|
||||||
if isJust columns
|
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
|
let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> parseSimpleQuery (toS reqBody) in
|
||||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
||||||
ct ->
|
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
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
action =
|
action =
|
||||||
case method of
|
case method of
|
||||||
@@ -321,7 +324,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
schema = fromMaybe defaultSchema profile
|
schema = fromMaybe defaultSchema profile
|
||||||
target =
|
target =
|
||||||
let
|
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
|
in
|
||||||
case path of
|
case path of
|
||||||
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
||||||
@@ -426,7 +431,7 @@ csvToJson (_, vals) =
|
|||||||
else JSON.String $ toS str
|
else JSON.String $ toS str
|
||||||
)
|
)
|
||||||
|
|
||||||
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
|
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
|
||||||
payloadAttributes raw json =
|
payloadAttributes raw json =
|
||||||
-- Test that Array contains only Objects having the same keys
|
-- Test that Array contains only Objects having the same keys
|
||||||
case json of
|
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,
|
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.
|
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 :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ContentType -> Bool -> Either ApiRequestError ProcDescription
|
||||||
findProc qi argumentsKeys paramsAsSingleObject allProcs =
|
findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
|
||||||
case matchProc of
|
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
|
[proc] -> Right proc
|
||||||
procs -> Left $ AmbiguousRpc (toList procs)
|
procs -> Left $ AmbiguousRpc (toList procs)
|
||||||
where
|
where
|
||||||
matchProc = filter matchesParams $ M.lookupDefault mempty qi allProcs -- first find the proc by name
|
matchProc = filter matchesParams $ M.lookupDefault mempty qi allProcs -- first find the proc by name
|
||||||
matchesParams proc =
|
matchesParams proc =
|
||||||
let params = pdParams proc in
|
let params = pdParams proc in
|
||||||
-- here we don't match by argument key(there isn't one) but by the single parameter type
|
-- exceptional case for Prefer: params=single-object
|
||||||
if paramsAsSingleObject then
|
if paramsAsSingleObject
|
||||||
case params of
|
then length params == 1 && (ppType <$> headMay params) `elem` [Just "json", Just "jsonb"]
|
||||||
[prm] -> ppType prm `elem` ["json", "jsonb"]
|
-- If the function has no parameters, the arguments keys must be empty as well
|
||||||
_ -> False
|
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
|
-- 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.
|
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
||||||
else case L.partition ppReq params of
|
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
|
-- If the function only has required parameters, the arguments keys must match those parameters
|
||||||
(reqParams, []) -> argumentsKeys == S.fromList (ppName <$> reqParams)
|
(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)
|
-- If the function only has optional parameters, the arguments keys can match none or any of them(a subset)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
|||||||
restrictRange)
|
restrictRange)
|
||||||
import PostgREST.Request.ApiRequest (Action (..),
|
import PostgREST.Request.ApiRequest (Action (..),
|
||||||
ApiRequest (..),
|
ApiRequest (..),
|
||||||
PayloadJSON (..))
|
Payload (..))
|
||||||
|
|
||||||
import PostgREST.Request.Parsers
|
import PostgREST.Request.Parsers
|
||||||
import PostgREST.Request.Preferences
|
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
|
-- update/delete filters can be only on the root table
|
||||||
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
||||||
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
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 :: ProcDescription -> ApiRequest -> ReadRequest -> CallRequest
|
||||||
callRequest proc apiReq readReq = FunctionCall {
|
callRequest proc apiReq readReq = FunctionCall {
|
||||||
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
||||||
, funCParams = specifiedParams
|
, funCParams = callParams
|
||||||
, funCArgs = pjRaw <$> iPayload apiReq
|
, funCArgs = payRaw <$> iPayload apiReq
|
||||||
, funCScalar = procReturnsScalar proc
|
, funCScalar = procReturnsScalar proc
|
||||||
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
||||||
, funCSingleParam = paramsAsSingleObject
|
|
||||||
, funCReturning = returningCols readReq []
|
, funCReturning = returningCols readReq []
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
||||||
specifiedParams =
|
callParams = case pdParams proc of
|
||||||
if paramsAsSingleObject
|
[prm] | paramsAsSingleObject -> OnePosParam prm
|
||||||
then pdParams proc
|
| ppName prm == mempty -> OnePosParam prm
|
||||||
else filter (\x -> ppName x `S.member` iColumns apiReq) $ pdParams proc
|
| otherwise -> KeyParams $ specifiedParams [prm]
|
||||||
|
prms -> KeyParams $ specifiedParams prms
|
||||||
|
specifiedParams params = filter (\x -> ppName x `S.member` iColumns apiReq) params
|
||||||
|
|
||||||
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
||||||
returningCols rr@(Node _ forest) pkCols
|
returningCols rr@(Node _ forest) pkCols
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ module PostgREST.Request.Types
|
|||||||
, Field
|
, Field
|
||||||
, Filter(..)
|
, Filter(..)
|
||||||
, CallQuery(..)
|
, CallQuery(..)
|
||||||
|
, CallParams(..)
|
||||||
, CallRequest
|
, CallRequest
|
||||||
, JoinCondition(..)
|
, JoinCondition(..)
|
||||||
, JsonOperand(..)
|
, JsonOperand(..)
|
||||||
@@ -40,7 +41,7 @@ import qualified GHC.Show (show)
|
|||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
import PostgREST.DbStructure.Proc (ProcParam)
|
import PostgREST.DbStructure.Proc (ProcParam (..))
|
||||||
import PostgREST.DbStructure.Relationship (Relationship)
|
import PostgREST.DbStructure.Relationship (Relationship)
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.Request.Preferences (PreferResolution)
|
import PostgREST.Request.Preferences (PreferResolution)
|
||||||
@@ -127,14 +128,17 @@ data MutateQuery
|
|||||||
|
|
||||||
data CallQuery = FunctionCall
|
data CallQuery = FunctionCall
|
||||||
{ funCQi :: QualifiedIdentifier
|
{ funCQi :: QualifiedIdentifier
|
||||||
, funCParams :: [ProcParam]
|
, funCParams :: CallParams
|
||||||
, funCArgs :: Maybe BL.ByteString
|
, funCArgs :: Maybe BL.ByteString
|
||||||
, funCScalar :: Bool
|
, funCScalar :: Bool
|
||||||
, funCMultipleCall :: Bool
|
, funCMultipleCall :: Bool
|
||||||
, funCSingleParam :: Bool
|
|
||||||
, funCReturning :: [FieldName]
|
, 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`
|
-- | The select value in `/tbl?select=alias:field::cast`
|
||||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 324 B |
+77
-3
@@ -1,11 +1,12 @@
|
|||||||
module Feature.RpcSpec where
|
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 (Application)
|
||||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
@@ -145,8 +146,8 @@ spec actualPgVersion =
|
|||||||
it "should fail with 300 Multiple Choices without explicit type casts" $
|
it "should fail with 300 Multiple Choices without explicit type casts" $
|
||||||
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
|
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"Overloaded functions with the same parameter name but different types are not supported",
|
"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)" } |]
|
"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
|
{ matchStatus = 300
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -1059,3 +1060,76 @@ spec actualPgVersion =
|
|||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, 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
@@ -15,7 +15,7 @@ import Protolude hiding (get, toS)
|
|||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
|
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
|
||||||
import PostgREST.Request.Types (CallQuery (..))
|
import PostgREST.Request.Types
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers
|
import PostgREST.DbStructure.Identifiers
|
||||||
import PostgREST.DbStructure.Proc
|
import PostgREST.DbStructure.Proc
|
||||||
@@ -33,30 +33,32 @@ 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 $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False]
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||||
(Just [str| {"id": 3} |]) False False False [])
|
(KeyParams [ProcParam "id" "int" True False])
|
||||||
|
(Just [str| {"id": 3} |]) False False [])
|
||||||
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 $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") [] Nothing False False False [])
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
|
||||||
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 $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||||
[ProcParam "a" "int" True False, ProcParam "b" "int" True False]
|
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
||||||
(Just [str| {"a": 3, "b": 4} |]) True False False [])
|
(Just [str| {"a": 3, "b": 4} |]) True False [])
|
||||||
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 $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False]
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||||
(Just [str| [{"id": 1}, {"id": 4}] |]) False True False [])
|
(KeyParams [ProcParam "id" "int" True False])
|
||||||
|
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
-- lower bound needed for now to make sure that cost is not Nothing
|
-- lower bound needed for now to make sure that cost is not Nothing
|
||||||
cost `shouldSatisfy` (> Just 2000)
|
cost `shouldSatisfy` (> Just 2000)
|
||||||
@@ -65,8 +67,8 @@ main = do
|
|||||||
it "should not exceed cost when calling scalar proc" $ do
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
cost <- exec pool $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||||
[ProcParam "a" "int" True False, ProcParam "b" "int" True 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 False [])
|
(Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])
|
||||||
liftIO $
|
liftIO $
|
||||||
cost `shouldSatisfy` (< Just 10)
|
cost `shouldSatisfy` (< Just 10)
|
||||||
|
|
||||||
|
|||||||
Vendored
+26
-2
@@ -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;
|
select row(x, y)::test.point_2d;
|
||||||
$$ language sql;
|
$$ 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;
|
select $1;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
@@ -2251,4 +2251,28 @@ A test for partitioned tables$$;
|
|||||||
foreign key (id_a, name_a) references test.partitioned_a (id, name)
|
foreign key (id_a, name_a) references test.partitioned_a (id, name)
|
||||||
);
|
);
|
||||||
end if;
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user