perf: shortcut for proc with no variadic arg

Also refactor how rpc params are converted to json
This commit is contained in:
steve-chavez
2020-10-29 18:53:24 -05:00
committed by Steve Chavez
parent 2798ced9b9
commit 8618ffa5fc
7 changed files with 71 additions and 73 deletions
+54 -36
View File
@@ -70,6 +70,32 @@ data Target = TargetIdent QualifiedIdentifier
| TargetUnknown [Text] | TargetUnknown [Text]
deriving Eq deriving Eq
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
data RpcParamValue = Fixed Text | Variadic [Text]
instance JSON.ToJSON RpcParamValue where
toJSON (Fixed v) = JSON.toJSON v
toJSON (Variadic v) = JSON.toJSON v
toRpcParamValue :: ProcDescription -> (Text, Text) -> (Text, RpcParamValue)
toRpcParamValue proc (k, v) | argIsVariadic k = (k, Variadic [v])
| otherwise = (k, Fixed v)
where
argIsVariadic arg = isJust $ find (\PgArg{pgaName, pgaVar} -> pgaName == arg && pgaVar) $ pdArgs proc
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> PayloadJSON
jsonRpcParams proc prms =
if not $ pdHasVariadic proc then -- if proc has no variadic arg, save steps and directly convert to json
ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
else
let paramsMap = M.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
ProcessedJSON (JSON.encode paramsMap) (S.fromList $ M.keys paramsMap)
where
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
mergeParams _ v = v -- repeated params for non-variadic arguments are not merged
{-| {-|
Describes what the user wants to do. This data type is a Describes what the user wants to do. This data type is a
translation of the raw elements of an HTTP request into domain translation of the raw elements of an HTTP request into domain
@@ -187,33 +213,27 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
(Just ProcessedJSON{pjKeys}, _) -> pjKeys (Just ProcessedJSON{pjKeys}, _) -> pjKeys
(Just RawJSON{}, Just cls) -> cls (Just RawJSON{}, Just cls) -> cls
_ -> S.empty _ -> S.empty
payload = payload = case contentType of
case (contentType, action) of CTApplicationJSON ->
(_, ActionInvoke InvGet) -> Right rpcPrmsToJson if isJust columns
(_, ActionInvoke InvHead) -> Right rpcPrmsToJson then Right $ RawJSON reqBody
(CTApplicationJSON, _) -> else note "All object keys must match" . payloadAttributes reqBody
if isJust columns =<< if BL.null reqBody && isTargetingProc
then Right $ RawJSON reqBody then Right emptyObject
else note "All object keys must match" . payloadAttributes reqBody else JSON.eitherDecode reqBody
=<< if BL.null reqBody && isTargetingProc CTTextCSV -> do
then Right emptyObject json <- csvToJson <$> CSV.decodeByName reqBody
else JSON.eitherDecode reqBody note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
(CTTextCSV, _) -> do CTUrlEncoded ->
json <- csvToJson <$> CSV.decodeByName reqBody let urlEncodedBody = parseSimpleQuery $ toS reqBody in
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json case target of
(CTUrlEncoded, _) -> TargetProc{tProc} ->
let json = paramsFromList . map (toS *** toS) . parseSimpleQuery $ toS reqBody Right $ jsonRpcParams tProc $ (toS *** toS) <$> urlEncodedBody
keys = S.fromList $ M.keys json in _ ->
Right $ ProcessedJSON (JSON.encode json) keys let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> urlEncodedBody in
(ct, _) -> Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
Left $ toS $ "Content-Type not acceptable: " <> toMime ct ct ->
rpcPrmsToJson = ProcessedJSON (JSON.encode $ paramsFromList rpcQParams) (S.fromList $ fst <$> rpcQParams) Left $ toS $ "Content-Type not acceptable: " <> toMime ct
paramsFromList ls = M.fromListWith mergeParams $ toRpcParamsWith isVariadic ls
where
isVariadic k =
case target of
TargetProc{tProc} -> argIsVariadic tProc k
_ -> False
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
@@ -257,16 +277,14 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
["rpc", pName] -> TargetProc (callFindProc pName) False ["rpc", pName] -> TargetProc (callFindProc pName) False
other -> TargetUnknown other other -> TargetUnknown other
shouldParsePayload = shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionInvoke InvPost]
action `elem` relevantPayload = case (target, action) of
[ActionCreate, ActionUpdate, ActionSingleUpsert, -- Though ActionInvoke GET/HEAD doesn't really have a payload, we use the payload variable as a way
ActionInvoke InvPost,
-- Though ActionInvoke{isGet=True}(a GET /rpc/..) doesn't really have a payload, we use the payload variable as a way
-- to store the query string arguments to the function. -- to store the query string arguments to the function.
ActionInvoke InvGet, (TargetProc{tProc}, ActionInvoke InvGet) -> Just $ jsonRpcParams tProc rpcQParams
ActionInvoke InvHead] (TargetProc{tProc}, ActionInvoke InvHead) -> Just $ jsonRpcParams tProc rpcQParams
relevantPayload | shouldParsePayload = rightToMaybe payload _ | shouldParsePayload -> rightToMaybe payload
| otherwise = Nothing | otherwise -> Nothing
path = pathInfo req path = pathInfo req
method = requestMethod req method = requestMethod req
hdrs = requestHeaders req hdrs = requestHeaders req
+5 -1
View File
@@ -128,7 +128,7 @@ sourceColumnFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
decodeProcs :: HD.Result ProcsMap decodeProcs :: HD.Result ProcsMap
decodeProcs = decodeProcs =
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance -- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey) <$> HD.rowList procRow map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey . addHasVariadic) <$> HD.rowList procRow
where where
procRow = ProcDescription procRow = ProcDescription
<$> column HD.text <$> column HD.text
@@ -141,6 +141,10 @@ decodeProcs =
<*> column HD.bool <*> column HD.bool
<*> column HD.char) <*> column HD.char)
<*> (parseVolatility <$> column HD.char) <*> (parseVolatility <$> column HD.char)
<*> pure False
addHasVariadic :: ProcDescription -> ProcDescription
addHasVariadic pd@ProcDescription{pdArgs} = pd{pdHasVariadic=isJust $ find pgaVar pdArgs}
addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription) addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription)
addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd) addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)
+4 -30
View File
@@ -4,7 +4,6 @@ Description : PostgREST common types and functions used by the rest of the modul
-} -}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Types where module PostgREST.Types where
@@ -148,14 +147,15 @@ data ProcDescription = ProcDescription {
, pdArgs :: [PgArg] , pdArgs :: [PgArg]
, pdReturnType :: RetType , pdReturnType :: RetType
, pdVolatility :: ProcVolatility , pdVolatility :: ProcVolatility
, pdHasVariadic :: Bool
} deriving (Show, Eq) } deriving (Show, Eq)
-- Order by least number of args in the case of overloaded functions -- Order by least number of args in the case of overloaded functions
instance Ord ProcDescription where instance Ord ProcDescription where
ProcDescription schema1 name1 des1 args1 rt1 vol1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2 ProcDescription schema1 name1 des1 args1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2 hasVar2
| schema1 == schema2 && name1 == name2 && length args1 < length args2 = LT | schema1 == schema2 && name1 == name2 && length args1 < length args2 = LT
| schema2 == schema2 && name1 == name2 && length args1 > length args2 = GT | schema2 == schema2 && name1 == name2 && length args1 > length args2 = GT
| otherwise = (schema1, name1, des1, args1, rt1, vol1) `compare` (schema2, name2, des2, args2, rt2, vol2) | otherwise = (schema1, name1, des1, args1, rt1, vol1, hasVar1) `compare` (schema2, name2, des2, args2, rt2, vol2, hasVar2)
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription). -- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
-- | It uses a HashMap for a faster lookup. -- | It uses a HashMap for a faster lookup.
@@ -171,7 +171,7 @@ findProc qi payloadKeys paramsAsSingleObject allProcs = fromMaybe fallback bestM
where where
-- instead of passing Maybe ProcDescription around, we create a fallback description here when we can't find a matching function -- instead of passing Maybe ProcDescription around, we create a fallback description here when we can't find a matching function
-- args is empty, but because "specifiedProcArgs" will fill the missing arguments with default type text, this is not a problem -- args is empty, but because "specifiedProcArgs" will fill the missing arguments with default type text, this is not a problem
fallback = ProcDescription (qiSchema qi) (qiName qi) Nothing mempty (SetOf $ Composite $ QualifiedIdentifier "" "record") Volatile fallback = ProcDescription (qiSchema qi) (qiName qi) Nothing mempty (SetOf $ Composite $ QualifiedIdentifier mempty "record") Volatile False
bestMatch = bestMatch =
case M.lookup qi allProcs of case M.lookup qi allProcs of
Nothing -> Nothing Nothing -> Nothing
@@ -202,12 +202,6 @@ procTableName proc = case pdReturnType proc of
Single (Composite qi) -> Just $ qiName qi Single (Composite qi) -> Just $ qiName qi
_ -> Nothing _ -> Nothing
argIsVariadic :: ProcDescription -> Text -> Bool
argIsVariadic proc arg =
case find (\PgArg{pgaName} -> pgaName == arg) $ pdArgs proc of
Just PgArg{pgaVar} -> pgaVar
_ -> False
type Schema = Text type Schema = Text
type TableName = Text type TableName = Text
@@ -414,26 +408,6 @@ type Alias = Text
type Cast = Text type Cast = Text
type NodeName = Text type NodeName = Text
-- RPC query param, used for POST of form-data and GET requests
data RpcParamValue = Fixed Text | Variadic [Text]
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
-- repeated params for non-variadic arguments are not merged
mergeParams _ v = v
instance JSON.ToJSON RpcParamValue where
toJSON (Fixed v) = JSON.toJSON v
toJSON (Variadic v) = JSON.toJSON v
type RpcParams = [(Text, RpcParamValue)]
toRpcParamsWith :: (Text -> Bool) -> [(Text, Text)] -> RpcParams
toRpcParamsWith isVariadic ls = toRpcParamValue <$> ls
where
toRpcParamValue (k, v)
| isVariadic k = (k, Variadic [v])
| otherwise = (k, Fixed v)
{-| {-|
Custom guc header, it's obtained by parsing the json in a: Custom guc header, it's obtained by parsing the json in a:
+2 -1
View File
@@ -7,7 +7,8 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import PostgREST.Types (PgVersion, pgVersion112, pgVersion121, pgVersion95) import PostgREST.Types (PgVersion, pgVersion112, pgVersion121,
pgVersion95)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
+2 -1
View File
@@ -10,7 +10,8 @@ import Test.Hspec.Wai.JSON
import Text.Heredoc import Text.Heredoc
import PostgREST.Types (PgVersion, pgVersion96, pgVersion112, pgVersion121) import PostgREST.Types (PgVersion, pgVersion112, pgVersion121,
pgVersion96)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
+1 -1
View File
@@ -11,7 +11,7 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Text.Heredoc import Text.Heredoc
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
+3 -3
View File
@@ -3,7 +3,7 @@ module Feature.RpcSpec where
import qualified Data.ByteString.Lazy as BL (empty) import qualified Data.ByteString.Lazy as BL (empty)
import Network.Wai (Application) import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleBody, simpleStatus, simpleHeaders)) import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
import Network.HTTP.Types import Network.HTTP.Types
import Test.Hspec hiding (pendingWith) import Test.Hspec hiding (pendingWith)
@@ -523,8 +523,8 @@ spec actualPgVersion =
`shouldRespondWith` `shouldRespondWith`
[json|["hi", "there"]|] [json|["hi", "there"]|]
it "returns first value for repeated params without VARIADIC" $ it "returns last value for repeated params without VARIADIC" $
get "/rpc/sayhello?name=world&name=ignored" get "/rpc/sayhello?name=ignored&name=world"
`shouldRespondWith` `shouldRespondWith`
[json|"Hello, world"|] [json|"Hello, world"|]