@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.
Files changed (33) hide show
  1. package/dist/index.js +29 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/snowflake_connection.d.ts +48 -13
  4. package/dist/snowflake_connection.js +144 -228
  5. package/dist/snowflake_connection.js.map +1 -1
  6. package/dist/snowflake_connection.spec.js +179 -14
  7. package/dist/snowflake_connection.spec.js.map +1 -1
  8. package/dist/snowflake_sample_strategy.spec.js +97 -0
  9. package/dist/snowflake_sample_strategy.spec.js.map +1 -0
  10. package/dist/snowflake_table_name.d.ts +19 -0
  11. package/dist/snowflake_table_name.js +80 -0
  12. package/dist/snowflake_table_name.js.map +1 -0
  13. package/dist/snowflake_variant_schema.d.ts +43 -0
  14. package/dist/snowflake_variant_schema.js +203 -0
  15. package/dist/snowflake_variant_schema.js.map +1 -0
  16. package/dist/snowflake_variant_schema.spec.js +150 -0
  17. package/dist/snowflake_variant_schema.spec.js.map +1 -0
  18. package/package.json +2 -2
  19. package/src/index.ts +34 -1
  20. package/src/snowflake_connection.spec.ts +219 -15
  21. package/src/snowflake_connection.ts +218 -262
  22. package/src/snowflake_sample_strategy.spec.ts +130 -0
  23. package/src/snowflake_table_name.ts +94 -0
  24. package/src/snowflake_variant_schema.spec.ts +188 -0
  25. package/src/snowflake_variant_schema.ts +301 -0
  26. package/dist/snowflake_executor.spec.js +0 -89
  27. package/dist/snowflake_executor.spec.js.map +0 -1
  28. package/dist/snowflake_setup.spec.js +0 -76
  29. package/dist/snowflake_setup.spec.js.map +0 -1
  30. package/src/snowflake_executor.spec.ts +0 -103
  31. package/src/snowflake_setup.spec.ts +0 -56
  32. /package/dist/{snowflake_executor.spec.d.ts → snowflake_sample_strategy.spec.d.ts} +0 -0
  33. /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 {createTestRuntime, mkTestModel} from '@malloydata/malloy/test';
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 100.
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 variant field to sql native when types conflict across rows', async () => {
174
- // data.foo is a scalar in one row and an object in another.
175
- // Schema discovery should not throw foo should degrade to sql native.
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 nested object inside array when types conflict', async () => {
203
- // Array analogue of the customer bug: items[*].foo is an object in
204
- // one row and a scalar in another. foo should degrade to sql native.
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 has conflicting types but stable is consistent.
270
- // stable should come through normally.
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
+ });