Revert bulk update patch

- Revert patch #2311
- Keep the refactor done to qsFiltersRoot
- Keep the refactor done to the items tables
- Add tests that now work with pg-safeupdate as a result
This commit is contained in:
Laurence Isla
2022-08-12 10:00:33 -05:00
committed by GitHub
parent 4ac47df528
commit 7de8d5446a
12 changed files with 161 additions and 282 deletions
-4
View File
@@ -28,7 +28,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2269, Allow `limit=0` in the request query to return an empty array - @gautam1168, @laurenceisla - #2269, Allow `limit=0` in the request query to return an empty array - @gautam1168, @laurenceisla
- #2268, Allow returning XML from single-column queries - @fjf2002 - #2268, Allow returning XML from single-column queries - @fjf2002
- #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002 - #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002
- #1959, Bulk update with PATCH - @steve-chavez
- #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez - #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez
+ Requires postgis >= 3.0 + Requires postgis >= 3.0
+ Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`. + Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.
@@ -61,7 +60,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2277, #2238, #1643, Prevent views from breaking one-to-many/many-to-one embeds when using column or FK as target - @steve-chavez - #2277, #2238, #1643, Prevent views from breaking one-to-many/many-to-one embeds when using column or FK as target - @steve-chavez
+ When using a column or FK as target for embedding(`/tbl?select=*,col-or-fk(*)`), only tables are now detected and views are not. + When using a column or FK as target for embedding(`/tbl?select=*,col-or-fk(*)`), only tables are now detected and views are not.
+ You can still use a column or an inferred FK on a view to embed a table(`/view?select=*,col-or-fk(*)`) + You can still use a column or an inferred FK on a view to embed a table(`/view?select=*,col-or-fk(*)`)
- #1959, An accidental full table PATCH(without filters) is not possible anymore, it requires filters or a `limit` parameter - @steve-chavez, @laurenceisla
- #2317, Increase the `db-pool-timeout` to 1 hour to prevent frequent high connection latency - @steve-chavez - #2317, Increase the `db-pool-timeout` to 1 hour to prevent frequent high connection latency - @steve-chavez
- #2341, The search path now correctly identifies schemas with uppercase and special characters in their names (regression) - @laurenceisla - #2341, The search path now correctly identifies schemas with uppercase and special characters in their names (regression) - @laurenceisla
- #2364, "404 Not Found" on nested routes and "405 Method Not Allowed" errors no longer start an empty database transaction - @steve-chavez - #2364, "404 Not Found" on nested routes and "405 Method Not Allowed" errors no longer start an empty database transaction - @steve-chavez
@@ -82,8 +80,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2277, Views now are not detected when embedding using the column or FK as target (`/view?select=*,column(*)`) - @steve-chavez - #2277, Views now are not detected when embedding using the column or FK as target (`/view?select=*,column(*)`) - @steve-chavez
+ This embedding form was easily made ambiguous whenever a new view was added. + This embedding form was easily made ambiguous whenever a new view was added.
+ For migrating, clients must be updated to the embedding form of `/view?select=*,other_view!column(*)`. + For migrating, clients must be updated to the embedding form of `/view?select=*,other_view!column(*)`.
- #1959, A full table PATCH(without filters) is now restricted, it requires a `limit` parameter - @steve-chavez
+ A `PATCH /tbl` will now result in 0 rows updated, unless `PATCH /tbl?limit=10&order=<pkcol>` is done
- #2312, Using `Prefer: return=representation` no longer returns a `Location` header - @laurenceisla - #2312, Using `Prefer: return=representation` no longer returns a `Location` header - @laurenceisla
## [9.0.1] - 2022-06-03 ## [9.0.1] - 2022-06-03
+2 -6
View File
@@ -360,12 +360,8 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
pure $ Wai.responseLBS HTTP.status200 (contentTypeHeaders context) $ LBS.fromStrict plan pure $ Wai.responseLBS HTTP.status200 (contentTypeHeaders context) $ LBS.fromStrict plan
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleUpdate identifier context@RequestContext{..} = do handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} _) = do
let resultSet <- writeQuery MutationUpdate identifier False mempty context
ApiRequest{..} = ctxApiRequest
pkCols = maybe mempty tablePKCols $ HM.lookup identifier $ dbTables ctxDbStructure
resultSet <- writeQuery MutationUpdate identifier False pkCols context
case resultSet of case resultSet of
RSStandard{..} -> do RSStandard{..} -> do
+6 -11
View File
@@ -103,7 +103,7 @@ mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returning
cols = BS.intercalate ", " $ pgFmtIdent <$> S.toList iCols cols = BS.intercalate ", " $ pgFmtIdent <$> S.toList iCols
-- An update without a limit is always filtered with a WHERE -- An update without a limit is always filtered with a WHERE
mutateRequestToQuery (Update mainQi uCols body logicForest pkFlts range ordts returnings) mutateRequestToQuery (Update mainQi uCols body logicForest range ordts returnings)
| S.null uCols = | S.null uCols =
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select= -- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
@@ -111,21 +111,17 @@ mutateRequestToQuery (Update mainQi uCols body logicForest pkFlts range ordts re
SQL.sql $ "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false" SQL.sql $ "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
| range == allRange = | range == allRange =
let whereLogic | null logicForest = if null pkFlts then "FALSE" else pgrstUpdateBodyF
| otherwise = logicForestF in
"WITH " <> normalizedBody body <> " " <> "WITH " <> normalizedBody body <> " " <>
"UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <> "UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <>
"FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) pgrst_update_body " <> "FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) _ " <>
"WHERE " <> whereLogic <> " " <> whereLogic <> " " <>
SQL.sql (returningF mainQi returnings) SQL.sql (returningF mainQi returnings)
| otherwise = | otherwise =
let whereLogic | null logicForest = mempty
| otherwise = " WHERE " <> logicForestF in
"WITH " <> normalizedBody body <> ", " <> "WITH " <> normalizedBody body <> ", " <>
"pgrst_update_body AS (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " ) LIMIT 1), " <> "pgrst_update_body AS (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " ) LIMIT 1), " <>
"pgrst_affected_rows AS (" <> "pgrst_affected_rows AS (" <>
"SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <> " " <> "SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <>
whereLogic <> " " <> whereLogic <> " " <>
orderF mainQi ordts <> " " <> orderF mainQi ordts <> " " <>
limitOffsetF range <> limitOffsetF range <>
@@ -136,11 +132,10 @@ mutateRequestToQuery (Update mainQi uCols body logicForest pkFlts range ordts re
SQL.sql (returningF mainQi returnings) SQL.sql (returningF mainQi returnings)
where where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
mainTbl = SQL.sql (fromQi mainQi) mainTbl = SQL.sql (fromQi mainQi)
logicForestF = intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
pgrstUpdateBodyF = SQL.sql (BS.intercalate " AND " $ (\x -> pgFmtColumn mainQi x <> " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_update_body") x) <$> pkFlts)
emptyBodyReturnedColumns = if null returnings then "NULL" else BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings) emptyBodyReturnedColumns = if null returnings then "NULL" else BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
nonRangeCols = BS.intercalate ", " (pgFmtIdent <> const " = pgrst_update_body." <> pgFmtIdent <$> S.toList uCols) nonRangeCols = BS.intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent col <> " = (SELECT " <> pgFmtIdent col <> " FROM pgrst_update_body) ") <$> S.toList uCols) rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent col <> " = (SELECT " <> pgFmtIdent col <> " FROM pgrst_update_body) ") <$> S.toList uCols)
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts) (whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
+1 -1
View File
@@ -318,7 +318,7 @@ mutateRequest mutation schema tName ApiRequest{..} pkCols readReq = mapLeft ApiR
case mutation of case mutation of
MutationCreate -> MutationCreate ->
Right $ Insert qi iColumns body ((,) <$> iPreferResolution <*> Just confCols) [] returnings Right $ Insert qi iColumns body ((,) <$> iPreferResolution <*> Just confCols) [] returnings
MutationUpdate -> Right $ Update qi iColumns body combinedLogic pkCols iTopLevelRange rootOrder returnings MutationUpdate -> Right $ Update qi iColumns body combinedLogic iTopLevelRange rootOrder returnings
MutationSingleUpsert -> MutationSingleUpsert ->
if null qsLogic && if null qsLogic &&
qsFilterFields == S.fromList pkCols && qsFilterFields == S.fromList pkCols &&
-1
View File
@@ -31,7 +31,6 @@ data MutateQuery
, updCols :: S.Set FieldName , updCols :: S.Set FieldName
, updBody :: Maybe LBS.ByteString , updBody :: Maybe LBS.ByteString
, where_ :: [LogicTree] , where_ :: [LogicTree]
, pkFilters :: [FieldName]
, mutRange :: NonnegRange , mutRange :: NonnegRange
, mutOrder :: [OrderTerm] , mutOrder :: [OrderTerm]
, returning :: [FieldName] , returning :: [FieldName]
+120 -22
View File
@@ -13,21 +13,58 @@ import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
describe "Enabling pg-safeupdate" $ do describe "Enabling pg-safeupdate" $ do
context "Full table update" $ context "Full table update" $ do
it "does not update and throws no error if no condition is present" $ it "does not update and throws error if no condition is present" $
request methodPatch "/safe_update" request methodPatch "/safe_update_items"
[("Prefer", "count=exact")] [("Prefer", "count=exact")]
[json| {"name": "New name"} |] [json| {"name": "New name"} |]
`shouldRespondWith`
[json|{
"code": "21000",
"details": null,
"hint": null,
"message": "UPDATE requires a WHERE clause"
}|]
{ matchStatus = 400 }
it "allows full table update if a filter is present" $ do
get "/safe_update_items"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPatch "/safe_update_items?id=gt.0"
[("Prefer", "tx=commit"), ("Prefer", "count=exact")]
[json| {"name": "updated-item"} |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 404 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/0" ] , "Content-Range" <:> "0-2/3"
, "Preference-Applied" <:> "tx=commit" ]
} }
get "/safe_update_items?order=id"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "updated-item", "observation": null }
, { "id": 2, "name": "updated-item", "observation": null }
, { "id": 3, "name": "updated-item", "observation": null }
]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "safe_update_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
context "Full table delete" $ do context "Full table delete" $ do
it "does not delete and throws error if no condition is present" $ it "does not delete and throws error if no condition is present" $
request methodDelete "/safe_delete" [] mempty request methodDelete "/safe_delete_items" [] mempty
`shouldRespondWith` `shouldRespondWith`
[json|{ [json|{
"code": "21000", "code": "21000",
@@ -37,40 +74,101 @@ spec =
}|] }|]
{ matchStatus = 400 } { matchStatus = 400 }
it "allows full table delete if a filter is present" $ it "allows full table delete if a filter is present" $ do
request methodDelete "/safe_delete?id=gt.0" get "/safe_delete_items"
[("Prefer", "count=exact")] `shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodDelete "/safe_delete_items?id=gt.0"
[("Prefer", "tx=commit"), ("Prefer", "count=exact")]
mempty mempty
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/3" ] , "Content-Range" <:> "*/3"
, "Preference-Applied" <:> "tx=commit" ]
} }
get "/safe_delete_items?order=id"
`shouldRespondWith`
[json|[]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "safe_delete_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
disabledSpec :: SpecWith ((), Application) disabledSpec :: SpecWith ((), Application)
disabledSpec = disabledSpec =
describe "Disabling pg-safeupdate" $ do describe "Disabling pg-safeupdate" $ do
context "Full table update" $ context "Full table update" $ do
it "does not update and does not throw error if no condition is present" $ it "works if no condition is present" $ do
request methodPatch "/unsafe_update" get "/unsafe_update_items"
[("Prefer", "count=exact")] `shouldRespondWith`
[json| {"name": "New name"} |] [json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPatch "/unsafe_update_items"
[("Prefer", "tx=commit"), ("Prefer", "count=exact")]
[json| {"name": "updated-item"} |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 404 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/0" ] , "Content-Range" <:> "0-2/3"
, "Preference-Applied" <:> "tx=commit" ]
} }
context "Full table delete" $ get "/unsafe_update_items?order=id"
it "deletes and does not throw error if no condition is present" $ do `shouldRespondWith`
request methodDelete "/unsafe_delete" [json|[
[("Prefer", "count=exact")] { "id": 1, "name": "updated-item", "observation": null }
, { "id": 2, "name": "updated-item", "observation": null }
, { "id": 3, "name": "updated-item", "observation": null }
]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "unsafe_update_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
context "Full table delete" $ do
it "works if no condition is present" $ do
get "/unsafe_delete_items"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodDelete "/unsafe_delete_items"
[("Prefer", "tx=commit"), ("Prefer", "count=exact")]
mempty mempty
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/3" ] , "Content-Range" <:> "*/3"
, "Preference-Applied" <:> "tx=commit" ]
} }
get "/unsafe_delete_items?order=id"
`shouldRespondWith`
[json|[]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "unsafe_delete_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
+5 -2
View File
@@ -98,8 +98,11 @@ spec =
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| [{"occupation": "Barista"}] |] [json| [{"occupation": "Barista"}] |]
`shouldRespondWith` `shouldRespondWith`
[json|[]|] [json|[
{ matchStatus = 404 } { "first_name": "Frances M.", "last_name": "Roe", "occupation": "Barista" },
{ "first_name": "Daniel B.", "last_name": "Lyon", "occupation": "Barista" },
{ "first_name": "Edwin S.", "last_name": "Smith", "occupation": "Barista" } ]|]
{ matchStatus = 200 }
it "doesn't affect deletions" $ it "doesn't affect deletions" $
request methodDelete "/employees?select=first_name,last_name" request methodDelete "/employees?select=first_name,last_name"
+2 -2
View File
@@ -65,7 +65,7 @@ spec =
} }
it "raises an error for multiple rows" $ do it "raises an error for multiple rows" $ do
request methodPatch "/addresses?limit=4&order=id" request methodPatch "/addresses"
[("Prefer", "tx=commit"), singular] [("Prefer", "tx=commit"), singular]
[json| { address: "zzz" } |] [json| { address: "zzz" } |]
`shouldRespondWith` `shouldRespondWith`
@@ -81,7 +81,7 @@ spec =
[json|[{"id":1,"address":"address 1"}]|] [json|[{"id":1,"address":"address 1"}]|]
it "raises an error for multiple rows with return=rep" $ do it "raises an error for multiple rows with return=rep" $ do
request methodPatch "/addresses?limit=4&order=id" request methodPatch "/addresses"
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
[json| { address: "zzz" } |] [json| { address: "zzz" } |]
`shouldRespondWith` `shouldRespondWith`
-189
View File
@@ -388,17 +388,6 @@ spec = do
} }
context "limited update" $ do context "limited update" $ do
it "does not work when no limit query or filter is given" $
request methodPatch "/limited_update_items"
[("Prefer", "tx=commit"), ("Prefer", "count=exact")]
[json| {"name": "updated-item"} |]
`shouldRespondWith`
""
{ matchStatus = 404
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/0" ]
}
it "works with the limit query param" $ do it "works with the limit query param" $ do
get "/limited_update_items" get "/limited_update_items"
`shouldRespondWith` `shouldRespondWith`
@@ -624,181 +613,3 @@ spec = do
[json| {"tbl_name": "limited_update_items_no_pk"} |] [json| {"tbl_name": "limited_update_items_no_pk"} |]
`shouldRespondWith` "" `shouldRespondWith` ""
{ matchStatus = 204 } { matchStatus = 204 }
context "bulk updates" $ do
it "can update tables with simple pk" $ do
get "/bulk_update_items"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPatch "/bulk_update_items"
[("Prefer", "tx=commit")]
[json|[
{ "id": 1, "name": "item-1 - 1st", "observation": "Lost item" }
, { "id": 3, "name": "item-3 - 3rd", "observation": null }
]|]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ]
}
get "/bulk_update_items?order=id"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1 - 1st", "observation": "Lost item" }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3 - 3rd", "observation": null }
]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "bulk_update_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
it "can update tables with composite pk" $ do
get "/bulk_update_items_cpk"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPatch "/bulk_update_items_cpk"
[("Prefer", "tx=commit")]
[json|[
{ "id": 1, "name": "item-1", "observation": "Lost item" }
, { "id": 2, "name": "item-2", "observation": null }
]|]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ]
}
get "/bulk_update_items_cpk?order=id"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": "Lost item" }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "bulk_update_items_cpk"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
it "updates with filters taking only the first item in the json array body" $ do
get "/bulk_update_items"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPatch "/bulk_update_items?id=eq.2"
[("Prefer", "tx=commit")]
[json|[
{ "id": 4, "name": "item-4", "observation": "Damaged item" }
, { "id": 3, "name": "item-3 - 3rd", "observation": null }
]|]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-0/*"
, "Preference-Applied" <:> "tx=commit" ]
}
get "/bulk_update_items?order=id"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
, { "id": 4, "name": "item-4", "observation": "Damaged item" }
]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "bulk_update_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
it "updates with limit and offset taking only the first item in the json array body" $ do
get "/bulk_update_items"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-2", "observation": null }
, { "id": 3, "name": "item-3", "observation": null }
]|]
request methodPatch "/bulk_update_items?limit=2&offset=1&order=id"
[("Prefer", "tx=commit")]
[json|[
{ "name": "item-4", "observation": "Damaged item" }
, { "name": "item-3 - 3rd", "observation": null }
]|]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ]
}
get "/bulk_update_items?order=id"
`shouldRespondWith`
[json|[
{ "id": 1, "name": "item-1", "observation": null }
, { "id": 2, "name": "item-4", "observation": "Damaged item" }
, { "id": 3, "name": "item-4", "observation": "Damaged item" }
]|]
request methodPost "/rpc/reset_items_tables"
[("Prefer", "tx=commit")]
[json| {"tbl_name": "bulk_update_items"} |]
`shouldRespondWith` ""
{ matchStatus = 204 }
it "rejects a json array that isn't exclusively composed of objects" $
request methodPatch "/bulk_update_items"
[("Prefer", "tx=commit")]
[json|[
{ "id": 1, "name": "Item 1" }
, 2
, "Item 2"
, { "id": 3, "name": "Item 3" }
]|]
`shouldRespondWith`
[json| {"message":"All object keys must match","code":"PGRST102","hint":null,"details":null} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
it "rejects a json array that has objects with different keys" $
request methodPatch "/bulk_update_items"
[("Prefer", "tx=commit")]
[json|[
{ "id": 1, "name": "Item 1" }
, { "id": 2 }
, { "id": 3, "name": "Item 3" }
]|]
`shouldRespondWith`
[json| {"message":"All object keys must match","code":"PGRST102","hint":null,"details":null} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
+8 -14
View File
@@ -781,12 +781,6 @@ INSERT INTO private.internal_job (id, parent_id) VALUES (2, 1);
TRUNCATE TABLE test.test CASCADE; TRUNCATE TABLE test.test CASCADE;
INSERT INTO test.test (id, parent_id) VALUES (1, null), (2, 1); INSERT INTO test.test (id, parent_id) VALUES (1, null), (2, 1);
TRUNCATE TABLE test.bulk_update_items CASCADE;
INSERT INTO test.bulk_update_items (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE test.bulk_update_items_cpk CASCADE;
INSERT INTO test.bulk_update_items_cpk (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE shops CASCADE; TRUNCATE TABLE shops CASCADE;
INSERT INTO shops(id, address, shop_geom) VALUES(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)'); INSERT INTO shops(id, address, shop_geom) VALUES(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)');
INSERT INTO shops(id, address, shop_geom) VALUES(2, '757 Massachusetts Ave', 'SRID=4326;POINT(-71.10543 42.366432)'); INSERT INTO shops(id, address, shop_geom) VALUES(2, '757 Massachusetts Ave', 'SRID=4326;POINT(-71.10543 42.366432)');
@@ -804,11 +798,11 @@ INSERT INTO "SPECIAL ""@/\#~_-".names (id, name) VALUES (1, 'John'), (2, 'Mary')
TRUNCATE TABLE do$llar$s CASCADE; TRUNCATE TABLE do$llar$s CASCADE;
INSERT INTO do$llar$s (a$num$) VALUES (100), (200), (300); INSERT INTO do$llar$s (a$num$) VALUES (100), (200), (300);
TRUNCATE TABLE safe_update CASCADE; TRUNCATE TABLE safe_update_items CASCADE;
INSERT INTO safe_update(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); INSERT INTO safe_update_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE safe_delete CASCADE; TRUNCATE TABLE safe_delete_items CASCADE;
INSERT INTO safe_delete(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); INSERT INTO safe_delete_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE unsafe_update CASCADE; TRUNCATE TABLE unsafe_update_items CASCADE;
INSERT INTO unsafe_update(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); INSERT INTO unsafe_update_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
TRUNCATE TABLE unsafe_delete CASCADE; TRUNCATE TABLE unsafe_delete_items CASCADE;
INSERT INTO unsafe_delete(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); INSERT INTO unsafe_delete_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
+4 -6
View File
@@ -189,16 +189,14 @@ GRANT ALL ON TABLE
, series_popularity , series_popularity
, test , test
, view_test , view_test
, bulk_update_items
, bulk_update_items_cpk
, shops , shops
, shop_bles , shop_bles
, "SPECIAL ""@/\#~_-".names , "SPECIAL ""@/\#~_-".names
, do$llar$s , do$llar$s
, safe_update , safe_update_items
, safe_delete , safe_delete_items
, unsafe_update , unsafe_update_items
, unsafe_delete , unsafe_delete_items
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+13 -24
View File
@@ -2534,7 +2534,7 @@ select * from limited_delete_items_cpk;
create function reset_items_tables(tbl_name text default '') returns void as $_$ begin create function reset_items_tables(tbl_name text default '') returns void as $_$ begin
execute format( execute format(
$$ $$
delete from %I; delete from %I where true; -- WHERE is required for pg-safeupdate tests
insert into %I values (1, 'item-1'), (2, 'item-2'), (3, 'item-3'); insert into %I values (1, 'item-1'), (2, 'item-2'), (3, 'item-3');
$$::text, $$::text,
tbl_name, tbl_name); tbl_name, tbl_name);
@@ -2638,21 +2638,6 @@ CREATE TABLE test.test (
CREATE OR REPLACE VIEW test.view_test AS CREATE OR REPLACE VIEW test.view_test AS
SELECT id FROM test.test; SELECT id FROM test.test;
-- Tables to test bulk updates
CREATE TABLE test.bulk_update_items (
id INT PRIMARY KEY,
name TEXT,
observation TEXT
);
CREATE TABLE test.bulk_update_items_cpk (
id INT,
name TEXT,
observation TEXT,
PRIMARY KEY (id, name)
);
create extension if not exists postgis with schema extensions; create extension if not exists postgis with schema extensions;
create table shops ( create table shops (
@@ -2692,24 +2677,28 @@ CREATE TABLE do$llar$s (
-- Tables and functions to test the pg-safeupdate library -- Tables and functions to test the pg-safeupdate library
CREATE TABLE test.safe_update( CREATE TABLE test.safe_update_items(
id INT PRIMARY KEY, id INT PRIMARY KEY,
name TEXT name TEXT,
observation TEXT
); );
CREATE TABLE test.safe_delete( CREATE TABLE test.safe_delete_items(
id INT PRIMARY KEY, id INT PRIMARY KEY,
name TEXT name TEXT,
observation TEXT
); );
CREATE TABLE test.unsafe_update( CREATE TABLE test.unsafe_update_items(
id INT PRIMARY KEY, id INT PRIMARY KEY,
name TEXT name TEXT,
observation TEXT
); );
CREATE TABLE test.unsafe_delete( CREATE TABLE test.unsafe_delete_items(
id INT PRIMARY KEY, id INT PRIMARY KEY,
name TEXT name TEXT,
observation TEXT
); );
CREATE OR REPLACE FUNCTION test.load_safeupdate() RETURNS VOID AS $$ CREATE OR REPLACE FUNCTION test.load_safeupdate() RETURNS VOID AS $$