Skip to content

Commit f531476

Browse files
taimoorzaeemsteve-chavez
authored andcommitted
fix: filter on unselected columns in a table-valued function
1 parent cd5a611 commit f531476

File tree

5 files changed

+57
-4
lines changed

5 files changed

+57
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
3535
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
3636
- #3600, #3926, Improve JWT errors - @taimoorzaeem
3737
- #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem
38+
- #3965, Fix filter on unselected columns in a table-valued function - @taimoorzaeem
3839

3940
### Changed
4041

src/PostgREST/Plan.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,7 @@ callPlan proc ApiRequest{} paramKeys args readReq = FunctionCall {
10191019
, funCScalar = funcReturnsScalar proc
10201020
, funCSetOfScalar = funcReturnsSetOfScalar proc
10211021
, funCRetCompositeAlias = funcReturnsCompositeAlias proc
1022+
, funCFilterFields = getFilterFieldNames readReq
10221023
, funCReturning = inferColsEmbedNeeds readReq []
10231024
}
10241025
where
@@ -1028,6 +1029,22 @@ callPlan proc ApiRequest{} paramKeys args readReq = FunctionCall {
10281029
| otherwise -> KeyParams $ specifiedParams [prm]
10291030
prms -> KeyParams $ specifiedParams prms
10301031

1032+
-- | Get filter fields/column names from read plan
1033+
getFilterFieldNames :: ReadPlanTree -> Set FieldName
1034+
getFilterFieldNames rpt = S.fromList $ foldr (\rp names -> names <> rpToFieldNames rp) [] rpt
1035+
where
1036+
rpToFieldNames :: ReadPlan -> [FieldName]
1037+
rpToFieldNames = logicTreesToFieldName . ReadPlan.where_
1038+
1039+
logicTreesToFieldName :: [CoercibleLogicTree] -> [FieldName]
1040+
logicTreesToFieldName = concatMap coLogicTreeToFieldNames
1041+
1042+
coLogicTreeToFieldNames :: CoercibleLogicTree -> [FieldName]
1043+
coLogicTreeToFieldNames = \case
1044+
CoercibleStmnt (CoercibleFilter{field=CoercibleField{cfName}}) -> [cfName]
1045+
CoercibleStmnt (CoercibleFilterNullEmbed _ cfName) -> [cfName] -- needs test coverage
1046+
CoercibleExpr _ _ clts -> concatMap coLogicTreeToFieldNames clts
1047+
10311048
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
10321049
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> S.Set FieldName
10331050
inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols

src/PostgREST/Plan/CallPlan.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ data CallPlan = FunctionCall
2525
, funCScalar :: Bool
2626
, funCSetOfScalar :: Bool
2727
, funCRetCompositeAlias :: Bool
28+
, funCFilterFields :: Set FieldName
2829
, funCReturning :: Set FieldName
2930
}
3031

src/PostgREST/Query/QueryBuilder.hs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ mutatePlanToQuery (Delete mainQi logicForest returnings) =
170170
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
171171

172172
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
173-
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
173+
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias filterFields returnings) pgVer =
174174
"SELECT " <> (if returnsScalar || returnsSetOfScalar then "pgrst_call.pgrst_scalar" else returnedColumns) <> " " <>
175175
fromCall
176176
where
@@ -211,10 +211,16 @@ callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScal
211211
-- We could fallback to providing this NULL value in those cases.
212212
encodeArg Nothing = "NULL"
213213

214+
-- the columns here would be the returnings + the columns that would later
215+
-- be used by a where clause filter, if they intersect, we remove the duplicates
216+
-- and if * is returned then no need to explicitly add filter columns
214217
returnedColumns :: SQL.Snippet
215-
returnedColumns
216-
| null returnings = "*"
217-
| otherwise = intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> S.toList returnings)
218+
returnedColumns = case S.toList returnings of
219+
[] -> "*"
220+
["*"] -> pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") "*"
221+
_ -> intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnedColumns')
222+
where
223+
returnedColumns' = S.toList $ returnings <> filterFields
218224

219225
-- | SQL query meant for COUNTing the root node of the Tree.
220226
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.

test/spec/Feature/Query/RpcSpec.hs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,3 +1422,31 @@ spec =
14221422
, matchHeaders = [ "Content-Length" <:> "105"
14231423
, matchContentTypeJson ]
14241424
}
1425+
1426+
context "test table valued function with filter" $ do
1427+
it "works with filter on unselected columns" $
1428+
request methodGet "/rpc/getallprojects?select=id,client_id&name=like.OSX"
1429+
[] ""
1430+
`shouldRespondWith`
1431+
[json| [{"id":4,"client_id":2}] |]
1432+
{ matchStatus = 200
1433+
, matchHeaders = [matchContentTypeJson]
1434+
}
1435+
1436+
it "works with filter on unselected columns with null embed" $
1437+
request methodGet "/rpc/getallprojects?select=id,clients(id)&clients.name=not.is.null"
1438+
[] ""
1439+
`shouldRespondWith`
1440+
[json| [{"id":1,"clients":{"id": 1}}, {"id":2,"clients":{"id": 1}}, {"id":3,"clients":{"id": 2}}, {"id":4,"clients":{"id": 2}}, {"id":5,"clients":null}] |]
1441+
{ matchStatus = 200
1442+
, matchHeaders = [matchContentTypeJson]
1443+
}
1444+
1445+
it "works with logical filter on unselected columns" $
1446+
request methodGet "/rpc/getallprojects?select=id,client_id&or=(name.like.OSX,name.like.IOS)"
1447+
[] ""
1448+
`shouldRespondWith`
1449+
[json| [{"id":3,"client_id":2}, {"id":4,"client_id":2}] |]
1450+
{ matchStatus = 200
1451+
, matchHeaders = [matchContentTypeJson]
1452+
}

0 commit comments

Comments
 (0)