From bdfb11001e375fc692e20df97351ba3a01335b27 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 1 Apr 2019 13:45:38 -0500 Subject: [PATCH] Fix #1264, allow bulk RPC call --- CHANGELOG.md | 1 + src/PostgREST/QueryBuilder.hs | 77 ++++++++++++++++++++--------------- test/Feature/RpcSpec.hs | 36 ++++++++++++++++ test/fixtures/schema.sql | 7 +++- 4 files changed, 87 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65375073c..1ad24750c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #690, Add `?columns` query parameter for faster bulk inserts, also ignores unspecified json keys in a payload - @steve-chavez - #1239, Add support for resource embedding on materialized views - @vitorbaptista +- #1264, Add support for bulk RPC call - @steve-chavez ### Fixed diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 443464e31..69c18cbe4 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -148,29 +148,18 @@ callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bo callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField pgVer = unicodeStatement sql (HE.param HE.unknown) decodeProc True where - sql = - if returnsScalar then [qc| - WITH {argsRecord}, - {sourceCTEName} AS ( - SELECT {fromQi qi}({args}) - ) - SELECT - {countResultF} AS total_result_set, - 1 AS page_total, - {scalarBodyF} AS body, - {responseHeaders} AS response_headers - FROM ({selectQuery}) _postgrest_t;|] - else [qc| - WITH {argsRecord}, - {sourceCTEName} AS ( - SELECT * FROM {fromQi qi}({args}) - ) - SELECT - {countResultF} AS total_result_set, - pg_catalog.count(_postgrest_t) AS page_total, - {bodyF} AS body, - {responseHeaders} AS response_headers - FROM ({selectQuery}) _postgrest_t;|] + sql =[qc| + WITH + {argsRecord}, + {sourceCTEName} AS ( + {sourceBody} + ) + SELECT + {countResultF} AS total_result_set, + pg_catalog.count(_postgrest_t) AS page_total, + {bodyF} AS body, + {responseHeaders} AS response_headers + FROM ({selectQuery}) _postgrest_t;|] (argsRecord, args) | paramsAsSingleObject = ("_args_record AS (SELECT NULL)", "$1::json") @@ -182,25 +171,47 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para "SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")", ")"] - , intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM _args_record)") <$> pgArgs)) + , intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := _args_record." <> pgFmtIdent (pgaName a)) <$> pgArgs)) + + sourceBody :: SqlFragment + sourceBody + | paramsAsSingleObject || null pgArgs = + if returnsScalar + then [qc| SELECT {fromQi qi}({args}) |] + else [qc| SELECT * FROM {fromQi qi}({args}) |] + | otherwise = + if returnsScalar + then [qc| SELECT {fromQi qi}({args}) FROM _args_record |] + else [qc| SELECT _.* + FROM _args_record, + LATERAL ( SELECT * FROM {fromQi qi}({args}) ) _ |] + + bodyF + | returnsScalar = scalarBodyF + | isSingle = asJsonSingleF + | asCsv = asCsvF + | isJust binaryField = asBinaryF $ fromJust binaryField + | otherwise = asJsonF + + scalarBodyF + | asBinary = asBinaryF _procName + | otherwise = unwords [ + "CASE", + "WHEN pg_catalog.count(_postgrest_t) = 1", + "THEN (json_agg(_postgrest_t." <> pgFmtIdent _procName <> ")->0)::character varying", + "ELSE (json_agg(_postgrest_t." <> pgFmtIdent _procName <> "))::character varying", + "END"] + countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text _procName = qiName qi responseHeaders = if pgVer >= pgVersion96 then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15 else "'[]'" :: Text + decodeProc = HD.rowMaybe procRow procRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8 <*> HD.column HD.bytea <*> HD.column HD.bytea - scalarBodyF - | asBinary = asBinaryF _procName - | otherwise = "(row_to_json(_postgrest_t)->" <> pgFmtLit _procName <> ")::character varying" - - bodyF - | isSingle = asJsonSingleF - | asCsv = asCsvF - | isJust binaryField = asBinaryF $ fromJust binaryField - | otherwise = asJsonF pgFmtIdent :: SqlFragment -> SqlFragment pgFmtIdent x = "\"" <> replace "\"" "\"\"" (trimNullChars $ toS x) <> "\"" diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 2c3ba2a3a..c3752b660 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -349,6 +349,42 @@ spec = [json|"Hello, John"|] { matchHeaders = [matchContentTypeJson] } + context "bulk RPC" $ do + it "works with a scalar function an returns a json array" $ + post "/rpc/add_them" + [json|[ + {"a": 1, "b": 2}, + {"a": 4, "b": 6}, + {"a": 100, "b": 200} + ]|] `shouldRespondWith` + [json| + [3, 10, 300] + |] { matchHeaders = [matchContentTypeJson] } + + it "works with a scalar function an returns a json array when posting CSV" $ + request methodPost "/rpc/add_them" [("Content-Type", "text/csv")] + "a,b\n1,2\n4,6\n100,200" + `shouldRespondWith` + [json| + [3, 10, 300] + |] + { matchStatus = 200 + , matchHeaders = [matchContentTypeJson] + } + + it "works with a non-scalar result" $ + post "/rpc/get_projects_below?select=id,name" + [json|[ + {"id": 1}, + {"id": 5} + ]|] `shouldRespondWith` + [json| + [{"id":1,"name":"Windows 7"}, + {"id":2,"name":"Windows 10"}, + {"id":3,"name":"IOS"}, + {"id":4,"name":"OSX"}] + |] { matchHeaders = [matchContentTypeJson] } + context "only for GET rpc" $ do it "should fail on mutating procs" $ do get "/rpc/callcounter" `shouldRespondWith` 500 diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 4c6cd0ce3..ff62b1e4d 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1659,4 +1659,9 @@ CREATE TABLE test.openapi_types( "a_numeric" numeric, "a_real" real, "a_double_precision" double precision -); \ No newline at end of file +); + +create function add_them(a integer, b integer) +returns integer as $$ + select a + b; +$$ language sql;