Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ export {
};
import * as protos from '../protos/protos';
export {protos};
export * as query from './query';
162 changes: 162 additions & 0 deletions src/query/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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.

import {BigQueryClient, BigQueryClientOptions} from '../bigquery';

Check warning on line 15 in src/query/helper.ts

View workflow job for this annotation

GitHub Actions / lint

'BigQueryClientOptions' is defined but never used
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import {BigQueryClient, BigQueryClientOptions} from '../bigquery';
import {BigQueryClient} from '../bigquery';

import {Query} from './query';
import {CallOptions} from './options';
import {protos} from '..';
import {randomUUID} from 'crypto';

/**
* The QueryHelper provides a simplified interface for interacting with BigQuery
* queries. It handles the lifecycle of query jobs, from creation to result
* retrieval.
*/
export class QueryHelper {
private client: BigQueryClient;
private projectId?: string;

/**
* @param {object} opts - The configuration object.
* @param {BigQueryClient} opts.client - A BigQueryClient instance.
* @param {string} [opts.projectId] - The project ID to use. Optional.
*/
constructor(opts: {client: BigQueryClient; projectId?: string}) {
this.client = opts.client;
this.projectId = opts.projectId;
void this.initialize();
}

/**
* Retrieves the project ID from the client.
*
* @returns {Promise<string>} A promise that resolves with the project ID.
*/
async getProjectId(): Promise<string> {
if (this.projectId) {
return this.projectId;
}
const {jobClient} = this.getBigQueryClient();
const projectId = await jobClient.getProjectId();
this.projectId = projectId;
return projectId;
}

/**
* Initialize the client.
* Performs asynchronous operations (such as authentication) and prepares the client.
* This function will be called automatically when any class method is called for the
* first time, but if you need to initialize it before calling an actual method,
* feel free to call initialize() directly.
*
* You can await on this method if you want to make sure the client is initialized.
*
* @returns {Promise} A promise that resolves when auth is complete.
*/
async initialize(): Promise<void> {
if (this.projectId) {
return;
}
const {jobClient} = this.getBigQueryClient();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we be having this instead mess with the underlying job client rather than the centralized one?

Thinking about this though, I do now see why you want the knowledge of the centralized BigQueryClient's existence and how you're going to be using it

await jobClient.initialize();
const projectId = await this.getProjectId();
this.projectId = projectId;
}

/**
* Starts a query job using the `jobs.query` API.
*
* @param {protos.google.cloud.bigquery.v2.IPostQueryRequest} request
* The request object that will be sent.
* @param {CallOptions} [options]
* Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details.
* @returns {Promise<Query>} A promise that resolves with a Query instance.
*/
async startQuery(
request: protos.google.cloud.bigquery.v2.IPostQueryRequest,
options?: CallOptions,
): Promise<Query> {
if (!request.projectId) {
request.projectId = this.projectId;
}
if (!request.queryRequest) {
throw new Error('queryRequest is required');
}
if (!request.queryRequest.requestId) {
request.queryRequest.requestId = randomUUID();
}
return Query.fromQueryRequest_(this, request, options);
}

/**
* Starts a new asynchronous query job using the `jobs.insert` API.
*
* @param {protos.google.cloud.bigquery.v2.IJob} job
* A job resource to insert.
* @param {CallOptions} [options]
* Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details.
* @returns {Promise<Query>} A promise that resolves with a Query instance.
*/
async startQueryJob(
job: protos.google.cloud.bigquery.v2.IJob,
options?: CallOptions,
): Promise<Query> {
const config = job.configuration;
if (!config) {
throw new Error('job is missing configuration');
}
const queryConfig = config.query;
if (!queryConfig) {
throw new Error('job is not a query');
}
job.jobReference ||= {};
if (!job.jobReference.jobId) {
job.jobReference.jobId = randomUUID();
}

return Query.fromJobRequest_(this, job, this.projectId, options);
}

/**
* Attaches to an existing query job.
*
* @param {protos.google.cloud.bigquery.v2.IJobReference} jobReference
* A job reference object.
* @param {CallOptions} [options]
* Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details.
* @returns {Promise<Query>} A promise that resolves with a Query instance.
*/
async attachJob(
jobReference: protos.google.cloud.bigquery.v2.IJobReference,
options?: CallOptions,
): Promise<Query> {
if (!jobReference.jobId) {
throw new Error('attachJob requires a non-empty JobReference.JobId');
}
if (!jobReference.projectId) {
jobReference.projectId = this.projectId;
}

return Query.fromJobRef_(this, jobReference, options);
}

/**
* Returns the BigQueryClient instance.
*
* @returns {BigQueryClient} The BigQueryClient instance.
*/
getBigQueryClient(): BigQueryClient {
return this.client;
}
}
18 changes: 18 additions & 0 deletions src/query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.

export {QueryHelper} from './helper';
export {Query} from './query';
export {Row} from './row';
export {RowIterator} from './iterator';
41 changes: 41 additions & 0 deletions src/query/iterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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.

import {Query} from './query';
import {Row} from './row';

/**
* The RowIterator provides a way to iterate over the rows of a query result.
* It can be used with `for await...of` loops.
*/
export class RowIterator {
private query: Query;

/**
* @param {Query} query - The Query instance to iterate over.
* @internal
*/
constructor(query: Query) {
this.query = query;
}

/**
* Asynchronously iterates over the rows in the query result.
*
* @yields {Row} A row from the query result.
*/
async *[Symbol.asyncIterator](): AsyncGenerator<Row> {
// TODO(#1541): implement iterator
}
}
25 changes: 25 additions & 0 deletions src/query/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.

import {CallOptions as GaxCallOptions} from 'google-gax';

/**
* Extended call options that include an AbortSignal.
*/
export interface CallOptions extends GaxCallOptions {
/**
* An AbortSignal to cancel the operation.
*/
signal?: AbortSignal;
}
Loading
Loading