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:
committed by
Steve Chavez
parent
91689e28f0
commit
bae5afbad6
+2
-1
@@ -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
|
||||
+ 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`
|
||||
- #1988, Allow specifying `unknown` for the `is` operator - @steve-chavez
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -586,4 +587,4 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Fixed
|
||||
- Make filter position match docs, e.g. `?order=col.asc` rather
|
||||
than `?order=asc.col`.
|
||||
than `?order=asc.col`.
|
||||
|
||||
@@ -61,7 +61,8 @@ import PostgREST.Request.Types (Alias, Field, Filter (..),
|
||||
Operation (..),
|
||||
OrderDirection (..),
|
||||
OrderNulls (..),
|
||||
OrderTerm (..), SelectItem)
|
||||
OrderTerm (..), SelectItem,
|
||||
TrileanVal (..))
|
||||
|
||||
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
|
||||
"like" -> unknownLiteral (T.map star val)
|
||||
"ilike" -> unknownLiteral (T.map star val)
|
||||
"is" -> isAllowed 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:
|
||||
-- + 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.
|
||||
@@ -252,13 +262,6 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
|
||||
sqlOperator o = SQL.sql $ M.lookupDefault "=" o operators
|
||||
notOp = if hasNot then "NOT" else mempty
|
||||
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 (qi1, col1) (qi2, col2)) =
|
||||
|
||||
@@ -195,15 +195,22 @@ pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)
|
||||
pOperation =
|
||||
Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal
|
||||
<|> In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||
<|> Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||
<|> pFts
|
||||
<?> "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
|
||||
op <- foldl1 (<|>) (try . string . toS <$> ftsOps)
|
||||
lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_")))
|
||||
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
|
||||
|
||||
pSingleVal :: Parser SingleVal
|
||||
|
||||
@@ -31,6 +31,7 @@ module PostgREST.Request.Types
|
||||
, ReadRequest
|
||||
, SelectItem
|
||||
, SingleVal
|
||||
, TrileanVal(..)
|
||||
, fstFieldNames
|
||||
) where
|
||||
|
||||
@@ -209,6 +210,7 @@ data OpExpr =
|
||||
data Operation
|
||||
= Op Operator SingleVal
|
||||
| In ListVal
|
||||
| Is TrileanVal
|
||||
| Fts Operator (Maybe Language) SingleVal
|
||||
deriving (Eq)
|
||||
|
||||
@@ -220,3 +222,11 @@ 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
|
||||
deriving Eq
|
||||
|
||||
@@ -73,6 +73,23 @@ spec actualPgVersion = do
|
||||
|
||||
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
|
||||
get "/simple_pk?k=like.*yx" `shouldRespondWith`
|
||||
[json|[{"k":"xyyx","extra":"u"}]|]
|
||||
|
||||
Vendored
+3
@@ -727,3 +727,6 @@ INSERT INTO test.contact (id,name, clientid) values (1,'Wally Walton',1),(2,'Wil
|
||||
|
||||
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');
|
||||
|
||||
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);
|
||||
|
||||
Vendored
+2
-1
@@ -159,6 +159,7 @@ GRANT ALL ON TABLE
|
||||
, client
|
||||
, clientinfo
|
||||
, contact
|
||||
, chores
|
||||
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_default TO postgrest_test_anonymous;
|
||||
END IF;
|
||||
END$do$;
|
||||
END$do$;
|
||||
|
||||
Vendored
+6
@@ -2417,3 +2417,9 @@ CREATE TABLE clientinfo (
|
||||
, clientid int unique references client(id)
|
||||
, other text
|
||||
);
|
||||
|
||||
CREATE TABLE chores (
|
||||
id int primary key
|
||||
, name text
|
||||
, done bool
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user