blob: c52fb1a80120bd4ec3fdd70596997eaf0257b984 [file] [edit]
// Copyright 2025 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
//
// https://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 query
import (
"context"
"sync"
"time"
"cloud.google.com/go/bigquery/v2/apiv2/bigquerypb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// Query represents a handle to a query job. Its methods can be used to wait for
// the job to complete and to iterate over the results.
type Query struct {
h *Helper
projectID string
jobID string
location string
queryID string
// context for background pooling
ctx context.Context
mu sync.RWMutex
complete bool
ready chan struct{}
err error
cachedTotalRows uint64
}
// Create Query handler using jobs.query request and start background pooling job
func newQueryJobFromQueryRequest(ctx context.Context, h *Helper, req *bigquerypb.PostQueryRequest, opts ...gax.CallOption) *Query {
q := &Query{
h: h,
ctx: ctx,
ready: make(chan struct{}),
}
go q.runQuery(req, opts)
return q
}
// Create Query handler using jobs.insert request and start background pooling job
func newQueryJobFromJob(ctx context.Context, h *Helper, projectID string, job *bigquerypb.Job, opts ...gax.CallOption) *Query {
q := &Query{
h: h,
ctx: ctx,
ready: make(chan struct{}),
projectID: projectID,
}
go q.insertQuery(job, opts)
return q
}
// Create Query handler from JobReference response and start background pooling job
func newQueryJobFromJobReference(ctx context.Context, h *Helper, jobRef *bigquerypb.JobReference, opts ...gax.CallOption) *Query {
q := &Query{
h: h,
ctx: ctx,
ready: make(chan struct{}),
}
q.consumeQueryResponse(&bigquerypb.GetQueryResultsResponse{
JobReference: jobRef,
})
go q.waitForQueryBackground(opts)
return q
}
// Read returns a RowIterator for the query results.
func (q *Query) Read(ctx context.Context, opts ...ReadOption) (*RowIterator, error) {
// TODO: proper setup iterator
return &RowIterator{}, nil
}
// Wait blocks until the query has completed. The provided context can be used to
// cancel the wait. If the query completes successfully, Wait returns nil.
// Otherwise, it returns the error that caused the query to fail.
//
// Wait is a convenience wrapper around Done and Err.
func (q *Query) Wait(ctx context.Context) error {
select {
case <-q.Done():
return q.Err()
case <-ctx.Done():
return ctx.Err()
}
}
// Done returns a channel that is closed when the query has completed.
// It can be used in a select statement to perform non-blocking waits.
//
// Example:
//
// select {
// case <-q.Done():
// if err := q.Err(); err != nil {
// // Handle error.
// }
// // Query is complete.
// case <-time.After(30*time.Second):
// // Timeout logic
// default:
// // Query is still running.
// }
func (q *Query) Done(opts ...gax.CallOption) <-chan struct{} {
return q.ready
}
// Err returns the final error state of the query. It is only valid to call Err
// after the channel returned by Done has been closed. If the query completed
// successfully, Err returns nil.
func (q *Query) Err() error {
q.mu.RLock()
defer q.mu.RUnlock()
err := q.ctx.Err()
if err != nil {
return err
}
return q.err
}
func (q *Query) insertQuery(job *bigquerypb.Job, opts []gax.CallOption) {
res, err := q.h.c.InsertJob(q.ctx, &bigquerypb.InsertJobRequest{
ProjectId: q.projectID,
Job: job,
}, opts...)
if err != nil {
q.markDone(err)
return
}
q.consumeQueryResponse(&bigquerypb.GetQueryResultsResponse{
JobReference: res.GetJobReference(),
})
go q.waitForQueryBackground(opts)
}
func (q *Query) runQuery(req *bigquerypb.PostQueryRequest, opts []gax.CallOption) {
res, err := q.h.c.Query(q.ctx, req, opts...)
if err != nil {
q.markDone(err)
return
}
q.queryID = res.GetQueryId()
q.consumeQueryResponse(res)
go q.waitForQueryBackground(opts)
}
func (q *Query) waitForQueryBackground(opts []gax.CallOption) {
backoff := gax.Backoff{
Initial: 50 * time.Millisecond,
Multiplier: 1.3,
Max: 60 * time.Second,
}
for !q.complete {
err := q.waitForQuery(q.ctx, opts)
if err != nil {
q.markDone(err)
return
}
select {
case <-time.After(backoff.Pause()):
case <-q.ctx.Done():
q.markDone(q.ctx.Err())
return
}
}
q.markDone(nil)
}
func (q *Query) markDone(err error) {
q.mu.Lock()
defer q.mu.Unlock()
// Check if already done to prevent panic on closing closed channel.
select {
case <-q.ready:
// Already closed
return
default:
// Not closed yet
q.err = err
close(q.ready)
}
}
func (q *Query) waitForQuery(ctx context.Context, opts []gax.CallOption) error {
res, err := q.h.c.GetQueryResults(ctx, &bigquerypb.GetQueryResultsRequest{
ProjectId: q.projectID,
JobId: q.jobID,
Location: q.location,
MaxResults: wrapperspb.UInt32(0),
FormatOptions: &bigquerypb.DataFormatOptions{
UseInt64Timestamp: true,
},
}, opts...)
if err != nil {
return err
}
q.consumeQueryResponse(res)
return nil
}
// Common fields from jobs.query and jobs.getQueryResults
// Needs to be updated as new fields are consumed
type queryResponse interface {
GetJobComplete() *wrapperspb.BoolValue
GetJobReference() *bigquerypb.JobReference
GetTotalRows() *wrapperspb.UInt64Value
}
func (q *Query) consumeQueryResponse(res queryResponse) {
q.mu.Lock()
defer q.mu.Unlock()
if res.GetJobComplete() != nil {
q.complete = res.GetJobComplete().GetValue()
}
jobRef := res.GetJobReference()
if jobRef != nil {
q.projectID = jobRef.GetProjectId()
q.jobID = jobRef.GetJobId()
if jobRef.GetLocation() != nil {
q.location = jobRef.GetLocation().GetValue()
}
}
if res.GetTotalRows() != nil {
q.cachedTotalRows = res.GetTotalRows().GetValue()
}
// TODO: save schema, page token, total rows and parse rows
}
// QueryID returns the auto-generated ID for the query.
// This is only populated for stateless queries (i.e. those started via jobs.query)
// after the query has been submitted.
func (q *Query) QueryID() string {
q.mu.RLock()
defer q.mu.RUnlock()
return q.queryID
}
// JobReference returns a reference to the query job.
// This will be nil until the query job has been successfully submitted.
func (q *Query) JobReference() *bigquerypb.JobReference {
q.mu.RLock()
defer q.mu.RUnlock()
if q.jobID == "" {
return nil
}
return &bigquerypb.JobReference{
ProjectId: q.projectID,
JobId: q.jobID,
Location: wrapperspb.String(q.location),
}
}
// Schema returns the schema of the query results.
// This will be nil until the query has completed and the schema is available.
func (q *Query) Schema() *bigquerypb.TableSchema {
return nil // TODO: fill schema
}
// Complete returns true if the query job has finished execution.
func (q *Query) Complete() bool {
q.mu.RLock()
defer q.mu.RUnlock()
return q.complete
}