@malloydata/malloy-tests 0.0.425 → 0.0.427
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/package.json +10 -10
- package/src/core/persist.spec.ts +190 -0
- package/src/databases/all/streaming.spec.ts +159 -0
- package/src/core/streaming.spec.ts +0 -163
package/package.json
CHANGED
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"malloyc": "ts-node ../scripts/malloy-to-json"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@malloydata/db-bigquery": "0.0.
|
|
24
|
-
"@malloydata/db-duckdb": "0.0.
|
|
25
|
-
"@malloydata/db-postgres": "0.0.
|
|
26
|
-
"@malloydata/db-snowflake": "0.0.
|
|
27
|
-
"@malloydata/db-trino": "0.0.
|
|
28
|
-
"@malloydata/malloy": "0.0.
|
|
29
|
-
"@malloydata/malloy-tag": "0.0.
|
|
30
|
-
"@malloydata/render": "0.0.
|
|
31
|
-
"@malloydata/render-validator": "0.0.
|
|
23
|
+
"@malloydata/db-bigquery": "0.0.427",
|
|
24
|
+
"@malloydata/db-duckdb": "0.0.427",
|
|
25
|
+
"@malloydata/db-postgres": "0.0.427",
|
|
26
|
+
"@malloydata/db-snowflake": "0.0.427",
|
|
27
|
+
"@malloydata/db-trino": "0.0.427",
|
|
28
|
+
"@malloydata/malloy": "0.0.427",
|
|
29
|
+
"@malloydata/malloy-tag": "0.0.427",
|
|
30
|
+
"@malloydata/render": "0.0.427",
|
|
31
|
+
"@malloydata/render-validator": "0.0.427",
|
|
32
32
|
"events": "^3.3.0",
|
|
33
33
|
"jsdom": "^22.1.0",
|
|
34
34
|
"luxon": "^3.7.2",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"overrides": {
|
|
43
43
|
"typescript": "^6.0.3"
|
|
44
44
|
},
|
|
45
|
-
"version": "0.0.
|
|
45
|
+
"version": "0.0.427"
|
|
46
46
|
}
|
package/src/core/persist.spec.ts
CHANGED
|
@@ -2179,4 +2179,194 @@ describe('source persistence', () => {
|
|
|
2179
2179
|
expect(sql).toContain('cached.by_carrier');
|
|
2180
2180
|
});
|
|
2181
2181
|
});
|
|
2182
|
+
|
|
2183
|
+
// Reported on #3015. Both describes assert the behavior the documentation
|
|
2184
|
+
// describes, so a failure here is the report reproducing.
|
|
2185
|
+
describe('sql_select substitution', () => {
|
|
2186
|
+
// One model, two persisted sources of the two persistable kinds, plus a
|
|
2187
|
+
// third that reaches the sql_select through interpolation.
|
|
2188
|
+
const TWO_KINDS = `${PERSIST_ANNOTATION}
|
|
2189
|
+
${FLIGHTS_SOURCE}
|
|
2190
|
+
|
|
2191
|
+
#@ persist
|
|
2192
|
+
source: qs is flights -> {
|
|
2193
|
+
group_by: carrier
|
|
2194
|
+
aggregate: flight_count is count()
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
#@ persist
|
|
2198
|
+
source: ss is ${tstDB}.sql("""
|
|
2199
|
+
SELECT carrier, COUNT(*) as flight_count
|
|
2200
|
+
FROM malloytest.flights
|
|
2201
|
+
GROUP BY carrier
|
|
2202
|
+
""")
|
|
2203
|
+
|
|
2204
|
+
#@ persist
|
|
2205
|
+
source: via_interp is ${tstDB}.sql("""
|
|
2206
|
+
SELECT * FROM %{ ss }
|
|
2207
|
+
""")
|
|
2208
|
+
`;
|
|
2209
|
+
|
|
2210
|
+
// A manifest holding a built table for every persist source in the model.
|
|
2211
|
+
async function manifestForAll(): Promise<BuildManifest> {
|
|
2212
|
+
const model = await wrapTestModel(tstRuntime, TWO_KINDS).model.getModel();
|
|
2213
|
+
const digest = await getDigest();
|
|
2214
|
+
const manifest = createManifest();
|
|
2215
|
+
for (const source of Object.values(model.getBuildPlan().sources)) {
|
|
2216
|
+
addManifestEntry(
|
|
2217
|
+
manifest,
|
|
2218
|
+
source.makeBuildId(digest, source.getSQL()),
|
|
2219
|
+
`cached.${source.name}_table`
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
return manifest;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
async function sqlForQuery(query: string): Promise<string> {
|
|
2226
|
+
const manifest = await manifestForAll();
|
|
2227
|
+
return runtimeWithManifest(manifest)
|
|
2228
|
+
.loadQuery(`${TWO_KINDS}\n${query}`)
|
|
2229
|
+
.getSQL();
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
it('query_source reads its built table', async () => {
|
|
2233
|
+
const sql = await sqlForQuery('run: qs -> { select: * }');
|
|
2234
|
+
expect(sql).toContain('cached.qs_table');
|
|
2235
|
+
expect(sql).not.toContain('COUNT(');
|
|
2236
|
+
});
|
|
2237
|
+
|
|
2238
|
+
// Pending #3016 — a directly-referenced sql_select never reaches the
|
|
2239
|
+
// manifest. The two passing tests around these are the controls: the
|
|
2240
|
+
// other persistable kind substitutes, and the same source substitutes
|
|
2241
|
+
// when interpolation reaches it, so the entry and its key are right.
|
|
2242
|
+
it.skip('sql_select reads its built table', async () => {
|
|
2243
|
+
const sql = await sqlForQuery('run: ss -> { select: * }');
|
|
2244
|
+
expect(sql).toContain('cached.ss_table');
|
|
2245
|
+
expect(sql).not.toContain('COUNT(');
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
it('sql_select reached by interpolation reads its built table', async () => {
|
|
2249
|
+
// The manifest key and the entry are right — this path finds them.
|
|
2250
|
+
const sql = await sqlForQuery('run: via_interp -> { select: * }');
|
|
2251
|
+
expect(sql).toContain('cached.ss_table');
|
|
2252
|
+
expect(sql).not.toContain('COUNT(');
|
|
2253
|
+
});
|
|
2254
|
+
|
|
2255
|
+
it.skip('strict mode catches a missing sql_select entry', async () => {
|
|
2256
|
+
// Strict mode's promise is that a persist source with no manifest entry
|
|
2257
|
+
// throws rather than silently compiling to inline SQL.
|
|
2258
|
+
const strict = createManifest();
|
|
2259
|
+
strict.strict = true;
|
|
2260
|
+
await expect(
|
|
2261
|
+
runtimeWithManifest(strict)
|
|
2262
|
+
.loadQuery(`${TWO_KINDS}\nrun: ss -> { select: * }`)
|
|
2263
|
+
.getSQL()
|
|
2264
|
+
).rejects.toThrow(/not found in manifest/);
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
it.skip('a join of both kinds substitutes both', async () => {
|
|
2268
|
+
const sql = await sqlForQuery(`
|
|
2269
|
+
run: flights extend {
|
|
2270
|
+
join_one: q is qs on carrier = q.carrier
|
|
2271
|
+
join_one: s is ss on carrier = s.carrier
|
|
2272
|
+
} -> {
|
|
2273
|
+
group_by: carrier
|
|
2274
|
+
aggregate: a is q.flight_count.sum(), b is s.flight_count.sum()
|
|
2275
|
+
}
|
|
2276
|
+
`);
|
|
2277
|
+
expect(sql).toContain('cached.qs_table');
|
|
2278
|
+
expect(sql).toContain('cached.ss_table');
|
|
2279
|
+
});
|
|
2280
|
+
});
|
|
2281
|
+
|
|
2282
|
+
describe('per-call buildManifest on ModelMaterializer.loadQuery', () => {
|
|
2283
|
+
const MODEL = `${PERSIST_ANNOTATION}
|
|
2284
|
+
${FLIGHTS_SOURCE}
|
|
2285
|
+
|
|
2286
|
+
#@ persist
|
|
2287
|
+
source: by_carrier is flights -> {
|
|
2288
|
+
group_by: carrier
|
|
2289
|
+
aggregate: flight_count is count()
|
|
2290
|
+
}
|
|
2291
|
+
`;
|
|
2292
|
+
const QUERY = 'run: by_carrier -> { select: * }';
|
|
2293
|
+
|
|
2294
|
+
async function manifestForByCarrier(): Promise<BuildManifest> {
|
|
2295
|
+
const model = await wrapTestModel(tstRuntime, MODEL).model.getModel();
|
|
2296
|
+
const {manifest} = await buildManifestFor(
|
|
2297
|
+
model.getBuildPlan(),
|
|
2298
|
+
'by_carrier',
|
|
2299
|
+
'cached.by_carrier'
|
|
2300
|
+
);
|
|
2301
|
+
return manifest;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function plainRuntime() {
|
|
2305
|
+
return new SingleConnectionRuntime({
|
|
2306
|
+
connection: tstRuntime.connection,
|
|
2307
|
+
urlReader: testFileSpace,
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
it('applies a manifest passed to the call', async () => {
|
|
2312
|
+
const manifest = await manifestForByCarrier();
|
|
2313
|
+
const sql = await plainRuntime()
|
|
2314
|
+
.loadModel(MODEL)
|
|
2315
|
+
.loadQuery(QUERY, {buildManifest: manifest})
|
|
2316
|
+
.getSQL();
|
|
2317
|
+
expect(sql).toContain('cached.by_carrier');
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
it('applies a manifest passed to loadModel', async () => {
|
|
2321
|
+
const manifest = await manifestForByCarrier();
|
|
2322
|
+
const sql = await plainRuntime()
|
|
2323
|
+
.loadModel(MODEL, {buildManifest: manifest})
|
|
2324
|
+
.loadQuery(QUERY)
|
|
2325
|
+
.getSQL();
|
|
2326
|
+
expect(sql).toContain('cached.by_carrier');
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
it('EMPTY_BUILD_MANIFEST suppresses the runtime manifest', async () => {
|
|
2330
|
+
// The dangerous direction: asking for unsubstituted SQL and being
|
|
2331
|
+
// handed the substituted form.
|
|
2332
|
+
const manifest = await manifestForByCarrier();
|
|
2333
|
+
const sql = await runtimeWithManifest(manifest)
|
|
2334
|
+
.loadModel(MODEL)
|
|
2335
|
+
.loadQuery(QUERY, {buildManifest: EMPTY_BUILD_MANIFEST})
|
|
2336
|
+
.getSQL();
|
|
2337
|
+
expect(sql).not.toContain('cached.by_carrier');
|
|
2338
|
+
expect(sql).toContain('COUNT(');
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
it('loadQueryByName applies a manifest passed to the call', async () => {
|
|
2342
|
+
// The sibling loaders merge their options; this is the control.
|
|
2343
|
+
const manifest = await manifestForByCarrier();
|
|
2344
|
+
const sql = await plainRuntime()
|
|
2345
|
+
.loadModel(`${MODEL}\nquery: named is by_carrier -> { select: * }`)
|
|
2346
|
+
.loadQueryByName('named', {buildManifest: manifest})
|
|
2347
|
+
.getSQL();
|
|
2348
|
+
expect(sql).toContain('cached.by_carrier');
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
it('Runtime.loadQuery applies a manifest passed to the call', async () => {
|
|
2352
|
+
// The other control: the same options on the Runtime entry point.
|
|
2353
|
+
const manifest = await manifestForByCarrier();
|
|
2354
|
+
const sql = await plainRuntime()
|
|
2355
|
+
.loadQuery(`${MODEL}\n${QUERY}`, {buildManifest: manifest})
|
|
2356
|
+
.getSQL();
|
|
2357
|
+
expect(sql).toContain('cached.by_carrier');
|
|
2358
|
+
});
|
|
2359
|
+
|
|
2360
|
+
it('applies defaultRowLimit passed to the call', async () => {
|
|
2361
|
+
// buildManifest is not special: nothing in CompileQueryOptions
|
|
2362
|
+
// survives this call.
|
|
2363
|
+
const sql = await plainRuntime()
|
|
2364
|
+
.loadModel(MODEL)
|
|
2365
|
+
.loadQuery('run: flights -> { select: carrier }', {
|
|
2366
|
+
defaultRowLimit: 7,
|
|
2367
|
+
})
|
|
2368
|
+
.getSQL();
|
|
2369
|
+
expect(sql).toContain('LIMIT 7');
|
|
2370
|
+
});
|
|
2371
|
+
});
|
|
2182
2372
|
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {DataRecord, WriteStream} from '@malloydata/malloy';
|
|
7
|
+
import {CSVWriter, JSONWriter} from '@malloydata/malloy';
|
|
8
|
+
import {RuntimeList, allDatabases} from '../../runtimes';
|
|
9
|
+
import {databasesFromEnvironmentOr} from '../../util';
|
|
10
|
+
import {TestSelect} from '../../test-select';
|
|
11
|
+
|
|
12
|
+
class StringAccumulator implements WriteStream {
|
|
13
|
+
public accumulatedValue = '';
|
|
14
|
+
|
|
15
|
+
write(text: string) {
|
|
16
|
+
this.accumulatedValue += text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
close() {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const runtimes = new RuntimeList(databasesFromEnvironmentOr(allDatabases));
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await runtimes.closeAll();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
runtimes.runtimeMap.forEach((runtime, databaseName) => {
|
|
31
|
+
// Streaming is an optional connection capability (StreamingConnection).
|
|
32
|
+
// Dialects which do not implement it skip rather than fail.
|
|
33
|
+
const streams = runtime.connection.canStream();
|
|
34
|
+
|
|
35
|
+
// TestSelect quotes the column alias per dialect, so the name survives
|
|
36
|
+
// Snowflake's folding of unquoted identifiers to upper case.
|
|
37
|
+
const ts = new TestSelect(runtime.dialect);
|
|
38
|
+
const bigintSQL = ts.generate({big_id: ts.mk_bigint(19999)});
|
|
39
|
+
|
|
40
|
+
test.when(streams)(`basic stream test - ${databaseName}`, async () => {
|
|
41
|
+
const stream = runtime
|
|
42
|
+
.loadModel(
|
|
43
|
+
`source: airports is ${databaseName}.table('malloytest.airports')`
|
|
44
|
+
)
|
|
45
|
+
.loadQuery('run: airports -> { select: code; order_by: code }')
|
|
46
|
+
.runStream({rowLimit: 10});
|
|
47
|
+
const rows: DataRecord[] = [];
|
|
48
|
+
for await (const row of stream) {
|
|
49
|
+
rows.push(row);
|
|
50
|
+
}
|
|
51
|
+
expect(rows.length).toBe(10);
|
|
52
|
+
expect(rows[0].cell('code').string.value).toBe('00A');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test.when(streams)(`stream to JSON - ${databaseName}`, async () => {
|
|
56
|
+
const stream = runtime
|
|
57
|
+
.loadModel(
|
|
58
|
+
`source: airports is ${databaseName}.table('malloytest.airports')`
|
|
59
|
+
)
|
|
60
|
+
.loadQuery('run: airports -> { select: code; order_by: code }')
|
|
61
|
+
.runStream({rowLimit: 1});
|
|
62
|
+
const accummulator = new StringAccumulator();
|
|
63
|
+
const jsonWriter = new JSONWriter(accummulator);
|
|
64
|
+
await jsonWriter.process(stream);
|
|
65
|
+
expect(accummulator.accumulatedValue).toBe(
|
|
66
|
+
`[
|
|
67
|
+
{
|
|
68
|
+
"code": "00A"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
`
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test.when(streams)(`stream to CSV - ${databaseName}`, async () => {
|
|
76
|
+
const stream = runtime
|
|
77
|
+
.loadModel(
|
|
78
|
+
`source: airports is ${databaseName}.table('malloytest.airports')`
|
|
79
|
+
)
|
|
80
|
+
.loadQuery('run: airports -> { select: code; order_by: code }')
|
|
81
|
+
.runStream({rowLimit: 1});
|
|
82
|
+
const accummulator = new StringAccumulator();
|
|
83
|
+
const csvWriter = new CSVWriter(accummulator);
|
|
84
|
+
await csvWriter.process(stream);
|
|
85
|
+
expect(accummulator.accumulatedValue).toBe('code\n00A\n');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test.when(streams)(`JSON with timestamp - ${databaseName}`, async () => {
|
|
89
|
+
const stream = runtime
|
|
90
|
+
.loadModel(
|
|
91
|
+
`source: flights is ${databaseName}.table('malloytest.flights')`
|
|
92
|
+
)
|
|
93
|
+
.loadQuery(
|
|
94
|
+
"run: flights -> { select: dep_time; where: carrier = 'WN' and origin = 'SJC'; order_by: dep_time; limit: 1 }"
|
|
95
|
+
)
|
|
96
|
+
.runStream({rowLimit: 1});
|
|
97
|
+
const accummulator = new StringAccumulator();
|
|
98
|
+
const jsonWriter = new JSONWriter(accummulator);
|
|
99
|
+
await jsonWriter.process(stream);
|
|
100
|
+
const result = JSON.parse(accummulator.accumulatedValue);
|
|
101
|
+
expect(result.length).toBe(1);
|
|
102
|
+
// Should be an ISO date string
|
|
103
|
+
expect(typeof result[0].dep_time).toBe('string');
|
|
104
|
+
expect(result[0].dep_time).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test.when(streams)(`CSV with timestamp - ${databaseName}`, async () => {
|
|
108
|
+
const stream = runtime
|
|
109
|
+
.loadModel(
|
|
110
|
+
`source: flights is ${databaseName}.table('malloytest.flights')`
|
|
111
|
+
)
|
|
112
|
+
.loadQuery(
|
|
113
|
+
"run: flights -> { select: dep_time; where: carrier = 'WN' and origin = 'SJC'; order_by: dep_time; limit: 1 }"
|
|
114
|
+
)
|
|
115
|
+
.runStream({rowLimit: 1});
|
|
116
|
+
const accummulator = new StringAccumulator();
|
|
117
|
+
const csvWriter = new CSVWriter(accummulator);
|
|
118
|
+
await csvWriter.process(stream);
|
|
119
|
+
const lines = accummulator.accumulatedValue.trim().split('\n');
|
|
120
|
+
expect(lines.length).toBe(2);
|
|
121
|
+
expect(lines[0]).toBe('dep_time');
|
|
122
|
+
// Should be formatted as a date string
|
|
123
|
+
expect(lines[1]).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test.when(streams)(`JSON with bigint - ${databaseName}`, async () => {
|
|
127
|
+
const stream = runtime
|
|
128
|
+
.loadModel(
|
|
129
|
+
`source: bigint_test is ${databaseName}.sql("""${bigintSQL}""")`
|
|
130
|
+
)
|
|
131
|
+
.loadQuery('run: bigint_test -> { select: * }')
|
|
132
|
+
.runStream({rowLimit: 1});
|
|
133
|
+
const accummulator = new StringAccumulator();
|
|
134
|
+
const jsonWriter = new JSONWriter(accummulator);
|
|
135
|
+
await jsonWriter.process(stream);
|
|
136
|
+
const result = JSON.parse(accummulator.accumulatedValue);
|
|
137
|
+
expect(result.length).toBe(1);
|
|
138
|
+
// Bigint should be serialized as a string to preserve precision
|
|
139
|
+
expect(typeof result[0].big_id).toBe('string');
|
|
140
|
+
expect(result[0].big_id).toBe('19999');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test.when(streams)(`CSV with bigint - ${databaseName}`, async () => {
|
|
144
|
+
const stream = runtime
|
|
145
|
+
.loadModel(
|
|
146
|
+
`source: bigint_test is ${databaseName}.sql("""${bigintSQL}""")`
|
|
147
|
+
)
|
|
148
|
+
.loadQuery('run: bigint_test -> { select: * }')
|
|
149
|
+
.runStream({rowLimit: 1});
|
|
150
|
+
const accummulator = new StringAccumulator();
|
|
151
|
+
const csvWriter = new CSVWriter(accummulator);
|
|
152
|
+
await csvWriter.process(stream);
|
|
153
|
+
const lines = accummulator.accumulatedValue.trim().split('\n');
|
|
154
|
+
expect(lines.length).toBe(2);
|
|
155
|
+
expect(lines[0]).toBe('big_id');
|
|
156
|
+
// Should be a number without quotes
|
|
157
|
+
expect(lines[1]).toBe('19999');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright Contributors to the Malloy project
|
|
3
|
-
* SPDX-License-Identifier: MIT
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {DataRecord, WriteStream} from '@malloydata/malloy';
|
|
7
|
-
import {CSVWriter, JSONWriter} from '@malloydata/malloy';
|
|
8
|
-
import {RuntimeList} from '../runtimes';
|
|
9
|
-
import {describeIfDatabaseAvailable} from '../util';
|
|
10
|
-
|
|
11
|
-
class StringAccumulator implements WriteStream {
|
|
12
|
-
public accumulatedValue = '';
|
|
13
|
-
|
|
14
|
-
write(text: string) {
|
|
15
|
-
this.accumulatedValue += text;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
close() {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const [describe, databases] = describeIfDatabaseAvailable([
|
|
24
|
-
'bigquery',
|
|
25
|
-
'postgres',
|
|
26
|
-
'duckdb',
|
|
27
|
-
'duckdb_wasm',
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
describe('Streaming tests', () => {
|
|
31
|
-
if (!databases.length) {
|
|
32
|
-
it.skip('skipped', () => {});
|
|
33
|
-
}
|
|
34
|
-
const runtimes = new RuntimeList(databases);
|
|
35
|
-
|
|
36
|
-
afterAll(async () => {
|
|
37
|
-
await runtimes.closeAll();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
runtimes.runtimeMap.forEach((runtime, databaseName) => {
|
|
41
|
-
it(`basic stream test - ${databaseName}`, async () => {
|
|
42
|
-
const stream = runtime
|
|
43
|
-
.loadModel(
|
|
44
|
-
`source: airports is ${databaseName}.table('malloytest.airports')`
|
|
45
|
-
)
|
|
46
|
-
.loadQuery('run: airports -> { select: code; order_by: code }')
|
|
47
|
-
.runStream({rowLimit: 10});
|
|
48
|
-
const rows: DataRecord[] = [];
|
|
49
|
-
for await (const row of stream) {
|
|
50
|
-
rows.push(row);
|
|
51
|
-
}
|
|
52
|
-
expect(rows.length).toBe(10);
|
|
53
|
-
expect(rows[0].cell('code').string.value).toBe('00A');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it(`stream to JSON - ${databaseName}`, async () => {
|
|
57
|
-
const stream = runtime
|
|
58
|
-
.loadModel(
|
|
59
|
-
`source: airports is ${databaseName}.table('malloytest.airports')`
|
|
60
|
-
)
|
|
61
|
-
.loadQuery('run: airports -> { select: code; order_by: code }')
|
|
62
|
-
.runStream({rowLimit: 1});
|
|
63
|
-
const accummulator = new StringAccumulator();
|
|
64
|
-
const jsonWriter = new JSONWriter(accummulator);
|
|
65
|
-
await jsonWriter.process(stream);
|
|
66
|
-
expect(accummulator.accumulatedValue).toBe(
|
|
67
|
-
`[
|
|
68
|
-
{
|
|
69
|
-
"code": "00A"
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
`
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it(`stream to CSV - ${databaseName}`, async () => {
|
|
77
|
-
const stream = runtime
|
|
78
|
-
.loadModel(
|
|
79
|
-
`source: airports is ${databaseName}.table('malloytest.airports')`
|
|
80
|
-
)
|
|
81
|
-
.loadQuery('run: airports -> { select: code; order_by: code }')
|
|
82
|
-
.runStream({rowLimit: 1});
|
|
83
|
-
const accummulator = new StringAccumulator();
|
|
84
|
-
const csvWriter = new CSVWriter(accummulator);
|
|
85
|
-
await csvWriter.process(stream);
|
|
86
|
-
expect(accummulator.accumulatedValue).toBe('code\n00A\n');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it(`JSON with timestamp - ${databaseName}`, async () => {
|
|
90
|
-
const stream = runtime
|
|
91
|
-
.loadModel(
|
|
92
|
-
`source: flights is ${databaseName}.table('malloytest.flights')`
|
|
93
|
-
)
|
|
94
|
-
.loadQuery(
|
|
95
|
-
"run: flights -> { select: dep_time; where: carrier = 'WN' and origin = 'SJC'; order_by: dep_time; limit: 1 }"
|
|
96
|
-
)
|
|
97
|
-
.runStream({rowLimit: 1});
|
|
98
|
-
const accummulator = new StringAccumulator();
|
|
99
|
-
const jsonWriter = new JSONWriter(accummulator);
|
|
100
|
-
await jsonWriter.process(stream);
|
|
101
|
-
const result = JSON.parse(accummulator.accumulatedValue);
|
|
102
|
-
expect(result.length).toBe(1);
|
|
103
|
-
// Should be an ISO date string
|
|
104
|
-
expect(typeof result[0].dep_time).toBe('string');
|
|
105
|
-
expect(result[0].dep_time).toMatch(
|
|
106
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it(`CSV with timestamp - ${databaseName}`, async () => {
|
|
111
|
-
const stream = runtime
|
|
112
|
-
.loadModel(
|
|
113
|
-
`source: flights is ${databaseName}.table('malloytest.flights')`
|
|
114
|
-
)
|
|
115
|
-
.loadQuery(
|
|
116
|
-
"run: flights -> { select: dep_time; where: carrier = 'WN' and origin = 'SJC'; order_by: dep_time; limit: 1 }"
|
|
117
|
-
)
|
|
118
|
-
.runStream({rowLimit: 1});
|
|
119
|
-
const accummulator = new StringAccumulator();
|
|
120
|
-
const csvWriter = new CSVWriter(accummulator);
|
|
121
|
-
await csvWriter.process(stream);
|
|
122
|
-
const lines = accummulator.accumulatedValue.trim().split('\n');
|
|
123
|
-
expect(lines.length).toBe(2);
|
|
124
|
-
expect(lines[0]).toBe('dep_time');
|
|
125
|
-
// Should be formatted as a date string
|
|
126
|
-
expect(lines[1]).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it(`JSON with bigint - ${databaseName}`, async () => {
|
|
130
|
-
const stream = runtime
|
|
131
|
-
.loadModel(
|
|
132
|
-
`source: bigint_test is ${databaseName}.sql("SELECT CAST(19999 AS BIGINT) as big_id")`
|
|
133
|
-
)
|
|
134
|
-
.loadQuery('run: bigint_test -> { select: * }')
|
|
135
|
-
.runStream({rowLimit: 1});
|
|
136
|
-
const accummulator = new StringAccumulator();
|
|
137
|
-
const jsonWriter = new JSONWriter(accummulator);
|
|
138
|
-
await jsonWriter.process(stream);
|
|
139
|
-
const result = JSON.parse(accummulator.accumulatedValue);
|
|
140
|
-
expect(result.length).toBe(1);
|
|
141
|
-
// Bigint should be serialized as a string to preserve precision
|
|
142
|
-
expect(typeof result[0].big_id).toBe('string');
|
|
143
|
-
expect(result[0].big_id).toBe('19999');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it(`CSV with bigint - ${databaseName}`, async () => {
|
|
147
|
-
const stream = runtime
|
|
148
|
-
.loadModel(
|
|
149
|
-
`source: bigint_test is ${databaseName}.sql("SELECT CAST(19999 AS BIGINT) as big_id")`
|
|
150
|
-
)
|
|
151
|
-
.loadQuery('run: bigint_test -> { select: * }')
|
|
152
|
-
.runStream({rowLimit: 1});
|
|
153
|
-
const accummulator = new StringAccumulator();
|
|
154
|
-
const csvWriter = new CSVWriter(accummulator);
|
|
155
|
-
await csvWriter.process(stream);
|
|
156
|
-
const lines = accummulator.accumulatedValue.trim().split('\n');
|
|
157
|
-
expect(lines.length).toBe(2);
|
|
158
|
-
expect(lines[0]).toBe('big_id');
|
|
159
|
-
// Should be a number without quotes
|
|
160
|
-
expect(lines[1]).toBe('19999');
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
});
|