Skip to content

fix(graphql): execute one operation per request, selected per spec (fixes #142) - #143

Merged
allmonday merged 1 commit into
masterfrom
fix/multi-operation-selection
Sep 3, 2026
Merged

fix(graphql): execute one operation per request, selected per spec (fixes #142)#143
allmonday merged 1 commit into
masterfrom
fix/multi-operation-selection

Conversation

@allmonday

Copy link
Copy Markdown
Collaborator

Summary

Fixes #142. parse_document merged every operation's top-level selections into one flat dict, discarding operation ownership:

  • ≤6.1.2 (silent, worse): with multiple operations selecting the same group, the later tree overwrote the earlier one, and the executor serialized the first operation with the second operation's projection tree — measured: a mutation declaring { id } returned email too. Response keys overwrote each other as well, and mutations in both operations executed.
  • since 6.2.0 (visible): the duplicate-response-key detection made the same document raise ALIAS_CONFLICT — safer, but rejecting legal GraphQL (multi-operation documents + operationName are spec-standard).

This PR restores the operation dimension end to end: parse per operation, execute exactly one operation per request.

Changes

Piece What
QueryParser.parse_operations() New: per-operation parse trees (definition / operation / name / selections). Same-name groups in different operations coexist; in-operation duplicate response keys still conflict (specs/023 FR-007). parse_document keeps its legacy flat behavior, now documented with a CAUTION.
execute_query(parsed_operations=...) GraphQL-spec selection: operationName matches a named operation; a single anonymous operation runs as-is; several operations without a name is an error (graphql-core wording: "Must provide operation name if query contains multiple operations."). The legacy all-definitions path remains for direct callers.
GraphQLHandler Switches to the spec path — the operation_name it always accepted (and the federation transport always passed, introspect.py) finally takes effect.
compose_query Exactly one operation per document (a bare MCP query string has no operationName channel); multi-operation documents are rejected with a clear message.

Behavior change

Multi-operation documents without a name used to execute ALL definitions (with cross-contaminated selections); they now error per spec. Single-operation requests — the overwhelmingly common case — are completely unaffected (locked by tests).

Verification

  • 12 new tests across the matrix: issue-scenario regression (projection isolation — no email leak), multi-op-without-name error, unknown operationName, single-anonymous-op unchanged, legacy path unchanged, handler e2e (operationName selection), compose multi-op rejection, parser unit tests.
  • Full suite: 1672 passed / 0 failed; ruff check src/ clean.
  • specs/023 tasks.md "已知边界" record updated to point here.

🤖 Generated with Claude Code

)

parse_document merged every operation's top-level selections into one
flat dict, discarding operation ownership — with multiple operations
selecting the same group, the later tree silently overwrote the earlier
one on ≤6.1.2 and the executor serialized with the WRONG projection
(field leakage; measured: a mutation declaring { id } returned email).
Since specs/023 the same document raised ResponseKeyConflictError
instead — visible, but rejecting legal GraphQL.

- QueryParser.parse_operations(): per-operation parse trees carrying
  definition/operation/name; in-operation duplicate response keys still
  conflict; parse_document keeps its legacy flat behavior (documented)
- execute_query(parsed_operations=...): GraphQL-spec selection —
  operationName matches a named operation, a single anonymous operation
  runs as-is, several operations without a name is an error
  (graphql-core wording). The legacy all-definitions path stays for
  direct callers
- GraphQLHandler uses the spec path — the operationName it always
  accepted (and federation always passed) finally takes effect
- compose_query: exactly one operation per document (no operationName
  channel on a bare MCP query string); multi-operation documents are
  rejected with a clear message

Behavior change: multi-operation documents without a name used to
execute ALL definitions (with cross-contaminated selections); they now
error. Single-operation requests are unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
@allmonday
allmonday merged commit a852e02 into master Sep 3, 2026
6 checks passed
allmonday added a commit that referenced this pull request Sep 3, 2026
Version bump for PR #143 (issue #142): per-operation parse trees,
spec-compliant operation selection (operationName finally takes effect),
compose single-operation constraint.
Updates docs/changelog.md, pyproject.toml, uv.lock.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

Multi-operation documents: same-name top-level groups cross-contaminate selections — silent field leakage on ≤6.1.2

1 participant