Allow finishing a json path with single arrow ->
Now a json can be obtained without resorting to casting - Previously: `/json_arr?select=data->>2::json` - Now: `/json_arr?select=data->2`
This commit is contained in:
committed by
Steve Chávez
parent
30dfadec7b
commit
d34afe861a
@@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Changed
|
||||
|
||||
- #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez
|
||||
- #1128, Allow finishing a json path with a single arrow `->`. Now a json can be obtained without resorting to casting, Previously: `/json_arr?select=data->>2::json`, now: `/json_arr?select=data->2` - @steve-chavez
|
||||
|
||||
## [0.5.0.0] - 2018-05-14
|
||||
|
||||
|
||||
@@ -81,15 +81,22 @@ pFieldName = do
|
||||
dash = isDash *> pure '-'
|
||||
|
||||
pJsonPath :: Parser JsonPath
|
||||
pJsonPath = (<>) <$> many pJsonPathOp <*> ( (:[]) <$> (string "->>" *> (try pJIdx <|> pJKey)) )
|
||||
pJsonPath = many pJsonOperation
|
||||
where
|
||||
pJsonPathOp :: Parser JsonPathOp
|
||||
pJsonPathOp = try (string "->" *> pJIdx) <|> try (string "->" *> pJKey)
|
||||
pJKey = JKey . toS <$> pFieldName
|
||||
pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
|
||||
pEnd = try (void $ lookAhead (string "->")) <|>
|
||||
try (void $ lookAhead (string "::")) <|>
|
||||
try eof
|
||||
pJsonOperation :: Parser JsonOperation
|
||||
pJsonOperation = pJsonArrow <*> pJsonOperand
|
||||
|
||||
pJsonArrow =
|
||||
try (string "->>" $> J2Arrow) <|>
|
||||
try (string "->" $> JArrow)
|
||||
|
||||
pJsonOperand =
|
||||
let pJKey = JKey . toS <$> pFieldName
|
||||
pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
|
||||
pEnd = try (void $ lookAhead (string "->")) <|>
|
||||
try (void $ lookAhead (string "::")) <|>
|
||||
try eof in
|
||||
try pJIdx <|> try pJKey
|
||||
|
||||
pField :: Parser Field
|
||||
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
|
||||
|
||||
@@ -450,22 +450,22 @@ pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
||||
|
||||
pgFmtJsonPath :: JsonPath -> SqlFragment
|
||||
pgFmtJsonPath = \case
|
||||
[] -> ""
|
||||
[x] -> "->>" <> pgFmtJsonPathOp x
|
||||
(x:xs) -> "->" <> pgFmtJsonPathOp x <> pgFmtJsonPath xs
|
||||
[] -> ""
|
||||
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||
(J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||
where
|
||||
pgFmtJsonPathOp (JKey k) = pgFmtLit k
|
||||
pgFmtJsonPathOp (JIdx i) = pgFmtLit i <> "::int"
|
||||
pgFmtJsonOperand (JKey k) = pgFmtLit k
|
||||
pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int"
|
||||
|
||||
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
|
||||
pgFmtAs _ [] Nothing = ""
|
||||
pgFmtAs fName jp Nothing = case lastMay jp of
|
||||
pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
|
||||
Just (JKey key) -> " AS " <> pgFmtIdent key
|
||||
Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey)
|
||||
-- We get the lastKey because on:
|
||||
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
|
||||
-- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ]
|
||||
where lastKey = jpOp <$> find (\case JKey{} -> True; _ -> False) (reverse jp)
|
||||
where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jp)
|
||||
Nothing -> ""
|
||||
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||
|
||||
|
||||
@@ -238,12 +238,15 @@ instance Show LogicOperator where
|
||||
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq)
|
||||
|
||||
type FieldName = Text
|
||||
type JsonPath = [JsonPathOp]
|
||||
{-|
|
||||
Json path operands as specified in https://www.postgresql.org/docs/9.5/static/functions-json.html
|
||||
the array index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
|
||||
Json path operations as specified in https://www.postgresql.org/docs/9.4/static/functions-json.html
|
||||
-}
|
||||
data JsonPathOp = JKey{jpOp :: Text} | JIdx{jpOp :: Text} deriving (Show, Eq)
|
||||
type JsonPath = [JsonOperation]
|
||||
-- | Represents the single arrow `->` or double arrow `->>` operators
|
||||
data JsonOperation = JArrow{jOp :: JsonOperand} | J2Arrow{jOp :: JsonOperand} deriving (Show, Eq)
|
||||
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
|
||||
data JsonOperand = JKey{jVal :: Text} | JIdx{jVal :: Text} deriving (Show, Eq)
|
||||
|
||||
type Field = (FieldName, JsonPath)
|
||||
type Alias = Text
|
||||
type Cast = Text
|
||||
|
||||
@@ -48,6 +48,20 @@ spec = describe "json and jsonb operators" $ do
|
||||
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
-- TODO the status code for the error is 404, this is because 42883 represents undefined function
|
||||
-- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate
|
||||
it "fails when a double arrow ->> is followed with a single arrow ->" $ do
|
||||
get "/json_arr?select=data->>c->1"
|
||||
`shouldRespondWith` [json|
|
||||
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
|
||||
"details":null,"code":"42883","message":"operator does not exist: text -> integer"} |]
|
||||
{ matchStatus = 404 , matchHeaders = [] }
|
||||
get "/json_arr?select=data->>c->b"
|
||||
`shouldRespondWith` [json|
|
||||
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
|
||||
"details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |]
|
||||
{ matchStatus = 404 , matchHeaders = [] }
|
||||
|
||||
context "with array index" $ do
|
||||
it "can get array of ints and alias/cast it" $ do
|
||||
get "/json_arr?select=data->>0::int&id=in.(1,2)" `shouldRespondWith`
|
||||
@@ -86,6 +100,23 @@ spec = describe "json and jsonb operators" $ do
|
||||
[json| [{"xy-6":3}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "finishing json path with single arrow ->" $ do
|
||||
it "works when finishing with a key" $ do
|
||||
get "/json_arr?select=data->c&id=in.(7,8)" `shouldRespondWith`
|
||||
[json| [{"c":[1,2,3]}, {"c":[{"d": [4,5,6,7,8]}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when finishing with an index" $ do
|
||||
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->c->0->d&id=eq.8" `shouldRespondWith`
|
||||
[json| [{"d":[4,5,6,7,8]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "filtering response" $ do
|
||||
it "can filter by properties inside json column" $ do
|
||||
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
||||
|
||||
Reference in New Issue
Block a user