Skip to content

Commit a9d74eb

Browse files
taimoorzaeemsteve-chavez
authored andcommitted
feat: allow not_null value for the is operator
1 parent a1769d1 commit a9d74eb

File tree

7 files changed

+51
-31
lines changed

7 files changed

+51
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1313
- #3560, Log resolved host in "Listening on ..." messages - @develop7
1414
- #3727, Log maximum pool size - @steve-chavez
1515
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
16+
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
1617

1718
### Fixed
1819

docs/references/api/tables_views.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ imatch :code:`~*` ~* operator, see :ref:`pattern_matching`
7272
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
7373
– also supports commas in quoted strings like
7474
:code:`?a=in.("hi,there","yes,you")`
75-
is :code:`IS` checking for exact equality (null,true,false,unknown)
75+
is :code:`IS` checking for exact equality (null,not_null,true,false,unknown)
7676
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
7777
fts :code:`@@` :ref:`fts` using to_tsquery
7878
plfts :code:`@@` :ref:`fts` using plainto_tsquery

src/PostgREST/ApiRequest/QueryParams.hs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import PostgREST.SchemaCache.Identifiers (FieldName)
4646
import PostgREST.ApiRequest.Types (AggregateFunction (..),
4747
EmbedParam (..), EmbedPath, Field,
4848
Filter (..), FtsOperator (..),
49-
Hint, JoinType (..),
49+
Hint, IsVal (..), JoinType (..),
5050
JsonOperand (..),
5151
JsonOperation (..), JsonPath,
5252
ListVal, LogicOperator (..),
@@ -56,8 +56,7 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
5656
OrderNulls (..), OrderTerm (..),
5757
QPError (..), QuantOperator (..),
5858
SelectItem (..),
59-
SimpleOperator (..), SingleVal,
60-
TrileanVal (..))
59+
SimpleOperator (..), SingleVal)
6160

6261
import Protolude hiding (Sum, try)
6362

@@ -640,7 +639,7 @@ pOpExpr pSVal = do
640639
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
641640

642641
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
643-
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
642+
pIs = Is <$> (try (string "is" *> pDelimiter) *> pIsVal)
644643

645644
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
646645

@@ -653,11 +652,12 @@ pOpExpr pSVal = do
653652
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
654653
pDelimiter *> (OpQuant op quant <$> pSVal)
655654

656-
pTriVal = try (ciString "null" $> TriNull)
657-
<|> try (ciString "unknown" $> TriUnknown)
658-
<|> try (ciString "true" $> TriTrue)
659-
<|> try (ciString "false" $> TriFalse)
660-
<?> "null or trilean value (unknown, true, false)"
655+
pIsVal = try (ciString "null" $> IsNull)
656+
<|> try (ciString "not_null" $> IsNotNull)
657+
<|> try (ciString "true" $> IsTriTrue)
658+
<|> try (ciString "false" $> IsTriFalse)
659+
<|> try (ciString "unknown" $> IsTriUnknown)
660+
<?> "isVal: (null, not_null, true, false, unknown)"
661661

662662
pFts = do
663663
op <- try (string "fts" $> FilterFts)

src/PostgREST/ApiRequest/Types.hs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module PostgREST.ApiRequest.Types
2828
, RaiseError(..)
2929
, RangeError(..)
3030
, SingleVal
31-
, TrileanVal(..)
31+
, IsVal(..)
3232
, SimpleOperator(..)
3333
, QuantOperator(..)
3434
, FtsOperator(..)
@@ -218,7 +218,7 @@ data Operation
218218
= Op SimpleOperator SingleVal
219219
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
220220
| In ListVal
221-
| Is TrileanVal
221+
| Is IsVal
222222
| IsDistinctFrom SingleVal
223223
| Fts FtsOperator (Maybe Language) SingleVal
224224
deriving (Eq, Show)
@@ -231,12 +231,13 @@ type SingleVal = Text
231231
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
232232
type ListVal = [Text]
233233

234-
-- | Three-valued logic values
235-
data TrileanVal
236-
= TriTrue
237-
| TriFalse
238-
| TriNull
239-
| TriUnknown
234+
data IsVal
235+
= IsNull
236+
| IsNotNull
237+
-- Trilean values
238+
| IsTriTrue
239+
| IsTriFalse
240+
| IsTriUnknown
240241
deriving (Eq, Show)
241242

242243
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers

