diff --git a/src/PostgREST/Query/QueryBuilder.hs b/src/PostgREST/Query/QueryBuilder.hs index c9c597a5f..a42c11907 100644 --- a/src/PostgREST/Query/QueryBuilder.hs +++ b/src/PostgREST/Query/QueryBuilder.hs @@ -50,7 +50,7 @@ readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicFores (joins, selects) = foldr getJoinsSelects ([],[]) forest getJoinsSelects :: ReadRequest -> ([H.Snippet], [H.Snippet]) -> ([H.Snippet], [H.Snippet]) -getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=Table{tableName=table}}, alias, _, Just joinType, _)) _) (j,s) = +getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=Table{tableName=table}}, alias, _, Just joinType, _)) _) (joins,selects) = let subquery = readRequestToQuery rr in case card of M2O _ -> @@ -59,21 +59,25 @@ getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTab sel = H.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName) joi = (if joinType == JTInner then " INNER" else " LEFT") <> " JOIN LATERAL( " <> subquery <> " ) AS " <> H.sql localTableName <> " ON TRUE " in - (joi:j,sel:s) + (joi:joins,sel:selects) _ -> case joinType of JTInner -> let aliasOrName = fromMaybe name alias - localTableName = pgFmtIdent $ table <> "_" <> aliasOrName - sel = H.sql $ localTableName <> "._ AS " <> pgFmtIdent aliasOrName - joi = "INNER JOIN LATERAL( SELECT json_agg(_) AS _ FROM (" <> subquery <> " ) _) AS " <> - H.sql localTableName <> " ON " <> H.sql localTableName <> "IS NOT NULL" in - (joi:j,sel:s) + locTblName = table <> "_" <> aliasOrName + localTableName = pgFmtIdent locTblName + internalTableName = pgFmtIdent $ "_" <> locTblName + sel = H.sql $ localTableName <> "." <> internalTableName <> " AS " <> pgFmtIdent aliasOrName + joi = "INNER JOIN LATERAL(" <> + "SELECT json_agg(" <> H.sql internalTableName <> ") AS " <> H.sql internalTableName <> + "FROM (" <> subquery <> " ) AS " <> H.sql internalTableName <> + ") AS " <> H.sql localTableName <> " ON " <> H.sql localTableName <> "IS NOT NULL" in + (joi:joins,sel:selects) JTLeft -> let sel = "COALESCE ((" <> "SELECT json_agg(" <> H.sql (pgFmtIdent table) <> ".*) " <> "FROM (" <> subquery <> ") " <> H.sql (pgFmtIdent table) <> " " <> "), '[]') AS " <> H.sql (pgFmtIdent (fromMaybe name alias)) in - (j,sel:s) + (joins,sel:selects) getJoinsSelects _ _ = ([], []) mutateRequestToQuery :: MutateRequest -> H.Snippet diff --git a/test/Feature/EmbedInnerJoinSpec.hs b/test/Feature/EmbedInnerJoinSpec.hs index ad0bd5f8d..4f5a94735 100644 --- a/test/Feature/EmbedInnerJoinSpec.hs +++ b/test/Feature/EmbedInnerJoinSpec.hs @@ -220,6 +220,26 @@ spec = [json| [{"id":3,"client":{"id":2}}, {"id":4,"client":{"id":2}}] |] { matchHeaders = [matchContentTypeJson] } + it "works with many one-to-many relationships" $ do + -- https://github.com/PostgREST/postgrest/issues/1977 + get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" `shouldRespondWith` + [json|[ + {"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}, {"name":"Wilma Wellers"}],"clientinfo":[{"other":"123 Main St"}]}, + {"id":2,"name":"Target", "contact":[{"name":"Tabby Targo"}],"clientinfo":[{"other":"456 South 3rd St"}]}, + {"id":3,"name":"Big Lots","contact":[{"name":"Bobby Bots"}, {"name":"Bonnie Bits"}, {"name":"Billy Boats"}],"clientinfo":[{"other":"789 Palm Tree Ln"}]} + ]|] + { matchHeaders = [matchContentTypeJson] } + get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&contact.name=eq.Wally%20Walton" `shouldRespondWith` + [json|[ + {"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}],"clientinfo":[{"other":"123 Main St"}]} + ]|] + { matchHeaders = [matchContentTypeJson] } + get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&clientinfo.other=eq.456%20South%203rd%20St" `shouldRespondWith` + [json|[ + {"id":2,"name":"Target","clientinfo":[{"other":"456 South 3rd St"}],"contact":[{"name":"Tabby Targo"}]} + ]|] + { matchHeaders = [matchContentTypeJson] } + notDefaultConfig :: SpecWith ((), Application) notDefaultConfig = describe "Embedding with a default inner join(db-embed-default-join = 'inner')" $ do diff --git a/test/fixtures/data.sql b/test/fixtures/data.sql index 142cc01ad..6573b11f8 100644 --- a/test/fixtures/data.sql +++ b/test/fixtures/data.sql @@ -706,4 +706,13 @@ TRUNCATE TABLE test.trade_unions CASCADE; INSERT INTO test.trade_unions (id, name) VALUES (1,'union-1'), (2,'union-2'), (3, 'union-3'), (4, 'union-4'); TRUNCATE TABLE test.suppliers_trade_unions CASCADE; -INSERT INTO test.suppliers_trade_unions (supplier_id, trade_union_id) VALUES (1,1), (1,2), (2,3), (2,4); \ No newline at end of file +INSERT INTO test.suppliers_trade_unions (supplier_id, trade_union_id) VALUES (1,1), (1,2), (2,3), (2,4); + +TRUNCATE TABLE test.client CASCADE; +INSERT INTO test.client (id,name) values (1,'Walmart'),(2,'Target'),(3,'Big Lots'); + +TRUNCATE TABLE test.contact CASCADE; +INSERT INTO test.contact (id,name, clientid) values (1,'Wally Walton',1),(2,'Wilma Wellers',1),(3,'Tabby Targo',2),(4,'Bobby Bots',3),(5,'Bonnie Bits',3),(6,'Billy Boats',3) returning *; + +TRUNCATE TABLE test.clientinfo CASCADE; +INSERT INTO test.clientinfo (id,clientid, other) values (1,1,'123 Main St'),(2,2,'456 South 3rd St'),(3,3,'789 Palm Tree Ln'); diff --git a/test/fixtures/privileges.sql b/test/fixtures/privileges.sql index 828433888..1a73cd451 100644 --- a/test/fixtures/privileges.sql +++ b/test/fixtures/privileges.sql @@ -156,6 +156,9 @@ GRANT ALL ON TABLE , products_suppliers , trade_unions , suppliers_trade_unions + , client + , clientinfo + , contact TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 5d36ace7c..5fc548903 100644 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -2347,4 +2347,22 @@ create table suppliers_trade_unions( supplier_id int references suppliers(id), trade_union_id int references trade_unions(id), primary key (supplier_id, trade_union_id) -); \ No newline at end of file +); + + +CREATE TABLE client ( + id int primary key +, name text +); + +CREATE TABLE contact ( + id int primary key +, name text +, clientid int references client(id) +); + +CREATE TABLE clientinfo ( + id serial primary key +, clientid int unique references client(id) +, other text +);