Skip to content

Releases

v0.26.3 — -R works again against current rq and keeps rq's hedges; remote schemas say their cache age, fall back to a stale copy with disclosure, and retry transient failures; corrected queries no longer take a minute.

@dpep dpep released this 03 Oct 14:21
1972e8a

Added

  • A live schema whose refetch fails is answered from the expired copy
    instead of failing — when the endpoint is down, times out, answers 5xx, or
    answers with a GraphQL errors body, and a cached copy exists (also under
    --refresh). stderr says what failed and how old the copy is, even under
    -q; JSON source adds "stale": true and error. Exit 0: an answer
    was given. A 401/403 never falls back, and with no copy the fetch fails as
    before.
  • An answer from a cached copy of a live schema says how old it is:
    from a copy of <url> cached 41m ago — --refresh to refetch on stderr
    (silenced by -q), and a source object (url, cached: true,
    age_secs) on every JSON row and on the -J no_matches row. Rows from a
    file or a fresh fetch are unchanged.
  • A miss from a remembered discovery says so: no matches for "x" in schema.graphql (remembered from an earlier walk — --refresh re-walks), and
    the -J miss row carries source: {path, discovered: true, remembered}
    whenever gqls picked the schema.
  • A live fetch retries a transient failure. A refused or dropped
    connection, a 502/503/504, or a 429 (honouring Retry-After up to 5 s) is
    tried up to three times in all, with a short backoff, inside a 40 s budget —
    so a hung endpoint costs one timeout, not three. A 4xx refusal and a GraphQL
    errors body are never retried. -v says what was retried and why.
  • A fetch still running after 2 s says so (still waiting on <url>); a
    hung endpoint was silent for 30.
  • -R passes rq's hedges through. A class declared in several places shows
    also in <file:line> and carries declarations/also_in in JSON. A hit
    from a checkout rq is still indexing says rq still indexing: N of M files
    on stderr and carries rq's warming object; a hit rq held back as
    provisional (rq's exit 2) is kept, marked (provisional), with
    "provisional": true on every JSON row. Exit 0 — it's an answer.

Changed

  • --profile times the explain check and the miss diagnosis as their own
    phases
    (explain check, miss diagnosis); output is now just writing
    the results, which it was always presumed to be.
  • -R -j renames confidence to rq_confidence. It was rq's confidence
    in the name match, so a (guess) rq matched exactly read as
    "confidence": 1.0. gqls's own verdict is loose. A script reading
    confidence must read rq_confidence — though against rq 0.52.0 or newer
    -R failed outright, so no current setup was producing the old field.
  • -R exits 1 when rq is still indexing and found nothing, with rq's
    progress and hint, instead of exiting 0 under a no code definition found
    that read as definitive. A miss on a finished index still exits 0.
  • A failing rq is reported in its own words — its JSON error and its
    exit code — rather than the first stderr line beside exit Some(64).

Fixed

  • A failed fetch says what happened once. introspecting URL: URL: Network Error: Network Error: … reads introspecting URL: couldn't connect (Connection refused), and an HTTP failure names its status and the
    server's GraphQL error message (HTTP 401 Unauthorized: bad token).
  • A query correcting to a common field name no longer takes a minute.
    Deciding whether a query names one record re-scanned every name in the
    schema once per record it corrected to: on a 48k-record schema, emial
    (→ 2,811 email fields) took about 60 s and ñame 77 s. It is asked once
    per query now, and the check allocates nothing for ASCII names and runs in
    parallel — emial takes about 16 ms. Every result is unchanged.
  • -R works again. It passed rq --no-record, which rq removed in 0.52.0,
    so every resolve against a current rq failed with unexpected argument.
    gqls now passes rq only --ndjson --limit 10 (and --verbose under -v),
    and a test drives the installed rq when there is one.

v0.26.2

Choose a tag to compare

@dpep dpep released this 20 Sep 20:39
88d3702

Changed

  • The and N more hint no longer quotes the command it suggests. A
    trailing dot is no shell metacharacter, and quoting it there taught the habit
    everywhere else.

  • A phrase in your own words finds more than it used to. Three rules widen
    what counts as covering one of its words:

    Where a schema names something in its own vocabulary and describes it in the
    user's, the description now bridges them — current user reaches
    Query.viewer ("The currently authenticated user"), which its name alone
    never did.

    The words a question is built from (who, what, does, has, this,
    my) named nothing in any schema, so each one a record happened to echo
    lifted its word count and buried the records matching the words you meant.
    They're dropped before scoring now, like the other stopwords, so a question
    asked as a sentence narrows about as well as the keywords inside it.

    A word matching nothing at all is retried as its singular, one tier
    below a direct match: types never reached pokemon_v2_type, because the
    trailing s has nowhere to land in a subsequence and a prefixed name is far
    outside the typo budget. The other direction already worked, since type
    lands inside types on its own.

    A description word counts toward how much of a phrase a record covers, but
    the bar to clear is still the best coverage by name — prose can reach it,
    never raise it, so a record whose description happens to mention a word can't
    displace the one named after it. Single-word queries are matched against
    names only and are untouched by all three.

    Measured, each against the labelled sets in script/eval/: the description
    rule took MRR from 0.43 to 0.47 on 49 GitHub queries, rescuing five and
    losing none; dropping question words took it from 0.06 to 0.13 on 30 queries
    written as full questions by someone other than their author, leaving the
    shorter-phrase sets unchanged; the singular retry took the held-out set from
    0.22 to 0.27, and four of six sampled PokeAPI root fields from unreachable to
    found.

  • A generated schema's boilerplate ranks below what it wraps. Hasura and
    PostGraphile surround every real table with a dozen derived types — filters,
    aggregates, sort orders, insert shapes, _stream subscriptions — each
    carrying the table's name, so each matches everything the table matches. On
    PokeAPI's schema they are 22,667 of 26,218 records, and they filled the whole
    first page of every answer: list of egg groups had 50 of them above the
    table it named. Relay's Connection and Edge are the same shape in the
    other spelling — RepositoryRulesetEdge was the top answer for "programming
    languages used by a repo" — and now rank below the type they wrap too. Both
    lists are suffix conventions their generators own, so a hand-written schema
    is untouched, and a type actually called Edge is left alone.

Fixed

  • A truncated wildcard said so twice. A query compiles its filters once to
    search and once to explain, and the warning lived in the matcher.
  • A cache location gqls can't use is said aloud. A relative or empty
    XDG_CACHE_HOME is ignored (the XDG spec's rule, and what keeps
    --clear-cache out of your cwd) — but silently, so a run cached somewhere
    other than where you pointed it. It now says which variable it ignored.
  • Having nowhere to cache no longer reports an empty cache.
    --clear-cache with neither XDG_CACHE_HOME nor HOME set said
    cleared 0 cached file(s), which reads as "the cache was already empty".
  • A qualifier naming no type leads with what it searched. The line read
    no type named "Repo" — matching …, which looks like a failure above an
    answer that is usually right (Repo.owner finds Repository.owner).

v0.26.1

Choose a tag to compare

@dpep dpep released this 20 Sep 05:30
6d5010a

Fixed

  • --clear-cache took any *.tmp* file, including .tmpl templates. It
    now takes only gqls's own shape, <name>.tmp<pid> — the same data-loss class
    as the bug above, narrowed but not closed.
  • A cache directory that is a symlink is cleared rather than skipped.
    Writes follow it, so --clear-cache said cleared 0 cached file(s) over a
    full cache — indistinguishable from an empty one. A symlink inside the
    cache is still never followed.
  • A non-ASCII type name no longer aborts a search. The word check indexed a
    char table with a byte offset, so an introspection dump carrying one panicked
    (index out of bounds) instead of answering.
  • The plural of a schema word isn't corrected into a lookalike either.
    star was protected by star_count but stars still became start; a
    plural the schema really has (starts → start) still corrects. -e and
    -R now judge that against the whole schema, as explaining already did.
  • A path-shaped query is fine once the source is named. gqls schema.graphql 'read/write access' was refused as a bad source, though a second positional
    can't be one; the same query on stdin was answered.
  • A path that isn't a schema source is refused. gqls user ./schema.grapqhl folded the typo into the query and answered from whatever
    schema discovery found, exit 0. A positional containing /, or naming an
    existing file, that isn't a schema source now exits 1 saying what a source
    looks like.
  • --returns given an enum value or a directive says what it is
    (ADMIN is a value of enum Role, not a type), as it already did for a field;
    the field message no longer says an input field is "fetched".
  • A qualifier naming no type says the search went unscoped. gqls Repo.name fell back to matching every type's members — documented, but
    silent, so the scope looked applied.
  • --fuzzy=x is dropped with the same warning as --fuzzy, rather than
    clap's tip to search for the flag's own text.
  • 0.26.0's schema-word rule refused names it should have drafted. It
    stopped a correction whenever the typed word was a word of any name —
    including the target's own, so binary -e stopped drafting isBinary, and
    published, stargazer and about 45 others on GitHub's schema went the same
    way. A word of the target's own name, or its plural, now counts as naming
    it. And _ now separates words, so star against star_count or
    STAR_COUNT is protected from becoming start the way starCount was.
  • --clear-cache could delete files gqls doesn't own. 0.26.0 cleared
    every file under the cache dir, and an empty or relative XDG_CACHE_HOME or
    HOME — common in CI and containers — resolved that dir against the cwd, so
    a project directory named gqls lost its files; a symlink inside the cache
    was followed too. The cache location now needs an absolute path, as the XDG
    spec says, and clearing deletes only the kinds of file gqls writes (and the
    .vecs older releases wrote), never following a symlink. If you ran
    --clear-cache on 0.26.0 with either variable empty or relative, check the
    directory you ran it from.
  • --returns given a field says so. gqls --returns User.name answered
    nothing returns User.name — a claim about the schema, when the flag takes a
    type. It now exits 1 naming what the field returns and pointing at
    gqls User.name -e, which drafts a query that fetches it.

v0.26.0

Choose a tag to compare

@dpep dpep released this 18 Sep 17:56
066f5df

Removed

  • Semantic search. gqls is fuzzy-only now: one build, no ONNX Runtime, no
    model download, no background embedding. It wasn't earning its keep: on 50
    intent queries against GitHub's schema, the default fuzzy+semantic combine
    ranked the right record lower than fuzzy alone (MRR 0.37 vs 0.42), and
    semantic on its own only tied fuzzy on the synonym-style queries it existed
    for. --semantic, --model and --warm are now usage errors (exit 2)
    that say why — drop them from scripts. --fuzzy still runs, since every
    query is fuzzy now, but warns that it does nothing. The semantic /
    semantic-dynamic cargo features and the GQLS_NO_AUTOWARM,
    GQLS_MODEL_DIR and GQLS_SEMANTIC_FLOOR variables are gone too, and JSON
    rows no longer carry degraded. Run gqls --clear-cache once to reclaim the
    embedding vectors an earlier release cached — it now empties the whole cache
    directory, files it no longer writes included. The embedding model under
    ~/.cache/huggingface is left alone, since other tools may share it.

Fixed

  • A word the schema already uses is no longer "corrected" into a lookalike.
    gqls star explained CheckAnnotationSpan.start as a misspelling and hid
    addStar behind "76 other matches"; gqls star -e drafted against it.
    Across every word in GitHub's schema, 83 were corrected this way — host to
    RateLimit.cost, the to the country code TH — and a few more, like
    bot, stopped explaining the record they named exactly because a bogus
    correction tied with it. A correction now applies only where the part it
    corrects isn't a word of some name; strat and Usr.email still correct.
  • A miss that a filter caused counts its matches in plain English. It said
    43 match without it, and 1 matches for one.
  • --profile JSON rounds milliseconds to two decimals rather than printing
    float noise like 0.10579200000000001.
  • -e warns only about deprecated fields the draft selects. A field left as
    a commented # name: Type { … } hole, or dropped from an implementor's
    fragment because the interface already selected it, was named in the
    deprecated: line with nothing in the draft to find. On the GitHub schema at
    --depth 2, 125 of 300 root drafts (42%) named at least one such field; none
    do now. Fields that really are selected keep both their inline # deprecated:
    flag and their place in the line.
  • The deprecated: line no longer says (flagged inline). The drafted field
    itself is deprecated often enough to matter and never carries an inline note —
    a note goes on a selection, and the target is the operation. It reads
    (drafted anyway) now, which says the thing worth saying: nothing was
    silently dropped.

v0.25.0

Choose a tag to compare

@dpep dpep released this 15 Sep 04:26
733f1f3

Added

  • --via picks the route -e draws. The walk settles each type at its
    nearest hop, so on a schema with a global-ID lookup every type is one hop from
    Query.node and gqls <gh> Issue.title -e drafts
    node(id:) { … on Issue { title } } — the route through a repository wasn't
    ranked low, it was never a candidate. --via names a prefix of the route in
    the notation # paths prints, and the walk takes only the field named at each
    of those hops: -e --via Query.repository gives
    repository(owner:, name:) { issue(number:) { title } }, and
    --via 'Query.repository > Repository.issues' goes through the connection.
    Hand back any path the listing prints, at any length, including an input's
    Mutation.addStar(input:). It repairs a draft for someone who noticed the
    route was silly — # paths listing four generic ID lookups is the only tell —
    and does nothing for someone who pastes the first answer. Fixing that default
    is a weighted walk rather than a sort key, and is not this.

Fixed

  • A deprecated field is named once in the -e warning, not once per place
    the draft selects it. A type selected under two fields was listed twice; the
    inline flags were right, the summary line wasn't.

v0.24.0

Choose a tag to compare

@dpep dpep released this 14 Sep 04:07
2507081

Changed

  • -e says when a draft is too large to paste. A selection set fans out by
    the branching factor of the schema, so --depth is cheap to type and
    expensive to render — GitHub's Repository is 6KB at depth 1 and 200KB at
    depth 3. The size is invisible until it has scrolled past, so a draft over
    32KB now says how big it is, and names --depth when that's what got you
    there. Not a cap: a large schema can legitimately want a large draft. The
    threshold is measured — across four schemas every draft anyone would paste
    came in under 9KB, and the smallest unusable one was 88KB.

  • A fuzzy score is now the fraction of a perfect match, where perfect means
    the query is the name — one scale, meaning the same thing whatever the
    query's length, the name's length, or which branch produced it. A clean word
    inside a longer name used to score an order of magnitude below a prefix match
    of the same quality, so the weak-tail cut dropped it outright: gqls <schema> disput never reached Mutation.in_app_disputes at any limit. It ranks 85th of
    116 now — still past a default page, but findable.

    --json/--ndjson consumers: scores are smaller and differently shaped.
    A perfect match is 1000, plus up to 300 where a Type. qualifier names the
    right parent; the old top was 1060. Note that only the fuzzy ranker writes on
    this scale — --semantic reports a cosine in 0..1 and the default combine of
    the two reports a rank-fusion score around 0.03, as both always did. Sort on
    score within one query; don't compare it across two.

  • Kind — root field, type, leaf field — no longer adds to the score; it
    breaks ties between matches of equal quality instead.

  • A name that repeats the query, or ends with it, outranks a shorter name that
    merely starts with it.
    On a schema where every name shares a prefix —
    anything Hasura-generated — gqls <schema> pokemon returned four unrelated
    tables ahead of pokemon_v2_pokemon, which sat at rank 21 with the object type
    at 899. Measured over 2309 queries on four schemas, 292 changed top hits are
    better and 17 worse by an independent referee; the table-lookup case went from
    17 unfindable to 7.

  • --profile times the network apart from the parse when the source is a
    URL. Both were inside one load span, so a slow run against a live endpoint
    gave no way to tell a slow endpoint from slow gqls — on one measurement the
    split was 481ms of network against 3ms of parsing.

  • The one-time embedding pass no longer guesses "may take a minute". It
    reports an estimate measured from the run's own rate, and now prints to
    non-terminal stderr every 15s — a piped or CI run could not tell slow from
    hung, and on the largest schema tested this is minutes, not one.

  • --json/--ndjson carry degraded: true when semantic ranking fell back
    to the hash embedder; absent otherwise. The warning was stderr-only, which
    never reaches a caller parsing stdout.

  • Cached introspection responses are bounded by count and size, least
    recently used evicted, rather than by age alone.

  • -e/-R say out loud when several records are spelled exactly like the
    query.
    id names Character.id, Location.id and Episode.id; the pick
    used to look settled. It still drafts and still exits 0 — the runners-up go to
    stderr, and -e -j gains also_named ([] when there was no choice), because
    a stderr line doesn't reach a JSON consumer.

  • -e says when one level of selection reaches no leaf at all. A Relay
    connection has nothing but object-valued fields, so the default draft runs and
    fetches nothing; the note points at --depth. The draft itself is unchanged.
    Said only where --depth can actually help: a namespace container whose
    fields every one takes required arguments draws the same empty-looking
    selection, but no depth selects through them — draft the inner field by name.

  • --depth is capped at 6. A selection set fans out geometrically, so a
    mistyped depth was a stack overflow or a gigabyte of stdout rather than a slow
    answer: GitHub's Repository drafts 1.8MB at depth 4 and 877MB at 7, chime's
    User 90MB at 12. Past the cap nothing is a document you could paste. A
    --depth above it now drafts at 6 and says so on stderr; scripts passing a
    larger number get a smaller draft instead of a crash.

Added

  • --resolve follows a namespaced root. A field on CardMutationRoot looks
    for Mutations::Card::ActivateCard, and Queries::/Subscriptions:: are
    tried beside Resolvers:: for root fields. Across six field shapes on a real
    server repo the tally went from 1 clean hit, 1 near miss, 3 misses and 1
    confidently wrong answer, to 5 clean hits, 1 honest miss and none wrong.
  • -v says when introspection can't report applied directives. The protocol
    exposes directive definitions but not their applications, so directives is
    always empty from an endpoint or a dump however many @auths the SDL applies —
    a limit of the protocol, not of the schema. Said only when the schema defines
    directives of its own, so it stays quiet on the five every server defines.
    @deprecated is the exception and is reconstructed.

Fixed

  • -e could overflow the stack on a legal schema, at the default depth. The
    payload/errors convention expands an errors field at the last level too, and
    did it unconditionally — so a schema where the errors chain returns to a type
    already being selected (Payload.errors -> UserError.errors -> Payload, or
    just Payload { errors: Payload }) recursed until the process aborted with
    exit 134. The convention still expands; it just no longer re-opens a type
    open above it, which is a cycle with no level left to spend.
  • gqls announced "no matches" over rows it had just printed. The miss
    message was decided by the fuzzy match count, but the default ranking also
    prints rows that matched by meaning alone — so stderr said the schema had
    nothing while stdout listed fields, and an agent reading one reached the
    opposite conclusion of an agent reading the other. Rows now get a message
    that says what they are.
  • A single -J query that matched nothing printed nothing at all. Zero rows
    on a row-per-line stream is zero bytes, so the caller could not tell a miss
    from a query that never ran, and the degraded flag marking weak ranking had
    nowhere to ride. It now emits {"status":"no_matches"} — with degraded when
    it applies — the way a batch always has. -j is unchanged: an empty array was
    already a complete answer.
  • An argument answers when nothing matched a name, not when nothing matched
    at all.
    The first pass matches names and qualified paths, so a weak
    subsequence of someone else's path counted as an answer and suppressed the
    argument pass entirely. gqls <github> until returned CheckRun.title at
    0.128 of a perfect match and hid the seven fields that take until: at 1.0 —
    one unrelated type in the schema was enough to do it. Path matches still stand
    when nothing takes the query as an argument either, and an argument still
    never outranks a name: first returns the fields named for it, not the 348
    that take one.
  • One long field name no longer indents every other row of a fields block.
    The search table's columns were capped last round and the block you get by
    naming a type wasn't, so a single generated name or return type set the
    width for every row under it — a 35-column name and a 38-column type gave
    every line of one real type an 81-column indent against an 80-column
    terminal. Both columns are now capped the way the table's are (24 and 28,
    the ninetieth percentile on two production schemas), and a cell wider than
    its column overflows its own line instead of moving everyone else's. Across
    those schemas the share of field rows running past 80 columns falls from 14%
    and 24% to 5%. A schema with no outlying names renders unchanged.
  • The introspection cache ignored the credentials that fetched a schema. It
    keyed on the URL alone, so the first response to succeed was replayed for every
    later run against that URL whatever headers were supplied — or not supplied.
    A revoked or rotated token kept working for up to an hour, a CI check existing
    to assert auth is enforced passed falsely, and two callers sharing a URL with
    different credentials could be served each other's schema. The key covers the
    request headers now — sorted, names lowercased, hashed, never written to the
    path. Entries cached under the old rule are unreachable and refetch once;
    nothing for you to do.
  • A parsed-record cache file that won't decode says so under -v before
    falling back to reparsing, the way the introspection cache already did.
  • -e leaked a multi-line deprecation reason out of its comment. A #
    comment ends at the newline, so only the reason's first line stayed commented
    and the rest landed in the selection set, where a server reads it as field
    names. On one real schema that broke 80 of 2083 drafts — all from a single
    field, because it's selected under every actor chain. Every free-form string a
    draft quotes is flattened to one line now, including an optional argument's
    block-string default, which was independently broken.
  • A type's fields block showed no directives. The type carried its own and a
    named field carried its own, but the list between them carried none — so
    working out which subgraph owns each field of a federated type meant naming
    every field, one command each. Directives sit in the description's cell now,
    after any (deprecated: …) marker, because the two are disjoint in practice:
    across 14,806 field records in four schemas, exactly one carries both. A schema
    whose fields have no applied directives renders exactly as before — which is
    every schema loaded by introspection, since the protoco...
