@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.
- 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 +146 -228
- package/dist/snowflake_connection.js.map +1 -1
- package/dist/snowflake_connection.spec.js +84 -14
- package/dist/snowflake_connection.spec.js.map +1 -1
- package/dist/snowflake_sample_strategy.spec.d.ts +1 -0
- package/dist/snowflake_sample_strategy.spec.js +25 -0
- package/dist/snowflake_sample_strategy.spec.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.d.ts +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 +88 -14
- package/src/snowflake_connection.ts +220 -262
- package/src/snowflake_sample_strategy.spec.ts +43 -0
- package/src/snowflake_variant_schema.spec.ts +188 -0
- package/src/snowflake_variant_schema.ts +301 -0
|
@@ -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.
|
|
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.
|
|
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
|
-
'
|
|
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
|
|
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
|
|
174
|
-
// data.foo is
|
|
175
|
-
//
|
|
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
|
|
203
|
-
// Array analogue
|
|
204
|
-
//
|
|
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
|
|
270
|
-
//
|
|
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(
|