@malloydata/db-snowflake 0.0.374 → 0.0.376

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,150 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const malloy_1 = require("@malloydata/malloy");
8
+ const snowflake_variant_schema_1 = require("./snowflake_variant_schema");
9
+ describe('snowflake variant schema helper', () => {
10
+ const dialect = new malloy_1.SnowflakeDialect();
11
+ function inferField(nestedColumn, rows) {
12
+ const state = (0, snowflake_variant_schema_1.createVariantSchemaState)();
13
+ (0, snowflake_variant_schema_1.seedTopLevelShape)(state, nestedColumn);
14
+ for (const row of rows) {
15
+ (0, snowflake_variant_schema_1.accumulateVariantPath)(state, new snowflake_variant_schema_1.PathParser(row.path).segments(), row.type);
16
+ }
17
+ return (0, snowflake_variant_schema_1.buildTopLevelField)(nestedColumn, state, dialect);
18
+ }
19
+ test('reconstructs object shape from descendant-only evidence', () => {
20
+ expect(inferField({ kind: 'variant', name: 'BASE_TOUCHPOINT' }, [
21
+ { path: 'BASE_TOUCHPOINT.NETWORK', type: 'varchar' },
22
+ { path: 'BASE_TOUCHPOINT.PLATFORM', type: 'varchar' },
23
+ ])).toEqual({
24
+ type: 'record',
25
+ name: 'BASE_TOUCHPOINT',
26
+ join: 'one',
27
+ fields: [
28
+ { name: 'NETWORK', type: 'string' },
29
+ { name: 'PLATFORM', type: 'string' },
30
+ ],
31
+ });
32
+ });
33
+ test('degrades object-array conflict at a shared prefix', () => {
34
+ expect(inferField({ kind: 'variant', name: 'X' }, [
35
+ { path: 'X.Y', type: 'varchar' },
36
+ { path: 'X[*].Z', type: 'decimal' },
37
+ ])).toEqual({
38
+ type: 'sql native',
39
+ rawType: 'variant',
40
+ name: 'X',
41
+ });
42
+ });
43
+ test('builds array of records from stable descendants', () => {
44
+ expect(inferField({ kind: 'variant', name: 'ITEMS' }, [
45
+ { path: 'ITEMS[*].FOO', type: 'varchar' },
46
+ { path: 'ITEMS[*].BAR', type: 'boolean' },
47
+ ])).toEqual({
48
+ type: 'array',
49
+ name: 'ITEMS',
50
+ join: 'many',
51
+ elementTypeDef: { type: 'record_element' },
52
+ fields: [
53
+ { name: 'FOO', type: 'string' },
54
+ { name: 'BAR', type: 'boolean' },
55
+ ],
56
+ });
57
+ });
58
+ test('top-level array with no descendants becomes array of variant', () => {
59
+ expect(inferField({ kind: 'array', name: 'DIMENSION_SET_IDS' }, [])).toEqual({
60
+ type: 'array',
61
+ name: 'DIMENSION_SET_IDS',
62
+ join: 'many',
63
+ elementTypeDef: { type: 'sql native', rawType: 'variant' },
64
+ fields: [
65
+ { name: 'value', type: 'sql native', rawType: 'variant' },
66
+ {
67
+ name: 'each',
68
+ type: 'sql native',
69
+ rawType: 'variant',
70
+ e: { node: 'field', path: ['value'] },
71
+ },
72
+ ],
73
+ });
74
+ });
75
+ test('top-level DESCRIBE seed stays authoritative over conflicting sample', () => {
76
+ expect(inferField({ kind: 'array', name: 'DIMENSION_SET_IDS' }, [
77
+ { path: 'DIMENSION_SET_IDS.foo', type: 'varchar' },
78
+ ])).toEqual({
79
+ type: 'array',
80
+ name: 'DIMENSION_SET_IDS',
81
+ join: 'many',
82
+ elementTypeDef: { type: 'sql native', rawType: 'variant' },
83
+ fields: [
84
+ { name: 'value', type: 'sql native', rawType: 'variant' },
85
+ {
86
+ name: 'each',
87
+ type: 'sql native',
88
+ rawType: 'variant',
89
+ e: { node: 'field', path: ['value'] },
90
+ },
91
+ ],
92
+ });
93
+ });
94
+ test('top-level object with no descendants becomes opaque variant', () => {
95
+ expect(inferField({ kind: 'object', name: 'PAYLOAD' }, [])).toEqual({
96
+ type: 'sql native',
97
+ rawType: 'variant',
98
+ name: 'PAYLOAD',
99
+ });
100
+ });
101
+ test('quoted path names with punctuation are preserved', () => {
102
+ expect(inferField({ kind: 'variant', name: 'DATA' }, [
103
+ { path: "DATA['a.b'][*]['c[d]']", type: 'varchar' },
104
+ ])).toEqual({
105
+ type: 'record',
106
+ name: 'DATA',
107
+ join: 'one',
108
+ fields: [
109
+ {
110
+ type: 'array',
111
+ name: 'a.b',
112
+ join: 'many',
113
+ elementTypeDef: { type: 'record_element' },
114
+ fields: [{ name: 'c[d]', type: 'string' }],
115
+ },
116
+ ],
117
+ });
118
+ });
119
+ test('leaf-type conflicts degrade to variant', () => {
120
+ const first = { kind: 'leaf', type: 'varchar' };
121
+ const second = { kind: 'leaf', type: 'decimal' };
122
+ expect((0, snowflake_variant_schema_1.mergeShape)(first, second)).toEqual({ kind: 'variant' });
123
+ });
124
+ test('scalar-vs-object at same path degrades that field only', () => {
125
+ // The sample emits both the scalar observation and the object
126
+ // observation (same path, two rows from the distinct (path, type)
127
+ // query). mergeShape collapses DATA.foo to variant; the parent
128
+ // DATA stays a record and siblings keep their types.
129
+ expect(inferField({ kind: 'variant', name: 'DATA' }, [
130
+ { path: 'DATA.foo', type: 'object' },
131
+ { path: 'DATA.foo', type: 'varchar' },
132
+ { path: 'DATA.foo.bar', type: 'decimal' },
133
+ { path: 'DATA.sib', type: 'varchar' },
134
+ ])).toEqual({
135
+ type: 'record',
136
+ name: 'DATA',
137
+ join: 'one',
138
+ fields: [
139
+ { type: 'sql native', rawType: 'variant', name: 'foo' },
140
+ { type: 'string', name: 'sib' },
141
+ ],
142
+ });
143
+ });
144
+ test('variant shape is monotonic', () => {
145
+ expect((0, snowflake_variant_schema_1.mergeShape)({ kind: 'variant' }, { kind: 'object' })).toEqual({
146
+ kind: 'variant',
147
+ });
148
+ });
149
+ });
150
+ //# sourceMappingURL=snowflake_variant_schema.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snowflake_variant_schema.spec.js","sourceRoot":"","sources":["../src/snowflake_variant_schema.spec.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/db-snowflake",
3
- "version": "0.0.374",
3
+ "version": "0.0.376",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@malloydata/malloy": "0.0.374",
25
+ "@malloydata/malloy": "0.0.376",
26
26
  "generic-pool": "^3.9.0",
