diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c61e38a7..7aa83be92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. From versio ### Fixed - Ensure Listener connections are released by @mkleczek in #4614 +- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707 ## [14.3] - 2026-01-03 diff --git a/src/PostgREST/Plan.hs b/src/PostgREST/Plan.hs index f070b1e33..e0d632ee4 100644 --- a/src/PostgREST/Plan.hs +++ b/src/PostgREST/Plan.hs @@ -963,10 +963,17 @@ addRanges ApiRequest{..} rReq = addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addLogicTrees ctx ApiRequest{..} rReq = - foldr addLogicTreeToNode (Right rReq) qsLogic + foldr addLogicTreeToNode (Right rReq) logic where QueryParams.QueryParams{..} = iQueryParams + logic = + case iAction of + ActDb (ActRelationRead _ _) -> qsLogic + ActDb (ActRoutine _ _) -> qsLogic + -- For mutations, take the non-root logic filters. These will only affect the embeddings and not the top level of the returned representation. + _ -> filter (not . null . fst) qsLogic + addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f) diff --git a/test/spec/Feature/Query/AndOrParamsSpec.hs b/test/spec/Feature/Query/AndOrParamsSpec.hs index 0a206c99b..faa344214 100644 --- a/test/spec/Feature/Query/AndOrParamsSpec.hs +++ b/test/spec/Feature/Query/AndOrParamsSpec.hs @@ -252,21 +252,39 @@ spec = [json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|] { matchStatus = 201 } - context "used with PATCH" $ + context "used with PATCH" $ do it "succeeds when using and/or params" $ request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name" [("Prefer", "return=representation")] [json|{ name : "updated grandchild entity"}|] `shouldRespondWith` [json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|] { matchHeaders = [matchContentTypeJson] } + it "succeeds when the filtered column is modified" $ + request methodPatch "/entities?select=id,name&or=(name.is.null,name.like.*test*)" + [("Prefer", "return=representation")] + [json|{ "name" : "updated entity" }|] `shouldRespondWith` + [json|[{ "id": 4, "name": "updated entity" }]|] + { matchHeaders = [matchContentTypeJson] } + it "succeeds when the filtered column is not selected in the returned representation" $ + request methodPatch "/entities?select=id&or=(name.is.null,name.like.*test*)" + [("Prefer", "return=representation")] + [json|{ "name" : "updated entity" }|] `shouldRespondWith` + [json|[{ "id": 4 }]|] + { matchHeaders = [matchContentTypeJson] } - context "used with DELETE" $ + context "used with DELETE" $ do it "succeeds when using and/or params" $ request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name" [("Prefer", "return=representation")] "" `shouldRespondWith` [json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|] + it "succeeds when the filtered column is not selected in the returned representation" $ + request methodDelete "/entities?select=id&or=(name.is.null,name.like.*test*)" + [("Prefer", "return=representation")] + "" + `shouldRespondWith` + [json|[{ "id": 4 }]|] it "can query columns that begin with and/or reserved words" $ get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200