Skip to content

feat(compiler): Introduce ExecutableDocumentBuilder for operations with multiple sources#1017

Open
trevor-scheer wants to merge 14 commits intoapollographql:mainfrom
trevor-scheer:executable_document_builder
Open

feat(compiler): Introduce ExecutableDocumentBuilder for operations with multiple sources#1017
trevor-scheer wants to merge 14 commits intoapollographql:mainfrom
trevor-scheer:executable_document_builder

Conversation

@trevor-scheer
Copy link
Copy Markdown
Contributor

@trevor-scheer trevor-scheer commented Nov 25, 2025

Note

This PR was almost entirely written by Claude. I've reviewed this extensively myself and included the prompt and implementation plan for posterity.

Details

Prompt

@crates/apollo-compiler/src/executable/from_ast.rs @crates/apollo-compiler/src/schema/from_ast.rs

Create a plan and write to local .claude folder. ExecutableDocument and Schema are quite similar. I need a builder for ExecutableDocuments consisting of multiple sources. Schema already has this concept of schema builder, and you can parse schema AST into a schema builder. I want to be able to parse executable AST into an executable document builder.

ExecutableDocumentBuilder Implementation Plan

Context

Currently, ExecutableDocument is created directly from a single AST document via the document_from_ast function in crates/apollo-compiler/src/executable/from_ast.rs. However, Schema has a more sophisticated builder pattern (SchemaBuilder in crates/apollo-compiler/src/schema/from_ast.rs) that allows building a schema from multiple source files incrementally.

We need to implement an ExecutableDocumentBuilder that mirrors the SchemaBuilder pattern, allowing users to:

  1. Build executable documents from multiple source files
  2. Accumulate operations and fragments across multiple AST documents
  3. Handle collisions and errors gracefully
  4. Preserve source information for all definitions

Key Design Parallels with SchemaBuilder

SchemaBuilder Structure (lines 7-15 of schema/from_ast.rs)

pub struct SchemaBuilder {
    adopt_orphan_extensions: bool,
    ignore_builtin_redefinitions: bool,
    pub(crate) schema: Schema,
    schema_definition: SchemaDefinitionStatus,
    orphan_type_extensions: IndexMap<Name, Vec<ast::Definition>>,
    pub(crate) errors: DiagnosticList,
}

Proposed ExecutableDocumentBuilder Structure

pub struct ExecutableDocumentBuilder {
    pub(crate) document: ExecutableDocument,
    schema: Option<Schema>,  // Optional schema for type checking
    pub(crate) errors: DiagnosticList,
}

Implementation Steps

1. Create ExecutableDocumentBuilder Struct

File: crates/apollo-compiler/src/executable/from_ast.rs

Add the builder struct at the beginning of the file:

  • Store the in-progress ExecutableDocument
  • Store an optional Schema reference for validation during building
  • Store accumulated DiagnosticList
  • NO configuration flags needed (unlike SchemaBuilder which has adopt_orphan_extensions and ignore_builtin_redefinitions)

2. Implement Builder Constructor and Configuration

File: crates/apollo-compiler/src/executable/from_ast.rs

Add methods:

  • new(schema: Option<&Schema>) -> Self - Create a new builder with optional schema
  • Similar to SchemaBuilder::new() which clones built-in types (lines 69-71 of schema/from_ast.rs)

3. Refactor document_from_ast to Use Builder Pattern

File: crates/apollo-compiler/src/executable/from_ast.rs

Current function signature (line 9):

pub(crate) fn document_from_ast(
    schema: Option<&Schema>,
    document: &ast::Document,
    errors: &mut DiagnosticList,
    type_system_definitions_are_errors: bool,
) -> ExecutableDocument

Refactor to:

  • Keep existing function as a convenience wrapper that creates a builder internally
  • Create new ExecutableDocumentBuilder::add_ast_document(&mut self, document: &ast::Document, type_system_definitions_are_errors: bool)
  • Move the document building logic (lines 15-125) into the builder's add_ast_document method

Key differences from current implementation:

  • Instead of returning ExecutableDocument immediately, accumulate into self.document
  • Handle operation name collisions (lines 31-60) by checking self.document.operations
  • Handle fragment name collisions (lines 93-106) by checking self.document.fragments
  • Accumulate source maps into self.document.sources

4. Implement Core Builder Methods

File: crates/apollo-compiler/src/executable/from_ast.rs

Add methods mirroring SchemaBuilder:

a) add_ast_document (similar to lines 106-269 of schema/from_ast.rs)

  • Take &ast::Document and process all executable definitions
  • Accumulate operations into self.document.operations:
    • Named operations go into operations.named
    • Anonymous operations go into operations.anonymous
    • Detect and report collisions
  • Accumulate fragments into self.document.fragments
  • Report type system definitions as errors if type_system_definitions_are_errors is true
  • Merge source maps from document.sources into self.document.sources

