@malloydata/db-bigquery 0.0.1
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/README.md +7 -0
- package/dist/bigquery.spec.d.ts +1 -0
- package/dist/bigquery.spec.js +148 -0
- package/dist/bigquery_connection.d.ts +91 -0
- package/dist/bigquery_connection.js +577 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Malloy
|
|
2
|
+
|
|
3
|
+
Malloy is an experimental language for describing data relationships and transformations. It is both a semantic modeling language and a querying language that runs queries against a relational database. Malloy currently connects to BigQuery and Postgres, and natively supports DuckDB. We've built a Visual Studio Code extension to facilitate building Malloy data models, querying and transforming data, and creating simple visualizations and dashboards.
|
|
4
|
+
|
|
5
|
+
## This package
|
|
6
|
+
|
|
7
|
+
This package facilitates using the `malloydata/malloy` library with BigQuery - see [here](https://github.com/looker-open-source/malloy/blob/main/packages/malloy/README.md) for additional information.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const malloy = __importStar(require("@malloydata/malloy"));
|
|
27
|
+
const bigquery_connection_1 = require("./bigquery_connection");
|
|
28
|
+
const bigquery_1 = require("@google-cloud/bigquery");
|
|
29
|
+
const util = __importStar(require("util"));
|
|
30
|
+
const fs = __importStar(require("fs"));
|
|
31
|
+
const url_1 = require("url");
|
|
32
|
+
describe("db:BigQuery", () => {
|
|
33
|
+
let bq;
|
|
34
|
+
let runtime;
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
bq = new bigquery_connection_1.BigQueryConnection("test");
|
|
37
|
+
const files = {
|
|
38
|
+
readURL: async (url) => {
|
|
39
|
+
const filePath = (0, url_1.fileURLToPath)(url);
|
|
40
|
+
return await util.promisify(fs.readFile)(filePath, "utf8");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
runtime = new malloy.Runtime(files, bq);
|
|
44
|
+
});
|
|
45
|
+
it("runs a SQL query", async () => {
|
|
46
|
+
const res = await bq.runSQL(`SELECT 1 as t`);
|
|
47
|
+
expect(res.rows[0]["t"]).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
it("costs a SQL query", async () => {
|
|
50
|
+
const res = await bq.costQuery(`SELECT * FROM malloy-data.faa.airports`);
|
|
51
|
+
expect(res).toBe(3029200);
|
|
52
|
+
});
|
|
53
|
+
it("gets table schema", async () => {
|
|
54
|
+
const res = await bq.getTableFieldSchema(`malloy-data.faa.carriers`);
|
|
55
|
+
expect(res.schema).toStrictEqual({
|
|
56
|
+
fields: [
|
|
57
|
+
{ name: "code", type: "STRING" },
|
|
58
|
+
{ name: "name", type: "STRING" },
|
|
59
|
+
{ name: "nickname", type: "STRING" },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it.todo("gets table structdefs");
|
|
64
|
+
it("runs a Malloy query", async () => {
|
|
65
|
+
const sql = await runtime
|
|
66
|
+
.loadModel("explore: carriers is table('malloy-data.faa.carriers') { measure: carrier_count is count() }")
|
|
67
|
+
.loadQuery("query: carriers -> { aggregate: carrier_count }")
|
|
68
|
+
.getSQL();
|
|
69
|
+
const res = await bq.runSQL(sql);
|
|
70
|
+
expect(res.rows[0]["carrier_count"]).toBe(21);
|
|
71
|
+
});
|
|
72
|
+
it("streams a Malloy query for download", async () => {
|
|
73
|
+
const sql = await runtime
|
|
74
|
+
.loadModel("explore: carriers is table('malloy-data.faa.carriers') { measure: carrier_count is count() }")
|
|
75
|
+
.loadQuery("query: carriers -> { group_by: name }")
|
|
76
|
+
.getSQL();
|
|
77
|
+
const res = await bq.downloadMalloyQuery(sql);
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
let count = 0;
|
|
80
|
+
res.on("data", () => (count += 1));
|
|
81
|
+
res.on("end", () => {
|
|
82
|
+
expect(count).toBe(21);
|
|
83
|
+
resolve(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("manifests a temporary table", async () => {
|
|
88
|
+
const fullTempTableName = await bq.manifestTemporaryTable("SELECT 1 as t");
|
|
89
|
+
const splitTableName = fullTempTableName.split(".");
|
|
90
|
+
const sdk = new bigquery_1.BigQuery();
|
|
91
|
+
const dataset = sdk.dataset(splitTableName[1]);
|
|
92
|
+
const table = dataset.table(splitTableName[2]);
|
|
93
|
+
const exists = await table.exists();
|
|
94
|
+
expect(exists).toBeTruthy();
|
|
95
|
+
});
|
|
96
|
+
describe.skip("manifests permanent table", () => {
|
|
97
|
+
const datasetName = "test_malloy_test_dataset";
|
|
98
|
+
const tableName = "test_malloy_test_table";
|
|
99
|
+
const sdk = new bigquery_1.BigQuery();
|
|
100
|
+
// delete entire dataset before each test and once tests are complete
|
|
101
|
+
const deleteTestDataset = async () => {
|
|
102
|
+
const dataset = sdk.dataset(datasetName);
|
|
103
|
+
if ((await dataset.exists())[0]) {
|
|
104
|
+
await dataset.delete({
|
|
105
|
+
force: true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
beforeEach(deleteTestDataset);
|
|
110
|
+
afterAll(deleteTestDataset);
|
|
111
|
+
it("throws if dataset does not exist and createDataset=false", async () => {
|
|
112
|
+
await expect(async () => {
|
|
113
|
+
await bq.manifestPermanentTable("SELECT 1 as t", datasetName, tableName, false, false);
|
|
114
|
+
}).rejects.toThrowError(`Dataset ${datasetName} does not exist`);
|
|
115
|
+
});
|
|
116
|
+
it("creates dataset if createDataset=true", async () => {
|
|
117
|
+
// note - dataset does not exist b/c of beforeEach()
|
|
118
|
+
await bq.manifestPermanentTable("SELECT 1 as t", datasetName, tableName, false, true);
|
|
119
|
+
const dataset = sdk.dataset(datasetName);
|
|
120
|
+
const [exists] = await dataset.exists();
|
|
121
|
+
expect(exists).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
it("throws if table exist and overwriteExistingTable=false", async () => {
|
|
124
|
+
const newDatasetResponse = await sdk.createDataset(datasetName);
|
|
125
|
+
const dataset = newDatasetResponse[0];
|
|
126
|
+
const tableMeta = { name: tableName };
|
|
127
|
+
await dataset.createTable(tableName, tableMeta);
|
|
128
|
+
await expect(async () => {
|
|
129
|
+
await bq.manifestPermanentTable("SELECT 1 as t", datasetName, tableName, false, true);
|
|
130
|
+
}).rejects.toThrowError(`Table ${tableName} already exists`);
|
|
131
|
+
});
|
|
132
|
+
it("manifests a table", async () => {
|
|
133
|
+
const jobId = await bq.manifestPermanentTable("SELECT 1 as t", datasetName, tableName, false, true);
|
|
134
|
+
// wait for job to complete
|
|
135
|
+
const [job] = await sdk.job(jobId).get();
|
|
136
|
+
let [metaData] = await job.getMetadata();
|
|
137
|
+
while (metaData.status.state !== "DONE") {
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
139
|
+
[metaData] = await job.getMetadata();
|
|
140
|
+
}
|
|
141
|
+
// query the new table
|
|
142
|
+
const [queryJob] = await sdk.createQueryJob(`SELECT * FROM ${datasetName}.${tableName}`);
|
|
143
|
+
const [results] = await queryJob.getQueryResults();
|
|
144
|
+
expect(results[0]).toStrictEqual({ t: 1 });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
//# sourceMappingURL=bigquery.spec.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
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 { QueryData, StructDef, MalloyQueryData, FieldTypeDef, SQLBlock, Connection, QueryDataRow } from "@malloydata/malloy";
|
|
5
|
+
import { PooledConnection } from "@malloydata/malloy";
|
|
6
|
+
import { FetchSchemaAndRunSimultaneously, FetchSchemaAndRunStreamSimultaneously, PersistSQLResults, StreamingConnection } from "@malloydata/malloy/src/runtime_types";
|
|
7
|
+
export interface BigQueryManagerOptions {
|
|
8
|
+
credentials?: {
|
|
9
|
+
clientId: string;
|
|
10
|
+
clientSecret: string;
|
|
11
|
+
refreshToken: string | null;
|
|
12
|
+
};
|
|
13
|
+
projectId?: string | undefined;
|
|
14
|
+
userAgent: string;
|
|
15
|
+
}
|
|
16
|
+
export interface BigQueryQueryOptions {
|
|
17
|
+
rowLimit: number;
|
|
18
|
+
}
|
|
19
|
+
interface BigQueryConnectionConfiguration {
|
|
20
|
+
defaultProject?: string;
|
|
21
|
+
serviceAccountKeyPath?: string;
|
|
22
|
+
location?: string;
|
|
23
|
+
maximumBytesBilled?: string;
|
|
24
|
+
timeoutMs?: string;
|
|
25
|
+
}
|
|
26
|
+
interface SchemaInfo {
|
|
27
|
+
schema: bigquery.ITableFieldSchema;
|
|
28
|
+
needsPartitionPsuedoColumn: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare type QueryOptionsReader = Partial<BigQueryQueryOptions> | (() => Partial<BigQueryQueryOptions>);
|
|
31
|
+
export declare class BigQueryAuthenticationError extends Error {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
34
|
+
export declare class BigQueryConnection implements Connection, PersistSQLResults, StreamingConnection, FetchSchemaAndRunSimultaneously {
|
|
35
|
+
static DEFAULT_QUERY_OPTIONS: BigQueryQueryOptions;
|
|
36
|
+
private bigQuery;
|
|
37
|
+
private projectId;
|
|
38
|
+
private temporaryTables;
|
|
39
|
+
private defaultProject;
|
|
40
|
+
private schemaCache;
|
|
41
|
+
private sqlSchemaCache;
|
|
42
|
+
private queryOptions?;
|
|
43
|
+
private config;
|
|
44
|
+
private location;
|
|
45
|
+
readonly name: string;
|
|
46
|
+
bqToMalloyTypes: {
|
|
47
|
+
[key: string]: Partial<FieldTypeDef>;
|
|
48
|
+
};
|
|
49
|
+
constructor(name: string, queryOptions?: QueryOptionsReader, config?: BigQueryConnectionConfiguration);
|
|
50
|
+
get dialectName(): string;
|
|
51
|
+
private readQueryOptions;
|
|
52
|
+
isPool(): this is PooledConnection;
|
|
53
|
+
canPersist(): this is PersistSQLResults;
|
|
54
|
+
canStream(): this is StreamingConnection;
|
|
55
|
+
canFetchSchemaAndRunSimultaneously(): this is FetchSchemaAndRunSimultaneously;
|
|
56
|
+
canFetchSchemaAndRunStreamSimultaneously(): this is FetchSchemaAndRunStreamSimultaneously;
|
|
57
|
+
private _runSQL;
|
|
58
|
+
runSQL(sqlCommand: string, options?: Partial<BigQueryQueryOptions>, rowIndex?: number): Promise<MalloyQueryData>;
|
|
59
|
+
runSQLBlockAndFetchResultSchema(sqlBlock: SQLBlock, options?: {
|
|
60
|
+
rowLimit?: number | undefined;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
data: MalloyQueryData;
|
|
63
|
+
schema: StructDef;
|
|
64
|
+
}>;
|
|
65
|
+
downloadMalloyQuery(sqlCommand: string): Promise<ResourceStream<RowMetadata>>;
|
|
66
|
+
private dryRunSQLQuery;
|
|
67
|
+
costQuery(sqlCommand: string): Promise<number>;
|
|
68
|
+
structDefFromSQL(sqlCommand: string): Promise<StructDef>;
|
|
69
|
+
getTableFieldSchema(tableURL: string): Promise<SchemaInfo>;
|
|
70
|
+
executeSQLRaw(sqlCommand: string): Promise<QueryData>;
|
|
71
|
+
test(): Promise<void>;
|
|
72
|
+
manifestTemporaryTable(sqlCommand: string): Promise<string>;
|
|
73
|
+
manifestPermanentTable(sqlCommand: string, datasetName: string, tableName: string, overwriteExistingTable?: boolean, createDataset?: boolean): Promise<string>;
|
|
74
|
+
private addFieldsToStructDef;
|
|
75
|
+
private tableURLtoTablePath;
|
|
76
|
+
private structDefFromTableSchema;
|
|
77
|
+
private structDefFromSQLSchema;
|
|
78
|
+
fetchSchemaForTables(missing: string[]): Promise<{
|
|
79
|
+
schemas: Record<string, StructDef>;
|
|
80
|
+
errors: Record<string, string>;
|
|
81
|
+
}>;
|
|
82
|
+
private getSQLBlockSchema;
|
|
83
|
+
fetchSchemaForSQLBlocks(sqlRefs: SQLBlock[]): Promise<{
|
|
84
|
+
schemas: Record<string, StructDef>;
|
|
85
|
+
errors: Record<string, string>;
|
|
86
|
+
}>;
|
|
87
|
+
private createBigQueryJobAndGetResults;
|
|
88
|
+
private createBigQueryJob;
|
|
89
|
+
runSQLStream(sqlCommand: string, options?: Partial<BigQueryQueryOptions>): AsyncIterableIterator<QueryDataRow>;
|
|
90
|
+
}
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright 2022 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* This program is free software; you can redistribute it and/or
|
|
6
|
+
* modify it under the terms of the GNU General Public License
|
|
7
|
+
* version 2 as published by the Free Software Foundation.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.BigQueryConnection = exports.BigQueryAuthenticationError = void 0;
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const bigquery_1 = require("@google-cloud/bigquery");
|
|
41
|
+
const googleCommon = __importStar(require("@google-cloud/common"));
|
|
42
|
+
const gaxios_1 = require("gaxios");
|
|
43
|
+
const malloy_1 = require("@malloydata/malloy");
|
|
44
|
+
const malloy_2 = require("@malloydata/malloy");
|
|
45
|
+
// BigQuery SDK apparently throws various authentication errors from Gaxios (https://github.com/googleapis/gaxios) and from
|
|
46
|
+
// @google-cloud/common (https://www.npmjs.com/package/@google-cloud/common)
|
|
47
|
+
// catching and rewriting here a single kind of authentication error we can consistently catch further up the stack
|
|
48
|
+
class BigQueryAuthenticationError extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "BigQueryAuthenticationError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.BigQueryAuthenticationError = BigQueryAuthenticationError;
|
|
55
|
+
const maybeRewriteError = (e) => {
|
|
56
|
+
// GaxiosError happens if credentials are revoked (for example, client.revokeCredentials()) or if
|
|
57
|
+
// the refresh token is invalid
|
|
58
|
+
// ApiErrors happen if token is revoked (for example, client.revokeToken(creds.access_token!))
|
|
59
|
+
if (e instanceof Error) {
|
|
60
|
+
if ((e instanceof gaxios_1.GaxiosError && e.code === "400") ||
|
|
61
|
+
(e instanceof googleCommon.ApiError && e.code === 401)) {
|
|
62
|
+
return new BigQueryAuthenticationError(e.message);
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
return e;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// something throw a non-Error, and we didn't expect that
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Default maximumBytesBilled value, 25GiB
|
|
74
|
+
*/
|
|
75
|
+
const MAXIMUM_BYTES_BILLED = String(25 * 1024 * 1024 * 1024);
|
|
76
|
+
/**
|
|
77
|
+
* Default timeoutMs value, 10 Mins
|
|
78
|
+
*/
|
|
79
|
+
const TIMEOUT_MS = 1000 * 60 * 10;
|
|
80
|
+
// manage access to BQ, control costs, enforce global data/API limits
|
|
81
|
+
class BigQueryConnection {
|
|
82
|
+
constructor(name, queryOptions, config = {}) {
|
|
83
|
+
this.temporaryTables = new Map();
|
|
84
|
+
this.schemaCache = new Map();
|
|
85
|
+
this.sqlSchemaCache = new Map();
|
|
86
|
+
this.bqToMalloyTypes = {
|
|
87
|
+
DATE: { type: "date" },
|
|
88
|
+
STRING: { type: "string" },
|
|
89
|
+
INTEGER: { type: "number", numberType: "integer" },
|
|
90
|
+
INT64: { type: "number", numberType: "integer" },
|
|
91
|
+
FLOAT: { type: "number", numberType: "float" },
|
|
92
|
+
FLOAT64: { type: "number", numberType: "float" },
|
|
93
|
+
NUMERIC: { type: "number", numberType: "float" },
|
|
94
|
+
BIGNUMERIC: { type: "number", numberType: "float" },
|
|
95
|
+
TIMESTAMP: { type: "timestamp" },
|
|
96
|
+
BOOLEAN: { type: "boolean" },
|
|
97
|
+
BOOL: { type: "boolean" },
|
|
98
|
+
// TODO (https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#tablefieldschema):
|
|
99
|
+
// BYTES
|
|
100
|
+
// DATETIME
|
|
101
|
+
// TIME
|
|
102
|
+
// GEOGRAPHY
|
|
103
|
+
};
|
|
104
|
+
this.name = name;
|
|
105
|
+
this.bigQuery = new bigquery_1.BigQuery({
|
|
106
|
+
userAgent: `Malloy/${malloy_1.Malloy.version}`,
|
|
107
|
+
keyFilename: config.serviceAccountKeyPath,
|
|
108
|
+
});
|
|
109
|
+
// record project ID because for unclear reasons we have to modify the project ID on the SDK when
|
|
110
|
+
// we want to use the tables API
|
|
111
|
+
this.projectId = this.bigQuery.projectId;
|
|
112
|
+
this.defaultProject = config.defaultProject || this.bigQuery.projectId;
|
|
113
|
+
this.queryOptions = queryOptions;
|
|
114
|
+
this.config = config;
|
|
115
|
+
this.location = config.location || "US";
|
|
116
|
+
}
|
|
117
|
+
get dialectName() {
|
|
118
|
+
return "standardsql";
|
|
119
|
+
}
|
|
120
|
+
readQueryOptions() {
|
|
121
|
+
const options = BigQueryConnection.DEFAULT_QUERY_OPTIONS;
|
|
122
|
+
if (this.queryOptions) {
|
|
123
|
+
if (this.queryOptions instanceof Function) {
|
|
124
|
+
return { ...options, ...this.queryOptions() };
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
return { ...options, ...this.queryOptions };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
isPool() {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
canPersist() {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
canStream() {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
canFetchSchemaAndRunSimultaneously() {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
canFetchSchemaAndRunStreamSimultaneously() {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
async _runSQL(sqlCommand, options = {}, rowIndex = 0) {
|
|
150
|
+
var _a, _b, _c, _d;
|
|
151
|
+
const defaultOptions = this.readQueryOptions();
|
|
152
|
+
const pageSize = (_a = options.rowLimit) !== null && _a !== void 0 ? _a : defaultOptions.rowLimit;
|
|
153
|
+
try {
|
|
154
|
+
const queryResultsOptions = {
|
|
155
|
+
maxResults: pageSize,
|
|
156
|
+
startIndex: rowIndex.toString(),
|
|
157
|
+
};
|
|
158
|
+
const jobResult = await this.createBigQueryJobAndGetResults(sqlCommand, undefined, queryResultsOptions);
|
|
159
|
+
const totalRows = +(((_b = jobResult[2]) === null || _b === void 0 ? void 0 : _b.totalRows)
|
|
160
|
+
? jobResult[2].totalRows
|
|
161
|
+
: "0");
|
|
162
|
+
// TODO need to probably surface the cause of the schema not present error
|
|
163
|
+
if (((_c = jobResult[2]) === null || _c === void 0 ? void 0 : _c.schema) === undefined) {
|
|
164
|
+
throw new Error("Schema not present");
|
|
165
|
+
}
|
|
166
|
+
// TODO even though we have 10 minute timeout limit, we still should confirm that resulting metadata has "jobComplete: true"
|
|
167
|
+
const data = { rows: jobResult[0], totalRows };
|
|
168
|
+
const schema = (_d = jobResult[2]) === null || _d === void 0 ? void 0 : _d.schema;
|
|
169
|
+
return { data, schema };
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
throw maybeRewriteError(e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async runSQL(sqlCommand, options = {}, rowIndex = 0) {
|
|
176
|
+
const { data } = await this._runSQL(sqlCommand, options, rowIndex);
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
async runSQLBlockAndFetchResultSchema(sqlBlock, options) {
|
|
180
|
+
const { data, schema: schemaRaw } = await this._runSQL(sqlBlock.select, options);
|
|
181
|
+
const schema = this.structDefFromSQLSchema(sqlBlock, schemaRaw);
|
|
182
|
+
return { data, schema };
|
|
183
|
+
}
|
|
184
|
+
async downloadMalloyQuery(sqlCommand) {
|
|
185
|
+
const job = await this.createBigQueryJob({
|
|
186
|
+
query: sqlCommand,
|
|
187
|
+
});
|
|
188
|
+
return job.getQueryResultsStream();
|
|
189
|
+
}
|
|
190
|
+
async dryRunSQLQuery(sqlCommand) {
|
|
191
|
+
try {
|
|
192
|
+
const [result] = await this.bigQuery.createQueryJob({
|
|
193
|
+
location: this.location,
|
|
194
|
+
query: sqlCommand,
|
|
195
|
+
dryRun: true,
|
|
196
|
+
});
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
throw maybeRewriteError(e);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async costQuery(sqlCommand) {
|
|
204
|
+
const dryRunResults = await this.dryRunSQLQuery(sqlCommand);
|
|
205
|
+
return Number(dryRunResults.metadata.statistics.totalBytesProcessed);
|
|
206
|
+
}
|
|
207
|
+
async structDefFromSQL(sqlCommand) {
|
|
208
|
+
const dryRunResults = await this.dryRunSQLQuery(sqlCommand);
|
|
209
|
+
const destinationTable = dryRunResults.metadata.configuration.query.destinationTable;
|
|
210
|
+
return this.structDefFromTableSchema(`${destinationTable.projectId}.${destinationTable.datasetId}.${destinationTable.tableId}`, dryRunResults.metadata.statistics.query.schema);
|
|
211
|
+
}
|
|
212
|
+
async getTableFieldSchema(tableURL) {
|
|
213
|
+
var _a, _b;
|
|
214
|
+
const { tablePath: tableName } = (0, malloy_2.parseTableURL)(tableURL);
|
|
215
|
+
const segments = tableName.split(".");
|
|
216
|
+
// paths can have two or three segments
|
|
217
|
+
// if there are only two segments, assume the dataset is "local" to the current billing project
|
|
218
|
+
let projectId, datasetNamePart, tableNamePart;
|
|
219
|
+
if (segments.length === 2) {
|
|
220
|
+
[datasetNamePart, tableNamePart] = segments;
|
|
221
|
+
projectId = this.defaultProject;
|
|
222
|
+
}
|
|
223
|
+
else if (segments.length === 3)
|
|
224
|
+
[projectId, datasetNamePart, tableNamePart] = segments;
|
|
225
|
+
else
|
|
226
|
+
throw new Error(`Improper table path: ${tableName}. A table path requires 2 or 3 segments`);
|
|
227
|
+
try {
|
|
228
|
+
// TODO The `dataset` API has no way to set a different `projectId` than the one stored in the BQ
|
|
229
|
+
// instance. So we hack it until a better way exists: we set the `this.bigQuery.projectId`
|
|
230
|
+
// to the `projectId` for the dataset, then put it back when we're done. Importantly, we
|
|
231
|
+
// set it back _before_ we await the promise, thus avoiding a "concurrency" issue. We've decided
|
|
232
|
+
// this is better than creating a new BQ instance every time we need to get a table schema.
|
|
233
|
+
if (projectId)
|
|
234
|
+
this.bigQuery.projectId = projectId;
|
|
235
|
+
const table = this.bigQuery.dataset(datasetNamePart).table(tableNamePart);
|
|
236
|
+
const metadataPromise = table.getMetadata();
|
|
237
|
+
this.bigQuery.projectId = this.projectId;
|
|
238
|
+
const [metadata] = await metadataPromise;
|
|
239
|
+
return {
|
|
240
|
+
schema: metadata.schema,
|
|
241
|
+
needsPartitionPsuedoColumn: ((_a = metadata.timePartitioning) === null || _a === void 0 ? void 0 : _a.type) !== undefined &&
|
|
242
|
+
((_b = metadata.timePartitioning) === null || _b === void 0 ? void 0 : _b.field) === undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
throw maybeRewriteError(e);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async executeSQLRaw(sqlCommand) {
|
|
250
|
+
const result = await this.createBigQueryJobAndGetResults(sqlCommand);
|
|
251
|
+
return result[0];
|
|
252
|
+
}
|
|
253
|
+
async test() {
|
|
254
|
+
await this.dryRunSQLQuery("SELECT 1");
|
|
255
|
+
}
|
|
256
|
+
/*
|
|
257
|
+
* Do we need to care about multiple overlapping calls to this function? No.
|
|
258
|
+
*
|
|
259
|
+
* If two long-running jobs using the exact same SQL are started by the same user and overlap - meaning, there is
|
|
260
|
+
* a job running with some SQL, and while it is PENDING or RUNNING, another job with the same SQL is added -
|
|
261
|
+
* the ultimate destination table for those jobs is the same table, if the source table(s) haven't changed, and
|
|
262
|
+
* SQL didn't use some mutable functions.
|
|
263
|
+
*
|
|
264
|
+
* If second job goes to RUNNING after first job finished, it will just reuse its results (this is the caching
|
|
265
|
+
* behavior). But if both of them are RUNNING at the same time, they will both do the work and consume the slots.
|
|
266
|
+
* If they finish around same time, the second job discards its results. If second job finishes much later than
|
|
267
|
+
* the first one (like an hour or so), it will update this destination table to extend its TTL
|
|
268
|
+
*
|
|
269
|
+
* This means that longer-term, we may want to block multiple calls at the app level (if tons of dashboards were
|
|
270
|
+
* hitting this, creating many instances of the same table run, that might start to cost $$) but for now we're
|
|
271
|
+
* ok with this simple approach
|
|
272
|
+
*
|
|
273
|
+
*/
|
|
274
|
+
async manifestTemporaryTable(sqlCommand) {
|
|
275
|
+
const hash = crypto.createHash("md5").update(sqlCommand).digest("hex");
|
|
276
|
+
const tempTableName = this.temporaryTables.get(hash);
|
|
277
|
+
if (tempTableName !== undefined) {
|
|
278
|
+
return tempTableName;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
try {
|
|
282
|
+
const [job] = await this.bigQuery.createQueryJob({
|
|
283
|
+
location: this.location,
|
|
284
|
+
query: sqlCommand,
|
|
285
|
+
});
|
|
286
|
+
let [metaData] = await job.getMetadata();
|
|
287
|
+
// wait for job to complete, because we need the table name
|
|
288
|
+
// TODO just because a job is "DONE" doesn't mean it ended correctly, should probably also confirm
|
|
289
|
+
// status is successful & that table was created
|
|
290
|
+
// TODO this needs better error handling and a timeout so that issues dont result in infinite looping
|
|
291
|
+
while (metaData.status.state !== "DONE") {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
293
|
+
[metaData] = await job.getMetadata();
|
|
294
|
+
}
|
|
295
|
+
// save table name
|
|
296
|
+
if (metaData.configuration &&
|
|
297
|
+
metaData.configuration.query &&
|
|
298
|
+
metaData.configuration.query.destinationTable) {
|
|
299
|
+
const destinationTable = metaData.configuration.query.destinationTable;
|
|
300
|
+
const fullTableName = `${destinationTable.projectId}.${destinationTable.datasetId}.${destinationTable.tableId}`;
|
|
301
|
+
this.temporaryTables.set(hash, fullTableName);
|
|
302
|
+
return fullTableName;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
throw new Error("bigquery.job.getMetadata() - metadata should have configuration but does not");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
throw maybeRewriteError(e);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// TODO there is reasonable argument that this has nothing to do with Malloy, and should be implemented
|
|
314
|
+
// by whatever library is using Malloy.
|
|
315
|
+
async manifestPermanentTable(sqlCommand, datasetName, tableName, overwriteExistingTable = false, createDataset = false) {
|
|
316
|
+
let dataset = this.bigQuery.dataset(datasetName);
|
|
317
|
+
if (!(await dataset.exists())[0]) {
|
|
318
|
+
if (createDataset) {
|
|
319
|
+
const newDatasetResponse = await this.bigQuery.createDataset(datasetName);
|
|
320
|
+
dataset = newDatasetResponse[0];
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
throw new Error(`Dataset ${datasetName} does not exist`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const table = dataset.table(tableName);
|
|
327
|
+
if ((await table.exists())[0] && !overwriteExistingTable) {
|
|
328
|
+
throw new Error(`Table ${tableName} already exists`);
|
|
329
|
+
}
|
|
330
|
+
const [job] = await this.bigQuery.createQueryJob({
|
|
331
|
+
query: sqlCommand,
|
|
332
|
+
location: this.location,
|
|
333
|
+
destination: table,
|
|
334
|
+
});
|
|
335
|
+
// if creating this job didn't throw, there's an ID.
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
337
|
+
return job.id;
|
|
338
|
+
}
|
|
339
|
+
addFieldsToStructDef(structDef, tableFieldSchema) {
|
|
340
|
+
if (!tableFieldSchema.fields)
|
|
341
|
+
return;
|
|
342
|
+
for (const field of tableFieldSchema.fields) {
|
|
343
|
+
// BigQuery SDK sets type & name to optional even though they are required, assume they exist
|
|
344
|
+
const type = field.type;
|
|
345
|
+
const name = field.name;
|
|
346
|
+
// check for an array
|
|
347
|
+
if (field.mode === "REPEATED" && !["STRUCT", "RECORD"].includes(type)) {
|
|
348
|
+
const malloyType = this.bqToMalloyTypes[type];
|
|
349
|
+
if (malloyType) {
|
|
350
|
+
const innerStructDef = {
|
|
351
|
+
type: "struct",
|
|
352
|
+
name,
|
|
353
|
+
dialect: this.dialectName,
|
|
354
|
+
structSource: { type: "nested" },
|
|
355
|
+
structRelationship: { type: "nested", field: name, isArray: true },
|
|
356
|
+
fields: [{ ...malloyType, name: "value" }],
|
|
357
|
+
};
|
|
358
|
+
structDef.fields.push(innerStructDef);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else if (["STRUCT", "RECORD"].includes(type)) {
|
|
362
|
+
const innerStructDef = {
|
|
363
|
+
type: "struct",
|
|
364
|
+
name,
|
|
365
|
+
dialect: this.dialectName,
|
|
366
|
+
structSource: field.mode === "REPEATED" ? { type: "nested" } : { type: "inline" },
|
|
367
|
+
structRelationship: field.mode === "REPEATED"
|
|
368
|
+
? { type: "nested", field: name, isArray: false }
|
|
369
|
+
: { type: "inline" },
|
|
370
|
+
fields: [],
|
|
371
|
+
};
|
|
372
|
+
this.addFieldsToStructDef(innerStructDef, field);
|
|
373
|
+
structDef.fields.push(innerStructDef);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const malloyType = this.bqToMalloyTypes[type];
|
|
377
|
+
if (malloyType) {
|
|
378
|
+
structDef.fields.push({ name, ...malloyType });
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
structDef.fields.push({
|
|
382
|
+
name,
|
|
383
|
+
type: "string",
|
|
384
|
+
e: [`BigQuery type "${type}" not supported by Malloy`],
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
tableURLtoTablePath(tableURL) {
|
|
391
|
+
const { tablePath } = (0, malloy_2.parseTableURL)(tableURL);
|
|
392
|
+
if (tablePath.split(".").length === 2) {
|
|
393
|
+
return `${this.defaultProject}.${tablePath}`;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
return tablePath;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
structDefFromTableSchema(tableURL, schemaInfo) {
|
|
400
|
+
const structDef = {
|
|
401
|
+
type: "struct",
|
|
402
|
+
name: tableURL,
|
|
403
|
+
dialect: this.dialectName,
|
|
404
|
+
structSource: {
|
|
405
|
+
type: "table",
|
|
406
|
+
tablePath: this.tableURLtoTablePath(tableURL),
|
|
407
|
+
},
|
|
408
|
+
structRelationship: { type: "basetable", connectionName: this.name },
|
|
409
|
+
fields: [],
|
|
410
|
+
};
|
|
411
|
+
this.addFieldsToStructDef(structDef, schemaInfo.schema);
|
|
412
|
+
if (schemaInfo.needsPartitionPsuedoColumn) {
|
|
413
|
+
structDef.fields.push({
|
|
414
|
+
type: "timestamp",
|
|
415
|
+
name: "_PARTITIONTIME",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return structDef;
|
|
419
|
+
}
|
|
420
|
+
structDefFromSQLSchema(sqlBlock, tableFieldSchema) {
|
|
421
|
+
const structDef = {
|
|
422
|
+
type: "struct",
|
|
423
|
+
name: sqlBlock.name,
|
|
424
|
+
dialect: this.dialectName,
|
|
425
|
+
structSource: {
|
|
426
|
+
type: "sql",
|
|
427
|
+
method: "subquery",
|
|
428
|
+
sqlBlock,
|
|
429
|
+
},
|
|
430
|
+
structRelationship: { type: "basetable", connectionName: this.name },
|
|
431
|
+
fields: [],
|
|
432
|
+
};
|
|
433
|
+
this.addFieldsToStructDef(structDef, tableFieldSchema);
|
|
434
|
+
return structDef;
|
|
435
|
+
}
|
|
436
|
+
async fetchSchemaForTables(missing) {
|
|
437
|
+
const schemas = {};
|
|
438
|
+
const errors = {};
|
|
439
|
+
for (const tableURL of missing) {
|
|
440
|
+
let inCache = this.schemaCache.get(tableURL);
|
|
441
|
+
if (!inCache) {
|
|
442
|
+
try {
|
|
443
|
+
const tableFieldSchema = await this.getTableFieldSchema(tableURL);
|
|
444
|
+
inCache = {
|
|
445
|
+
schema: this.structDefFromTableSchema(tableURL, tableFieldSchema),
|
|
446
|
+
};
|
|
447
|
+
this.schemaCache.set(tableURL, inCache);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
inCache = { error: error.message };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (inCache.schema !== undefined) {
|
|
454
|
+
schemas[tableURL] = inCache.schema;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
errors[tableURL] = inCache.error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return { schemas, errors };
|
|
461
|
+
}
|
|
462
|
+
async getSQLBlockSchema(sqlRef) {
|
|
463
|
+
// We do a simple retry-loop here, as a temporary fix for a transient
|
|
464
|
+
// error in which sometimes requesting results from a job yields an
|
|
465
|
+
// access denied error. It seems that in these cases, simply trying again
|
|
466
|
+
// solves the problem. This is being currently investigated by
|
|
467
|
+
// @christopherswenson and @lloydtabb. Same as below.
|
|
468
|
+
let lastFetchError;
|
|
469
|
+
for (let retries = 0; retries < 3; retries++) {
|
|
470
|
+
try {
|
|
471
|
+
const [job] = await this.bigQuery.createQueryJob({
|
|
472
|
+
location: this.location,
|
|
473
|
+
query: sqlRef.select,
|
|
474
|
+
dryRun: true,
|
|
475
|
+
});
|
|
476
|
+
return job.metadata.statistics.query.schema;
|
|
477
|
+
}
|
|
478
|
+
catch (fetchError) {
|
|
479
|
+
lastFetchError = fetchError;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
throw lastFetchError;
|
|
483
|
+
}
|
|
484
|
+
async fetchSchemaForSQLBlocks(sqlRefs) {
|
|
485
|
+
const schemas = {};
|
|
486
|
+
const errors = {};
|
|
487
|
+
for (const sqlRef of sqlRefs) {
|
|
488
|
+
const key = sqlRef.name;
|
|
489
|
+
let inCache = this.sqlSchemaCache.get(key);
|
|
490
|
+
if (!inCache) {
|
|
491
|
+
try {
|
|
492
|
+
const tableFieldSchema = await this.getSQLBlockSchema(sqlRef);
|
|
493
|
+
inCache = {
|
|
494
|
+
schema: this.structDefFromSQLSchema(sqlRef, tableFieldSchema),
|
|
495
|
+
};
|
|
496
|
+
this.schemaCache.set(key, inCache);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
inCache = { error: error.message };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (inCache.schema !== undefined) {
|
|
503
|
+
schemas[key] = inCache.schema;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
errors[key] = inCache.error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { schemas, errors };
|
|
510
|
+
}
|
|
511
|
+
// TODO this needs to extend the wait for results using a timeout set by the user,
|
|
512
|
+
// and probably needs to loop to check for results - BQ docs now say that after ~2min of waiting,
|
|
513
|
+
// no matter what you set for timeoutMs, they will probably just return.
|
|
514
|
+
async createBigQueryJobAndGetResults(sqlCommand, createQueryJobOptions, getQueryResultsOptions) {
|
|
515
|
+
try {
|
|
516
|
+
const job = await this.createBigQueryJob({
|
|
517
|
+
query: sqlCommand,
|
|
518
|
+
...createQueryJobOptions,
|
|
519
|
+
});
|
|
520
|
+
// TODO we should check if this is still required?
|
|
521
|
+
// We do a simple retry-loop here, as a temporary fix for a transient
|
|
522
|
+
// error in which sometimes requesting results from a job yields an
|
|
523
|
+
// access denied error. It seems that in these cases, simply trying again
|
|
524
|
+
// solves the problem. This is being currently investigated by
|
|
525
|
+
// @christopherswenson and @lloydtabb.
|
|
526
|
+
let lastFetchError;
|
|
527
|
+
for (let retries = 0; retries < 3; retries++) {
|
|
528
|
+
try {
|
|
529
|
+
return await job.getQueryResults({
|
|
530
|
+
timeoutMs: 1000 * 60 * 2,
|
|
531
|
+
...getQueryResultsOptions,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
catch (fetchError) {
|
|
535
|
+
lastFetchError = fetchError;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
throw lastFetchError;
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
throw maybeRewriteError(e);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async createBigQueryJob(createQueryJobOptions) {
|
|
545
|
+
const [job] = await this.bigQuery.createQueryJob({
|
|
546
|
+
location: this.location,
|
|
547
|
+
maximumBytesBilled: this.config.maximumBytesBilled || MAXIMUM_BYTES_BILLED,
|
|
548
|
+
jobTimeoutMs: Number(this.config.timeoutMs) || TIMEOUT_MS,
|
|
549
|
+
...createQueryJobOptions,
|
|
550
|
+
});
|
|
551
|
+
return job;
|
|
552
|
+
}
|
|
553
|
+
runSQLStream(sqlCommand, options = {}) {
|
|
554
|
+
const bigQuery = this.bigQuery;
|
|
555
|
+
function streamBigQuery(onError, onData, onEnd) {
|
|
556
|
+
let index = 0;
|
|
557
|
+
function handleData(rowMetadata) {
|
|
558
|
+
onData(rowMetadata);
|
|
559
|
+
index += 1;
|
|
560
|
+
if (options.rowLimit !== undefined && index >= options.rowLimit) {
|
|
561
|
+
this.end();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
bigQuery
|
|
565
|
+
.createQueryStream(sqlCommand)
|
|
566
|
+
.on("error", onError)
|
|
567
|
+
.on("data", handleData)
|
|
568
|
+
.on("end", onEnd);
|
|
569
|
+
}
|
|
570
|
+
return (0, malloy_1.toAsyncGenerator)(streamBigQuery);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
exports.BigQueryConnection = BigQueryConnection;
|
|
574
|
+
BigQueryConnection.DEFAULT_QUERY_OPTIONS = {
|
|
575
|
+
rowLimit: 10,
|
|
576
|
+
};
|
|
577
|
+
//# sourceMappingURL=bigquery_connection.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BigQueryConnection } from "./bigquery_connection";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* This program is free software; you can redistribute it and/or
|
|
6
|
+
* modify it under the terms of the GNU General Public License
|
|
7
|
+
* version 2 as published by the Free Software Foundation.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.BigQueryConnection = void 0;
|
|
16
|
+
var bigquery_connection_1 = require("./bigquery_connection");
|
|
17
|
+
Object.defineProperty(exports, "BigQueryConnection", { enumerable: true, get: function () { return bigquery_connection_1.BigQueryConnection; } });
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@malloydata/db-bigquery",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "GPL-2.0",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"lint": "eslint '**/*.ts{,x}'",
|
|
9
|
+
"lint-fix": "eslint '**/*.ts{,x}' --fix",
|
|
10
|
+
"test": "jest --config=../../jest.config.js",
|
|
11
|
+
"build": "tsc --build",
|
|
12
|
+
"malloyc": "ts-node ../../scripts/malloy-to-json"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@google-cloud/bigquery": "^5.5.0",
|
|
16
|
+
"@google-cloud/common": "^3.6.0",
|
|
17
|
+
"@malloydata/malloy": "^0.0.1",
|
|
18
|
+
"gaxios": "^4.2.0"
|
|
19
|
+
}
|
|
20
|
+
}
|