Read more

v0.23.3

Choose a tag to compare

@dpep dpep released this 10 Sep 14:27
92df555

Fixed

  • Naming Query or Mutation listed no fields. A root type's fields carry
    their operation's kind rather than Field, so the fields block skipped them —
    leaving the one type whose fields are most worth listing showing none at all.

Added

  • An argument's name finds the field that takes it. gqls followRenames
    found nothing: arguments aren't records, so their names were unsearchable and
    the README promised otherwise. The field that takes one is now matched by it —
    which is what you'd call anyway, and naming that field says what the argument
    is for. It ranks below every name and path match and so only surfaces when
    nothing else matched, which is what keeps a Relay schema's 348 first
    arguments from burying a search that meant a field.
  • -e reaches an input object that only another input holds. An input is
    drafted through the field that takes it, and one nothing takes was refused —
    though it may still be held by one that something takes
    (AddressValidationInput { address: AddressInput! }), which is exactly where
    a caller needs to see it go. The draft is the outer input's, with the one you
    asked about expanded where it sits inside the variables block, and a stderr
    line saying which input carries it. That takes one production schema's
    draftable input objects from 198 of 230 to all 230. --json carries
    passed_inside.

v0.23.2

Choose a tag to compare

@dpep dpep released this 10 Sep 05:25
0e48efb

Added

  • -e reaches a target through as many hops as it takes. It only ever
    drafted a field or type one hop from a root operation field, which on a schema
    that namespaces its roots (Query.early_pay: EarlyPayQueryRoot, with the real
    fields hanging off that) is a small fraction of what's there. Across the two
    production schemas gqls is swept against, 340 of 1737 types and 643 of 1102
    drafted before; 1734 and 1101 do now, and everything still refused is
    genuinely unreachable rather than too deep. It walks the type graph out from
    the roots and nests through the shortest chain it finds, on both edges — a
    field or type by what returns it, an input object by what takes it, which
    takes one schema's draftable input objects from 168 of 230 to 198. The walk is
    cycle-safe, prefers the chain asking fewest arguments among equals, and stops
    at six hops — a cap neither schema reaches, since every type either one can
    reach at all lands inside it. Past the cap the refusal names the distance, so
    "too deep" reads differently from "not there".

    via and each # paths entry now spell the whole chain, Query.early_pay > EarlyPayQueryRoot.status, in text and --json alike. A one-hop path is
    unchanged; the one label that moves is a nested input consumer's, which used
    to name only the field taking the input and now carries the way in as well.

    Still refused: an input object that no field takes and only another input
    object holds — a third edge, and 32 of that schema's 230.

  • Arguments carry what they're for. Both loaders dropped argument
    descriptions — the introspection query had always asked for them — so the one
    thing a signature can't tell you was the one thing gqls couldn't show:
    owner: String! never says it wants a login. Naming a field now lists its
    arguments with the schema's prose for each, -e prints an # arguments:
    block for the ones along the chain, and --json carries them on both. Only
    documented arguments appear; a block of bare names would restate the
    signature. Arguments are still not searchable by name — they aren't records —
    and the record cache re-reads once on upgrade, as it does whenever its format
    gains a field.

  • A piped query explains what it names. The explanation tier — the
    annotations, and the match/values/fields/referenced_by keys — was
    switched off for stdin, so the same query answered differently depending on
    whether it arrived as an argument or down a pipe, with nothing saying so. A
    pipe is how an agent drives gqls, which is where the fuller answer is worth
    most. --no-explain still forces the list.

  • A Type. query with nothing to enumerate answers with the type. A union
    has no fields, so gqls 'SearchHit.' reported no matches while the union it
    named sat one line away with its members and what references it. An
    enumeration that finds something still never explains — a trailing dot is one
    character from the type's own name, and Query. means the root fields.

  • Naming an object or an interface lists its fields. Only an input object
    did, on the reasoning that an object's fields show up in a selection set
    somewhere else — they don't, and reaching them meant a second search
    (User.) that ranks and truncates rather than listing. Each field comes with
    its type, description and deprecation; one taking arguments is marked
    posts(…). Past a couple of dozen the list is elided with the command that
    spells out the rest (… and 121 more — gqls 'Repository.' -l 145 lists them all); --json carries every field and every argument signature.

  • -e drafts an operation for a type. gqls Animal -e used to answer
    "can't draft an operation for a union". Asking about a type is asking how to
    fetch one, so it now drafts the root that reaches it — narrowed to it where
    that root returns something broader, so gqls Cat -e is
    pets { ... on Cat { … } }. An enum or a scalar still can't be selected, but
    says so pointing at --returns, which is the question with an answer.

  • --returns widens rather than dead-ending. Nothing returns the
    Commentable interface, so the filter answered nothing — while every field
    returning a Post hands you one, and -e drafts exactly that. When no field
    returns the named type outright, the filter now matches what narrows to it,
    and says so on stderr. A wildcard is left alone, and so is a type something
    already returns.

