diff --git a/firestore/pipeline_constant.go b/firestore/pipeline_constant.go index edf55dc598d1..f48c6e552baa 100644 --- a/firestore/pipeline_constant.go +++ b/firestore/pipeline_constant.go @@ -56,6 +56,20 @@ func ConstantOf(value any) Expression { return &constant{baseExpression: &baseExpression{err: err}} } return &constant{baseExpression: &baseExpression{pbVal: pbVal}} + } + + // Safely fall back to arrays/slices + v := reflect.ValueOf(value) + if !v.IsValid() { + return &constant{baseExpression: &baseExpression{err: fmt.Errorf("firestore: unknown constant type: %T", value)}} + } + switch v.Kind() { + case reflect.Slice, reflect.Array: + pbVal, _, err := toProtoValue(v) + if err != nil { + return &constant{baseExpression: &baseExpression{err: err}} + } + return &constant{baseExpression: &baseExpression{pbVal: pbVal}} default: return &constant{baseExpression: &baseExpression{err: fmt.Errorf("firestore: unknown constant type: %T", value)}} } diff --git a/firestore/pipeline_constant_test.go b/firestore/pipeline_constant_test.go new file mode 100644 index 000000000000..0b4631309e9e --- /dev/null +++ b/firestore/pipeline_constant_test.go @@ -0,0 +1,55 @@ +// 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 ( + "testing" +) + +func TestConstantOf_SlicesAndArrays(t *testing.T) { + tests := []struct { + name string + input any + }{ + { + name: "slice of ints", + input: []int{1, 2, 3}, + }, + { + name: "array of ints", + input: [3]int{1, 2, 3}, + }, + { + name: "slice of strings", + input: []string{"a", "b", "c"}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + expr := ConstantOf(tc.input) + if expr == nil { + t.Fatalf("ConstantOf returned nil") + } + + pbVal, err := expr.toProto() + if err != nil { + t.Fatalf("toProto() failed with error: %v", err) + } + if pbVal == nil { + t.Fatalf("expected non-nil pb.Value") + } + }) + } +} diff --git a/firestore/to_value.go b/firestore/to_value.go index 71fb9db9ca49..27e4070786d9 100644 --- a/firestore/to_value.go +++ b/firestore/to_value.go @@ -81,6 +81,15 @@ func toProtoValue(v reflect.Value) (pbv *pb.Value, sawTransform bool, err error) case Expression: pbVal, err := exprToProtoValue(x) return pbVal, false, err + case AggregateFunction: + if x == nil { + return nullValue, false, nil + } + if v := reflect.ValueOf(x); v.Kind() == reflect.Ptr && v.IsNil() { + return nullValue, false, nil + } + pbVal, err := x.toProto() + return pbVal, false, err case Vector64: return vectorToProtoValue(x), false, nil case *latlng.LatLng: diff --git a/firestore/to_value_test.go b/firestore/to_value_test.go index b4580ea27bc5..ac48cdc6f7f3 100644 --- a/firestore/to_value_test.go +++ b/firestore/to_value_test.go @@ -118,6 +118,25 @@ func TestToProtoValue_Conversions(t *testing.T) { in: (*DocumentRef)(nil), want: nullValue, }, + { + desc: "nil AggregateFunction", + in: AggregateFunction(nil), + want: nullValue, + }, + { + desc: "nil pointer to AggregateFunction", + in: (*baseAggregateFunction)(nil), + want: nullValue, + }, + { + desc: "AggregateFunction", + in: CountAll(), + want: &pb.Value{ValueType: &pb.Value_FunctionValue{ + FunctionValue: &pb.Function{ + Name: "count", + }, + }}, + }, { desc: "bool", in: true,