feat: Allow embedding without selecting any column (#2574)

This commit is contained in:
Steve Chavez
2022-11-25 18:37:57 -05:00
committed by GitHub
parent f7009635d6
commit 5e9dba5292
5 changed files with 83 additions and 34 deletions
+1
View File
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
+ Allows including the join table columns when resource embedding + Allows including the join table columns when resource embedding
+ Allows disambiguating a recursive m2m embed + Allows disambiguating a recursive m2m embed
+ Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction + Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction
- #2340, Allow embedding without selecting any column - @steve-chavez
### Fixed ### Fixed
+21 -25
View File
@@ -36,8 +36,8 @@ import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
eof, errorPos, letter, eof, errorPos, letter,
lookAhead, many1, noneOf, lookAhead, many1, noneOf,
notFollowedBy, oneOf, notFollowedBy, oneOf,
optionMaybe, sepBy1, string, optionMaybe, sepBy, sepBy1,
try, (<?>)) string, try, (<?>))
import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.RangeQuery (NonnegRange, allRange,
rangeGeq, rangeLimit, rangeGeq, rangeLimit,
@@ -323,26 +323,25 @@ pTreePath = do
-- >>> P.parse pFieldForest "" "*,..client(*),other(*)" -- >>> P.parse pFieldForest "" "*,..client(*),other(*)"
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}] -- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
-- --
-- >>> P.parse pFieldForest "" ""
-- Right []
--
-- >>> P.parse pFieldForest "" "id,clients(name[])" -- >>> P.parse pFieldForest "" "id,clients(name[])"
-- Left (line 1, column 16): -- Left (line 1, column 16):
-- unexpected '[' -- unexpected '['
-- expecting letter, digit, "-", "!", "(", "->>", "->", "::", ")", "," or end of input -- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
-- --
-- >>> P.parse pFieldForest "" "data->>-78xy" -- >>> P.parse pFieldForest "" "data->>-78xy"
-- Left (line 1, column 11): -- Left (line 1, column 11):
-- unexpected 'x' -- unexpected 'x'
-- expecting digit, "->", "::", ".", "," or end of input -- expecting digit, "->", "::", ".", "," or end of input
pFieldForest :: Parser [Tree SelectItem] pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',') pFieldForest = pFieldTree `sepBy` lexeme (char ',')
where where
pFieldTree :: Parser (Tree SelectItem) pFieldTree = Node <$> try pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
pFieldTree = try (Node <$> pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest) <|> Node <$> try pRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
try (Node <$> pRelationSelect <*> between (char '(') (char ')') pFieldForest) <|>
Node <$> pFieldSelect <*> pure [] Node <$> pFieldSelect <*> pure []
pStar :: Parser Text
pStar = string "*" $> "*"
-- | -- |
-- Parse field names -- Parse field names
-- --
@@ -480,13 +479,12 @@ aliasSeparator = char ':' >> notFollowedBy (char ':')
-- Left (line 1, column 6): -- Left (line 1, column 6):
-- unexpected '>' -- unexpected '>'
pRelationSelect :: Parser SelectItem pRelationSelect :: Parser SelectItem
pRelationSelect = lexeme $ try ( do pRelationSelect = lexeme $ do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
name <- pFieldName name <- pFieldName
(hint, jType) <- pEmbedParams (hint, jType) <- pEmbedParams
try (void $ lookAhead (string "(")) try (void $ lookAhead (string "("))
return $ SelectRelation name alias hint jType return $ SelectRelation name alias hint jType
)
-- | -- |
-- Parse regular fields in select -- Parse regular fields in select
@@ -524,23 +522,22 @@ pRelationSelect = lexeme $ try ( do
-- unexpected end of input -- unexpected end of input
-- expecting letter or digit -- expecting letter or digit
pFieldSelect :: Parser SelectItem pFieldSelect :: Parser SelectItem
pFieldSelect = lexeme $ pFieldSelect = lexeme $ try (do
try (
do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
cast' <- optionMaybe (string "::" *> pIdentifier)
pEnd
return $ SelectField fld (toS <$> cast') alias
)
<|> do
s <- pStar s <- pStar
pEnd pEnd
return $ SelectField (s, []) Nothing Nothing return $ SelectField (s, []) Nothing Nothing)
<|> do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
cast' <- optionMaybe (string "::" *> pIdentifier)
pEnd
return $ SelectField fld (toS <$> cast') alias
where where
pEnd = try (void $ lookAhead (string ")")) <|> pEnd = try (void $ lookAhead (string ")")) <|>
try (void $ lookAhead (string ",")) <|> try (void $ lookAhead (string ",")) <|>
try eof try eof
pStar = string "*" $> "*"
-- | -- |
-- Parse spread relations in select -- Parse spread relations in select
@@ -565,12 +562,11 @@ pFieldSelect = lexeme $
-- Left (line 1, column 8): -- Left (line 1, column 8):
-- unexpected '>' -- unexpected '>'
pSpreadRelationSelect :: Parser SelectItem pSpreadRelationSelect :: Parser SelectItem
pSpreadRelationSelect = lexeme $ try ( do pSpreadRelationSelect = lexeme $ do
name <- string ".." >> pFieldName name <- string ".." >> pFieldName
(hint, jType) <- pEmbedParams (hint, jType) <- pEmbedParams
try (void $ lookAhead (string "(")) try (void $ lookAhead (string "("))
return $ SpreadRelation name hint jType return $ SpreadRelation name hint jType
)
pEmbedParams :: Parser (Maybe Hint, Maybe JoinType) pEmbedParams :: Parser (Maybe Hint, Maybe JoinType)
pEmbedParams = do pEmbedParams = do
+3 -3
View File
@@ -76,7 +76,7 @@ readQuery req conf@AppConfig{..} apiReq@ApiRequest{..} = do
resultSet <- resultSet <-
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareRead Statements.prepareRead
(QueryBuilder.readPlanToQuery req) (QueryBuilder.readPlanToQuery True req)
(if iPreferCount == Just EstimatedCount then (if iPreferCount == Just EstimatedCount then
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows) QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
@@ -163,7 +163,7 @@ invokeQuery proc CallReadPlan{crReadPlan, crCallPlan} apiReq@ApiRequest{..} conf
(Proc.procReturnsScalar proc) (Proc.procReturnsScalar proc)
(Proc.procReturnsSingle proc) (Proc.procReturnsSingle proc)
(QueryBuilder.callPlanToQuery crCallPlan) (QueryBuilder.callPlanToQuery crCallPlan)
(QueryBuilder.readPlanToQuery crReadPlan) (QueryBuilder.readPlanToQuery True crReadPlan)
(QueryBuilder.readPlanToCountQuery crReadPlan) (QueryBuilder.readPlanToCountQuery crReadPlan)
(shouldCount iPreferCount) (shouldCount iPreferCount)
iAcceptMediaType iAcceptMediaType
@@ -217,7 +217,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan} apiReq conf =
in in
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareWrite Statements.prepareWrite
(QueryBuilder.readPlanToQuery mrReadPlan) (QueryBuilder.readPlanToQuery True mrReadPlan)
(QueryBuilder.mutatePlanToQuery mrMutatePlan) (QueryBuilder.mutatePlanToQuery mrMutatePlan)
isInsert isInsert
(iAcceptMediaType apiReq) (iAcceptMediaType apiReq)
+7 -6
View File
@@ -39,10 +39,10 @@ import PostgREST.RangeQuery (allRange)
import Protolude import Protolude
readPlanToQuery :: ReadPlanTree -> SQL.Snippet readPlanToQuery :: Bool -> ReadPlanTree -> SQL.Snippet
readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds} forest) = readPlanToQuery isRoot (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds} forest) =
"SELECT " <> "SELECT " <>
intercalateSnippet ", " ((pgFmtSelectItem qi <$> select) ++ selects) <> " " <> intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if isRoot && null select && null forest then defRootSelect else select)) ++ selects) <> " " <>
fromFrag <> " " <> fromFrag <> " " <>
intercalateSnippet " " joins <> " " <> intercalateSnippet " " joins <> " " <>
(if null logicForest && null relJoinConds (if null logicForest && null relJoinConds
@@ -53,13 +53,14 @@ readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,o
where where
fromFrag = fromF relToParent mainQi fromAlias fromFrag = fromF relToParent mainQi fromAlias
qi = getQualifiedIdentifier relToParent mainQi fromAlias qi = getQualifiedIdentifier relToParent mainQi fromAlias
defRootSelect = [(("*", []), Nothing, Nothing)] -- gets all columns in case an empty select, e.g. `/tbl?select=`, is done.
(selects, joins) = foldr getSelectsJoins ([],[]) forest (selects, joins) = foldr getSelectsJoins ([],[]) forest
getSelectsJoins :: ReadPlanTree -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet]) getSelectsJoins :: ReadPlanTree -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet])
getSelectsJoins (Node ReadPlan{relToParent=Nothing} _) _ = ([], []) getSelectsJoins (Node ReadPlan{relToParent=Nothing} _) _ = ([], [])
getSelectsJoins rr@(Node ReadPlan{relName, relToParent=Just rel, relAggAlias, relAlias, relJoinType, relIsSpread} _) (selects,joins) = getSelectsJoins rr@(Node ReadPlan{select, relName, relToParent=Just rel, relAggAlias, relAlias, relJoinType, relIsSpread} forest) (selects,joins) =
let let
subquery = readPlanToQuery rr subquery = readPlanToQuery False rr
aliasOrName = pgFmtIdent $ fromMaybe relName relAlias aliasOrName = pgFmtIdent $ fromMaybe relName relAlias
aggAlias = pgFmtIdent relAggAlias aggAlias = pgFmtIdent relAggAlias
correlatedSubquery sub al cond = correlatedSubquery sub al cond =
@@ -77,7 +78,7 @@ getSelectsJoins rr@(Node ReadPlan{relName, relToParent=Just rel, relAggAlias, re
"FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias "FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias
) aggAlias $ if relJoinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE") ) aggAlias $ if relJoinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE")
in in
(sel:selects, joi:joins) (if null select && null forest then selects else sel:selects, joi:joins)
mutatePlanToQuery :: MutatePlan -> SQL.Snippet mutatePlanToQuery :: MutatePlan -> SQL.Snippet
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _) = mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _) =
+51
View File
@@ -1224,3 +1224,54 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
let respHeaders = simpleHeaders r let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` noProfileHeader respHeaders `shouldSatisfy` noProfileHeader
context "empty embed" $ do
it "works on a many-to-one relationship" $ do
get "/projects?select=id,name,clients()" `shouldRespondWith`
[json| [
{"id":1,"name":"Windows 7"},
{"id":2,"name":"Windows 10"},
{"id":3,"name":"IOS"},
{"id":4,"name":"OSX"},
{"id":5,"name":"Orphan"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/projects?select=id,name,clients!inner()&clients.id=eq.2" `shouldRespondWith`
[json|[
{"id":3,"name":"IOS"},
{"id":4,"name":"OSX"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on a one-to-many relationship" $ do
get "/clients?select=id,name,projects()" `shouldRespondWith`
[json| [{"id":1,"name":"Microsoft"}, {"id":2,"name":"Apple"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/clients?select=id,name,projects!inner()&projects.name=eq.IOS" `shouldRespondWith`
[json|[{"id":2,"name":"Apple"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on a many-to-many relationship" $ do
get "/users?select=*,tasks!inner()" `shouldRespondWith`
[json| [{"id":1,"name":"Angela Martin"}, {"id":2,"name":"Michael Scott"}, {"id":3,"name":"Dwight Schrute"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/users?select=*,tasks!inner()&tasks.id=eq.3" `shouldRespondWith`
[json|[{"id":1,"name":"Angela Martin"}]|]
{ matchHeaders = [matchContentTypeJson] }
context "empty root select" $
it "gives all columns" $ do
get "/projects?select=" `shouldRespondWith`
[json|[
{"id":1,"name":"Windows 7","client_id":1},
{"id":2,"name":"Windows 10","client_id":1},
{"id":3,"name":"IOS","client_id":2},
{"id":4,"name":"OSX","client_id":2},
{"id":5,"name":"Orphan","client_id":null}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/getallprojects?select=" `shouldRespondWith`
[json|[
{"id":1,"name":"Windows 7","client_id":1},
{"id":2,"name":"Windows 10","client_id":1},
{"id":3,"name":"IOS","client_id":2},
{"id":4,"name":"OSX","client_id":2},
{"id":5,"name":"Orphan","client_id":null}]|]
{ matchHeaders = [matchContentTypeJson] }