Skip to content

refactor(firestore): fix aliasing#14440

Merged
bhshkh merged 2 commits into
googleapis:mainfrom
bhshkh:refactor/fspq-fix-alias
Apr 14, 2026
Merged

refactor(firestore): fix aliasing#14440
bhshkh merged 2 commits into
googleapis:mainfrom
bhshkh:refactor/fspq-fix-alias

Conversation

@bhshkh
Copy link
Copy Markdown
Contributor

@bhshkh bhshkh commented Apr 14, 2026

Redesigning aliasing

@bhshkh bhshkh requested review from a team as code owners April 14, 2026 21:22
@product-auto-label product-auto-label Bot added the api: firestore Issues related to the Firestore API. label Apr 14, 2026
@bhshkh
Copy link
Copy Markdown
Contributor Author

bhshkh commented Apr 14, 2026

The Problem

Initially, the Go SDK utilized struct embedding for AliasedExpression and AliasedAggregate:

type AliasedExpression struct {
	*baseExpression
	alias string
}

Because of Go's embedding rules, AliasedExpression automatically inherited all methods from *baseExpression, including .As(), .Add(), .Multiply(), etc. This created a significant API trap where users could write nonsensical code:

// Invalid logic but compiled successfully:
expr := firestore.Constant(1).As("number").Add(2)

In the example above, calling .Add(2) executed on the embedded *baseExpression. It returned a brand new Expression representing the addition, silently dropping the "number" alias.

Furthermore, users could chain aliases indefinitely (expr.As("a").As("b")), which would continually discard the previous alias.

The Solution

To ensure compile-time safety and semantic clarity, the Go implementation is updated to remove struct embedding entirely.

AliasedExpression and AliasedAggregate now use named fields to hold their underlying expressions and only implement the Selectable interface (or their respective interface constraints):

type AliasedExpression struct {
	expr  Expression
	alias string
}
// Does NOT implement Expression interface methods like Add(), Multiply(), or As()

Benefits of this Approach:

  1. Compile-Time Safety: Chaining aliases like expr.As("a").As("b") results in an immediate compiler error (AliasedExpression has no field or method As).
  2. Prevents Dropped Aliases: Users can no longer accidentally call math or string functions on an aliased expression, preventing bugs where aliases vanish from the pipeline query.
  3. Semantic Clarity: Conceptually, an alias is the final projection step in a Select or AddFields stage. It should not be treated as an intermediate Expression anymore.

Cross-SDK Comparison of As() Behavior

The implementations of As() varied significantly across the different Firestore SDKs. The Go SDK has intentionally aligned with the strictness of the Node.js SDK.

SDK As().As() Chaining Behavior Implementation Details Intentional Design?
Go (Current) Compile Error AliasedExpression is strictly a Selectable and does not expose As() or any Expression methods. Limits alias to exactly 1 per expression. Yes. Provides maximum type-safety and prevents accidental method calls on projections.
Node.js Compile/Runtime Error AliasedExpression does not implement an as() method and isn't a child of Expression. Limits alias to exactly 1. Yes. Strictly enforces a single alias per expression.
Java Replaces previous alias Explicitly implements as() on AliasedExpression. It unwraps the first alias and replaces it. Limits chaining to 2 calls because the return type of the second call is a Selectable interface (which lacks an as() method). Yes, but slightly awkward as it requires an intermediate return type change to cap the chaining.
Python Nests aliases indefinitely AliasedExpression inherits from Expression (via Selectable). Calling as_() wraps the existing AliasedExpression recursively. This produces nested map structures in the Protocol Buffer (e.g., {"test": {"number": 1}}). Likely No. This seems to be an unintended side effect of inheritance, creating potentially invalid proto payloads.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors AliasedAggregate and AliasedExpression by replacing embedded base structs with explicit interface fields. The review feedback highlights opportunities to improve consistency between these two types, specifically by ensuring AliasedAggregate stores the full AggregateFunction interface and providing a getter method for its unexported fields to match the design of AliasedExpression.

Comment thread firestore/pipeline_aggregate.go
Comment thread firestore/pipeline_aggregate.go
Copy link
Copy Markdown
Contributor

@daniel-sanche daniel-sanche left a comment

Choose a reason for hiding this comment

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

LGTM

@bhshkh bhshkh merged commit c887413 into googleapis:main Apr 14, 2026
11 checks passed
@bhshkh bhshkh deleted the refactor/fspq-fix-alias branch April 14, 2026 21:54
daniel-sanche added a commit to googleapis/google-cloud-python that referenced this pull request Apr 21, 2026
Currently, AliasedExpressions are treated like regular expressions. You
can execute additional expressions off of them
(`a.as_("number").add(5)`), or chain them
(`a.as_("first").as_("second")`). But the backend doesn't actually
support aliases being used in this way

This PR raises an exception if an alias is used in a context it doesn't
support


Go version: googleapis/google-cloud-go#14440
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: firestore Issues related to the Firestore API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants