@malloydata/db-bigquery 0.0.425 → 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.
@@ -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,10 +122,19 @@ 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>;
103
- runSQLStream(sqlCommand: string, { rowLimit, abortSignal }?: RunSQLOptions): AsyncIterableIterator<QueryRecord>;
137
+ runSQLStream(sqlCommand: string, { rowLimit, abortSignal, queryMetadata }?: RunSQLOptions): AsyncIterableIterator<QueryRecord>;
104
138
  fetchTableMetadata(tablePath: string): Promise<TableMetadata>;
105
139
  close(): Promise<void>;
106
140
  }
@@ -38,11 +38,41 @@ 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");
44
45
  const malloy_1 = require("@malloydata/malloy");
45
46
  const connection_1 = require("@malloydata/malloy/connection");
47
+ // BigQuery label grammar: keys and values are lowercase, <=63 chars, and
48
+ // [a-z0-9_-]; keys must start with a lowercase letter. Values are transformed
49
+ // to fit (lowercase, disallowed chars -> '_', truncate); a key that can't be
50
+ // made valid (e.g. it starts with a digit) is dropped. BigQuery's 64-label cap
51
+ // needs no check here: the contract allows fewer properties than that.
52
+ const BQ_MAX_LEN = 63;
53
+ function sanitizeBigQueryValue(value) {
54
+ return value
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9_-]/g, '_')
57
+ .slice(0, BQ_MAX_LEN);
58
+ }
59
+ function sanitizeBigQueryKey(key) {
60
+ const sanitized = sanitizeBigQueryValue(key);
61
+ return /^[a-z]/.test(sanitized) ? sanitized : undefined;
62
+ }
63
+ /** Render a query's metadata as BigQuery job labels, in BQ's grammar. */
64
+ function toBigQueryLabels(labels) {
65
+ if (labels === undefined)
66
+ return undefined;
67
+ const out = {};
68
+ for (const [key, value] of Object.entries(labels)) {
69
+ const sanitizedKey = sanitizeBigQueryKey(key);
70
+ if (sanitizedKey === undefined)
71
+ continue; // can't be a valid BQ key; drop
72
+ out[sanitizedKey] = sanitizeBigQueryValue(value);
73
+ }
74
+ return Object.keys(out).length > 0 ? out : undefined;
75
+ }
46
76
  // BigQuery SDK apparently throws various authentication errors from Gaxios (https://github.com/googleapis/gaxios) and from
47
77
  // @google-cloud/common (https://www.npmjs.com/package/@google-cloud/common)
48
78
  // catching and rewriting here a single kind of authentication error we can consistently catch further up the stack
@@ -75,9 +105,185 @@ const maybeRewriteError = (e) => {
75
105
  */
76
106
  const MAXIMUM_BYTES_BILLED = String(25 * 1024 * 1024 * 1024);
77
107
  /**
78
- * Default timeoutMs value, 10 Mins
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.
79
114
  */
80
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
+ }
81
287
  // manage access to BQ, control costs, enforce global data/API limits
