@dalgo/bigquery 0.0.0-stage → 0.2.0

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +472 -2
  3. package/dist/analytical/canonical.d.ts +10 -0
  4. package/dist/analytical/canonical.d.ts.map +1 -0
  5. package/dist/analytical/canonical.js +50 -0
  6. package/dist/analytical/client.d.ts +84 -0
  7. package/dist/analytical/client.d.ts.map +1 -0
  8. package/dist/analytical/client.js +730 -0
  9. package/dist/analytical/deadline.d.ts +13 -0
  10. package/dist/analytical/deadline.d.ts.map +1 -0
  11. package/dist/analytical/deadline.js +23 -0
  12. package/dist/analytical/google-identity.d.ts +40 -0
  13. package/dist/analytical/google-identity.d.ts.map +1 -0
  14. package/dist/analytical/google-identity.js +158 -0
  15. package/dist/analytical/ledger.d.ts +80 -0
  16. package/dist/analytical/ledger.d.ts.map +1 -0
  17. package/dist/analytical/ledger.js +89 -0
  18. package/dist/analytical/metadata-client.d.ts +66 -0
  19. package/dist/analytical/metadata-client.d.ts.map +1 -0
  20. package/dist/analytical/metadata-client.js +247 -0
  21. package/dist/analytical/metadata-harness.d.ts +28 -0
  22. package/dist/analytical/metadata-harness.d.ts.map +1 -0
  23. package/dist/analytical/metadata-harness.js +127 -0
  24. package/dist/analytical/metadata.d.ts +8 -0
  25. package/dist/analytical/metadata.d.ts.map +1 -0
  26. package/dist/analytical/metadata.js +155 -0
  27. package/dist/analytical/protocol.d.ts +157 -0
  28. package/dist/analytical/protocol.d.ts.map +1 -0
  29. package/dist/analytical/protocol.js +320 -0
  30. package/dist/analytical/public-metadata.d.ts +29 -0
  31. package/dist/analytical/public-metadata.d.ts.map +1 -0
  32. package/dist/analytical/public-metadata.js +70 -0
  33. package/dist/analytical/transport.d.ts +39 -0
  34. package/dist/analytical/transport.d.ts.map +1 -0
  35. package/dist/analytical/transport.js +224 -0
  36. package/dist/analytical/values.d.ts +18 -0
  37. package/dist/analytical/values.d.ts.map +1 -0
  38. package/dist/analytical/values.js +157 -0
  39. package/dist/analytical/wire.d.ts +26 -0
  40. package/dist/analytical/wire.d.ts.map +1 -0
  41. package/dist/analytical/wire.js +141 -0
  42. package/dist/analytical.d.ts +27 -0
  43. package/dist/analytical.d.ts.map +1 -0
  44. package/dist/analytical.js +14 -0
  45. package/dist/database.d.ts +35 -0
  46. package/dist/database.d.ts.map +1 -0
  47. package/dist/database.js +437 -0
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +2 -0
  51. package/dist/sql.d.ts +29 -0
  52. package/dist/sql.d.ts.map +1 -0
  53. package/dist/sql.js +198 -0
  54. package/dist/types.d.ts +49 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +1 -0
  57. package/package.json +58 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EAGH,KAAK,KAAK,EACV,KAAK,QAAQ,EAEb,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAkB,uBAAuB,EAAwC,MAAM,YAAY,CAAC;AAoLhH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAgB,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE,MAAM;CAKlC;AAED,qGAAqG;AACrG;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,QAAQ;;gBAY5B,OAAO,EAAE,uBAAuB;IAgBtC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IAa9D,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAgCzF,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IA2BvE,oFAAoF;IAC7E,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvE,mFAAmF;IAC5E,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1E,MAAM,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAKhC,uBAAuB,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,WAAW,EAAE,oBAAoB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAKzH,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,QAAQ;YAKF,QAAQ;YAsCR,OAAO;YAoBP,OAAO;YA0CP,YAAY;CAW3B"}
