Skip to content

Treat unsupplied query variables as an empty variables object#867

Merged
milessabin merged 2 commits into
typelevel:mainfrom
phdoerfler:fix/issue-743
Jul 20, 2026
Merged

Treat unsupplied query variables as an empty variables object#867
milessabin merged 2 commits into
typelevel:mainfrom
phdoerfler:fix/issue-743

Conversation

@phdoerfler

@phdoerfler phdoerfler commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Note: The current check failure is due to the usual Your tlBaseVersion 0.28 is behind the latest tag 0.29.0.

As a bit of an experiment, I put Claude to the task of figuring this one out. I have reviewed what I could and I have instructed Claude to adhere to proper test first etiquette and do a critical review of its own code but ultimately I am a bit out of my element here. This fix could be wrong. I am not deep enough into Grackle's compiler internals to deem this a correct fix or not. This being said: To me it looks fine. Also, the fix is very small, there is a reproducible test and the bug report is very recent.

And now without further ado, here is the PR text Claude proposed:


Fixes #743.

Symptom

Compiling a query that references a declared variable — at a field argument or inside an input object literal — fails with a spurious Variable 'x' is undefined when no variables JSON is supplied:

query getZuckProfile($x: String) {
  search(pattern: { name: $x }) {
    id
  }
}

The same query compiles fine with untypedVars = Some(json"{}"), which is the tell: per the spec (CoerceVariableValues), an absent variable values map and an empty one must behave identically.

Root cause

compileVars short-circuits to Map.empty when untypedVars is None, so declared variables never receive Vars entries. Any subsequent reference then fails the lookup in Value.elaborateValue with the misleading "undefined" error. The Some path instead coerces every declared variable via checkVarValue, which already handles absence correctly (defaults apply, nullable variables coerce to AbsentValue, non-nullable variables without defaults are errors).

Note this is not specific to input objects, despite how the issue was encountered — a plain profilePic(size: $x) with no variables supplied failed the same way. (The input-object variable collection bug in this area was fixed separately in #845; this reproducer still failed after it.)

Fix

The None case now runs the same coercion as the Some path with every variable absent.

Behaviour changes (all spec-aligned, review attention welcome)

Two existing test expectations codified the old behaviour and had to change:

  1. variable not defined (2) (now "variable declared but not provided is treated as absent"): a declared nullable variable used in a nullable argument position now compiles successfully with the argument absent, instead of failing. This is the same mechanism as unused var error is triggered when var appears in an input value #743 — the issue could not be fixed while keeping this expectation.
  2. variable not defined (5): @skip(if: $skipName) with $skipName declared but unprovided still fails (a Boolean! position with no value), but now in directive argument validation (Expected Boolean found 'null' for 'if' in directive skip) rather than with the misleading undefined message.

Additionally:

  1. A declared non-nullable variable without a default now errors at compile time when no variables are supplied (Value of type Int required for 'x' in variable values), even if unreferenced — matching both the spec and the existing behaviour when a variables object is supplied without that key.
  2. Variable-level default values now apply when no variables are supplied (previously the empty Vars map ignored them entirely).

Tests

  • The reproducer from the issue/743 branch (thanks @tpolecat), watched fail with the reported error before the fix, and run with unused-variable reporting enabled so it verifies the variable is seen as used.
  • Equivalence pin: the same query with an explicit empty variables object (passes before and after the fix — the fix makes None consistent with it).
  • Error pin for the non-nullable-unprovided case, and a pin for default-value application with no variables supplied.
  • Full core suite passes on Scala 2.13 and 3 (546 tests), plus the circe and generic modules.

@milessabin milessabin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is along the right lines, but I don't like the way the tests have been handled. Instead of repurposing test("variable not defined (2)") that test should have been fixed by marking the type as not null, so that the test doesn't fail as a result of the null default being applied. A new test should be added to cover the defaulted case, and probably some others to cover variations on defaults.

Fixes typelevel#743.

compileVars short-circuited to an empty Vars map when no variables JSON
was supplied, so declared variables never received entries and any
reference to one - at a field argument or inside an input object
literal - failed elaboration with a spurious "Variable 'x' is
undefined". Supplying an empty variables object took the coercion path
and worked correctly, an inconsistency: per the GraphQL spec
(CoerceVariableValues) an absent variable values map is equivalent to
an empty one.

The None case now coerces each declared variable against an absent
value exactly as the Some path does for variables missing from the
supplied object: defaults apply, nullable variables coerce to
AbsentValue and are treated as not provided downstream, and
non-nullable variables without defaults are errors.

Two existing expectations changed as a consequence, both spec-aligned:
a declared nullable variable used in a nullable argument position now
compiles successfully with the argument absent rather than failing, and
using a declared-but-unprovided variable in @Skip(if:) now fails in
directive argument validation (a Boolean! position with no value)
rather than with the misleading 'undefined' message.
Review follow-up: the fix newly makes variable-level defaults reachable
when no variables JSON is supplied (previously the empty Vars map
ignored them), which was uncovered. Also drop reportUnused = false from
the issue 743 reproducer so it verifies the variable is seen as used,
not merely that the spurious undefined error is gone.

@milessabin milessabin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@milessabin
milessabin merged commit e5844af into typelevel:main Jul 20, 2026
13 checks passed
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.

unused var error is triggered when var appears in an input value

2 participants