Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions firestore/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,14 @@ func WithIndexMode(indexMode string) ExecuteOption {
}

// Execute executes the pipeline and returns an iterator for streaming the results.
Comment thread
bhshkh marked this conversation as resolved.
Outdated
func (p *Pipeline) Execute(ctx context.Context) *PipelineResultIterator {
func (p *Pipeline) Execute(ctx context.Context) *PipelineSnapshot {
ctx = withResourceHeader(ctx, p.c.path())
ctx = withRequestParamsHeader(ctx, reqParamsHeaderVal(p.c.path()))

return &PipelineResultIterator{
iter: newStreamPipelineResultIterator(ctx, p),
return &PipelineSnapshot{
iter: &PipelineResultIterator{
iter: newStreamPipelineResultIterator(ctx, p),
},
}
}

Expand Down
57 changes: 0 additions & 57 deletions firestore/pipeline_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ import (
"google.golang.org/api/iterator"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)

// PipelineResult is a result returned from executing a pipeline.
Expand Down Expand Up @@ -178,61 +176,6 @@ func (it *PipelineResultIterator) GetAll() ([]*PipelineResult, error) {
return results, nil
}

// ExplainStats returns stats from query explain.
// If [WithExplainMode] was set to [ExplainModeExplain] or left unset, then this returns nil
func (it *PipelineResultIterator) ExplainStats() *ExplainStats {
if it == nil {
return &ExplainStats{err: errors.New("firestore: iterator is nil")}
}
if it.err == nil || it.err != iterator.Done {
return &ExplainStats{err: errStatsBeforeEnd}
}
statsPb, statsErr := it.iter.getExplainStats()
return &ExplainStats{statsPb: statsPb, err: statsErr}
}

// ExplainStats is query explain stats.
//
// Contains all metadata related to pipeline planning and execution, specific
// contents depend on the supplied pipeline options.
type ExplainStats struct {
statsPb *pb.ExplainStats
err error
}

// GetRawData returns the explain stats in an encoded proto format, as returned from the Firestore backend.
// The caller is responsible for unpacking this proto message.
func (es *ExplainStats) GetRawData() (*anypb.Any, error) {
if es.err != nil {
return nil, es.err
}
if es.statsPb == nil {
return nil, nil
}

return es.statsPb.GetData(), nil
}

// GetText returns the explain stats string verbatim as returned from the Firestore backend
// when explain stats were requested with `outputFormat = 'text'`, this
// If explain stats were requested with `outputFormat = 'json'`, this returns the explain stats
// as stringified JSON, which was returned from the Firestore backend.
func (es *ExplainStats) GetText() (string, error) {
if es.err != nil {
return "", es.err
}
if es.statsPb == nil || es.statsPb.GetData() == nil {
return "", nil
}

var data wrapperspb.StringValue
if err := es.statsPb.GetData().UnmarshalTo(&data); err != nil {
return "", fmt.Errorf("firestore: failed to unmarshal Any to wrapperspb.StringValue: %w", err)
}

return data.GetValue(), nil
}

// pipelineResultIteratorInternal is an unexported interface defining the core iteration logic.
type pipelineResultIteratorInternal interface {
next() (*PipelineResult, error)
Expand Down
34 changes: 17 additions & 17 deletions firestore/pipeline_result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,14 @@ func TestPipelineResultIterator_ExplainStats(t *testing.T) {
p: p,
statsPb: statsTextPb,
}
publicIter := &PipelineResultIterator{iter: mockIter, err: iterator.Done} // Pre-set to done
ps := &PipelineSnapshot{&PipelineResultIterator{iter: mockIter, err: iterator.Done}} // Pre-set to done

stats := publicIter.ExplainStats()
stats := ps.ExplainStats()
if stats.err != nil {
t.Fatalf("ExplainStats() error: %v", stats.err)
}

text, err := stats.GetText()
text, err := stats.Text()
if err != nil {
t.Fatalf("GetText() error: %v", err)
}
Expand All @@ -428,14 +428,14 @@ func TestPipelineResultIterator_ExplainStats(t *testing.T) {
p: p,
statsPb: statsRawPb,
}
publicIter := &PipelineResultIterator{iter: mockIter, err: iterator.Done}
ps := &PipelineSnapshot{&PipelineResultIterator{iter: mockIter, err: iterator.Done}}

stats := publicIter.ExplainStats()
stats := ps.ExplainStats()
if stats.err != nil {
t.Fatalf("ExplainStats() error: %v", stats.err)
}

rawData, err := stats.GetRawData()
rawData, err := stats.RawData()
if err != nil {
t.Fatalf("GetRawData() error: %v", err)
}
Expand All @@ -446,9 +446,9 @@ func TestPipelineResultIterator_ExplainStats(t *testing.T) {

t.Run("error case - iterator not done", func(t *testing.T) {
mockIter := &streamPipelineResultIterator{}
publicIter := &PipelineResultIterator{iter: mockIter} // err is nil
ps := &PipelineSnapshot{&PipelineResultIterator{iter: mockIter}} // err is nil

stats := publicIter.ExplainStats()
stats := ps.ExplainStats()
if stats.err == nil {
t.Fatal("ExplainStats() expected error, got nil")
}
Expand All @@ -458,42 +458,42 @@ func TestPipelineResultIterator_ExplainStats(t *testing.T) {
})

t.Run("error case - iterator is nil", func(t *testing.T) {
var publicIter *PipelineResultIterator
stats := publicIter.ExplainStats()
var ps *PipelineSnapshot
stats := ps.ExplainStats()
if stats.err == nil {
t.Fatal("ExplainStats() on nil iterator expected error, got nil")
}
})

t.Run("error case - GetText with wrong data type", func(t *testing.T) {
mockIter := &streamPipelineResultIterator{statsPb: statsRawPb}
publicIter := &PipelineResultIterator{iter: mockIter, err: iterator.Done}
ps := &PipelineSnapshot{&PipelineResultIterator{iter: mockIter, err: iterator.Done}}

stats := publicIter.ExplainStats()
_, err := stats.GetText()
stats := ps.ExplainStats()
_, err := stats.Text()
if err == nil {
t.Fatal("GetText() with wrong data type expected error, got nil")
}
})

t.Run("no stats available", func(t *testing.T) {
mockIter := &streamPipelineResultIterator{statsPb: nil} // No stats
publicIter := &PipelineResultIterator{iter: mockIter, err: iterator.Done}
ps := &PipelineSnapshot{&PipelineResultIterator{iter: mockIter, err: iterator.Done}}

stats := publicIter.ExplainStats()
stats := ps.ExplainStats()
if stats.err != nil {
t.Fatalf("ExplainStats() error: %v", stats.err)
}

text, err := stats.GetText()
text, err := stats.Text()
if err != nil {
t.Fatalf("GetText() error: %v", err)
}
if text != "" {
t.Errorf("GetText(): got %q, want empty string", text)
}

rawData, err := stats.GetRawData()
rawData, err := stats.RawData()
if err != nil {
t.Fatalf("GetRawData() error: %v", err)
}
Expand Down
91 changes: 91 additions & 0 deletions firestore/pipeline_snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package firestore

import (
"errors"
"fmt"

pb "cloud.google.com/go/firestore/apiv1/firestorepb"
"google.golang.org/api/iterator"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)

type PipelineSnapshot struct {

Check failure on line 27 in firestore/pipeline_snapshot.go

View workflow job for this annotation

GitHub Actions / vet

exported type PipelineSnapshot should have comment or be unexported
iter *PipelineResultIterator
}

func (ps *PipelineSnapshot) Results() *PipelineResultIterator {

Check failure on line 31 in firestore/pipeline_snapshot.go

View workflow job for this annotation

GitHub Actions / vet

exported method PipelineSnapshot.Results should have comment or be unexported
return ps.iter
}

// ExplainStats returns stats from query explain.
// If [WithExplainMode] was set to [ExplainModeExplain] or left unset, then no stats will be available.
func (ps *PipelineSnapshot) ExplainStats() *ExplainStats {
if ps == nil {
return &ExplainStats{err: errors.New("firestore: PipelineSnapshot is nil")}
}
if ps.iter == nil {
return &ExplainStats{err: errors.New("firestore: PipelineResultIterator is nil")}
}
if ps.iter.err == nil || ps.iter.err != iterator.Done {
return &ExplainStats{err: errStatsBeforeEnd}
}
statsPb, statsErr := ps.iter.iter.getExplainStats()
return &ExplainStats{statsPb: statsPb, err: statsErr}
}

// ExplainStats is query explain stats.
//
// Contains all metadata related to pipeline planning and execution, specific
// contents depend on the supplied pipeline options.
type ExplainStats struct {
statsPb *pb.ExplainStats
err error
}

// RawData returns the explain stats in an encoded proto format, as returned from the Firestore backend.
// The caller is responsible for unpacking this proto message.
func (es *ExplainStats) RawData() (*anypb.Any, error) {
if es.err != nil {
return nil, es.err
}
if es.statsPb == nil {
return nil, nil
}

return es.statsPb.GetData(), nil
}

// Text returns the explain stats string verbatim as returned from the Firestore backend
// when explain stats were requested with `outputFormat = 'text'`
// If explain stats were requested with `outputFormat = 'json'`, this returns the explain stats
// as stringified JSON, which was returned from the Firestore backend.
Comment thread
bhshkh marked this conversation as resolved.
Outdated
func (es *ExplainStats) Text() (string, error) {
if es.err != nil {
return "", es.err
}
if es.statsPb == nil || es.statsPb.GetData() == nil {
return "", nil
}

var data wrapperspb.StringValue
if err := es.statsPb.GetData().UnmarshalTo(&data); err != nil {
return "", fmt.Errorf("firestore: failed to unmarshal Any to wrapperspb.StringValue: %w", err)
}

return data.GetValue(), nil
}
6 changes: 4 additions & 2 deletions firestore/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,12 @@ func (t *Transaction) WithReadOptions(opts ...ReadOption) *Transaction {
}

// Execute runs the given pipeline in the context of the transaction.
func (t *Transaction) Execute(p *Pipeline) *PipelineResultIterator {
func (t *Transaction) Execute(p *Pipeline) *PipelineSnapshot {
if len(t.writes) > 0 {
t.readAfterWrite = true
return &PipelineResultIterator{err: errReadAfterWrite}
return &PipelineSnapshot{
iter: &PipelineResultIterator{err: errReadAfterWrite},
}
}
p2 := p.copy()
p2.tx = t
Expand Down
2 changes: 1 addition & 1 deletion firestore/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ func TestTransactionErrors(t *testing.T) {
return err
}
p := c.Pipeline().Collection("C").Select("x")
it := tx.Execute(p)
it := tx.Execute(p).Results()
it.Stop()
return it.err
})
Expand Down
Loading