@malloydata/db-bigquery 0.0.24-dev230224214106 → 0.0.24-dev230301165401
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 +6 -5
- package/dist/bigquery_connection.js +114 -108
- 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;
|
|
@@ -23,7 +23,8 @@ interface BigQueryConnectionConfiguration {
|
|
|
23
23
|
}
|
|
24
24
|
interface SchemaInfo {
|
|
25
25
|
schema: bigquery.ITableFieldSchema;
|
|
26
|
-
|
|
26
|
+
needsTableSuffixPseudoColumn: boolean;
|
|
27
|
+
needsPartitionPseudoColumn: boolean;
|
|
27
28
|
}
|
|
28
29
|
declare type QueryOptionsReader = Partial<BigQueryQueryOptions> | (() => Partial<BigQueryQueryOptions>);
|
|
29
30
|
export declare class BigQueryAuthenticationError extends Error {
|
|
@@ -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;
|
|
@@ -236,14 +236,16 @@ class BigQueryConnection {
|
|
|
236
236
|
// this is better than creating a new BQ instance every time we need to get a table schema.
|
|
237
237
|
if (projectId)
|
|
238
238
|
this.bigQuery.projectId = projectId;
|
|
239
|
+
const needTableSuffixPseudoColumn = tableNamePart && tableNamePart[tableNamePart.length - 1] === '*';
|
|
239
240
|
const table = this.bigQuery.dataset(datasetNamePart).table(tableNamePart);
|
|
240
241
|
const metadataPromise = table.getMetadata();
|
|
241
242
|
this.bigQuery.projectId = this.projectId;
|
|
242
243
|
const [metadata] = await metadataPromise;
|
|
243
244
|
return {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
schema: metadata.schema,
|
|
246
|
+
needsTableSuffixPseudoColumn: needTableSuffixPseudoColumn,
|
|
247
|
+
needsPartitionPseudoColumn: ((_a = metadata.timePartitioning) === null || _a === void 0 ? void 0 : _a.type) !== undefined &&
|
|
248
|
+
((_b = metadata.timePartitioning) === null || _b === void 0 ? void 0 : _b.field) === undefined,
|
|
247
249
|
};
|
|
248
250
|
}
|
|
249
251
|
catch (e) {
|
|
@@ -255,7 +257,7 @@ class BigQueryConnection {
|
|
|
255
257
|
return result[0];
|
|
256
258
|
}
|
|
257
259
|
async test() {
|
|
258
|
-
await this.dryRunSQLQuery(
|
|
260
|
+
await this.dryRunSQLQuery('SELECT 1');
|
|
259
261
|
}
|
|
260
262
|
/*
|
|
261
263
|
* Do we need to care about multiple overlapping calls to this function? No.
|
|
@@ -276,7 +278,7 @@ class BigQueryConnection {
|
|
|
276
278
|
*
|
|
277
279
|
*/
|
|
278
280
|
async manifestTemporaryTable(sqlCommand) {
|
|
279
|
-
const hash = crypto.createHash(
|
|
281
|
+
const hash = crypto.createHash('md5').update(sqlCommand).digest('hex');
|
|
280
282
|
const tempTableName = this.temporaryTables.get(hash);
|
|
281
283
|
if (tempTableName !== undefined) {
|
|
282
284
|
return tempTableName;
|
|
@@ -284,16 +286,16 @@ class BigQueryConnection {
|
|
|
284
286
|
else {
|
|
285
287
|
try {
|
|
286
288
|
const [job] = await this.bigQuery.createQueryJob({
|
|
287
|
-
|
|
288
|
-
|
|
289
|
+
location: this.location,
|
|
290
|
+
query: sqlCommand,
|
|
289
291
|
});
|
|
290
292
|
let [metaData] = await job.getMetadata();
|
|
291
293
|
// wait for job to complete, because we need the table name
|
|
292
294
|
// TODO just because a job is "DONE" doesn't mean it ended correctly, should probably also confirm
|
|
293
295
|
// status is successful & that table was created
|
|
294
296
|
// 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(
|
|
297
|
+
while (metaData.status.state !== 'DONE') {
|
|
298
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
297
299
|
[metaData] = await job.getMetadata();
|
|
298
300
|
}
|
|
299
301
|
// save table name
|
|
@@ -306,7 +308,7 @@ class BigQueryConnection {
|
|
|
306
308
|
return fullTableName;
|
|
307
309
|
}
|
|
308
310
|
else {
|
|
309
|
-
throw new Error(
|
|
311
|
+
throw new Error('bigquery.job.getMetadata() - metadata should have configuration but does not');
|
|
310
312
|
}
|
|
311
313
|
}
|
|
312
314
|
catch (e) {
|
|
@@ -332,9 +334,9 @@ class BigQueryConnection {
|
|
|
332
334
|
throw new Error(`Table ${tableName} already exists`);
|
|
333
335
|
}
|
|
334
336
|
const [job] = await this.bigQuery.createQueryJob({
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
query: sqlCommand,
|
|
338
|
+
location: this.location,
|
|
339
|
+
destination: table,
|
|
338
340
|
});
|
|
339
341
|
// if creating this job didn't throw, there's an ID.
|
|
340
342
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
@@ -348,44 +350,42 @@ class BigQueryConnection {
|
|
|
348
350
|
const type = field.type;
|
|
349
351
|
const name = field.name;
|
|
350
352
|
// check for an array
|
|
351
|
-
if (field.mode ===
|
|
353
|
+
if (field.mode === 'REPEATED' && !['STRUCT', 'RECORD'].includes(type)) {
|
|
352
354
|
const malloyType = this.bqToMalloyTypes[type];
|
|
353
355
|
if (malloyType) {
|
|
354
356
|
const innerStructDef = {
|
|
355
|
-
|
|
357
|
+
type: 'struct',
|
|
356
358
|
name,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
dialect: this.dialectName,
|
|
360
|
+
structSource: { type: 'nested' },
|
|
361
|
+
structRelationship: {
|
|
362
|
+
type: 'nested',
|
|
363
|
+
field: name,
|
|
364
|
+
isArray: true,
|
|
363
365
|
},
|
|
364
|
-
|
|
366
|
+
fields: [{ ...malloyType, name: 'value' }],
|
|
365
367
|
};
|
|
366
368
|
structDef.fields.push(innerStructDef);
|
|
367
369
|
}
|
|
368
370
|
}
|
|
369
|
-
else if ([
|
|
371
|
+
else if (['STRUCT', 'RECORD'].includes(type)) {
|
|
370
372
|
const innerStructDef = {
|
|
371
|
-
|
|
373
|
+
type: 'struct',
|
|
372
374
|
name,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
: { "type": "inline" },
|
|
380
|
-
"fields": []
|
|
375
|
+
dialect: this.dialectName,
|
|
376
|
+
structSource: field.mode === 'REPEATED' ? { type: 'nested' } : { type: 'inline' },
|
|
377
|
+
structRelationship: field.mode === 'REPEATED'
|
|
378
|
+
? { type: 'nested', field: name, isArray: false }
|
|
379
|
+
: { type: 'inline' },
|
|
380
|
+
fields: [],
|
|
381
381
|
};
|
|
382
382
|
this.addFieldsToStructDef(innerStructDef, field);
|
|
383
383
|
structDef.fields.push(innerStructDef);
|
|
384
384
|
}
|
|
385
385
|
else {
|
|
386
386
|
const malloyType = this.bqToMalloyTypes[type] || {
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
type: 'unsupported',
|
|
388
|
+
rawType: type.toLowerCase(),
|
|
389
389
|
};
|
|
390
390
|
structDef.fields.push({ name, ...malloyType });
|
|
391
391
|
}
|
|
@@ -393,7 +393,7 @@ class BigQueryConnection {
|
|
|
393
393
|
}
|
|
394
394
|
tableURLtoTablePath(tableURL) {
|
|
395
395
|
const { tablePath } = (0, malloy_1.parseTableURI)(tableURL);
|
|
396
|
-
if (tablePath.split(
|
|
396
|
+
if (tablePath.split('.').length === 2) {
|
|
397
397
|
return `${this.defaultProject}.${tablePath}`;
|
|
398
398
|
}
|
|
399
399
|
else {
|
|
@@ -402,43 +402,49 @@ class BigQueryConnection {
|
|
|
402
402
|
}
|
|
403
403
|
structDefFromTableSchema(tableURL, schemaInfo) {
|
|
404
404
|
const structDef = {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
type: 'struct',
|
|
406
|
+
name: tableURL,
|
|
407
|
+
dialect: this.dialectName,
|
|
408
|
+
structSource: {
|
|
409
|
+
type: 'table',
|
|
410
|
+
tablePath: this.tableURLtoTablePath(tableURL),
|
|
411
411
|
},
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
412
|
+
structRelationship: {
|
|
413
|
+
type: 'basetable',
|
|
414
|
+
connectionName: this.name,
|
|
415
415
|
},
|
|
416
|
-
|
|
416
|
+
fields: [],
|
|
417
417
|
};
|
|
418
418
|
this.addFieldsToStructDef(structDef, schemaInfo.schema);
|
|
419
|
-
if (schemaInfo.
|
|
419
|
+
if (schemaInfo.needsTableSuffixPseudoColumn) {
|
|
420
420
|
structDef.fields.push({
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
type: 'string',
|
|
422
|
+
name: '_TABLE_SUFFIX',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (schemaInfo.needsPartitionPseudoColumn) {
|
|
426
|
+
structDef.fields.push({
|
|
427
|
+
type: 'timestamp',
|
|
428
|
+
name: '_PARTITIONTIME',
|
|
423
429
|
});
|
|
424
430
|
}
|
|
425
431
|
return structDef;
|
|
426
432
|
}
|
|
427
433
|
structDefFromSQLSchema(sqlBlock, tableFieldSchema) {
|
|
428
434
|
const structDef = {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
sqlBlock
|
|
435
|
+
type: 'struct',
|
|
436
|
+
name: sqlBlock.name,
|
|
437
|
+
dialect: this.dialectName,
|
|
438
|
+
structSource: {
|
|
439
|
+
type: 'sql',
|
|
440
|
+
method: 'subquery',
|
|
441
|
+
sqlBlock,
|
|
436
442
|
},
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
443
|
+
structRelationship: {
|
|
444
|
+
type: 'basetable',
|
|
445
|
+
connectionName: this.name,
|
|
440
446
|
},
|
|
441
|
-
|
|
447
|
+
fields: [],
|
|
442
448
|
};
|
|
443
449
|
this.addFieldsToStructDef(structDef, tableFieldSchema);
|
|
444
450
|
return structDef;
|
|
@@ -452,12 +458,12 @@ class BigQueryConnection {
|
|
|
452
458
|
try {
|
|
453
459
|
const tableFieldSchema = await this.getTableFieldSchema(tableURL);
|
|
454
460
|
inCache = {
|
|
455
|
-
|
|
461
|
+
schema: this.structDefFromTableSchema(tableURL, tableFieldSchema),
|
|
456
462
|
};
|
|
457
463
|
this.schemaCache.set(tableURL, inCache);
|
|
458
464
|
}
|
|
459
465
|
catch (error) {
|
|
460
|
-
inCache = {
|
|
466
|
+
inCache = { error: error.message };
|
|
461
467
|
}
|
|
462
468
|
}
|
|
463
469
|
if (inCache.schema !== undefined) {
|
|
@@ -479,9 +485,9 @@ class BigQueryConnection {
|
|
|
479
485
|
for (let retries = 0; retries < 3; retries++) {
|
|
480
486
|
try {
|
|
481
487
|
const [job] = await this.bigQuery.createQueryJob({
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
488
|
+
location: this.location,
|
|
489
|
+
query: sqlRef.selectStr,
|
|
490
|
+
dryRun: true,
|
|
485
491
|
});
|
|
486
492
|
return job.metadata.statistics.query.schema;
|
|
487
493
|
}
|
|
@@ -498,11 +504,11 @@ class BigQueryConnection {
|
|
|
498
504
|
try {
|
|
499
505
|
const tableFieldSchema = await this.getSQLBlockSchema(sqlRef);
|
|
500
506
|
inCache = {
|
|
501
|
-
|
|
507
|
+
structDef: this.structDefFromSQLSchema(sqlRef, tableFieldSchema),
|
|
502
508
|
};
|
|
503
509
|
}
|
|
504
510
|
catch (error) {
|
|
505
|
-
inCache = {
|
|
511
|
+
inCache = { error: error.message };
|
|
506
512
|
}
|
|
507
513
|
this.sqlSchemaCache.set(key, inCache);
|
|
508
514
|
}
|
|
@@ -514,8 +520,8 @@ class BigQueryConnection {
|
|
|
514
520
|
async createBigQueryJobAndGetResults(sqlCommand, createQueryJobOptions, getQueryResultsOptions) {
|
|
515
521
|
try {
|
|
516
522
|
const job = await this.createBigQueryJob({
|
|
517
|
-
|
|
518
|
-
...createQueryJobOptions
|
|
523
|
+
query: sqlCommand,
|
|
524
|
+
...createQueryJobOptions,
|
|
519
525
|
});
|
|
520
526
|
// TODO we should check if this is still required?
|
|
521
527
|
// We do a simple retry-loop here, as a temporary fix for a transient
|
|
@@ -527,8 +533,8 @@ class BigQueryConnection {
|
|
|
527
533
|
for (let retries = 0; retries < 3; retries++) {
|
|
528
534
|
try {
|
|
529
535
|
return await job.getQueryResults({
|
|
530
|
-
|
|
531
|
-
...getQueryResultsOptions
|
|
536
|
+
timeoutMs: 1000 * 60 * 2,
|
|
537
|
+
...getQueryResultsOptions,
|
|
532
538
|
});
|
|
533
539
|
}
|
|
534
540
|
catch (fetchError) {
|
|
@@ -543,10 +549,10 @@ class BigQueryConnection {
|
|
|
543
549
|
}
|
|
544
550
|
async createBigQueryJob(createQueryJobOptions) {
|
|
545
551
|
const [job] = await this.bigQuery.createQueryJob({
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
...createQueryJobOptions
|
|
552
|
+
location: this.location,
|
|
553
|
+
maximumBytesBilled: this.config.maximumBytesBilled || MAXIMUM_BYTES_BILLED,
|
|
554
|
+
jobTimeoutMs: Number(this.config.timeoutMs) || TIMEOUT_MS,
|
|
555
|
+
...createQueryJobOptions,
|
|
550
556
|
});
|
|
551
557
|
return job;
|
|
552
558
|
}
|
|
@@ -563,9 +569,9 @@ class BigQueryConnection {
|
|
|
563
569
|
}
|
|
564
570
|
bigQuery
|
|
565
571
|
.createQueryStream(sqlCommand)
|
|
566
|
-
.on(
|
|
567
|
-
.on(
|
|
568
|
-
.on(
|
|
572
|
+
.on('error', onError)
|
|
573
|
+
.on('data', handleData)
|
|
574
|
+
.on('end', onEnd);
|
|
569
575
|
}
|
|
570
576
|
return (0, malloy_1.toAsyncGenerator)(streamBigQuery);
|
|
571
577
|
}
|
|
@@ -575,6 +581,6 @@ class BigQueryConnection {
|
|
|
575
581
|
}
|
|
576
582
|
exports.BigQueryConnection = BigQueryConnection;
|
|
577
583
|
BigQueryConnection.DEFAULT_QUERY_OPTIONS = {
|
|
578
|
-
|
|
584
|
+
rowLimit: 10,
|
|
579
585
|
};
|
|
580
586
|
//# 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-dev230301165401",
|
|
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-dev230301165401",
|
|
25
29
|
"gaxios": "^4.2.0"
|
|
26
30
|
}
|
|
27
31
|
}
|