googleapis / googleapis/google-cloud-node

bigquery: `query()` never uses the `jobs.query` fast path when `jobTimeoutMs` is set

Open
#9,119 0 comments 0 reactions 0 assignees View on GitHub
api: bigquery
Dominant language
TypeScript
Stars
3.2k
Forks
712
Avg merge
2d 3h
Merged PRs (30d)
99

Description

### Library Name

@google-cloud/bigquery

### A screenshot that you have tested with "Try this API".

The screenshots show the issue is client-side, not API-side:

1. `jobs.query` accepts `jobTimeoutMs` and returns results normally (HTTP 200).Image
2. `jobs.query` enforces it: a ~30s query with `jobTimeoutMs: "1000"` returns HTTP 499 with `"Job execution was cancelled: Job timed out after 1 sec"`.Image

### What would you like to see in the library?

This is a request for the handwritten client logic in `handwritten/bigquery`, not for the API surface: the API already supports the field, and the limitation is a client-side denylist.

### Current behavior

`BigQuery.query()` routes queries to the accelerated `jobs.query` endpoint unless an option appears on a denylist in `buildQueryRequest_()`.
`jobTimeoutMs` is on that list, so setting it silently forces every query through the slower `jobs.insert` + `getQueryResults` path:

https://github.com/googleapis/google-cloud-node/blob/13d1ca8247789323094df9ed916e3ad184053cd7/handwritten/bigquery/src/bigquery.ts#L2354

```js
// always goes through jobs.insert + polling, even for a tiny query
const [rows] = await bigquery.query({query: 'SELECT 1', jobTimeoutMs: 60000});
```

### The API now supports `jobTimeoutMs` on `jobs.query`

- The `jobs.query` request body now has a `jobTimeoutMs` field. This repository's generated `IQueryRequest` type already includes it: https://github.com/googleapis/google-cloud-node/blob/13d1ca8247789323094df9ed916e3ad184053cd7/handwritten/bigquery/src/types.d.ts#L4437-L4440
- The field's documentation explicitly covers the no-job case: "This timeout applies to the query even if a job does not need to be created."

### Proposed change

Remove `jobTimeoutMs` from the denylist and map it into the `IQueryRequest`, converting the number to a string as the API expects an int64 field. This mirrors what `createQueryJob()` already does for the `jobs.insert` path.

A patch with a unit test is ready on my fork, based on current main. I will open a PR if the approach looks good: https://github.com/googleapis/google-cloud-node/compare/main...takaebato:google-cloud-node:bigquery-jobtimeoutms-fast-path

### Describe alternatives you've considered

No real alternative: users today can only drop `jobTimeoutMs` or fall back to `createQueryJob()` and manage the job manually.

### Additional context/notes

I verified the server-side behavior with direct REST calls.

**1. `jobTimeoutMs` is enforced on the fast path, with the identical error as the slow path**

A query that runs for roughly 30 seconds, submitted with a 1 second timeout via `POST /projects/$PROJECT/queries`:

```json
{
"query": "SELECT COUNT(*) FROM UNNEST(GENERATE_ARRAY(1, 1000000)) a CROSS JOIN UNNEST(GENERATE_ARRAY(1, 1000)) b WHERE MOD(a + b, 7) = 0",
"useLegacySql": false,
"useQueryCache": false,
"jobTimeoutMs": "1000",
"timeoutMs": 120000
}
```

returns HTTP 499 synchronously after about 7 seconds:

```json
{
"error": {
"code": 499,
"message": "Job execution was cancelled: Job timed out after 1 sec",
"errors": [
{
"message": "Job execution was cancelled: Job timed out after 1 sec",
"domain": "global",
"reason": "stopped"
}
],
"status": "CANCELLED"
}
}
```

The same query submitted via `POST /projects/$PROJECT/jobs` with `{"configuration": {"query": {...}, "jobTimeoutMs": "1000"}}` ends as DONE with an `errorResult` carrying the identical reason and message, and `jobs.getQueryResults` for that job returns a byte-identical HTTP 499 body. So the error surfaced to the client is the same regardless of path.

**2. The timeout applies even when no job is created**

The same fast path request with `"jobCreationMode": "JOB_CREATION_OPTIONAL"` added was cancelled as well, with the same error shape, confirming the documented behavior ("This timeout applies to the query even if a job does not need to be created").

**3. The `jobTimeoutMs: 0` case: only the request body changes, not the behavior**

Before the patch, `query({query, jobTimeoutMs: 0})` already took the fast path, because the denylist check is truthiness-based and 0 is falsy. The field was not sent in that case. With the patch, the field is sent as `"jobTimeoutMs": "0"`. The server accepts `"0"` and treats it as "no timeout", so the observable behavior is the same, verified with

```json
{"query": "SELECT 1", "useLegacySql": false, "jobTimeoutMs": "0"}
```

returning HTTP 200 with results. This also matches `createQueryJob()`, which has always sent `"0"` on the `jobs.insert` path.

Contributor guide

Open the contributing guide

Research direction

Start in handwritten/bigquery/src/bigquery.ts at buildQueryRequest_(), where the denylist controls the jobs.query fast path. Check the IQueryRequest definition in handwritten/bigquery/src/types.d.ts and the existing createQueryJob() mapping, then add or update the relevant unit test. Done means jobTimeoutMs is sent as a string through jobs.query and the test verifies the fast-path request.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, data
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.