@malloydata/db-bigquery 0.0.24-dev230224214106 → 0.0.24-dev230228224434
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.spec.js +45 -45
- package/dist/bigquery_connection.d.ts +4 -4
- package/dist/bigquery_connection.js +105 -107
- package/dist/index.d.ts +1 -1
- package/package.json +6 -2
package/dist/bigquery.spec.js
CHANGED
|
@@ -51,119 +51,119 @@ const bigquery_1 = require("@google-cloud/bigquery");
|
|
|
51
51
|
const util = __importStar(require("util"));
|
|
52
52
|
const fs = __importStar(require("fs"));
|
|
53
53
|
const url_1 = require("url");
|
|
54
|
-
describe(
|
|
54
|
+
describe('db:BigQuery', () => {
|
|
55
55
|
let bq;
|
|
56
56
|
let runtime;
|
|
57
57
|
beforeAll(() => {
|
|
58
|
-
bq = new bigquery_connection_1.BigQueryConnection(
|
|
58
|
+
bq = new bigquery_connection_1.BigQueryConnection('test');
|
|
59
59
|
const files = {
|
|
60
|
-
|
|
60
|
+
readURL: async (url) => {
|
|
61
61
|
const filePath = (0, url_1.fileURLToPath)(url);
|
|
62
|
-
return await util.promisify(fs.readFile)(filePath,
|
|
63
|
-
}
|
|
62
|
+
return await util.promisify(fs.readFile)(filePath, 'utf8');
|
|
63
|
+
},
|
|
64
64
|
};
|
|
65
65
|
runtime = new malloy.Runtime(files, bq);
|
|
66
66
|
});
|
|
67
|
-
it(
|
|
68
|
-
const res = await bq.runSQL(
|
|
69
|
-
expect(res.rows[0][
|
|
67
|
+
it('runs a SQL query', async () => {
|
|
68
|
+
const res = await bq.runSQL('SELECT 1 as t');
|
|
69
|
+
expect(res.rows[0]['t']).toBe(1);
|
|
70
70
|
});
|
|
71
|
-
it(
|
|
72
|
-
const res = await bq.costQuery(
|
|
71
|
+
it('costs a SQL query', async () => {
|
|
72
|
+
const res = await bq.costQuery('SELECT * FROM malloy-data.faa.airports');
|
|
73
73
|
expect(res).toBe(3029200);
|
|
74
74
|
});
|
|
75
|
-
it(
|
|
76
|
-
const res = await bq.getTableFieldSchema(
|
|
75
|
+
it('gets table schema', async () => {
|
|
76
|
+
const res = await bq.getTableFieldSchema('malloy-data.faa.carriers');
|
|
77
77
|
expect(res.schema).toStrictEqual({
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
{
|
|
81
|
-
{
|
|
82
|
-
]
|
|
78
|
+
fields: [
|
|
79
|
+
{ name: 'code', type: 'STRING' },
|
|
80
|
+
{ name: 'name', type: 'STRING' },
|
|
81
|
+
{ name: 'nickname', type: 'STRING' },
|
|
82
|
+
],
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
-
it.todo(
|
|
86
|
-
it(
|
|
85
|
+
it.todo('gets table structdefs');
|
|
86
|
+
it('runs a Malloy query', async () => {
|
|
87
87
|
const sql = await runtime
|
|
88
88
|
.loadModel("explore: carriers is table('malloy-data.faa.carriers') { measure: carrier_count is count() }")
|
|
89
|
-
.loadQuery(
|
|
89
|
+
.loadQuery('query: carriers -> { aggregate: carrier_count }')
|
|
90
90
|
.getSQL();
|
|
91
91
|
const res = await bq.runSQL(sql);
|
|
92
|
-
expect(res.rows[0][
|
|
92
|
+
expect(res.rows[0]['carrier_count']).toBe(21);
|
|
93
93
|
});
|
|
94
|
-
it(
|
|
94
|
+
it('streams a Malloy query for download', async () => {
|
|
95
95
|
const sql = await runtime
|
|
96
96
|
.loadModel("explore: carriers is table('malloy-data.faa.carriers') { measure: carrier_count is count() }")
|
|
97
|
-
.loadQuery(
|
|
97
|
+
.loadQuery('query: carriers -> { group_by: name }')
|
|
98
98
|
.getSQL();
|
|
99
99
|
const res = await bq.downloadMalloyQuery(sql);
|
|
100
|
-
return new Promise(
|
|
100
|
+
return new Promise(resolve => {
|
|
101
101
|
let count = 0;
|
|
102
|
-
res.on(
|
|
103
|
-
res.on(
|
|
102
|
+
res.on('data', () => (count += 1));
|
|
103
|
+
res.on('end', () => {
|
|
104
104
|
expect(count).toBe(21);
|
|
105
105
|
resolve(true);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
-
it(
|
|
110
|
-
const fullTempTableName = await bq.manifestTemporaryTable(
|
|
111
|
-
const splitTableName = fullTempTableName.split(
|
|
109
|
+
it('manifests a temporary table', async () => {
|
|
110
|
+
const fullTempTableName = await bq.manifestTemporaryTable('SELECT 1 as t');
|
|
111
|
+
const splitTableName = fullTempTableName.split('.');
|
|
112
112
|
const sdk = new bigquery_1.BigQuery();
|
|
113
113
|
const dataset = sdk.dataset(splitTableName[1]);
|
|
114
114
|
const table = dataset.table(splitTableName[2]);
|
|
115
115
|
const exists = await table.exists();
|
|
116
116
|
expect(exists).toBeTruthy();
|
|
117
117
|
});
|
|
118
|
-
describe.skip(
|
|
119
|
-
const datasetName =
|
|
120
|
-
const tableName =
|
|
118
|
+
describe.skip('manifests permanent table', () => {
|
|
119
|
+
const datasetName = 'test_malloy_test_dataset';
|
|
120
|
+
const tableName = 'test_malloy_test_table';
|
|
121
121
|
const sdk = new bigquery_1.BigQuery();
|
|
122
122
|
// delete entire dataset before each test and once tests are complete
|
|
123
123
|
const deleteTestDataset = async () => {
|
|
124
124
|
const dataset = sdk.dataset(datasetName);
|
|
125
125
|
if ((await dataset.exists())[0]) {
|
|
126
126
|
await dataset.delete({
|
|
127
|
-
|
|
127
|
+
force: true,
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
};
|
|
131
131
|
beforeEach(deleteTestDataset);
|
|
132
132
|
afterAll(deleteTestDataset);
|
|
133
|
-
it(
|
|
133
|
+
it('throws if dataset does not exist and createDataset=false', async () => {
|
|
134
134
|
await expect(async () => {
|
|
135
|
-
await bq.manifestPermanentTable(
|
|
135
|
+
await bq.manifestPermanentTable('SELECT 1 as t', datasetName, tableName, false, false);
|
|
136
136
|
}).rejects.toThrowError(`Dataset ${datasetName} does not exist`);
|
|
137
137
|
});
|
|
138
|
-
it(
|
|
138
|
+
it('creates dataset if createDataset=true', async () => {
|
|
139
139
|
// note - dataset does not exist b/c of beforeEach()
|
|
140
|
-
await bq.manifestPermanentTable(
|
|
140
|
+
await bq.manifestPermanentTable('SELECT 1 as t', datasetName, tableName, false, true);
|
|
141
141
|
const dataset = sdk.dataset(datasetName);
|
|
142
142
|
const [exists] = await dataset.exists();
|
|
143
143
|
expect(exists).toBeTruthy();
|
|
144
144
|
});
|
|
145
|
-
it(
|
|
145
|
+
it('throws if table exist and overwriteExistingTable=false', async () => {
|
|
146
146
|
const newDatasetResponse = await sdk.createDataset(datasetName);
|
|
147
147
|
const dataset = newDatasetResponse[0];
|
|
148
|
-
const tableMeta = {
|
|
148
|
+
const tableMeta = { name: tableName };
|
|
149
149
|
await dataset.createTable(tableName, tableMeta);
|
|
150
150
|
await expect(async () => {
|
|
151
|
-
await bq.manifestPermanentTable(
|
|
151
|
+
await bq.manifestPermanentTable('SELECT 1 as t', datasetName, tableName, false, true);
|
|
152
152
|
}).rejects.toThrowError(`Table ${tableName} already exists`);
|
|
153
153
|
});
|
|
154
|
-
it(
|
|
155
|
-
const jobId = await bq.manifestPermanentTable(
|
|
154
|
+
it('manifests a table', async () => {
|
|
155
|
+
const jobId = await bq.manifestPermanentTable('SELECT 1 as t', datasetName, tableName, false, true);
|
|
156
156
|
// wait for job to complete
|
|
157
157
|
const [job] = await sdk.job(jobId).get();
|
|
158
158
|
let [metaData] = await job.getMetadata();
|
|
159
|
-
while (metaData.status.state !==
|
|
160
|
-
await new Promise(
|
|
159
|
+
while (metaData.status.state !== 'DONE') {
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
161
161
|
[metaData] = await job.getMetadata();
|
|
162
162
|
}
|
|
163
163
|
// query the new table
|
|
164
164
|
const [queryJob] = await sdk.createQueryJob(`SELECT * FROM ${datasetName}.${tableName}`);
|
|
165
165
|
const [results] = await queryJob.getQueryResults();
|
|
166
|
-
expect(results[0]).toStrictEqual({
|
|
166
|
+
expect(results[0]).toStrictEqual({ t: 1 });
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
169
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { RowMetadata } from
|
|
2
|
-
import bigquery from
|
|
3
|
-
import { ResourceStream } from
|
|
4
|
-
import { Connection, FieldTypeDef, MalloyQueryData, PersistSQLResults, PooledConnection, QueryData, QueryDataRow, SQLBlock, StreamingConnection, StructDef } from
|
|
1
|
+
import { RowMetadata } from '@google-cloud/bigquery';
|
|
2
|
+
import bigquery from '@google-cloud/bigquery/build/src/types';
|
|
3
|
+
import { ResourceStream } from '@google-cloud/paginator';
|
|
4
|
+
import { Connection, FieldTypeDef, MalloyQueryData, PersistSQLResults, PooledConnection, QueryData, QueryDataRow, SQLBlock, StreamingConnection, StructDef } from '@malloydata/malloy';
|
|
5
5
|
export interface BigQueryManagerOptions {
|
|
6
6
|
credentials?: {
|
|
7
7
|
clientId: string;
|
|
@@ -57,7 +57,7 @@ const malloy_1 = require("@malloydata/malloy");
|
|
|
57
57
|
class BigQueryAuthenticationError extends Error {
|
|
58
58
|
constructor(message) {
|
|
59
59
|
super(message);
|
|
60
|
-
this.name =
|
|
60
|
+
this.name = 'BigQueryAuthenticationError';
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
exports.BigQueryAuthenticationError = BigQueryAuthenticationError;
|
|
@@ -66,7 +66,7 @@ const maybeRewriteError = (e) => {
|
|
|
66
66
|
// the refresh token is invalid
|
|
67
67
|
// ApiErrors happen if token is revoked (for example, client.revokeToken(creds.access_token!))
|
|
68
68
|
if (e instanceof Error) {
|
|
69
|
-
if ((e instanceof gaxios_1.GaxiosError && e.code ===
|
|
69
|
+
if ((e instanceof gaxios_1.GaxiosError && e.code === '400') ||
|
|
70
70
|
(e instanceof googleCommon.ApiError && e.code === 401)) {
|
|
71
71
|
return new BigQueryAuthenticationError(e.message);
|
|
72
72
|
}
|
|
@@ -93,18 +93,18 @@ class BigQueryConnection {
|
|
|
93
93
|
this.schemaCache = new Map();
|
|
94
94
|
this.sqlSchemaCache = new Map();
|
|
95
95
|
this.bqToMalloyTypes = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
DATE: { type: 'date' },
|
|
97
|
+
STRING: { type: 'string' },
|
|
98
|
+
INTEGER: { type: 'number', numberType: 'integer' },
|
|
99
|
+
INT64: { type: 'number', numberType: 'integer' },
|
|
100
|
+
FLOAT: { type: 'number', numberType: 'float' },
|
|
101
|
+
FLOAT64: { type: 'number', numberType: 'float' },
|
|
102
|
+
NUMERIC: { type: 'number', numberType: 'float' },
|
|
103
|
+
BIGNUMERIC: { type: 'number', numberType: 'float' },
|
|
104
|
+
TIMESTAMP: { type: 'timestamp' },
|
|
105
|
+
BOOLEAN: { type: 'boolean' },
|
|
106
|
+
BOOL: { type: 'boolean' },
|
|
107
|
+
JSON: { type: 'json' },
|
|
108
108
|
// TODO (https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#tablefieldschema):
|
|
109
109
|
// BYTES
|
|
110
110
|
// DATETIME
|
|
@@ -113,8 +113,8 @@ class BigQueryConnection {
|
|
|
113
113
|
};
|
|
114
114
|
this.name = name;
|
|
115
115
|
this.bigQuery = new bigquery_1.BigQuery({
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
userAgent: `Malloy/${malloy_1.Malloy.version}`,
|
|
117
|
+
keyFilename: config.serviceAccountKeyPath,
|
|
118
118
|
});
|
|
119
119
|
// record project ID because for unclear reasons we have to modify the project ID on the SDK when
|
|
120
120
|
// we want to use the tables API
|
|
@@ -122,10 +122,10 @@ class BigQueryConnection {
|
|
|
122
122
|
this.defaultProject = config.defaultProject || this.bigQuery.projectId;
|
|
123
123
|
this.queryOptions = queryOptions;
|
|
124
124
|
this.config = config;
|
|
125
|
-
this.location = config.location ||
|
|
125
|
+
this.location = config.location || 'US';
|
|
126
126
|
}
|
|
127
127
|
get dialectName() {
|
|
128
|
-
return
|
|
128
|
+
return 'standardsql';
|
|
129
129
|
}
|
|
130
130
|
readQueryOptions() {
|
|
131
131
|
const options = BigQueryConnection.DEFAULT_QUERY_OPTIONS;
|
|
@@ -156,19 +156,19 @@ class BigQueryConnection {
|
|
|
156
156
|
const pageSize = (_a = options.rowLimit) !== null && _a !== void 0 ? _a : defaultOptions.rowLimit;
|
|
157
157
|
try {
|
|
158
158
|
const queryResultsOptions = {
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
maxResults: pageSize,
|
|
160
|
+
startIndex: rowIndex.toString(),
|
|
161
161
|
};
|
|
162
162
|
const jobResult = await this.createBigQueryJobAndGetResults(sqlCommand, undefined, queryResultsOptions);
|
|
163
163
|
const totalRows = +(((_b = jobResult[2]) === null || _b === void 0 ? void 0 : _b.totalRows)
|
|
164
164
|
? jobResult[2].totalRows
|
|
165
|
-
:
|
|
165
|
+
: '0');
|
|
166
166
|
// TODO need to probably surface the cause of the schema not present error
|
|
167
167
|
if (((_c = jobResult[2]) === null || _c === void 0 ? void 0 : _c.schema) === undefined) {
|
|
168
|
-
throw new Error(
|
|
168
|
+
throw new Error('Schema not present');
|
|
169
169
|
}
|
|
170
170
|
// TODO even though we have 10 minute timeout limit, we still should confirm that resulting metadata has "jobComplete: true"
|
|
171
|
-
const data = {
|
|
171
|
+
const data = { rows: jobResult[0], totalRows };
|
|
172
172
|
const schema = (_d = jobResult[2]) === null || _d === void 0 ? void 0 : _d.schema;
|
|
173
173
|
return { data, schema };
|
|
174
174
|
}
|
|
@@ -181,22 +181,22 @@ class BigQueryConnection {
|
|
|
181
181
|
return data;
|
|
182
182
|
}
|
|
183
183
|
async runSQLBlockAndFetchResultSchema(sqlBlock, options) {
|
|
184
|
-
const { data,
|
|
184
|
+
const { data, schema: schemaRaw } = await this._runSQL(sqlBlock.selectStr, options);
|
|
185
185
|
const schema = this.structDefFromSQLSchema(sqlBlock, schemaRaw);
|
|
186
186
|
return { data, schema };
|
|
187
187
|
}
|
|
188
188
|
async downloadMalloyQuery(sqlCommand) {
|
|
189
189
|
const job = await this.createBigQueryJob({
|
|
190
|
-
|
|
190
|
+
query: sqlCommand,
|
|
191
191
|
});
|
|
192
192
|
return job.getQueryResultsStream();
|
|
193
193
|
}
|
|
194
194
|
async dryRunSQLQuery(sqlCommand) {
|
|
195
195
|
try {
|
|
196
196
|
const [result] = await this.bigQuery.createQueryJob({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
location: this.location,
|
|
198
|
+
query: sqlCommand,
|
|
199
|
+
dryRun: true,
|
|
200
200
|
});
|
|
201
201
|
return result;
|
|
202
202
|
}
|
|
@@ -215,8 +215,8 @@ class BigQueryConnection {
|
|
|
215
215
|
}
|
|
216
216
|
async getTableFieldSchema(tableURL) {
|
|
217
217
|
var _a, _b;
|
|
218
|
-
const {
|
|
219
|
-
const segments = tableName.split(
|
|
218
|
+
const { tablePath: tableName } = (0, malloy_1.parseTableURI)(tableURL);
|
|
219
|
+
const segments = tableName.split('.');
|
|
220
220
|
// paths can have two or three segments
|
|
221
221
|
// if there are only two segments, assume the dataset is "local" to the current billing project
|
|
222
222
|
let projectId, datasetNamePart, tableNamePart;
|
|
@@ -241,9 +241,9 @@ class BigQueryConnection {
|
|
|
241
241
|
this.bigQuery.projectId = this.projectId;
|
|
242
242
|
const [metadata] = await metadataPromise;
|
|
243
243
|
return {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
((_b = metadata.timePartitioning) === null || _b === void 0 ? void 0 : _b.field) === undefined
|
|
244
|
+
schema: metadata.schema,
|
|
245
|
+
needsPartitionPsuedoColumn: ((_a = metadata.timePartitioning) === null || _a === void 0 ? void 0 : _a.type) !== undefined &&
|
|
246
|
+
((_b = metadata.timePartitioning) === null || _b === void 0 ? void 0 : _b.field) === undefined,
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
catch (e) {
|
|
@@ -255,7 +255,7 @@ class BigQueryConnection {
|
|
|
255
255
|
return result[0];
|
|
256
256
|
}
|
|
257
257
|
async test() {
|
|
258
|
-
await this.dryRunSQLQuery(
|
|
258
|
+
await this.dryRunSQLQuery('SELECT 1');
|
|
259
259
|
}
|
|
260
260
|
/*
|
|
261
261
|
* Do we need to care about multiple overlapping calls to this function? No.
|
|
@@ -276,7 +276,7 @@ class BigQueryConnection {
|
|
|
276
276
|
*
|
|
277
277
|
*/
|
|
278
278
|
async manifestTemporaryTable(sqlCommand) {
|
|
279
|
-
const hash = crypto.createHash(
|
|
279
|
+
const hash = crypto.createHash('md5').update(sqlCommand).digest('hex');
|
|
280
280
|
const tempTableName = this.temporaryTables.get(hash);
|
|
281
281
|
if (tempTableName !== undefined) {
|
|
282
282
|
return tempTableName;
|
|
@@ -284,16 +284,16 @@ class BigQueryConnection {
|
|
|
284
284
|
else {
|
|
285
285
|
try {
|
|
286
286
|
const [job] = await this.bigQuery.createQueryJob({
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
location: this.location,
|
|
288
|
+
query: sqlCommand,
|
|
289
289
|
});
|
|
290
290
|
let [metaData] = await job.getMetadata();
|
|
291
291
|
// wait for job to complete, because we need the table name
|
|
292
292
|
// TODO just because a job is "DONE" doesn't mean it ended correctly, should probably also confirm
|
|
293
293
|
// status is successful & that table was created
|
|
294
294
|
// TODO this needs better error handling and a timeout so that issues dont result in infinite looping
|
|
295
|
-
while (metaData.status.state !==
|
|
296
|
-
await new Promise(
|
|
295
|
+
while (metaData.status.state !== 'DONE') {
|
|
296
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
297
297
|
[metaData] = await job.getMetadata();
|
|
298
298
|
}
|
|
299
299
|
// save table name
|
|
@@ -306,7 +306,7 @@ class BigQueryConnection {
|
|
|
306
306
|
return fullTableName;
|
|
307
307
|
}
|
|
308
308
|
else {
|
|
309
|
-
throw new Error(
|
|
309
|
+
throw new Error('bigquery.job.getMetadata() - metadata should have configuration but does not');
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
catch (e) {
|
|
@@ -332,9 +332,9 @@ class BigQueryConnection {
|
|
|
332
332
|
throw new Error(`Table ${tableName} already exists`);
|
|
333
333
|
}
|
|
334
334
|
const [job] = await this.bigQuery.createQueryJob({
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
query: sqlCommand,
|
|
336
|
+
location: this.location,
|
|
337
|
+
destination: table,
|
|
338
338
|
});
|
|
339
339
|
// if creating this job didn't throw, there's an ID.
|
|
340
340
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -348,44 +348,42 @@ class BigQueryConnection {
|
|
|
348
348
|
const type = field.type;
|
|
349
349
|
const name = field.name;
|
|
350
350
|
// check for an array
|
|
351
|
-
if (field.mode ===
|
|
351
|
+
if (field.mode === 'REPEATED' && !['STRUCT', 'RECORD'].includes(type)) {
|
|
352
352
|
const malloyType = this.bqToMalloyTypes[type];
|
|
353
353
|
if (malloyType) {
|
|
354
354
|
const innerStructDef = {
|
|
355
|
-
|
|
355
|
+
type: 'struct',
|
|
356
356
|
name,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
357
|
+
dialect: this.dialectName,
|
|
358
|
+
structSource: { type: 'nested' },
|
|
359
|
+
structRelationship: {
|
|
360
|
+
type: 'nested',
|
|
361
|
+
field: name,
|
|
362
|
+
isArray: true,
|
|
363
363
|
},
|
|
364
|
-
|
|
364
|
+
fields: [{ ...malloyType, name: 'value' }],
|
|
365
365
|
};
|
|
366
366
|
structDef.fields.push(innerStructDef);
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
-
else if ([
|
|
369
|
+
else if (['STRUCT', 'RECORD'].includes(type)) {
|
|
370
370
|
const innerStructDef = {
|
|
371
|
-
|
|
371
|
+
type: 'struct',
|
|
372
372
|
name,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
: { "type": "inline" },
|
|
380
|
-
"fields": []
|
|
373
|
+
dialect: this.dialectName,
|
|
374
|
+
structSource: field.mode === 'REPEATED' ? { type: 'nested' } : { type: 'inline' },
|
|
375
|
+
structRelationship: field.mode === 'REPEATED'
|
|
376
|
+
? { type: 'nested', field: name, isArray: false }
|
|
377
|
+
: { type: 'inline' },
|
|
378
|
+
fields: [],
|
|
381
379
|
};
|
|
382
380
|
this.addFieldsToStructDef(innerStructDef, field);
|
|
383
381
|
structDef.fields.push(innerStructDef);
|
|
384
382
|
}
|
|
385
383
|
else {
|
|
386
384
|
const malloyType = this.bqToMalloyTypes[type] || {
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
type: 'unsupported',
|
|
386
|
+
rawType: type.toLowerCase(),
|
|
389
387
|
};
|
|
390
388
|
structDef.fields.push({ name, ...malloyType });
|
|
391
389
|
}
|
|
@@ -393,7 +391,7 @@ class BigQueryConnection {
|
|
|
393
391
|
}
|
|
394
392
|
tableURLtoTablePath(tableURL) {
|
|
395
393
|
const { tablePath } = (0, malloy_1.parseTableURI)(tableURL);
|
|
396
|
-
if (tablePath.split(
|
|
394
|
+
if (tablePath.split('.').length === 2) {
|
|
397
395
|
return `${this.defaultProject}.${tablePath}`;
|
|
398
396
|
}
|
|
399
397
|
else {
|
|
@@ -402,43 +400,43 @@ class BigQueryConnection {
|
|
|
402
400
|
}
|
|
403
401
|
structDefFromTableSchema(tableURL, schemaInfo) {
|
|
404
402
|
const structDef = {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
403
|
+
type: 'struct',
|
|
404
|
+
name: tableURL,
|
|
405
|
+
dialect: this.dialectName,
|
|
406
|
+
structSource: {
|
|
407
|
+
type: 'table',
|
|
408
|
+
tablePath: this.tableURLtoTablePath(tableURL),
|
|
411
409
|
},
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
410
|
+
structRelationship: {
|
|
411
|
+
type: 'basetable',
|
|
412
|
+
connectionName: this.name,
|
|
415
413
|
},
|
|
416
|
-
|
|
414
|
+
fields: [],
|
|
417
415
|
};
|
|
418
416
|
this.addFieldsToStructDef(structDef, schemaInfo.schema);
|
|
419
417
|
if (schemaInfo.needsPartitionPsuedoColumn) {
|
|
420
418
|
structDef.fields.push({
|
|
421
|
-
|
|
422
|
-
|
|
419
|
+
type: 'timestamp',
|
|
420
|
+
name: '_PARTITIONTIME',
|
|
423
421
|
});
|
|
424
422
|
}
|
|
425
423
|
return structDef;
|
|
426
424
|
}
|
|
427
425
|
structDefFromSQLSchema(sqlBlock, tableFieldSchema) {
|
|
428
426
|
const structDef = {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
sqlBlock
|
|
427
|
+
type: 'struct',
|
|
428
|
+
name: sqlBlock.name,
|
|
429
|
+
dialect: this.dialectName,
|
|
430
|
+
structSource: {
|
|
431
|
+
type: 'sql',
|
|
432
|
+
method: 'subquery',
|
|
433
|
+
sqlBlock,
|
|
436
434
|
},
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
435
|
+
structRelationship: {
|
|
436
|
+
type: 'basetable',
|
|
437
|
+
connectionName: this.name,
|
|
440
438
|
},
|
|
441
|
-
|
|
439
|
+
fields: [],
|
|
442
440
|
};
|
|
443
441
|
this.addFieldsToStructDef(structDef, tableFieldSchema);
|
|
444
442
|
return structDef;
|
|
@@ -452,12 +450,12 @@ class BigQueryConnection {
|
|
|
452
450
|
try {
|
|
453
451
|
const tableFieldSchema = await this.getTableFieldSchema(tableURL);
|
|
454
452
|
inCache = {
|
|
455
|
-
|
|
453
|
+
schema: this.structDefFromTableSchema(tableURL, tableFieldSchema),
|
|
456
454
|
};
|
|
457
455
|
this.schemaCache.set(tableURL, inCache);
|
|
458
456
|
}
|
|
459
457
|
catch (error) {
|
|
460
|
-
inCache = {
|
|
458
|
+
inCache = { error: error.message };
|
|
461
459
|
}
|
|
462
460
|
}
|
|
463
461
|
if (inCache.schema !== undefined) {
|
|
@@ -479,9 +477,9 @@ class BigQueryConnection {
|
|
|
479
477
|
for (let retries = 0; retries < 3; retries++) {
|
|
480
478
|
try {
|
|
481
479
|
const [job] = await this.bigQuery.createQueryJob({
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
480
|
+
location: this.location,
|
|
481
|
+
query: sqlRef.selectStr,
|
|
482
|
+
dryRun: true,
|
|
485
483
|
});
|
|
486
484
|
return job.metadata.statistics.query.schema;
|
|
487
485
|
}
|
|
@@ -498,11 +496,11 @@ class BigQueryConnection {
|
|
|
498
496
|
try {
|
|
499
497
|
const tableFieldSchema = await this.getSQLBlockSchema(sqlRef);
|
|
500
498
|
inCache = {
|
|
501
|
-
|
|
499
|
+
structDef: this.structDefFromSQLSchema(sqlRef, tableFieldSchema),
|
|
502
500
|
};
|
|
503
501
|
}
|
|
504
502
|
catch (error) {
|
|
505
|
-
inCache = {
|
|
503
|
+
inCache = { error: error.message };
|
|
506
504
|
}
|
|
507
505
|
this.sqlSchemaCache.set(key, inCache);
|
|
508
506
|
}
|
|
@@ -514,8 +512,8 @@ class BigQueryConnection {
|
|
|
514
512
|
async createBigQueryJobAndGetResults(sqlCommand, createQueryJobOptions, getQueryResultsOptions) {
|
|
515
513
|
try {
|
|
516
514
|
const job = await this.createBigQueryJob({
|
|
517
|
-
|
|
518
|
-
...createQueryJobOptions
|
|
515
|
+
query: sqlCommand,
|
|
516
|
+
...createQueryJobOptions,
|
|
519
517
|
});
|
|
520
518
|
// TODO we should check if this is still required?
|
|
521
519
|
// We do a simple retry-loop here, as a temporary fix for a transient
|
|
@@ -527,8 +525,8 @@ class BigQueryConnection {
|
|
|
527
525
|
for (let retries = 0; retries < 3; retries++) {
|
|
528
526
|
try {
|
|
529
527
|
return await job.getQueryResults({
|
|
530
|
-
|
|
531
|
-
...getQueryResultsOptions
|
|
528
|
+
timeoutMs: 1000 * 60 * 2,
|
|
529
|
+
...getQueryResultsOptions,
|
|
532
530
|
});
|
|
533
531
|
}
|
|
534
532
|
catch (fetchError) {
|
|
@@ -543,10 +541,10 @@ class BigQueryConnection {
|
|
|
543
541
|
}
|
|
544
542
|
async createBigQueryJob(createQueryJobOptions) {
|
|
545
543
|
const [job] = await this.bigQuery.createQueryJob({
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
...createQueryJobOptions
|
|
544
|
+
location: this.location,
|
|
545
|
+
maximumBytesBilled: this.config.maximumBytesBilled || MAXIMUM_BYTES_BILLED,
|
|
546
|
+
jobTimeoutMs: Number(this.config.timeoutMs) || TIMEOUT_MS,
|
|
547
|
+
...createQueryJobOptions,
|
|
550
548
|
});
|
|
551
549
|
return job;
|
|
552
550
|
}
|
|
@@ -563,9 +561,9 @@ class BigQueryConnection {
|
|
|
563
561
|
}
|
|
564
562
|
bigQuery
|
|
565
563
|
.createQueryStream(sqlCommand)
|
|
566
|
-
.on(
|
|
567
|
-
.on(
|
|
568
|
-
.on(
|
|
564
|
+
.on('error', onError)
|
|
565
|
+
.on('data', handleData)
|
|
566
|
+
.on('end', onEnd);
|
|
569
567
|
}
|
|
570
568
|
return (0, malloy_1.toAsyncGenerator)(streamBigQuery);
|
|
571
569
|
}
|
|
@@ -575,6 +573,6 @@ class BigQueryConnection {
|
|
|
575
573
|
}
|
|
576
574
|
exports.BigQueryConnection = BigQueryConnection;
|
|
577
575
|
BigQueryConnection.DEFAULT_QUERY_OPTIONS = {
|
|
578
|
-
|
|
576
|
+
rowLimit: 10,
|
|
579
577
|
};
|
|
580
578
|
//# sourceMappingURL=bigquery_connection.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { BigQueryConnection } from
|
|
1
|
+
export { BigQueryConnection } from './bigquery_connection';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/db-bigquery",
|
|
3
|
-
"version": "0.0.24-
|
|
3
|
+
"version": "0.0.24-dev230228224434",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/malloydata/malloy"
|
|
11
11
|
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=10"
|
|
14
|
+
},
|
|
12
15
|
"scripts": {
|
|
13
16
|
"lint": "eslint '**/*.ts{,x}'",
|
|
14
17
|
"lint-fix": "eslint '**/*.ts{,x}' --fix",
|
|
@@ -21,7 +24,8 @@
|
|
|
21
24
|
"dependencies": {
|
|
22
25
|
"@google-cloud/bigquery": "^5.5.0",
|
|
23
26
|
"@google-cloud/common": "^3.6.0",
|
|
24
|
-
"@
|
|
27
|
+
"@google-cloud/paginator": "^4.0.1",
|
|
28
|
+
"@malloydata/malloy": "^0.0.24-dev230228224434",
|
|
25
29
|
"gaxios": "^4.2.0"
|
|
26
30
|
}
|
|
27
31
|
}
|