feat: Include partitioned tables into the schema cache
Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables
This commit is contained in:
@@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
## [8.0.0] - 2021-07-25
|
## [8.0.0] - 2021-07-25
|
||||||
|
|||||||
@@ -323,38 +323,47 @@ accessibleTables =
|
|||||||
relname as table_name,
|
relname as table_name,
|
||||||
d.description as table_description,
|
d.description as table_description,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind IN ('r','p')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
OR (
|
||||||
OR EXISTS (
|
c.relkind IN ('v','f')
|
||||||
SELECT 1
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
||||||
FROM pg_trigger
|
OR EXISTS (
|
||||||
WHERE
|
SELECT 1
|
||||||
pg_trigger.tgrelid = c.oid
|
FROM pg_trigger
|
||||||
AND (pg_trigger.tgtype::integer & 69) = 69
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
AND (pg_trigger.tgtype::integer & 69) = 69
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) AS insertable,
|
) AS insertable,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind IN ('r','p')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
OR (
|
||||||
-- CMD_UPDATE
|
c.relkind IN ('v','f')
|
||||||
OR EXISTS (
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
||||||
SELECT 1
|
-- CMD_UPDATE
|
||||||
FROM pg_trigger
|
OR EXISTS (
|
||||||
WHERE
|
SELECT 1
|
||||||
pg_trigger.tgrelid = c.oid
|
FROM pg_trigger
|
||||||
and (pg_trigger.tgtype::integer & 81) = 81
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
and (pg_trigger.tgtype::integer & 81) = 81
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) as updatable,
|
) as updatable,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind IN ('r','p')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
OR (
|
||||||
-- CMD_DELETE
|
c.relkind IN ('v','f')
|
||||||
OR EXISTS (
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
||||||
SELECT 1
|
-- CMD_DELETE
|
||||||
FROM pg_trigger
|
OR EXISTS (
|
||||||
WHERE
|
SELECT 1
|
||||||
pg_trigger.tgrelid = c.oid
|
FROM pg_trigger
|
||||||
and (pg_trigger.tgtype::integer & 73) = 73
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
and (pg_trigger.tgtype::integer & 73) = 73
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) as deletable
|
) as deletable
|
||||||
from
|
from
|
||||||
@@ -362,7 +371,7 @@ accessibleTables =
|
|||||||
join pg_namespace n on n.oid = c.relnamespace
|
join pg_namespace n on n.oid = c.relnamespace
|
||||||
left join pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
left join pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
||||||
where
|
where
|
||||||
c.relkind in ('v', 'r', 'm', 'f')
|
c.relkind in ('v','r','m','f','p')
|
||||||
and n.nspname = $1
|
and n.nspname = $1
|
||||||
and (
|
and (
|
||||||
pg_has_role(c.relowner, 'USAGE')
|
pg_has_role(c.relowner, 'USAGE')
|
||||||
@@ -468,7 +477,7 @@ allTables =
|
|||||||
c.relname AS table_name,
|
c.relname AS table_name,
|
||||||
d.description AS table_description,
|
d.description AS table_description,
|
||||||
(
|
(
|
||||||
c.relkind = 'r'
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
c.relkind in ('v','f')
|
c.relkind in ('v','f')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
||||||
@@ -488,7 +497,7 @@ allTables =
|
|||||||
)
|
)
|
||||||
) AS insertable,
|
) AS insertable,
|
||||||
(
|
(
|
||||||
c.relkind = 'r'
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
c.relkind in ('v','f')
|
c.relkind in ('v','f')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
||||||
@@ -504,7 +513,7 @@ allTables =
|
|||||||
)
|
)
|
||||||
) AS updatable,
|
) AS updatable,
|
||||||
(
|
(
|
||||||
c.relkind = 'r'
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
c.relkind in ('v','f')
|
c.relkind in ('v','f')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
||||||
@@ -522,7 +531,7 @@ allTables =
|
|||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
LEFT JOIN pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
LEFT JOIN pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
||||||
WHERE c.relkind IN ('v','r','m','f')
|
WHERE c.relkind IN ('v','r','m','f','p')
|
||||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
ORDER BY table_schema, table_name |]
|
ORDER BY table_schema, table_name |]
|
||||||
|
|
||||||
@@ -559,7 +568,7 @@ allColumns tabs =
|
|||||||
pg_catalog.pg_namespace n
|
pg_catalog.pg_namespace n
|
||||||
WHERE
|
WHERE
|
||||||
r.contype IN ('f', 'p', 'u')
|
r.contype IN ('f', 'p', 'u')
|
||||||
AND c.relkind IN ('r', 'v', 'f', 'm')
|
AND c.relkind IN ('r', 'v', 'f', 'm', 'p')
|
||||||
AND r.conrelid = c.oid
|
AND r.conrelid = c.oid
|
||||||
AND c.relnamespace = n.oid
|
AND c.relnamespace = n.oid
|
||||||
AND n.nspname <> ANY (ARRAY['pg_catalog', 'information_schema'] || $1)
|
AND n.nspname <> ANY (ARRAY['pg_catalog', 'information_schema'] || $1)
|
||||||
@@ -617,7 +626,7 @@ allColumns tabs =
|
|||||||
NOT pg_is_other_temp_schema(nc.oid)
|
NOT pg_is_other_temp_schema(nc.oid)
|
||||||
AND a.attnum > 0
|
AND a.attnum > 0
|
||||||
AND NOT a.attisdropped
|
AND NOT a.attisdropped
|
||||||
AND c.relkind in ('r', 'v', 'f', 'm')
|
AND c.relkind in ('r', 'v', 'f', 'm', 'p')
|
||||||
-- Filter only columns that are FK/PK or in the api schema:
|
-- Filter only columns that are FK/PK or in the api schema:
|
||||||
AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL)
|
AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL)
|
||||||
)
|
)
|
||||||
@@ -716,7 +725,7 @@ allPrimaryKeys tabs =
|
|||||||
nc.oid = c.connamespace
|
nc.oid = c.connamespace
|
||||||
AND nr.oid = r.relnamespace
|
AND nr.oid = r.relnamespace
|
||||||
AND c.conrelid = r.oid
|
AND c.conrelid = r.oid
|
||||||
AND r.relkind = 'r'
|
AND r.relkind IN ('r', 'p')
|
||||||
AND NOT pg_is_other_temp_schema(nr.oid)
|
AND NOT pg_is_other_temp_schema(nr.oid)
|
||||||
AND c.contype = 'p'
|
AND c.contype = 'p'
|
||||||
),
|
),
|
||||||
@@ -753,7 +762,7 @@ allPrimaryKeys tabs =
|
|||||||
AND r.oid = c.conrelid
|
AND r.oid = c.conrelid
|
||||||
AND nc.oid = c.connamespace
|
AND nc.oid = c.connamespace
|
||||||
AND c.contype in ('p', 'u', 'f')
|
AND c.contype in ('p', 'u', 'f')
|
||||||
AND r.relkind = 'r'
|
AND r.relkind IN ('r', 'p')
|
||||||
AND NOT pg_is_other_temp_schema(nr.oid)
|
AND NOT pg_is_other_temp_schema(nr.oid)
|
||||||
) ss
|
) ss
|
||||||
WHERE
|
WHERE
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion112,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
||||||
pgVersion130)
|
pgVersion112, pgVersion130)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
@@ -111,7 +111,7 @@ spec actualPgVersion = do
|
|||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "requesting headers only representation" $
|
context "requesting headers only representation" $ do
|
||||||
it "should not throw and return location header when selecting without PK" $
|
it "should not throw and return location header when selecting without PK" $
|
||||||
request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")]
|
request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")]
|
||||||
[json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` ""
|
[json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` ""
|
||||||
@@ -120,6 +120,15 @@ spec actualPgVersion = do
|
|||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "should not throw and return location header for partitioned tables when selecting without PK" $
|
||||||
|
request methodPost "/partitioned_a" [("Prefer", "return=headers-only")]
|
||||||
|
[json|{"id":5,"name":"first"}|] `shouldRespondWith` ""
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = [ "Location" <:> "/partitioned_a?id=eq.5&name=eq.first"
|
||||||
|
, "Content-Range" <:> "*/*" ]
|
||||||
|
}
|
||||||
|
|
||||||
context "requesting no representation" $
|
context "requesting no representation" $
|
||||||
it "should not throw and return no location header when selecting without PK" $
|
it "should not throw and return no location header when selecting without PK" $
|
||||||
request methodPost "/projects?select=name,client_id" []
|
request methodPost "/projects?select=name,client_id" []
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import Network.HTTP.Types
|
|||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
|
pgVersion110)
|
||||||
|
|
||||||
import PostgREST.Version (docsVersion)
|
import PostgREST.Version (docsVersion)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec = describe "OpenAPI" $ do
|
spec actualPgVersion = describe "OpenAPI" $ do
|
||||||
it "root path returns a valid openapi spec" $ do
|
it "root path returns a valid openapi spec" $ do
|
||||||
validateOpenApiResponse [("Accept", "application/openapi+json")]
|
validateOpenApiResponse [("Accept", "application/openapi+json")]
|
||||||
request methodHead "/" (acceptHdrs "application/openapi+json") ""
|
request methodHead "/" (acceptHdrs "application/openapi+json") ""
|
||||||
@@ -195,6 +198,32 @@ spec = describe "OpenAPI" $ do
|
|||||||
]
|
]
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion100) $ do
|
||||||
|
describe "Partitioned table" $
|
||||||
|
|
||||||
|
it "includes partitioned table properties" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let method s = key "paths" . key "/partitioned_a" . key s
|
||||||
|
getSummary = r ^? method "get" . key "summary"
|
||||||
|
getDescription = r ^? method "get" . key "description"
|
||||||
|
getParameterId = r ^? method "get" . key "parameters" . nth 0 . key "$ref"
|
||||||
|
getParameterName = r ^? method "get" . key "parameters" . nth 1 . key "$ref"
|
||||||
|
getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
|
||||||
|
getSummary `shouldBe` Just "A partitioned table"
|
||||||
|
|
||||||
|
getDescription `shouldBe` Just "A test for partitioned tables"
|
||||||
|
|
||||||
|
getParameterId `shouldBe` Just "#/parameters/rowFilter.partitioned_a.id"
|
||||||
|
|
||||||
|
getParameterName `shouldBe` Just "#/parameters/rowFilter.partitioned_a.name"
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
getParameterRef `shouldBe` Just "#/parameters/rowFilter.partitioned_a.id_ref"
|
||||||
|
|
||||||
describe "Materialized view" $
|
describe "Materialized view" $
|
||||||
|
|
||||||
it "includes materialized view properties" $ do
|
it "includes materialized view properties" $ do
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import Network.HTTP.Types
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
|
pgVersion110)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec = describe "Allow header" $ do
|
spec actualPgVersion = describe "Allow header" $ do
|
||||||
context "a table" $ do
|
context "a table" $ do
|
||||||
it "includes read/write verbs for writeable table" $ do
|
it "includes read/write verbs for writeable table" $ do
|
||||||
r <- request methodOptions "/items" [] ""
|
r <- request methodOptions "/items" [] ""
|
||||||
@@ -19,6 +22,19 @@ spec = describe "Allow header" $ do
|
|||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion100) $
|
||||||
|
context "a partitioned table" $ do
|
||||||
|
it "includes read/write verbs for writeable partitioned tables" $ do
|
||||||
|
r <- request methodOptions "/partitioned_a" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" (
|
||||||
|
if actualPgVersion >= pgVersion110 then
|
||||||
|
"OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
else
|
||||||
|
"OPTIONS,GET,HEAD,POST,PATCH,DELETE"
|
||||||
|
)
|
||||||
|
|
||||||
context "a view" $ do
|
context "a view" $ do
|
||||||
context "auto updatable" $ do
|
context "auto updatable" $ do
|
||||||
it "includes read/write verbs for auto updatable views with pk" $ do
|
it "includes read/write verbs for auto updatable views with pk" $ do
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import Test.Hspec hiding (pendingWith)
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion112,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
||||||
pgVersion121, pgVersion96)
|
pgVersion112, pgVersion121,
|
||||||
|
pgVersion96)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
@@ -395,6 +396,82 @@ spec actualPgVersion = do
|
|||||||
[json|[{"id":1,"computed_overload":true}]|]
|
[json|[{"id":1,"computed_overload":true}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $ do
|
||||||
|
describe "partitioned tables embedding" $ do
|
||||||
|
it "can request a table as parent from a partitioned table" $
|
||||||
|
get "/partitioned_a?id=in.(1,2)&select=id,name,reference_from_partitioned(id)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"first","reference_from_partitioned":{"id":1}},
|
||||||
|
{"id":2,"name":"first","reference_from_partitioned":null}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request partitioned tables as children from a table" $
|
||||||
|
get "/reference_from_partitioned?select=id,partitioned_a(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"partitioned_a":[{"id":1,"name":"first"}]},
|
||||||
|
{"id":2,"partitioned_a":[]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion121) $ do
|
||||||
|
it "can request tables as children from a partitioned table" $
|
||||||
|
get "/partitioned_a?id=in.(1,2)&select=id,name,reference_to_partitioned(id)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"first","reference_to_partitioned":[]},
|
||||||
|
{"id":2,"name":"first","reference_to_partitioned":[{"id":2}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request a partitioned table as parent from a table" $
|
||||||
|
get "/reference_to_partitioned?select=id,partitioned_a(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"partitioned_a":null},
|
||||||
|
{"id":2,"partitioned_a":{"id":2,"name":"first"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request partitioned tables as children from a partitioned table" $
|
||||||
|
get "/partitioned_a?id=in.(1,2,4)&select=id,name,partitioned_b(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"first","partitioned_b":[]},
|
||||||
|
{"id":2,"name":"first","partitioned_b":[{"id":2,"name":"first_b"}]},
|
||||||
|
{"id":4,"name":"second","partitioned_b":[{"id":4,"name":"second_b"}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request a partitioned table as parent from a partitioned table" $ do
|
||||||
|
get "/partitioned_b?id=in.(2,4)&select=id,name,partitioned_a(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":2,"name":"first_b","partitioned_a":{"id":2,"name":"first"}},
|
||||||
|
{"id":4,"name":"second_b","partitioned_a":{"id":4,"name":"second"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request partitions as children from a partitioned table" $
|
||||||
|
get "/partitioned_a?id=in.(1,2,4)&select=id,name,first_partition_b(id)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"first","first_partition_b":[]},
|
||||||
|
{"id":2,"name":"first","first_partition_b":[{"id":2}]},
|
||||||
|
{"id":4,"name":"second","first_partition_b":[]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request a partitioned table as parent from a partition" $
|
||||||
|
get "/first_partition_b?select=id,name,partitioned_a(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"first_b","partitioned_a":null},
|
||||||
|
{"id":2,"name":"first_b","partitioned_a":{"id":2,"name":"first"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request a partition as parent from a partitioned table" $
|
||||||
|
get "/partitioned_b?id=in.(1,3,4)&select=id,name,second_partition_a(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"first_b","second_partition_a":null},
|
||||||
|
{"id":3,"name":"second_b","second_partition_a":null},
|
||||||
|
{"id":4,"name":"second_b","second_partition_a":{"id":4,"name":"second"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request partitioned tables as children from a partition" $
|
||||||
|
get "/second_partition_a?select=id,name,partitioned_b(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":3,"name":"second","partitioned_b":[]},
|
||||||
|
{"id":4,"name":"second","partitioned_b":[{"id":4,"name":"second_b"}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
describe "view embedding" $ do
|
describe "view embedding" $ do
|
||||||
it "can detect fk relations through views to tables in the public schema" $
|
it "can detect fk relations through views to tables in the public schema" $
|
||||||
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
|
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110)
|
||||||
|
|
||||||
import Protolude hiding (get, put)
|
import Protolude hiding (get, put)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec =
|
spec actualPgVersion =
|
||||||
describe "UPSERT" $ do
|
describe "UPSERT" $ do
|
||||||
context "with POST" $ do
|
context "with POST" $ do
|
||||||
context "when Prefer: resolution=merge-duplicates is specified" $ do
|
context "when Prefer: resolution=merge-duplicates is specified" $ do
|
||||||
@@ -43,6 +45,20 @@ spec =
|
|||||||
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $
|
||||||
|
request methodPost "/partitioned_a" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "id": 4, "name": "second", "id_ref": 2},
|
||||||
|
{ "id": 6, "name": "first", "id_ref": 1 }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "id": 4, "name": "second", "id_ref": 2 },
|
||||||
|
{ "id": 6, "name": "first", "id_ref": 1 }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "succeeds when the payload has no elements" $
|
it "succeeds when the payload has no elements" $
|
||||||
request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
[json|[]|] `shouldRespondWith`
|
[json|[]|] `shouldRespondWith`
|
||||||
@@ -99,6 +115,19 @@ spec =
|
|||||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $
|
||||||
|
request methodPost "/partitioned_a" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "id": 4, "name": "second", "id_ref": 1 },
|
||||||
|
{ "id": 7, "name": "second", "id_ref": 2 }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "id": 7, "name": "second", "id_ref": 2 }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "INSERTs and ignores rows on single unique key conflict" $
|
it "INSERTs and ignores rows on single unique key conflict" $
|
||||||
request methodPost "/single_unique?on_conflict=unique_key"
|
request methodPost "/single_unique?on_conflict=unique_key"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
[("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
@@ -264,6 +293,19 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
|
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "succeeds on a partitioned table with composite pk" $ do
|
||||||
|
-- assert that the next request will indeed be an insert
|
||||||
|
get "/partitioned_a?id=eq.8&name=eq.first"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[]|]
|
||||||
|
|
||||||
|
request methodPut "/partitioned_a?id=eq.8&name=eq.first"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| [ { "id": 8, "name": "first" } ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [ { "id": 8, "name": "first", "id_ref": null } ]|]
|
||||||
|
|
||||||
it "succeeds if the table has only PK cols and no other cols" $ do
|
it "succeeds if the table has only PK cols and no other cols" $ do
|
||||||
-- assert that the next request will indeed be an insert
|
-- assert that the next request will indeed be an insert
|
||||||
get "/only_pk?id=eq.10"
|
get "/only_pk?id=eq.10"
|
||||||
@@ -315,6 +357,19 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "succeeds on a partitioned table with composite pk" $ do
|
||||||
|
-- assert that the next request will indeed be an update
|
||||||
|
get "/partitioned_a?id=eq.2&name=eq.first"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [ { "id": 2, "name": "first", "id_ref": null } ]|]
|
||||||
|
|
||||||
|
request methodPut "/partitioned_a?id=eq.2&name=eq.first"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| [ { "id": 2, "name": "first", "id_ref": 1 } ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [ { "id": 2, "name": "first", "id_ref": 1 } ]|]
|
||||||
|
|
||||||
it "succeeds if the table has only PK cols and no other cols" $ do
|
it "succeeds if the table has only PK cols and no other cols" $ do
|
||||||
-- assert that the next request will indeed be an update
|
-- assert that the next request will indeed be an update
|
||||||
get "/only_pk?id=eq.1"
|
get "/only_pk?id=eq.1"
|
||||||
|
|||||||
+3
-3
@@ -127,14 +127,14 @@ main = do
|
|||||||
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
|
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
|
||||||
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
|
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
|
||||||
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
|
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
|
||||||
, ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec)
|
, ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec actualPgVersion)
|
||||||
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec)
|
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec actualPgVersion)
|
||||||
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
|
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
|
||||||
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
|
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
|
||||||
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
|
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
|
||||||
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
||||||
, ("Feature.UpdateSpec" , Feature.UpdateSpec.spec)
|
, ("Feature.UpdateSpec" , Feature.UpdateSpec.spec)
|
||||||
, ("Feature.UpsertSpec" , Feature.UpsertSpec.spec)
|
, ("Feature.UpsertSpec" , Feature.UpsertSpec.spec actualPgVersion)
|
||||||
]
|
]
|
||||||
|
|
||||||
hspec $ do
|
hspec $ do
|
||||||
|
|||||||
Vendored
+25
@@ -663,3 +663,28 @@ INSERT INTO private.films (id, title) VALUES (12,'douze commandements'), (2001,'
|
|||||||
|
|
||||||
TRUNCATE TABLE private.personnages CASCADE;
|
TRUNCATE TABLE private.personnages CASCADE;
|
||||||
INSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méchant'), (2001,2,'astronaute');
|
INSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méchant'), (2001,2,'astronaute');
|
||||||
|
|
||||||
|
DO $do$BEGIN
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN
|
||||||
|
INSERT INTO test.partitioned_a(id, name) VALUES (1,'first');
|
||||||
|
INSERT INTO test.partitioned_a(id, name) VALUES (2,'first');
|
||||||
|
INSERT INTO test.partitioned_a(id, name) VALUES (3,'second');
|
||||||
|
INSERT INTO test.partitioned_a(id, name) VALUES (4,'second');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN
|
||||||
|
INSERT INTO test.reference_from_partitioned(id) VALUES (1),(2);
|
||||||
|
|
||||||
|
UPDATE test.partitioned_a SET id_ref = 1 WHERE id = 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN
|
||||||
|
INSERT INTO test.partitioned_b(id, name) VALUES (1,'first_b');
|
||||||
|
INSERT INTO test.partitioned_b(id, name, id_a, name_a) VALUES (2,'first_b', 2, 'first');
|
||||||
|
INSERT INTO test.partitioned_b(id, name) VALUES (3,'second_b');
|
||||||
|
INSERT INTO test.partitioned_b(id, name, id_a, name_a) VALUES (4,'second_b', 4, 'second');
|
||||||
|
|
||||||
|
INSERT INTO test.reference_to_partitioned(id) VALUES (1);
|
||||||
|
INSERT INTO test.reference_to_partitioned(id, id_a, name_a) VALUES (2, 2, 'first');
|
||||||
|
END IF;
|
||||||
|
END$do$;
|
||||||
Vendored
+20
@@ -179,3 +179,23 @@ REVOKE EXECUTE ON FUNCTION privileged_hello(text) FROM PUBLIC; -- All functions
|
|||||||
GRANT EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;
|
GRANT EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;
|
||||||
|
|
||||||
GRANT USAGE ON SCHEMA test TO postgrest_test_default_role;
|
GRANT USAGE ON SCHEMA test TO postgrest_test_default_role;
|
||||||
|
|
||||||
|
|
||||||
|
DO $do$BEGIN
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN
|
||||||
|
GRANT ALL ON TABLE test.partitioned_a TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.first_partition_a TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.second_partition_a TO postgrest_test_anonymous;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN
|
||||||
|
GRANT ALL ON TABLE test.reference_from_partitioned TO postgrest_test_anonymous;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN
|
||||||
|
GRANT ALL ON TABLE test.partitioned_b TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.first_partition_b TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.second_partition_b TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.reference_to_partitioned TO postgrest_test_anonymous;
|
||||||
|
END IF;
|
||||||
|
END$do$;
|
||||||
Vendored
+58
@@ -2185,3 +2185,61 @@ create table private.rollen (
|
|||||||
foreign key (film_id) references test.filme(id),
|
foreign key (film_id) references test.filme(id),
|
||||||
foreign key (rolle_id) references test.schauspieler(id)
|
foreign key (rolle_id) references test.schauspieler(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Tables used for testing embedding between partitioned tables
|
||||||
|
|
||||||
|
do $do$begin
|
||||||
|
-- partitioned tables using the PARTITION syntax are supported from pg v10
|
||||||
|
if (select current_setting('server_version_num')::int >= 100000) then
|
||||||
|
create table test.partitioned_a(
|
||||||
|
id int not null,
|
||||||
|
name varchar(64) not null
|
||||||
|
) partition by list (name);
|
||||||
|
|
||||||
|
comment on table test.partitioned_a is
|
||||||
|
$$A partitioned table
|
||||||
|
|
||||||
|
A test for partitioned tables$$;
|
||||||
|
|
||||||
|
create table test.first_partition_a partition of test.partitioned_a
|
||||||
|
for values in ('first');
|
||||||
|
|
||||||
|
create table test.second_partition_a partition of test.partitioned_a
|
||||||
|
for values in ('second');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- primary keys for partitioned tables are supported from pg v11
|
||||||
|
if (select current_setting('server_version_num')::int >= 110000) then
|
||||||
|
create table test.reference_from_partitioned (
|
||||||
|
id int primary key
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table test.partitioned_a add primary key (id, name);
|
||||||
|
alter table test.partitioned_a add column id_ref int references test.reference_from_partitioned(id);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- foreign keys referencing partitioned tables are supported from pg v12
|
||||||
|
if (select current_setting('server_version_num')::int >= 120000) then
|
||||||
|
create table test.partitioned_b(
|
||||||
|
id int not null,
|
||||||
|
name varchar(64) not null,
|
||||||
|
id_a int,
|
||||||
|
name_a varchar(64),
|
||||||
|
primary key (id, name),
|
||||||
|
foreign key (id_a, name_a) references test.partitioned_a (id, name)
|
||||||
|
) partition by list (name);
|
||||||
|
|
||||||
|
create table test.first_partition_b partition of test.partitioned_b
|
||||||
|
for values in ('first_b');
|
||||||
|
|
||||||
|
create table test.second_partition_b partition of test.partitioned_b
|
||||||
|
for values in ('second_b');
|
||||||
|
|
||||||
|
create table test.reference_to_partitioned (
|
||||||
|
id int not null primary key,
|
||||||
|
id_a int,
|
||||||
|
name_a varchar(64),
|
||||||
|
foreign key (id_a, name_a) references test.partitioned_a (id, name)
|
||||||
|
);
|
||||||
|
end if;
|
||||||
|
end$do$;
|
||||||
@@ -709,6 +709,7 @@ def test_invalid_role_claim_key_notify_reload(defaultenv):
|
|||||||
|
|
||||||
postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
||||||
|
|
||||||
|
|
||||||
def test_db_prepared_statements_enable(defaultenv):
|
def test_db_prepared_statements_enable(defaultenv):
|
||||||
"Should use prepared statements when the setting is enabled."
|
"Should use prepared statements when the setting is enabled."
|
||||||
|
|
||||||
@@ -716,6 +717,7 @@ def test_db_prepared_statements_enable(defaultenv):
|
|||||||
response = postgrest.session.post("/rpc/uses_prepared_statements")
|
response = postgrest.session.post("/rpc/uses_prepared_statements")
|
||||||
assert response.text == "true"
|
assert response.text == "true"
|
||||||
|
|
||||||
|
|
||||||
def test_db_prepared_statements_disable(defaultenv):
|
def test_db_prepared_statements_disable(defaultenv):
|
||||||
"Should not use any prepared statements when the setting is disabled."
|
"Should not use any prepared statements when the setting is disabled."
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user