Add missing tests for NOT IN accepting empty values and refactor
Go back to operators map since the previously defined Operator constructors added innecesary complexity also rename Path to EmbedPath
This commit is contained in:
@@ -180,7 +180,7 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
|||||||
of type (ReadRequest->ReadRequest) that are in (Either ParseError a) context
|
of type (ReadRequest->ReadRequest) that are in (Either ParseError a) context
|
||||||
-}
|
-}
|
||||||
where
|
where
|
||||||
filters :: Either ApiRequestError [(Path, Filter)]
|
filters :: Either ApiRequestError [(EmbedPath, Filter)]
|
||||||
filters = mapM pRequestFilter flts
|
filters = mapM pRequestFilter flts
|
||||||
where
|
where
|
||||||
action = iAction apiRequest
|
action = iAction apiRequest
|
||||||
@@ -188,30 +188,30 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
|
|||||||
| action == ActionRead = iFilters apiRequest
|
| action == ActionRead = iFilters apiRequest
|
||||||
| action == ActionInvoke = iFilters apiRequest
|
| action == ActionInvoke = iFilters apiRequest
|
||||||
| otherwise = filter (( "." `isInfixOf` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update
|
| otherwise = filter (( "." `isInfixOf` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update
|
||||||
orders :: Either ApiRequestError [(Path, [OrderTerm])]
|
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
|
||||||
orders = mapM pRequestOrder $ iOrder apiRequest
|
orders = mapM pRequestOrder $ iOrder apiRequest
|
||||||
ranges :: Either ApiRequestError [(Path, NonnegRange)]
|
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
|
||||||
ranges = mapM pRequestRange $ M.toList $ iRange apiRequest
|
ranges = mapM pRequestRange $ M.toList $ iRange apiRequest
|
||||||
|
|
||||||
addFilterToNode :: Filter -> ReadRequest -> ReadRequest
|
addFilterToNode :: Filter -> ReadRequest -> ReadRequest
|
||||||
addFilterToNode flt (Node (q@Select {flt_=flts}, i) f) = Node (q {flt_=flt:flts}, i) f
|
addFilterToNode flt (Node (q@Select {flt_=flts}, i) f) = Node (q {flt_=flt:flts}, i) f
|
||||||
|
|
||||||
addFilter :: (Path, Filter) -> ReadRequest -> ReadRequest
|
addFilter :: (EmbedPath, Filter) -> ReadRequest -> ReadRequest
|
||||||
addFilter = addProperty addFilterToNode
|
addFilter = addProperty addFilterToNode
|
||||||
|
|
||||||
addOrderToNode :: [OrderTerm] -> ReadRequest -> ReadRequest
|
addOrderToNode :: [OrderTerm] -> ReadRequest -> ReadRequest
|
||||||
addOrderToNode o (Node (q,i) f) = Node (q{order=Just o}, i) f
|
addOrderToNode o (Node (q,i) f) = Node (q{order=Just o}, i) f
|
||||||
|
|
||||||
addOrder :: (Path, [OrderTerm]) -> ReadRequest -> ReadRequest
|
addOrder :: (EmbedPath, [OrderTerm]) -> ReadRequest -> ReadRequest
|
||||||
addOrder = addProperty addOrderToNode
|
addOrder = addProperty addOrderToNode
|
||||||
|
|
||||||
addRangeToNode :: NonnegRange -> ReadRequest -> ReadRequest
|
addRangeToNode :: NonnegRange -> ReadRequest -> ReadRequest
|
||||||
addRangeToNode r (Node (q,i) f) = Node (q{range_=r}, i) f
|
addRangeToNode r (Node (q,i) f) = Node (q{range_=r}, i) f
|
||||||
|
|
||||||
addRange :: (Path, NonnegRange) -> ReadRequest -> ReadRequest
|
addRange :: (EmbedPath, NonnegRange) -> ReadRequest -> ReadRequest
|
||||||
addRange = addProperty addRangeToNode
|
addRange = addProperty addRangeToNode
|
||||||
|
|
||||||
addProperty :: (a -> ReadRequest -> ReadRequest) -> (Path, a) -> ReadRequest -> ReadRequest
|
addProperty :: (a -> ReadRequest -> ReadRequest) -> (EmbedPath, a) -> ReadRequest -> ReadRequest
|
||||||
addProperty f ([], a) n = f a n
|
addProperty f ([], a) n = f a n
|
||||||
addProperty f (path, a) (Node rn forest) =
|
addProperty f (path, a) (Node rn forest) =
|
||||||
case targetNode of
|
case targetNode of
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ module PostgREST.OpenAPI (
|
|||||||
|
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Data.Aeson (decode, encode)
|
import Data.Aeson (decode, encode)
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
|
import qualified Data.Set as Set
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Data.Text (unpack, pack, concat, intercalate, init, tail, toLower)
|
import Data.Text (unpack, pack, concat, intercalate, init, tail, toLower)
|
||||||
import qualified Data.Set as Set
|
|
||||||
import Network.URI (parseURI, isAbsoluteURI,
|
import Network.URI (parseURI, isAbsoluteURI,
|
||||||
URI (..), URIAuth (..))
|
URI (..), URIAuth (..))
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ import Data.Swagger
|
|||||||
import PostgREST.ApiRequest (ContentType(..))
|
import PostgREST.ApiRequest (ContentType(..))
|
||||||
import PostgREST.Config (prettyVersion)
|
import PostgREST.Config (prettyVersion)
|
||||||
import PostgREST.Types (Table(..), Column(..), PgArg(..),
|
import PostgREST.Types (Table(..), Column(..), PgArg(..),
|
||||||
Proxy(..), ProcDescription(..), toMime, Operator(..))
|
Proxy(..), ProcDescription(..), toMime, operators)
|
||||||
|
|
||||||
makeMimeList :: [ContentType] -> MimeList
|
makeMimeList :: [ContentType] -> MimeList
|
||||||
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
|
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
|
||||||
@@ -72,7 +73,7 @@ makeOperatorPattern =
|
|||||||
intercalate "|"
|
intercalate "|"
|
||||||
[ concat ["^", x, y, "[.]"] |
|
[ concat ["^", x, y, "[.]"] |
|
||||||
x <- ["not[.]", ""],
|
x <- ["not[.]", ""],
|
||||||
y <- map show [Equals ..] ]
|
y <- M.keys operators ]
|
||||||
|
|
||||||
makeRowFilter :: Column -> Param
|
makeRowFilter :: Column -> Param
|
||||||
makeRowFilter c =
|
makeRowFilter c =
|
||||||
|
|||||||
+13
-13
@@ -1,23 +1,23 @@
|
|||||||
module PostgREST.Parsers where
|
module PostgREST.Parsers where
|
||||||
|
|
||||||
import Protolude hiding (try, intercalate)
|
import Protolude hiding (try, intercalate)
|
||||||
import Control.Monad ((>>))
|
import Control.Monad ((>>))
|
||||||
import Data.Foldable (foldl1)
|
import Data.Foldable (foldl1)
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.Text (intercalate, replace, strip)
|
import Data.Text (intercalate, replace, strip)
|
||||||
import Data.List (init, last)
|
import Data.List (init, last)
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
|
import PostgREST.RangeQuery (NonnegRange,allRange)
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
import Text.ParserCombinators.Parsec hiding (many, (<|>))
|
import Text.ParserCombinators.Parsec hiding (many, (<|>))
|
||||||
import Text.Read (read)
|
|
||||||
import PostgREST.RangeQuery (NonnegRange,allRange)
|
|
||||||
import Text.Parsec.Error
|
import Text.Parsec.Error
|
||||||
|
|
||||||
pRequestSelect :: Text -> Text -> Either ApiRequestError ReadRequest
|
pRequestSelect :: Text -> Text -> Either ApiRequestError ReadRequest
|
||||||
pRequestSelect rootName selStr =
|
pRequestSelect rootName selStr =
|
||||||
mapError $ parse (pReadRequest rootName) ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
|
mapError $ parse (pReadRequest rootName) ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
|
||||||
|
|
||||||
pRequestFilter :: (Text, Text) -> Either ApiRequestError (Path, Filter)
|
pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
|
||||||
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||||
where
|
where
|
||||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||||
@@ -25,14 +25,14 @@ pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
|||||||
path = fst <$> treePath
|
path = fst <$> treePath
|
||||||
fld = snd <$> treePath
|
fld = snd <$> treePath
|
||||||
|
|
||||||
pRequestOrder :: (Text, Text) -> Either ApiRequestError (Path, [OrderTerm])
|
pRequestOrder :: (Text, Text) -> Either ApiRequestError (EmbedPath, [OrderTerm])
|
||||||
pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
|
pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
|
||||||
where
|
where
|
||||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||||
path = fst <$> treePath
|
path = fst <$> treePath
|
||||||
ord' = parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
|
ord' = parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
|
||||||
|
|
||||||
pRequestRange :: (ByteString, NonnegRange) -> Either ApiRequestError (Path, NonnegRange)
|
pRequestRange :: (ByteString, NonnegRange) -> Either ApiRequestError (EmbedPath, NonnegRange)
|
||||||
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
||||||
where
|
where
|
||||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||||
@@ -59,7 +59,7 @@ pReadRequest rootNodeName = do
|
|||||||
newForest =
|
newForest =
|
||||||
foldr treeEntry (Node (Select [] [fn] [] Nothing allRange, (fn, Nothing, alias)) []) fldForest:rForest
|
foldr treeEntry (Node (Select [] [fn] [] Nothing allRange, (fn, Nothing, alias)) []) fldForest:rForest
|
||||||
|
|
||||||
pTreePath :: Parser (Path,Field)
|
pTreePath :: Parser (EmbedPath, Field)
|
||||||
pTreePath = do
|
pTreePath = do
|
||||||
p <- pFieldName `sepBy1` pDelimiter
|
p <- pFieldName `sepBy1` pDelimiter
|
||||||
jp <- optionMaybe pJsonPath
|
jp <- optionMaybe pJsonPath
|
||||||
@@ -124,17 +124,17 @@ pOperation = try ( string "not" *> pDelimiter *> (Operation True <$> pExpr)) <|>
|
|||||||
where
|
where
|
||||||
pExpr :: Parser (Operator, Operand)
|
pExpr :: Parser (Operator, Operand)
|
||||||
pExpr =
|
pExpr =
|
||||||
((,) <$> (read <$> foldl1 (<|>) (try . string . show <$> notInOps)) <*> (pDelimiter *> pVText))
|
((,) <$> (toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys notInOps)) <*> pVText)
|
||||||
<|> try (string (show In) *> pDelimiter *> ((,) <$> pure In <*> pVTextL))
|
<|> ((,) <$> (toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys inOps)) <*> pVTextL)
|
||||||
<|> try (string (show NotIn) *> pDelimiter *> ((,) <$> pure NotIn <*> pVTextL))
|
|
||||||
<?> "operator (eq, gt, ...)"
|
<?> "operator (eq, gt, ...)"
|
||||||
notInOps = [Equals .. Contained]
|
inOps = M.filterWithKey (const . flip elem ["in", "notin"]) operators
|
||||||
|
notInOps = M.difference operators inOps
|
||||||
|
|
||||||
pVText :: Parser Operand
|
pVText :: Parser Operand
|
||||||
pVText = VText . toS <$> many anyChar
|
pVText = VText . toS <$> many anyChar
|
||||||
|
|
||||||
pVTextL :: Parser Operand
|
pVTextL :: Parser Operand
|
||||||
pVTextL = VTextL <$> pLValue `sepBy1` char ','
|
pVTextL = VTextL <$> lexeme pLValue `sepBy1` char ','
|
||||||
where
|
where
|
||||||
pLValue :: Parser Text
|
pLValue :: Parser Text
|
||||||
pLValue = toS <$> (try (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",") ) <|> many (noneOf ","))
|
pLValue = toS <$> (try (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",") ) <|> many (noneOf ","))
|
||||||
|
|||||||
@@ -379,7 +379,7 @@ getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
|
|||||||
ftN = tableName ft
|
ftN = tableName ft
|
||||||
ltN = fromMaybe "" (tableName <$> lt)
|
ltN = fromMaybe "" (tableName <$> lt)
|
||||||
toFilter :: Text -> Text -> Column -> Column -> Filter
|
toFilter :: Text -> Text -> Column -> Column -> Filter
|
||||||
toFilter tb ftb c fc = Filter (colName c, Nothing) (Operation False (Equals, VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}})))
|
toFilter tb ftb c fc = Filter (colName c, Nothing) (Operation False ("=", VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}})))
|
||||||
|
|
||||||
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
|
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
|
||||||
unicodeStatement = H.statement . T.encodeUtf8
|
unicodeStatement = H.statement . T.encodeUtf8
|
||||||
@@ -407,39 +407,37 @@ pgFmtSelectItem table (f@(_, jp), Nothing, alias) = pgFmtField table f <> pgFmtA
|
|||||||
pgFmtSelectItem table (f@(_, jp), Just cast, alias) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs jp alias
|
pgFmtSelectItem table (f@(_, jp), Just cast, alias) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs jp alias
|
||||||
|
|
||||||
pgFmtFilter :: QualifiedIdentifier -> Filter -> SqlFragment
|
pgFmtFilter :: QualifiedIdentifier -> Filter -> SqlFragment
|
||||||
pgFmtFilter table (Filter fld (Operation hasNot_ ex@(op, operand))) = notOp <> " " <> case operand of
|
pgFmtFilter table (Filter fld (Operation hasNot_ ex)) = notOp <> " " <> case ex of
|
||||||
VForeignKey fQi (ForeignKey Column{colTable=Table{tableName=fTableName}, colName=fColName}) ->
|
(op, VText val) -> pgFmtFieldOp op <> " " <> case op of
|
||||||
pgFmtField fQi fld <> " " <> opToSqlFragment op <> " " <> pgFmtColumn (removeSourceCTESchema (qiSchema fQi) fTableName) fColName
|
"like" -> unknownLiteral (T.map star val)
|
||||||
_ -> pgFmtField table fld <> " " <> pgFmtExpr ex
|
"ilike" -> unknownLiteral (T.map star val)
|
||||||
where
|
"@@" -> "to_tsquery(" <> unknownLiteral val <> ") "
|
||||||
notOp = if hasNot_ then "NOT" else ""
|
"is" -> whiteList val
|
||||||
|
"isnot" -> whiteList val
|
||||||
pgFmtExpr :: (Operator, Operand) -> SqlFragment
|
_ -> unknownLiteral val
|
||||||
pgFmtExpr ex =
|
(op, VTextL vals) -> pgFmtIn op vals -- in and notin
|
||||||
case ex of
|
(op, VForeignKey fQi (ForeignKey Column{colTable=Table{tableName=fTableName}, colName=fColName})) ->
|
||||||
(Like, VText val) -> opToSqlFragment Like <> " " <> unknownLiteral (T.map star val)
|
pgFmtField fQi fld <> " " <> sqlOperator op <> " " <> pgFmtColumn (removeSourceCTESchema (qiSchema fQi) fTableName) fColName
|
||||||
(ILike, VText val) -> opToSqlFragment ILike <> " " <> unknownLiteral (T.map star val)
|
|
||||||
(TSearch, VText val) -> opToSqlFragment TSearch <> " " <> "to_tsquery(" <> unknownLiteral val <> ") "
|
|
||||||
(Is, VText val) -> opToSqlFragment Is <> " " <> whiteList val
|
|
||||||
(In, VTextL vals) -> exprForIn vals
|
|
||||||
(NotIn, VTextL vals) -> opToSqlFragment NotIn <> " " <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
|
|
||||||
(op, VText val) -> opToSqlFragment op <> " " <> unknownLiteral val
|
|
||||||
_ -> "" -- should not happen, all possible combinations are defined in Parsers
|
|
||||||
where
|
where
|
||||||
|
pgFmtFieldOp op = pgFmtField table fld <> " " <> sqlOperator op
|
||||||
|
sqlOperator o = HM.lookupDefault "=" o operators
|
||||||
|
notOp = if hasNot_ then "NOT" else ""
|
||||||
star c = if c == '*' then '%' else c
|
star c = if c == '*' then '%' else c
|
||||||
unknownLiteral = (<> "::unknown ") . pgFmtLit
|
unknownLiteral = (<> "::unknown ") . pgFmtLit
|
||||||
whiteList :: Text -> SqlFragment
|
whiteList :: Text -> SqlFragment
|
||||||
whiteList v = fromMaybe
|
whiteList v = fromMaybe
|
||||||
(toS (pgFmtLit v) <> "::unknown ")
|
(toS (pgFmtLit v) <> "::unknown ")
|
||||||
(find ((==) . toLower $ v) ["null","true","false"])
|
(find ((==) . toLower $ v) ["null","true","false"])
|
||||||
exprForIn :: [Text] -> SqlFragment
|
pgFmtIn :: Operator -> [Text] -> SqlFragment
|
||||||
exprForIn vals =
|
pgFmtIn op vals =
|
||||||
let emptyValForIn = "= any('{}') " in
|
-- Workaround because for postgresql "col IN ()" is invalid syntax, we instead do "col = any('{}')"
|
||||||
|
let emptyValForIn o = (if "not" `isInfixOf` o then "NOT " else "") -- handle case of "notin" operator
|
||||||
|
<> pgFmtField table fld <> " = any('{}') " in
|
||||||
case T.null <$> headMay vals of
|
case T.null <$> headMay vals of
|
||||||
Just isNull -> if isNull && length vals == 1
|
Just isNull -> if isNull && length vals == 1
|
||||||
then emptyValForIn
|
then emptyValForIn op
|
||||||
else opToSqlFragment In <> " " <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
|
else pgFmtFieldOp op <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
|
||||||
Nothing -> emptyValForIn
|
Nothing -> emptyValForIn op
|
||||||
|
|
||||||
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
|
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
|
||||||
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
|
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
|
||||||
|
|||||||
+24
-62
@@ -1,10 +1,9 @@
|
|||||||
module PostgREST.Types where
|
module PostgREST.Types where
|
||||||
import Protolude
|
import Protolude
|
||||||
import qualified GHC.Show
|
import qualified GHC.Show
|
||||||
import qualified GHC.Read
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
@@ -137,66 +136,27 @@ data Proxy = Proxy {
|
|||||||
, proxyPath :: Text
|
, proxyPath :: Text
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
data Operator = Equals | Gte | Gt | Lte | Lt | Neq | Like | ILike | Is | IsNot |
|
type Operator = Text
|
||||||
TSearch | Contains | Contained | In | NotIn deriving (Eq, Enum)
|
operators :: M.HashMap Operator SqlFragment
|
||||||
|
operators = M.fromList [
|
||||||
instance Show Operator where
|
("eq", "="),
|
||||||
show op = case op of
|
("gte", ">="),
|
||||||
Equals -> "eq"
|
("gt", ">"),
|
||||||
Gte -> "gte"
|
("lte", "<="),
|
||||||
Gt -> "gt"
|
("lt", "<"),
|
||||||
Lte -> "lte"
|
("neq", "<>"),
|
||||||
Lt -> "lt"
|
("like", "LIKE"),
|
||||||
Neq -> "neq"
|
("ilike", "ILIKE"),
|
||||||
Like -> "like"
|
("in", "IN"),
|
||||||
ILike -> "ilike"
|
("notin", "NOT IN"),
|
||||||
In -> "in"
|
("isnot", "IS NOT"),
|
||||||
NotIn -> "notin"
|
("is", "IS"),
|
||||||
IsNot -> "isnot"
|
("@@", "@@"),
|
||||||
Is -> "is"
|
("@>", "@>"),
|
||||||
TSearch -> "@@"
|
("<@", "<@")]
|
||||||
Contains -> "@>"
|
|
||||||
Contained -> "<@"
|
|
||||||
|
|
||||||
instance Read Operator where
|
|
||||||
readsPrec _ op = case op of
|
|
||||||
"eq" -> [(Equals, "")]
|
|
||||||
"gte" -> [(Gte, "")]
|
|
||||||
"gt" -> [(Gt, "")]
|
|
||||||
"lte" -> [(Lte, "")]
|
|
||||||
"lt" -> [(Lt, "")]
|
|
||||||
"neq" -> [(Neq, "")]
|
|
||||||
"like" -> [(Like, "")]
|
|
||||||
"ilike" -> [(ILike, "")]
|
|
||||||
"in" -> [(In, "")]
|
|
||||||
"notin" -> [(NotIn, "")]
|
|
||||||
"isnot" -> [(IsNot, "")]
|
|
||||||
"is" -> [(Is, "")]
|
|
||||||
"@@" -> [(TSearch, "")]
|
|
||||||
"@>" -> [(Contains, "")]
|
|
||||||
"<@" -> [(Contained, "")]
|
|
||||||
_ -> []
|
|
||||||
|
|
||||||
opToSqlFragment :: Operator -> SqlFragment
|
|
||||||
opToSqlFragment op = case op of
|
|
||||||
Equals -> "="
|
|
||||||
Gte -> ">="
|
|
||||||
Gt -> ">"
|
|
||||||
Lte -> "<="
|
|
||||||
Lt -> "<"
|
|
||||||
Neq -> "<>"
|
|
||||||
Like -> "LIKE"
|
|
||||||
ILike -> "ILIKE"
|
|
||||||
In -> "IN"
|
|
||||||
NotIn -> "NOT IN"
|
|
||||||
IsNot -> "IS NOT"
|
|
||||||
Is -> "IS"
|
|
||||||
TSearch -> "@@"
|
|
||||||
Contains -> "@>"
|
|
||||||
Contained -> "<@"
|
|
||||||
|
|
||||||
data Operation = Operation{ hasNot::Bool, expr::(Operator, Operand) } deriving (Eq, Show)
|
data Operation = Operation{ hasNot::Bool, expr::(Operator, Operand) } deriving (Eq, Show)
|
||||||
data Operand = VText Text | VTextL [Text] | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq)
|
data Operand = VText Text | VTextL [Text] | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq)
|
||||||
|
|
||||||
type FieldName = Text
|
type FieldName = Text
|
||||||
type JsonPath = [Text]
|
type JsonPath = [Text]
|
||||||
type Field = (FieldName, Maybe JsonPath)
|
type Field = (FieldName, Maybe JsonPath)
|
||||||
@@ -204,12 +164,14 @@ type Alias = Text
|
|||||||
type Cast = Text
|
type Cast = Text
|
||||||
type NodeName = Text
|
type NodeName = Text
|
||||||
type SelectItem = (Field, Maybe Cast, Maybe Alias)
|
type SelectItem = (Field, Maybe Cast, Maybe Alias)
|
||||||
type Path = [Text]
|
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
|
||||||
|
type EmbedPath = [Text]
|
||||||
|
data Filter = Filter { field::Field, operation::Operation } deriving (Show, Eq)
|
||||||
|
|
||||||
data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm], range_::NonnegRange } deriving (Show, Eq)
|
data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm], range_::NonnegRange } deriving (Show, Eq)
|
||||||
data MutateQuery = Insert { in_::TableName, qPayload::PayloadJSON, returning::[FieldName] }
|
data MutateQuery = Insert { in_::TableName, qPayload::PayloadJSON, returning::[FieldName] }
|
||||||
| Delete { in_::TableName, where_::[Filter], returning::[FieldName] }
|
| Delete { in_::TableName, where_::[Filter], returning::[FieldName] }
|
||||||
| Update { in_::TableName, qPayload::PayloadJSON, where_::[Filter], returning::[FieldName] } deriving (Show, Eq)
|
| Update { in_::TableName, qPayload::PayloadJSON, where_::[Filter], returning::[FieldName] } deriving (Show, Eq)
|
||||||
data Filter = Filter { field::Field, operation::Operation } deriving (Show, Eq)
|
|
||||||
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
|
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias))
|
||||||
type ReadRequest = Tree ReadNode
|
type ReadRequest = Tree ReadNode
|
||||||
type MutateRequest = MutateQuery
|
type MutateRequest = MutateQuery
|
||||||
|
|||||||
@@ -772,8 +772,8 @@ spec = do
|
|||||||
get "/w_or_wo_comma_names?name=in.Williams\"Hebdon, John\"" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=in.Williams\"Hebdon, John\"" `shouldRespondWith`
|
||||||
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
describe "IN empty set" $ do
|
describe "IN and NOT IN empty set" $ do
|
||||||
context "returns an empty result set when no value is present" $ do
|
context "returns an empty result for IN when no value is present" $ do
|
||||||
it "works for integer" $
|
it "works for integer" $
|
||||||
get "/items_with_different_col_types?int_data=in." `shouldRespondWith`
|
get "/items_with_different_col_types?int_data=in." `shouldRespondWith`
|
||||||
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
||||||
@@ -799,8 +799,17 @@ spec = do
|
|||||||
get "/items_with_different_col_types?time_data=in." `shouldRespondWith`
|
get "/items_with_different_col_types?time_data=in." `shouldRespondWith`
|
||||||
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns all results for notin when no value is present" $
|
||||||
|
get "/items_with_different_col_types?int_data=notin.&select=int_data" `shouldRespondWith`
|
||||||
|
[json| [{int_data: 1}] |] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns all results for not.in when no value is present" $
|
||||||
|
get "/items_with_different_col_types?int_data=not.in.&select=int_data" `shouldRespondWith`
|
||||||
|
[json| [{int_data: 1}] |] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "returns an empty result ignoring spaces" $
|
it "returns an empty result ignoring spaces" $
|
||||||
get "/items_with_different_col_types?int_data=in. " `shouldRespondWith` 400
|
get "/items_with_different_col_types?int_data=in. " `shouldRespondWith`
|
||||||
|
[json| [] |] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "only returns an empty result set if the in value is empty" $
|
it "only returns an empty result set if the in value is empty" $
|
||||||
get "/items_with_different_col_types?int_data=in. ,3,4" `shouldRespondWith` 400
|
get "/items_with_different_col_types?int_data=in. ,3,4" `shouldRespondWith` 400
|
||||||
|
|||||||
Vendored
+3
@@ -294,6 +294,9 @@ INSERT INTO w_or_wo_comma_names VALUES ('Smith, Joseph');
|
|||||||
INSERT INTO w_or_wo_comma_names VALUES ('David White');
|
INSERT INTO w_or_wo_comma_names VALUES ('David White');
|
||||||
INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
|
INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
|
||||||
|
|
||||||
|
TRUNCATE TABLE items_with_different_col_types CASCADE;
|
||||||
|
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user