@malloydata/db-snowflake 0.0.126-dev240305182920

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.
@@ -0,0 +1,359 @@
1
+ /*
2
+ * Copyright 2023 Google LLC
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining
5
+ * a copy of this software and associated documentation files
6
+ * (the "Software"), to deal in the Software without restriction,
7
+ * including without limitation the rights to use, copy, modify, merge,
8
+ * publish, distribute, sublicense, and/or sell copies of the Software,
9
+ * and to permit persons to whom the Software is furnished to do so,
10
+ * subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be
13
+ * included in all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import * as crypto from 'crypto';
25
+ import {
26
+ RunSQLOptions,
27
+ MalloyQueryData,
28
+ QueryRunStats,
29
+ Connection,
30
+ PersistSQLResults,
31
+ StreamingConnection,
32
+ PooledConnection,
33
+ SQLBlock,
34
+ StructDef,
35
+ QueryDataRow,
36
+ SnowflakeDialect,
37
+ NamedStructDefs,
38
+ } from '@malloydata/malloy';
39
+ import {SnowflakeExecutor} from './snowflake_executor';
40
+ import {
41
+ FetchSchemaOptions,
42
+ TestableConnection,
43
+ } from '@malloydata/malloy/dist/runtime_types';
44
+ import {ConnectionOptions} from 'snowflake-sdk';
45
+ import {Options as PoolOptions} from 'generic-pool';
46
+
47
+ type namespace = {database: string; schema: string};
48
+
49
+ export interface SnowflakeConnectionOptions {
50
+ // snowflake sdk connection options
51
+ connOptions?: ConnectionOptions;
52
+ // generic pool options to help maintain a pool of connections to snowflake
53
+ poolOptions?: PoolOptions;
54
+
55
+ // the database and schema where we can perform temporary table operations.
56
+ // for example, if we want to create a temp table for fetching schema of an sql block
57
+ // we could use this database & schema instead of the main database & schema
58
+ scratchSpace?: namespace;
59
+
60
+ queryOptions?: RunSQLOptions;
61
+ }
62
+
63
+ export class SnowflakeConnection
64
+ implements
65
+ Connection,
66
+ PersistSQLResults,
67
+ StreamingConnection,
68
+ TestableConnection
69
+ {
70
+ private readonly dialect = new SnowflakeDialect();
71
+ private executor: SnowflakeExecutor;
72
+ private schemaCache = new Map<
73
+ string,
74
+ | {schema: StructDef; error?: undefined; timestamp: number}
75
+ | {error: string; schema?: undefined; timestamp: number}
76
+ >();
77
+ private sqlSchemaCache = new Map<
78
+ string,
79
+ | {
80
+ structDef: StructDef;
81
+ error?: undefined;
82
+ timestamp: number;
83
+ }
84
+ | {error: string; structDef?: undefined; timestamp: number}
85
+ >();
86
+
87
+ // the database & schema where we do temporary operations like creating a temp table
88
+ private scratchSpace?: namespace;
89
+ private queryOptions: RunSQLOptions;
90
+
91
+ constructor(
92
+ public readonly name: string,
93
+ options?: SnowflakeConnectionOptions
94
+ ) {
95
+ let connOptions = options?.connOptions;
96
+ if (connOptions === undefined) {
97
+ // try to get connection options from ~/.snowflake/connections.toml
98
+ connOptions = SnowflakeExecutor.getConnectionOptionsFromToml();
99
+ }
100
+ this.executor = new SnowflakeExecutor(connOptions, options?.poolOptions);
101
+ this.scratchSpace = options?.scratchSpace;
102
+ this.queryOptions = options?.queryOptions ?? {};
103
+ }
104
+
105
+ get dialectName(): string {
106
+ return 'snowflake';
107
+ }
108
+
109
+ // TODO: make it support nesting soon
110
+ public get supportsNesting(): boolean {
111
+ return false;
112
+ }
113
+
114
+ public isPool(): this is PooledConnection {
115
+ return true;
116
+ }
117
+
118
+ public canPersist(): this is PersistSQLResults {
119
+ return true;
120
+ }
121
+
122
+ public canStream(): this is StreamingConnection {
123
+ return true;
124
+ }
125
+
126
+ public async estimateQueryCost(_sqlCommand: string): Promise<QueryRunStats> {
127
+ return {};
128
+ }
129
+
130
+ async close(): Promise<void> {
131
+ await this.executor.done();
132
+ }
133
+
134
+ private getTempTableName(sqlCommand: string): string {
135
+ const hash = crypto.createHash('md5').update(sqlCommand).digest('hex');
136
+ let tableName = `tt${hash}`;
137
+ if (this.scratchSpace) {
138
+ tableName = `${this.scratchSpace.database}.${this.scratchSpace.schema}.${tableName}`;
139
+ }
140
+ return tableName;
141
+ }
142
+
143
+ public async runSQL(
144
+ sql: string,
145
+ options?: RunSQLOptions
146
+ ): Promise<MalloyQueryData> {
147
+ const rowLimit = options?.rowLimit ?? this.queryOptions?.rowLimit;
148
+ let rows = await this.executor.batch(sql);
149
+ if (rowLimit !== undefined && rows.length > rowLimit) {
150
+ rows = rows.slice(0, rowLimit);
151
+ }
152
+ return {rows, totalRows: rows.length};
153
+ }
154
+
155
+ public async *runSQLStream(
156
+ sqlCommand: string,
157
+ options: RunSQLOptions = {}
158
+ ): AsyncIterableIterator<QueryDataRow> {
159
+ const streamQueryOptions = {
160
+ ...this.queryOptions,
161
+ ...options,
162
+ };
163
+
164
+ for await (const row of await this.executor.stream(
165
+ sqlCommand,
166
+ streamQueryOptions
167
+ )) {
168
+ yield row;
169
+ }
170
+ }
171
+
172
+ public async test(): Promise<void> {
173
+ await this.executor.batch('SELECT 1');
174
+ }
175
+
176
+ private async schemaFromQuery(
177
+ infoQuery: string,
178
+ structDef: StructDef
179
+ ): Promise<void> {
180
+ const rows = await this.executor.batch(infoQuery);
181
+ for (const row of rows) {
182
+ const snowflakeDataType = row['DATA_TYPE'] as string;
183
+ const s = structDef;
184
+ const malloyType = this.dialect.sqlTypeToMalloyType(snowflakeDataType);
185
+ const name = row['COLUMN_NAME'] as string;
186
+ if (malloyType) {
187
+ s.fields.push({...malloyType, name});
188
+ } else {
189
+ s.fields.push({
190
+ type: 'unsupported',
191
+ rawType: snowflakeDataType,
192
+ name,
193
+ });
194
+ }
195
+ }
196
+ }
197
+
198
+ private async getTableSchema(
199
+ tableKey: string,
200
+ tablePath: string
201
+ ): Promise<StructDef> {
202
+ // looks like snowflake:schemaName.tableName
203
+ tableKey = tableKey.toLowerCase();
204
+
205
+ let [schema, tableName] = ['', tablePath];
206
+ const schema_and_table = tablePath.split('.');
207
+ if (schema_and_table.length === 2) {
208
+ [schema, tableName] = schema_and_table;
209
+ }
210
+
211
+ const structDef: StructDef = {
212
+ type: 'struct',
213
+ dialect: 'snowflake',
214
+ name: tableKey,
215
+ structSource: {type: 'table', tablePath},
216
+ structRelationship: {
217
+ type: 'basetable',
218
+ connectionName: this.name,
219
+ },
220
+ fields: [],
221
+ };
222
+ // This is how we get variant information
223
+
224
+ // WITH tbl as (
225
+ // SELECT * FROM malloytest.ga_sample
226
+ // )
227
+ // SELECT regexp_replace(PATH, '\\[.*\\]', '[]') as PATH, lower(TYPEOF(value)) as type
228
+ // FROM (select object_construct(*) o from tbl limit 100)
229
+ // ,table(flatten(input => o, recursive => true)) as meta
230
+ // WHERE lower(TYPEOF(value)) <> 'array'
231
+ // GROUP BY 1,2
232
+ // ORDER BY PATH
233
+
234
+ const infoQuery = `
235
+ SELECT
236
+ column_name, -- LOWER(COLUMN_NAME) AS column_name,
237
+ LOWER(DATA_TYPE) as data_type
238
+ FROM
239
+ INFORMATION_SCHEMA.COLUMNS
240
+ WHERE
241
+ table_schema = UPPER('${schema}')
242
+ AND table_name = UPPER('${tableName}');
243
+ `;
244
+
245
+ await this.schemaFromQuery(infoQuery, structDef);
246
+ return structDef;
247
+ }
248
+
249
+ public async fetchSchemaForTables(
250
+ missing: Record<string, string>,
251
+ {refreshTimestamp}: FetchSchemaOptions
252
+ ): Promise<{
253
+ schemas: Record<string, StructDef>;
254
+ errors: Record<string, string>;
255
+ }> {
256
+ const schemas: NamedStructDefs = {};
257
+ const errors: {[name: string]: string} = {};
258
+
259
+ for (const tableKey in missing) {
260
+ let inCache = this.schemaCache.get(tableKey);
261
+ if (
262
+ !inCache ||
263
+ (refreshTimestamp && refreshTimestamp > inCache.timestamp)
264
+ ) {
265
+ const tablePath = missing[tableKey];
266
+ const timestamp = refreshTimestamp || Date.now();
267
+ try {
268
+ inCache = {
269
+ schema: await this.getTableSchema(tableKey, tablePath),
270
+ timestamp,
271
+ };
272
+ this.schemaCache.set(tableKey, inCache);
273
+ } catch (error) {
274
+ inCache = {error: error.message, timestamp};
275
+ }
276
+ }
277
+ if (inCache.schema !== undefined) {
278
+ schemas[tableKey] = inCache.schema;
279
+ } else {
280
+ errors[tableKey] = inCache.error || 'Unknown schema fetch error';
281
+ }
282
+ }
283
+ return {schemas, errors};
284
+ }
285
+
286
+ private async getSQLBlockSchema(sqlRef: SQLBlock): Promise<StructDef> {
287
+ const structDef: StructDef = {
288
+ type: 'struct',
289
+ dialect: 'snowflake',
290
+ name: sqlRef.name,
291
+ structSource: {
292
+ type: 'sql',
293
+ method: 'subquery',
294
+ sqlBlock: sqlRef,
295
+ },
296
+ structRelationship: {
297
+ type: 'basetable',
298
+ connectionName: this.name,
299
+ },
300
+ fields: [],
301
+ };
302
+
303
+ // create temp table with same schema as the query
304
+ const tempTableName = this.getTempTableName(sqlRef.selectStr);
305
+ this.runSQL(
306
+ `
307
+ CREATE OR REPLACE TEMP TABLE ${tempTableName} as SELECT * FROM (
308
+ ${sqlRef.selectStr}
309
+ ) as x WHERE false;
310
+ `
311
+ );
312
+
313
+ const infoQuery = `
314
+ SELECT
315
+ column_name, -- LOWER(column_name) as column_name,
316
+ LOWER(data_type) as data_type
317
+ FROM
318
+ INFORMATION_SCHEMA.COLUMNS
319
+ WHERE
320
+ table_name = UPPER('${tempTableName}');
321
+ `;
322
+ await this.schemaFromQuery(infoQuery, structDef);
323
+ return structDef;
324
+ }
325
+
326
+ public async fetchSchemaForSQLBlock(
327
+ sqlRef: SQLBlock,
328
+ {refreshTimestamp}: FetchSchemaOptions
329
+ ): Promise<
330
+ | {structDef: StructDef; error?: undefined}
331
+ | {error: string; structDef?: undefined}
332
+ > {
333
+ const key = sqlRef.name;
334
+ let inCache = this.sqlSchemaCache.get(key);
335
+ if (
336
+ !inCache ||
337
+ (refreshTimestamp && refreshTimestamp > inCache.timestamp)
338
+ ) {
339
+ const timestamp = refreshTimestamp ?? Date.now();
340
+ try {
341
+ inCache = {
342
+ structDef: await this.getSQLBlockSchema(sqlRef),
343
+ timestamp,
344
+ };
345
+ } catch (error) {
346
+ inCache = {error: error.message, timestamp};
347
+ }
348
+ this.sqlSchemaCache.set(key, inCache);
349
+ }
350
+ return inCache;
351
+ }
352
+
353
+ public async manifestTemporaryTable(sqlCommand: string): Promise<string> {
354
+ const tableName = this.getTempTableName(sqlCommand);
355
+ const cmd = `CREATE OR REPLACE TEMP TABLE ${tableName} AS (${sqlCommand});`;
356
+ await this.runSQL(cmd);
357
+ return tableName;
358
+ }
359
+ }
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright 2023 Google LLC
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining
5
+ * a copy of this software and associated documentation files
6
+ * (the "Software"), to deal in the Software without restriction,
7
+ * including without limitation the rights to use, copy, modify, merge,
8
+ * publish, distribute, sublicense, and/or sell copies of the Software,
9
+ * and to permit persons to whom the Software is furnished to do so,
10
+ * subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be
13
+ * included in all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import {SnowflakeExecutor} from './snowflake_executor';
25
+ import {QueryData, RunSQLOptions} from '@malloydata/malloy';
26
+
27
+ const envDatabases =
28
+ process.env['MALLOY_DATABASES'] || process.env['MALLOY_DATABASE'];
29
+
30
+ let describe = globalThis.describe;
31
+ if (envDatabases && !envDatabases.includes('snowflake')) {
32
+ describe = describe.skip;
33
+ }
34
+
35
+ class SnowflakeExecutorTestSetup {
36
+ private executor_: SnowflakeExecutor;
37
+ constructor(private executor: SnowflakeExecutor) {
38
+ this.executor_ = executor;
39
+ }
40
+
41
+ async runBatch(sqlText: string): Promise<QueryData> {
42
+ let ret: QueryData = [];
43
+ await (async () => {
44
+ const rows = await this.executor_.batch(sqlText);
45
+ return rows;
46
+ })().then((rows: QueryData) => {
47
+ ret = rows;
48
+ });
49
+ return ret;
50
+ }
51
+
52
+ async runStreaming(sqlText: string, queryOptions?: RunSQLOptions) {
53
+ const rows: QueryData = [];
54
+ await (async () => {
55
+ for await (const row of await this.executor_.stream(
56
+ sqlText,
57
+ queryOptions
58
+ )) {
59
+ rows.push(row);
60
+ }
61
+ })();
62
+ return rows;
63
+ }
64
+
65
+ async done() {
66
+ await this.executor_.done();
67
+ }
68
+ }
69
+
70
+ describe('db:SnowflakeExecutor', () => {
71
+ let db: SnowflakeExecutorTestSetup;
72
+ let query: string;
73
+
74
+ beforeAll(() => {
75
+ const connOptions =
76
+ SnowflakeExecutor.getConnectionOptionsFromEnv() ||
77
+ SnowflakeExecutor.getConnectionOptionsFromToml();
78
+ const executor = new SnowflakeExecutor(connOptions);
79
+ db = new SnowflakeExecutorTestSetup(executor);
80
+ query = `
81
+ select
82
+ *
83
+ from
84
+ (
85
+ values
86
+ (1, 'one'),
87
+ (2, 'two'),
88
+ (3, 'three'),
89
+ (4, 'four'),
90
+ (5, 'five')
91
+ );
92
+ `;
93
+ });
94
+
95
+ afterAll(async () => {
96
+ await db.done();
97
+ });
98
+
99
+ it('verifies batch execute', async () => {
100
+ const rows = await db.runBatch(query);
101
+ expect(rows.length).toBe(5);
102
+ });
103
+
104
+ it('verifies stream iterable', async () => {
105
+ const rows = await db.runStreaming(query, {rowLimit: 2});
106
+ expect(rows.length).toBe(2);
107
+ });
108
+ });
@@ -0,0 +1,229 @@
1
+ /*
2
+ * Copyright 2023 Google LLC
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining
5
+ * a copy of this software and associated documentation files
6
+ * (the "Software"), to deal in the Software without restriction,
7
+ * including without limitation the rights to use, copy, modify, merge,
8
+ * publish, distribute, sublicense, and/or sell copies of the Software,
9
+ * and to permit persons to whom the Software is furnished to do so,
10
+ * subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be
13
+ * included in all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ */
23
+
24
+ import snowflake, {
25
+ SnowflakeError,
26
+ Statement,
27
+ Connection,
28
+ ConnectionOptions,
29
+ } from 'snowflake-sdk';
30
+ import {Pool, Options as PoolOptions} from 'generic-pool';
31
+ import * as toml from 'toml';
32
+ import * as fs from 'fs';
33
+ import * as path from 'path';
34
+ import {Readable} from 'stream';
35
+ import {
36
+ toAsyncGenerator,
37
+ QueryData,
38
+ QueryDataRow,
39
+ RunSQLOptions,
40
+ } from '@malloydata/malloy';
41
+
42
+ export interface ConnectionConfigFile {
43
+ // a toml file with snowflake connection settings
44
+ // if not provided, we will try to read ~/.snowflake/config
45
+ config_file_path?: string;
46
+ // the name of connection in the config file
47
+ // if not provided, we will try to use the "default" connection
48
+ connection_name?: string;
49
+ }
50
+
51
+ // function columnNameToLowerCase(row: QueryDataRow): QueryDataRow {
52
+ // const ret: QueryDataRow = {};
53
+ // for (const key in row) {
54
+ // ret[key.toLowerCase()] = row[key];
55
+ // }
56
+ // return ret;
57
+ // }
58
+
59
+ export class SnowflakeExecutor {
60
+ private static defaultPoolOptions_: PoolOptions = {
61
+ min: 1,
62
+ max: 1,
63
+ // ensure we validate a connection before giving it to a client
64
+ testOnBorrow: true,
65
+ testOnReturn: true,
66
+ };
67
+ private static defaultConnectionOptions = {
68
+ clientSessionKeepAlive: true, // default = false
69
+ clientSessionKeepAliveHeartbeatFrequency: 900, // default = 3600
70
+ };
71
+
72
+ private pool_: Pool<Connection>;
73
+ constructor(connOptions: ConnectionOptions, poolOptions?: PoolOptions) {
74
+ this.pool_ = snowflake.createPool(connOptions, {
75
+ ...SnowflakeExecutor.defaultPoolOptions_,
76
+ ...(poolOptions ?? {}),
77
+ });
78
+ }
79
+
80
+ public static getConnectionOptionsFromEnv(): ConnectionOptions | undefined {
81
+ const account = process.env['SNOWFLAKE_ACCOUNT'];
82
+ if (account) {
83
+ const username = process.env['SNOWFLAKE_USER'];
84
+ const password = process.env['SNOWFLAKE_PASSWORD'];
85
+ const warehouse = process.env['SNOWFLAKE_WAREHOUSE'];
86
+ const database = process.env['SNOWFLAKE_DATABASE'];
87
+ const schema = process.env['SNOWFLAKE_SCHEMA'];
88
+ return {
89
+ account,
90
+ username,
91
+ password,
92
+ warehouse,
93
+ database,
94
+ schema,
95
+ };
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ public static getConnectionOptionsFromToml(
101
+ options?: ConnectionConfigFile
102
+ ): ConnectionOptions {
103
+ let location: string | undefined = options?.config_file_path;
104
+ if (location === undefined) {
105
+ const homeDir = process.env['HOME'] || process.env['USERPROFILE'];
106
+ if (homeDir === undefined) {
107
+ throw new Error('could not find a path to connections.toml');
108
+ }
109
+ location = path.join(homeDir, '.snowflake', 'connections.toml');
110
+ }
111
+
112
+ if (!fs.existsSync(location)) {
113
+ throw new Error(
114
+ `provided snowflake connection config file: ${location} does not exist`
115
+ );
116
+ }
117
+
118
+ const tomlData = fs.readFileSync(location, 'utf-8');
119
+ const connections = toml.parse(tomlData);
120
+ const tomlConnectionName = options?.connection_name ?? 'default';
121
+ const connection = connections[tomlConnectionName];
122
+ if (connection === undefined) {
123
+ throw new Error(
124
+ `provided snowflake connection name: ${tomlConnectionName} does not exist at ${location}`
125
+ );
126
+ }
127
+
128
+ // sometimes the connection file uses "user" instead of "username"
129
+ // because the python api expects 'user'
130
+ connection['username'] = connection['username'] ?? connection['user'];
131
+ if (!connection || !connection.account || !connection.username) {
132
+ throw new Error(
133
+ `provided snowflake connection config file at ${location} is not valid`
134
+ );
135
+ }
136
+
137
+ return {
138
+ // some basic options we configure by default but can be overriden
139
+ ...SnowflakeExecutor.defaultConnectionOptions,
140
+ account: connection.account,
141
+ username: connection.username,
142
+ ...connection,
143
+ };
144
+ }
145
+
146
+ public async done() {
147
+ await this.pool_.drain().then(() => {
148
+ this.pool_.clear();
149
+ });
150
+ }
151
+
152
+ public async _execute(sqlText: string, conn: Connection): Promise<QueryData> {
153
+ return new Promise((resolve, reject) => {
154
+ const _statment = conn.execute({
155
+ sqlText,
156
+ complete: (
157
+ err: SnowflakeError | undefined,
158
+ _stmt: Statement,
159
+ rows?: QueryData
160
+ ) => {
161
+ if (err) {
162
+ reject(err);
163
+ } else if (rows) {
164
+ resolve(rows);
165
+ }
166
+ },
167
+ });
168
+ });
169
+ }
170
+
171
+ private async _setSessionParams(conn: Connection) {
172
+ // set some default session parameters
173
+ // this is quite imporant for snowflake because malloy tends to add quotes to all database identifiers
174
+ // and snowflake is case sensitive by with quotes but matches against all caps identifiers without quotes
175
+ // await this._execute(
176
+ // 'ALTER SESSION SET QUOTED_IDENTIFIERS_IGNORE_CASE = true;',
177
+ // conn
178
+ // );
179
+ // set utc as the default timezone which is the malloy convention
180
+ await this._execute("ALTER SESSION SET TIMEZONE = 'UTC';", conn);
181
+ // ensure week starts on Sunday which is the malloy convention
182
+ await this._execute('ALTER SESSION SET WEEK_START = 7;', conn);
183
+ }
184
+
185
+ public async batch(sqlText: string): Promise<QueryData> {
186
+ return await this.pool_.use(async (conn: Connection) => {
187
+ await this._setSessionParams(conn);
188
+ return await this._execute(sqlText, conn);
189
+ });
190
+ }
191
+
192
+ public async stream(
193
+ sqlText: string,
194
+ options?: RunSQLOptions
195
+ ): Promise<AsyncIterableIterator<QueryDataRow>> {
196
+ const pool: Pool<Connection> = this.pool_;
197
+ return await pool.acquire().then(async (conn: Connection) => {
198
+ await this._setSessionParams(conn);
199
+ const stmt: Statement = conn.execute({
200
+ sqlText,
201
+ streamResult: true,
202
+ });
203
+ const stream: Readable = stmt.streamRows();
204
+ function streamSnowflake(
205
+ onError: (error: Error) => void,
206
+ onData: (data: QueryDataRow) => void,
207
+ onEnd: () => void
208
+ ) {
209
+ function handleEnd() {
210
+ onEnd();
211
+ pool.release(conn);
212
+ }
213
+
214
+ let index = 0;
215
+ function handleData(this: Readable, row: QueryDataRow) {
216
+ onData(row);
217
+ index += 1;
218
+ if (options?.rowLimit !== undefined && index >= options.rowLimit) {
219
+ onEnd();
220
+ }
221
+ }
222
+ stream.on('error', onError);
223
+ stream.on('data', handleData);
224
+ stream.on('end', handleEnd);
225
+ }
226
+ return Promise.resolve(toAsyncGenerator<QueryDataRow>(streamSnowflake));
227
+ });
228
+ }
229
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.packages.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "composite": true
7
+ },
8
+ "references": [
9
+ {
10
+ "path": "../malloy"
11
+ }
12
+ ]
13
+ }