diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index e9e290d65..05bae221c 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -39,7 +39,8 @@ import PostgREST.Types ( QualifiedIdentifier (..) , ContentType(..) , ApiRequestError(..) , toMime - , operators) + , operators + , FtsMode(..)) import Data.Ranged.Ranges (Range(..), rangeIntersection, emptyRange) import qualified Data.CaseInsensitive as CI import Web.Cookie (parseCookiesText) @@ -141,8 +142,16 @@ userApiRequest schema req reqBody ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts _ -> (flts, []) flts = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn ["order", "limit", "offset", "and", "or"] k) ] - hasOperator val = foldr ((||) . flip T.isPrefixOf val) False $ (<> ".") <$> M.keys operators + hasOperator val = foldr ((||) . flip T.isPrefixOf val) False ((<> ".") <$> (M.keys operators ++ ["not", show Plain, show Phrase])) + || hasLanguageFts val isEmbedPath = T.isInfixOf "." + -- handle "?tsv=english.fts.possible" case + hasLanguageFts val = case T.splitOn "." val of + [_, "fts", _] -> True + [_, "fts"] -> True + [_, "@@", _] -> True -- TODO: '@@' deprecated + [_, "@@"] -> True + _ -> False isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path payload = case decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type" of diff --git a/src/PostgREST/Parsers.hs b/src/PostgREST/Parsers.hs index dfbbe1b7a..d27522dfc 100644 --- a/src/PostgREST/Parsers.hs +++ b/src/PostgREST/Parsers.hs @@ -147,8 +147,8 @@ pOpExpr pSVal pLVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOper "operator (eq, gt, ...)" pFts = do mode <- option Normal $ - try (string "phrase" *> pDelimiter *> pure Phrase) - <|> try (string "plain" *> pDelimiter *> pure Plain) + try (string (show Phrase) *> pDelimiter *> pure Phrase) + <|> try (string (show Plain) *> pDelimiter *> pure Plain) lang <- try (Just <$> manyTill (letter <|> digit <|> oneOf "_") (try (string ".fts") <|> try (string ".@@")) <* pDelimiter) -- TODO: '@@' deprecated <|> try (string "fts" *> pDelimiter) *> pure Nothing diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index df56a7234..3e9efd3cf 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -179,8 +179,13 @@ data Operation = Op Operator SingleVal | Fts FtsMode (Maybe Language) SingleVal | Join QualifiedIdentifier ForeignKey deriving (Eq, Show) -data FtsMode = Normal | Plain | Phrase deriving (Eq, Show) +data FtsMode = Normal | Plain | Phrase deriving Eq +instance Show FtsMode where + show Normal = mzero + show Plain = "plain" + show Phrase = "phrase" type Language = Text + -- | Represents a single value in a filter, e.g. id=eq.singleval type SingleVal = Text -- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3) diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 517527d71..43712c7d1 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -286,3 +286,38 @@ spec = get "/rpc/get_projects_below?id=5&id=gt.2&select=id" `shouldRespondWith` [json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] } + + it "should work with filters that have the not operator" $ do + get "/rpc/get_projects_below?id=5&id=not.gt.2&select=id" `shouldRespondWith` + [json|[{ "id": 1 }, { "id": 2 }]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_projects_below?id=5&id=not.in.(1,3)&select=id" `shouldRespondWith` + [json|[{ "id": 2 }, { "id": 4 }]|] + { matchHeaders = [matchContentTypeJson] } + + it "should work with filters that use the plain/phrase with language fts operator" $ do + get "/rpc/get_tsearch?text_search_vector=english.fts.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=plain.fts.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=phrase.english.fts.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=not.english.fts.fun%7Crat" `shouldRespondWith` + [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] + { matchHeaders = [matchContentTypeJson] } + -- TODO: '@@' deprecated + get "/rpc/get_tsearch?text_search_vector=english.@@.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=plain.@@.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=phrase.english.@@.impossible" `shouldRespondWith` + [json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|] + { matchHeaders = [matchContentTypeJson] } + get "/rpc/get_tsearch?text_search_vector=not.english.@@.fun%7Crat" `shouldRespondWith` + [json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|] + { matchHeaders = [matchContentTypeJson] } diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index be35626e3..b10adeb31 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1250,6 +1250,10 @@ $$ language sql; create function test.privileged_hello(name text) returns text as $$ select 'Privileged hello to ' || $1; $$ language sql; + +create function test.get_tsearch() returns setof test.tsearch AS $$ + SELECT * FROM test.tsearch; +$$ language sql; -- -- PostgreSQL database dump complete --