@malloydata/db-bigquery 0.0.426 → 0.0.427
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bigquery_connection.d.ts +35 -1
- package/dist/bigquery_connection.js +217 -35
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RowMetadata } from '@google-cloud/bigquery';
|
|
1
|
+
import type { Job, PagedResponse, Query, QueryResultsOptions, RowMetadata } from '@google-cloud/bigquery';
|
|
2
2
|
import type bigquery from '@google-cloud/bigquery/build/src/types';
|
|
3
3
|
import type { ResourceStream } from '@google-cloud/paginator';
|
|
4
4
|
import type { Connection, ConnectionConfig, ConnectionParameterValue, MalloyQueryData, PersistSQLResults, QueryData, QueryRecord, QueryOptionsReader, QueryRunStats, RunSQLOptions, StreamingConnection, TableSourceDef, SQLSourceDef, SQLSourceRequest } from '@malloydata/malloy';
|
|
@@ -54,6 +54,31 @@ interface SchemaInfo {
|
|
|
54
54
|
export declare class BigQueryAuthenticationError extends Error {
|
|
55
55
|
constructor(message: string);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch a job's query results, polling until the job completes, `deadlineMs`
|
|
59
|
+
* elapses, or the caller aborts. A single getQueryResults call cannot wait out a
|
|
60
|
+
* slow query: BigQuery bounds how long one call blocks server-side (its docs
|
|
61
|
+
* note the call typically returns after ~200s even when a larger timeoutMs is
|
|
62
|
+
* requested) and then reports `jobComplete: false` while the job is still
|
|
63
|
+
* running normally. So for a query that runs longer than one call's server wait
|
|
64
|
+
* we re-issue getQueryResults until it finishes.
|
|
65
|
+
*
|
|
66
|
+
* `deadlineMs` bounds the total wait (the connection's configured timeoutMs).
|
|
67
|
+
* On reaching it we cancel the job before throwing: BigQuery's jobTimeoutMs
|
|
68
|
+
* should already be cancelling it server-side, but if that timeout error has not
|
|
69
|
+
* come back yet, cancelling here stops the job from running (and billing) past
|
|
70
|
+
* the point we stopped waiting. `abortSignal` lets the loop exit promptly on an
|
|
71
|
+
* external cancel (the caller cancels the job in that case). A bounded number of
|
|
72
|
+
* retries absorbs the transient access-denied error BigQuery intermittently
|
|
73
|
+
* returns on first fetch. Between polls a minimum interval is enforced so a poll
|
|
74
|
+
* that returns sooner than requested does not spin the loop. `now` and `wait`
|
|
75
|
+
* are injectable for testing.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getQueryResultsUntilComplete(job: Pick<Job, 'getQueryResults' | 'cancel'>, getQueryResultsOptions: QueryResultsOptions, deadlineMs: number, { abortSignal, now, wait, }?: {
|
|
78
|
+
abortSignal?: AbortSignal;
|
|
79
|
+
now?: () => number;
|
|
80
|
+
wait?: (ms: number, abortSignal?: AbortSignal) => Promise<void>;
|
|
81
|
+
}): Promise<PagedResponse<RowMetadata, Query, bigquery.IGetQueryResultsResponse>>;
|
|
57
82
|
export declare class BigQueryConnection extends BaseConnection implements Connection, PersistSQLResults, StreamingConnection {
|
|
58
83
|
readonly name: string;
|
|
59
84
|
private readonly dialect;
|
|
@@ -97,6 +122,15 @@ export declare class BigQueryConnection extends BaseConnection implements Connec
|
|
|
97
122
|
private encodeProjectSegment;
|
|
98
123
|
fetchTableSchema(tableName: string, tablePath: string): Promise<TableSourceDef | string>;
|
|
99
124
|
private getSQLBlockSchema;
|
|
125
|
+
/**
|
|
126
|
+
* The effective query timeout in milliseconds. A configured value that is
|
|
127
|
+
* unset, blank, non-numeric, zero, or negative falls back to TIMEOUT_MS; only
|
|
128
|
+
* a positive number overrides the default. The result bounds both the
|
|
129
|
+
* BigQuery job (jobTimeoutMs) and the getQueryResultsUntilComplete poll
|
|
130
|
+
* deadline, so it must be positive: a zero or negative would make the job
|
|
131
|
+
* cancel on the first poll instead of running.
|
|
132
|
+
*/
|
|
133
|
+
private resolvedTimeoutMs;
|
|
100
134
|
private createBigQueryJobAndGetResults;
|
|
101
135
|
private createBigQueryJob;
|
|
102
136
|
initiateJobAndGetLinkToConsole(sqlCommand: string, dryRun?: boolean): Promise<string>;
|
|
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.BigQueryConnection = exports.BigQueryAuthenticationError = void 0;
|
|
41
|
+
exports.getQueryResultsUntilComplete = getQueryResultsUntilComplete;
|
|
41
42
|
const bigquery_1 = require("@google-cloud/bigquery");
|
|
42
43
|
const googleCommon = __importStar(require("@google-cloud/common"));
|
|
43
44
|
const gaxios_1 = require("gaxios");
|
|
@@ -104,9 +105,185 @@ const maybeRewriteError = (e) => {
|
|
|
104
105
|
*/
|
|
105
106
|
const MAXIMUM_BYTES_BILLED = String(25 * 1024 * 1024 * 1024);
|
|
106
107
|
/**
|
|
107
|
-
* Default timeoutMs
|
|
108
|
+
* Default connection timeoutMs, 10 minutes. Bounds both the BigQuery job
|
|
109
|
+
* (jobTimeoutMs) and, via getQueryResultsUntilComplete, how long results are
|
|
110
|
+
* polled. When this connector runs behind an HTTP layer (e.g. the Malloy
|
|
111
|
+
* Publisher), set the connection's timeoutMs below that layer's socket/request
|
|
112
|
+
* timeout so a long query is ended by this deadline with a clear error, rather
|
|
113
|
+
* than racing an opaque socket reset.
|
|
108
114
|
*/
|
|
109
115
|
const TIMEOUT_MS = 1000 * 60 * 10;
|
|
116
|
+
/**
|
|
117
|
+
* How long each getQueryResults call asks to wait for the job to finish before
|
|
118
|
+
* returning. BigQuery returns after at most ~200s regardless of the requested
|
|
119
|
+
* value, so this is kept under that ceiling; queries that take longer are
|
|
120
|
+
* covered by polling across multiple calls (see getQueryResultsUntilComplete).
|
|
121
|
+
*/
|
|
122
|
+
const GET_QUERY_RESULTS_POLL_MS = 1000 * 60 * 2;
|
|
123
|
+
/**
|
|
124
|
+
* Minimum spacing between getQueryResults polls. Each call is asked to block
|
|
125
|
+
* server-side for up to GET_QUERY_RESULTS_POLL_MS, but BigQuery may return
|
|
126
|
+
* `jobComplete: false` sooner than requested; this floor keeps the loop from
|
|
127
|
+
* busy-polling in that case. Only the shortfall below it is waited out, so a
|
|
128
|
+
* poll that already blocked longer adds nothing.
|
|
129
|
+
*/
|
|
130
|
+
const GET_QUERY_RESULTS_MIN_POLL_INTERVAL_MS = 1000;
|
|
131
|
+
/**
|
|
132
|
+
* Issue one getQueryResults call via the callback overload so we can read the
|
|
133
|
+
* structured `jobComplete` flag rather than pattern-matching the client's error
|
|
134
|
+
* string. On the still-running path the client invokes the callback with a
|
|
135
|
+
* synthetic "did not complete before ..." Error *and* an apiResponse whose
|
|
136
|
+
* `jobComplete === false`; we key off that boolean, which does not drift across
|
|
137
|
+
* client releases the way the message can.
|
|
138
|
+
*
|
|
139
|
+
* `autoPaginate: false` is forced, not optional: getQueryResults is
|
|
140
|
+
* paginator-wrapped, and at the default the paginator hands the callback the
|
|
141
|
+
* error alone, dropping the apiResponse read here — so every still-running poll
|
|
142
|
+
* would be misread as a fetch error. It does not change what a completed fetch
|
|
143
|
+
* returns.
|
|
144
|
+
*
|
|
145
|
+
* Resolves promptly if `abortSignal` fires, so a caller's timeout/cancel is not
|
|
146
|
+
* left blocked behind BigQuery's server-side wait.
|
|
147
|
+
*/
|
|
148
|
+
function pollQueryResults(job, options, abortSignal) {
|
|
149
|
+
return new Promise(resolve => {
|
|
150
|
+
let settled = false;
|
|
151
|
+
const settle = (outcome) => {
|
|
152
|
+
if (settled) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
settled = true;
|
|
156
|
+
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', onAbort);
|
|
157
|
+
resolve(outcome);
|
|
158
|
+
};
|
|
159
|
+
const onAbort = () => settle({ kind: 'aborted' });
|
|
160
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
161
|
+
settle({ kind: 'aborted' });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', onAbort);
|
|
165
|
+
const pollOptions = { ...options, autoPaginate: false };
|
|
166
|
+
job.getQueryResults(pollOptions, (err, rows, nextQuery, apiResponse) => {
|
|
167
|
+
if ((apiResponse === null || apiResponse === void 0 ? void 0 : apiResponse.jobComplete) === false) {
|
|
168
|
+
settle({ kind: 'stillRunning' });
|
|
169
|
+
}
|
|
170
|
+
else if (err) {
|
|
171
|
+
settle({ kind: 'error', error: err });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
settle({
|
|
175
|
+
kind: 'complete',
|
|
176
|
+
value: [
|
|
177
|
+
rows !== null && rows !== void 0 ? rows : [],
|
|
178
|
+
nextQuery !== null && nextQuery !== void 0 ? nextQuery : null,
|
|
179
|
+
apiResponse,
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Cancel a job without letting a cancel failure mask the caller's real error. */
|
|
187
|
+
async function cancelJobQuietly(job) {
|
|
188
|
+
try {
|
|
189
|
+
await job.cancel();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Best-effort: the job may already be finishing or gone; the caller still
|
|
193
|
+
// reports the timeout that prompted the cancel.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A delay used to space out polls; resolves early if `abortSignal` fires, and is
|
|
198
|
+
* injectable so tests need not wait real time.
|
|
199
|
+
*/
|
|
200
|
+
function delay(ms, abortSignal) {
|
|
201
|
+
return new Promise(resolve => {
|
|
202
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
203
|
+
resolve();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', onAbort);
|
|
208
|
+
resolve();
|
|
209
|
+
}, ms);
|
|
210
|
+
const onAbort = () => {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
resolve();
|
|
213
|
+
};
|
|
214
|
+
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Fetch a job's query results, polling until the job completes, `deadlineMs`
|
|
219
|
+
* elapses, or the caller aborts. A single getQueryResults call cannot wait out a
|
|
220
|
+
* slow query: BigQuery bounds how long one call blocks server-side (its docs
|
|
221
|
+
* note the call typically returns after ~200s even when a larger timeoutMs is
|
|
222
|
+
* requested) and then reports `jobComplete: false` while the job is still
|
|
223
|
+
* running normally. So for a query that runs longer than one call's server wait
|
|
224
|
+
* we re-issue getQueryResults until it finishes.
|
|
225
|
+
*
|
|
226
|
+
* `deadlineMs` bounds the total wait (the connection's configured timeoutMs).
|
|
227
|
+
* On reaching it we cancel the job before throwing: BigQuery's jobTimeoutMs
|
|
228
|
+
* should already be cancelling it server-side, but if that timeout error has not
|
|
229
|
+
* come back yet, cancelling here stops the job from running (and billing) past
|
|
230
|
+
* the point we stopped waiting. `abortSignal` lets the loop exit promptly on an
|
|
231
|
+
* external cancel (the caller cancels the job in that case). A bounded number of
|
|
232
|
+
* retries absorbs the transient access-denied error BigQuery intermittently
|
|
233
|
+
* returns on first fetch. Between polls a minimum interval is enforced so a poll
|
|
234
|
+
* that returns sooner than requested does not spin the loop. `now` and `wait`
|
|
235
|
+
* are injectable for testing.
|
|
236
|
+
*/
|
|
237
|
+
async function getQueryResultsUntilComplete(job, getQueryResultsOptions, deadlineMs, { abortSignal, now = Date.now, wait = delay, } = {}) {
|
|
238
|
+
const startedAt = now();
|
|
239
|
+
let transientRetries = 0;
|
|
240
|
+
for (;;) {
|
|
241
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
242
|
+
throw new Error('BigQuery getQueryResults was aborted before the query completed.');
|
|
243
|
+
}
|
|
244
|
+
const remainingMs = deadlineMs - (now() - startedAt);
|
|
245
|
+
if (remainingMs <= 0) {
|
|
246
|
+
await cancelJobQuietly(job);
|
|
247
|
+
throw new Error(`BigQuery query did not complete within the configured timeout of ${deadlineMs}ms. ` +
|
|
248
|
+
"Raise the connection's timeoutMs to allow longer-running queries.");
|
|
249
|
+
}
|
|
250
|
+
// Clamp the per-poll server wait to what's left of the deadline: a short
|
|
251
|
+
// timeout then fails fast, and the overall deadline can't be overshot by a
|
|
252
|
+
// full poll interval. timeoutMs is applied last so a caller-supplied
|
|
253
|
+
// timeoutMs cannot override the poll interval.
|
|
254
|
+
const pollTimeoutMs = Math.max(1, Math.min(GET_QUERY_RESULTS_POLL_MS, remainingMs));
|
|
255
|
+
const polledAt = now();
|
|
256
|
+
const outcome = await pollQueryResults(job, { ...getQueryResultsOptions, timeoutMs: pollTimeoutMs }, abortSignal);
|
|
257
|
+
switch (outcome.kind) {
|
|
258
|
+
case 'complete':
|
|
259
|
+
return outcome.value;
|
|
260
|
+
case 'stillRunning': {
|
|
261
|
+
// BigQuery can return `jobComplete: false` sooner than the timeoutMs we
|
|
262
|
+
// asked for; without a floor between polls the loop would busy-poll and
|
|
263
|
+
// hammer the API. Wait out only the shortfall below a minimum interval,
|
|
264
|
+
// bounded by the remaining deadline, so a poll that already blocked adds
|
|
265
|
+
// no latency. The abort/deadline checks at the top of the loop then
|
|
266
|
+
// decide whether to continue, and a still-running poll never consumes
|
|
267
|
+
// the transient-retry budget.
|
|
268
|
+
const backoffMs = Math.min(GET_QUERY_RESULTS_MIN_POLL_INTERVAL_MS - (now() - polledAt), deadlineMs - (now() - startedAt));
|
|
269
|
+
if (backoffMs > 0) {
|
|
270
|
+
await wait(backoffMs, abortSignal);
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
case 'aborted':
|
|
275
|
+
throw new Error('BigQuery getQueryResults was aborted before the query completed.');
|
|
276
|
+
case 'error':
|
|
277
|
+
// A (possibly transient) fetch error — e.g. the intermittent
|
|
278
|
+
// access-denied BigQuery returns on first fetch. Retry a bounded number
|
|
279
|
+
// of times, then surface the real error.
|
|
280
|
+
if (transientRetries++ < 3) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
throw outcome.error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
110
287
|
// manage access to BQ, control costs, enforce global data/API limits
|
|
111
288
|
class BigQueryConnection extends connection_1.BaseConnection {
|
|
112
289
|
constructor(arg, queryOptions, config = {}) {
|
|
@@ -197,7 +374,8 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
197
374
|
const totalRows = +(((_a = jobResult[2]) === null || _a === void 0 ? void 0 : _a.totalRows)
|
|
198
375
|
? jobResult[2].totalRows
|
|
199
376
|
: '0');
|
|
200
|
-
//
|
|
377
|
+
// jobComplete is guaranteed here: getQueryResultsUntilComplete only
|
|
378
|
+
// returns once BigQuery reports the job complete (it polls otherwise).
|
|
201
379
|
const queryCostBytes = (_b = jobResult[2]) === null || _b === void 0 ? void 0 : _b.totalBytesProcessed;
|
|
202
380
|
const data = {
|
|
203
381
|
rows: jobResult[0],
|
|
@@ -542,9 +720,18 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
542
720
|
}
|
|
543
721
|
throw lastFetchError;
|
|
544
722
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
723
|
+
/**
|
|
724
|
+
* The effective query timeout in milliseconds. A configured value that is
|
|
725
|
+
* unset, blank, non-numeric, zero, or negative falls back to TIMEOUT_MS; only
|
|
726
|
+
* a positive number overrides the default. The result bounds both the
|
|
727
|
+
* BigQuery job (jobTimeoutMs) and the getQueryResultsUntilComplete poll
|
|
728
|
+
* deadline, so it must be positive: a zero or negative would make the job
|
|
729
|
+
* cancel on the first poll instead of running.
|
|
730
|
+
*/
|
|
731
|
+
resolvedTimeoutMs() {
|
|
732
|
+
const configured = Number(this.config.timeoutMs);
|
|
733
|
+
return configured > 0 ? configured : TIMEOUT_MS;
|
|
734
|
+
}
|
|
548
735
|
async createBigQueryJobAndGetResults(sqlCommand, createQueryJobOptions, getQueryResultsOptions, abortSignal) {
|
|
549
736
|
try {
|
|
550
737
|
const job = await this.createBigQueryJob({
|
|
@@ -555,37 +742,29 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
555
742
|
job.cancel();
|
|
556
743
|
};
|
|
557
744
|
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', cancel);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return num;
|
|
574
|
-
}
|
|
575
|
-
return String(val);
|
|
576
|
-
},
|
|
745
|
+
try {
|
|
746
|
+
// Poll for results until the job completes or the connection's
|
|
747
|
+
// configured timeout elapses; a single getQueryResults call can't wait
|
|
748
|
+
// out a long-running query (see getQueryResultsUntilComplete). The
|
|
749
|
+
// deadline is the same resolved timeout that bounds the job's
|
|
750
|
+
// jobTimeoutMs, so the client stops waiting right about when BigQuery
|
|
751
|
+
// stops running the job.
|
|
752
|
+
return await getQueryResultsUntilComplete(job, {
|
|
753
|
+
wrapIntegers: {
|
|
754
|
+
integerTypeCastFunction: (val) => {
|
|
755
|
+
const num = Number(val);
|
|
756
|
+
if (Number.isSafeInteger(num)) {
|
|
757
|
+
return num;
|
|
758
|
+
}
|
|
759
|
+
return String(val);
|
|
577
760
|
},
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
finally {
|
|
585
|
-
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', cancel);
|
|
586
|
-
}
|
|
761
|
+
},
|
|
762
|
+
...getQueryResultsOptions,
|
|
763
|
+
}, this.resolvedTimeoutMs(), { abortSignal });
|
|
764
|
+
}
|
|
765
|
+
finally {
|
|
766
|
+
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', cancel);
|
|
587
767
|
}
|
|
588
|
-
throw lastFetchError;
|
|
589
768
|
}
|
|
590
769
|
catch (e) {
|
|
591
770
|
throw maybeRewriteError(e);
|
|
@@ -599,7 +778,10 @@ class BigQueryConnection extends connection_1.BaseConnection {
|
|
|
599
778
|
const [job] = await this.bigQuery.createQueryJob({
|
|
600
779
|
location: this.location,
|
|
601
780
|
maximumBytesBilled: this.config.maximumBytesBilled || MAXIMUM_BYTES_BILLED,
|
|
602
|
-
|
|
781
|
+
// Shadow the resolved timeout to the job's server-side limit so BigQuery
|
|
782
|
+
// cancels a runaway job on its own; getQueryResultsUntilComplete polls up
|
|
783
|
+
// to the same deadline on the client side.
|
|
784
|
+
jobTimeoutMs: this.resolvedTimeoutMs(),
|
|
603
785
|
...options,
|
|
604
786
|
});
|
|
605
787
|
return job;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/db-bigquery",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.427",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@google-cloud/bigquery": "7.9.4",
|
|
27
27
|
"@google-cloud/common": "5.0.2",
|
|
28
28
|
"@google-cloud/paginator": "5.0.2",
|
|
29
|
-
"@malloydata/malloy": "0.0.
|
|
29
|
+
"@malloydata/malloy": "0.0.427",
|
|
30
30
|
"gaxios": "^4.2.0"
|
|
31
31
|
}
|
|
32
32
|
}
|