This is an automated email from the ASF dual-hosted git repository.

jrmccluskey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new d11883d91a6 Add biquery query parameter option to support parametrize 
query (#39389)
d11883d91a6 is described below

commit d11883d91a66277ed0a29b24daa998412a2fbd45
Author: Makoto Nagai <[email protected]>
AuthorDate: Wed Sep 9 23:26:55 2026 +0900

    Add biquery query parameter option to support parametrize query (#39389)
    
    * Add biquery query parameters to support parametrize query
    
    * Use encoding/gob for encode/decode to preserve type information
    
    QueryParameter and QueryParameterValue contains filed with any type.
    Converting go types to encoding/json lose type informations some of go
    types.
    The registered types are ones supported in the implementation in
    paramValue method.
    
https://github.com/googleapis/google-cloud-go/blob/bigquery/v1.79.0/bigquery/params.go#L434
    
    * Update coder.go
    
    Fix typo
    
    ---------
    
    Co-authored-by: Jack McCluskey 
<[email protected]>
---
 sdks/go.mod                                        |   2 +-
 sdks/go/pkg/beam/io/bigqueryio/bigquery.go         |  32 +-
 sdks/go/pkg/beam/io/bigqueryio/bigquery_test.go    |  35 ++
 sdks/go/pkg/beam/io/bigqueryio/coder.go            |  88 ++++
 sdks/go/pkg/beam/io/bigqueryio/coder_test.go       | 495 +++++++++++++++++++++
 sdks/go/test/integration/integration.go            |   4 +-
 .../integration/io/bigqueryio/bigqueryio_test.go   | 182 +++++++-
 .../test/integration/io/bigqueryio/helper_test.go  |  73 ++-
 8 files changed, 878 insertions(+), 33 deletions(-)

diff --git a/sdks/go.mod b/sdks/go.mod
index 222aa328b1b..293bd34354e 100644
--- a/sdks/go.mod
+++ b/sdks/go.mod
@@ -69,6 +69,7 @@ require (
 )
 
 require (
+       cloud.google.com/go v0.123.0
        github.com/avast/retry-go/v4 v4.7.0
        github.com/fsouza/fake-gcs-server v1.56.1
        github.com/golang-cz/devslog v0.0.17
@@ -137,7 +138,6 @@ require (
 )
 
 require (
-       cloud.google.com/go v0.123.0 // indirect
        cloud.google.com/go/compute/metadata v0.9.0 // indirect
        cloud.google.com/go/iam v1.12.0 // indirect
        cloud.google.com/go/longrunning v1.2.0 // indirect
diff --git a/sdks/go/pkg/beam/io/bigqueryio/bigquery.go 
b/sdks/go/pkg/beam/io/bigqueryio/bigquery.go
index a80661e22d3..bbe951969b9 100644
--- a/sdks/go/pkg/beam/io/bigqueryio/bigquery.go
+++ b/sdks/go/pkg/beam/io/bigqueryio/bigquery.go
@@ -114,6 +114,11 @@ func constructSelectStatement(t reflect.Type, tagKey 
string, table string) strin
 type QueryOptions struct {
        // UseStandardSQL enables BigQuery's Standard SQL dialect when 
executing a query.
        UseStandardSQL bool
+       // Parameters are the query parameters for parameterized queries.
+       // In the current implementation, user-defines types are not supported 
in Value field.
+       // Use *bigquery.QueryParameterValue to build STRUCT/ARRAY parameters
+       // or use go primitive types explicitly.
+       parameters []bigquery.QueryParameter
 }
 
 // UseStandardSQL enables BigQuery's Standard SQL dialect when executing a 
query.
@@ -124,6 +129,15 @@ func UseStandardSQL() func(qo *QueryOptions) error {
        }
 }
 
+// WithQueryParameters sets the query parameters for parameterized queries.
+// See QueryOptions.parameters for the list of supported Value types.
+func WithQueryParameters(params ...bigquery.QueryParameter) func(qo 
*QueryOptions) error {
+       return func(qo *QueryOptions) error {
+               qo.parameters = params
+               return nil
+       }
+}
+
 // Query executes a query. The output must have a schema compatible with the 
given
 // type, t. It returns a PCollection<t>.
 func Query(s beam.Scope, project, q string, t reflect.Type, options 
...func(*QueryOptions) error) beam.PCollection {
@@ -142,7 +156,16 @@ func query(s beam.Scope, project, query string, t 
reflect.Type, options ...func(
        }
 
        imp := beam.Impulse(s)
-       return beam.ParDo(s, &queryFn{Project: project, Query: query, Type: 
beam.EncodedType{T: t}, Options: queryOptions}, imp, beam.TypeDefinition{Var: 
beam.XType, T: t})
+       queryParameters, err := encodeQueryParameters(queryOptions.parameters)
+       if err != nil {
+               panic(errors.Wrapf(err, "bigqueryio.Query: failed to encode 
query parameters"))
+       }
+       return beam.ParDo(
+               s,
+               &queryFn{Project: project, Query: query, Type: 
beam.EncodedType{T: t}, QueryParameters: queryParameters, Options: 
queryOptions},
+               imp,
+               beam.TypeDefinition{Var: beam.XType, T: t},
+       )
 }
 
 type queryFn struct {
@@ -152,6 +175,8 @@ type queryFn struct {
        Query string `json:"query"`
        // Type is the encoded schema type.
        Type beam.EncodedType `json:"type"`
+       // QueryParameters are serialized query parameters for parameterized 
queries.
+       QueryParameters []byte `json:"query_parameters"`
        // Options specifies additional query execution options.
        Options QueryOptions `json:"options"`
 }
@@ -167,6 +192,11 @@ func (f *queryFn) ProcessElement(ctx context.Context, _ 
[]byte, emit func(beam.X
        if !f.Options.UseStandardSQL {
                q.UseLegacySQL = true
        }
+       parameters, err := decodeQueryParameters(f.QueryParameters)
+       if err != nil {
+               return errors.Wrapf(err, "bigqueryio.queryFn: failed to decode 
query parameters")
+       }
+       q.Parameters = parameters
 
        it, err := q.Read(ctx)
        if err != nil {
diff --git a/sdks/go/pkg/beam/io/bigqueryio/bigquery_test.go 
b/sdks/go/pkg/beam/io/bigqueryio/bigquery_test.go
index b955136e833..9f83eeb847f 100644
--- a/sdks/go/pkg/beam/io/bigqueryio/bigquery_test.go
+++ b/sdks/go/pkg/beam/io/bigqueryio/bigquery_test.go
@@ -148,3 +148,38 @@ func Test_mustInferSchema(t *testing.T) {
                })
        }
 }
+
+func TestWithQueryParameters(t *testing.T) {
+       t.Run("WithQueryParameters sets parameters correctly", func(t 
*testing.T) {
+               params := []bigquery.QueryParameter{
+                       {Name: "param1", Value: "value1"},
+                       {Name: "param2", Value: 42},
+               }
+
+               opt := WithQueryParameters(params...)
+               queryOpts := &QueryOptions{}
+               if err := opt(queryOpts); err != nil {
+                       t.Fatalf("WithQueryParameters() failed: %v", err)
+               }
+
+               if len(queryOpts.parameters) != 2 {
+                       t.Errorf("Expected 2 parameters, got %d", 
len(queryOpts.parameters))
+               }
+
+               if queryOpts.parameters[0].Name != "param1" {
+                       t.Errorf("Expected param1, got %s", 
queryOpts.parameters[0].Name)
+               }
+
+               if queryOpts.parameters[0].Value != "value1" {
+                       t.Errorf("Expected value1, got %s", 
queryOpts.parameters[0].Value)
+               }
+
+               if queryOpts.parameters[1].Name != "param2" {
+                       t.Errorf("Expected param2, got %s", 
queryOpts.parameters[1].Name)
+               }
+
+               if queryOpts.parameters[1].Value != 42 {
+                       t.Errorf("Expected 42, got %v", 
queryOpts.parameters[1].Value)
+               }
+       })
+}
diff --git a/sdks/go/pkg/beam/io/bigqueryio/coder.go 
b/sdks/go/pkg/beam/io/bigqueryio/coder.go
new file mode 100644
index 00000000000..baa82ca473d
--- /dev/null
+++ b/sdks/go/pkg/beam/io/bigqueryio/coder.go
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 bigqueryio
+
+import (
+       "bytes"
+       "encoding/gob"
+       "io"
+       "math/big"
+       "time"
+
+       "cloud.google.com/go/bigquery"
+       "cloud.google.com/go/civil"
+
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors"
+)
+
+// init registers the concrete types that bigquery.QueryParameter.Value may 
hold
+// so gob can encode/decode them through the interface{} field.
+func init() {
+       // bigquery's paramType/paramValue only recognize
+       // *bigquery.QueryParameterValue as an explicitly-typed parameter.
+       gob.Register(&bigquery.QueryParameterValue{})
+       gob.Register(bigquery.NullInt64{})
+       gob.Register(bigquery.NullFloat64{})
+       gob.Register(bigquery.NullString{})
+       gob.Register(bigquery.NullBool{})
+       gob.Register(bigquery.NullTimestamp{})
+       gob.Register(bigquery.NullDate{})
+       gob.Register(bigquery.NullTime{})
+       gob.Register(bigquery.NullDateTime{})
+       gob.Register(bigquery.NullGeography{})
+       gob.Register(bigquery.NullJSON{})
+       gob.Register(civil.Date{})
+       gob.Register(civil.Time{})
+       gob.Register(civil.DateTime{})
+       gob.Register(time.Time{})
+       gob.Register(&big.Rat{})
+       gob.Register(&bigquery.IntervalValue{})
+       gob.Register(&bigquery.RangeValue{})
+}
+
+func encodeQueryParameters(params []bigquery.QueryParameter) ([]byte, error) {
+       if params == nil {
+               return []byte{}, nil
+       }
+       // validate each element to tell which parameter is unsupported.
+       for _, p := range params {
+               if err := 
gob.NewEncoder(io.Discard).Encode([]bigquery.QueryParameter{p}); err != nil {
+                       return nil, errors.Errorf(
+                               "bigqueryio: query parameter %q has unsupported 
value type %T (%v). "+
+                                       "WithQueryParameters only supports 
bool, string, numeric types, []byte, "+
+                                       "time.Time, civil.Date/Time/DateTime, 
*big.Rat, bigquery.Null* types, "+
+                                       "*bigquery.IntervalValue, 
*bigquery.RangeValue, and *bigquery.QueryParameterValue. "+
+                                       "For STRUCT/ARRAY parameters or other 
custom types, build a "+
+                                       "*bigquery.QueryParameterValue 
explicitly instead", p.Name, p.Value, err)
+               }
+       }
+       var buf bytes.Buffer
+       if err := gob.NewEncoder(&buf).Encode(params); err != nil {
+               return nil, err
+       }
+       return buf.Bytes(), nil
+}
+
+func decodeQueryParameters(data []byte) ([]bigquery.QueryParameter, error) {
+       if len(data) == 0 {
+               return []bigquery.QueryParameter{}, nil
+       }
+       var params []bigquery.QueryParameter
+       if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&params); err != 
nil {
+               return nil, err
+       }
+       return params, nil
+}
diff --git a/sdks/go/pkg/beam/io/bigqueryio/coder_test.go 
b/sdks/go/pkg/beam/io/bigqueryio/coder_test.go
new file mode 100644
index 00000000000..b569e9400b6
--- /dev/null
+++ b/sdks/go/pkg/beam/io/bigqueryio/coder_test.go
@@ -0,0 +1,495 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 bigqueryio
+
+import (
+       "math/big"
+       "reflect"
+       "strings"
+       "testing"
+       "time"
+
+       "cloud.google.com/go/bigquery"
+       "cloud.google.com/go/civil"
+       "github.com/google/go-cmp/cmp"
+)
+
+// bigRatComparer lets cmp.Diff compare *big.Rat values.
+var bigRatComparer = cmp.Comparer(func(x, y *big.Rat) bool {
+       if x == nil || y == nil {
+               return x == y
+       }
+       return x.Cmp(y) == 0
+})
+
+func Test_encodeDecodeQueryOptions(t *testing.T) {
+       tests := []struct {
+               name string
+               val  bigquery.QueryParameter
+       }{
+               {
+                       name: "string",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: "value",
+                       },
+               },
+               {
+                       name: "nil",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: nil,
+                       },
+               },
+               {
+                       name: "int",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: 50,
+                       },
+               },
+               {
+                       name: "int8",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: int8(50),
+                       },
+               },
+               {
+                       name: "int16",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: int16(50),
+                       },
+               },
+               {
+                       name: "int32",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: int32(50),
+                       },
+               },
+               {
+                       name: "int64",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: int64(50),
+                       },
+               },
+               {
+                       name: "uint8",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: uint8(50),
+                       },
+               },
+               {
+                       name: "uint16",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: uint16(50),
+                       },
+               },
+               {
+                       name: "uint32",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: uint32(50),
+                       },
+               },
+               {
+                       name: "float32",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: float32(50.5),
+                       },
+               },
+               {
+                       name: "float64",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: 50.0,
+                       },
+               },
+               {
+                       name: "bool",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: true,
+                       },
+               },
+               {
+                       name: "[]byte",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: []byte("value"),
+                       },
+               },
+               {
+                       name: "[]int slice",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: []int{1, 2, 3},
+                       },
+               },
+               {
+                       name: "[]string slice",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: []string{"a", "b", "c"},
+                       },
+               },
+               {
+                       name: "time.Time",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: time.Date(2024, 1, 2, 3, 4, 5, 0, 
time.UTC),
+                       },
+               },
+               {
+                       name: "civil.Date",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: civil.Date{Year: 2024, Month: 1, Day: 2},
+                       },
+               },
+               {
+                       name: "civil.Time",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: civil.Time{Hour: 3, Minute: 4, Second: 
5},
+                       },
+               },
+               {
+                       name: "civil.DateTime",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: civil.DateTime{
+                                       Date: civil.Date{Year: 2024, Month: 1, 
Day: 2},
+                                       Time: civil.Time{Hour: 3, Minute: 4, 
Second: 5},
+                               },
+                       },
+               },
+               {
+                       name: "*big.Rat",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: big.NewRat(22, 7),
+                       },
+               },
+               {
+                       name: "*bigquery.IntervalValue",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: &bigquery.IntervalValue{
+                                       Years:          1,
+                                       Months:         2,
+                                       Days:           3,
+                                       Hours:          4,
+                                       Minutes:        5,
+                                       Seconds:        6,
+                                       SubSecondNanos: 7,
+                               },
+                       },
+               },
+               {
+                       name: "*bigquery.RangeValue",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: &bigquery.RangeValue{
+                                       Start: civil.Date{Year: 2024, Month: 1, 
Day: 1},
+                                       End:   civil.Date{Year: 2024, Month: 
12, Day: 31},
+                               },
+                       },
+               },
+               {
+                       name: "NullInt64 valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullInt64{Int64: 50, Valid: 
true},
+                       },
+               },
+               {
+                       name: "NullInt64 null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullInt64{Valid: false},
+                       },
+               },
+               {
+                       name: "NullFloat64 valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullFloat64{Float64: 50.5, 
Valid: true},
+                       },
+               },
+               {
+                       name: "NullFloat64 null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullFloat64{Valid: false},
+                       },
+               },
+               {
+                       name: "NullString valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullString{StringVal: "value", 
Valid: true},
+                       },
+               },
+               {
+                       name: "NullString null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullString{Valid: false},
+                       },
+               },
+               {
+                       name: "NullBool valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullBool{Bool: true, Valid: 
true},
+                       },
+               },
+               {
+                       name: "NullBool null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullBool{Valid: false},
+                       },
+               },
+               {
+                       name: "NullTimestamp valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullTimestamp{Timestamp: 
time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC), Valid: true},
+                       },
+               },
+               {
+                       name: "NullTimestamp null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullTimestamp{Valid: false},
+                       },
+               },
+               {
+                       name: "NullDate valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullDate{Date: civil.Date{Year: 
2024, Month: 1, Day: 2}, Valid: true},
+                       },
+               },
+               {
+                       name: "NullDate null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullDate{Valid: false},
+                       },
+               },
+               {
+                       name: "NullTime valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullTime{Time: civil.Time{Hour: 
3, Minute: 4, Second: 5}, Valid: true},
+                       },
+               },
+               {
+                       name: "NullTime null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullTime{Valid: false},
+                       },
+               },
+               {
+                       name: "NullDateTime valid",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: bigquery.NullDateTime{
+                                       DateTime: civil.DateTime{
+                                               Date: civil.Date{Year: 2024, 
Month: 1, Day: 2},
+                                               Time: civil.Time{Hour: 3, 
Minute: 4, Second: 5},
+                                       },
+                                       Valid: true,
+                               },
+                       },
+               },
+               {
+                       name: "NullDateTime null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullDateTime{Valid: false},
+                       },
+               },
+               {
+                       name: "NullGeography valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullGeography{GeographyVal: 
"POINT(1 2)", Valid: true},
+                       },
+               },
+               {
+                       name: "NullGeography null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullGeography{Valid: false},
+                       },
+               },
+               {
+                       name: "NullJSON valid",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullJSON{JSONVal: `{"a":1}`, 
Valid: true},
+                       },
+               },
+               {
+                       name: "NullJSON null",
+                       val: bigquery.QueryParameter{
+                               Name:  "key",
+                               Value: bigquery.NullJSON{Valid: false},
+                       },
+               },
+               {
+                       name: "QueryParameterValue BIGNUMERIC",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: &bigquery.QueryParameterValue{
+                                       Type: bigquery.StandardSQLDataType{
+                                               TypeKind: "BIGNUMERIC",
+                                       },
+                                       Value: 
"12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890",
+                               },
+                       },
+               },
+               {
+                       name: "QueryParameterValue ARRAY of STRUCT",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: &bigquery.QueryParameterValue{
+                                       Type: bigquery.StandardSQLDataType{
+                                               ArrayElementType: 
&bigquery.StandardSQLDataType{
+                                                       StructType: 
&bigquery.StandardSQLStructType{
+                                                               Fields: 
[]*bigquery.StandardSQLField{
+                                                                       {
+                                                                               
Name: "NumberField",
+                                                                               
Type: &bigquery.StandardSQLDataType{
+                                                                               
        TypeKind: "INT64",
+                                                                               
},
+                                                                       },
+                                                               },
+                                                       },
+                                               },
+                                       },
+                                       ArrayValue: 
[]bigquery.QueryParameterValue{
+                                               {StructValue: 
map[string]bigquery.QueryParameterValue{
+                                                       "NumberField": {
+                                                               Value: 
int64(42),
+                                                       },
+                                               }},
+                                               {StructValue: 
map[string]bigquery.QueryParameterValue{
+                                                       "NumberField": {
+                                                               Value: 
int64(43),
+                                                       },
+                                               }},
+                                       },
+                               },
+                       },
+               },
+               {
+                       name: "QueryParameterValue STRUCT",
+                       val: bigquery.QueryParameter{
+                               Name: "key",
+                               Value: &bigquery.QueryParameterValue{
+                                       Type: bigquery.StandardSQLDataType{
+                                               StructType: 
&bigquery.StandardSQLStructType{
+                                                       Fields: 
[]*bigquery.StandardSQLField{
+                                                               {
+                                                                       Name: 
"NumberField",
+                                                                       Type: 
&bigquery.StandardSQLDataType{
+                                                                               
TypeKind: "INT64",
+                                                                       },
+                                                               },
+                                                       },
+                                               },
+                                       },
+                                       StructValue: 
map[string]bigquery.QueryParameterValue{
+                                               "NumberField": {
+                                                       Value: int64(42),
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       encoded, err := 
encodeQueryParameters([]bigquery.QueryParameter{tt.val})
+                       if err != nil {
+                               t.Fatalf("encodeQueryParameters() error = %v", 
err)
+                       }
+
+                       decoded, err := decodeQueryParameters(encoded)
+                       if err != nil {
+                               t.Fatalf("decodeQueryParameters() error = %v", 
err)
+                       }
+
+                       gotType := reflect.TypeOf(decoded[0].Value)
+                       wantType := reflect.TypeOf(tt.val.Value)
+                       if gotType != wantType {
+                               t.Errorf("type not preserved: want %v, got %v", 
wantType, gotType)
+                       }
+
+                       if diff := cmp.Diff(tt.val, decoded[0], 
bigRatComparer); diff != "" {
+                               t.Errorf("encode/decode mismatch (-want 
+got):\n%s", diff)
+                       }
+               })
+       }
+}
+
+// customParam is a struct type deliberately not registered with gob.
+type customParam struct {
+       Foo string
+       Bar int64
+}
+
+func TestEncodeQueryParameters_UnsupportedType(t *testing.T) {
+       t.Parallel()
+       _, err := encodeQueryParameters([]bigquery.QueryParameter{
+               {Name: "custom", Value: customParam{Foo: "a", Bar: 1}},
+       })
+       if err == nil {
+               t.Fatal("encodeQueryParameters() error = nil, want error for 
unsupported type")
+       }
+       if !strings.Contains(err.Error(), `"custom"`) {
+               t.Errorf("encodeQueryParameters() error = %q, want it to name 
the parameter %q", err.Error(), "custom")
+       }
+       if !strings.Contains(err.Error(), "bigqueryio.customParam") {
+               t.Errorf("encodeQueryParameters() error = %q, want it to name 
the type %q", err.Error(), "bigqueryio.customParam")
+       }
+}
+
+func TestEncodeQueryParameters_UnsupportedTypeAmongSupported(t *testing.T) {
+       t.Parallel()
+       _, err := encodeQueryParameters([]bigquery.QueryParameter{
+               {Name: "ok", Value: "value1"},
+               {Name: "bad", Value: customParam{Foo: "a", Bar: 1}},
+       })
+       if err == nil {
+               t.Fatal("encodeQueryParameters() error = nil, want error for 
unsupported type")
+       }
+       if !strings.Contains(err.Error(), `"bad"`) {
+               t.Errorf("encodeQueryParameters() error = %q, want it to name 
the offending parameter %q, not the supported one", err.Error(), "bad")
+       }
+}
diff --git a/sdks/go/test/integration/integration.go 
b/sdks/go/test/integration/integration.go
index a0eef7d1018..535a7fb8413 100644
--- a/sdks/go/test/integration/integration.go
+++ b/sdks/go/test/integration/integration.go
@@ -67,7 +67,7 @@ var directFilters = []string{
        // The direct runner does not yet support cross-language.
        "TestXLang.*",
        "TestKafkaIO.*",
-       "TestBigQueryIO.*",
+       "TestBigQueryIO_[^WQ].*",
        "TestBigtableIO.*",
        "TestSpannerIO.*",
        "TestDebeziumIO_BasicRead",
@@ -200,7 +200,7 @@ var flinkFilters = []string{
        "TestTestStreamTimersEventTime",
 
        "TestTimers_EventTime_WithNoOutputTimestamp", // Encounter error: 
TimestampCombiner moved element from TIMESTAMP_MAX_VALUE to earlier time (end 
of global window) for window GlobalWindow
-       "TestTimers_ProcessingTime.*",    // Flink doesn't support processing 
time timers.
+       "TestTimers_ProcessingTime.*",                // Flink doesn't support 
processing time timers.
 
        // no support for BundleFinalizer
        "TestParDoBundleFinalizer.*",
diff --git a/sdks/go/test/integration/io/bigqueryio/bigqueryio_test.go 
b/sdks/go/test/integration/io/bigqueryio/bigqueryio_test.go
index 337083745c5..47701cb3d51 100644
--- a/sdks/go/test/integration/io/bigqueryio/bigqueryio_test.go
+++ b/sdks/go/test/integration/io/bigqueryio/bigqueryio_test.go
@@ -20,6 +20,7 @@ import (
        "flag"
        "fmt"
        "math/rand"
+       "reflect"
        "strings"
        "testing"
        "time"
@@ -30,6 +31,7 @@ import (
        "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts"
        "github.com/apache/beam/sdks/v2/go/pkg/beam/register"
        _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert"
        "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest"
        "github.com/apache/beam/sdks/v2/go/test/integration"
 )
@@ -79,11 +81,29 @@ func shuffleText() []string {
        return words
 }
 
+func generateTestRows(seed int64, size int) []TestRow {
+       rand.Seed(seed)
+       words := shuffleText()
+       rows := make([]TestRow, size)
+       for i := 0; i < size; i++ {
+               rows[i] = TestRow{
+                       Counter: int64(i),
+                       RandData: RandData{
+                               Flip: rand.Int63n(2) != 0,
+                               Num:  rand.Int63(),
+                               Word: words[i],
+                       },
+               }
+       }
+       return rows
+}
+
 // RandData is a struct of various types of random data.
 type RandData struct {
-       Flip bool   `bigquery:"flip"` // Flip is a bool with a random chance of 
either result (a coin flip).
-       Num  int64  `bigquery:"num"`  // Num is a random int64.
-       Word string `bigquery:"word"` // Word is a randomly selected word from 
a sample text.
+       Flip         bool                `bigquery:"flip"`          // Flip is 
a bool with a random chance of either result (a coin flip).
+       Num          int64               `bigquery:"num"`           // Num is a 
random int64.
+       Word         string              `bigquery:"word"`          // Word is 
a randomly selected word from a sample text.
+       NullableWord bigquery.NullString `bigquery:"nullable_word"` // Word is 
a randomly selected word from a sample text.
 }
 
 // ddlSchema is a string for BigQuery data definition language that 
corresponds to TestRow.
@@ -91,7 +111,8 @@ const ddlTestRowSchema = "counter INT64 NOT NULL, " +
        "rand_data STRUCT<" +
        "flip BOOL NOT NULL," +
        "num INT64 NOT NULL," +
-       "word STRING NOT NULL" +
+       "word STRING NOT NULL," +
+       "nullable_word STRING" +
        "> NOT NULL"
 
 // CreateTestRowsFn is a DoFn that creates randomized TestRows based on a seed.
@@ -101,16 +122,143 @@ type CreateTestRowsFn struct {
 
 // ProcessElement creates a number of TestRows, populating the randomized data.
 func (fn *CreateTestRowsFn) ProcessElement(_ []byte, emit func(TestRow)) {
-       rand.Seed(fn.seed)
-       words := shuffleText()
-       for i := 0; i < inputSize; i++ {
-               emit(TestRow{
-                       Counter: int64(i),
-                       RandData: RandData{
-                               Flip: rand.Int63n(2) != 0,
-                               Num:  rand.Int63(),
-                               Word: words[i],
+       rows := generateTestRows(fn.seed, inputSize)
+       for _, row := range rows {
+               emit(row)
+       }
+}
+
+func TestBigQueryIO_Query(t *testing.T) {
+       integration.CheckFilters(t)
+       checkFlags(t)
+
+       ctx := context.Background()
+       // Get the GCP project
+       // this assumes dataflow is running in the same project as the project 
in which the bigquery dataset
+       // is located
+       project := gcpopts.GetProject(ctx)
+       bigqueryDataset := *integration.BigQueryDataset
+
+       tests := []struct {
+               name          string
+               query         string
+               queryOptions  []func(*bigqueryio.QueryOptions) error
+               preCreate     bool
+               insertData    bool
+               expectedCount int
+               wantErr       bool
+       }{
+               {
+                       name:  "Query without parameters",
+                       query: "SELECT * FROM `%s`",
+                       queryOptions: []func(*bigqueryio.QueryOptions) error{
+                               bigqueryio.UseStandardSQL(),
                        },
+                       preCreate:     true,
+                       insertData:    true,
+                       expectedCount: inputSize,
+                       wantErr:       false,
+               },
+               {
+                       name:  "Query with parameters",
+                       query: "SELECT * FROM `%s` WHERE counter = @key1 and 
rand_data = @key2",
+                       queryOptions: []func(*bigqueryio.QueryOptions) error{
+                               bigqueryio.UseStandardSQL(),
+                               
bigqueryio.WithQueryParameters([]bigquery.QueryParameter{
+                                       bigquery.QueryParameter{
+                                               Name:  "key1",
+                                               Value: -1, // to ensure 
expectedCount is 0
+                                       },
+                                       bigquery.QueryParameter{
+                                               Name: "key2",
+                                               Value: 
&bigquery.QueryParameterValue{
+                                                       Type: 
bigquery.StandardSQLDataType{
+                                                               StructType: 
&bigquery.StandardSQLStructType{
+                                                                       Fields: 
[]*bigquery.StandardSQLField{
+                                                                               
{
+                                                                               
        Name: "flip",
+                                                                               
        Type: &bigquery.StandardSQLDataType{
+                                                                               
                TypeKind: "BOOL",
+                                                                               
        },
+                                                                               
},
+                                                                               
{
+                                                                               
        Name: "num",
+                                                                               
        Type: &bigquery.StandardSQLDataType{
+                                                                               
                TypeKind: "INT64",
+                                                                               
        },
+                                                                               
},
+                                                                               
{
+                                                                               
        Name: "word",
+                                                                               
        Type: &bigquery.StandardSQLDataType{
+                                                                               
                TypeKind: "STRING",
+                                                                               
        },
+                                                                               
},
+                                                                               
{
+                                                                               
        Name: "nullable_word",
+                                                                               
        Type: &bigquery.StandardSQLDataType{
+                                                                               
                TypeKind: "STRING",
+                                                                               
        },
+                                                                               
},
+                                                                       },
+                                                               },
+                                                       },
+                                                       StructValue: 
map[string]bigquery.QueryParameterValue{
+                                                               "flip": {
+                                                                       Value: 
true,
+                                                               },
+                                                               "num": {
+                                                                       Value: 
int64(1),
+                                                               },
+                                                               "word": {
+                                                                       Value: 
"ipsum",
+                                                               },
+                                                               
"nullable_word": {
+                                                                       Value: 
bigquery.NullString{
+                                                                               
StringVal: "nullable",
+                                                                               
Valid:     true,
+                                                                       },
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               }...),
+                       },
+                       preCreate:     true,
+                       insertData:    true,
+                       expectedCount: 0,
+                       wantErr:       false,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tableID := fmt.Sprintf("%s_temp_%v", "go_bqio_it", 
time.Now().UnixNano())
+                       tableName := fmt.Sprintf("%s.%s", bigqueryDataset, 
tableID)
+                       fullyQualifiedTableName := fmt.Sprintf("%s.%s.%s", 
project, bigqueryDataset, tableID)
+                       if tt.preCreate {
+                               newTempTable(t, tableName, ddlTestRowSchema, 
project)
+                       }
+                       testRows := generateTestRows(time.Now().UnixNano(), 
inputSize)
+                       if tt.insertData {
+                               insertTestRows(ctx, t, project, 
bigqueryDataset, tableID, testRows)
+                               waitForRows(ctx, t, project, bigqueryDataset, 
tableID, len(testRows))
+                       }
+                       t.Cleanup(func() {
+                               deleteTempTable(t, tableName, project)
+                       })
+
+                       p, s := beam.NewPipelineWithRoot()
+
+                       q := fmt.Sprintf(tt.query, fullyQualifiedTableName)
+                       queryType := reflect.TypeOf((*TestRow)(nil)).Elem()
+                       pcol := bigqueryio.Query(s, project, q, queryType, 
tt.queryOptions...)
+
+                       passert.Count(s, pcol, fmt.Sprintf("Should have %d 
rows", tt.expectedCount), tt.expectedCount)
+                       if err := ptest.Run(p); (err != nil) != tt.wantErr {
+                               t.Fatalf("ptest.Run() err = %v, wantErr %v", 
err, tt.wantErr)
+                       } else if err != nil {
+                               // Pipeline failed as expected, return early
+                               return
+                       }
                })
        }
 }
@@ -161,18 +309,18 @@ func TestBigQueryIO_Write(t *testing.T) {
                        tableID := fmt.Sprintf("%s_temp_%v", "go_bqio_it", 
time.Now().UnixNano())
                        tableName := fmt.Sprintf("%s.%s", 
*integration.BigQueryDataset, tableID)
                        if tt.preCreate {
-                               newTempTable(t, tableName, ddlTestRowSchema)
+                               newTempTable(t, tableName, ddlTestRowSchema, 
project)
                        }
                        t.Cleanup(func() {
-                               deleteTempTable(t, tableName)
+                               deleteTempTable(t, tableName, project)
                        })
                        createTestRows := &CreateTestRowsFn{seed: 
time.Now().UnixNano()}
                        p, s := beam.NewPipelineWithRoot()
 
                        // Generate elements and write to table.
                        rows := beam.ParDo(s, createTestRows, beam.Impulse(s))
-                       bigqueryio.Write(s, project, fmt.Sprintf("%s:%s", 
project, tableName), rows,
-                               
bigqueryio.WithCreateDisposition(tt.createDisposition))
+                       table := fmt.Sprintf("%s:%s", project, tableName)
+                       bigqueryio.Write(s, project, table, rows, 
bigqueryio.WithCreateDisposition(tt.createDisposition))
 
                        if err := ptest.Run(p); (err != nil) != tt.wantErr {
                                t.Fatalf("ptest.Run() err = %v, wantErr %v", 
err, tt.wantErr)
diff --git a/sdks/go/test/integration/io/bigqueryio/helper_test.go 
b/sdks/go/test/integration/io/bigqueryio/helper_test.go
index e39ed87d7ff..d55da8fbdd0 100644
--- a/sdks/go/test/integration/io/bigqueryio/helper_test.go
+++ b/sdks/go/test/integration/io/bigqueryio/helper_test.go
@@ -20,6 +20,7 @@ import (
        "fmt"
        "os/exec"
        "testing"
+       "time"
 
        "cloud.google.com/go/bigquery"
        "github.com/apache/beam/sdks/v2/go/test/integration"
@@ -31,11 +32,11 @@ import (
 //
 // newTable takes the name of a BigQuery dataset and a DDL schema for the data,
 // and generates that table with a unique suffix and an expiration time of a 
day later.
-func newTempTable(t *testing.T, name string, schema string) {
+func newTempTable(t *testing.T, name string, schema string, projectID string) {
        t.Helper()
 
        query := fmt.Sprintf("CREATE TABLE `%s`(%s) 
OPTIONS(expiration_timestamp=TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 
DAY))", name, schema)
-       cmd := exec.Command("bq", "query", "--use_legacy_sql=false", query)
+       cmd := exec.Command("bq", "query", fmt.Sprintf("--project_id=%s", 
projectID), "--use_legacy_sql=false", query)
        _, err := cmd.CombinedOutput()
        if err != nil {
                t.Fatalf("error creating BigQuery table: %v", err)
@@ -45,11 +46,11 @@ func newTempTable(t *testing.T, name string, schema string) 
{
 
 // deleteTable deletes a BigQuery table using BigQuery's Data Definition 
Language (DDL) and the
 // "bq query" console command. Reference: 
https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language
-func deleteTempTable(t *testing.T, table string) {
+func deleteTempTable(t *testing.T, table string, projectID string) {
        t.Helper()
 
        query := fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)
-       cmd := exec.Command("bq", "query", "--use_legacy_sql=false", query)
+       cmd := exec.Command("bq", "query", fmt.Sprintf("--project_id=%s", 
projectID), "--use_legacy_sql=false", query)
        t.Logf("Deleting BigQuery table %v", table)
        _, err := cmd.CombinedOutput()
        if err != nil {
@@ -66,16 +67,64 @@ func checkTableExistsAndNonEmpty(ctx context.Context, t 
*testing.T, project, tab
        }
        defer client.Close()
 
-       tableRef := client.Dataset(*integration.BigQueryDataset).Table(tableID)
-       metadata, err := tableRef.Metadata(ctx)
+       q := client.Query(fmt.Sprintf("SELECT COUNT(*) FROM `%s.%s.%s`", 
project, *integration.BigQueryDataset, tableID))
+       it, err := q.Read(ctx)
        if err != nil {
-               t.Fatalf("unable to find table: %v", err)
+               t.Fatalf("error querying table row count: %v", err)
        }
-       streamingBuffer := metadata.StreamingBuffer
-       if streamingBuffer == nil {
-               t.Fatalf("there's no streaming buffer for the table")
+       var row []bigquery.Value
+       if err := it.Next(&row); err != nil {
+               t.Fatalf("error reading row count result: %v", err)
        }
-       if streamingBuffer.EstimatedRows != inputSize {
-               t.Fatalf("streamingBuffer.EstimatedRows = %v, want %v", 
streamingBuffer.EstimatedRows, inputSize)
+       count, ok := row[0].(int64)
+       if !ok {
+               t.Fatalf("unexpected type for row count: %T", row[0])
+       }
+       if count != inputSize {
+               t.Fatalf("row count = %v, want %v", count, inputSize)
+       }
+}
+
+// waitForRows polls until the table has at least wantCount rows visible, or 
times out.
+func waitForRows(ctx context.Context, t *testing.T, project, dataset, tableID 
string, wantCount int) {
+       t.Helper()
+
+       client, err := bigquery.NewClient(ctx, project)
+       if err != nil {
+               t.Fatalf("error creating BigQuery client: %v", err)
+       }
+       defer client.Close()
+
+       deadline := time.Now().Add(2 * time.Minute)
+       for time.Now().Before(deadline) {
+               q := client.Query(fmt.Sprintf("SELECT COUNT(*) FROM 
`%s.%s.%s`", project, dataset, tableID))
+               it, err := q.Read(ctx)
+               if err == nil {
+                       var row []bigquery.Value
+                       if err := it.Next(&row); err == nil {
+                               if count, ok := row[0].(int64); ok && 
int(count) >= wantCount {
+                                       return
+                               }
+                       }
+               }
+               time.Sleep(5 * time.Second)
+       }
+       t.Fatalf("timed out waiting for %d rows in %s.%s", wantCount, dataset, 
tableID)
+}
+
+func insertTestRows(ctx context.Context, t *testing.T, project, dataset, 
tableID string, rows []TestRow) {
+       t.Helper()
+
+       client, err := bigquery.NewClient(ctx, project)
+       if err != nil {
+               t.Fatalf("error creating BigQuery client: %v", err)
+       }
+       defer client.Close()
+       ds := client.Dataset(dataset)
+       tbl := ds.Table(tableID)
+       inserter := tbl.Inserter()
+
+       if err := inserter.Put(ctx, rows); err != nil {
+               t.Fatalf("error inserting test rows: %v", err)
        }
 }

Reply via email to