@@ -0,0 +1,437 @@
1
+ import { Key, UnsupportedError, identityCodec, } from "@dalgo/core";
2
+ import { compileBigQueryQuery, parameter, quoteTable, validateIdentifier, validateProjectId } from "./sql.js";
3
+ const apiBase = "https://bigquery.googleapis.com/bigquery/v2";
4
+ function isObject(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function asResult(value, context) {
8
+ if (!isObject(value))
9
+ throw new TypeError(`malformed BigQuery ${context} response`);
10
+ if (value.jobComplete !== undefined && typeof value.jobComplete !== "boolean") {
11
+ throw new TypeError(`malformed BigQuery ${context} jobComplete`);
12
+ }
13
+ if (value.rows !== undefined && !Array.isArray(value.rows))
14
+ throw new TypeError(`malformed BigQuery ${context} rows`);
15
+ if (value.pageToken !== undefined && typeof value.pageToken !== "string")
16
+ throw new TypeError(`malformed BigQuery ${context} pageToken`);
17
+ if (value.errors !== undefined && !Array.isArray(value.errors))
18
+ throw new TypeError(`malformed BigQuery ${context} errors`);
19
+ return value;
20
+ }
21
+ function identityOr(codec) {
22
+ return (codec ?? identityCodec);
23
+ }
24
+ function exactPositiveInteger(value, fallback, label) {
25
+ const result = value ?? fallback;
26
+ if (!Number.isSafeInteger(result) || result < 1)
27
+ throw new TypeError(`${label} must be a positive safe integer`);
28
+ return result;
29
+ }
30
+ function optionalBytes(value) {
31
+ if (value === undefined)
32
+ return undefined;
33
+ const asString = typeof value === "bigint" ? value.toString() : String(value);
34
+ if (!/^\d+$/u.test(asString))
35
+ throw new TypeError("maximumBytesBilled must be a non-negative integer");
36
+ return asString;
37
+ }
38
+ function metadata(result) {
39
+ return {
40
+ ...(result.jobReference?.jobId === undefined ? {} : { jobId: result.jobReference.jobId }),
41
+ ...(result.totalBytesProcessed === undefined ? {} : { totalBytesProcessed: result.totalBytesProcessed }),
42
+ ...(result.totalBytesBilled === undefined ? {} : { totalBytesBilled: result.totalBytesBilled }),
43
+ ...(result.cacheHit === undefined ? {} : { cacheHit: result.cacheHit }),
44
+ };
45
+ }
46
+ function schemaTypeMatches(expected, actual) {
47
+ const normalized = actual?.toUpperCase();
48
+ if (expected.type === "BOOL")
49
+ return normalized === "BOOL" || normalized === "BOOLEAN";
50
+ if (expected.type === "INT64")
51
+ return normalized === "INT64" || normalized === "INTEGER";
52
+ if (expected.type === "FLOAT64")
53
+ return normalized === "FLOAT64" || normalized === "FLOAT";
54
+ return normalized === expected.type;
55
+ }
56
+ function rowsFrom(result, context, table) {
57
+ const fields = result.schema?.fields;
58
+ if (!Array.isArray(fields))
59
+ throw new TypeError(`BigQuery ${context} response omitted result schema`);
60
+ const expected = [
61
+ { name: "__dalgo_key", column: table.keyColumn },
62
+ ...Object.entries(table.columns).map(([name, column]) => ({ name, column })),
63
+ ];
64
+ if (fields.length !== expected.length)
65
+ throw new TypeError(`malformed BigQuery ${context} projection`);
66
+ const names = new Set();
67
+ for (let index = 0; index < fields.length; index += 1) {
68
+ const field = fields[index];
69
+ const expectedField = expected[index];
70
+ if (field === undefined || expectedField === undefined || typeof field.name !== "string" || names.has(field.name)
71
+ || field.name !== expectedField.name || !schemaTypeMatches(expectedField.column, field.type)) {
72
+ throw new TypeError(`malformed BigQuery ${context} schema`);
73
+ }
74
+ names.add(field.name);
75
+ }
76
+ const rows = result.rows ?? [];
77
+ return rows.map((row, rowIndex) => {
78
+ if (!isObject(row) || !Array.isArray(row.f) || row.f.length !== fields.length) {
79
+ throw new TypeError(`malformed BigQuery ${context} row ${String(rowIndex)}`);
80
+ }
81
+ const decoded = {};
82
+ for (let index = 0; index < fields.length; index += 1) {
83
+ const field = fields[index];
84
+ const cell = row.f[index];
85
+ if (field === undefined || cell === undefined || !isObject(cell) || !("v" in cell)) {
86
+ throw new TypeError(`malformed BigQuery ${context} cell ${String(index)}`);
87
+ }
88
+ // Deliberately preserve BigQuery's JSON wire value (for example INT64 stays a string).
89
+ decoded[field.name] = cell.v;
90
+ }
91
+ return decoded;
92
+ });
93
+ }
94
+ function keyWireValue(row) {
95
+ const value = row.__dalgo_key;
96
+ if (typeof value !== "string" && typeof value !== "number") {
97
+ throw new TypeError("BigQuery key column must produce a string or number wire value");
98
+ }
99
+ return String(value);
100
+ }
101
+ function recordData(row) {
102
+ const data = { ...row };
103
+ delete data.__dalgo_key;
104
+ return data;
105
+ }
106
+ function assertNoParent(key) {
107
+ if (key.parent !== undefined)
108
+ throw new UnsupportedError("BigQuery nested collection keys");
109
+ }
110
+ function validateTable(table) {
111
+ validateIdentifier(table.datasetId, "datasetId");
112
+ validateIdentifier(table.tableId, "tableId");
113
+ validateIdentifier(table.keyColumn.column, "key column");
114
+ const columns = new Set([table.keyColumn.column]);
115
+ for (const [field, column] of Object.entries(table.columns)) {
116
+ if (field === "__dalgo_key")
117
+ throw new TypeError("__dalgo_key is reserved by the BigQuery adapter");
118
+ validateIdentifier(field, "mapped field");
119
+ validateIdentifier(column.column, "mapped column");
120
+ if (columns.has(column.column))
121
+ throw new TypeError(`duplicate BigQuery mapped column: ${column.column}`);
122
+ columns.add(column.column);
123
+ }
124
+ }
125
+ function frozenColumn(column) {
126
+ return Object.freeze({ column: column.column, type: column.type, ...(column.nullable === undefined ? {} : { nullable: column.nullable }) });
127
+ }
128
+ function frozenTables(tables) {
129
+ const copy = {};
130
+ for (const [collection, table] of Object.entries(tables)) {
131
+ validateIdentifier(collection, "collection name");
132
+ validateTable(table);
133
+ const columns = {};
134
+ for (const [field, column] of Object.entries(table.columns))
135
+ columns[field] = frozenColumn(column);
136
+ copy[collection] = Object.freeze({
137
+ datasetId: table.datasetId,
138
+ tableId: table.tableId,
139
+ keyColumn: frozenColumn(table.keyColumn),
140
+ columns: Object.freeze(columns),
141
+ });
142
+ }
143
+ return Object.freeze(copy);
144
+ }
145
+ export class BigQueryHttpError extends Error {
146
+ status;
147
+ constructor(status) {
148
+ super(`BigQuery request failed with HTTP ${String(status)}`);
149
+ this.name = "BigQueryHttpError";
150
+ this.status = status;
151
+ }
152
+ }
153
+ /** A completed BigQuery job reported a failure. The API error body is intentionally not retained. */
154
+ /**
155
+ * DALgo adapter over BigQuery's REST jobs.query and jobs.getQueryResults APIs.
156
+ * It intentionally never accepts a service-account key or embeds access tokens.
157
+ */
158
+ export class BigQueryDatabase {
159
+ #projectId;
160
+ #tables;
161
+ #accessToken;
162
+ #location;
163
+ #maximumBytesBilled;
164
+ #maxRows;
165
+ #pageSize;
166
+ #timeoutMs;
167
+ #initialWaitMs;
168
+ #fetch;
169
+ constructor(options) {
170
+ validateProjectId(options.projectId);
171
+ if (typeof options.accessToken !== "function")
172
+ throw new TypeError("accessToken must be a function returning an OAuth token");
173
+ if (options.location !== undefined && options.location.trim().length === 0)
174
+ throw new TypeError("location must not be empty");
175
+ this.#projectId = options.projectId;
176
+ this.#tables = frozenTables(options.tables);
177
+ this.#accessToken = options.accessToken;
178
+ this.#location = options.location;
179
+ this.#maximumBytesBilled = optionalBytes(options.maximumBytesBilled);
180
+ this.#maxRows = exactPositiveInteger(options.maxRows, 1000, "maxRows");
181
+ this.#pageSize = exactPositiveInteger(options.pageSize, 500, "pageSize");
182
+ this.#timeoutMs = exactPositiveInteger(options.timeoutMs, 30_000, "timeoutMs");
183
+ this.#initialWaitMs = exactPositiveInteger(options.initialWaitMs, 1000, "initialWaitMs");
184
+ this.#fetch = options.fetch ?? globalThis.fetch;
185
+ }
186
+ async get(key, codec) {
187
+ const table = this.tableFor(key);
188
+ const query = this.selectForTable(table, `WHERE t.\`${table.keyColumn.column}\` = @key LIMIT 2`, [parameter("key", table.keyColumn, key.id)]);
189
+ const run = await this.runQuery(query, this.#pageSize);
190
+ const rows = await this.allRows(run, table);
191
+ if (rows.length === 0)
192
+ return { key, exists: false };
193
+ if (rows.length !== 1)
194
+ throw new UnsupportedError("BigQuery key mapping returned more than one row");
195
+ const row = rows[0];
196
+ if (row === undefined)
197
+ throw new Error("BigQuery query lost its only row");
198
+ if (keyWireValue(row) !== String(key.id))
199
+ throw new TypeError("BigQuery point read returned a different key");
200
+ return this.snapshot(key, row, codec, run.metadata);
201
+ }
202
+ async getMany(keys, codec) {
203
+ if (keys.length === 0)
204
+ return [];
205
+ if (keys.length > this.#maxRows)
206
+ throw new UnsupportedError(`BigQuery getMany above configured maxRows (${String(this.#maxRows)})`);
207
+ const firstKey = keys[0];
208
+ if (firstKey === undefined)
209
+ throw new Error("BigQuery getMany lost its first key");
210
+ const table = this.tableFor(firstKey);
211
+ for (const key of keys) {
212
+ if (this.tableFor(key) !== table)
213
+ throw new UnsupportedError("BigQuery getMany across different table mappings");
214
+ }
215
+ const values = keys.map((key) => key.id);
216
+ const arrayParameter = {
217
+ name: "keys",
218
+ parameterType: { type: "ARRAY", arrayType: { type: table.keyColumn.type } },
219
+ parameterValue: { arrayValues: values.map((value) => parameter("value", table.keyColumn, value).parameterValue) },
220
+ };
221
+ const query = this.selectForTable(table, `WHERE t.\`${table.keyColumn.column}\` IN UNNEST(@keys) LIMIT ${String(this.#maxRows + 1)}`, [arrayParameter]);
222
+ const run = await this.runQuery(query, Math.min(this.#pageSize, keys.length));
223
+ const rows = await this.allRows(run, table);
224
+ if (rows.length > keys.length)
225
+ throw new UnsupportedError("BigQuery key mapping returned duplicate rows");
226
+ const returnedKeys = rows.map((row) => keyWireValue(row));
227
+ if (new Set(returnedKeys).size !== returnedKeys.length) {
228
+ throw new UnsupportedError("BigQuery key mapping returned duplicate rows");
229
+ }
230
+ const returned = new Map(rows.map((row) => [keyWireValue(row), row]));
231
+ const requestedKeys = new Set(keys.map((key) => String(key.id)));
232
+ if (returnedKeys.some((key) => !requestedKeys.has(key)))
233
+ throw new TypeError("BigQuery getMany returned an unrequested key");
234
+ return keys.map((key) => {
235
+ const row = returned.get(String(key.id));
236
+ return row === undefined ? { key, exists: false } : this.snapshot(key, row, codec, run.metadata);
237
+ });
238
+ }
239
+ async query(query) {
240
+ if (query.source.kind !== "collection" || query.source.parent !== undefined) {
241
+ throw new UnsupportedError("BigQuery collection-group or nested collection queries");
242
+ }
243
+ const table = this.tableForCollection(query.source.name);
244
+ if ((query.offset ?? 0) !== 0)
245
+ throw new UnsupportedError("BigQuery offset queries");
246
+ const requested = query.limit ?? this.#maxRows;
247
+ if (requested > this.#maxRows)
248
+ throw new UnsupportedError(`BigQuery query limit above configured maxRows (${String(this.#maxRows)})`);
249
+ const compiled = compileBigQueryQuery(this.#projectId, table, query, requested + 1);
250
+ const run = await this.runQuery({ sql: compiled.sql, parameters: compiled.parameters }, Math.min(this.#pageSize, requested + 1));
251
+ const rows = await this.allRows(run, table);
252
+ const hasMore = rows.length > requested;
253
+ const selected = rows.slice(0, requested);
254
+ const records = selected.map((row) => {
255
+ const key = new Key(query.source.name, keyWireValue(row));
256
+ return this.snapshot(key, row, query.source.codec, run.metadata);
257
+ });
258
+ const last = selected.at(-1);
259
+ const nextCursor = hasMore && last !== undefined && query.orders.length > 0
260
+ ? { values: compiled.cursorColumns.map((column) => last[column.column === table.keyColumn.column ? "__dalgo_key" : this.fieldForColumn(table, column.column)]) }
261
+ : undefined;
262
+ if (nextCursor?.values.some((value) => value === null || value === undefined)) {
263
+ throw new UnsupportedError("BigQuery pagination over null cursor values");
264
+ }
265
+ return { records, ...(nextCursor === undefined ? {} : { nextCursor }) };
266
+ }
267
+ /** BigQuery cannot atomically enforce a unique key, so DALgo insert is rejected. */
268
+ insert(_key, _data, _codec) {
269
+ void [_key, _data, _codec];
270
+ return Promise.reject(new UnsupportedError("BigQuery insert without an enforced unique-key constraint"));
271
+ }
272
+ /** BigQuery MERGE cannot prove DALgo replacement semantics for non-unique keys. */
273
+ set(_key, _data, _codec) {
274
+ void [_key, _data, _codec];
275
+ return Promise.reject(new UnsupportedError("BigQuery set/upsert without an enforced unique-key constraint"));
276
+ }
277
+ update(_key, _data) {
278
+ void [_key, _data];
279
+ return Promise.reject(new UnsupportedError("BigQuery update without an atomically enforced unique-key constraint"));
280
+ }
281
+ delete(_key) {
282
+ void _key;
283
+ return Promise.reject(new UnsupportedError("BigQuery delete without an atomically enforced unique-key constraint"));
284
+ }
285
+ runReadwriteTransaction(callback) {
286
+ if (typeof callback !== "function")
287
+ return Promise.reject(new TypeError("transaction callback is required"));
288
+ return Promise.reject(new UnsupportedError("BigQuery callback transactions"));
289
+ }
290
+ tableFor(key) {
291
+ assertNoParent(key);
292
+ return this.tableForCollection(key.collection);
293
+ }
294
+ tableForCollection(collection) {
295
+ const table = this.#tables[collection];
296
+ if (table === undefined)
297
+ throw new UnsupportedError(`BigQuery collection has no table mapping: ${collection}`);
298
+ return table;
299
+ }
300
+ fieldForColumn(table, column) {
301
+ const field = Object.entries(table.columns).find(([, mapped]) => mapped.column === column)?.[0];
302
+ if (field === undefined)
303
+ throw new TypeError(`BigQuery cursor column is not mapped: ${column}`);
304
+ return field;
305
+ }
306
+ selectForTable(table, suffix, parameters) {
307
+ const columns = [
308
+ `t.\`${table.keyColumn.column}\` AS \`__dalgo_key\``,
309
+ ...Object.entries(table.columns).map(([field, column]) => `t.\`${column.column}\` AS \`${field}\``),
310
+ ];
311
+ return { sql: `SELECT ${columns.join(", ")} FROM ${quoteTable(this.#projectId, table)} AS t ${suffix}`, parameters };
312
+ }
313
+ snapshot(key, row, codec, queryMetadata) {
314
+ return { key, exists: true, data: identityOr(codec).decode(recordData(row)), metadata: { ...queryMetadata } };
315
+ }
316
+ async runQuery(query, maxResults) {
317
+ const deadline = Date.now() + this.#timeoutMs;
318
+ const first = asResult(await this.request("POST", `/projects/${encodeURIComponent(this.#projectId)}/queries`, {
319
+ query: query.sql,
320
+ useLegacySql: false,
321
+ parameterMode: "NAMED",
322
+ queryParameters: query.parameters,
323
+ timeoutMs: Math.min(this.#initialWaitMs, this.#timeoutMs),
324
+ jobTimeoutMs: String(this.#timeoutMs),
325
+ maxResults,
326
+ ...(this.#location === undefined ? {} : { location: this.#location }),
327
+ ...(this.#maximumBytesBilled === undefined ? {} : { maximumBytesBilled: this.#maximumBytesBilled }),
328
+ }, deadline), "jobs.query");
329
+ if (first.jobComplete === true) {
330
+ const reference = first.jobReference?.jobId === undefined ? undefined : { jobId: first.jobReference.jobId, ...(first.jobReference.location === undefined ? {} : { location: first.jobReference.location }) };
331
+ return { result: first, metadata: metadata(first), deadline, ...(reference === undefined ? {} : { reference }) };
332
+ }
333
+ const reference = first.jobReference;
334
+ if (reference?.jobId === undefined)
335
+ throw new TypeError("BigQuery unfinished query omitted job reference");
336
+ let delay = 100;
337
+ let result = first;
338
+ while (result.jobComplete !== true) {
339
+ if (Date.now() >= deadline)
340
+ throw new UnsupportedError("BigQuery query exceeded configured polling timeout");
341
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delay, Math.max(0, deadline - Date.now()))));
342
+ delay = Math.min(delay * 2, 1000);
343
+ const params = new URLSearchParams({ maxResults: String(maxResults) });
344
+ const location = reference.location ?? this.#location;
345
+ if (location !== undefined)
346
+ params.set("location", location);
347
+ result = asResult(await this.request("GET", `/projects/${encodeURIComponent(this.#projectId)}/queries/${encodeURIComponent(reference.jobId)}?${params.toString()}`, undefined, deadline), "jobs.getQueryResults");
348
+ }
349
+ return {
350
+ result,
351
+ metadata: { ...metadata(result), jobId: reference.jobId },
352
+ deadline,
353
+ reference: { jobId: reference.jobId, ...(reference.location === undefined ? {} : { location: reference.location }) },
354
+ };
355
+ }
356
+ async allRows(run, table) {
357
+ const rows = [...rowsFrom(run.result, "query", table)];
358
+ let pageToken = run.result.pageToken === "" ? undefined : run.result.pageToken;
359
+ const reference = run.reference;
360
+ const seenTokens = new Set();
361
+ while (pageToken !== undefined && rows.length < this.#maxRows + 1) {
362
+ if (reference === undefined)
363
+ throw new TypeError("BigQuery paged response omitted job reference");
364
+ if (seenTokens.has(pageToken))
365
+ throw new TypeError("BigQuery result paging repeated a page token");
366
+ seenTokens.add(pageToken);
367
+ const params = new URLSearchParams({ pageToken, maxResults: String(this.#pageSize) });
368
+ const location = reference.location ?? this.#location;
369
+ if (location !== undefined)
370
+ params.set("location", location);
371
+ const page = asResult(await this.request("GET", `/projects/${encodeURIComponent(this.#projectId)}/queries/${encodeURIComponent(reference.jobId)}?${params.toString()}`, undefined, run.deadline), "jobs.getQueryResults page");
372
+ if (page.jobComplete !== true)
373
+ throw new TypeError("BigQuery result page was not complete");
374
+ rows.push(...rowsFrom(page, "query page", table));
375
+ pageToken = page.pageToken === "" ? undefined : page.pageToken;
376
+ }
377
+ return rows;
378
+ }
379
+ async request(method, path, body, deadline) {
380
+ const token = await this.withDeadline(Promise.resolve().then(this.#accessToken), deadline, "BigQuery access token provider failed");
381
+ if (typeof token !== "string" || token.trim().length === 0)
382
+ throw new TypeError("accessToken must return a non-empty OAuth token");
383
+ const remaining = deadline - Date.now();
384
+ if (remaining <= 0)
385
+ throw new UnsupportedError("BigQuery operation exceeded configured timeout");
386
+ const controller = new AbortController();
387
+ const timer = setTimeout(() => controller.abort(), remaining);
388
+ try {
389
+ let response;
390
+ try {
391
+ response = await this.withDeadline(this.#fetch(`${apiBase}${path}`, {
392
+ method,
393
+ redirect: "error",
394
+ signal: controller.signal,
395
+ headers: {
396
+ authorization: `Bearer ${token}`,
397
+ accept: "application/json",
398
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
399
+ },
400
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
401
+ }), deadline, "BigQuery request failed");
402
+ }
403
+ catch {
404
+ throw new Error("BigQuery request failed");
405
+ }
406
+ let text;
407
+ try {
408
+ text = await this.withDeadline(response.text(), deadline, "BigQuery response could not be read");
409
+ }
410
+ catch {
411
+ throw new Error("BigQuery response could not be read");
412
+ }
413
+ if (!response.ok)
414
+ throw new BigQueryHttpError(response.status);
415
+ if (text.length === 0)
416
+ throw new TypeError("BigQuery response was unexpectedly empty");
417
+ try {
418
+ return JSON.parse(text);
419
+ }
420
+ catch {
421
+ throw new TypeError("BigQuery response was not JSON");
422
+ }
423
+ }
424
+ finally {
425
+ clearTimeout(timer);
426
+ }
427
+ }
428
+ async withDeadline(promise, deadline, failureMessage) {
429
+ const remaining = deadline - Date.now();
430
+ if (remaining <= 0)
431
+ throw new UnsupportedError("BigQuery operation exceeded configured timeout");
432
+ return new Promise((resolve, reject) => {
433
+ const timer = setTimeout(() => reject(new UnsupportedError("BigQuery operation exceeded configured timeout")), remaining);
434
+ void promise.then((value) => { clearTimeout(timer); resolve(value); }, () => { clearTimeout(timer); reject(new Error(failureMessage)); });
435
+ });
436
+ }
437
+ }
@@ -0,0 +1,4 @@
1
+ export { BigQueryDatabase, BigQueryHttpError } from "./database.js";
2
+ export { compileBigQueryQuery, parameter, quoteTable, validateIdentifier, validateProjectId } from "./sql.js";
3
+ export type { AccessTokenProvider, BigQueryColumn, BigQueryDatabaseOptions, BigQueryFetch, BigQueryQueryMetadata, BigQueryScalarType, BigQueryTable, } from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,UAAU,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC9G,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,GACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BigQueryDatabase, BigQueryHttpError } from "./database.js";
2
+ export { compileBigQueryQuery, parameter, quoteTable, validateIdentifier, validateProjectId } from "./sql.js";
package/dist/sql.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type StructuredQuery } from "@dalgo/core";
2
+ import type { BigQueryColumn, BigQueryScalarType, BigQueryTable } from "./types.js";
3
+ export interface BigQueryParameter {
4
+ readonly name: string;
5
+ readonly parameterType: {
6
+ readonly type: BigQueryScalarType | "ARRAY";
7
+ readonly arrayType?: {
8
+ readonly type: BigQueryScalarType;
9
+ };
10
+ };
11
+ readonly parameterValue: {
12
+ readonly value?: string;
13
+ readonly arrayValues?: readonly {
14
+ readonly value?: string;
15
+ }[];
16
+ };
17
+ }
18
+ export interface CompiledBigQueryQuery {
19
+ readonly sql: string;
20
+ readonly parameters: readonly BigQueryParameter[];
21
+ readonly cursorColumns: readonly BigQueryColumn[];
22
+ }
23
+ export declare function validateIdentifier(value: string, label: string): void;
24
+ /** Google project IDs may contain hyphens, unlike dataset/table/column identifiers. */
25
+ export declare function validateProjectId(value: string): void;
26
+ export declare function quoteTable(projectId: string, table: BigQueryTable): string;
27
+ export declare function parameter(name: string, column: BigQueryColumn, value: unknown): BigQueryParameter;
28
+ export declare function compileBigQueryQuery<T>(projectId: string, table: BigQueryTable, query: StructuredQuery<T>, rowLimit: number): CompiledBigQueryQuery;
29
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AACrH,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAA;SAAE,CAAA;KAAE,CAAC;IACpI,QAAQ,CAAC,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS;YAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CACrH;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,QAAQ,CAAC,aAAa,EAAE,SAAS,cAAc,EAAE,CAAC;CACnD;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAErE;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIrD;AAOD,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAK1E;AAsCD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAEjG;AAmGD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CA0BnJ"}