b) build (similar to lines 277-280 of schema/from_ast.rs)

pub fn build(self) -> Result<ExecutableDocument, WithErrors<ExecutableDocument>>
  • Call build_inner() and convert errors to result

c) build_inner (similar to lines 282-349 of schema/from_ast.rs)

pub(crate) fn build_inner(self) -> (ExecutableDocument, DiagnosticList)
  • Return the accumulated document and error list
  • NO orphan handling needed (no executable extensions concept)

5. Update ExecutableDocument API

File: crates/apollo-compiler/src/executable/mod.rs

Add a builder() method (similar to lines 439-441 of schema/mod.rs):

pub fn builder(schema: Option<&Valid<Schema>>) -> ExecutableDocumentBuilder {
    ExecutableDocumentBuilder::new(schema)
}

Consider deprecating or updating the existing parse method to internally use the builder.

6. Update Parser Integration

File: crates/apollo-compiler/src/parser.rs

Add new method similar to parse_into_schema_builder:

pub fn parse_into_executable_builder(
    &mut self,
    schema: Option<&Valid<Schema>>,
    source_text: impl Into<String>,
    path: impl AsRef<Path>,
    builder: &mut ExecutableDocumentBuilder,
)

Update parse_executable_inner (lines 261-271) to optionally use the builder pattern.

7. Export Builder in Public API

File: crates/apollo-compiler/src/executable/mod.rs

Add to the public exports (around line 71):

pub use crate::executable::from_ast::ExecutableDocumentBuilder;

8. Add Documentation

Add comprehensive documentation for:

  • ExecutableDocumentBuilder struct
  • All public builder methods
  • Example usage showing multi-file document building
  • Migration guide from document_from_ast to builder pattern

9. Update Tests

File: Create crates/apollo-compiler/src/executable/from_ast_tests.rs or update existing tests

Add tests for:

  • Building document from multiple sources
  • Handling operation name collisions across files
  • Handling fragment name collisions across files
  • Source map preservation across multiple files
  • Mixed anonymous and named operations
  • Type system definitions in executable documents (error cases)

Key Differences from SchemaBuilder

  1. No Extensions: ExecutableDocument doesn't have the concept of extensions like Schema does, so no orphan handling
  2. No Built-ins: No built-in operations or fragments to initialize
  3. Simpler Collision Handling: Only need to track operation and fragment name collisions
  4. Schema is Optional: Schema is only needed for type validation, not for structure

Migration Path

For backwards compatibility:

  1. Keep document_from_ast as a public API
  2. Internally refactor it to use ExecutableDocumentBuilder
  3. Add deprecation notice recommending builder pattern for multi-file use cases
  4. Existing code continues to work unchanged

Benefits

  1. Multi-file Support: Users can build executable documents from multiple GraphQL files
  2. Consistency: Mirrors the SchemaBuilder API pattern users already know
  3. Flexibility: Optional schema allows building with or without type checking
  4. Better Error Handling: Accumulate all errors across files before returning
  5. Source Tracking: Properly track which file each operation/fragment came from

Example Usage

use apollo_compiler::{Schema, ExecutableDocument};

// Parse schema
let schema = Schema::parse_and_validate(schema_sdl, "schema.graphql")?;

// Build executable document from multiple files
let mut builder = ExecutableDocument::builder(Some(&schema));
Parser::new().parse_into_executable_builder(
    Some(&schema),
    query1_source,
    "query1.graphql",
    &mut builder
);
Parser::new().parse_into_executable_builder(
    Some(&schema),
    query2_source,
    "query2.graphql",
    &mut builder
);

let document = builder.build()?;

Files to Modify

  1. crates/apollo-compiler/src/executable/from_ast.rs - Main implementation
  2. crates/apollo-compiler/src/executable/mod.rs - Public API and exports
  3. crates/apollo-compiler/src/parser.rs - Parser integration
  4. Test files - Comprehensive test coverage

Estimated Complexity

  • Low Risk: Pattern is well-established by SchemaBuilder
  • Medium Complexity: ~300-400 lines of new code
  • High Value: Enables multi-file executable document composition

In frontend projects, operation documents are commonly created from multiple sources. Fragment definitions are often treated as global and stitched in by clients.

Example:
Fragment def
Usage

In order to build tooling that accommodates this common use case, we should be able to build ExecutableDocuments from multiple sources. Fortunately, we already accommodate a parallel use case for schema via the SchemaBuilder, so this implementation leans heavily on that pattern and prior art.