82
288
  class BigQueryConnection extends connection_1.BaseConnection {
83
289
  constructor(arg, queryOptions, config = {}) {
@@ -153,20 +359,23 @@ class BigQueryConnection extends connection_1.BaseConnection {
153
359
  get supportsNesting() {
154
360
  return true;
155
361
  }
156
- async _runSQL(sqlCommand, { rowLimit, abortSignal } = {}, rowIndex = 0) {
362
+ async _runSQL(sqlCommand, options = {}, rowIndex = 0) {
157
363
  var _a, _b, _c;
364
+ const { rowLimit, abortSignal } = options;
158
365
  const defaultOptions = this.readQueryOptions();
159
366
  const pageSize = rowLimit !== null && rowLimit !== void 0 ? rowLimit : defaultOptions.rowLimit;
367
+ const perCallLabels = toBigQueryLabels(this.queryMetadataBag(options.queryMetadata));
160
368
  try {
161
369
  const queryResultsOptions = {
162
370
  maxResults: pageSize,
163
371
  startIndex: rowIndex.toString(),
164
372
  };
165
- const jobResult = await this.createBigQueryJobAndGetResults(sqlCommand, undefined, queryResultsOptions, abortSignal);
373
+ const jobResult = await this.createBigQueryJobAndGetResults(sqlCommand, perCallLabels ? { labels: perCallLabels } : undefined, queryResultsOptions, abortSignal);
166
374
  const totalRows = +(((_a = jobResult[2]) === null || _a === void 0 ? void 0 : _a.totalRows)
167
375
  ? jobResult[2].totalRows
168
376
  : '0');
169
- // TODO even though we have 10 minute timeout limit, we still should confirm that resulting metadata has "jobComplete: true"
377
+ // jobComplete is guaranteed here: getQueryResultsUntilComplete only
378
+ // returns once BigQuery reports the job complete (it polls otherwise).
170
379
  const queryCostBytes = (_b = jobResult[2]) === null || _b === void 0 ? void 0 : _b.totalBytesProcessed;
171
380
  const data = {
172
381
  rows: jobResult[0],
@@ -511,9 +720,18 @@ class BigQueryConnection extends connection_1.BaseConnection {
511
720
  }
512
721
  throw lastFetchError;
513
722
  }
514
- // TODO this needs to extend the wait for results using a timeout set by the user,
515
- // and probably needs to loop to check for results - BQ docs now say that after ~2min of waiting,
516
- // no matter what you set for timeoutMs, they will probably just return.
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
+ }
517
735
  async createBigQueryJobAndGetResults(sqlCommand, createQueryJobOptions, getQueryResultsOptions, abortSignal) {
518
736
  try {
519
737
  const job = await this.createBigQueryJob({
@@ -524,37 +742,29 @@ class BigQueryConnection extends connection_1.BaseConnection {
524
742
  job.cancel();
525
743
  };
526
744
  abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', cancel);
527
- // TODO we should check if this is still required?
528
- // We do a simple retry-loop here, as a temporary fix for a transient
529
- // error in which sometimes requesting results from a job yields an
530
- // access denied error. It seems that in these cases, simply trying again
531
- // solves the problem. This is being currently investigated by
532
- // @christopherswenson and @lloydtabb.
533
- let lastFetchError;
534
- for (let retries = 0; retries < 3; retries++) {
535
- try {
536
- return await job.getQueryResults({
537
- timeoutMs: 1000 * 60 * 2, // TODO - this requires some rethinking, and is a hack to resolve some issues. talk to @bporterfield
538
- wrapIntegers: {
539
- integerTypeCastFunction: (val) => {
540
- const num = Number(val);
541
- if (Number.isSafeInteger(num)) {
542
- return num;
543
- }
544
- return String(val);
545
- },
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);
546
760
  },
547
- ...getQueryResultsOptions,
548
- });
549
- }
550
- catch (fetchError) {
551
- lastFetchError = fetchError;
552
- }
553
- finally {
554
- abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', cancel);
555
- }
761
+ },
762
+ ...getQueryResultsOptions,
763
+ }, this.resolvedTimeoutMs(), { abortSignal });
764
+ }
765
+ finally {
766
+ abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', cancel);
556
767
  }
557
- throw lastFetchError;
558
768
  }
559
769
  catch (e) {
560
770
  throw maybeRewriteError(e);
@@ -568,7 +778,10 @@ class BigQueryConnection extends connection_1.BaseConnection {
568
778
  const [job] = await this.bigQuery.createQueryJob({
569
779
  location: this.location,
570
780
  maximumBytesBilled: this.config.maximumBytesBilled || MAXIMUM_BYTES_BILLED,
571
- jobTimeoutMs: Number(this.config.timeoutMs) || TIMEOUT_MS,
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(),
572
785
  ...options,
573
786
  });
574
787
  return job;
@@ -581,7 +794,8 @@ class BigQueryConnection extends connection_1.BaseConnection {
581
794
  const url = `https://console.cloud.google.com/bigquery?project=${this.billingProjectId}&j=bq:${job.location}:${job.id}&page=queryresults`;
582
795
  return url;
583
796
  }
584
- runSQLStream(sqlCommand, { rowLimit, abortSignal } = {}) {
797
+ runSQLStream(sqlCommand, { rowLimit, abortSignal, queryMetadata } = {}) {
798
+ const labels = toBigQueryLabels(this.queryMetadataBag(queryMetadata));
585
799
  const streamBigQuery = (onError, onData, onEnd) => {
586
800
  let index = 0;
587
801
  function handleData(rowMetadata) {
@@ -592,8 +806,9 @@ class BigQueryConnection extends connection_1.BaseConnection {
592
806
  this.end();
593
807
  }
594
808
  }
809
+ const query = this.prependSetupSQL(sqlCommand);
595
810
  this.bigQuery
596
- .createQueryStream(this.prependSetupSQL(sqlCommand))
811
+ .createQueryStream(labels ? { query, labels } : query)
597
812
  .on('error', onError)
598
813
  .on('data', handleData)
599
814
  .on('end', onEnd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/db-bigquery",
3
- "version": "0.0.425",
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.425",
29
+ "@malloydata/malloy": "0.0.427",
30
30
  "gaxios": "^4.2.0"
31
31
  }
32
32
  }