Skip to content

feat: generate smaller variable names#1036

Open
Aenimus wants to merge 8 commits intomasterfrom
david/eng-6267-add-new-variable-name-generator
Open

feat: generate smaller variable names#1036
Aenimus wants to merge 8 commits intomasterfrom
david/eng-6267-add-new-variable-name-generator

Conversation

@Aenimus
Copy link
Copy Markdown
Member

@Aenimus Aenimus commented Jan 19, 2025

Currently, the 27th variable will be aa but the 53rd will be aaa. This means the size of the variable grows quickly.

In this approach, the variable names grows much slower:
a, ..., z, aa, ..., az, ba, ..., zz, aaa, aab, ...

2 chars at 27 variables,
3 chars at 703 variables,
4 chars at 18279 variables, etc.

Summary by CodeRabbit

  • New Features

    • Improved variable name generation for GraphQL operations with a more efficient sequential letter-based approach.
  • Tests

    • Added comprehensive test coverage for the new variable name generation functionality, including edge cases for large-scale variable sequences.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new V2 variable name generation mechanism for GraphQL operation definitions. It adds a LetterIndices type that implements base-26-style letter progression to generate unused variable definition names, along with a corresponding GenerateUnusedVariableDefinitionNameV2 method on the Document type. The implementation replaces the prior length-based nested-loop approach with iterative letter sequence generation.

Changes

Cohort / File(s) Summary
Variable Name Generation V2
v2/pkg/ast/ast_operation_definition.go
Added GenerateUnusedVariableDefinitionNameV2 method to Document type, which iteratively generates candidate variable names using a new LetterIndices type. Introduced LetterIndices struct with base-26 letter progression logic (Increment, incrementAt, resetAt, Bytes, Render methods) and two constructors (NewLetterIndices, NewDefaultLetterIndices).
Test Coverage
v2/pkg/ast/ast_operation_definition_test.go
Added comprehensive test cases validating LetterIndices.Increment() behavior across various starting combinations and multi-letter rollover scenarios. Included helper function schemaString(varNumber int) that generates large GraphQL query strings with variable names rendered via LetterIndices. Tests verify the new V2 generator returns expected variable names for multiple large input values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: generate smaller variable names' clearly and concisely describes the main change—implementing a new V2 variable name generator that produces lexicographically ordered names growing more slowly in length.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch david/eng-6267-add-new-variable-name-generator

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v2/pkg/ast/ast_operation_definition_test.go (1)

3-12: ⚠️ Potential issue | 🟡 Minor

Fix the import grouping to unblock CI.

gci is already failing on this block. The ast import is mixed into the stdlib section, so this file will not pass formatting as-is.

