googleapis / googleapis/google-cloud-node
bigquery: `query()` never uses the `jobs.query` fast path when `jobTimeoutMs` is set
- 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).
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"`.
### 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
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