27
27
  "snowflake-sdk": "2.3.1",
28
28
  "toml": "^3.0.0"
package/src/index.ts CHANGED
@@ -37,6 +37,8 @@ registerConnectionType('snowflake', {
37
37
  setupSQL,
38
38
  timeoutMs,
39
39
  schemaSampleTimeoutMs,
40
+ schemaSampleRowLimit,
41
+ schemaSampleFullScanMaxBytes,
40
42
  ...props
41
43
  } = config;
42
44
  // ConnectionConfig values are trusted to match ConnectionOptions fields
@@ -60,6 +62,18 @@ registerConnectionType('snowflake', {
60
62
  : typeof schemaSampleTimeoutMs === 'string'
61
63
  ? parseInt(schemaSampleTimeoutMs, 10)
62
64
  : undefined,
65
+ schemaSampleRowLimit:
66
+ typeof schemaSampleRowLimit === 'number'
67
+ ? schemaSampleRowLimit
68
+ : typeof schemaSampleRowLimit === 'string'
69
+ ? parseInt(schemaSampleRowLimit, 10)
70
+ : undefined,
71
+ schemaSampleFullScanMaxBytes:
72
+ typeof schemaSampleFullScanMaxBytes === 'number'
73
+ ? schemaSampleFullScanMaxBytes
74
+ : typeof schemaSampleFullScanMaxBytes === 'string'
75
+ ? parseInt(schemaSampleFullScanMaxBytes, 10)
76
+ : undefined,
63
77
  });
64
78
  },