This change largely extracts the functionality out of document_from_ast and into ExecutableDocumentBuilder.

Add ExecutableDocumentBuilder following the same pattern as SchemaBuilder,
enabling users to build executable documents from multiple source files.

Key features:
- Build executable documents from multiple GraphQL files
- Proper collision detection for operations and fragments across files
- Optional schema validation during building
- Source tracking for all definitions
- Backwards compatible with existing document_from_ast function

API additions:
- ExecutableDocumentBuilder struct with new(), add_ast_document(), and build() methods
- ExecutableDocument::builder() convenience method
- Parser::parse_into_executable_builder() for multi-file parsing

Example usage:
```rust
let mut builder = ExecutableDocument::builder(Some(&schema));
Parser::new().parse_into_executable_builder(
    Some(&schema),
    "query GetUser { user { id } }",
    "query1.graphql",
    &mut builder,
);
Parser::new().parse_into_executable_builder(
    Some(&schema),
    "query GetPost { post { title } }",
    "query2.graphql",
    &mut builder,
);
let document = builder.build().unwrap();
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@trevor-scheer trevor-scheer requested a review from a team as a code owner November 25, 2025 15:19
Add 8 new tests covering:
- Building documents from multiple files
- Combining queries and fragments from separate files
- Operation name collision detection across files
- Fragment name collision detection across files
- Building without a schema (validation-free mode)
- Source information preservation
- Anonymous/named operation mixing (error case)
- Multiple fragments in single query

All tests pass successfully, verifying the builder handles
multi-file scenarios correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@trevor-scheer trevor-scheer force-pushed the executable_document_builder branch from e830b14 to 04b9622 Compare November 25, 2025 15:24
Copy link
Copy Markdown
Contributor

@tninesling tninesling left a comment

Choose a reason for hiding this comment

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

This is looking good, I just have a few suggestions.

Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/parser.rs Outdated
Comment thread crates/apollo-compiler/tests/executable.rs Outdated
Comment thread crates/apollo-compiler/tests/executable.rs
@trevor-scheer
Copy link
Copy Markdown
Contributor Author

Thanks for the speedy review @tninesling! And nice to see ya, so to speak 😄

@trevor-scheer
Copy link
Copy Markdown
Contributor Author

@tninesling I spotted this issue comment from @lrlna this morning - we should see if they'd prefer this not land.

Copy link
Copy Markdown
Contributor

@tninesling tninesling left a comment

Choose a reason for hiding this comment

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

I think this is a nice ergonomic improvement to the API, and the changes look good with the recent adjustments. I don't think @lrlna has had a chance to take a detailed look, and it looks like they're out until next week. Feel free to add them as a reviewer if you're looking for their specific feedback, but from my perspective, this is good to go.

@trevor-scheer
Copy link
Copy Markdown
Contributor Author

@tninesling Ultimately your call. I'm happy for this to land but also I'm completely unblocked on using this, just building against my branch for now. I'd err on hearing their input in case they had any particular hesitations or alternatives in mind!

@trevor-scheer
Copy link
Copy Markdown
Contributor Author

@tninesling i don't have the perms to add reviewers, would you mind adding them, or bring this to their attention?

A happy middle ground option: introduce this API as experimental/feature-gated until @lrlna has the opportunity to provide their feedback.

I'm still in no rush, but just don't want this to go by the wayside forever either. Thanks!

@trevor-scheer
Copy link
Copy Markdown
Contributor Author

Thanks for the review @TylerBloom. I'm ready for this to land if y'all are. I'm still actively using this (and stuck on my fork)!

Copy link
Copy Markdown
Member

@lrlna lrlna left a comment

Choose a reason for hiding this comment

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

This is really close! It just needs slightly more tightened up doc comments, and a test for multiple anonymous operations. Thank you for your patience!

Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/from_ast.rs Outdated
Comment thread crates/apollo-compiler/src/executable/mod.rs Outdated
Comment thread crates/apollo-compiler/src/executable/mod.rs Outdated
Comment thread crates/apollo-compiler/src/parser.rs Outdated
}

#[test]
fn builder_handles_anonymous_and_named_operations() {
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.

This needs a sibling test for multiple anonymous operations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@trevor-scheer
Copy link
Copy Markdown
Contributor Author

Thanks @lrlna! Should be able to address feedback in the next ~few days.

@trevor-scheer trevor-scheer requested a review from lrlna March 31, 2026 03:14
Copy link
Copy Markdown
Member

@lrlna lrlna left a comment

Choose a reason for hiding this comment

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

Thank you so much @trevor-scheer! This looks good to me!

@trevor-scheer
Copy link
Copy Markdown
Contributor Author

@lrlna does this need anything else to be merged?

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.

4 participants