feat: allow unknown on the is operator

Also make the IS values strict at the Parser level since IS cannot be
parametrized.
This commit is contained in:
steve-chavez
2021-11-18 17:01:58 -05:00
committed by Steve Chavez
parent 91689e28f0
commit bae5afbad6
8 changed files with 60 additions and 12 deletions
+2 -1
View File
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1075, Allow filtering top-level resource based on embedded resources filters - @steve-chavez, @Iced-Sun - #1075, Allow filtering top-level resource based on embedded resources filters - @steve-chavez, @Iced-Sun
+ This is enabled by adding `!inner` to the embedded resource, e.g. `/projects?select=*,clients!inner(*)&clients.id=eq.12` + This is enabled by adding `!inner` to the embedded resource, e.g. `/projects?select=*,clients!inner(*)&clients.id=eq.12`
+ This behavior can be enabled by default with the `db-embed-default-join='inner'` config option, which saves the need for specifying `!inner` on every request. In this case, you can go back to the previous behavior per request by specifying `!left` on the embedded resource, e.g `/projects?select=*,clients!left(*)&clients.id=eq.12` + This behavior can be enabled by default with the `db-embed-default-join='inner'` config option, which saves the need for specifying `!inner` on every request. In this case, you can go back to the previous behavior per request by specifying `!left` on the embedded resource, e.g `/projects?select=*,clients!left(*)&clients.id=eq.12`
- #1988, Allow specifying `unknown` for the `is` operator - @steve-chavez
### Fixed ### Fixed
@@ -586,4 +587,4 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- Make filter position match docs, e.g. `?order=col.asc` rather - Make filter position match docs, e.g. `?order=col.asc` rather
than `?order=asc.col`. than `?order=asc.col`.
+12 -9
View File
@@ -61,7 +61,8 @@ import PostgREST.Request.Types (Alias, Field, Filter (..),
Operation (..), Operation (..),
OrderDirection (..), OrderDirection (..),
OrderNulls (..), OrderNulls (..),
OrderTerm (..), SelectItem) OrderTerm (..), SelectItem,
TrileanVal (..))
import Protolude hiding (cast) import Protolude hiding (cast)
@@ -234,9 +235,18 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
Op op val -> pgFmtFieldOp op <> " " <> case op of Op op val -> pgFmtFieldOp op <> " " <> case op of
"like" -> unknownLiteral (T.map star val) "like" -> unknownLiteral (T.map star val)
"ilike" -> unknownLiteral (T.map star val) "ilike" -> unknownLiteral (T.map star val)
"is" -> isAllowed val
_ -> unknownLiteral val _ -> unknownLiteral val
-- 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.
-- This is why `IS` operands are whitelisted at the Parsers.hs level
Is triVal -> pgFmtField table fld <> " IS " <> case triVal of
TriTrue -> "TRUE"
TriFalse -> "FALSE"
TriNull -> "NULL"
TriUnknown -> "UNKNOWN"
-- We don't use "IN", we use "= ANY". IN has the following disadvantages: -- We don't use "IN", we use "= ANY". IN has the following disadvantages:
-- + No way to use an empty value on IN: "col IN ()" is invalid syntax. With ANY we can do "= ANY('{}')" -- + No way to use an empty value on IN: "col IN ()" is invalid syntax. With ANY we can do "= ANY('{}')"
-- + Can invalidate prepared statements: multiple parameters on an IN($1, $2, $3) will lead to using different prepared statements and not take advantage of caching. -- + Can invalidate prepared statements: multiple parameters on an IN($1, $2, $3) will lead to using different prepared statements and not take advantage of caching.
@@ -252,13 +262,6 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
sqlOperator o = SQL.sql $ M.lookupDefault "=" o operators sqlOperator o = SQL.sql $ M.lookupDefault "=" o operators
notOp = if hasNot then "NOT" else mempty notOp = if hasNot then "NOT" else mempty
star c = if c == '*' then '%' else c star c = if c == '*' then '%' else c
-- 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 keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
isAllowed :: Text -> SQL.Snippet
isAllowed v = SQL.sql $ maybe
(pgFmtLit v <> "::unknown") encodeUtf8
(find ((==) . T.toLower $ v) ["null","true","false"])
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) = pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
+8 -1
View File
@@ -195,15 +195,22 @@ pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)
pOperation = pOperation =
Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal
<|> In <$> (try (string "in" *> pDelimiter) *> pListVal) <|> In <$> (try (string "in" *> pDelimiter) *> pListVal)
<|> Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
<|> pFts <|> pFts
<?> "operator (eq, gt, ...)" <?> "operator (eq, gt, ...)"
pTriVal = try (string "null" $> TriNull)
<|> try (string "unknown" $> TriUnknown)
<|> try (string "true" $> TriTrue)
<|> try (string "false" $> TriFalse)
<?> "null or trilean value (unknown, true, false)"
pFts = do pFts = do
op <- foldl1 (<|>) (try . string . toS <$> ftsOps) op <- foldl1 (<|>) (try . string . toS <$> ftsOps)
lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_"))) lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_")))
pDelimiter >> Fts (toS op) (toS <$> lang) <$> pSVal pDelimiter >> Fts (toS op) (toS <$> lang) <$> pSVal
ops = M.filterWithKey (const . flip notElem ("in":ftsOps)) operators ops = M.filterWithKey (const . flip notElem ("in":"is":ftsOps)) operators
ftsOps = M.keys ftsOperators ftsOps = M.keys ftsOperators
pSingleVal :: Parser SingleVal pSingleVal :: Parser SingleVal
+10
View File
@@ -31,6 +31,7 @@ module PostgREST.Request.Types
, ReadRequest , ReadRequest
, SelectItem , SelectItem
, SingleVal , SingleVal
, TrileanVal(..)
, fstFieldNames , fstFieldNames
) where ) where
@@ -209,6 +210,7 @@ data OpExpr =
data Operation data Operation
= Op Operator SingleVal = Op Operator SingleVal
| In ListVal | In ListVal
| Is TrileanVal
| Fts Operator (Maybe Language) SingleVal | Fts Operator (Maybe Language) SingleVal
deriving (Eq) deriving (Eq)
@@ -220,3 +222,11 @@ type SingleVal = Text
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3) -- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
type ListVal = [Text] type ListVal = [Text]
-- | Three-valued logic values
data TrileanVal
= TriTrue
| TriFalse
| TriNull
| TriUnknown
deriving Eq
+17
View File
@@ -73,6 +73,23 @@ spec actualPgVersion = do
get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|] get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]
it "matches with trilean values" $ do
get "/chores?done=is.true" `shouldRespondWith`
[json| [{"id": 1, "name": "take out the garbage", "done": true }] |]
{ matchHeaders = [matchContentTypeJson] }
get "/chores?done=is.false" `shouldRespondWith`
[json| [{"id": 2, "name": "do the laundry", "done": false }] |]
{ matchHeaders = [matchContentTypeJson] }
get "/chores?done=is.unknown" `shouldRespondWith`
[json| [{"id": 3, "name": "wash the dishes", "done": null }] |]
{ matchHeaders = [matchContentTypeJson] }
it "fails if 'is' used and there's no null or trilean value" $ do
get "/chores?done=is.nil" `shouldRespondWith` 400
get "/chores?done=is.ok" `shouldRespondWith` 400
it "matches with like" $ do it "matches with like" $ do
get "/simple_pk?k=like.*yx" `shouldRespondWith` get "/simple_pk?k=like.*yx" `shouldRespondWith`
[json|[{"k":"xyyx","extra":"u"}]|] [json|[{"k":"xyyx","extra":"u"}]|]
+3
View File
@@ -727,3 +727,6 @@ INSERT INTO test.contact (id,name, clientid) values (1,'Wally Walton',1),(2,'Wil
TRUNCATE TABLE test.clientinfo CASCADE; TRUNCATE TABLE test.clientinfo CASCADE;
INSERT INTO test.clientinfo (id,clientid, other) values (1,1,'123 Main St'),(2,2,'456 South 3rd St'),(3,3,'789 Palm Tree Ln'); INSERT INTO test.clientinfo (id,clientid, other) values (1,1,'123 Main St'),(2,2,'456 South 3rd St'),(3,3,'789 Palm Tree Ln');
TRUNCATE TABLE test.chores CASCADE;
INSERT INTO test.chores (id, name, done) values (1, 'take out the garbage', true), (2, 'do the laundry', false), (3, 'wash the dishes', null);
+2 -1
View File
@@ -159,6 +159,7 @@ GRANT ALL ON TABLE
, client , client
, clientinfo , clientinfo
, contact , contact
, chores
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
@@ -213,4 +214,4 @@ DO $do$BEGIN
GRANT ALL ON TABLE test.car_models_car_dealers_10to20 TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_models_car_dealers_10to20 TO postgrest_test_anonymous;
GRANT ALL ON TABLE test.car_models_car_dealers_default TO postgrest_test_anonymous; GRANT ALL ON TABLE test.car_models_car_dealers_default TO postgrest_test_anonymous;
END IF; END IF;
END$do$; END$do$;
+6
View File
@@ -2417,3 +2417,9 @@ CREATE TABLE clientinfo (
, clientid int unique references client(id) , clientid int unique references client(id)
, other text , other text
); );
CREATE TABLE chores (
id int primary key
, name text
, done bool
);