From d4caa0f5f56fb1690aaae36ba7778c4101ed56ac Mon Sep 17 00:00:00 2001 From: Taimoor Zaeem Date: Sat, 12 Jul 2025 14:12:38 +0500 Subject: [PATCH] fix: empty spread embeddings return unexpected SQL error Fixes the SQL error from postgres when an empty spread embeddings like `...table()` is requested. --- CHANGELOG.md | 1 + src/PostgREST/Query/QueryBuilder.hs | 12 +++---- test/spec/Feature/Query/SpreadQueriesSpec.hs | 34 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e44244f68..78d37594d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix OpenAPI broken docs link by @taimoorzaeem in #4080 - Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174 +- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887 ### Changed diff --git a/src/PostgREST/Query/QueryBuilder.hs b/src/PostgREST/Query/QueryBuilder.hs index 7a2fedfbd..d69a3709b 100644 --- a/src/PostgREST/Query/QueryBuilder.hs +++ b/src/PostgREST/Query/QueryBuilder.hs @@ -69,21 +69,21 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor getJoinSelects :: ReadPlanTree -> [SQL.Snippet] getJoinSelects (Node ReadPlan{relSelect} _) = - mapMaybe relSelectToSnippet relSelect + join $ map relSelectToSnippet relSelect where - relSelectToSnippet :: RelSelectField -> Maybe SQL.Snippet + relSelectToSnippet :: RelSelectField -> [SQL.Snippet] relSelectToSnippet fld = let aggAlias = pgFmtIdent $ rsAggAlias fld in case fld of JsonEmbed{rsEmptyEmbed = True} -> - Nothing + [] JsonEmbed{rsSelName, rsEmbedMode = JsonObject} -> - Just $ "row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName + ["row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName] JsonEmbed{rsSelName, rsEmbedMode = JsonArray} -> - Just $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName + ["COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName] Spread{rsSpreadSel, rsAggAlias} -> - Just $ intercalateSnippet ", " (pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel) + pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel getJoins :: ReadPlanTree -> [SQL.Snippet] getJoins (Node _ []) = [] diff --git a/test/spec/Feature/Query/SpreadQueriesSpec.hs b/test/spec/Feature/Query/SpreadQueriesSpec.hs index e584ae1b4..cfaa7f4c8 100644 --- a/test/spec/Feature/Query/SpreadQueriesSpec.hs +++ b/test/spec/Feature/Query/SpreadQueriesSpec.hs @@ -600,3 +600,37 @@ spec = { matchStatus = 200 , matchHeaders = [matchContentTypeJson] } + + context "empty spreads embeds" $ + it "should work return the same as empty embeddings" $ do + get "/actors?select=*,...films()" + `shouldRespondWith` + [json| [{"id":1,"name":"john"}, {"id":2,"name":"mary"}] |] + { matchStatus = 200 + , matchHeaders = [matchContentTypeJson] + } + + get "/grandchild_entities?select=name,...child_entities(parent_name:name,...entities())" + `shouldRespondWith` + [json| + [{"name":"grandchild entity 1","parent_name":"child entity 1"}, + {"name":"grandchild entity 2","parent_name":"child entity 1"}, + {"name":"grandchild entity 3","parent_name":"child entity 2"}, + {"name":"(grandchild,entity,4)","parent_name":"child entity 2"}, + {"name":"(grandchild,entity,5)","parent_name":"child entity 2"}] + |] + { matchStatus = 200 + , matchHeaders = [matchContentTypeJson] + } + + get "/factories?select=factory:name,...processes()" + `shouldRespondWith` + [json| + [{"factory":"Factory A"}, + {"factory":"Factory B"}, + {"factory":"Factory C"}, + {"factory":"Factory D"}] + |] + { matchStatus = 200 + , matchHeaders = [matchContentTypeJson] + }