Suggested import order
 import (
 	"fmt"
-	"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
 	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 
+	"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
 	"github.com/wundergraph/graphql-go-tools/v2/pkg/internal/unsafeparser"
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/ast/ast_operation_definition_test.go` around lines 3 - 12, The import
block mixes a non-stdlib import
(github.com/wundergraph/graphql-go-tools/v2/pkg/ast) into the stdlib group and
fails gci; reorder the imports so stdlib packages (fmt, strings, testing) come
first, then third-party packages
(github.com/wundergraph/graphql-go-tools/v2/pkg/ast and
github.com/stretchr/testify/assert), and finally the internal package
(github.com/wundergraph/graphql-go-tools/v2/pkg/internal/unsafeparser), with a
blank line between each group to match gci formatting.
🧹 Nitpick comments (3)
v2/pkg/ast/ast_operation_definition.go (2)

157-166: This lookup pattern can go quadratic on large operations.

GenerateUnusedVariableDefinitionNameV2 probes every candidate from a upward and does an existence lookup each time. For the large cases this PR explicitly targets, that can get expensive quickly if VariableDefinitionByNameAndOperation is scanning the operation’s variable list. Building a used-name set once per call would keep the new sequence without turning the hot path into repeated linear searches.

One way to keep this O(n)
 func (d *Document) GenerateUnusedVariableDefinitionNameV2(operationDefinition int) []byte {
+	used := make(map[string]struct{}, len(d.OperationDefinitions[operationDefinition].VariableDefinitions.Refs))
+	for _, ref := range d.OperationDefinitions[operationDefinition].VariableDefinitions.Refs {
+		value := d.VariableDefinitions[ref].VariableValue.Ref
+		used[d.VariableValueNameString(value)] = struct{}{}
+	}
+
 	l := NewDefaultLetterIndices()
 	for {
-		bytes := l.Bytes()
-		if _, exists := d.VariableDefinitionByNameAndOperation(operationDefinition, bytes); !exists {
-			return bytes
+		name := l.Render()
+		if _, exists := used[name]; !exists {
+			return []byte(name)
 		}
 		l.Increment()
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/ast/ast_operation_definition.go` around lines 157 - 166,
GenerateUnusedVariableDefinitionNameV2 currently probes every candidate and
calls VariableDefinitionByNameAndOperation repeatedly, which can be quadratic;
instead, iterate once over the operation’s variable definitions to build a
set/map of used names (e.g., map[string]struct{}) and then use
NewDefaultLetterIndices/Bytes/Increment to produce candidates, checking
membership against that set until an unused name is found; update the function
to reference GenerateUnusedVariableDefinitionNameV2,
VariableDefinitionByNameAndOperation (only for building the set or directly
iterate variable list), NewDefaultLetterIndices, Bytes, and Increment
accordingly so lookups are O(1) per candidate.

168-214: Avoid exporting LetterIndices as a mutable public contract.

This helper looks like an implementation detail of the generator, but LetterIndices, NewLetterIndices, NewDefaultLetterIndices, Bytes, and Render are now part of the package API. That also exposes a fragile invariant: callers can pass mismatched indices/bytes slices or mutate the returned/input slices and leave the instance in an invalid state. If external construction is not required, I’d keep this type unexported; otherwise the constructor should at least copy and validate its inputs and avoid returning the internal buffer directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/ast/ast_operation_definition.go` around lines 168 - 214, The
LetterIndices helper is leaking mutable internals and a fragile invariant;
either make the type unexported (rename LetterIndices -> letterIndices) and keep
constructors private, or harden the public API: in NewLetterIndices validate
that len(indices)==len(bytes), copy both input slices into a new struct, and in
Bytes/Render return copies (Bytes should return a new slice copy) so callers
can't mutate internal buffers; also ensure NewDefaultLetterIndices returns a
safe copied instance and keep methods (Increment, incrementAt, resetAt,
maxIndex) operating on the internal fields only.
v2/pkg/ast/ast_operation_definition_test.go (1)

110-128: Add one sparse-name regression case.

These assertions only cover contiguous prefixes (a..z, a..zz, ...), so an implementation that just returns “next after max” would still pass. A case like ($a, $c)b would lock in the actual contract: first unused name, not highest+1.

Example test to add
+	t.Run("fills the first gap, not just the tail", func(t *testing.T) {
+		op := unsafeparser.ParseGraphqlDocumentString(`query ($a: Int! $c: Int!) {
+	fielda(arg: $a)
+	fieldc(arg: $c)
+}`)
+		assert.Equal(t, "b", string(op.GenerateUnusedVariableDefinitionNameV2(0)))
+	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v2/pkg/ast/ast_operation_definition_test.go` around lines 110 - 128, The
tests for GenerateUnusedVariableDefinitionNameV2 only check contiguous sequences
and miss sparse cases; add a test using unsafeparser.ParseGraphqlDocumentString
with a schemaString that defines non-contiguous variable names (e.g., variables
"a" and "c") and assert that op.GenerateUnusedVariableDefinitionNameV2(0)
returns "b" to ensure the function picks the first unused name rather than
simply returning the next after the max; place this new t.Run alongside the
existing cases and reference the same helpers (schemaString,
unsafeparser.ParseGraphqlDocumentString, GenerateUnusedVariableDefinitionNameV2)
so the regression is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@v2/pkg/ast/ast_operation_definition_test.go`:
- Around line 3-12: The import block mixes a non-stdlib import
(github.com/wundergraph/graphql-go-tools/v2/pkg/ast) into the stdlib group and
fails gci; reorder the imports so stdlib packages (fmt, strings, testing) come
first, then third-party packages
(github.com/wundergraph/graphql-go-tools/v2/pkg/ast and
github.com/stretchr/testify/assert), and finally the internal package
(github.com/wundergraph/graphql-go-tools/v2/pkg/internal/unsafeparser), with a
blank line between each group to match gci formatting.

---

Nitpick comments:
In `@v2/pkg/ast/ast_operation_definition_test.go`:
- Around line 110-128: The tests for GenerateUnusedVariableDefinitionNameV2 only
check contiguous sequences and miss sparse cases; add a test using
unsafeparser.ParseGraphqlDocumentString with a schemaString that defines
non-contiguous variable names (e.g., variables "a" and "c") and assert that
op.GenerateUnusedVariableDefinitionNameV2(0) returns "b" to ensure the function
picks the first unused name rather than simply returning the next after the max;
place this new t.Run alongside the existing cases and reference the same helpers
(schemaString, unsafeparser.ParseGraphqlDocumentString,
GenerateUnusedVariableDefinitionNameV2) so the regression is covered.

In `@v2/pkg/ast/ast_operation_definition.go`:
- Around line 157-166: GenerateUnusedVariableDefinitionNameV2 currently probes
every candidate and calls VariableDefinitionByNameAndOperation repeatedly, which
can be quadratic; instead, iterate once over the operation’s variable
definitions to build a set/map of used names (e.g., map[string]struct{}) and
then use NewDefaultLetterIndices/Bytes/Increment to produce candidates, checking
membership against that set until an unused name is found; update the function
to reference GenerateUnusedVariableDefinitionNameV2,
VariableDefinitionByNameAndOperation (only for building the set or directly
iterate variable list), NewDefaultLetterIndices, Bytes, and Increment
accordingly so lookups are O(1) per candidate.
- Around line 168-214: The LetterIndices helper is leaking mutable internals and
a fragile invariant; either make the type unexported (rename LetterIndices ->
letterIndices) and keep constructors private, or harden the public API: in
NewLetterIndices validate that len(indices)==len(bytes), copy both input slices
into a new struct, and in Bytes/Render return copies (Bytes should return a new
slice copy) so callers can't mutate internal buffers; also ensure
NewDefaultLetterIndices returns a safe copied instance and keep methods
(Increment, incrementAt, resetAt, maxIndex) operating on the internal fields
only.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a388bbae-2720-4013-b1e5-9e3884da1fa2

📥 Commits

Reviewing files that changed from the base of the PR and between 18b292e and 547cfae.

📒 Files selected for processing (2)
  • v2/pkg/ast/ast_operation_definition.go
  • v2/pkg/ast/ast_operation_definition_test.go

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.

3 participants