Skip to content

feat: pick definitions the selected fields do not reference - #231

Draft
barisakcam wants to merge 9 commits into
COVESA:mainfrom
barisakcam:feat/retained-definitions
Draft

barisakcam wants to merge 9 commits into
COVESA:mainfrom
barisakcam:feat/retained-definitions

Conversation

@barisakcam

@barisakcam barisakcam commented Sep 28, 2026 •

Copy link
Copy Markdown

feat: pick definitions the selected fields do not reference

What

Adds @pick, a directive on the selection query that keeps definitions the selected fields do not reference — scalars, directives and enums that dependency-based filtering would otherwise drop.

query Selection
  @pick(
    enums: ["FuelType", "VehicleStatus"]
    scalars: ["DateTime"]
    directives: ["constraint"]
  ) {
  vehicle {
    id
    model
  }
}

Each argument takes three forms:

  • Absent — only referenced definitions are kept, exactly as before.
  • An empty list, such as scalars: [] — every definition of that kind is kept.
  • A list of names — those definitions are kept in addition to the referenced ones.

An enum is always kept whole. Filtering never changes what an enum means, and @instanceTag(exclude: ...) remains the way to steer how an instance tag unfolds.

A query that only picks definitions has no fields to name, so an empty selection set is read as selecting nothing. That makes definition-only schemas filterable for the first time: on the QUDT units vocabulary, @pick(enums: [...], directives: []) {} reduces 712 enums to the three a model actually needs.

Why

A selection query could choose fields but had no way to retain an unreferenced definition, so filtered output silently lost scalars and directives that models still depended on.

Notes for review

  • The directive is stripped from the document before validation rather than shipped as a schema definition, so nothing is added to the model and the name cannot collide with anything in it. The cost is that an external GraphQL tool reports an unknown directive for a selection query that uses one.
  • A query without @pick produces byte-identical output to before.
  • Enum value subsetting was implemented and then removed in fd3d829. It let a query change how an instance tag unfolds, which belongs to the model rather than to the query.
  • All four places that read a selection query share one parser, including the one that reads a vendored dependency's query.

Testing

21 tests in tests/test_pick.py over a fixture covering picking, validation, the empty selection set, and equivalence with { __typename }. Full suite: 892 passing, mypy and pre-commit clean.

Add @retainedDefinitions, a directive on the selection query that keeps
scalars, directives and enums the selected fields do not reference, and
narrows a retained enum to the values it lists. An empty list keeps every
definition of that kind; an absent argument leaves today's behaviour, so a
query without the directive filters exactly as before.

The directive is defined by s2dm rather than by the model, so it is taken
off the document before the document is validated against the schema.

Removing an enum value that a retained default or directive argument needs
is refused. Instance tag exclude entries are not treated as references: an
entry asserts that one instance is absent rather than consuming a value, so
an entry naming a removed value is dropped as vacuous. Entries naming no
removed value are left alone, so a misspelled entry still fails at
expansion.
Cover @retainedDefinitions in the selection query section: the three forms
each argument takes, selecting enum values, and what narrowing an instance
tag dimension does to the generated instances.
A selection that removes the last surviving combination left the container
type empty with nothing said. The generated JSON Schema then carries no
properties alongside additionalProperties false, which admits only the
empty object, so the filtered schema rejects the data it was derived from.

Warn rather than fail: the same state is reachable by excluding every
instance in the model, and that has always been allowed.
Enums can no longer be narrowed to a subset of their values. Selecting
values let a query change how an instance tag unfolds, which is what
@instanceTag(exclude: ...) is for and belongs to the model rather than to
the query. Picking an enum now keeps it whole, so filtering never changes
what an enum means.

The enums argument becomes a plain list of names like the other two, which
retires the EnumSelection scalar. Nothing can remove a value any more, so
the rule refusing removals that a retained default still needed, and the
rule dropping instance tag exclude entries left vacuous by a removal, both
go with it. The warning for an instance tag that expands to no instances
goes too: a selection can no longer empty the product, and reaching that
state by excluding every instance in the model has always been allowed.
A query that only picks definitions has no fields to name, but GraphQL
rejects an empty selection set. A schema of units, enums or shared
directives hits this immediately, since it carries nothing selectable.

Read `{}` as `{ __typename }`, which every type carries and which names
nothing in the model. The rewrite runs only after a parse failure and only
on a trailing empty pair, so a query broken for any other reason still
reports its own error. All four places that read a selection query share
the parser, including the one that reads a vendored dependency's query.

Report a name given under the wrong argument as such. A directive and a
type are looked up in different places, so neither is found where the
other is named, and "not defined in the model" was the only thing the
check could say about either. It now names the argument that would accept
the value instead.
Consolidate the two mirrored validation paths into one that asks where a
name actually belongs and compares that against where it was written, so
the three arguments share a single code path and a set of messages.

Replace the ALL magic string with a typed sentinel, so a selection can no
longer be built from an arbitrary string, and take the argument names from
constants rather than repeating them at each use.

Name the intermediate values that were previously nested inside calls, give
the directive and type parameters their real types instead of Any, and stop
using "selection" for both a picked name list and a query's selection set.
The editor validated the selection query against the model alone, so every
query using @pick was marked invalid and the apply button stayed disabled:
the directive is unknown to the model, and an empty selection set does not
parse. Read the query the way the server does — treat empty braces as
selecting nothing, and strip @pick before validating — and give the
editor's schema the directive definition so it stops underlining it. That
schema is built for validation and completion only and never leaves the
browser.

Drop a query root left with no fields from printed output. Selecting only
definitions leaves nothing on it, and an object type with no fields is
invalid GraphQL, which graphql-js rejects even though graphql-core accepts
it. Without the root the output is a fragment that composes into a model
with one, which is the shape the vocabulary it was filtered from already
has.
Stop stripping @pick in the playground before validating. The editor's
schema defines the directive now, so stripping it only hid mistakes inside
it: an unknown argument, a non-name in a list, and @pick on a mutation were
all accepted in the editor and rejected by the server.

Refuse a name GraphQL reserves for introspection. Such a name passed
validation, because the introspection types are in the type map, and was
then dropped when the picks were collected, so the user got neither the
definition nor an error.

Read @pick from the one query a selection file holds, rather than walking
every operation to find the first. A @pick on a later operation now stays
in the document, where validation rejects it as unknown.

Give the extraction and its validation a single entry point so a caller
cannot do one without the other, print inside the function that drops an
empty query root, and cover the kinds a selection can name with a test that
fails when one is added without being handled.
Removing the root with a line-anchored substitution on printed output left
behind anything the regex could not see. A documented root left its
description, which the next definition then silently claimed, and a root
named something other than Query left the schema block that named it, so
the output no longer rebuilt. Print a schema rebuilt without the root
instead, carrying the model's own description across, which takes both with
it and retires the constraint that this had to run before directives were
reattached.

Resolve a picked name against every namespace that defines it. Types and
directives are separate namespaces, so a scalar sharing a name with a
directive was unreachable: picking it under scalars was refused, and the
message sent the user to directives, which keeps the directive instead.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant