feat: allow not_null value for the is operator

This commit is contained in:
M. Taimoor Zaeem
2024-12-19 10:19:41 -05:00
committed by Steve Chavez
parent a1769d17be
commit a9d74eba2a
7 changed files with 51 additions and 31 deletions
+1
View File
@@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #3560, Log resolved host in "Listening on ..." messages - @develop7
- #3727, Log maximum pool size - @steve-chavez
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
### Fixed
+1 -1
View File
@@ -72,7 +72,7 @@ imatch :code:`~*` ~* operator, see :ref:`pattern_matching`
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
also supports commas in quoted strings like
:code:`?a=in.("hi,there","yes,you")`
is :code:`IS` checking for exact equality (null,true,false,unknown)
is :code:`IS` checking for exact equality (null,not_null,true,false,unknown)
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
fts :code:`@@` :ref:`fts` using to_tsquery
plfts :code:`@@` :ref:`fts` using plainto_tsquery
+9 -9
View File
@@ -46,7 +46,7 @@ import PostgREST.SchemaCache.Identifiers (FieldName)
import PostgREST.ApiRequest.Types (AggregateFunction (..),
EmbedParam (..), EmbedPath, Field,
Filter (..), FtsOperator (..),
Hint, JoinType (..),
Hint, IsVal (..), JoinType (..),
JsonOperand (..),
JsonOperation (..), JsonPath,
ListVal, LogicOperator (..),
@@ -56,8 +56,7 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
OrderNulls (..), OrderTerm (..),
QPError (..), QuantOperator (..),
SelectItem (..),
SimpleOperator (..), SingleVal,
TrileanVal (..))
SimpleOperator (..), SingleVal)
import Protolude hiding (Sum, try)
@@ -640,7 +639,7 @@ pOpExpr pSVal = do
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
pIs = Is <$> (try (string "is" *> pDelimiter) *> pIsVal)
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
@@ -653,11 +652,12 @@ pOpExpr pSVal = do
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
pDelimiter *> (OpQuant op quant <$> pSVal)
pTriVal = try (ciString "null" $> TriNull)
<|> try (ciString "unknown" $> TriUnknown)
<|> try (ciString "true" $> TriTrue)
<|> try (ciString "false" $> TriFalse)
<?> "null or trilean value (unknown, true, false)"
pIsVal = try (ciString "null" $> IsNull)
<|> try (ciString "not_null" $> IsNotNull)
<|> try (ciString "true" $> IsTriTrue)
<|> try (ciString "false" $> IsTriFalse)
<|> try (ciString "unknown" $> IsTriUnknown)
<?> "isVal: (null, not_null, true, false, unknown)"
pFts = do
op <- try (string "fts" $> FilterFts)
+9 -8
View File
@@ -28,7 +28,7 @@ module PostgREST.ApiRequest.Types
, RaiseError(..)
, RangeError(..)
, SingleVal
, TrileanVal(..)
, IsVal(..)
, SimpleOperator(..)
, QuantOperator(..)
, FtsOperator(..)
@@ -218,7 +218,7 @@ data Operation
= Op SimpleOperator SingleVal
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
| In ListVal
| Is TrileanVal
| Is IsVal
| IsDistinctFrom SingleVal
| Fts FtsOperator (Maybe Language) SingleVal
deriving (Eq, Show)
@@ -231,12 +231,13 @@ type SingleVal = Text
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
type ListVal = [Text]
-- | Three-valued logic values
data TrileanVal
= TriTrue
| TriFalse
| TriNull
| TriUnknown
data IsVal
= IsNull
| IsNotNull
-- Trilean values
| IsTriTrue
| IsTriFalse
| IsTriUnknown
deriving (Eq, Show)
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
+4 -4
View File
@@ -795,8 +795,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
--
-- Setup:
--
-- >>> let nullOp = OpExpr True (Is TriNull)
-- >>> let nonNullOp = OpExpr False (Is TriNull)
-- >>> let nullOp = OpExpr True (Is IsNull)
-- >>> let nonNullOp = OpExpr False (Is IsNull)
-- >>> let notEqOp = OpExpr True (Op OpNotEqual "val")
-- >>> :{
-- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)`
@@ -847,7 +847,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
--
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is TriNull)})]
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
--
-- 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.
--
@@ -869,7 +869,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _ _) opExpr)) ->
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
case (foundRP, opExpr) of
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
_ -> Right flt
flt@(CoercibleStmnt _) ->
Right flt
+10 -8
View File
@@ -59,6 +59,7 @@ import NeatInterpolation (trimming)
import PostgREST.ApiRequest.Types (AggregateFunction (..),
Alias, Cast,
FtsOperator (..),
IsVal (..),
JsonOperand (..),
JsonOperation (..),
JsonPath,
@@ -69,8 +70,7 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
OrderDirection (..),
OrderNulls (..),
QuantOperator (..),
SimpleOperator (..),
TrileanVal (..))
SimpleOperator (..))
import PostgREST.MediaType (MTVndPlanFormat (..),
MTVndPlanOption (..))
import PostgREST.Plan.ReadPlan (JoinCondition (..))
@@ -380,13 +380,15 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
-- 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.
-- 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.
-- This is why `IS` operands are whitelisted at the Parsers.hs level
Is triVal -> " IS " <> case triVal of
TriTrue -> "TRUE"
TriFalse -> "FALSE"
TriNull -> "NULL"
TriUnknown -> "UNKNOWN"
Is isVal -> " IS " <>
case isVal of
IsNull -> "NULL"
IsNotNull -> "NOT NULL"
IsTriTrue -> "TRUE"
IsTriFalse -> "FALSE"
IsTriUnknown -> "UNKNOWN"
IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val
+17 -1
View File
@@ -63,11 +63,21 @@ spec = do
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches not_null using is operator" $
get "/no_pk?a=is.not_null" `shouldRespondWith`
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches nulls in varchar and numeric fields alike" $ do
get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |]
{ matchHeaders = [matchContentTypeJson] }
it "not.is.not_null is equivalent to is.null" $ do
get "/no_pk?a=not.is.not_null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]
it "matches with trilean values" $ do
@@ -83,11 +93,17 @@ spec = do
[json| [{"id": 3, "name": "wash the dishes", "done": null }] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches with trilean values in upper or mixed case" $ do
it "matches with null and not_null values in upper or mixed case" $ do
get "/chores?done=is.NULL" `shouldRespondWith`
[json| [{"id": 3, "name": "wash the dishes", "done": null }] |]
{ matchHeaders = [matchContentTypeJson] }
get "/chores?done=is.NoT_NuLl" `shouldRespondWith`
[json| [{"id": 1, "name": "take out the garbage", "done": true }
,{"id": 2, "name": "do the laundry", "done": false }] |]
{ matchHeaders = [matchContentTypeJson] }
it "matches with trilean values in upper or mixed case" $ do
get "/chores?done=is.TRUE" `shouldRespondWith`
[json| [{"id": 1, "name": "take out the garbage", "done": true }] |]
{ matchHeaders = [matchContentTypeJson] }