Fix #701: allow quoted values for IN operator, refactor and Fix #641: allow IN filter to have no values (#854)

This commit is contained in:
Steve Chávez
2017-04-11 00:57:44 -05:00
committed by Joe Nelson
parent 5fffbbe381
commit 0a9d9cdded
10 changed files with 224 additions and 106 deletions
+2
View File
@@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Accept clients requesting `Content-Type: application/json` from / - @feynmanliang - Accept clients requesting `Content-Type: application/json` from / - @feynmanliang
- #493, Updating with empty JSON object makes zero updates @koulakis - #493, Updating with empty JSON object makes zero updates @koulakis
- Make HTTP headers and cookies available as GUCs #800 - @ruslantalpa - Make HTTP headers and cookies available as GUCs #800 - @ruslantalpa
- #701, Ability to quote values on IN filters - @steve-chavez
- #641, Allow IN filter to have no values - @steve-chavez
### Fixed ### Fixed
- #827, Avoid Warp reaper, extend socket timeout to 1 hour - @majorcode - #827, Avoid Warp reaper, extend socket timeout to 1 hour - @majorcode
-1
View File
@@ -266,7 +266,6 @@ app dbStructure conf apiRequest =
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
filterCol :: Schema -> TableName -> Column -> Bool filterCol :: Schema -> TableName -> Column -> Bool
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
filterCol _ _ _ = False
allPrKeys = dbPrimaryKeys dbStructure allPrKeys = dbPrimaryKeys dbStructure
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
jsonH = toHeader CTApplicationJSON jsonH = toHeader CTApplicationJSON
+2 -3
View File
@@ -22,9 +22,8 @@ import Data.Swagger
import PostgREST.ApiRequest (ContentType(..)) import PostgREST.ApiRequest (ContentType(..))
import PostgREST.Config (prettyVersion) import PostgREST.Config (prettyVersion)
import PostgREST.QueryBuilder (operators)
import PostgREST.Types (Table(..), Column(..), PgArg(..), import PostgREST.Types (Table(..), Column(..), PgArg(..),
Proxy(..), ProcDescription(..), toMime) Proxy(..), ProcDescription(..), toMime, Operator(..))
makeMimeList :: [ContentType] -> MimeList makeMimeList :: [ContentType] -> MimeList
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
@@ -73,7 +72,7 @@ makeOperatorPattern =
intercalate "|" intercalate "|"
[ concat ["^", x, y, "[.]"] | [ concat ["^", x, y, "[.]"] |
x <- ["not[.]", ""], x <- ["not[.]", ""],
y <- map fst operators ] y <- map show [Equals ..] ]
makeRowFilter :: Column -> Param makeRowFilter :: Column -> Param
makeRowFilter c = makeRowFilter c =
+22 -16
View File
@@ -2,13 +2,14 @@ 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.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.QueryBuilder (operators)
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 PostgREST.RangeQuery (NonnegRange,allRange)
import Text.Parsec.Error import Text.Parsec.Error
@@ -17,14 +18,12 @@ 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 (Path, Filter)
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> op <*> val) 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
opVal = parse pOpValueExp ("failed to parse filter (" ++ toS v ++ ")") $ toS v oper = parse pOperation ("failed to parse filter (" ++ toS v ++ ")") $ toS v
path = fst <$> treePath path = fst <$> treePath
fld = snd <$> treePath fld = snd <$> treePath
op = fst <$> opVal
val = snd <$> opVal
pRequestOrder :: (Text, Text) -> Either ApiRequestError (Path, [OrderTerm]) pRequestOrder :: (Text, Text) -> Either ApiRequestError (Path, [OrderTerm])
pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord' pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
@@ -120,22 +119,29 @@ pSelect = lexeme $
s <- pStar s <- pStar
return ((s, Nothing), Nothing, Nothing) return ((s, Nothing), Nothing, Nothing)
pOperator :: Parser Operator pOperation :: Parser Operation
pOperator = toS <$> (pOp <?> "operator (eq, gt, ...)") pOperation = try ( string "not" *> pDelimiter *> (Operation True <$> pExpr)) <|> Operation False <$> pExpr
where pOp = foldl (<|>) empty $ map (try . string . toS . fst) operators where
pExpr :: Parser (Operator, Operand)
pExpr =
((,) <$> (read <$> foldl1 (<|>) (try . string . show <$> notInOps)) <*> (pDelimiter *> pVText))
<|> try (string (show In) *> pDelimiter *> ((,) <$> pure In <*> pVTextL))
<|> try (string (show NotIn) *> pDelimiter *> ((,) <$> pure NotIn <*> pVTextL))
<?> "operator (eq, gt, ...)"
notInOps = [Equals .. Contained]
pValue :: Parser FValue pVText :: Parser Operand
pValue = VText <$> (toS <$> many anyChar) pVText = VText . toS <$> many anyChar
pVTextL :: Parser Operand
pVTextL = VTextL <$> pLValue `sepBy1` char ','
where
pLValue :: Parser Text
pLValue = toS <$> (try (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",") ) <|> many (noneOf ","))
pDelimiter :: Parser Char pDelimiter :: Parser Char
pDelimiter = char '.' <?> "delimiter (.)" pDelimiter = char '.' <?> "delimiter (.)"
pOperatiorWithNegation :: Parser Operator
pOperatiorWithNegation = try ( (<>) <$> ( toS <$> string "not." ) <*> pOperator) <|> pOperator
pOpValueExp :: Parser (Operator, FValue)
pOpValueExp = (,) <$> pOperatiorWithNegation <*> (pDelimiter *> pValue)
pOrder :: Parser [OrderTerm] pOrder :: Parser [OrderTerm]
pOrder = lexeme pOrderTerm `sepBy` char ',' pOrder = lexeme pOrderTerm `sepBy` char ','
+46 -78
View File
@@ -16,7 +16,6 @@ module PostgREST.QueryBuilder (
, createReadStatement , createReadStatement
, createWriteStatement , createWriteStatement
, getJoinConditions , getJoinConditions
, operators
, pgFmtIdent , pgFmtIdent
, pgFmtLit , pgFmtLit
, requestToQuery , requestToQuery
@@ -37,13 +36,12 @@ import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset,
import Data.Functor.Contravariant (contramap) import Data.Functor.Contravariant (contramap)
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.Maybe import Data.Maybe
import Data.Text (intercalate, unwords, replace, isInfixOf, toLower, split) import Data.Text (intercalate, unwords, replace, isInfixOf, toLower)
import qualified Data.Text as T (map, takeWhile, null) import qualified Data.Text as T (map, takeWhile, null)
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import Data.Tree (Tree(..)) import Data.Tree (Tree(..))
import qualified Data.Vector as V import qualified Data.Vector as V
import PostgREST.Types import PostgREST.Types
import qualified Data.Map as M
import Text.InterpolatedString.Perl6 (qc) import Text.InterpolatedString.Perl6 (qc)
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.Scientific ( FPFormat (..) import Data.Scientific ( FPFormat (..)
@@ -182,25 +180,6 @@ callProc qi params selectQuery countQuery _ countTotal isSingle paramsAsJson =
| isSingle = asJsonSingleF | isSingle = asJsonSingleF
| otherwise = asJsonF | otherwise = asJsonF
operators :: [(Text, SqlFragment)]
operators = [
("eq", "="),
("gte", ">="), -- has to be before gt (parsers)
("gt", ">"),
("lte", "<="), -- has to be before lt (parsers)
("lt", "<"),
("neq", "<>"),
("like", "like"),
("ilike", "ilike"),
("in", "in"),
("notin", "not in"),
("isnot", "is not"), -- has to be before is (parsers)
("is", "is"),
("@@", "@@"),
("@>", "@>"),
("<@", "<@")
]
pgFmtIdent :: SqlFragment -> SqlFragment pgFmtIdent :: SqlFragment -> SqlFragment
pgFmtIdent x = "\"" <> replace "\"" "\"\"" (trimNullChars $ toS x) <> "\"" pgFmtIdent x = "\"" <> replace "\"" "\"\"" (trimNullChars $ toS x) <> "\""
@@ -219,31 +198,27 @@ requestToCountQuery schema (DbRead (Node (Select _ _ conditions _ _, (mainTbl, _
unwords [ unwords [
"SELECT pg_catalog.count(*)", "SELECT pg_catalog.count(*)",
"FROM ", fromQi qi, "FROM ", fromQi qi,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi) localConditions )) `emptyOnNull` localConditions ("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi) localConditions )) `emptyOnNull` localConditions
] ]
where where
qi = if mainTbl == sourceCTEName qi = removeSourceCTESchema schema mainTbl
then QualifiedIdentifier "" mainTbl fn Filter{operation=Operation{expr=(_, VText _)}} = True
else QualifiedIdentifier schema mainTbl fn Filter{operation=Operation{expr=(_, VTextL _)}} = True
fn Filter{value=VText _} = True fn Filter{operation=Operation{expr=(_, VForeignKey _ _)}} = False
fn Filter{value=VForeignKey _ _} = False
localConditions = filter fn conditions localConditions = filter fn conditions
requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery
requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions ord range, (nodeName, maybeRelation, _)) forest)) = requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions ord range, (nodeName, maybeRelation, _)) forest)) =
query query
where where
-- TODO! the following helper functions are just to remove the "schema" part when the table is "source" which is the name
-- of our WITH query part
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation) mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
tblSchema tbl = if tbl == sourceCTEName then "" else schema qi = removeSourceCTESchema schema mainTbl
qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl toQi = removeSourceCTESchema schema
toQi t = QualifiedIdentifier (tblSchema t) t
query = unwords [ query = unwords [
"SELECT ", intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects), "SELECT ", intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM ", intercalate ", " (map (fromQi . toQi) tbls), "FROM ", intercalate ", " (map (fromQi . toQi) tbls),
unwords joins, unwords joins,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions, ("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi ) conditions )) `emptyOnNull` conditions,
orderF (fromMaybe [] ord), orderF (fromMaybe [] ord),
if isParent then "" else limitF range if isParent then "" else limitF range
] ]
@@ -272,11 +247,11 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls conditions
where where
node_name = fromMaybe name alias node_name = fromMaybe name alias
local_table_name = table <> "_" <> node_name local_table_name = table <> "_" <> node_name
replaceTableName localTableName (Filter a b (VForeignKey (QualifiedIdentifier "" _) c)) = Filter a b (VForeignKey (QualifiedIdentifier "" localTableName) c) replaceTableName localTableName (Filter a (Operation b (c, VForeignKey (QualifiedIdentifier "" _) d))) = Filter a (Operation b (c, VForeignKey (QualifiedIdentifier "" localTableName) d))
replaceTableName _ x = x replaceTableName _ x = x
sel = "row_to_json(" <> pgFmtIdent local_table_name <> ".*) AS " <> pgFmtIdent node_name sel = "row_to_json(" <> pgFmtIdent local_table_name <> ".*) AS " <> pgFmtIdent node_name
joi = " LEFT OUTER JOIN ( " <> subquery <> " ) AS " <> pgFmtIdent local_table_name <> joi = " LEFT OUTER JOIN ( " <> subquery <> " ) AS " <> pgFmtIdent local_table_name <>
" ON " <> intercalate " AND " ( map (pgFmtCondition qi . replaceTableName local_table_name) (getJoinConditions r) ) " ON " <> intercalate " AND " ( map (pgFmtFilter qi . replaceTableName local_table_name) (getJoinConditions r) )
where subquery = requestToQuery schema True (DbRead (Node n forst)) where subquery = requestToQuery schema True (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (j,sel:s) getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (j,sel:s)
where where
@@ -312,7 +287,7 @@ requestToQuery schema _ (DbMutate (Update mainTbl (PayloadJSON rows) conditions
unwords [ unwords [
"UPDATE ", fromQi qi, "UPDATE ", fromQi qi,
" SET " <> intercalate "," assignments <> " ", " SET " <> intercalate "," assignments <> " ",
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions, ("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi ) conditions )) `emptyOnNull` conditions,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnNull` returnings ("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnNull` returnings
] ]
Nothing -> undefined Nothing -> undefined
@@ -324,13 +299,16 @@ requestToQuery schema _ (DbMutate (Delete mainTbl conditions returnings)) =
qi = QualifiedIdentifier schema mainTbl qi = QualifiedIdentifier schema mainTbl
query = unwords [ query = unwords [
"DELETE FROM ", fromQi qi, "DELETE FROM ", fromQi qi,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions, ("WHERE " <> intercalate " AND " ( map (pgFmtFilter qi ) conditions )) `emptyOnNull` conditions,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnNull` returnings ("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnNull` returnings
] ]
sourceCTEName :: SqlFragment sourceCTEName :: SqlFragment
sourceCTEName = "pg_source" sourceCTEName = "pg_source"
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl
unquoted :: JSON.Value -> Text unquoted :: JSON.Value -> Text
unquoted (JSON.String t) = t unquoted (JSON.String t) = t
unquoted (JSON.Number n) = unquoted (JSON.Number n) =
@@ -401,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) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}})) toFilter tb ftb c fc = Filter (colName c, Nothing) (Operation False (Equals, 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
@@ -417,11 +395,6 @@ insertableValueWithType :: Text -> JSON.Value -> SqlFragment
insertableValueWithType t v = insertableValueWithType t v =
pgFmtLit (unquoted v) <> "::" <> t pgFmtLit (unquoted v) <> "::" <> t
whiteList :: Text -> SqlFragment
whiteList val = fromMaybe
(toS (pgFmtLit val) <> "::unknown ")
(find ((==) . toLower $ val) ["null","true","false"])
pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment
pgFmtColumn table "*" = fromQi table <> ".*" pgFmtColumn table "*" = fromQi table <> ".*"
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
@@ -433,45 +406,40 @@ pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment
pgFmtSelectItem table (f@(_, jp), Nothing, alias) = pgFmtField table f <> pgFmtAs jp alias pgFmtSelectItem table (f@(_, jp), Nothing, alias) = pgFmtField table f <> pgFmtAs jp alias
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
pgFmtCondition :: QualifiedIdentifier -> Filter -> SqlFragment pgFmtFilter :: QualifiedIdentifier -> Filter -> SqlFragment
pgFmtCondition table (Filter (col,jp) ops val) = pgFmtFilter table (Filter fld (Operation hasNot_ ex@(op, operand))) = notOp <> " " <> case operand of
notOp <> " " <> sqlCol <> " " <> pgFmtOperator opCode <> " " <> VForeignKey fQi (ForeignKey Column{colTable=Table{tableName=fTableName}, colName=fColName}) ->
if opCode `elem` ["is","isnot"] then whiteList (getInner val) else sqlValue pgFmtField fQi fld <> " " <> opToSqlFragment op <> " " <> pgFmtColumn (removeSourceCTESchema (qiSchema fQi) fTableName) fColName
_ -> pgFmtField table fld <> " " <> pgFmtExpr ex
where where
headPredicate:rest = split (=='.') ops notOp = if hasNot_ then "NOT" else ""
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (headDef "eq" rest) headPredicate
notOp = hasNot headPredicate ""
sqlCol = case val of
VText _ -> pgFmtColumn table col <> pgFmtJsonPath jp
VForeignKey qi _ -> pgFmtColumn qi col
sqlValue = valToStr val
getInner v = case v of
VText s -> s
_ -> ""
valToStr v = case v of
VText s -> pgFmtValue opCode s
VForeignKey (QualifiedIdentifier s _) (ForeignKey Column{colTable=Table{tableName=ft}, colName=fc}) -> pgFmtColumn qi fc
where qi = QualifiedIdentifier (if ft == sourceCTEName then "" else s) ft
_ -> ""
pgFmtValue :: Text -> Text -> SqlFragment pgFmtExpr :: (Operator, Operand) -> SqlFragment
pgFmtValue opCode val = pgFmtExpr ex =
case opCode of case ex of
"like" -> unknownLiteral $ T.map star val (Like, VText val) -> opToSqlFragment Like <> " " <> unknownLiteral (T.map star val)
"ilike" -> unknownLiteral $ T.map star val (ILike, VText val) -> opToSqlFragment ILike <> " " <> unknownLiteral (T.map star val)
"in" -> "(" <> intercalate ", " (map unknownLiteral $ split (==',') val) <> ") " (TSearch, VText val) -> opToSqlFragment TSearch <> " " <> "to_tsquery(" <> unknownLiteral val <> ") "
"notin" -> "(" <> intercalate ", " (map unknownLiteral $ split (==',') val) <> ") " (Is, VText val) -> opToSqlFragment Is <> " " <> whiteList val
"@@" -> "to_tsquery(" <> unknownLiteral val <> ") " (In, VTextL vals) -> exprForIn vals
_ -> unknownLiteral val (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
star c = if c == '*' then '%' else c star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit unknownLiteral = (<> "::unknown ") . pgFmtLit
whiteList :: Text -> SqlFragment
pgFmtOperator :: Text -> SqlFragment whiteList v = fromMaybe
pgFmtOperator opCode = fromMaybe "=" $ M.lookup opCode operatorsMap (toS (pgFmtLit v) <> "::unknown ")
where (find ((==) . toLower $ v) ["null","true","false"])
operatorsMap = M.fromList operators exprForIn :: [Text] -> SqlFragment
exprForIn vals =
let emptyValForIn = "= any('{}') " in
case T.null <$> headMay vals of
Just isNull -> if isNull && length vals == 1
then emptyValForIn
else opToSqlFragment In <> " " <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
Nothing -> emptyValForIn
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
+63 -7
View File
@@ -1,6 +1,7 @@
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 Data.HashMap.Strict as M
@@ -78,9 +79,7 @@ data Column =
, colDefault :: Maybe Text , colDefault :: Maybe Text
, colEnum :: [Text] , colEnum :: [Text]
, colFK :: Maybe ForeignKey , colFK :: Maybe ForeignKey
} } deriving (Show, Ord)
| Star { colTable :: Table }
deriving (Show, Ord)
type Synonym = (Column,Column) type Synonym = (Column,Column)
@@ -138,8 +137,66 @@ data Proxy = Proxy {
, proxyPath :: Text , proxyPath :: Text
} deriving (Show, Eq) } deriving (Show, Eq)
type Operator = Text data Operator = Equals | Gte | Gt | Lte | Lt | Neq | Like | ILike | Is | IsNot |
data FValue = VText Text | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq) TSearch | Contains | Contained | In | NotIn deriving (Eq, Enum)
instance Show Operator where
show op = case op of
Equals -> "eq"
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 -> "<@"
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 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)
@@ -152,7 +209,7 @@ data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter
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, operator::Operator, value::FValue} 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
@@ -194,7 +251,6 @@ instance Eq Table where
instance Eq Column where instance Eq Column where
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2 Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
_ == _ = False
-- | Convert from ContentType to a full HTTP Header -- | Convert from ContentType to a full HTTP Header
toHeader :: ContentType -> Header toHeader :: ContentType -> Header
+68 -1
View File
@@ -656,7 +656,7 @@ spec = do
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "fails if an operator is not given" $ it "fails if an operator is not given" $
get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"unexpected \"0\" expecting \"not.\" or operator (eq, gt, ...)","message":"\"failed to parse filter (0)\" (line 1, column 1)"} |] get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"unexpected \"0\" expecting \"not\" or operator (eq, gt, ...)","message":"\"failed to parse filter (0)\" (line 1, column 1)"} |]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
@@ -737,3 +737,70 @@ spec = do
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [] , matchHeaders = []
} }
describe "values with quotes in IN and NOTIN operators" $ do
it "succeeds when only quoted values are present" $ do
get "/w_or_wo_comma_names?name=in.\"Hebdon, John\"" `shouldRespondWith`
[json| [{"name":"Hebdon, John"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=in.\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\"" `shouldRespondWith`
[json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=notin.\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\"" `shouldRespondWith`
[json| [{"name":"David White"},{"name":"Larry Thompson"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=not.in.\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\"" `shouldRespondWith`
[json| [{"name":"David White"},{"name":"Larry Thompson"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "succeeds w/ and w/o quoted values" $ do
get "/w_or_wo_comma_names?name=in.David White,\"Hebdon, John\"" `shouldRespondWith`
[json| [{"name":"Hebdon, John"},{"name":"David White"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=not.in.\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\"" `shouldRespondWith`
[json| [{"name":"Williams, Mary"},{"name":"David White"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=notin.\"Hebdon, John\",David White,\"Williams, Mary\",Larry Thompson" `shouldRespondWith`
[json| [{"name":"Smith, Joseph"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "checks well formed quoted values" $ do
get "/w_or_wo_comma_names?name=in.\"\"Hebdon, John\"" `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=in.\"\"Hebdon, John\"\"Mary" `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
get "/w_or_wo_comma_names?name=in.Williams\"Hebdon, John\"" `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
describe "IN empty set" $ do
context "returns an empty result set when no value is present" $ do
it "works for integer" $
get "/items_with_different_col_types?int_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for text" $
get "/items_with_different_col_types?text_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for bool" $
get "/items_with_different_col_types?bool_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for bytea" $
get "/items_with_different_col_types?bin_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for char" $
get "/items_with_different_col_types?char_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for date" $
get "/items_with_different_col_types?date_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for real" $
get "/items_with_different_col_types?real_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "works for time" $
get "/items_with_different_col_types?time_data=in." `shouldRespondWith`
[json| [] |] { matchHeaders = [matchContentTypeJson] }
it "returns an empty result ignoring spaces" $
get "/items_with_different_col_types?int_data=in. " `shouldRespondWith` 400
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
+7
View File
@@ -287,6 +287,13 @@ TRUNCATE TABLE images CASCADE;
INSERT INTO images(name, img) VALUES ('A.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC', 'base64')); INSERT INTO images(name, img) VALUES ('A.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC', 'base64'));
INSERT INTO images(name, img) VALUES ('B.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII=', 'base64')); INSERT INTO images(name, img) VALUES ('B.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII=', 'base64'));
TRUNCATE TABLE w_or_wo_comma_names CASCADE;
INSERT INTO w_or_wo_comma_names VALUES ('Hebdon, John');
INSERT INTO w_or_wo_comma_names VALUES ('Williams, Mary');
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 ('Larry Thompson');
-- --
-- PostgreSQL database dump complete -- PostgreSQL database dump complete
-- --
+2
View File
@@ -51,6 +51,8 @@ GRANT ALL ON TABLE
, orders_view , orders_view
, images , images
, images_base64 , images_base64
, w_or_wo_comma_names
, items_with_different_col_types
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;
+12
View File
@@ -1161,6 +1161,18 @@ create function test.get_guc_value(name text) returns text as $$
select nullif(current_setting(name), '')::text; select nullif(current_setting(name), '')::text;
$$ language sql; $$ language sql;
create table w_or_wo_comma_names ( name text );
create table items_with_different_col_types (
int_data integer,
text_data text,
bool_data bool,
bin_data bytea,
char_data character varying,
date_data date,
real_data real,
time_data time
);
-- --
-- PostgreSQL database dump complete -- PostgreSQL database dump complete