From ca7ffd0a70fc7cb4cc4c76f2b6bf2af9ae762a35 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 8 Oct 2020 15:32:14 +0200 Subject: [PATCH] Fix RPC return type handling for domains with composite base type --- CHANGELOG.md | 1 + src/PostgREST/DbStructure.hs | 66 ++++++++++++++++++++++++------------ test/Feature/RpcSpec.hs | 16 +++++++++ test/fixtures/schema.sql | 24 +++++++++++++ 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91e6c12d..ae3c23d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #1162, Fix location header for POST request with select= without PK - @wolfgangwalther - #1585, Fix error messages on connection failure for localized postgres on Windows - @wolfgangwalther - #1636, Fix `application/octet-stream` appending `charset=utf-8` - @steve-chavez + - #1615, Fix RPC return type handling and embedding for domains with composite base type - @wolfgangwalther ### Changed diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 0caf949f3..8a7605c0f 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -140,7 +140,7 @@ decodeProcs = <$> column HD.text <*> column HD.text <*> column HD.bool - <*> column HD.char) + <*> column HD.bool) <*> (parseVolatility <$> column HD.char) <*> pure False @@ -165,18 +165,15 @@ decodeProcs = else Just $ PgArg (dropAround (== '"') name) (strip typ) (T.null def) isVariadic - parseRetType :: Text -> Text -> Bool -> Char -> RetType - parseRetType schema name isSetOf typ + parseRetType :: Text -> Text -> Bool -> Bool -> RetType + parseRetType schema name isSetOf isComposite | isSetOf = SetOf pgType | otherwise = Single pgType where qi = QualifiedIdentifier schema name - pgType = case typ of - 'c' -> Composite qi - 'p' -> if name == "record" -- Only pg pseudo type that is a row type is 'record' - then Composite qi - else Scalar qi - _ -> Scalar qi -- 'b'ase, 'd'omain, 'e'num, 'r'ange + pgType + | isComposite = Composite qi + | otherwise = Scalar qi parseVolatility :: Char -> ProcVolatility parseVolatility v | v == 'i' = Immutable @@ -195,22 +192,47 @@ accessibleProcs = H.Statement (toS sql) (param HE.text) decodeProcs True procsSqlQuery :: SqlQuery procsSqlQuery = [q| + -- Recursively get the base types of domains + WITH RECURSIVE + rec_types AS ( + SELECT + oid, + typbasetype, + COALESCE(NULLIF(typbasetype, 0), oid) AS base + FROM pg_type + UNION + SELECT + t.oid, + b.typbasetype, + COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base + FROM rec_types t + JOIN pg_type b ON t.typbasetype = b.oid + ), + base_types AS ( + SELECT + oid, + base + FROM rec_types + WHERE typbasetype = 0 + ) SELECT - pn.nspname as proc_schema, - p.proname as proc_name, - d.description as proc_description, - pg_get_function_arguments(p.oid) as args, - tn.nspname as rettype_schema, - coalesce(comp.relname, t.typname) as rettype_name, - p.proretset as rettype_is_setof, - t.typtype as rettype_typ, + pn.nspname AS proc_schema, + p.proname AS proc_name, + d.description AS proc_description, + pg_get_function_arguments(p.oid) AS args, + tn.nspname AS schema, + COALESCE(comp.relname, t.typname) AS name, + p.proretset AS rettype_is_setof, + -- Only pg pseudo type that is a row type is 'record' + (t.typtype = 'c' or t.typtype = 'p' and t.typname = 'record') AS rettype_is_composite, p.provolatile FROM pg_proc p - JOIN pg_namespace pn ON pn.oid = p.pronamespace - JOIN pg_type t ON t.oid = p.prorettype - JOIN pg_namespace tn ON tn.oid = t.typnamespace - LEFT JOIN pg_class comp ON comp.oid = t.typrelid - LEFT JOIN pg_catalog.pg_description as d on d.objoid = p.oid + JOIN pg_namespace pn ON pn.oid = p.pronamespace + JOIN base_types bt ON bt.oid = p.prorettype + JOIN pg_type t ON t.oid = bt.base + JOIN pg_namespace tn ON tn.oid = t.typnamespace + LEFT JOIN pg_class comp ON comp.oid = t.typrelid + LEFT JOIN pg_catalog.pg_description as d ON d.objoid = p.oid |] schemaDescription :: H.Statement Schema (Maybe Text) diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 05863f01d..7a1c4fd28 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -197,6 +197,16 @@ spec actualPgVersion = ]|] { matchHeaders = [matchContentTypeJson] } + when (actualPgVersion >= pgVersion110) $ + it "can embed if rpc returns domain of table type" $ do + post "/rpc/getproject_domain?select=id,name,client:clients(id),tasks(id)" + [json| { "id": 1} |] + `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] + get "/rpc/getproject_domain?id=1&select=id,name,client:clients(id),tasks(id)" + `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] + context "a proc that returns an empty rowset" $ it "returns empty json array" $ do post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith` @@ -258,6 +268,12 @@ spec actualPgVersion = it "cannot return composite type in hidden schema" $ post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401 + when (actualPgVersion >= pgVersion110) $ + it "returns domain of composite type" $ + post "/rpc/ret_composite_domain" [json|{}|] `shouldRespondWith` + [json|[{"x": 10, "y": 5}]|] + { matchHeaders = [matchContentTypeJson] } + it "returns single row from table" $ post "/rpc/single_article?select=id" [json|{"id": 2}|] `shouldRespondWith` [json|[{"id": 2}]|] diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 519407c78..12371d403 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -925,6 +925,19 @@ CREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF project update test.projects set name = $3 WHERE id >= $1 AND id <= $2 returning *; $_$; +-- domains on tables are only supported from pg 11 on +DO $do$BEGIN + IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN + CREATE DOMAIN projects_domain AS projects; + + CREATE FUNCTION getproject_domain(id int) RETURNS SETOF projects_domain + LANGUAGE sql + AS $_$ + SELECT projects::projects_domain FROM test.projects WHERE id = $1; + $_$; + END IF; +END$do$; + create table images ( name text not null, img bytea not null @@ -970,6 +983,17 @@ create function test.ret_point_2d() returns test.point_2d as $$ select row(10, 5)::test.point_2d; $$ language sql; +-- domains on composite types are only supported from pg 11 on +do $do$begin + if (SELECT current_setting('server_version_num')::int >= 110000) then + create domain test.composite_domain as test.point_2d; + + create function test.ret_composite_domain() returns test.composite_domain as $$ + select row(10, 5)::test.composite_domain; + $$ language sql; + end if; +end$do$; + create type private.point_3d as (x integer, y integer, z integer); create function test.ret_point_3d() returns private.point_3d as $$