From 7de8d5446aee3fb9db685d2fded6efd9af03f71f Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 12 Aug 2022 10:00:33 -0500 Subject: [PATCH] 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 --- CHANGELOG.md | 4 - src/PostgREST/App.hs | 8 +- src/PostgREST/Query/QueryBuilder.hs | 17 +- src/PostgREST/Request/DbRequestBuilder.hs | 2 +- src/PostgREST/Request/MutateQuery.hs | 1 - test/spec/Feature/Query/PgSafeUpdateSpec.hs | 142 ++++++++++++--- test/spec/Feature/Query/QueryLimitedSpec.hs | 7 +- test/spec/Feature/Query/SingularSpec.hs | 4 +- test/spec/Feature/Query/UpdateSpec.hs | 189 -------------------- test/spec/fixtures/data.sql | 22 +-- test/spec/fixtures/privileges.sql | 10 +- test/spec/fixtures/schema.sql | 37 ++-- 12 files changed, 161 insertions(+), 282 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d94ab5c..ffbf741f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - #2268, Allow returning XML from single-column queries - @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 + Requires postgis >= 3.0 + 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 + 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(*)`) - - #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 - #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 @@ -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 + 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(*)`. - - #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=` is done - #2312, Using `Prefer: return=representation` no longer returns a `Location` header - @laurenceisla ## [9.0.1] - 2022-06-03 diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 287388e0e..d0270bb5c 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -360,12 +360,8 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do pure $ Wai.responseLBS HTTP.status200 (contentTypeHeaders context) $ LBS.fromStrict plan handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response -handleUpdate identifier context@RequestContext{..} = do - let - ApiRequest{..} = ctxApiRequest - pkCols = maybe mempty tablePKCols $ HM.lookup identifier $ dbTables ctxDbStructure - - resultSet <- writeQuery MutationUpdate identifier False pkCols context +handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} _) = do + resultSet <- writeQuery MutationUpdate identifier False mempty context case resultSet of RSStandard{..} -> do diff --git a/src/PostgREST/Query/QueryBuilder.hs b/src/PostgREST/Query/QueryBuilder.hs index 9ae3d08f2..56295e13f 100644 --- a/src/PostgREST/Query/QueryBuilder.hs +++ b/src/PostgREST/Query/QueryBuilder.hs @@ -103,7 +103,7 @@ mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returning cols = BS.intercalate ", " $ pgFmtIdent <$> S.toList iCols -- 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 = -- 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= @@ -111,21 +111,17 @@ mutateRequestToQuery (Update mainQi uCols body logicForest pkFlts range ordts re SQL.sql $ "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false" | range == allRange = - let whereLogic | null logicForest = if null pkFlts then "FALSE" else pgrstUpdateBodyF - | otherwise = logicForestF in "WITH " <> normalizedBody body <> " " <> "UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <> - "FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) pgrst_update_body " <> - "WHERE " <> whereLogic <> " " <> + "FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) _ " <> + whereLogic <> " " <> SQL.sql (returningF mainQi returnings) | otherwise = - let whereLogic | null logicForest = mempty - | otherwise = " WHERE " <> logicForestF in "WITH " <> normalizedBody body <> ", " <> "pgrst_update_body AS (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " ) LIMIT 1), " <> "pgrst_affected_rows AS (" <> - "SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <> " " <> + "SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <> whereLogic <> " " <> orderF mainQi ordts <> " " <> limitOffsetF range <> @@ -136,11 +132,10 @@ mutateRequestToQuery (Update mainQi uCols body logicForest pkFlts range ordts re SQL.sql (returningF mainQi returnings) where + whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) 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) - 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) (whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts) diff --git a/src/PostgREST/Request/DbRequestBuilder.hs b/src/PostgREST/Request/DbRequestBuilder.hs index 9e9c663f1..5df3cd38a 100644 --- a/src/PostgREST/Request/DbRequestBuilder.hs +++ b/src/PostgREST/Request/DbRequestBuilder.hs @@ -318,7 +318,7 @@ mutateRequest mutation schema tName ApiRequest{..} pkCols readReq = mapLeft ApiR case mutation of MutationCreate -> 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 -> if null qsLogic && qsFilterFields == S.fromList pkCols && diff --git a/src/PostgREST/Request/MutateQuery.hs b/src/PostgREST/Request/MutateQuery.hs index cf0e62638..5d5d85616 100644 --- a/src/PostgREST/Request/MutateQuery.hs +++ b/src/PostgREST/Request/MutateQuery.hs @@ -31,7 +31,6 @@ data MutateQuery , updCols :: S.Set FieldName , updBody :: Maybe LBS.ByteString , where_ :: [LogicTree] - , pkFilters :: [FieldName] , mutRange :: NonnegRange , mutOrder :: [OrderTerm] , returning :: [FieldName] diff --git a/test/spec/Feature/Query/PgSafeUpdateSpec.hs b/test/spec/Feature/Query/PgSafeUpdateSpec.hs index e53412a85..66e73371b 100644 --- a/test/spec/Feature/Query/PgSafeUpdateSpec.hs +++ b/test/spec/Feature/Query/PgSafeUpdateSpec.hs @@ -13,21 +13,58 @@ import SpecHelper spec :: SpecWith ((), Application) spec = describe "Enabling pg-safeupdate" $ do - context "Full table update" $ - it "does not update and throws no error if no condition is present" $ - request methodPatch "/safe_update" + context "Full table update" $ do + it "does not update and throws error if no condition is present" $ + request methodPatch "/safe_update_items" [("Prefer", "count=exact")] [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` "" - { matchStatus = 404 + { matchStatus = 204 , 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 it "does not delete and throws error if no condition is present" $ - request methodDelete "/safe_delete" [] mempty + request methodDelete "/safe_delete_items" [] mempty `shouldRespondWith` [json|{ "code": "21000", @@ -37,40 +74,101 @@ spec = }|] { matchStatus = 400 } - it "allows full table delete if a filter is present" $ - request methodDelete "/safe_delete?id=gt.0" - [("Prefer", "count=exact")] + it "allows full table delete if a filter is present" $ do + get "/safe_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 "/safe_delete_items?id=gt.0" + [("Prefer", "tx=commit"), ("Prefer", "count=exact")] mempty `shouldRespondWith` "" { matchStatus = 204 , 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 = describe "Disabling pg-safeupdate" $ do - context "Full table update" $ - it "does not update and does not throw error if no condition is present" $ - request methodPatch "/unsafe_update" - [("Prefer", "count=exact")] - [json| {"name": "New name"} |] + context "Full table update" $ do + it "works if no condition is present" $ do + get "/unsafe_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 "/unsafe_update_items" + [("Prefer", "tx=commit"), ("Prefer", "count=exact")] + [json| {"name": "updated-item"} |] `shouldRespondWith` "" - { matchStatus = 404 + { matchStatus = 204 , matchHeaders = [ matchHeaderAbsent hContentType - , "Content-Range" <:> "*/0" ] + , "Content-Range" <:> "0-2/3" + , "Preference-Applied" <:> "tx=commit" ] } - context "Full table delete" $ - it "deletes and does not throw error if no condition is present" $ do - request methodDelete "/unsafe_delete" - [("Prefer", "count=exact")] + get "/unsafe_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": "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 `shouldRespondWith` "" { matchStatus = 204 , 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 } diff --git a/test/spec/Feature/Query/QueryLimitedSpec.hs b/test/spec/Feature/Query/QueryLimitedSpec.hs index 46f5e38ef..eb192876c 100644 --- a/test/spec/Feature/Query/QueryLimitedSpec.hs +++ b/test/spec/Feature/Query/QueryLimitedSpec.hs @@ -98,8 +98,11 @@ spec = [("Prefer", "return=representation")] [json| [{"occupation": "Barista"}] |] `shouldRespondWith` - [json|[]|] - { matchStatus = 404 } + [json|[ + { "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" $ request methodDelete "/employees?select=first_name,last_name" diff --git a/test/spec/Feature/Query/SingularSpec.hs b/test/spec/Feature/Query/SingularSpec.hs index 878fcde57..5f2122465 100644 --- a/test/spec/Feature/Query/SingularSpec.hs +++ b/test/spec/Feature/Query/SingularSpec.hs @@ -65,7 +65,7 @@ spec = } it "raises an error for multiple rows" $ do - request methodPatch "/addresses?limit=4&order=id" + request methodPatch "/addresses" [("Prefer", "tx=commit"), singular] [json| { address: "zzz" } |] `shouldRespondWith` @@ -81,7 +81,7 @@ spec = [json|[{"id":1,"address":"address 1"}]|] 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] [json| { address: "zzz" } |] `shouldRespondWith` diff --git a/test/spec/Feature/Query/UpdateSpec.hs b/test/spec/Feature/Query/UpdateSpec.hs index 32a327e42..e7754e35b 100644 --- a/test/spec/Feature/Query/UpdateSpec.hs +++ b/test/spec/Feature/Query/UpdateSpec.hs @@ -388,17 +388,6 @@ spec = 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 get "/limited_update_items" `shouldRespondWith` @@ -624,181 +613,3 @@ spec = do [json| {"tbl_name": "limited_update_items_no_pk"} |] `shouldRespondWith` "" { 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] - } diff --git a/test/spec/fixtures/data.sql b/test/spec/fixtures/data.sql index 108213374..b8d121da6 100644 --- a/test/spec/fixtures/data.sql +++ b/test/spec/fixtures/data.sql @@ -781,12 +781,6 @@ INSERT INTO private.internal_job (id, parent_id) VALUES (2, 1); TRUNCATE TABLE test.test CASCADE; 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; 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)'); @@ -804,11 +798,11 @@ INSERT INTO "SPECIAL ""@/\#~_-".names (id, name) VALUES (1, 'John'), (2, 'Mary') TRUNCATE TABLE do$llar$s CASCADE; INSERT INTO do$llar$s (a$num$) VALUES (100), (200), (300); -TRUNCATE TABLE safe_update CASCADE; -INSERT INTO safe_update(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); -TRUNCATE TABLE safe_delete CASCADE; -INSERT INTO safe_delete(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); -TRUNCATE TABLE unsafe_update CASCADE; -INSERT INTO unsafe_update(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); -TRUNCATE TABLE unsafe_delete CASCADE; -INSERT INTO unsafe_delete(id, name) VALUES (1, 'First'), (2, 'Second'), (3, 'Third'); +TRUNCATE TABLE safe_update_items CASCADE; +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_items CASCADE; +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_items CASCADE; +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_items CASCADE; +INSERT INTO unsafe_delete_items(id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL); diff --git a/test/spec/fixtures/privileges.sql b/test/spec/fixtures/privileges.sql index 9a0bc95a8..5a3e9fede 100644 --- a/test/spec/fixtures/privileges.sql +++ b/test/spec/fixtures/privileges.sql @@ -189,16 +189,14 @@ GRANT ALL ON TABLE , series_popularity , test , view_test - , bulk_update_items - , bulk_update_items_cpk , shops , shop_bles , "SPECIAL ""@/\#~_-".names , do$llar$s - , safe_update - , safe_delete - , unsafe_update - , unsafe_delete + , safe_update_items + , safe_delete_items + , unsafe_update_items + , unsafe_delete_items TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index b2e13ab19..d8deb1c8f 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -2534,7 +2534,7 @@ select * from limited_delete_items_cpk; create function reset_items_tables(tbl_name text default '') returns void as $_$ begin 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'); $$::text, tbl_name, tbl_name); @@ -2638,21 +2638,6 @@ CREATE TABLE test.test ( CREATE OR REPLACE VIEW test.view_test AS 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 table shops ( @@ -2692,24 +2677,28 @@ CREATE TABLE do$llar$s ( -- Tables and functions to test the pg-safeupdate library -CREATE TABLE test.safe_update( +CREATE TABLE test.safe_update_items( 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, - name TEXT + name TEXT, + observation TEXT ); -CREATE TABLE test.unsafe_update( +CREATE TABLE test.unsafe_update_items( 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, - name TEXT + name TEXT, + observation TEXT ); CREATE OR REPLACE FUNCTION test.load_safeupdate() RETURNS VOID AS $$