65
79
  properties: [
@@ -101,14 +115,33 @@ registerConnectionType('snowflake', {
101
115
  displayName: 'Timeout (ms)',
102
116
  type: 'number',
103
117
  optional: true,
118
+ default: 600000,
104
119
  },
105
120
  {
106
121
  name: 'schemaSampleTimeoutMs',
107
122
  displayName: 'Schema Sample Timeout (ms)',
108
123
  type: 'number',
109
124
  optional: true,
125
+ default: 15000,
126
+ description:
127
+ 'Timeout for the query that samples variant columns to detect their schema.',
128
+ },
129
+ {
130
+ name: 'schemaSampleRowLimit',
131
+ displayName: 'Schema Sample Row Limit',
132
+ type: 'number',
133
+ optional: true,
134
+ default: 1000,
135
+ description:
136
+ 'Row limit for the variant schema sample. Ignored for tables small enough to full-scan.',
137
+ },
138
+ {
139
+ name: 'schemaSampleFullScanMaxBytes',
140
+ displayName: 'Schema Full-Scan Max Bytes',
141
+ type: 'number',
142
+ optional: true,
110
143
  description:
111
- 'Timeout for the query that samples variant columns to detect their schema (default 15000)',
144
+ 'Tables with BYTES at or below this value are full-scanned during variant schema inference instead of sampled. When unset, the connection uses an internal threshold; picking a value here is a policy choice tied to the size-probe behavior.',
112
145
  },
113
146
  {
114
147
  name: 'setupSQL',
@@ -129,7 +129,7 @@ describe('db:Snowflake', () => {
129
129
  it('discovers variant schema through a view', async () => {
130
130
  // Create a view with a variant column, then fetch its schema.
131
131
  // This exercises the TABLESAMPLE fallback path — TABLESAMPLE fails
132
- // on views, so the code should fall back to LIMIT 100.
132
+ // on views, so the code should fall back to a plain LIMIT sample.
133
133
  const salt = Math.random().toString(36).slice(2, 10);
134
134
  const viewName = `malloytest.test_variant_view_${salt}`;
135
135
  await conn.runSQL(
@@ -147,6 +147,38 @@ describe('db:Snowflake', () => {
147
147
  }
148
148
  });
149
149
 
150
+ it('preserves top-level array shape when sample rows have no descendants', async () => {
151
+ // ARRAY comes from DESCRIBE TABLE, so even if recursive flatten sees no
152
+ // element paths we should still return an array<variant> field.
153
+ const salt = Math.random().toString(36).slice(2, 10);
154
+ const viewName = `malloytest.test_array_seed_${salt}`;
155
+ await conn.runSQL(
156
+ `CREATE OR REPLACE VIEW ${viewName} AS
157
+ SELECT ARRAY_CONSTRUCT() AS data`
158
+ );
159
+ try {
160
+ const schema = await conn.fetchTableSchema(viewName, viewName);
161
+ const dataField = schema.fields.find(f => f.name === 'DATA');
162
+ expect(dataField).toEqual({
163
+ type: 'array',
164
+ name: 'DATA',
165
+ join: 'many',
166
+ elementTypeDef: {type: 'sql native', rawType: 'variant'},
167
+ fields: [
168
+ {name: 'value', type: 'sql native', rawType: 'variant'},
169
+ {
170
+ name: 'each',
171
+ type: 'sql native',
172
+ rawType: 'variant',
173
+ e: {node: 'field', path: ['value']},
174
+ },
175
+ ],
176
+ });
177
+ } finally {
178
+ await conn.runSQL(`DROP VIEW IF EXISTS ${viewName}`);
179
+ }
180
+ });
181
+
150
182
  it('maps integer types to bigint', async () => {
151
183
  const x: malloy.SQLSourceDef = {
152
184
  type: 'sql_select',
@@ -170,16 +202,18 @@ describe('db:Snowflake', () => {
170
202
  ]);
171
203
  });
172
204
 
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.
205
+ it('degrades scalar-vs-object field to sql native without losing siblings', async () => {
206
+ // data.foo is an object in one row and a scalar in another. Honest
207
+ // policy: foo becomes sql native variant (caller must cast with
208
+ // `foo :: {bar :: number}` to query bar). The enclosing record is
209
+ // unaffected — sibling fields keep their inferred types.
176
210
  const salt = Math.random().toString(36).slice(2, 10);
177
211
  const viewName = `malloytest.test_variant_conflict_${salt}`;
178
212
  await conn.runSQL(
179
213
  `CREATE OR REPLACE VIEW ${viewName} AS
180
- SELECT parse_json('{"foo": {"bar": 1}}') AS data
214
+ SELECT parse_json('{"foo": {"bar": 1}, "sib": "hello"}') AS data
181
215
  UNION ALL
182
- SELECT parse_json('{"foo": "oops"}') AS data`
216
+ SELECT parse_json('{"foo": "oops", "sib": "world"}') AS data`
183
217
  );
184
218
  try {
185
219
  const schema = await conn.fetchTableSchema(viewName, viewName);
@@ -193,22 +227,24 @@ describe('db:Snowflake', () => {
193
227
  rawType: 'variant',
194
228
  name: 'foo',
195
229
  });
230
+ const sibField = dataField!.fields.find(f => f.name === 'sib');
231
+ expect(sibField).toEqual({type: 'string', name: 'sib'});
196
232
  }
197
233
  } finally {
198
234
  await conn.runSQL(`DROP VIEW IF EXISTS ${viewName}`);
199
235
  }
200
236
  });
201
237
 
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.
238
+ it('degrades scalar-vs-object inside an array element without losing the array', async () => {
239
+ // Array analogue: items[*].foo is an object in one row and a scalar
240
+ // in another. foo degrades to variant; items stays array<record>.
205
241
  const salt = Math.random().toString(36).slice(2, 10);
206
242
  const viewName = `malloytest.test_variant_array_obj_conflict_${salt}`;
207
243
  await conn.runSQL(
208
244
  `CREATE OR REPLACE VIEW ${viewName} AS
209
- SELECT parse_json('{"items": [{"foo": {"bar": 1}}]}') AS data
245
+ SELECT parse_json('{"items": [{"foo": {"bar": 1}, "sib": "a"}]}') AS data
210
246
  UNION ALL
211
- SELECT parse_json('{"items": [{"foo": "oops"}]}') AS data`
247
+ SELECT parse_json('{"items": [{"foo": "oops", "sib": "b"}]}') AS data`
212
248
  );
213
249
  try {
214
250
  const schema = await conn.fetchTableSchema(viewName, viewName);
@@ -229,6 +265,8 @@ describe('db:Snowflake', () => {
229
265
  rawType: 'variant',
230
266
  name: 'foo',
231
267
  });
268
+ const sibField = itemsField!.fields.find(f => f.name === 'sib');
269
+ expect(sibField).toEqual({type: 'string', name: 'sib'});
232
270
  }
233
271
  }
234
272
  } finally {
@@ -236,6 +274,41 @@ describe('db:Snowflake', () => {
236
274
  }
237
275
  });
238
276
 
277
+ it('full-scans a small base table with variant columns under the byte threshold', async () => {
278
+ // Base table (not view) small enough that BYTES lands under the
279
+ // default 100 MB schemaSampleFullScanMaxBytes. The probe sees the
280
+ // size and the code takes the full-scan branch — no TABLESAMPLE,
281
+ // no LIMIT. Every row contributes to the (path, type) histogram,
282
+ // so rare fields are caught.
283
+ const salt = Math.random().toString(36).slice(2, 10);
284
+ const tableName = `malloytest.test_variant_fullscan_${salt}`;
285
+ await conn.runSQL(
286
+ `CREATE OR REPLACE TABLE ${tableName} AS
287
+ SELECT parse_json('{"foo": 1, "bar": "hi"}') AS data
288
+ UNION ALL
289
+ SELECT parse_json('{"foo": 2, "bar": "bye"}') AS data`
290
+ );
291
+ try {
292
+ const schema = await conn.fetchTableSchema(tableName, tableName);
293
+ const dataField = schema.fields.find(f => f.name === 'DATA');
294
+ expect(dataField).toBeDefined();
295
+ expect(dataField!.type).toBe('record');
296
+ if (dataField!.type === 'record') {
297
+ expect(dataField!.fields.find(f => f.name === 'foo')).toEqual({
298
+ name: 'foo',
299
+ type: 'number',
300
+ numberType: 'bigint',
301
+ });
302
+ expect(dataField!.fields.find(f => f.name === 'bar')).toEqual({
303
+ name: 'bar',
304
+ type: 'string',
305
+ });
306
+ }
307
+ } finally {
308
+ await conn.runSQL(`DROP TABLE IF EXISTS ${tableName}`);
309
+ }
310
+ });
311
+
239
312
  it('degrades when same path is object in one row and array in another', async () => {
240
313
  // foo is an object in one row and an array in another.
241
314
  // foo should degrade to sql native.
@@ -265,9 +338,10 @@ describe('db:Snowflake', () => {
265
338
  }
266
339
  });
267
340
 
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.
341
+ it('preserves sibling fields when one field degrades to variant', async () => {
342
+ // foo is scalar in one row and object in another, while stable is
343
+ // always consistent. The degradation should stay local to foo and
344
+ // keep stable untouched.
271
345
  const salt = Math.random().toString(36).slice(2, 10);
272
346
  const viewName = `malloytest.test_variant_sibling_${salt}`;
273
347
  await conn.runSQL(