Fixed

  • A deprecated object-valued field drafted an operation that doesn't parse.
    The # deprecated note was appended to the field name, so on a field with a
    selection set the { landed inside the comment and the document came out
    unbalanced. It fired at the default depth, since the errors convention is
    always expanded: 8 of 130 root operations in one production schema were
    affected.
  • -e drafted operations a conformant server rejects. Two members of an
    abstract type giving the same field name a different type — GitHub's
    Organization.email: String beside User.email: String! — were selected
    under one response name, which the spec forbids (§5.3.2); graphql-js rejects
    it outright and graphql-ruby warns that it will. Each is aliased by its
    member now (userEmail: email). Every root of two production schemas, at
    three depths, validates strictly.
  • -e orphaned a selection set it meant to drop. A field an interface had
    already selected was dropped from its implementors' fragments one line at a
    time, so a field with a selection set left its body and closing brace behind
    and the operation didn't parse.
  • -e dropped an interface's implementors. One whose added fields are all
    object-valued has nothing but # field: Type { … } markers, and the
    interface path dropped every marker — so the fragment came out empty and the
    implementor vanished, with nothing saying it existed. A union in the same
    position kept its markers, so the two abstract paths disagreed.
  • -l decided whether an answer was a list or an explanation. Whether a
    query names exactly one record was read off the top -l rows, so the display
    limit changed the answer and, on a big schema, which record got explained —
    user -l 1, -l 2 and -l 3 each answered differently, one of them
    explaining a deprecated field as an exact match out of 84. The decision now
    runs over every record the filters admit, and the count of what it set aside
    is out of the whole match set.
  • --profile printed nothing with -e or -R. Both return before the
    report block.
  • The semantic index was promised when nothing was building it. With
    GQLS_NO_AUTOWARM set — or when no warm could be spawned — the "building the
    semantic index in the background" line printed every run regardless, forever.
  • An annotation row stayed a row. implemented by was uncapped while
    referenced by elided past six, so GitHub's Node printed 128 implementor
    names. -D also swapped values and referenced by, though it only chooses
    between an enum's block and collapsed forms.
  • @join__* matched nothing. A pattern only addressed paths when it held a
    ., so pasting back a directive the way gqls prints it silently missed.
  • --returns '[Card!]!' matched nothing. Wrappers came off the schema side
    of the comparison but not off the flag, so the type as the schema writes it
    was the one spelling that failed.
  • -l 0 reported "no matches" for a query with five of them: a miss is
    nothing matching, not an empty page.
  • The README and --help claimed arguments were searchable. They aren't
    records, so gqls followRenames finds nothing; you reach an argument through
    the field that takes it. The claim is gone rather than the gap papered over.
  • --returns with no QUERY reported no matches for "*". The wildcard is
    gqls's own — nothing the caller typed — so the miss now names the filter:
    nothing returns Zork.
  • -e reaches a field through the fragment that narrows to it. A field was
    only drafted when some root field returned its enclosing type outright, so
    Pet.nickname reported "isn't reachable in one hop" whenever the only path
    ran through a union or another abstract type — a query people write daily,
    and one gqls Query.pets -e already drafted. A root whose runtime types
    overlap the field's parent now counts, and the draft says how:
    pets { __typename ... on Pet { nickname } }. A root returning a type that
    implements the interface still selects the field directly, with no
    redundant fragment.
  • A truncated result list says so without -v. The N matches; showing top M count was a verbose-only diagnostic, so a list cut off at -l (20 by
    default) read as the complete answer — naming a type with more fields than
    that quietly lost the rest. It's normal stderr status now, and it no longer
    fires when explain mode collapsed the list, which reports its own hidden
    matches.
  • A piped batch now answers each query as it arrives. read_queries drained
    stdin to EOF before searching anything, so producer | gqls schema.graphql -J
    stayed silent until the producer closed — and never printed at all for a
    producer that doesn't. The output bytes are identical either way, which is why
    this went unnoticed; the new test asserts the timing instead.

v0.23.1

Choose a tag to compare

@dpep dpep released this 21 Aug 03:40
f5048bf

Fixed

  • Input field defaults were dropped at load time. role: Role = MEMBER loaded as plain
    Role, which left a defaulted field indistinguishable from a mandatory one —
    direction: OrderDirection! = ASC may be omitted, and the ! on its own says
    the opposite. SchemaRecord carries a default now, both loaders populate
    it, and it shows beside the type: direction OrderDirection! = ASC when you
    name the input, and "<OrderDirection! = ASC>" in an -e skeleton.
    --json carries it on input-field records and inside fields.

    On upgrade the parsed-record cache re-reads once, since its on-disk format
    gained a field. Nothing to do — a miss costs milliseconds.

v0.23.0

Choose a tag to compare

@dpep dpep released this 20 Aug 13:29
8d8b23a

Added

  • Naming an input object lists its fields. It was the one composite kind
    whose members the explanation never showed — an enum gave its values, a union
    its members, an input object a single line — so gqls CreateUserInput said
    nothing about what goes in one. Each field is listed with its type, its
    description and its deprecation, and --json carries the same under a
    fields key.
  • -e drafts an input object through the field that takes it. An input is
    never callable but always passable, so gqls PostFilter -e now drafts
    Query.posts(filter: $filter) instead of refusing. Other fields taking one
    are listed under # paths; an input field (CreateUserInput.email) drafts
    through its enclosing input. The argument carrying it is supplied even where
    the schema calls it optional — a draft for PostFilter that leaves filter
    out answers nothing. Such a draft stays about the input: the reply gets the
    barest selection a server accepts, and only the named input's own types are
    expanded, so PostFilter no longer spells out the three types hanging off a
    sibling argument it doesn't fill in. --depth 1 asks for the payload back.

Changed

  • The -e variables block is a fillable skeleton, and # input types: is
    gone.
    "<CreateUserInput!>" never said anything the ($input: CreateUserInput!) signature above it didn't, which is exactly why a second
    block had to spell the type out in SDL — so the shape got printed twice, in
    two notations, neither of which you could paste into a variables pane. An
    input-object argument is now expanded into its fields, in the schema's own
    order, inside the block you paste. A list gets one element; a self-reference
    (Filter { and: [Filter!] }) closes as a placeholder, since its shape is in
    the object containing it; an input the schema has no fields for stays a
    placeholder rather than becoming a {} that claims it takes nothing.

    What's left over is what JSON can't express, and it's named for what it is:
    # enums: lists the values for each enum the variables reach. And because an
    expanded key stops naming its type, the heading does — # variables — input: CreateUserInput!.

    --json consumers: input_types is replaced by enums (a list of
    "Role = ADMIN | MEMBER" strings) and variable_types (a list of
    "input: CreateUserInput!"). variables now nests instead of holding a flat
    type placeholder per key.

  • --depth 0 now means the barest valid selection rather than being
    silently clamped to 1. It's what an input-object draft takes by default, and
    what --depth 0 always looked like it should do.

  • referenced by counts arguments, not just return types. It scanned what
    returns a type and nothing else, so it reported nothing at all for input
    objects, which are only ever passed in. An argument reference reads
    Mutation.createUser(input:), naming where the value goes. Applies to every
    kind: Role now shows @auth(requires:) too.

  • The library API is a fifth of its former size — 148 public items down to
    30.
    lib.rs re-exported all twelve modules, so most of that surface was
    public by default rather than by decision — which the header above already
    said was never the intent. logging, paths, profile, render,
    resolve, semantic and style are now crate-private, along with 25 items
    inside the five modules that callers do use. model::SchemaRecord,
    model::Kind, load::sdl, load::LoadOptions, search::search,
    search::Hit, search::Filters, example::build and cli::run stay public.

Nothing to do on upgrade: the gqls binary and its CLI surface are unchanged.

Removed

  • semantic::search, a one-shot convenience wrapper, has never been called —
    the CLI builds its own Session. It was dead from the initial commit and
    invisible to dead_code only because semantic was a public module.

Fixed

  • A drafted selection set is never all comments. A type whose fields are
    every one object-valued got a selection set holding only # field: Type { … }
    markers, which no server will parse. It now selects __typename alongside
    them, the same answer already used for a type the schema doesn't detail.
  • paths::temp_dir is gated on _semantic, matching its only caller. It was
    compiled, and dead, in the fuzzy-only build.

Internal

  • unreachable_pub is on. A pub item inside a private module is reachable
    from nowhere, and pub is precisely what makes dead_code skip an item — so
    the two together were hiding the two entries above.