@elaraai/e3 1.0.79 → 1.0.80
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/README.md +11 -2
- package/dist/src/browser.d.ts +5 -4
- package/dist/src/browser.d.ts.map +1 -1
- package/dist/src/browser.js +3 -2
- package/dist/src/browser.js.map +1 -1
- package/dist/src/dataset-file.d.ts +17 -19
- package/dist/src/dataset-file.d.ts.map +1 -1
- package/dist/src/dataset-file.js +26 -45
- package/dist/src/dataset-file.js.map +1 -1
- package/dist/src/export.d.ts +2 -3
- package/dist/src/export.d.ts.map +1 -1
- package/dist/src/export.js +150 -80
- package/dist/src/export.js.map +1 -1
- package/dist/src/export.spec.js +177 -57
- package/dist/src/export.spec.js.map +1 -1
- package/dist/src/function.spec.js +29 -58
- package/dist/src/function.spec.js.map +1 -1
- package/dist/src/functions-link.spec.js +19 -17
- package/dist/src/functions-link.spec.js.map +1 -1
- package/dist/src/index.d.ts +56 -8
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/input.d.ts +8 -5
- package/dist/src/input.d.ts.map +1 -1
- package/dist/src/input.js +8 -5
- package/dist/src/input.js.map +1 -1
- package/dist/src/migration.d.ts +172 -0
- package/dist/src/migration.d.ts.map +1 -0
- package/dist/src/migration.js +108 -0
- package/dist/src/migration.js.map +1 -0
- package/dist/src/migration.spec.d.ts +6 -0
- package/dist/src/migration.spec.d.ts.map +1 -0
- package/dist/src/migration.spec.js +197 -0
- package/dist/src/migration.spec.js.map +1 -0
- package/dist/src/mutation-programs.spec.d.ts +6 -0
- package/dist/src/mutation-programs.spec.d.ts.map +1 -0
- package/dist/src/mutation-programs.spec.js +310 -0
- package/dist/src/mutation-programs.spec.js.map +1 -0
- package/dist/src/mutation.d.ts +117 -9
- package/dist/src/mutation.d.ts.map +1 -1
- package/dist/src/mutation.js +104 -46
- package/dist/src/mutation.js.map +1 -1
- package/dist/src/open-beast-link.spec.js +10 -8
- package/dist/src/open-beast-link.spec.js.map +1 -1
- package/dist/src/output.d.ts +117 -0
- package/dist/src/output.d.ts.map +1 -0
- package/dist/src/output.js +119 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/package.d.ts +7 -5
- package/dist/src/package.d.ts.map +1 -1
- package/dist/src/package.js +98 -8
- package/dist/src/package.js.map +1 -1
- package/dist/src/package.spec.d.ts +6 -0
- package/dist/src/package.spec.d.ts.map +1 -0
- package/dist/src/package.spec.js +16 -0
- package/dist/src/package.spec.js.map +1 -0
- package/dist/src/record-guards.d.ts +41 -0
- package/dist/src/record-guards.d.ts.map +1 -0
- package/dist/src/record-guards.js +44 -0
- package/dist/src/record-guards.js.map +1 -0
- package/dist/src/record-index.d.ts +129 -0
- package/dist/src/record-index.d.ts.map +1 -0
- package/dist/src/record-index.js +144 -0
- package/dist/src/record-index.js.map +1 -0
- package/dist/src/record-index.spec.d.ts +6 -0
- package/dist/src/record-index.spec.d.ts.map +1 -0
- package/dist/src/record-index.spec.js +160 -0
- package/dist/src/record-index.spec.js.map +1 -0
- package/dist/src/record-programs.d.ts +128 -0
- package/dist/src/record-programs.d.ts.map +1 -0
- package/dist/src/record-programs.js +480 -0
- package/dist/src/record-programs.js.map +1 -0
- package/dist/src/record.d.ts.map +1 -1
- package/dist/src/record.js +2 -0
- package/dist/src/record.js.map +1 -1
- package/dist/src/record.spec.js +71 -13
- package/dist/src/record.spec.js.map +1 -1
- package/dist/src/runner.d.ts +6 -9
- package/dist/src/runner.d.ts.map +1 -1
- package/dist/src/runner.js +9 -25
- package/dist/src/runner.js.map +1 -1
- package/dist/src/runner.spec.js +16 -34
- package/dist/src/runner.spec.js.map +1 -1
- package/dist/src/sha256.d.ts +4 -3
- package/dist/src/sha256.d.ts.map +1 -1
- package/dist/src/sha256.js +20 -17
- package/dist/src/sha256.js.map +1 -1
- package/dist/src/sha256.spec.js +10 -0
- package/dist/src/sha256.spec.js.map +1 -1
- package/dist/src/task.d.ts +116 -251
- package/dist/src/task.d.ts.map +1 -1
- package/dist/src/task.js +169 -435
- package/dist/src/task.js.map +1 -1
- package/dist/src/task.spec.js +174 -489
- package/dist/src/task.spec.js.map +1 -1
- package/dist/src/types.d.ts +215 -23
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/src/task.spec.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { describe, it } from 'node:test';
|
|
6
6
|
import assert from 'node:assert';
|
|
7
|
-
import { East, StringType, IntegerType, FloatType, StructType, DictType, SetType, ArrayType, variant,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { East, StringType, IntegerType, FloatType, StructType, DictType, SetType, ArrayType, NullType, FunctionType, variant, isTypeValueEqual, toEastTypeValue } from '@elaraai/east';
|
|
8
|
+
import { task, customTask, partition, streamTask } from './task.js';
|
|
9
|
+
import { output } from './output.js';
|
|
10
10
|
import { input } from './input.js';
|
|
11
|
+
/** Whether two East types are the same type. */
|
|
12
|
+
const sameType = (a, b) => isTypeValueEqual(toEastTypeValue(a), toEastTypeValue(b));
|
|
11
13
|
describe('task', () => {
|
|
12
14
|
describe('type inference', () => {
|
|
13
15
|
it('accepts single input dataset', () => {
|
|
@@ -24,22 +26,14 @@ describe('task', () => {
|
|
|
24
26
|
assert.strictEqual(repeat_greet.kind, 'task');
|
|
25
27
|
assert.strictEqual(repeat_greet.name, 'repeat_greet');
|
|
26
28
|
});
|
|
27
|
-
it('accepts
|
|
28
|
-
const a_input = input('a', StringType, variant('value', 'a'));
|
|
29
|
-
const b_input = input('b', IntegerType, variant('value', 1n));
|
|
30
|
-
const c_input = input('c', FloatType, variant('value', 1.0));
|
|
31
|
-
const combine = task('combine', [a_input, b_input, c_input], East.function([StringType, IntegerType, FloatType], StringType, ($, a, _b, _c) => $.return(a)));
|
|
32
|
-
assert.strictEqual(combine.kind, 'task');
|
|
33
|
-
assert.strictEqual(combine.inputs.length, 4); // 3 inputs + function_ir
|
|
34
|
-
});
|
|
35
|
-
it('accepts four input datasets (tuple type preservation)', () => {
|
|
29
|
+
it('accepts four input datasets (tuple type preservation), each an input of the body', () => {
|
|
36
30
|
const a_input = input('a', StringType, variant('value', 'a'));
|
|
37
31
|
const b_input = input('b', IntegerType, variant('value', 1n));
|
|
38
32
|
const c_input = input('c', FloatType, variant('value', 1.0));
|
|
39
33
|
const d_input = input('d', StringType, variant('value', 'd'));
|
|
40
34
|
const combine_four = task('combine_four', [a_input, b_input, c_input, d_input], East.function([StringType, IntegerType, FloatType, StringType], StringType, ($, a, _b, _c, _d) => $.return(a)));
|
|
41
35
|
assert.strictEqual(combine_four.kind, 'task');
|
|
42
|
-
assert.
|
|
36
|
+
assert.deepStrictEqual(combine_four.inputs, [a_input, b_input, c_input, d_input]);
|
|
43
37
|
});
|
|
44
38
|
it('accepts struct type inputs', () => {
|
|
45
39
|
const PersonType = StructType({
|
|
@@ -61,30 +55,14 @@ describe('task', () => {
|
|
|
61
55
|
// shout depends on greet's output
|
|
62
56
|
assert.ok(shout.deps.has(greet.output));
|
|
63
57
|
});
|
|
64
|
-
it('allows mixing inputs and task outputs', () => {
|
|
65
|
-
const name_input = input('name', StringType, variant('value', 'World'));
|
|
66
|
-
const suffix_input = input('suffix', StringType, variant('value', '!'));
|
|
67
|
-
const greet = task('greet', [name_input], East.function([StringType], StringType, ($, name) => $.return(East.str `Hello, ${name}`)));
|
|
68
|
-
const add_suffix = task('add_suffix', [greet.output, suffix_input], East.function([StringType, StringType], StringType, ($, greeting, suffix) => $.return(East.str `${greeting}${suffix}`)));
|
|
69
|
-
assert.strictEqual(add_suffix.kind, 'task');
|
|
70
|
-
assert.strictEqual(add_suffix.inputs.length, 3); // function_ir + 2 inputs
|
|
71
|
-
});
|
|
72
58
|
});
|
|
73
59
|
describe('async functions', () => {
|
|
74
60
|
it('accepts an async function', () => {
|
|
75
61
|
const name_input = input('name', StringType, variant('value', 'World'));
|
|
76
62
|
const greet = task('greet', [name_input], East.asyncFunction([StringType], StringType, ($, name) => $.return(East.str `Hello, ${name}!`)));
|
|
77
63
|
assert.strictEqual(greet.kind, 'task');
|
|
78
|
-
assert.strictEqual(greet.name, 'greet');
|
|
79
|
-
assert.strictEqual(greet.output.kind, 'dataset');
|
|
80
64
|
assert.strictEqual(greet.output.type, StringType);
|
|
81
|
-
|
|
82
|
-
it('accepts async function with multiple inputs', () => {
|
|
83
|
-
const name_input = input('name', StringType, variant('value', 'World'));
|
|
84
|
-
const count_input = input('count', IntegerType, variant('value', 1n));
|
|
85
|
-
const repeat_greet = task('repeat_greet', [name_input, count_input], East.asyncFunction([StringType, IntegerType], StringType, ($, name, _count) => $.return(East.str `Hello, ${name}!`)));
|
|
86
|
-
assert.strictEqual(repeat_greet.kind, 'task');
|
|
87
|
-
assert.strictEqual(repeat_greet.inputs.length, 3); // function_ir + 2 inputs
|
|
65
|
+
assert.strictEqual(greet.body.kind, 'east');
|
|
88
66
|
});
|
|
89
67
|
});
|
|
90
68
|
describe('task structure', () => {
|
|
@@ -97,383 +75,92 @@ describe('task', () => {
|
|
|
97
75
|
variant('field', 'output'),
|
|
98
76
|
]);
|
|
99
77
|
});
|
|
100
|
-
it('
|
|
78
|
+
it('carries the function as its program, returning the output, in the data role', () => {
|
|
101
79
|
const name_input = input('name', StringType, variant('value', 'World'));
|
|
102
|
-
const greet = task('greet', [name_input], East.function([StringType], StringType, ($, name) => $.return(name)));
|
|
103
|
-
//
|
|
104
|
-
|
|
80
|
+
const greet = task('greet', [name_input], East.function([StringType], StringType, ($, name) => $.return(East.str `Hello, ${name}!`)));
|
|
81
|
+
// The program is the body itself — no dataset holds it, and the inputs
|
|
82
|
+
// are the datasets alone.
|
|
83
|
+
assert.deepStrictEqual(greet.inputs, [name_input]);
|
|
84
|
+
assert.strictEqual(greet.outputKind, undefined);
|
|
85
|
+
assert.deepStrictEqual(greet.role, variant('data', null));
|
|
86
|
+
assert.strictEqual(greet.body.kind, 'east');
|
|
87
|
+
if (greet.body.kind !== 'east')
|
|
88
|
+
return;
|
|
89
|
+
const run = greet.body.program.compile([]);
|
|
90
|
+
assert.strictEqual(run('World'), 'Hello, World!');
|
|
105
91
|
});
|
|
106
|
-
it('
|
|
92
|
+
it('keeps a custom runner and the role it is given', () => {
|
|
107
93
|
const name_input = input('name', StringType, variant('value', 'World'));
|
|
94
|
+
const role = variant('ui', { paths: [name_input.path], functions: [], records: [], pages: [] });
|
|
108
95
|
const greet = task('greet', [name_input], East.function([StringType], StringType, ($, name) => $.return(name)), {
|
|
109
96
|
runner: { runtime: 'custom', command: ['uv', 'run', 'east-py', 'run', '-p', 'east-py-std'] },
|
|
97
|
+
role,
|
|
110
98
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
describe('partitionTask', () => {
|
|
117
|
-
const SaleType = StructType({ sku: StringType, qty: IntegerType });
|
|
118
|
-
const SaleKeyType = StructType({ sku: StringType, period: IntegerType });
|
|
119
|
-
it('builds a partition-kind task with the metadata spec', () => {
|
|
120
|
-
const sales = input('sales', DictType(SaleKeyType, SaleType));
|
|
121
|
-
const totals = input('rates', DictType(StringType, FloatType));
|
|
122
|
-
const cleaned = partitionTask('cleaned', {
|
|
123
|
-
partitions: [sales],
|
|
124
|
-
inputs: [totals],
|
|
125
|
-
output: DictType(SaleKeyType, SaleType),
|
|
126
|
-
targetPartitionBytes: 1024,
|
|
127
|
-
}, ($, slice, _rates) => $.return(slice));
|
|
128
|
-
assert.strictEqual(cleaned.kind, 'task');
|
|
129
|
-
assert.strictEqual(cleaned.taskKind, TASK_KIND_PARTITION);
|
|
130
|
-
// function_ir + 1 partition + 1 ordinary input
|
|
131
|
-
assert.strictEqual(cleaned.inputs.length, 3);
|
|
132
|
-
assert.strictEqual(cleaned.inputs[0].name, 'function_ir');
|
|
133
|
-
assert.strictEqual(cleaned.inputs[1], sales);
|
|
134
|
-
assert.strictEqual(cleaned.inputs[2], totals);
|
|
135
|
-
assert.deepStrictEqual(cleaned.output.path, [
|
|
136
|
-
variant('field', 'tasks'),
|
|
137
|
-
variant('field', 'cleaned'),
|
|
138
|
-
variant('field', 'output'),
|
|
139
|
-
]);
|
|
140
|
-
const meta = decodePartitionTaskMetadata(cleaned.metadata);
|
|
141
|
-
assert.strictEqual(meta.partitions, 1n);
|
|
142
|
-
assert.strictEqual(meta.by.type, 'none');
|
|
143
|
-
assert.strictEqual(meta.combine.type, 'none');
|
|
144
|
-
assert.strictEqual(meta.targetPartitionBytes, 1024n);
|
|
145
|
-
});
|
|
146
|
-
it('carries by and combine as decodable EastIR bundles', () => {
|
|
147
|
-
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
148
|
-
const totals = partitionTask('totals', {
|
|
149
|
-
partitions: [sales],
|
|
150
|
-
by: (_$, key) => key.sku,
|
|
151
|
-
output: DictType(StringType, IntegerType),
|
|
152
|
-
combine: ($, a, b) => {
|
|
153
|
-
// Partials are frozen task inputs — fold into a copy.
|
|
154
|
-
const acc = $.let(a.copy());
|
|
155
|
-
$(acc.mergeAll(b, ($, v1, v2) => v1.add(v2), ($, _k) => 0n));
|
|
156
|
-
$.return(acc);
|
|
157
|
-
},
|
|
158
|
-
}, ($, slice) => slice.toArray(($, v, k) => ({ sku: k.sku, v }))
|
|
159
|
-
.groupReduce(($, x) => x.sku, ($, _k) => 0n, ($, acc, x) => acc.add(x.v)));
|
|
160
|
-
const meta = decodePartitionTaskMetadata(totals.metadata);
|
|
161
|
-
assert.strictEqual(meta.partitions, 1n);
|
|
162
|
-
assert.strictEqual(meta.by.type, 'some');
|
|
163
|
-
assert.strictEqual(meta.combine.type, 'some');
|
|
164
|
-
// Both blobs decode as free East functions with the declared signatures.
|
|
165
|
-
const byIr = decodeEastIR(meta.by.type === 'some' ? meta.by.value : new Uint8Array());
|
|
166
|
-
assert.strictEqual(byIr.ir.value.parameters.length, 1);
|
|
167
|
-
const combineIr = decodeEastIR(meta.combine.type === 'some' ? meta.combine.value : new Uint8Array());
|
|
168
|
-
assert.strictEqual(combineIr.ir.value.parameters.length, 2);
|
|
169
|
-
});
|
|
170
|
-
it('accepts a whole-key identity by and a leading-prefix struct by', () => {
|
|
171
|
-
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
172
|
-
const identity = partitionTask('by_identity', {
|
|
173
|
-
partitions: [sales],
|
|
174
|
-
by: (_$, key) => key,
|
|
175
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
176
|
-
}, ($, slice) => $.return(slice));
|
|
177
|
-
assert.strictEqual(decodePartitionTaskMetadata(identity.metadata).by.type, 'some');
|
|
178
|
-
const prefix = partitionTask('by_prefix', {
|
|
179
|
-
partitions: [sales],
|
|
180
|
-
by: (_$, key) => ({ sku: key.sku }),
|
|
181
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
182
|
-
}, ($, slice) => $.return(slice));
|
|
183
|
-
assert.strictEqual(decodePartitionTaskMetadata(prefix.metadata).by.type, 'some');
|
|
184
|
-
});
|
|
185
|
-
it('rejects a by that is not a leading key prefix, naming the dataset and its field order', () => {
|
|
186
|
-
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
187
|
-
assert.throws(() => partitionTask('bad_by', {
|
|
188
|
-
partitions: [sales],
|
|
189
|
-
by: (_$, key) => key.period,
|
|
190
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
191
|
-
}, ($, slice) => $.return(slice)), /not a leading prefix of partitioned dataset 'sales' key field order \(sku, period\)/);
|
|
192
|
-
});
|
|
193
|
-
it('rejects unsupported by projection shapes, echoing the accepted shapes', () => {
|
|
194
|
-
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
195
|
-
assert.throws(() => partitionTask('computed_by', {
|
|
196
|
-
partitions: [sales],
|
|
197
|
-
by: (_$, key) => East.str `${key.sku}-x`,
|
|
198
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
199
|
-
}, ($, slice) => $.return(slice)), /`by` must project a leading prefix of the partition key — accepted shapes: the key itself, a leading field/);
|
|
200
|
-
});
|
|
201
|
-
describe('`by` nested leading-field paths', () => {
|
|
202
|
-
const NestedKeyType = StructType({
|
|
203
|
-
head: StructType({ region: StringType, store: IntegerType }),
|
|
204
|
-
seq: IntegerType,
|
|
99
|
+
// A custom runtime runs the same program, with run's arguments.
|
|
100
|
+
assert.strictEqual(greet.body.kind, 'east');
|
|
101
|
+
assert.deepStrictEqual(greet.runner, { runtime: 'custom', command: ['uv', 'run', 'east-py', 'run', '-p', 'east-py-std'] });
|
|
102
|
+
assert.strictEqual(greet.role, role);
|
|
205
103
|
});
|
|
206
|
-
it('
|
|
207
|
-
const sales = input('
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
assert.
|
|
216
|
-
// One level of the same spine (a whole-struct leading field).
|
|
217
|
-
const oneLevel = partitionTask('by_nested_head', {
|
|
218
|
-
partitions: [sales],
|
|
219
|
-
by: (_$, key) => key.head,
|
|
220
|
-
output: DictType(NestedKeyType, IntegerType),
|
|
221
|
-
}, ($, slice) => $.return(slice));
|
|
222
|
-
assert.strictEqual(decodePartitionTaskMetadata(oneLevel.metadata).by.type, 'some');
|
|
223
|
-
});
|
|
224
|
-
it('rejects a nested path that leaves the first-field spine, naming the level', () => {
|
|
225
|
-
const sales = input('nsales2', DictType(NestedKeyType, IntegerType));
|
|
226
|
-
assert.throws(() => partitionTask('bad_nested', {
|
|
227
|
-
partitions: [sales],
|
|
228
|
-
by: (_$, key) => key.head.store,
|
|
229
|
-
output: DictType(NestedKeyType, IntegerType),
|
|
230
|
-
}, ($, slice) => $.return(slice)), /path \(key\.head\.store\) reads 'store', which is not the first field of partitioned dataset 'nsales2' key level \(region, store\)/);
|
|
231
|
-
// A non-leading top-level field stays rejected with the prefix error.
|
|
232
|
-
assert.throws(() => partitionTask('bad_nested_top', {
|
|
233
|
-
partitions: [sales],
|
|
234
|
-
by: (_$, key) => key.seq,
|
|
235
|
-
output: DictType(NestedKeyType, IntegerType),
|
|
236
|
-
}, ($, slice) => $.return(slice)), /projects \(seq\), which is not a leading prefix of partitioned dataset 'nsales2' key field order \(head, seq\)/);
|
|
237
|
-
});
|
|
238
|
-
it('ByResult rejects non-projection returns at compile time', () => {
|
|
239
|
-
const sales = input('nsales4', DictType(NestedKeyType, IntegerType));
|
|
240
|
-
const out = DictType(NestedKeyType, IntegerType);
|
|
241
|
-
// Thunks only — nothing runs; the @ts-expect-error directives pin the
|
|
242
|
-
// TYPE-level rejections (the build fails if any of them stops erroring).
|
|
243
|
-
const bad1 = () => partitionTask('t_bad1', {
|
|
244
|
-
partitions: [sales],
|
|
245
|
-
// @ts-expect-error a bare host value is not a key projection
|
|
246
|
-
by: () => 42,
|
|
247
|
-
output: out,
|
|
248
|
-
}, ($, s) => $.return(s));
|
|
249
|
-
const bad2 = () => partitionTask('t_bad2', {
|
|
250
|
-
partitions: [sales],
|
|
251
|
-
// @ts-expect-error `r` is not a field of the partition key
|
|
252
|
-
by: (_$, k) => ({ r: k.head }),
|
|
253
|
-
output: out,
|
|
254
|
-
}, ($, s) => $.return(s));
|
|
255
|
-
const bad3 = () => partitionTask('t_bad3', {
|
|
256
|
-
partitions: [sales],
|
|
257
|
-
// @ts-expect-error `seq` requires an integer expression, not the head struct
|
|
258
|
-
by: (_$, k) => ({ seq: k.head }),
|
|
259
|
-
output: out,
|
|
260
|
-
}, ($, s) => $.return(s));
|
|
261
|
-
assert.ok(bad1 !== undefined && bad2 !== undefined && bad3 !== undefined);
|
|
262
|
-
});
|
|
263
|
-
it('rejects a struct literal over nested paths (deliberately unsupported)', () => {
|
|
264
|
-
const sales = input('nsales3', DictType(NestedKeyType, IntegerType));
|
|
265
|
-
assert.throws(() => partitionTask('bad_struct_nested', {
|
|
266
|
-
partitions: [sales],
|
|
267
|
-
// ByResult already rejects this shape at COMPILE time (`r` is not
|
|
268
|
-
// a key field) — the cast exercises the build-time backstop that
|
|
269
|
-
// guards untyped/JS callers.
|
|
270
|
-
by: ((_$, key) => ({ r: key.head.region })),
|
|
271
|
-
output: DictType(NestedKeyType, IntegerType),
|
|
272
|
-
}, ($, slice) => $.return(slice)), /accepted shapes: the key itself/);
|
|
104
|
+
it('refuses an input marked with e3.partition, which only a stream task takes', () => {
|
|
105
|
+
const sales = input('sales', DictType(StringType, FloatType));
|
|
106
|
+
const total = East.function([DictType(StringType, FloatType)], FloatType, ($, s) => s.sum());
|
|
107
|
+
// The type refuses it; the runtime check names the task for a caller
|
|
108
|
+
// outside TypeScript's reach.
|
|
109
|
+
// @ts-expect-error a partitioned input is not a dataset
|
|
110
|
+
const typed = () => task('total', [partition(sales)], total);
|
|
111
|
+
assert.ok(typed !== undefined);
|
|
112
|
+
const untypedTask = task;
|
|
113
|
+
assert.throws(() => untypedTask('total', [partition(sales)], total), /^Error: task 'total': input 'sales' is marked with e3\.partition, which only e3\.streamTask takes — pass the dataset itself$/);
|
|
273
114
|
});
|
|
274
115
|
});
|
|
275
|
-
it('rejects non-collection partitions and mixed-kind co-partitioning', () => {
|
|
276
|
-
const scalar = input('scalar', IntegerType, variant('value', 1n));
|
|
277
|
-
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
278
|
-
const skus = input('skus', SetType(StringType));
|
|
279
|
-
const rows = input('rows', ArrayType(IntegerType));
|
|
280
|
-
assert.throws(() => partitionTask('bad', {
|
|
281
|
-
partitions: [scalar],
|
|
282
|
-
output: IntegerType,
|
|
283
|
-
}, ($, s) => $.return(s)), /must be a collection \(Array, Set or Dict\)/);
|
|
284
|
-
assert.throws(() => partitionTask('mixed', {
|
|
285
|
-
partitions: [sales, skus],
|
|
286
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
287
|
-
}, ($, a, _b) => $.return(a)), /co-partitioning is restricted to Dict\/Set roots/);
|
|
288
|
-
assert.throws(() => partitionTask('arrays', {
|
|
289
|
-
partitions: [rows, rows],
|
|
290
|
-
output: ArrayType(IntegerType),
|
|
291
|
-
}, ($, a, _b) => $.return(a)), /co-partitioning is restricted to Dict\/Set roots/);
|
|
292
|
-
});
|
|
293
|
-
it('co-partitions same-keyed dicts and types by over the shared key', () => {
|
|
294
|
-
const a = input('a', DictType(SaleKeyType, IntegerType));
|
|
295
|
-
const b = input('b', DictType(SaleKeyType, FloatType));
|
|
296
|
-
const reconcile = partitionTask('reconcile', {
|
|
297
|
-
partitions: [a, b],
|
|
298
|
-
by: (_$, key) => key.sku,
|
|
299
|
-
output: DictType(SaleKeyType, FloatType),
|
|
300
|
-
}, ($, _sliceA, sliceB) => $.return(sliceB));
|
|
301
|
-
assert.strictEqual(reconcile.taskKind, TASK_KIND_PARTITION);
|
|
302
|
-
assert.strictEqual(decodePartitionTaskMetadata(reconcile.metadata).partitions, 2n);
|
|
303
|
-
// function_ir + 2 partitions
|
|
304
|
-
assert.strictEqual(reconcile.inputs.length, 3);
|
|
305
|
-
});
|
|
306
|
-
describe('heterogeneous co-partition keys (the implicit projection)', () => {
|
|
307
|
-
// Same fields, opposite declared order — the field-wise intersection is
|
|
308
|
-
// non-empty, but the datasets SORT differently, so implicit boundary
|
|
309
|
-
// alignment would silently mis-assign rows at run time.
|
|
310
|
-
const ReversedKeyType = StructType({ period: IntegerType, sku: StringType });
|
|
311
|
-
it('rejects reordered key fields when no `by` is given', () => {
|
|
312
|
-
const a = input('h_a', DictType(SaleKeyType, IntegerType));
|
|
313
|
-
const b = input('h_b', DictType(ReversedKeyType, IntegerType));
|
|
314
|
-
assert.throws(() => partitionTask('implicit_misorder', {
|
|
315
|
-
partitions: [a, b],
|
|
316
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
317
|
-
}, ($, sliceA, _sliceB) => $.return(sliceA)), /with no `by`, co-partition boundaries align on partitioned dataset 'h_a' key order \(sku, period\), which is not a leading prefix of partitioned dataset 'h_b' key field order \(period, sku\)/);
|
|
318
|
-
});
|
|
319
|
-
it('rejects reordered key fields under an identity `by` too', () => {
|
|
320
|
-
const a = input('h_c', DictType(SaleKeyType, IntegerType));
|
|
321
|
-
const b = input('h_d', DictType(ReversedKeyType, IntegerType));
|
|
322
|
-
assert.throws(() => partitionTask('identity_misorder', {
|
|
323
|
-
partitions: [a, b],
|
|
324
|
-
by: (_$, key) => key,
|
|
325
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
326
|
-
}, ($, sliceA, _sliceB) => $.return(sliceA)), /the identity `by` projection reads the shared key fields \(sku, period\), which is not a leading prefix of partitioned dataset 'h_d' key field order \(period, sku\)/);
|
|
327
|
-
});
|
|
328
|
-
it('rejects a same-named leading field whose types differ across datasets', () => {
|
|
329
|
-
// `sku` exists in both keys at position 0 but with different types, so
|
|
330
|
-
// the intersection drops it — the primary's comparator would compare
|
|
331
|
-
// string skus against integer skus by kind rank.
|
|
332
|
-
const IntSkuKeyType = StructType({ sku: IntegerType, period: IntegerType });
|
|
333
|
-
const a = input('h_e', DictType(SaleKeyType, IntegerType));
|
|
334
|
-
const b = input('h_f', DictType(IntSkuKeyType, IntegerType));
|
|
335
|
-
assert.throws(() => partitionTask('type_mismatch', {
|
|
336
|
-
partitions: [a, b],
|
|
337
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
338
|
-
}, ($, sliceA, _sliceB) => $.return(sliceA)), /with no `by`, co-partition boundaries align on partitioned dataset 'h_e' key order/);
|
|
339
|
-
});
|
|
340
|
-
it('accepts a secondary whose key extends the primary key with trailing fields', () => {
|
|
341
|
-
// The primary's full key IS a leading prefix of the secondary's — the
|
|
342
|
-
// implicit alignment (projecting onto the primary's fields) is
|
|
343
|
-
// monotone for both, so this stays legal without `by`.
|
|
344
|
-
const ExtendedKeyType = StructType({ sku: StringType, period: IntegerType, line: IntegerType });
|
|
345
|
-
const a = input('h_g', DictType(SaleKeyType, IntegerType));
|
|
346
|
-
const b = input('h_h', DictType(ExtendedKeyType, IntegerType));
|
|
347
|
-
const extended = partitionTask('extended_ok', {
|
|
348
|
-
partitions: [a, b],
|
|
349
|
-
output: DictType(SaleKeyType, IntegerType),
|
|
350
|
-
}, ($, sliceA, _sliceB) => $.return(sliceA));
|
|
351
|
-
assert.strictEqual(extended.taskKind, TASK_KIND_PARTITION);
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
it('rejects a non-collection output in splice mode, and accepts it with combine', () => {
|
|
355
|
-
const sales = input('splice_sales', DictType(SaleKeyType, IntegerType));
|
|
356
|
-
const TotalType = StructType({ total: IntegerType });
|
|
357
|
-
// Without combine, partitions return SHARDS that splice — a struct
|
|
358
|
-
// cannot; this used to surface only at run time, on the first input
|
|
359
|
-
// large enough to carve into two partitions.
|
|
360
|
-
assert.throws(() => partitionTask('struct_splice', {
|
|
361
|
-
partitions: [sales],
|
|
362
|
-
output: TotalType,
|
|
363
|
-
}, ($, slice) => $.return({ total: slice.size() })), /without `combine`, each partition returns a shard of the output and the shards splice \(or, with `merge`, merge\) in key order — the output must be a collection \(Array, Set or Dict\), got Struct/);
|
|
364
|
-
const folded = partitionTask('struct_combine', {
|
|
365
|
-
partitions: [sales],
|
|
366
|
-
output: TotalType,
|
|
367
|
-
combine: ($, a, b) => $.return({ total: a.total.add(b.total) }),
|
|
368
|
-
}, ($, slice) => $.return({ total: slice.size() }));
|
|
369
|
-
assert.strictEqual(folded.taskKind, TASK_KIND_PARTITION);
|
|
370
|
-
});
|
|
371
116
|
});
|
|
372
|
-
describe('
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
assert.strictEqual(dictMeta.merge.type, 'some');
|
|
383
|
-
assert.strictEqual(dictMeta.mergeSets, false);
|
|
384
|
-
assert.strictEqual(dictMeta.combine.type, 'none');
|
|
385
|
-
// The bundle is the (Key, Value, Value) -> Value the orchestrator compiles.
|
|
386
|
-
const bundle = decodeEastIR(dictMeta.merge.type === 'some' ? dictMeta.merge.value : new Uint8Array());
|
|
387
|
-
const resolve = bundle.compile([]);
|
|
388
|
-
const left = { id: 1n, name: 'left' };
|
|
389
|
-
assert.deepStrictEqual(resolve(1n, left, { id: 1n, name: 'right' }), left);
|
|
390
|
-
const ids = partitionTask('merge_ids', {
|
|
391
|
-
partitions: [events],
|
|
392
|
-
output: SetType(IntegerType),
|
|
393
|
-
merge: 'union',
|
|
394
|
-
}, ($, slice) => $.return(slice.keys()));
|
|
395
|
-
const setMeta = decodePartitionTaskMetadata(ids.metadata);
|
|
396
|
-
assert.strictEqual(setMeta.merge.type, 'none');
|
|
397
|
-
assert.strictEqual(setMeta.mergeSets, true);
|
|
398
|
-
});
|
|
399
|
-
it('carries the merge command of the task\'s runner, built at export, and none without merge', () => {
|
|
400
|
-
// The orchestrator's merge units run this command IR as an ordinary
|
|
401
|
-
// execution: `<runner> merge --merge <merge IR> --range <range> -i
|
|
402
|
-
// <partial>... -o <out>` for a Dict output, `--union` for a Set — the
|
|
403
|
-
// key range is the input after the merge IR, then the partials.
|
|
404
|
-
const byId = partitionTask('merge_cmd_by_id', {
|
|
405
|
-
partitions: [events],
|
|
406
|
-
output: DictType(IntegerType, RowType),
|
|
407
|
-
merge: ($, _key, a, _b) => $.return(a),
|
|
408
|
-
runner: { runtime: 'east-c', platforms: ['east-c-std'] },
|
|
409
|
-
}, ($, slice) => $.return(slice));
|
|
410
|
-
const dictMeta = decodePartitionTaskMetadata(byId.metadata);
|
|
411
|
-
assert.strictEqual(dictMeta.mergeCommand.type, 'some');
|
|
412
|
-
const dictCommand = decodeEastIR(dictMeta.mergeCommand.type === 'some' ? dictMeta.mergeCommand.value : new Uint8Array());
|
|
413
|
-
assert.deepStrictEqual(dictCommand.compile([])(['merge.beast2', 'range.beast2', 'p0.beast2', 'p1.beast2'], 'out.beast2'), ['east-c', 'merge', '-p', 'east-c-std', '--merge', 'merge.beast2', '--range', 'range.beast2', '-i', 'p0.beast2', '-i', 'p1.beast2', '-o', 'out.beast2']);
|
|
414
|
-
const ids = partitionTask('merge_cmd_ids', {
|
|
415
|
-
partitions: [events],
|
|
416
|
-
output: SetType(IntegerType),
|
|
417
|
-
merge: 'union',
|
|
418
|
-
}, ($, slice) => $.return(slice.keys()));
|
|
419
|
-
const setMeta = decodePartitionTaskMetadata(ids.metadata);
|
|
420
|
-
const setCommand = decodeEastIR(setMeta.mergeCommand.type === 'some' ? setMeta.mergeCommand.value : new Uint8Array());
|
|
421
|
-
assert.deepStrictEqual(setCommand.compile([])(['range.beast2', 'p0.beast2', 'p1.beast2'], 'out.beast2'), ['east-node', 'merge', '-p', '@elaraai/east-node-std', '--union', '--range', 'range.beast2', '-i', 'p0.beast2', '-i', 'p1.beast2', '-o', 'out.beast2']);
|
|
422
|
-
const spliced = partitionTask('merge_cmd_none', {
|
|
423
|
-
partitions: [events],
|
|
424
|
-
output: DictType(IntegerType, RowType),
|
|
425
|
-
}, ($, slice) => $.return(slice));
|
|
426
|
-
assert.strictEqual(decodePartitionTaskMetadata(spliced.metadata).mergeCommand.type, 'none');
|
|
117
|
+
describe('customTask', () => {
|
|
118
|
+
it('carries its command as the body, with no runner', () => {
|
|
119
|
+
const name_input = input('name', StringType, variant('value', 'World'));
|
|
120
|
+
const echo = customTask('echo', [name_input], StringType, (_$, inputs, out) => East.str `cp ${inputs.get(0n)} ${out}`);
|
|
121
|
+
assert.strictEqual(echo.runner, undefined);
|
|
122
|
+
assert.deepStrictEqual(echo.inputs, [name_input]);
|
|
123
|
+
assert.strictEqual(echo.body.kind, 'command');
|
|
124
|
+
if (echo.body.kind !== 'command')
|
|
125
|
+
return;
|
|
126
|
+
assert.deepStrictEqual(echo.body.command.compile([])(['in.beast2'], 'out.beast2'), ['bash', '-c', 'cp in.beast2 out.beast2']);
|
|
427
127
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
assert.
|
|
441
|
-
|
|
442
|
-
|
|
128
|
+
});
|
|
129
|
+
describe('e3.output', () => {
|
|
130
|
+
it('types each kind\'s output and emit', () => {
|
|
131
|
+
const array = output.array(FloatType);
|
|
132
|
+
assert.ok(sameType(array.type, ArrayType(FloatType)));
|
|
133
|
+
assert.ok(sameType(array.emit, FunctionType([FloatType], NullType)));
|
|
134
|
+
const set = output.set(StringType);
|
|
135
|
+
assert.ok(sameType(set.type, SetType(StringType)));
|
|
136
|
+
assert.ok(sameType(set.emit, FunctionType([StringType], NullType)));
|
|
137
|
+
const dict = output.dict(StringType, IntegerType);
|
|
138
|
+
assert.ok(sameType(dict.type, DictType(StringType, IntegerType)));
|
|
139
|
+
assert.ok(sameType(dict.emit, FunctionType([StringType, IntegerType], NullType)));
|
|
140
|
+
assert.strictEqual(dict.merge, undefined);
|
|
141
|
+
// A fold's output is the folded value, whatever its type.
|
|
142
|
+
const fold = output.fold(ArrayType(IntegerType), { zero: [], combine: ($, a, b) => a.concat(b) });
|
|
143
|
+
assert.ok(sameType(fold.type, ArrayType(IntegerType)));
|
|
144
|
+
assert.ok(sameType(fold.emit, FunctionType([ArrayType(IntegerType)], NullType)));
|
|
443
145
|
});
|
|
444
|
-
it('
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const bySku = partitionTask('merge_by_sku', {
|
|
451
|
-
partitions: [sales],
|
|
452
|
-
output: DictType(StringType, IntegerType),
|
|
453
|
-
merge: (_$, _sku, a, b) => a.add(b),
|
|
454
|
-
}, ($, slice) => slice.toArray(($, v, k) => ({ sku: k.sku, v }))
|
|
455
|
-
.groupReduce(($, x) => x.sku, ($, _k) => 0n, ($, acc, x) => acc.add(x.v)));
|
|
456
|
-
const meta = decodePartitionTaskMetadata(bySku.metadata);
|
|
457
|
-
const merge = decodeEastIR(meta.merge.type === 'some' ? meta.merge.value : new Uint8Array());
|
|
458
|
-
assert.strictEqual(merge.ir.value.parameters.length, 3);
|
|
459
|
-
assert.strictEqual(merge.compile([])('x', 2n, 3n), 5n);
|
|
146
|
+
it('builds a dict\'s merge over the key and two values, and a fold\'s combine over two values', () => {
|
|
147
|
+
const dict = output.dict(StringType, IntegerType, { merge: (_$, _key, a, b) => a.add(b) });
|
|
148
|
+
assert.strictEqual(dict.merge.compile([])('x', 2n, 3n), 5n);
|
|
149
|
+
const fold = output.fold(IntegerType, { zero: 0n, combine: (_$, a, b) => a.add(b) });
|
|
150
|
+
assert.strictEqual(fold.zero, 0n);
|
|
151
|
+
assert.strictEqual(fold.combine.compile([])(2n, 3n), 5n);
|
|
460
152
|
});
|
|
461
|
-
it('refuses
|
|
462
|
-
assert.throws(() =>
|
|
463
|
-
partitions: [events],
|
|
464
|
-
output: DictType(IntegerType, RowType),
|
|
465
|
-
merge: ($, _key, a, _b) => $.return(a),
|
|
466
|
-
runner: { runtime: 'custom', command: ['my-runner'] },
|
|
467
|
-
}, ($, slice) => $.return(slice)), /^Error: partitionTask 'merge_custom': merge runs the fan-in on the task's runner, which must be a stock runtime \(east-node, east-py, east-c\) — the custom runtime has no merge command$/);
|
|
153
|
+
it('refuses a fold whose zero is not a value of its type', () => {
|
|
154
|
+
assert.throws(() => output.fold(IntegerType, { zero: 'none', combine: (_$, a, b) => a.add(b) }), /^Error: e3\.output\.fold: zero is not a value of \.Integer$/);
|
|
468
155
|
});
|
|
469
156
|
});
|
|
470
157
|
describe('streamTask', () => {
|
|
471
158
|
const EventType = StructType({ at: IntegerType, amount: FloatType });
|
|
472
|
-
it('
|
|
159
|
+
it('passes the inputs, then emit, to a body whose output kind types the task', () => {
|
|
473
160
|
const events = input('events', ArrayType(EventType));
|
|
474
161
|
const balances = streamTask('balances', {
|
|
475
|
-
|
|
476
|
-
output:
|
|
162
|
+
inputs: [events],
|
|
163
|
+
output: output.array(FloatType),
|
|
477
164
|
}, ($, events, emit) => {
|
|
478
165
|
const balance = $.let(0.0);
|
|
479
166
|
$.for(events, ($, event) => {
|
|
@@ -482,119 +169,117 @@ describe('streamTask', () => {
|
|
|
482
169
|
});
|
|
483
170
|
});
|
|
484
171
|
assert.strictEqual(balances.kind, 'task');
|
|
485
|
-
assert.
|
|
486
|
-
|
|
487
|
-
assert.
|
|
488
|
-
assert.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
assert.strictEqual(
|
|
492
|
-
|
|
493
|
-
|
|
172
|
+
assert.deepStrictEqual(balances.inputs, [events]);
|
|
173
|
+
assert.strictEqual(balances.outputKind?.kind, 'array');
|
|
174
|
+
assert.ok(sameType(balances.output.type, ArrayType(FloatType)));
|
|
175
|
+
assert.deepStrictEqual(balances.role, variant('data', null));
|
|
176
|
+
// The body's parameters are the inputs and then emit, which the runner
|
|
177
|
+
// passes: here, a sink recording what is emitted.
|
|
178
|
+
assert.strictEqual(balances.body.kind, 'east');
|
|
179
|
+
if (balances.body.kind !== 'east')
|
|
180
|
+
return;
|
|
181
|
+
const emitted = [];
|
|
182
|
+
const run = balances.body.program.compile([]);
|
|
183
|
+
run([{ at: 1n, amount: 2.5 }, { at: 2n, amount: 1.0 }], (x) => { emitted.push(x); return null; });
|
|
184
|
+
assert.deepStrictEqual(emitted, [2.5, 3.5]);
|
|
494
185
|
});
|
|
495
|
-
it('
|
|
496
|
-
const limit = input('limit', IntegerType, variant('value', 3n));
|
|
186
|
+
it('is a producer with no inputs, emitting keys and values into a dict', () => {
|
|
497
187
|
const producer = streamTask('ingest', {
|
|
498
|
-
inputs: [
|
|
499
|
-
output:
|
|
500
|
-
}, ($,
|
|
501
|
-
const source = $.const([0n, 1n, 2n
|
|
188
|
+
inputs: [],
|
|
189
|
+
output: output.dict(IntegerType, StringType),
|
|
190
|
+
}, ($, emit) => {
|
|
191
|
+
const source = $.const([0n, 1n, 2n], ArrayType(IntegerType));
|
|
502
192
|
$.for(source, ($, k) => {
|
|
503
|
-
|
|
504
|
-
$(emit(k, East.str `row-${k}`));
|
|
505
|
-
});
|
|
193
|
+
$(emit(k, East.str `row-${k}`));
|
|
506
194
|
});
|
|
507
195
|
});
|
|
508
|
-
assert.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
196
|
+
assert.deepStrictEqual(producer.inputs, []);
|
|
197
|
+
assert.strictEqual(producer.outputKind?.kind, 'dict');
|
|
198
|
+
if (producer.body.kind !== 'east')
|
|
199
|
+
return assert.fail('a stream task runs a program');
|
|
200
|
+
const emitted = [];
|
|
201
|
+
producer.body.program.compile([])((k, v) => { emitted.push([k, v]); return null; });
|
|
202
|
+
assert.deepStrictEqual(emitted, [[0n, 'row-0'], [1n, 'row-1'], [2n, 'row-2']]);
|
|
514
203
|
});
|
|
515
|
-
it('
|
|
204
|
+
it('refuses the custom runtime, and an output that is not an output kind', () => {
|
|
516
205
|
const events = input('events', ArrayType(EventType));
|
|
517
|
-
assert.throws(() => streamTask('scalar_out', {
|
|
518
|
-
stream: events,
|
|
519
|
-
output: FloatType,
|
|
520
|
-
}, () => { }), /output must be a collection type/);
|
|
521
206
|
assert.throws(() => streamTask('custom_stream', {
|
|
522
|
-
|
|
523
|
-
output:
|
|
207
|
+
inputs: [events],
|
|
208
|
+
output: output.array(FloatType),
|
|
524
209
|
runner: { runtime: 'custom', command: ['my-runner'] },
|
|
525
|
-
}, () => { }),
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
runner: { runtime: 'east-c', platforms: ['east-c-std'] },
|
|
531
|
-
}, ($, events, emit) => {
|
|
532
|
-
$.for(events, ($, event) => {
|
|
533
|
-
$(emit(event.amount));
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
assert.strictEqual(cStream.taskKind, TASK_KIND_STREAM);
|
|
210
|
+
}, () => { }), /^Error: streamTask 'custom_stream': the custom runtime runs only a program that returns its output — use a stock runtime \(east-node, east-py, east-c\)$/);
|
|
211
|
+
// The static types forbid a bare East type; a caller outside TypeScript's
|
|
212
|
+
// reach still gets a message naming the task.
|
|
213
|
+
const untypedStreamTask = streamTask;
|
|
214
|
+
assert.throws(() => untypedStreamTask('scalar_out', { inputs: [events], output: FloatType }, () => { }), /^Error: streamTask 'scalar_out': output is an output kind — e3\.output\.array, set, dict or fold$/);
|
|
537
215
|
});
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
// function_ir, merge_ir, the stream, then the ordinary input.
|
|
555
|
-
assert.deepStrictEqual(totals.inputs.map((d) => d.name), ['function_ir', 'merge_ir', 'merge_stream_events', 'merge_stream_rates']);
|
|
556
|
-
const mergeIR = totals.inputs[1];
|
|
557
|
-
assert.deepStrictEqual(mergeIR.path, [variant('field', 'tasks'), variant('field', 'totals'), variant('field', 'merge_ir')]);
|
|
558
|
-
assert.strictEqual(mergeIR.writable, false);
|
|
559
|
-
const fold = mergeIR.default.compile([]);
|
|
560
|
-
assert.strictEqual(fold('x', 1.5, 2.0), 3.5);
|
|
561
|
-
const meta = decodeStreamTaskMetadata(totals.metadata);
|
|
562
|
-
assert.deepStrictEqual(meta, { stream: true, emit: 'dict', merge: 'function' });
|
|
563
|
-
// The runner receives the merge IR as --merge and streams the first -i input.
|
|
564
|
-
const argv = totals.command.compile([])(['body.beast2', 'merge.beast2', 'events.beast2', 'rates.beast2'], 'out.beast2');
|
|
565
|
-
assert.deepStrictEqual(argv, [
|
|
566
|
-
'east-c', 'run', '-p', 'east-c-std', '--emit', 'dict', '--merge', 'merge.beast2', '--stream', '0',
|
|
567
|
-
'-i', 'events.beast2', '-i', 'rates.beast2', '-o', 'out.beast2', 'body.beast2',
|
|
568
|
-
]);
|
|
569
|
-
});
|
|
570
|
-
it('collapses equal Set elements with union, which stages no input', () => {
|
|
571
|
-
const events = input('union_stream_events', ArrayType(StringType));
|
|
572
|
-
const accounts = streamTask('accounts', {
|
|
573
|
-
stream: events,
|
|
574
|
-
output: SetType(StringType),
|
|
575
|
-
merge: 'union',
|
|
576
|
-
}, ($, events, emit) => {
|
|
577
|
-
$.for(events, ($, account) => {
|
|
578
|
-
$(emit(account));
|
|
579
|
-
});
|
|
216
|
+
});
|
|
217
|
+
describe('e3.partition', () => {
|
|
218
|
+
const SaleKeyType = StructType({ sku: StringType, period: IntegerType });
|
|
219
|
+
const NestedKeyType = StructType({ head: StructType({ region: StringType, store: IntegerType }), seq: IntegerType });
|
|
220
|
+
it('marks a stream task\'s input, which its body receives typed as the whole dataset', () => {
|
|
221
|
+
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
222
|
+
const rates = input('rates', FloatType, variant('value', 1.0));
|
|
223
|
+
const bySku = partition(sales, { by: ['sku'] });
|
|
224
|
+
const totals = streamTask('totals', {
|
|
225
|
+
inputs: [bySku, rates],
|
|
226
|
+
output: output.dict(StringType, FloatType, { merge: (_$, _sku, a, b) => a.add(b) }),
|
|
227
|
+
}, ($, sales, rate, emit) => {
|
|
228
|
+
$.for(sales, ($, qty, key) => {
|
|
229
|
+
$(emit(key.sku, qty.toFloat().multiply(rate)));
|
|
580
230
|
});
|
|
581
|
-
assert.deepStrictEqual(accounts.inputs.map((d) => d.name), ['function_ir', 'union_stream_events']);
|
|
582
|
-
assert.deepStrictEqual(decodeStreamTaskMetadata(accounts.metadata), { stream: true, emit: 'set', merge: 'union' });
|
|
583
|
-
const argv = accounts.command.compile([])(['body.beast2', 'events.beast2'], 'out.beast2');
|
|
584
|
-
assert.deepStrictEqual(argv, [
|
|
585
|
-
'east-node', 'run', '-p', '@elaraai/east-node-std', '--emit', 'set', '--union', '--stream', '0',
|
|
586
|
-
'-i', 'events.beast2', '-o', 'out.beast2', 'body.beast2',
|
|
587
|
-
]);
|
|
588
|
-
});
|
|
589
|
-
it('refuses merge for an Array output and in the wrong form for the output kind', () => {
|
|
590
|
-
// The static types already forbid both; a caller outside TypeScript's
|
|
591
|
-
// reach still gets a message naming the task.
|
|
592
|
-
const untypedStreamTask = streamTask;
|
|
593
|
-
const takeLeft = (_$, _k, a) => a;
|
|
594
|
-
assert.throws(() => untypedStreamTask('merge_array_out', { output: ArrayType(FloatType), merge: takeLeft }, () => { }), /^Error: streamTask 'merge_array_out': merge applies to Dict \(a function\) or Set \('union'\) outputs, got Array$/);
|
|
595
|
-
assert.throws(() => untypedStreamTask('union_on_dict', { output: TotalsType, merge: 'union' }, () => { }), /streamTask 'union_on_dict': merge applies to Dict \(a function\) or Set \('union'\) outputs, got Dict with 'union'/);
|
|
596
|
-
assert.throws(() => untypedStreamTask('fn_on_set', { output: SetType(StringType), merge: takeLeft }, () => { }), /streamTask 'fn_on_set': merge applies to Dict \(a function\) or Set \('union'\) outputs, got Set with a function/);
|
|
597
231
|
});
|
|
232
|
+
assert.deepStrictEqual(bySku, { kind: 'partition', dataset: sales, by: ['sku'] });
|
|
233
|
+
assert.deepStrictEqual(totals.inputs, [bySku, rates]);
|
|
234
|
+
// The task depends on the dataset under the mark.
|
|
235
|
+
assert.ok(totals.deps.has(sales));
|
|
236
|
+
assert.ok(totals.deps.has(rates));
|
|
237
|
+
});
|
|
238
|
+
it('accepts leading key fields, the last of which may read first fields into a struct', () => {
|
|
239
|
+
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
240
|
+
const nested = input('nested', DictType(NestedKeyType, IntegerType));
|
|
241
|
+
const body = () => { };
|
|
242
|
+
for (const by of [[], ['sku'], ['sku', 'period']]) {
|
|
243
|
+
streamTask('ok', { inputs: [partition(sales, { by })], output: output.set(StringType) }, body);
|
|
244
|
+
}
|
|
245
|
+
for (const by of [['head'], ['head.region'], ['head', 'seq']]) {
|
|
246
|
+
streamTask('ok', { inputs: [partition(nested, { by })], output: output.set(StringType) }, body);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
it('refuses a by that is not leading key fields, naming the task, the input and the key\'s fields', () => {
|
|
250
|
+
const sales = input('sales', DictType(SaleKeyType, IntegerType));
|
|
251
|
+
const nested = input('nested', DictType(NestedKeyType, IntegerType));
|
|
252
|
+
const body = () => { };
|
|
253
|
+
assert.throws(() => streamTask('bad_by', { inputs: [partition(sales, { by: ['period'] })], output: output.set(StringType) }, body), /^Error: streamTask 'bad_by': partitioned input 'sales': `by` \(period\) must name leading key fields in order — the key's fields are \(sku, period\), and only the last entry may read into one, as 'at\.day' does$/);
|
|
254
|
+
assert.throws(() => streamTask('path_first', { inputs: [partition(nested, { by: ['head.region', 'seq'] })], output: output.set(StringType) }, body), /`by` \(head\.region, seq\) must name leading key fields in order/);
|
|
255
|
+
assert.throws(() => streamTask('path_step', { inputs: [partition(nested, { by: ['head.store'] })], output: output.set(StringType) }, body), /^Error: streamTask 'path_step': partitioned input 'nested': `by` path 'head\.store' reads 'store', which is not the first field of \(region, store\) — rows sort by a struct's first field, so only it groups them$/);
|
|
256
|
+
});
|
|
257
|
+
it('refuses a partitioned input that is not a collection, or a by on an input with no key fields', () => {
|
|
258
|
+
const scalar = input('scalar', IntegerType, variant('value', 1n));
|
|
259
|
+
const rows = input('rows', ArrayType(IntegerType));
|
|
260
|
+
const skus = input('skus', SetType(StringType));
|
|
261
|
+
const body = () => { };
|
|
262
|
+
assert.throws(() => streamTask('scalar', { inputs: [partition(scalar)], output: output.set(StringType) }, body), /^Error: streamTask 'scalar': partitioned input 'scalar' must be a collection \(Array, Set or Dict\), got Integer$/);
|
|
263
|
+
assert.throws(() => streamTask('array_by', { inputs: [partition(rows, { by: ['x'] })], output: output.set(StringType) }, body), /partitioned input 'rows' is an Array, cut by position, so it has no key for `by` to name/);
|
|
264
|
+
assert.throws(() => streamTask('string_by', { inputs: [partition(skus, { by: ['x'] })], output: output.set(StringType) }, body), /partitioned input 'skus' has a String key, which has no fields for `by` to name/);
|
|
265
|
+
// An Array cut by position, with no `by`, is a partitioned input.
|
|
266
|
+
streamTask('array', { inputs: [partition(rows)], output: output.array(IntegerType) }, body);
|
|
267
|
+
});
|
|
268
|
+
it('co-partitions inputs cut by keys, or by fields, of the same types — and nothing else', () => {
|
|
269
|
+
const a = input('a', DictType(SaleKeyType, IntegerType));
|
|
270
|
+
const b = input('b', DictType(SaleKeyType, FloatType));
|
|
271
|
+
const bySkuOnly = input('by_sku', DictType(StringType, FloatType));
|
|
272
|
+
const lines = input('lines', DictType(StructType({ sku: StringType, line: IntegerType }), IntegerType));
|
|
273
|
+
const reversed = input('reversed', DictType(StructType({ period: IntegerType, sku: StringType }), IntegerType));
|
|
274
|
+
const rows = input('rows', ArrayType(IntegerType));
|
|
275
|
+
const body = () => { };
|
|
276
|
+
// Identical keys; a key against a `by` field of its type; two `by`s over
|
|
277
|
+
// different keys.
|
|
278
|
+
streamTask('same_keys', { inputs: [partition(a), partition(b)], output: output.set(StringType) }, body);
|
|
279
|
+
streamTask('key_and_by', { inputs: [partition(bySkuOnly), partition(a, { by: ['sku'] })], output: output.set(StringType) }, body);
|
|
280
|
+
streamTask('two_bys', { inputs: [partition(a, { by: ['sku'] }), partition(lines, { by: ['sku'] })], output: output.set(StringType) }, body);
|
|
281
|
+
assert.throws(() => streamTask('reordered', { inputs: [partition(a), partition(reversed)], output: output.set(StringType) }, body), /^Error: streamTask 'reordered': co-partitioned inputs 'a' and 'reversed' have no common key — they are cut by \(\.Struct \[\(name="sku", type=\.String\), \(name="period", type=\.Integer\)\]\) and \(\.Struct \[\(name="period", type=\.Integer\), \(name="sku", type=\.String\)\]\); give each a `by` naming fields of the same types$/);
|
|
282
|
+
assert.throws(() => streamTask('with_array', { inputs: [partition(a), partition(rows)], output: output.set(StringType) }, body), /^Error: streamTask 'with_array': co-partitioned inputs are cut at the same keys, so each must be a Set or a Dict — partition one input and pass the others whole$/);
|
|
598
283
|
});
|
|
599
284
|
});
|
|
600
285
|
//# sourceMappingURL=task.spec.js.map
|