Remove partitions from the schema cache

breaking: Embedding, OpenApi and other features related to the schema cache are no longer allowed on partitions.
This commit is contained in:
Laurence Isla
2021-11-18 14:12:14 -05:00
committed by GitHub
parent 041f73cae8
commit 91689e28f0
14 changed files with 208 additions and 134 deletions
+1
View File
@@ -30,6 +30,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1857, Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 - @laurenceisla, @robertsosinski - #1857, Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 - @laurenceisla, @robertsosinski
+ Getting the value for a header GUC on PostgreSQL 14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims` + Getting the value for a header GUC on PostgreSQL 14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims`
+ PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default) + PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default)
- #1783, Partitions (created using `PARTITION OF`) are no longer included in the schema cache. - @laurenceisla
## [8.0.0] - 2021-07-25 ## [8.0.0] - 2021-07-25
+2 -2
View File
@@ -462,12 +462,12 @@ handleInvoke invMethod proc context@RequestContext{..} = do
(if invMethod == InvHead then mempty else LBS.fromStrict body) (if invMethod == InvHead then mempty else LBS.fromStrict body)
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest ctxPgVersion) = do
body <- body <-
lift $ case configOpenApiMode of lift $ case configOpenApiMode of
OAFollowPriv -> OAFollowPriv ->
OpenAPI.encode conf dbStructure OpenAPI.encode conf dbStructure
<$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements) <$> SQL.statement tSchema (DbStructure.accessibleTables ctxPgVersion configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements) <*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements) <*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
OAIgnorePriv -> OAIgnorePriv ->
+2
View File
@@ -53,6 +53,7 @@ main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
dumpSchema :: AppState -> IO LBS.ByteString dumpSchema :: AppState -> IO LBS.ByteString
dumpSchema appState = do dumpSchema appState = do
AppConfig{..} <- AppState.getConfig appState AppConfig{..} <- AppState.getConfig appState
actualPgVersion <- AppState.getPgVersion appState
result <- result <-
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
SQL.use (AppState.getPool appState) $ SQL.use (AppState.getPool appState) $
@@ -60,6 +61,7 @@ dumpSchema appState = do
queryDbStructure queryDbStructure
(toList configDbSchemas) (toList configDbSchemas)
configDbExtraSearchPath configDbExtraSearchPath
actualPgVersion
configDbPreparedStatements configDbPreparedStatements
SQL.release $ AppState.getPool appState SQL.release $ AppState.getPool appState
case result of case result of
+15 -9
View File
@@ -41,6 +41,7 @@ import Data.Set as S (fromList)
import Data.Text (split) import Data.Text (split)
import Text.InterpolatedString.Perl6 (q) import Text.InterpolatedString.Perl6 (q)
import PostgREST.Config.PgVersion (PgVersion, pgVersion100)
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..), import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
Schema, TableName) Schema, TableName)
import PostgREST.DbStructure.Proc (PgType (..), import PostgREST.DbStructure.Proc (PgType (..),
@@ -82,10 +83,10 @@ type ViewColumn = Column
-- | A SQL query that can be executed independently -- | A SQL query that can be executed independently
type SqlQuery = ByteString type SqlQuery = ByteString
queryDbStructure :: [Schema] -> [Schema] -> Bool -> SQL.Transaction DbStructure queryDbStructure :: [Schema] -> [Schema] -> PgVersion -> Bool -> SQL.Transaction DbStructure
queryDbStructure schemas extraSearchPath prepared = do queryDbStructure schemas extraSearchPath pgVer prepared = do
SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
tabs <- SQL.statement mempty $ allTables prepared tabs <- SQL.statement mempty $ allTables pgVer prepared
cols <- SQL.statement schemas $ allColumns tabs prepared cols <- SQL.statement schemas $ allColumns tabs prepared
srcCols <- SQL.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared srcCols <- SQL.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared
m2oRels <- SQL.statement mempty $ allM2ORels tabs cols prepared m2oRels <- SQL.statement mempty $ allM2ORels tabs cols prepared
@@ -311,8 +312,8 @@ schemaDescription =
where where
n.nspname = $1 |] n.nspname = $1 |]
accessibleTables :: Bool -> SQL.Statement Schema [Table] accessibleTables :: PgVersion -> Bool -> SQL.Statement Schema [Table]
accessibleTables = accessibleTables pgVer =
SQL.Statement sql (param HE.text) decodeTables SQL.Statement sql (param HE.text) decodeTables
where where
sql = [q| sql = [q|
@@ -350,7 +351,8 @@ accessibleTables =
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','p') c.relkind in ('v','r','m','f','p')
and n.nspname = $1 and n.nspname = $1 |]
<> relIsNotPartition pgVer <> [q|
and ( and (
pg_has_role(c.relowner, 'USAGE') pg_has_role(c.relowner, 'USAGE')
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
@@ -445,8 +447,8 @@ addViewPrimaryKeys srcCols = concatMap (\pk ->
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in
pk : viewPks) pk : viewPks)
allTables :: Bool -> SQL.Statement () [Table] allTables :: PgVersion -> Bool -> SQL.Statement () [Table]
allTables = allTables pgVer =
SQL.Statement sql HE.noParams decodeTables SQL.Statement sql HE.noParams decodeTables
where where
sql = [q| sql = [q|
@@ -484,9 +486,13 @@ allTables =
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','p') 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') |]
<> relIsNotPartition pgVer <> [q|
ORDER BY table_schema, table_name |] ORDER BY table_schema, table_name |]
relIsNotPartition :: PgVersion -> SqlQuery
relIsNotPartition pgVer = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty
allColumns :: [Table] -> Bool -> SQL.Statement [Schema] [Column] allColumns :: [Table] -> Bool -> SQL.Statement [Schema] [Column]
allColumns tabs = allColumns tabs =
SQL.Statement sql (arrayParam HE.text) (decodeColumns tabs) SQL.Statement sql (arrayParam HE.text) (decodeColumns tabs)
+2 -1
View File
@@ -152,10 +152,11 @@ connectionStatus appState =
loadSchemaCache :: AppState -> IO SCacheStatus loadSchemaCache :: AppState -> IO SCacheStatus
loadSchemaCache appState = do loadSchemaCache appState = do
AppConfig{..} <- AppState.getConfig appState AppConfig{..} <- AppState.getConfig appState
actualPgVersion <- AppState.getPgVersion appState
result <- result <-
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
SQL.use (AppState.getPool appState) . transaction SQL.ReadCommitted SQL.Read $ SQL.use (AppState.getPool appState) . transaction SQL.ReadCommitted SQL.Read $
queryDbStructure (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements queryDbStructure (toList configDbSchemas) configDbExtraSearchPath actualPgVersion configDbPreparedStatements
case result of case result of
Left e -> do Left e -> do
let let
+3 -3
View File
@@ -122,10 +122,10 @@ spec actualPgVersion = do
when (actualPgVersion >= pgVersion110) $ when (actualPgVersion >= pgVersion110) $
it "should not throw and return location header for partitioned tables when selecting without PK" $ it "should not throw and return location header for partitioned tables when selecting without PK" $
request methodPost "/partitioned_a" [("Prefer", "return=headers-only")] request methodPost "/car_models" [("Prefer", "return=headers-only")]
[json|{"id":5,"name":"first"}|] `shouldRespondWith` "" [json|{"name":"Enzo","year":2021}|] `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [ "Location" <:> "/partitioned_a?id=eq.5&name=eq.first" , matchHeaders = [ "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021"
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
+6 -6
View File
@@ -204,11 +204,11 @@ spec actualPgVersion = describe "OpenAPI" $ do
it "includes partitioned table properties" $ do it "includes partitioned table properties" $ do
r <- simpleBody <$> get "/" r <- simpleBody <$> get "/"
let method s = key "paths" . key "/partitioned_a" . key s let method s = key "paths" . key "/car_models" . key s
getSummary = r ^? method "get" . key "summary" getSummary = r ^? method "get" . key "summary"
getDescription = r ^? method "get" . key "description" getDescription = r ^? method "get" . key "description"
getParameterId = r ^? method "get" . key "parameters" . nth 0 . key "$ref" getParameterName = r ^? method "get" . key "parameters" . nth 0 . key "$ref"
getParameterName = r ^? method "get" . key "parameters" . nth 1 . key "$ref" getParameterYear = r ^? method "get" . key "parameters" . nth 1 . key "$ref"
getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref" getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref"
liftIO $ do liftIO $ do
@@ -217,12 +217,12 @@ spec actualPgVersion = describe "OpenAPI" $ do
getDescription `shouldBe` Just "A test for partitioned tables" getDescription `shouldBe` Just "A test for partitioned tables"
getParameterId `shouldBe` Just "#/parameters/rowFilter.partitioned_a.id" getParameterName `shouldBe` Just "#/parameters/rowFilter.car_models.name"
getParameterName `shouldBe` Just "#/parameters/rowFilter.partitioned_a.name" getParameterYear `shouldBe` Just "#/parameters/rowFilter.car_models.year"
when (actualPgVersion >= pgVersion110) $ when (actualPgVersion >= pgVersion110) $
getParameterRef `shouldBe` Just "#/parameters/rowFilter.partitioned_a.id_ref" getParameterRef `shouldBe` Just "#/parameters/rowFilter.car_models.car_brand_name"
describe "Materialized view" $ describe "Materialized view" $
+1 -1
View File
@@ -25,7 +25,7 @@ spec actualPgVersion = describe "Allow header" $ do
when (actualPgVersion >= pgVersion100) $ when (actualPgVersion >= pgVersion100) $
context "a partitioned table" $ do context "a partitioned table" $ do
it "includes read/write verbs for writeable partitioned tables" $ do it "includes read/write verbs for writeable partitioned tables" $ do
r <- request methodOptions "/partitioned_a" [] "" r <- request methodOptions "/car_models" [] ""
liftIO $ liftIO $
simpleHeaders r `shouldSatisfy` simpleHeaders r `shouldSatisfy`
matchHeader "Allow" ( matchHeader "Allow" (
+59 -41
View File
@@ -399,78 +399,96 @@ spec actualPgVersion = do
when (actualPgVersion >= pgVersion110) $ do when (actualPgVersion >= pgVersion110) $ do
describe "partitioned tables embedding" $ do describe "partitioned tables embedding" $ do
it "can request a table as parent from a partitioned table" $ 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` get "/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"name":"first","reference_from_partitioned":{"id":1}}, [{"name":"DeLorean","year":1981,"car_brands":{"name":"DMC"}},
{"id":2,"name":"first","reference_from_partitioned":null}] |] {"name":"Murcielago","year":2001,"car_brands":{"name":"Lamborghini"}}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can request partitioned tables as children from a table" $ it "can request partitioned tables as children from a table" $
get "/reference_from_partitioned?select=id,partitioned_a(id,name)&order=id.asc" `shouldRespondWith` get "/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"partitioned_a":[{"id":1,"name":"first"}]}, [{"name":"DMC","car_models":[{"name":"DeLorean","year":1981}]},
{"id":2,"partitioned_a":[]}] |] {"name":"Ferrari","car_models":[{"name":"F310-B","year":1997}]},
{"name":"Lamborghini","car_models":[{"name":"Murcielago","year":2001},{"name":"Veneno","year":2013}]}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
when (actualPgVersion >= pgVersion121) $ do when (actualPgVersion >= pgVersion121) $ do
it "can request tables as children from a partitioned table" $ 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` get "/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"name":"first","reference_to_partitioned":[]}, [{"name":"DeLorean","year":1981,"car_racers":[]},
{"id":2,"name":"first","reference_to_partitioned":[{"id":2}]}] |] {"name":"F310-B","year":1997,"car_racers":[{"name":"Michael Schumacher"}]}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can request a partitioned table as parent from a table" $ 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` get "/car_racers?select=name,car_models(name,year)&order=name.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"partitioned_a":null}, [{"name":"Alain Prost","car_models":null},
{"id":2,"partitioned_a":{"id":2,"name":"first"}}] |] {"name":"Michael Schumacher","car_models":{"name":"F310-B","year":1997}}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can request partitioned tables as children from a partitioned table" $ 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` get "/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"name":"first","partitioned_b":[]}, [{"name":"DeLorean","year":1981,"car_model_sales":[{"date":"2021-01-14","quantity":7},{"date":"2021-01-15","quantity":9}]},
{"id":2,"name":"first","partitioned_b":[{"id":2,"name":"first_b"}]}, {"name":"Murcielago","year":2001,"car_model_sales":[{"date":"2021-02-11","quantity":1},{"date":"2021-02-12","quantity":3}]},
{"id":4,"name":"second","partitioned_b":[{"id":4,"name":"second_b"}]}] |] {"name":"Veneno","year":2013,"car_model_sales":[]}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can request a partitioned table as parent from a partitioned table" $ do 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` get "/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc" `shouldRespondWith`
[json| [json|
[{"id":2,"name":"first_b","partitioned_a":{"id":2,"name":"first"}}, [{"date":"2021-01-15","quantity":9,"car_models":{"name":"DeLorean","year":1981}},
{"id":4,"name":"second_b","partitioned_a":{"id":4,"name":"second"}}] |] {"date":"2021-02-11","quantity":1,"car_models":{"name":"Murcielago","year":2001}}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can request partitions as children from a partitioned table" $ it "can request many to many relationships between partitioned tables ignoring the intermediate table partitions" $
get "/partitioned_a?id=in.(1,2,4)&select=id,name,first_partition_b(id)&order=id.asc" `shouldRespondWith` get "/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4" `shouldRespondWith`
[json| [json|
[{"id":1,"name":"first","first_partition_b":[]}, [{"name":"DeLorean","year":1981,"car_dealers":[{"name":"Springfield Cars S.A.","city":"Springfield"}]},
{"id":2,"name":"first","first_partition_b":[{"id":2}]}, {"name":"F310-B","year":1997,"car_dealers":[]},
{"id":4,"name":"second","first_partition_b":[]}] |] {"name":"Murcielago","year":2001,"car_dealers":[{"name":"The Best Deals S.A.","city":"Franklin"}]},
{ matchHeaders = [matchContentTypeJson] } {"name":"Veneno","year":2013,"car_dealers":[]}] |]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "can request a partitioned table as parent from a partition" $ it "cannot request partitions as children from a partitioned table" $
get "/first_partition_b?select=id,name,partitioned_a(id,name)&order=id.asc" `shouldRespondWith` get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"name":"first_b","partitioned_a":null}, {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
{"id":2,"name":"first_b","partitioned_a":{"id":2,"name":"first"}}] |] "message":"Could not find a relationship between car_models and car_model_sales_202101 in the schema cache"} |]
{ matchHeaders = [matchContentTypeJson] } { matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
it "can request a partition as parent from a partitioned table" $ it "cannot request a partitioned table as parent from a partition" $
get "/partitioned_b?id=in.(1,3,4)&select=id,name,second_partition_a(id,name)&order=id.asc" `shouldRespondWith` get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
[json| [json|
[{"id":1,"name":"first_b","second_partition_a":null}, {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
{"id":3,"name":"second_b","second_partition_a":null}, "message":"Could not find a relationship between car_model_sales_202101 and car_models in the schema cache"} |]
{"id":4,"name":"second_b","second_partition_a":{"id":4,"name":"second"}}] |] { matchStatus = 400
{ matchHeaders = [matchContentTypeJson] } , matchHeaders = [matchContentTypeJson]
}
it "can request partitioned tables as children from a partition" $ it "cannot request a partition as parent from a partitioned table" $
get "/second_partition_a?select=id,name,partitioned_b(id,name)&order=id.asc" `shouldRespondWith` get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
[json| [json|
[{"id":3,"name":"second","partitioned_b":[]}, {"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
{"id":4,"name":"second","partitioned_b":[{"id":4,"name":"second_b"}]}] |] "message":"Could not find a relationship between car_model_sales and car_models_default in the schema cache"} |]
{ matchHeaders = [matchContentTypeJson] } { matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
it "cannot request partitioned tables as children from a partition" $
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
[json|
{"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
"message":"Could not find a relationship between car_models_default and car_model_sales in the schema cache"} |]
{ matchStatus = 400
, 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" $
+18 -18
View File
@@ -47,13 +47,13 @@ spec actualPgVersion =
when (actualPgVersion >= pgVersion110) $ when (actualPgVersion >= pgVersion110) $
it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $ it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $
request methodPost "/partitioned_a" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [ [json| [
{ "id": 4, "name": "second", "id_ref": 2}, { "name": "Murcielago", "year": 2001, "car_brand_name": null},
{ "id": 6, "name": "first", "id_ref": 1 } { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }
]|] `shouldRespondWith` [json| [ ]|] `shouldRespondWith` [json| [
{ "id": 4, "name": "second", "id_ref": 2 }, { "name": "Murcielago", "year": 2001, "car_brand_name": null},
{ "id": 6, "name": "first", "id_ref": 1 } { "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }
]|] ]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
@@ -117,12 +117,12 @@ spec actualPgVersion =
when (actualPgVersion >= pgVersion110) $ when (actualPgVersion >= pgVersion110) $
it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $ it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $
request methodPost "/partitioned_a" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json| [ [json| [
{ "id": 4, "name": "second", "id_ref": 1 }, { "name": "Murcielago", "year": 2001, "car_brand_name": "Ferrari" },
{ "id": 7, "name": "second", "id_ref": 2 } { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }
]|] `shouldRespondWith` [json| [ ]|] `shouldRespondWith` [json| [
{ "id": 7, "name": "second", "id_ref": 2 } { "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }
]|] ]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
@@ -296,15 +296,15 @@ spec actualPgVersion =
when (actualPgVersion >= pgVersion110) $ when (actualPgVersion >= pgVersion110) $
it "succeeds on a partitioned table with composite pk" $ do it "succeeds on a partitioned table with composite pk" $ do
-- assert that the next request will indeed be an insert -- assert that the next request will indeed be an insert
get "/partitioned_a?id=eq.8&name=eq.first" get "/car_models?name=eq.Supra&year=eq.2021"
`shouldRespondWith` `shouldRespondWith`
[json|[]|] [json|[]|]
request methodPut "/partitioned_a?id=eq.8&name=eq.first" request methodPut "/car_models?name=eq.Supra&year=eq.2021"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| [ { "id": 8, "name": "first" } ]|] [json| [ { "name": "Supra", "year": 2021 } ]|]
`shouldRespondWith` `shouldRespondWith`
[json| [ { "id": 8, "name": "first", "id_ref": null } ]|] [json| [ { "name": "Supra", "year": 2021, "car_brand_name": 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
@@ -360,15 +360,15 @@ spec actualPgVersion =
when (actualPgVersion >= pgVersion110) $ when (actualPgVersion >= pgVersion110) $
it "succeeds on a partitioned table with composite pk" $ do it "succeeds on a partitioned table with composite pk" $ do
-- assert that the next request will indeed be an update -- assert that the next request will indeed be an update
get "/partitioned_a?id=eq.2&name=eq.first" get "/car_models?name=eq.DeLorean&year=eq.1981"
`shouldRespondWith` `shouldRespondWith`
[json| [ { "id": 2, "name": "first", "id_ref": null } ]|] [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": "DMC" } ]|]
request methodPut "/partitioned_a?id=eq.2&name=eq.first" request methodPut "/car_models?name=eq.DeLorean&year=eq.1981"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| [ { "id": 2, "name": "first", "id_ref": 1 } ]|] [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]
`shouldRespondWith` `shouldRespondWith`
[json| [ { "id": 2, "name": "first", "id_ref": 1 } ]|] [json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": 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 update -- assert that the next request will indeed be an update
+4 -2
View File
@@ -69,6 +69,7 @@ main = do
loadDbStructure pool loadDbStructure pool
(configDbSchemas $ testCfg testDbConn) (configDbSchemas $ testCfg testDbConn)
(configDbExtraSearchPath $ testCfg testDbConn) (configDbExtraSearchPath $ testCfg testDbConn)
actualPgVersion
let let
-- For tests that run with the same refDbStructure -- For tests that run with the same refDbStructure
@@ -88,6 +89,7 @@ main = do
loadDbStructure pool loadDbStructure pool
(configDbSchemas config) (configDbSchemas config)
(configDbExtraSearchPath config) (configDbExtraSearchPath config)
actualPgVersion
appState <- AppState.initWithPool pool config appState <- AppState.initWithPool pool config
AppState.putPgVersion appState actualPgVersion AppState.putPgVersion appState actualPgVersion
AppState.putDbStructure appState customDbStructure AppState.putDbStructure appState customDbStructure
@@ -237,5 +239,5 @@ main = do
describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced
where where
loadDbStructure pool schemas extraSearchPath = loadDbStructure pool schemas extraSearchPath actualPgVersion =
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ queryDbStructure (toList schemas) extraSearchPath True) either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ queryDbStructure (toList schemas) extraSearchPath actualPgVersion True)
+23 -12
View File
@@ -670,26 +670,37 @@ INSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méc
DO $do$BEGIN DO $do$BEGIN
IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN
INSERT INTO test.partitioned_a(id, name) VALUES (1,'first'); INSERT INTO test.car_models(name, year) VALUES ('DeLorean',1981);
INSERT INTO test.partitioned_a(id, name) VALUES (2,'first'); INSERT INTO test.car_models(name, year) VALUES ('F310-B',1997);
INSERT INTO test.partitioned_a(id, name) VALUES (3,'second'); INSERT INTO test.car_models(name, year) VALUES ('Veneno',2013);
INSERT INTO test.partitioned_a(id, name) VALUES (4,'second'); INSERT INTO test.car_models(name, year) VALUES ('Murcielago',2001);
END IF; END IF;
IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN
INSERT INTO test.reference_from_partitioned(id) VALUES (1),(2); INSERT INTO test.car_brands(name) VALUES ('DMC');
INSERT INTO test.car_brands(name) VALUES ('Ferrari');
INSERT INTO test.car_brands(name) VALUES ('Lamborghini');
UPDATE test.partitioned_a SET id_ref = 1 WHERE id = 1; UPDATE test.car_models SET car_brand_name = 'DMC' WHERE name = 'DeLorean';
UPDATE test.car_models SET car_brand_name = 'Ferrari' WHERE name = 'F310-B';
UPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Veneno';
UPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Murcielago';
END IF; END IF;
IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN
INSERT INTO test.partitioned_b(id, name) VALUES (1,'first_b'); INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-14',7,'DeLorean',1981);
INSERT INTO test.partitioned_b(id, name, id_a, name_a) VALUES (2,'first_b', 2, 'first'); INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-15',9,'DeLorean',1981);
INSERT INTO test.partitioned_b(id, name) VALUES (3,'second_b'); INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-11',1,'Murcielago',2001);
INSERT INTO test.partitioned_b(id, name, id_a, name_a) VALUES (4,'second_b', 4, 'second'); INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-12',3,'Murcielago',2001);
INSERT INTO test.reference_to_partitioned(id) VALUES (1); INSERT INTO test.car_racers(name) VALUES ('Alain Prost');
INSERT INTO test.reference_to_partitioned(id, id_a, name_a) VALUES (2, 2, 'first'); INSERT INTO test.car_racers(name, car_model_name, car_model_year) VALUES ('Michael Schumacher', 'F310-B', 1997);
INSERT INTO test.car_dealers(name,city) VALUES ('Springfield Cars S.A.','Springfield');
INSERT INTO test.car_dealers(name,city) VALUES ('The Best Deals S.A.','Franklin');
INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('DeLorean',1981,'Springfield Cars S.A.','Springfield',15);
INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('Murcielago',2001,'The Best Deals S.A.','Franklin',2);
END IF; END IF;
END$do$; END$do$;
+14 -8
View File
@@ -192,19 +192,25 @@ GRANT USAGE ON SCHEMA test TO postgrest_test_default_role;
DO $do$BEGIN DO $do$BEGIN
IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN 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.car_models TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.first_partition_a TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_models_2021 TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.second_partition_a TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_models_default TO postgrest_test_anonymous;
END IF; END IF;
IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN
GRANT ALL ON TABLE test.reference_from_partitioned TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_brands TO postgrest_test_anonymous;
END IF; END IF;
IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN 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.car_model_sales TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.first_partition_b TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_model_sales_202101 TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.second_partition_b TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_model_sales_default TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.reference_to_partitioned TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_racers TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_dealers TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_dealers_springfield TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_dealers_default TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_models_car_dealers TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_models_car_dealers_10to20 TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_models_car_dealers_default TO postgrest_test_anonymous;
END IF; END IF;
END$do$; END$do$;
+58 -31
View File
@@ -2245,56 +2245,83 @@ create table private.rollen (
do $do$begin do $do$begin
-- partitioned tables using the PARTITION syntax are supported from pg v10 -- partitioned tables using the PARTITION syntax are supported from pg v10
if (select current_setting('server_version_num')::int >= 100000) then if (select current_setting('server_version_num')::int >= 100000) then
create table test.partitioned_a( create table test.car_models(
id int not null, name varchar(64) not null,
name varchar(64) not null year int not null
) partition by list (name); ) partition by list (year);
comment on table test.partitioned_a is comment on table test.car_models is
$$A partitioned table $$A partitioned table
A test for partitioned tables$$; A test for partitioned tables$$;
create table test.first_partition_a partition of test.partitioned_a create table test.car_models_2021 partition of test.car_models
for values in ('first'); for values in (2021);
create table test.car_models_default partition of test.car_models
create table test.second_partition_a partition of test.partitioned_a for values in (1981,1997,2001,2013);
for values in ('second');
end if; end if;
-- primary keys for partitioned tables are supported from pg v11 -- primary keys for partitioned tables are supported from pg v11
if (select current_setting('server_version_num')::int >= 110000) then if (select current_setting('server_version_num')::int >= 110000) then
create table test.reference_from_partitioned ( create table test.car_brands (
id int primary key name varchar(64) primary key
); );
alter table test.partitioned_a add primary key (id, name); alter table test.car_models add primary key (name, year);
alter table test.partitioned_a add column id_ref int references test.reference_from_partitioned(id); alter table test.car_models add column car_brand_name varchar(64) references test.car_brands(name);
end if; end if;
-- foreign keys referencing partitioned tables are supported from pg v12 -- foreign keys referencing partitioned tables are supported from pg v12
if (select current_setting('server_version_num')::int >= 120000) then if (select current_setting('server_version_num')::int >= 120000) then
create table test.partitioned_b( create table test.car_model_sales(
id int not null, date varchar(64) not null,
name varchar(64) not null, quantity int not null,
id_a int, car_model_name varchar(64),
name_a varchar(64), car_model_year int,
primary key (id, name), primary key (date, car_model_name, car_model_year),
foreign key (id_a, name_a) references test.partitioned_a (id, name) foreign key (car_model_name, car_model_year) references test.car_models (name, year)
) partition by list (name); ) partition by range (date);
create table test.first_partition_b partition of test.partitioned_b create table test.car_model_sales_202101 partition of test.car_model_sales
for values in ('first_b'); for values from ('2021-01-01') to ('2021-01-31');
create table test.second_partition_b partition of test.partitioned_b create table test.car_model_sales_default partition of test.car_model_sales
for values in ('second_b'); default;
create table test.reference_to_partitioned ( create table test.car_racers (
id int not null primary key, name varchar(64) not null primary key,
id_a int, car_model_name varchar(64),
name_a varchar(64), car_model_year int,
foreign key (id_a, name_a) references test.partitioned_a (id, name) foreign key (car_model_name, car_model_year) references test.car_models (name, year)
); );
create table test.car_dealers (
name varchar(64) not null,
city varchar(64) not null,
primary key (name, city)
) partition by list (city);
create table test.car_dealers_springfield partition of test.car_dealers
for values in ('Springfield');
create table test.car_dealers_default partition of test.car_dealers
default;
create table test.car_models_car_dealers (
car_model_name varchar(64) not null,
car_model_year int not null,
car_dealer_name varchar(64) not null,
car_dealer_city varchar(64) not null,
quantity int not null,
foreign key (car_model_name, car_model_year) references test.car_models (name, year),
foreign key (car_dealer_name, car_dealer_city) references test.car_dealers (name, city)
) partition by range (quantity);
create table test.car_models_car_dealers_10to20 partition of test.car_models_car_dealers
for values from (10) to (20);
create table test.car_models_car_dealers_default partition of test.car_models_car_dealers
default;
end if; end if;
end$do$; end$do$;