src/PostgREST/Plan.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -795,8 +795,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
795795
--
796796
-- Setup:
797797
--
798-
-- >>> let nullOp = OpExpr True (Is TriNull)
799-
-- >>> let nonNullOp = OpExpr False (Is TriNull)
798+
-- >>> let nullOp = OpExpr True (Is IsNull)
799+
-- >>> let nonNullOp = OpExpr False (Is IsNull)
800800
-- >>> let notEqOp = OpExpr True (Op OpNotEqual "val")
801801
-- >>> :{
802802
-- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)`
@@ -847,7 +847,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
847847
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
848848
--
849849
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
850-
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is TriNull)})]
850+
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
851851
--
852852
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
853853
--
@@ -869,7 +869,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
869869
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _ _) opExpr)) ->
870870
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
871871
case (foundRP, opExpr) of
872-
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
872+
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
873873
_ -> Right flt
874874
flt@(CoercibleStmnt _) ->
875875
Right flt

src/PostgREST/Query/SqlFragment.hs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import NeatInterpolation (trimming)
5959
import PostgREST.ApiRequest.Types (AggregateFunction (..),
6060
Alias, Cast,
6161
FtsOperator (..),
62+
IsVal (..),
6263
JsonOperand (..),
6364
JsonOperation (..),
6465
JsonPath,
@@ -69,8 +70,7 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
6970
OrderDirection (..),
7071
OrderNulls (..),
7172
QuantOperator (..),
72-
SimpleOperator (..),
73-
TrileanVal (..))
73+
SimpleOperator (..))
7474
import PostgREST.MediaType (MTVndPlanFormat (..),
7575
MTVndPlanOption (..))
7676
import PostgREST.Plan.ReadPlan (JoinCondition (..))
@@ -380,13 +380,15 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
380380

381381
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
382382
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
383-
-- However that would not accept the TRUE/FALSE/NULL/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
383+
-- However that would not accept the TRUE/FALSE/NULL/"NOT NULL"/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
384384
-- This is why `IS` operands are whitelisted at the Parsers.hs level
385-
Is triVal -> " IS " <> case triVal of
386-
TriTrue -> "TRUE"
387-
TriFalse -> "FALSE"
388-
TriNull -> "NULL"
389-
TriUnknown -> "UNKNOWN"
385+
Is isVal -> " IS " <>
386+
case isVal of
387+
IsNull -> "NULL"
388+
IsNotNull -> "NOT NULL"
389+
IsTriTrue -> "TRUE"
390+
IsTriFalse -> "FALSE"
391+
IsTriUnknown -> "UNKNOWN"
390392

391393
IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val
392394

test/spec/Feature/Query/QuerySpec.hs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,21 @@ spec = do
6363
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
6464
{ matchHeaders = [matchContentTypeJson] }
6565

66+
it "matches not_null using is operator" $
67+
get "/no_pk?a=is.not_null" `shouldRespondWith`
68+
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
69+
{ matchHeaders = [matchContentTypeJson] }
70+
6671
it "matches nulls in varchar and numeric fields alike" $ do
6772
get "/no_pk?a=is.null" `shouldRespondWith`
6873
[json| [{"a": null, "b": null}] |]
6974
{ matchHeaders = [matchContentTypeJson] }
7075

76+
it "not.is.not_null is equivalent to is.null" $ do
77+
get "/no_pk?a=not.is.not_null" `shouldRespondWith`
78+
[json| [{"a": null, "b": null}] |]
79+
{ matchHeaders = [matchContentTypeJson] }
80+
7181
get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]
7282

7383
it "matches with trilean values" $ do
@@ -83,11 +93,17 @@ spec = do
8393
[json| [{"id": 3, "name": "wash the dishes", "done": null }] |]
8494
{ matchHeaders = [matchContentTypeJson] }
8595

86-
it "matches with trilean values in upper or mixed case" $ do
96+
it "matches with null and not_null values in upper or mixed case" $ do
8797
get "/chores?done=is.NULL" `shouldRespondWith`
8898
[json| [{"id": 3, "name": "wash the dishes", "done": null }] |]
8999
{ matchHeaders = [matchContentTypeJson] }
90100

101+
get "/chores?done=is.NoT_NuLl" `shouldRespondWith`
102+
[json| [{"id": 1, "name": "take out the garbage", "done": true }
103+
,{"id": 2, "name": "do the laundry", "done": false }] |]
104+
{ matchHeaders = [matchContentTypeJson] }
105+
106+
it "matches with trilean values in upper or mixed case" $ do
91107
get "/chores?done=is.TRUE" `shouldRespondWith`
92108
[json| [{"id": 1, "name": "take out the garbage", "done": true }] |]
93109
{ matchHeaders = [matchContentTypeJson] }

0 commit comments

Comments
 (0)