diff --git a/CHANGELOG.md b/CHANGELOG.md index a8fccb2b808..bf6f5230fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #996, Fix embedded column conflicts table name - @grotsev - #974, Fix RPC error when function has single OUT param - @steve-chavez - #1021, Reduce join size in allColumns for faster program start - @nextstopsun +- #411, Remove the need for pk in &select for parent embed - @steve-chavez ## [0.4.3.0] - 2017-09-06 diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index ade4939aae7..601d7095774 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -31,7 +31,6 @@ import PostgREST.ApiRequest ( ApiRequest(..) import PostgREST.Error (apiRequestError) import PostgREST.Parsers import PostgREST.RangeQuery (NonnegRange, restrictRange) -import PostgREST.QueryBuilder (getJoinFilters, sourceCTEName) import PostgREST.Types import Protolude hiding (from, dropWhile, drop) @@ -194,7 +193,7 @@ addJoinFilters :: Schema -> ReadRequest -> Either ApiRequestError ReadRequest addJoinFilters schema (Node node@(query, nodeProps@(_, relation, _, _)) forest) = case relation of Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node - Just Relation{relType=Parent} -> Node node <$> updatedForest + Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just rel@Relation{relType=Many, relLTable=(Just linkTable)} -> let rq = augmentQuery rel in @@ -205,6 +204,21 @@ addJoinFilters schema (Node node@(query, nodeProps@(_, relation, _, _)) forest) augmentQuery rel = foldr addFilterToReadQuery query (getJoinFilters rel) addFilterToReadQuery flt rq@Select{where_=lf} = rq{where_=addFilterToLogicForest flt lf}::ReadQuery +getJoinFilters :: Relation -> [Filter] +getJoinFilters (Relation t cols ft fcs typ lt lc1 lc2) = + case typ of + Child -> zipWith (toFilter tN ftN) cols fcs + Parent -> zipWith (toFilter tN ftN) cols fcs + Many -> zipWith (toFilter tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2) + Root -> undefined --error "undefined getJoinFilters" + where + s = if typ == Parent then "" else tableSchema t + tN = tableName t + ftN = tableName ft + ltN = fromMaybe "" (tableName <$> lt) + toFilter :: Text -> Text -> Column -> Column -> Filter + toFilter tb ftb c fc = Filter (colName c, Nothing) (OpExpr False (Join (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}}))) + addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest) addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [ flip (foldr addFilter) <$> filters, diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 323c2bfef7b..ef931b5bdf3 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -14,12 +14,10 @@ module PostgREST.QueryBuilder ( callProc , createReadStatement , createWriteStatement - , getJoinFilters , pgFmtIdent , pgFmtLit , requestToQuery , requestToCountQuery - , sourceCTEName , unquoted , ResultsWithCount , pgFmtEnvVar @@ -255,15 +253,12 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest <> "FROM (" <> subquery <> ") " <> pgFmtIdent table <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) where subquery = requestToQuery schema False (DbRead (Node n forst)) - getQueryParts (Node n@(_, (name, Just r@Relation{relType=Parent,relTable=Table{tableName=table}}, alias, _)) forst) (j,s) = (joi:j,sel:s) + getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}}, alias, _)) forst) (j,s) = (joi:j,sel:s) where - node_name = fromMaybe name alias - local_table_name = table <> "_" <> node_name - replaceTableName localTableName (Filter a (OpExpr b (Join (QualifiedIdentifier "" _) c))) = Filter a (OpExpr b (Join (QualifiedIdentifier "" localTableName) c)) - replaceTableName _ x = x - sel = "row_to_json(" <> pgFmtIdent local_table_name <> ".*) AS " <> pgFmtIdent node_name - joi = " LEFT OUTER JOIN ( " <> subquery <> " ) AS " <> pgFmtIdent local_table_name <> - " ON " <> intercalate " AND " ( map (pgFmtFilter qi . replaceTableName local_table_name) (getJoinFilters r) ) + aliasOrName = fromMaybe name alias + localTableName = pgFmtIdent $ table <> "_" <> aliasOrName + sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName + joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE " 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) where @@ -315,9 +310,6 @@ requestToQuery schema _ (DbMutate (Delete mainTbl logicForest returnings)) = ("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings ] -sourceCTEName :: SqlFragment -sourceCTEName = "pg_source" - removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl @@ -378,21 +370,6 @@ fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n n = qiName t s = qiSchema t -getJoinFilters :: Relation -> [Filter] -getJoinFilters (Relation t cols ft fcs typ lt lc1 lc2) = - case typ of - Child -> zipWith (toFilter tN ftN) cols fcs - Parent -> zipWith (toFilter tN ftN) cols fcs - Many -> zipWith (toFilter tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2) - Root -> undefined --error "undefined getJoinFilters" - where - s = if typ == Parent then "" else tableSchema t - tN = tableName t - ftN = tableName ft - ltN = fromMaybe "" (tableName <$> lt) - toFilter :: Text -> Text -> Column -> Column -> Filter - toFilter tb ftb c fc = Filter (colName c, Nothing) (OpExpr False (Join (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}}))) - unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b unicodeStatement = H.statement . T.encodeUtf8 diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index d8f0a2b51e0..80f0921822c 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -277,3 +277,6 @@ data PgVersion = PgVersion { pgvNum :: Int32 , pgvName :: Text } deriving (Eq, Ord, Show) + +sourceCTEName :: SqlFragment +sourceCTEName = "pg_source" diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 5e6872a19fc..b292fe8e6a0 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -281,6 +281,39 @@ spec = do get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` [str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] + it "requesting parent without specifying primary key" $ do + get "/projects?select=name,client{name}" `shouldRespondWith` + [json|[ + {"name":"Windows 7","client":{"name": "Microsoft"}}, + {"name":"Windows 10","client":{"name": "Microsoft"}}, + {"name":"IOS","client":{"name": "Apple"}}, + {"name":"OSX","client":{"name": "Apple"}}, + {"name":"Orphan","client":null} + ]|] + { matchHeaders = [matchContentTypeJson] } + get "/articleStars?select=createdAt,article{owner},user{name}&limit=1" `shouldRespondWith` + [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|] + { matchHeaders = [matchContentTypeJson] } + + it "requesting parent and renaming primary key" $ + get "/projects?select=name,client{clientId:id,name}" `shouldRespondWith` + [json|[ + {"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}}, + {"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}}, + {"name":"IOS","client":{"name": "Apple", "clientId": 2}}, + {"name":"OSX","client":{"name": "Apple", "clientId": 2}}, + {"name":"Orphan","client":null} + ]|] + { matchHeaders = [matchContentTypeJson] } + + it "requesting parent and specifying/renaming one key of the composite primary key" $ do + get "/comments?select=*,users_tasks{userId:user_id}" `shouldRespondWith` + [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"userId": 2}}]|] + { matchHeaders = [matchContentTypeJson] } + get "/comments?select=*,users_tasks{taskId:task_id}" `shouldRespondWith` + [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|] + { matchHeaders = [matchContentTypeJson] } + it "embed data with two fk pointing to the same table" $ get "/orders?id=eq.1&select=id, name, billing_address_id{id}, shipping_address_id{id}" `shouldRespondWith` [str|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]