@malloydata/db-snowflake 0.0.375 → 0.0.377
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/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/dist/snowflake_connection.d.ts +48 -13
- package/dist/snowflake_connection.js +144 -228
- package/dist/snowflake_connection.js.map +1 -1
- package/dist/snowflake_connection.spec.js +179 -14
- package/dist/snowflake_connection.spec.js.map +1 -1
- package/dist/snowflake_sample_strategy.spec.js +97 -0
- package/dist/snowflake_sample_strategy.spec.js.map +1 -0
- package/dist/snowflake_table_name.d.ts +19 -0
- package/dist/snowflake_table_name.js +80 -0
- package/dist/snowflake_table_name.js.map +1 -0
- package/dist/snowflake_variant_schema.d.ts +43 -0
- package/dist/snowflake_variant_schema.js +203 -0
- package/dist/snowflake_variant_schema.js.map +1 -0
- package/dist/snowflake_variant_schema.spec.js +150 -0
- package/dist/snowflake_variant_schema.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +34 -1
- package/src/snowflake_connection.spec.ts +219 -15
- package/src/snowflake_connection.ts +218 -262
- package/src/snowflake_sample_strategy.spec.ts +130 -0
- package/src/snowflake_table_name.ts +94 -0
- package/src/snowflake_variant_schema.spec.ts +188 -0
- package/src/snowflake_variant_schema.ts +301 -0
- package/dist/snowflake_executor.spec.js +0 -89
- package/dist/snowflake_executor.spec.js.map +0 -1
- package/dist/snowflake_setup.spec.js +0 -76
- package/dist/snowflake_setup.spec.js.map +0 -1
- package/src/snowflake_executor.spec.ts +0 -103
- package/src/snowflake_setup.spec.ts +0 -56
- /package/dist/{snowflake_executor.spec.d.ts → snowflake_sample_strategy.spec.d.ts} +0 -0
- /package/dist/{snowflake_setup.spec.d.ts → snowflake_variant_schema.spec.d.ts} +0 -0
|
@@ -22,11 +22,19 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import * as malloy from '@malloydata/malloy';
|
|
25
|
-
import {
|
|
25
|
+
import type {QueryData, RunSQLOptions} from '@malloydata/malloy';
|
|
26
|
+
import {
|
|
27
|
+
createTestRuntime,
|
|
28
|
+
describeIfDatabaseAvailable,
|
|
29
|
+
mkTestModel,
|
|
30
|
+
} from '@malloydata/malloy/test';
|
|
26
31
|
import '@malloydata/malloy/test/matchers';
|
|
32
|
+
import crypto from 'crypto';
|
|
27
33
|
import {SnowflakeConnection} from './snowflake_connection';
|
|
28
34
|
import {SnowflakeExecutor} from './snowflake_executor';
|
|
29
35
|
|
|
36
|
+
const [describeSnowflakeExecutor] = describeIfDatabaseAvailable(['snowflake']);
|
|
37
|
+
|
|
30
38
|
describe('db:Snowflake', () => {
|
|
31
39
|
const connOptions =
|
|
32
40
|
SnowflakeExecutor.getConnectionOptionsFromEnv() ||
|
|
@@ -129,7 +137,7 @@ describe('db:Snowflake', () => {
|
|
|
129
137
|
it('discovers variant schema through a view', async () => {
|
|
130
138
|
// Create a view with a variant column, then fetch its schema.
|
|
131
139
|
// This exercises the TABLESAMPLE fallback path — TABLESAMPLE fails
|
|
132
|
-
// on views, so the code should fall back to LIMIT
|
|
140
|
+
// on views, so the code should fall back to a plain LIMIT sample.
|
|
133
141
|
const salt = Math.random().toString(36).slice(2, 10);
|
|
134
142
|
const viewName = `malloytest.test_variant_view_${salt}`;
|
|
135
143
|
await conn.runSQL(
|
|
@@ -147,6 +155,38 @@ describe('db:Snowflake', () => {
|
|
|
147
155
|
}
|
|
148
156
|
});
|
|
149
157
|
|
|
158
|
+
it('preserves top-level array shape when sample rows have no descendants', async () => {
|
|
159
|
+
// ARRAY comes from DESCRIBE TABLE, so even if recursive flatten sees no
|
|
160
|
+
// element paths we should still return an array<variant> field.
|
|
161
|
+
const salt = Math.random().toString(36).slice(2, 10);
|
|
162
|
+
const viewName = `malloytest.test_array_seed_${salt}`;
|
|
163
|
+
await conn.runSQL(
|
|
164
|
+
`CREATE OR REPLACE VIEW ${viewName} AS
|
|
165
|
+
SELECT ARRAY_CONSTRUCT() AS data`
|
|
166
|
+
);
|
|
167
|
+
try {
|
|
168
|
+
const schema = await conn.fetchTableSchema(viewName, viewName);
|
|
169
|
+
const dataField = schema.fields.find(f => f.name === 'DATA');
|
|
170
|
+
expect(dataField).toEqual({
|
|
171
|
+
type: 'array',
|
|
172
|
+
name: 'DATA',
|
|
173
|
+
join: 'many',
|
|
174
|
+
elementTypeDef: {type: 'sql native', rawType: 'variant'},
|
|
175
|
+
fields: [
|
|
176
|
+
{name: 'value', type: 'sql native', rawType: 'variant'},
|
|
177
|
+
{
|
|
178
|
+
name: 'each',
|
|
179
|
+
type: 'sql native',
|
|
180
|
+
rawType: 'variant',
|
|
181
|
+
e: {node: 'field', path: ['value']},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
} finally {
|
|
186
|
+
await conn.runSQL(`DROP VIEW IF EXISTS ${viewName}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
150
190
|
it('maps integer types to bigint', async () => {
|
|
151
191
|
const x: malloy.SQLSourceDef = {
|
|
152
192
|
type: 'sql_select',
|
|
@@ -170,16 +210,18 @@ describe('db:Snowflake', () => {
|
|
|
170
210
|
]);
|
|
171
211
|
});
|
|
172
212
|
|
|
173
|
-
it('degrades
|
|
174
|
-
// data.foo is
|
|
175
|
-
//
|
|
213
|
+
it('degrades scalar-vs-object field to sql native without losing siblings', async () => {
|
|
214
|
+
// data.foo is an object in one row and a scalar in another. Honest
|
|
215
|
+
// policy: foo becomes sql native variant (caller must cast with
|
|
216
|
+
// `foo :: {bar :: number}` to query bar). The enclosing record is
|
|
217
|
+
// unaffected — sibling fields keep their inferred types.
|
|
176
218
|
const salt = Math.random().toString(36).slice(2, 10);
|
|
177
219
|
const viewName = `malloytest.test_variant_conflict_${salt}`;
|
|
178
220
|
await conn.runSQL(
|
|
179
221
|
`CREATE OR REPLACE VIEW ${viewName} AS
|
|
180
|
-
SELECT parse_json('{"foo": {"bar": 1}}') AS data
|
|
222
|
+
SELECT parse_json('{"foo": {"bar": 1}, "sib": "hello"}') AS data
|
|
181
223
|
UNION ALL
|
|
182
|
-
SELECT parse_json('{"foo": "oops"}') AS data`
|
|
224
|
+
SELECT parse_json('{"foo": "oops", "sib": "world"}') AS data`
|
|
183
225
|
);
|
|
184
226
|
try {
|
|
185
227
|
const schema = await conn.fetchTableSchema(viewName, viewName);
|
|
@@ -193,22 +235,24 @@ describe('db:Snowflake', () => {
|
|
|
193
235
|
rawType: 'variant',
|
|
194
236
|
name: 'foo',
|
|
195
237
|
});
|
|
238
|
+
const sibField = dataField!.fields.find(f => f.name === 'sib');
|
|
239
|
+
expect(sibField).toEqual({type: 'string', name: 'sib'});
|
|
196
240
|
}
|
|
197
241
|
} finally {
|
|
198
242
|
await conn.runSQL(`DROP VIEW IF EXISTS ${viewName}`);
|
|
199
243
|
}
|
|
200
244
|
});
|
|
201
245
|
|
|
202
|
-
it('degrades
|
|
203
|
-
// Array analogue
|
|
204
|
-
//
|
|
246
|
+
it('degrades scalar-vs-object inside an array element without losing the array', async () => {
|
|
247
|
+
// Array analogue: items[*].foo is an object in one row and a scalar
|
|
248
|
+
// in another. foo degrades to variant; items stays array<record>.
|
|
205
249
|
const salt = Math.random().toString(36).slice(2, 10);
|
|
206
250
|
const viewName = `malloytest.test_variant_array_obj_conflict_${salt}`;
|
|
207
251
|
await conn.runSQL(
|
|
208
252
|
`CREATE OR REPLACE VIEW ${viewName} AS
|
|
209
|
-
SELECT parse_json('{"items": [{"foo": {"bar": 1}}]}') AS data
|
|
253
|
+
SELECT parse_json('{"items": [{"foo": {"bar": 1}, "sib": "a"}]}') AS data
|
|
210
254
|
UNION ALL
|
|
211
|
-
SELECT parse_json('{"items": [{"foo": "oops"}]}') AS data`
|
|
255
|
+
SELECT parse_json('{"items": [{"foo": "oops", "sib": "b"}]}') AS data`
|
|
212
256
|
);
|
|
213
257
|
try {
|
|
214
258
|
const schema = await conn.fetchTableSchema(viewName, viewName);
|
|
@@ -229,6 +273,8 @@ describe('db:Snowflake', () => {
|
|
|
229
273
|
rawType: 'variant',
|
|
230
274
|
name: 'foo',
|
|
231
275
|
});
|
|
276
|
+
const sibField = itemsField!.fields.find(f => f.name === 'sib');
|
|
277
|
+
expect(sibField).toEqual({type: 'string', name: 'sib'});
|
|
232
278
|
}
|
|
233
279
|
}
|
|
234
280
|
} finally {
|
|
@@ -236,6 +282,41 @@ describe('db:Snowflake', () => {
|
|
|
236
282
|
}
|
|
237
283
|
});
|
|
238
284
|
|
|
285
|
+
it('full-scans a small base table with variant columns under the byte threshold', async () => {
|
|
286
|
+
// Base table (not view) small enough that BYTES lands under the
|
|
287
|
+
// default 100 MB schemaSampleFullScanMaxBytes. The probe sees the
|
|
288
|
+
// size and the code takes the full-scan branch — no TABLESAMPLE,
|
|
289
|
+
// no LIMIT. Every row contributes to the (path, type) histogram,
|
|
290
|
+
// so rare fields are caught.
|
|
291
|
+
const salt = Math.random().toString(36).slice(2, 10);
|
|
292
|
+
const tableName = `malloytest.test_variant_fullscan_${salt}`;
|
|
293
|
+
await conn.runSQL(
|
|
294
|
+
`CREATE OR REPLACE TABLE ${tableName} AS
|
|
295
|
+
SELECT parse_json('{"foo": 1, "bar": "hi"}') AS data
|
|
296
|
+
UNION ALL
|
|
297
|
+
SELECT parse_json('{"foo": 2, "bar": "bye"}') AS data`
|
|
298
|
+
);
|
|
299
|
+
try {
|
|
300
|
+
const schema = await conn.fetchTableSchema(tableName, tableName);
|
|
301
|
+
const dataField = schema.fields.find(f => f.name === 'DATA');
|
|
302
|
+
expect(dataField).toBeDefined();
|
|
303
|
+
expect(dataField!.type).toBe('record');
|
|
304
|
+
if (dataField!.type === 'record') {
|
|
305
|
+
expect(dataField!.fields.find(f => f.name === 'foo')).toEqual({
|
|
306
|
+
name: 'foo',
|
|
307
|
+
type: 'number',
|
|
308
|
+
numberType: 'bigint',
|
|
309
|
+
});
|
|
310
|
+
expect(dataField!.fields.find(f => f.name === 'bar')).toEqual({
|
|
311
|
+
name: 'bar',
|
|
312
|
+
type: 'string',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
await conn.runSQL(`DROP TABLE IF EXISTS ${tableName}`);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
239
320
|
it('degrades when same path is object in one row and array in another', async () => {
|
|
240
321
|
// foo is an object in one row and an array in another.
|
|
241
322
|
// foo should degrade to sql native.
|
|
@@ -265,9 +346,10 @@ describe('db:Snowflake', () => {
|
|
|
265
346
|
}
|
|
266
347
|
});
|
|
267
348
|
|
|
268
|
-
it('preserves sibling fields when one field degrades', async () => {
|
|
269
|
-
// foo
|
|
270
|
-
//
|
|
349
|
+
it('preserves sibling fields when one field degrades to variant', async () => {
|
|
350
|
+
// foo is scalar in one row and object in another, while stable is
|
|
351
|
+
// always consistent. The degradation should stay local to foo and
|
|
352
|
+
// keep stable untouched.
|
|
271
353
|
const salt = Math.random().toString(36).slice(2, 10);
|
|
272
354
|
const viewName = `malloytest.test_variant_sibling_${salt}`;
|
|
273
355
|
await conn.runSQL(
|
|
@@ -348,3 +430,125 @@ describe('numeric value reading', () => {
|
|
|
348
430
|
);
|
|
349
431
|
});
|
|
350
432
|
});
|
|
433
|
+
|
|
434
|
+
class SnowflakeExecutorTestSetup {
|
|
435
|
+
private executor_: SnowflakeExecutor;
|
|
436
|
+
constructor(executor: SnowflakeExecutor) {
|
|
437
|
+
this.executor_ = executor;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async runBatch(sqlText: string): Promise<QueryData> {
|
|
441
|
+
let ret: QueryData = [];
|
|
442
|
+
await (async () => {
|
|
443
|
+
const rows = await this.executor_.batch(sqlText);
|
|
444
|
+
return rows;
|
|
445
|
+
})().then((rows: QueryData) => {
|
|
446
|
+
ret = rows;
|
|
447
|
+
});
|
|
448
|
+
return ret;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async runStreaming(sqlText: string, queryOptions?: RunSQLOptions) {
|
|
452
|
+
const rows: QueryData = [];
|
|
453
|
+
await (async () => {
|
|
454
|
+
for await (const row of await this.executor_.stream(
|
|
455
|
+
sqlText,
|
|
456
|
+
queryOptions
|
|
457
|
+
)) {
|
|
458
|
+
rows.push(row);
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
return rows;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async done() {
|
|
465
|
+
await this.executor_.done();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
describeSnowflakeExecutor('db:SnowflakeExecutor', () => {
|
|
470
|
+
let db: SnowflakeExecutorTestSetup;
|
|
471
|
+
let query: string;
|
|
472
|
+
|
|
473
|
+
beforeAll(() => {
|
|
474
|
+
const connOptions =
|
|
475
|
+
SnowflakeExecutor.getConnectionOptionsFromEnv() ||
|
|
476
|
+
SnowflakeExecutor.getConnectionOptionsFromToml();
|
|
477
|
+
const executor = new SnowflakeExecutor(connOptions);
|
|
478
|
+
db = new SnowflakeExecutorTestSetup(executor);
|
|
479
|
+
query = `
|
|
480
|
+
select
|
|
481
|
+
*
|
|
482
|
+
from
|
|
483
|
+
(
|
|
484
|
+
values
|
|
485
|
+
(1, 'one'),
|
|
486
|
+
(2, 'two'),
|
|
487
|
+
(3, 'three'),
|
|
488
|
+
(4, 'four'),
|
|
489
|
+
(5, 'five')
|
|
490
|
+
);
|
|
491
|
+
`;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
afterAll(async () => {
|
|
495
|
+
await db.done();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('verifies batch execute', async () => {
|
|
499
|
+
const rows = await db.runBatch(query);
|
|
500
|
+
expect(rows.length).toBe(5);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('verifies stream iterable', async () => {
|
|
504
|
+
const rows = await db.runStreaming(query, {rowLimit: 2});
|
|
505
|
+
expect(rows.length).toBe(2);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('setupSQL', () => {
|
|
510
|
+
const connOptions =
|
|
511
|
+
SnowflakeExecutor.getConnectionOptionsFromEnv() ||
|
|
512
|
+
SnowflakeExecutor.getConnectionOptionsFromToml();
|
|
513
|
+
const uid = crypto.randomBytes(4).toString('hex');
|
|
514
|
+
const connections: SnowflakeConnection[] = [];
|
|
515
|
+
|
|
516
|
+
function makeConn(name: string, setupSQL: string): SnowflakeConnection {
|
|
517
|
+
const conn = new SnowflakeConnection(name, {connOptions, setupSQL});
|
|
518
|
+
connections.push(conn);
|
|
519
|
+
return conn;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
afterAll(async () => {
|
|
523
|
+
await Promise.all(connections.map(c => c.close()));
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('runs a single setup statement', async () => {
|
|
527
|
+
const conn = makeConn(
|
|
528
|
+
'snowflake_setup_single',
|
|
529
|
+
`SET setup_test_${uid} = 42`
|
|
530
|
+
);
|
|
531
|
+
const result = await conn.runSQL(`SELECT $setup_test_${uid} AS V`);
|
|
532
|
+
expect(malloy.API.rowDataToNumber(result.rows[0]['V'])).toBe(42);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('runs multiple semicolon-newline-separated statements', async () => {
|
|
536
|
+
const conn = makeConn(
|
|
537
|
+
'snowflake_setup_multi',
|
|
538
|
+
[`SET setup_a_${uid} = 10`, `SET setup_b_${uid} = 20`].join(';\n')
|
|
539
|
+
);
|
|
540
|
+
const result = await conn.runSQL(
|
|
541
|
+
`SELECT $setup_a_${uid} + $setup_b_${uid} AS V`
|
|
542
|
+
);
|
|
543
|
+
expect(malloy.API.rowDataToNumber(result.rows[0]['V'])).toBe(30);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('handles multi-line statements', async () => {
|
|
547
|
+
const conn = makeConn(
|
|
548
|
+
'snowflake_setup_multiline',
|
|
549
|
+
`SET\n setup_ml_${uid} = 99`
|
|
550
|
+
);
|
|
551
|
+
const result = await conn.runSQL(`SELECT $setup_ml_${uid} AS V`);
|
|
552
|
+
expect(malloy.API.rowDataToNumber(result.rows[0]['V'])).toBe(99);
|
|
553
|
+
});
|
|
554
|
+
});
|