Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@
"test": "c8 mocha build/test"
},
"dependencies": {
"big.js": "^7.0.1",
"google-gax": "^5.0.0"
},
"devDependencies": {
"@types/big.js": "^6.2.2",
"@types/mocha": "^10.0.10",
"@types/node": "^22.15.21",
"@types/sinon": "^17.0.4",
"c8": "^10.1.3",
"eslint-plugin-prettier": "^5.5.1",
"gapic-tools": "^1.0.2",
"gts": "^6.0.2",
"jsdoc": "^4.0.4",
Expand Down
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';
36 changes: 36 additions & 0 deletions src/query/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 {protos} from '../';

/**
* QueryFromSQL creates a query configuration from a SQL string.
* @param {string} sql The SQL query.
* @returns {protos.google.cloud.bigquery.v2.IPostQueryRequest}
*/
export function queryFromSQL(
projectId: string,
sql: string,
): protos.google.cloud.bigquery.v2.IPostQueryRequest {
return {
queryRequest: {
query: sql,
useLegacySql: {value: false},
formatOptions: {
useInt64Timestamp: true,
},
},
projectId,
};
}
138 changes: 138 additions & 0 deletions src/query/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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,
SubClientOptions,
} from '../bigquery';
import {QueryJob, CallOptions} from './job';
import {protos} from '../';
import {queryFromSQL as builderQueryFromSQL} from './builder';
import {QueryReader} from './reader';

/**
* QueryClient is a client for running queries in BigQuery.
*/
export class QueryClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting - why have an entirely new client rather than have this be part of the central client?

private client: BigQueryClient;
projectId: string;
private billingProjectId: string;

/**
* @param {BigQueryClientOptions} options - The configuration object.
*/
constructor(
options?: BigQueryClientOptions,
subClientOptions?: SubClientOptions,
) {
this.client = new BigQueryClient(options, subClientOptions);
this.projectId = '';
this.billingProjectId = '';
void this.client.jobClient.getProjectId().then(projectId => {
this.projectId = projectId;
if (this.billingProjectId !== '') {
this.billingProjectId = projectId;
}
});
}

setBillingProjectId(projectId: string) {
this.billingProjectId = projectId;
}

/**
* QueryFromSQL creates a query configuration from a SQL string.
* @param {string} sql The SQL query.
* @returns {protos.google.cloud.bigquery.v2.IPostQueryRequest}
*/
queryFromSQL(sql: string): protos.google.cloud.bigquery.v2.IPostQueryRequest {
const req = builderQueryFromSQL(this.projectId, sql);
return req;
}

/**
* NewQueryReader creates a new QueryReader.
* @returns {QueryReader}
*/
newQueryReader(): QueryReader {
return new QueryReader(this);
}

/**
* Runs a query and returns a QueryJob handle.
*
* @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<QueryJob>}
*/
async startQuery(
request: protos.google.cloud.bigquery.v2.IPostQueryRequest,
options?: CallOptions,
): Promise<QueryJob> {
const [response] = await this.client.jobClient.query(request, options);
return new QueryJob(this, response);
}

/**
* Runs a query and returns a QueryJob handle.
*
* @param {protos.google.cloud.bigquery.v2.IQueryRequest} 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<QueryJob>}
*/
async startQueryRequest(
request: protos.google.cloud.bigquery.v2.IQueryRequest,
options?: CallOptions,
): Promise<QueryJob> {
return this.startQuery(
{
queryRequest: request,
projectId: this.projectId,
},
options,
);
}

/**
* Starts a new asynchronous job.
*
* @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<QueryJob>}
*/
async startQueryJob(
job: protos.google.cloud.bigquery.v2.IJob,
options?: CallOptions,
): Promise<QueryJob> {
const [response] = await this.client.jobClient.insertJob(
{
projectId: this.projectId,
job,
},
options,
);
return new QueryJob(this, {jobReference: response.jobReference});
}

getBigQueryClient(): BigQueryClient {
return this.client;
}
}
47 changes: 47 additions & 0 deletions src/query/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Will these utils only be useful for queries, or are they also useful with other clients?

export function civilDateString(d: Date): string {
return d.toISOString().slice(0, 10);
}

export function civilTimeString(value: string | Date): string {
if (value instanceof Date) {
const h = `${value.getHours()}`.padStart(2, '0');
const m = `${value.getMinutes()}`.padStart(2, '0');
const s = `${value.getSeconds()}`.padStart(2, '0');
const f = `${value.getMilliseconds()}`.padStart(3, '0');
return `${h}:${m}:${s}.${f}`;
}
return value;
}

export function civilDateTimeString(value: Date | string): string {
if (value instanceof Date) {
let time;
if (value.getHours()) {
time = civilTimeString(value);
}
const y = `${value.getFullYear()}`.padStart(2, '0');
const m = `${value.getMonth() + 1}`.padStart(2, '0');
const d = `${value.getDate()}`.padStart(2, '0');
time = time ? ' ' + time : '';
return `${y}-${m}-${d}${time}`;
}
return value.replace(/^(.*)T(.*)Z$/, '$1 $2');
}

export function timestampString(ts: Date): string {
return ts.toISOString().replace('T', ' ').replace('Z', '');
}
20 changes: 20 additions & 0 deletions src/query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 {QueryClient} from './client';
export {QueryJob} from './job';
export {Row} from './row';
export {RowIterator} from './iterator';
export {queryFromSQL} from './builder';
export {QueryReader, withPageToken} from './reader';
60 changes: 60 additions & 0 deletions src/query/iterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 {QueryJob} from './job';
import {Row} from './row';

/**
* RowIterator iterates over the results of a query.
*/
export class RowIterator {
Copy link
Contributor

Choose a reason for hiding this comment

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

can you use the iterator without the reader, or is this only ever used in the context of the reader? If only ever coupled with the reader, my nit would be to have them in the same file

private job: QueryJob;
private pageToken?: string;
private rows: Row[] = [];

constructor(
job: QueryJob,
opts?: {
rows?: Row[];
pageToken?: string;
},
) {
this.job = job;
this.pageToken = opts?.pageToken;
this.rows = opts?.rows ?? [];
}

async fetchRows() {
const [rows, _, pageToken] = await this.job._getRows(this.pageToken);

Check warning on line 39 in src/query/iterator.ts

View workflow job for this annotation

GitHub Actions / lint

'_' is assigned a value but never used
this.rows = rows;
this.pageToken = pageToken || undefined;
}

/**
* Asynchronously iterates over the rows in the query result.
*/
async *[Symbol.asyncIterator](): AsyncGenerator<Row> {
if (this.rows.length > 0) {
for (const row of this.rows) {
yield row;
}
}
while (this.pageToken) {
await this.fetchRows();
for (const row of this.rows) {
yield row;
}
}
}
}
Loading
Loading