From c4e70da05d850d74e5b65e5424cbcff611c45874 Mon Sep 17 00:00:00 2001 From: apoorva-01 Date: Thu, 20 Aug 2026 02:22:44 +0530 Subject: [PATCH] Reject composite fields selected without a subfield selection They were silently resolving to an empty object instead of erroring, which the GraphQL spec disallows. --- docs/changelog.md | 2 + src/parser_util.rs | 6 ++ .../resolve_error_empty_selection_set.out | 81 +++++++++++++++++++ .../sql/resolve_error_empty_selection_set.sql | 60 ++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 test/expected/resolve_error_empty_selection_set.out create mode 100644 test/sql/resolve_error_empty_selection_set.sql diff --git a/docs/changelog.md b/docs/changelog.md index 8258d74a..e5f7ae59 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -136,3 +136,5 @@ - bugfix: Remove double NON_NULL wrapping on byPk argument types ## master + +- bugfix: Reject queries that select a composite field without a subfield selection diff --git a/src/parser_util.rs b/src/parser_util.rs index 20722b12..2ef6fb9d 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -26,6 +26,12 @@ where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, { + if selection_set.items.is_empty() { + return Err(GraphQLError::validation(format!( + "Selection set must not be empty for type '{type_name}'" + ))); + } + let mut selections: Vec> = vec![]; for selection in &selection_set.items { diff --git a/test/expected/resolve_error_empty_selection_set.out b/test/expected/resolve_error_empty_selection_set.out new file mode 100644 index 00000000..000ac0ea --- /dev/null +++ b/test/expected/resolve_error_empty_selection_set.out @@ -0,0 +1,81 @@ +begin; + create table account( + id serial primary key, + parent_id int references account(id) + ); + -- A composite field selected with no subfields must be rejected, not + -- silently resolved. See https://github.com/supabase/pg_graphql/issues/412 + -- top level connection + select graphql.resolve($$ + { + accountCollection + } + $$); + resolve +--------------------------------------------------------------------------------------------------------- + {"data": null, "errors": [{"message": "Selection set must not be empty for type 'AccountConnection'"}]} +(1 row) + + -- nested edges + select graphql.resolve($$ + { + accountCollection { + edges + } + } + $$); + resolve +--------------------------------------------------------------------------------------------------- + {"data": null, "errors": [{"message": "Selection set must not be empty for type 'AccountEdge'"}]} +(1 row) + + -- nested node + select graphql.resolve($$ + { + accountCollection { + edges { + node + } + } + } + $$); + resolve +----------------------------------------------------------------------------------------------- + {"data": null, "errors": [{"message": "Selection set must not be empty for type 'Account'"}]} +(1 row) + + -- node by primary key + select graphql.resolve($$ + { + accountByPk(id: 1) + } + $$); + resolve +----------------------------------------------------------------------------------------------- + {"data": null, "errors": [{"message": "Selection set must not be empty for type 'Account'"}]} +(1 row) + + -- mutation payload + select graphql.resolve($$ + mutation { + insertIntoAccountCollection(objects: [{ }]) + } + $$); + resolve +------------------------------------------------------------------------------------------------------------- + {"data": null, "errors": [{"message": "Selection set must not be empty for type 'AccountInsertResponse'"}]} +(1 row) + + -- a field skipped away at the top level still reports the empty operation + -- selection set, unchanged by the above + select graphql.resolve($$ + { + accountCollection @skip(if: true) + } + $$); + resolve +-------------------------------------------------------------- + {"errors": [{"message": "Selection set must not be empty"}]} +(1 row) + +rollback; diff --git a/test/sql/resolve_error_empty_selection_set.sql b/test/sql/resolve_error_empty_selection_set.sql new file mode 100644 index 00000000..e8883844 --- /dev/null +++ b/test/sql/resolve_error_empty_selection_set.sql @@ -0,0 +1,60 @@ +begin; + + create table account( + id serial primary key, + parent_id int references account(id) + ); + + -- A composite field selected with no subfields must be rejected, not + -- silently resolved. See https://github.com/supabase/pg_graphql/issues/412 + + -- top level connection + select graphql.resolve($$ + { + accountCollection + } + $$); + + -- nested edges + select graphql.resolve($$ + { + accountCollection { + edges + } + } + $$); + + -- nested node + select graphql.resolve($$ + { + accountCollection { + edges { + node + } + } + } + $$); + + -- node by primary key + select graphql.resolve($$ + { + accountByPk(id: 1) + } + $$); + + -- mutation payload + select graphql.resolve($$ + mutation { + insertIntoAccountCollection(objects: [{ }]) + } + $$); + + -- a field skipped away at the top level still reports the empty operation + -- selection set, unchanged by the above + select graphql.resolve($$ + { + accountCollection @skip(if: true) + } + $$); + +rollback;