@elaraai/e3-types 1.0.76 → 1.0.78
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/src/api.d.ts +25 -0
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +18 -0
- package/dist/src/api.js.map +1 -1
- package/dist/src/dataflow.d.ts +10 -6
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js.map +1 -1
- package/dist/src/dataset-type.d.ts +71 -0
- package/dist/src/dataset-type.d.ts.map +1 -0
- package/dist/src/dataset-type.js +101 -0
- package/dist/src/dataset-type.js.map +1 -0
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.d.ts.map +1 -1
- package/dist/src/index.d.ts +8 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/package.d.ts +33 -3
- package/dist/src/package.d.ts.map +1 -1
- package/dist/src/package.js +47 -8
- package/dist/src/package.js.map +1 -1
- package/dist/src/runner.d.ts +35 -9
- package/dist/src/runner.d.ts.map +1 -1
- package/dist/src/runner.js +46 -13
- package/dist/src/runner.js.map +1 -1
- package/dist/src/runner.spec.js +50 -1
- package/dist/src/runner.spec.js.map +1 -1
- package/dist/src/stream.d.ts +97 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +134 -0
- package/dist/src/stream.js.map +1 -0
- package/dist/src/stream.spec.d.ts +6 -0
- package/dist/src/stream.spec.d.ts.map +1 -0
- package/dist/src/stream.spec.js +60 -0
- package/dist/src/stream.spec.js.map +1 -0
- package/dist/src/task.d.ts +198 -10
- package/dist/src/task.d.ts.map +1 -1
- package/dist/src/task.js +340 -11
- package/dist/src/task.js.map +1 -1
- package/dist/src/task.spec.d.ts +6 -0
- package/dist/src/task.spec.d.ts.map +1 -0
- package/dist/src/task.spec.js +169 -0
- package/dist/src/task.spec.js.map +1 -0
- package/dist/src/transfer.d.ts +70 -3
- package/dist/src/transfer.d.ts.map +1 -1
- package/dist/src/transfer.js +76 -3
- package/dist/src/transfer.js.map +1 -1
- package/dist/src/transfer.spec.d.ts +6 -0
- package/dist/src/transfer.spec.d.ts.map +1 -0
- package/dist/src/transfer.spec.js +75 -0
- package/dist/src/transfer.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/api.ts +18 -0
- package/src/dataflow.ts +10 -6
- package/src/dataset-type.ts +134 -0
- package/src/execution.ts +18 -0
- package/src/index.ts +37 -0
- package/src/package.ts +50 -8
- package/src/runner.spec.ts +56 -1
- package/src/runner.ts +45 -13
- package/src/stream.spec.ts +88 -0
- package/src/stream.ts +170 -0
- package/src/task.spec.ts +208 -0
- package/src/task.ts +374 -13
- package/src/transfer.spec.ts +98 -0
- package/src/transfer.ts +84 -3
package/src/task.spec.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Partition and stream task wire helpers (issue #770): the partition
|
|
8
|
+
* metadata's merge fields and its triple decoder, the stream metadata's
|
|
9
|
+
* `merge` mode and its dual decoder, the partition plan round trip, and the
|
|
10
|
+
* `by` projection shapes the orchestrator evaluates by reading key fields
|
|
11
|
+
* instead of compiling the projection.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { ArrayType, BlobType, BooleanType, East, IntegerType, OptionType, StringType, StructType, compareFor, encodeBeast2For, encodeEastIR, isTypeValueEqual, none, some, toEastTypeValue, variant } from '@elaraai/east';
|
|
17
|
+
import type { FunctionIR } from '@elaraai/east';
|
|
18
|
+
import {
|
|
19
|
+
decodePartitionPlan,
|
|
20
|
+
decodePartitionTaskMetadata,
|
|
21
|
+
decodeStreamTaskMetadata,
|
|
22
|
+
encodePartitionPlan,
|
|
23
|
+
encodePartitionTaskMetadata,
|
|
24
|
+
encodeStreamTaskMetadata,
|
|
25
|
+
partitionProjectionShape,
|
|
26
|
+
projectKey,
|
|
27
|
+
projectedKeyType,
|
|
28
|
+
type PartitionPlan,
|
|
29
|
+
type PartitionTaskMetadata,
|
|
30
|
+
type ProjectionShape,
|
|
31
|
+
type StreamTaskMetadata,
|
|
32
|
+
} from './task.js';
|
|
33
|
+
import { mergeCommandIr } from './stream.js';
|
|
34
|
+
|
|
35
|
+
describe('PartitionTaskMetadataType', () => {
|
|
36
|
+
const mergeIr = encodeEastIR(East.function([IntegerType, StringType, StringType], StringType, (_$, _key, acc, value) => acc.concat(value)).toIR());
|
|
37
|
+
const mergeCommand = encodeEastIR(mergeCommandIr(variant('east_c', { platforms: ['east-c-std'] }), 'function'));
|
|
38
|
+
|
|
39
|
+
/** The metadata with every blob as a plain Uint8Array — the decoder hands
|
|
40
|
+
* back Buffers, whose prototype strict deep equality would reject. */
|
|
41
|
+
function plain(meta: PartitionTaskMetadata): PartitionTaskMetadata {
|
|
42
|
+
const blob = (option: { type: string; value: unknown }) =>
|
|
43
|
+
option.type === 'some' ? some(new Uint8Array(option.value as Uint8Array)) : none;
|
|
44
|
+
return { ...meta, by: blob(meta.by), combine: blob(meta.combine), merge: blob(meta.merge), mergeCommand: blob(meta.mergeCommand) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('round-trips the merge fields and the merge command', () => {
|
|
48
|
+
const meta: PartitionTaskMetadata = {
|
|
49
|
+
partitions: 1n, by: none, combine: none, targetPartitionBytes: 1024n,
|
|
50
|
+
merge: some(mergeIr), mergeSets: false, mergeCommand: some(mergeCommand),
|
|
51
|
+
};
|
|
52
|
+
assert.deepEqual(plain(decodePartitionTaskMetadata(encodePartitionTaskMetadata(meta))), meta);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('decodes v1.0.77 metadata, exported before merge commands, with mergeCommand none', () => {
|
|
56
|
+
const shape = StructType({
|
|
57
|
+
partitions: IntegerType, by: OptionType(BlobType), combine: OptionType(BlobType), targetPartitionBytes: IntegerType,
|
|
58
|
+
merge: OptionType(BlobType), mergeSets: BooleanType,
|
|
59
|
+
});
|
|
60
|
+
const legacy = encodeBeast2For(shape)({ partitions: 2n, by: none, combine: none, targetPartitionBytes: 512n, merge: some(mergeIr), mergeSets: false });
|
|
61
|
+
assert.deepEqual(plain(decodePartitionTaskMetadata(legacy)), {
|
|
62
|
+
partitions: 2n, by: none, combine: none, targetPartitionBytes: 512n, merge: some(mergeIr), mergeSets: false, mergeCommand: none,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('decodes metadata exported before merge assembly with every merge field off', () => {
|
|
67
|
+
const shape = StructType({ partitions: IntegerType, by: OptionType(BlobType), combine: OptionType(BlobType), targetPartitionBytes: IntegerType });
|
|
68
|
+
const legacy = encodeBeast2For(shape)({ partitions: 1n, by: none, combine: none, targetPartitionBytes: 256n });
|
|
69
|
+
assert.deepEqual(decodePartitionTaskMetadata(legacy), {
|
|
70
|
+
partitions: 1n, by: none, combine: none, targetPartitionBytes: 256n, merge: none, mergeSets: false, mergeCommand: none,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws for bytes of no known metadata shape', () => {
|
|
75
|
+
assert.throws(() => decodePartitionTaskMetadata(encodeBeast2For(IntegerType)(7n)));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('StreamTaskMetadataType', () => {
|
|
80
|
+
it('round-trips the merge mode', () => {
|
|
81
|
+
const meta: StreamTaskMetadata = { stream: true, emit: 'dict', merge: 'function' };
|
|
82
|
+
assert.deepEqual(decodeStreamTaskMetadata(encodeStreamTaskMetadata(meta)), meta);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('decodes metadata exported before merge modes as merge none', () => {
|
|
86
|
+
const legacy = encodeBeast2For(StructType({ stream: BooleanType, emit: StringType }))({ stream: false, emit: 'set' });
|
|
87
|
+
assert.deepEqual(decodeStreamTaskMetadata(legacy), { stream: false, emit: 'set', merge: 'none' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('throws for bytes of no known metadata shape', () => {
|
|
91
|
+
assert.throws(() => decodeStreamTaskMetadata(encodeBeast2For(IntegerType)(7n)));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('PartitionPlanType', () => {
|
|
96
|
+
it('round-trips a plan, before and after its slices are carved and its merge ranges planned', () => {
|
|
97
|
+
const plan: PartitionPlan = {
|
|
98
|
+
partitions: ['a'.repeat(64), 'b'.repeat(64)],
|
|
99
|
+
boundaries: [0n, 4n, 9n],
|
|
100
|
+
splits: [[{ seg: 0n, offset: 0n }, { seg: 3n, offset: 17n }, { seg: 8n, offset: 0n }, { seg: 12n, offset: 0n }]],
|
|
101
|
+
slices: [],
|
|
102
|
+
merges: [],
|
|
103
|
+
};
|
|
104
|
+
assert.deepEqual(decodePartitionPlan(encodePartitionPlan(plan)), plan);
|
|
105
|
+
|
|
106
|
+
const carved: PartitionPlan = { ...plan, slices: [['c'.repeat(64), 'd'.repeat(64), 'e'.repeat(64)], ['f'.repeat(64), '0'.repeat(64), '1'.repeat(64)]] };
|
|
107
|
+
assert.deepEqual(decodePartitionPlan(encodePartitionPlan(carved)), carved);
|
|
108
|
+
|
|
109
|
+
const merged: PartitionPlan = {
|
|
110
|
+
...carved,
|
|
111
|
+
merges: [{ partials: ['2'.repeat(64), '3'.repeat(64)], ranges: ['4'.repeat(64), '5'.repeat(64)] }],
|
|
112
|
+
};
|
|
113
|
+
assert.deepEqual(decodePartitionPlan(encodePartitionPlan(merged)), merged);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('decodes a plan recorded before merge ranges existed, with none', () => {
|
|
117
|
+
// The pre-`merges` wire shape, encoded directly.
|
|
118
|
+
const PreMergesPlanType = StructType({
|
|
119
|
+
partitions: ArrayType(StringType),
|
|
120
|
+
boundaries: ArrayType(IntegerType),
|
|
121
|
+
splits: ArrayType(ArrayType(StructType({ seg: IntegerType, offset: IntegerType }))),
|
|
122
|
+
slices: ArrayType(ArrayType(StringType)),
|
|
123
|
+
});
|
|
124
|
+
const older = encodeBeast2For(PreMergesPlanType)({
|
|
125
|
+
partitions: ['a'.repeat(64)],
|
|
126
|
+
boundaries: [0n, 2n],
|
|
127
|
+
splits: [],
|
|
128
|
+
slices: [['b'.repeat(64), 'c'.repeat(64)]],
|
|
129
|
+
});
|
|
130
|
+
assert.deepEqual(decodePartitionPlan(older), {
|
|
131
|
+
partitions: ['a'.repeat(64)],
|
|
132
|
+
boundaries: [0n, 2n],
|
|
133
|
+
splits: [],
|
|
134
|
+
slices: [['b'.repeat(64), 'c'.repeat(64)]],
|
|
135
|
+
merges: [],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('partition projections', () => {
|
|
141
|
+
const AtType = StructType({ day: IntegerType, hour: IntegerType });
|
|
142
|
+
const KeyType = StructType({ region: StringType, at: AtType, sku: StringType });
|
|
143
|
+
const keyTypeValue = toEastTypeValue(KeyType);
|
|
144
|
+
type Key = { region: string; at: { day: bigint; hour: bigint }; sku: string };
|
|
145
|
+
const keys: Key[] = [
|
|
146
|
+
{ region: 'eu', at: { day: 2n, hour: 7n }, sku: 'b' },
|
|
147
|
+
{ region: 'eu', at: { day: 2n, hour: 7n }, sku: 'a' },
|
|
148
|
+
{ region: 'eu', at: { day: 1n, hour: 9n }, sku: 'c' },
|
|
149
|
+
{ region: 'ap', at: { day: 3n, hour: 0n }, sku: 'a' },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/** A `by` projection over the key, compiled, and its shape. */
|
|
153
|
+
function projection(by: ($: any, key: any) => any): { compiled: (key: Key) => unknown; shape: ProjectionShape | null } {
|
|
154
|
+
const bundle = East.function([KeyType], undefined, by).toIR();
|
|
155
|
+
return {
|
|
156
|
+
compiled: bundle.compile([]) as (key: Key) => unknown,
|
|
157
|
+
shape: partitionProjectionShape(bundle.ir as FunctionIR),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether two comparisons agree in sign. */
|
|
162
|
+
const sameSign = (a: number, b: number): boolean => Math.sign(a) === Math.sign(b);
|
|
163
|
+
|
|
164
|
+
it('reads the identity, a leading field, a struct literal of leading fields and a nested first-field path', () => {
|
|
165
|
+
assert.deepEqual(projection(($, key) => key).shape, { kind: 'fields', names: [] });
|
|
166
|
+
assert.deepEqual(projection(($, key) => key.region).shape, { kind: 'fields', names: ['region'] });
|
|
167
|
+
assert.deepEqual(projection(($, key) => ({ region: key.region, at: key.at })).shape, { kind: 'fields', names: ['region', 'at'] });
|
|
168
|
+
assert.deepEqual(projection(($, key) => key.at.day).shape, { kind: 'path', names: ['at', 'day'] });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns null for a projection that computes', () => {
|
|
172
|
+
assert.equal(projection(($, key) => key.region.concat('-')).shape, null);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('projects the identity to the key itself', () => {
|
|
176
|
+
const shape: ProjectionShape = { kind: 'fields', names: [] };
|
|
177
|
+
assert.ok(isTypeValueEqual(projectedKeyType(shape, keyTypeValue), keyTypeValue));
|
|
178
|
+
assert.equal(projectKey(shape, keys[0]), keys[0]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('projects a struct literal of leading fields exactly as the compiled projection does', () => {
|
|
182
|
+
const { compiled, shape } = projection(($, key) => ({ region: key.region, at: key.at }));
|
|
183
|
+
assert.ok(isTypeValueEqual(projectedKeyType(shape!, keyTypeValue), toEastTypeValue(StructType({ region: StringType, at: AtType }))));
|
|
184
|
+
for (const key of keys) assert.deepEqual(projectKey(shape!, key), compiled(key));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('projects a nested first-field path exactly as the compiled projection does', () => {
|
|
188
|
+
const { compiled, shape } = projection(($, key) => key.at.day);
|
|
189
|
+
assert.ok(isTypeValueEqual(projectedKeyType(shape!, keyTypeValue), toEastTypeValue(IntegerType)));
|
|
190
|
+
for (const key of keys) assert.equal(projectKey(shape!, key), compiled(key));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('orders a single leading field as the compiled projection does', () => {
|
|
194
|
+
const { compiled, shape } = projection(($, key) => key.region);
|
|
195
|
+
const cmp = compareFor(projectedKeyType(shape!, keyTypeValue) as any) as (a: unknown, b: unknown) => number;
|
|
196
|
+
const cmpCompiled = compareFor(StringType);
|
|
197
|
+
for (const a of keys) {
|
|
198
|
+
for (const b of keys) {
|
|
199
|
+
assert.ok(sameSign(cmp(projectKey(shape!, a), projectKey(shape!, b)), cmpCompiled(compiled(a) as string, compiled(b) as string)));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('throws when a shape reads a field the key type does not have', () => {
|
|
205
|
+
assert.throws(() => projectedKeyType({ kind: 'fields', names: ['store'] }, keyTypeValue), /field 'store'/);
|
|
206
|
+
assert.throws(() => projectedKeyType({ kind: 'path', names: ['at', 'minute'] }, keyTypeValue), /field 'minute'/);
|
|
207
|
+
});
|
|
208
|
+
});
|
package/src/task.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* specified paths - the task just references locations, not types.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { StructType, StringType, ArrayType, BlobType, BooleanType, IntegerType, OptionType, ValueTypeOf, decodeBeast2For, encodeBeast2For, none } from '@elaraai/east';
|
|
19
|
+
import { StructType, StringType, ArrayType, BlobType, BooleanType, IntegerType, OptionType, ValueTypeOf, decodeBeast2For, encodeBeast2For, none, variant } from '@elaraai/east';
|
|
20
|
+
import type { EastTypeValue, FunctionIR } from '@elaraai/east';
|
|
20
21
|
import { TreePathType } from './structure.js';
|
|
21
22
|
import { RunnerType } from './runner.js';
|
|
22
23
|
|
|
@@ -141,6 +142,14 @@ export const TASK_KIND_PARTITION = 'partition';
|
|
|
141
142
|
* beast2-encoded {@link StreamTaskMetadataType}. */
|
|
142
143
|
export const TASK_KIND_STREAM = 'stream';
|
|
143
144
|
|
|
145
|
+
/** Task kind of a merge unit — the runner's `merge` command over sorted
|
|
146
|
+
* partials of a partitioned task's keyed output (issue #770). The
|
|
147
|
+
* orchestrator writes one such task per partitioned task from the
|
|
148
|
+
* package's {@link PartitionTaskMetadataType} `mergeCommand`, and runs it
|
|
149
|
+
* as an ordinary content-addressed execution per group of partials. It
|
|
150
|
+
* carries no metadata. */
|
|
151
|
+
export const TASK_KIND_MERGE = 'merge';
|
|
152
|
+
|
|
144
153
|
/**
|
|
145
154
|
* Metadata of a {@link TASK_KIND_PARTITION} task.
|
|
146
155
|
*
|
|
@@ -167,44 +176,396 @@ export const PartitionTaskMetadataType = StructType({
|
|
|
167
176
|
combine: OptionType(BlobType),
|
|
168
177
|
/** Target carved-slice size in wire bytes — the only sizing knob. */
|
|
169
178
|
targetPartitionBytes: IntegerType,
|
|
179
|
+
/**
|
|
180
|
+
* `encodeEastIR` bundle of the per-key merge `(Key, Value, Value) -> Value`
|
|
181
|
+
* for a Dict output; `none` otherwise.
|
|
182
|
+
*
|
|
183
|
+
* Its presence (or {@link mergeSets}) selects the MERGE-TREE assembly:
|
|
184
|
+
* partials whose key ranges overlap are merged by the task's own runner, in
|
|
185
|
+
* a tree of merge executions ({@link mergeCommand}) folding equal keys with
|
|
186
|
+
* this function; disjoint partials are spliced; the orchestrator never
|
|
187
|
+
* decodes a partial. Appended after `targetPartitionBytes` (BEAST2 encodes
|
|
188
|
+
* struct fields positionally) with a dual decoder — see
|
|
189
|
+
* {@link decodePartitionTaskMetadata}.
|
|
190
|
+
*/
|
|
191
|
+
merge: OptionType(BlobType),
|
|
192
|
+
/** Whether a Set output assembles by the merge tree, keeping one of equal
|
|
193
|
+
* elements (`--union`). The Set twin of {@link merge}, which needs no
|
|
194
|
+
* function. */
|
|
195
|
+
mergeSets: BooleanType,
|
|
196
|
+
/**
|
|
197
|
+
* `encodeEastIR` bundle of the merge command `(input_paths, output_path) ->
|
|
198
|
+
* argv` — `mergeCommandIr` over the task's runner, in `function` mode with
|
|
199
|
+
* {@link merge} and `union` mode with {@link mergeSets} — that the
|
|
200
|
+
* orchestrator's merge units execute; `none` in splice and combine modes.
|
|
201
|
+
* Built at export, so the merge unit task is a package object like any
|
|
202
|
+
* other and nothing is synthesized at run time. Appended LAST with a
|
|
203
|
+
* triple decoder — see {@link decodePartitionTaskMetadata}.
|
|
204
|
+
*/
|
|
205
|
+
mergeCommand: OptionType(BlobType),
|
|
170
206
|
});
|
|
171
207
|
export type PartitionTaskMetadataType = typeof PartitionTaskMetadataType;
|
|
172
208
|
|
|
173
209
|
export type PartitionTaskMetadata = ValueTypeOf<typeof PartitionTaskMetadataType>;
|
|
174
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The pre-`merge` partition metadata wire shape, kept only so
|
|
213
|
+
* {@link decodePartitionTaskMetadata} can read tasks exported before the
|
|
214
|
+
* segment-merge assembly existed.
|
|
215
|
+
*/
|
|
216
|
+
const PreMergePartitionTaskMetadataType = StructType({
|
|
217
|
+
partitions: IntegerType,
|
|
218
|
+
by: OptionType(BlobType),
|
|
219
|
+
combine: OptionType(BlobType),
|
|
220
|
+
targetPartitionBytes: IntegerType,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The v1.0.77 partition metadata wire shape — `merge` and `mergeSets` but no
|
|
225
|
+
* `mergeCommand` — kept only so {@link decodePartitionTaskMetadata} can read
|
|
226
|
+
* tasks exported by that SDK; their merge units cannot run.
|
|
227
|
+
*/
|
|
228
|
+
const PreMergeCommandPartitionTaskMetadataType = StructType({
|
|
229
|
+
partitions: IntegerType,
|
|
230
|
+
by: OptionType(BlobType),
|
|
231
|
+
combine: OptionType(BlobType),
|
|
232
|
+
targetPartitionBytes: IntegerType,
|
|
233
|
+
merge: OptionType(BlobType),
|
|
234
|
+
mergeSets: BooleanType,
|
|
235
|
+
});
|
|
236
|
+
|
|
175
237
|
/** Encode a {@link PartitionTaskMetadataType} value for `TaskObject.metadata`. */
|
|
176
238
|
export const encodePartitionTaskMetadata: (value: PartitionTaskMetadata) => Uint8Array =
|
|
177
239
|
encodeBeast2For(PartitionTaskMetadataType);
|
|
178
240
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
241
|
+
const decodeCurrentPartitionMetadata = decodeBeast2For(PartitionTaskMetadataType);
|
|
242
|
+
const decodePreMergeCommandPartitionMetadata = decodeBeast2For(PreMergeCommandPartitionTaskMetadataType);
|
|
243
|
+
const decodePreMergePartitionMetadata = decodeBeast2For(PreMergePartitionTaskMetadataType);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Decode a `TaskObject.metadata` blob of a {@link TASK_KIND_PARTITION} task,
|
|
247
|
+
* tolerating the two older wire formats: the v1.0.77 shape without
|
|
248
|
+
* `mergeCommand`, and the pre-`merge` shape.
|
|
249
|
+
*
|
|
250
|
+
* @param data - the metadata blob
|
|
251
|
+
* @returns the decoded metadata, with `mergeCommand` (and, for the oldest
|
|
252
|
+
* bytes, `merge`/`mergeSets`) defaulted off
|
|
253
|
+
*/
|
|
254
|
+
export function decodePartitionTaskMetadata(data: Uint8Array): PartitionTaskMetadata {
|
|
255
|
+
try {
|
|
256
|
+
return decodeCurrentPartitionMetadata(data);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
try {
|
|
259
|
+
return { ...decodePreMergeCommandPartitionMetadata(data), mergeCommand: none };
|
|
260
|
+
} catch {
|
|
261
|
+
try {
|
|
262
|
+
return { ...decodePreMergePartitionMetadata(data), merge: none, mergeSets: false, mergeCommand: none };
|
|
263
|
+
} catch {
|
|
264
|
+
throw err; // no known shape — surface the current-format error
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
182
269
|
|
|
183
270
|
/**
|
|
184
271
|
* Metadata of a {@link TASK_KIND_STREAM} task.
|
|
185
272
|
*
|
|
186
|
-
* The task's wire `inputs` are laid out `[function_ir, stream?,
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* the body's (Null) return
|
|
273
|
+
* The task's wire `inputs` are laid out `[function_ir, merge_ir?, stream?,
|
|
274
|
+
* ...inputs]` — `merge_ir` only in `function` merge mode; the compiled body
|
|
275
|
+
* takes one trailing `emit` parameter beyond the wire inputs, and the runner
|
|
276
|
+
* writes the `-o` file from the emit sink instead of the body's (Null) return
|
|
277
|
+
* value.
|
|
190
278
|
*/
|
|
191
279
|
export const StreamTaskMetadataType = StructType({
|
|
192
|
-
/** Whether
|
|
193
|
-
* no streamed input). */
|
|
280
|
+
/** Whether the first input after the IRs is the streamed input (producer
|
|
281
|
+
* tasks have no streamed input). */
|
|
194
282
|
stream: BooleanType,
|
|
195
283
|
/** The output collection kind the emit sink writes: `"array"`, `"set"`, or
|
|
196
284
|
* `"dict"` — element/key/value types come from the body IR's emit
|
|
197
285
|
* parameter. */
|
|
198
286
|
emit: StringType,
|
|
287
|
+
/**
|
|
288
|
+
* How the emit sink treats equal keys, a `StreamMergeMode`: `"none"` —
|
|
289
|
+
* a duplicate key is an error; `"function"` — wire input 1 is the merge IR
|
|
290
|
+
* `(K, V, V) -> V`, folding equal Dict keys; `"union"` — equal Set elements
|
|
291
|
+
* collapse. Appended LAST (BEAST2 encodes struct fields positionally) with a
|
|
292
|
+
* dual decoder — see {@link decodeStreamTaskMetadata}.
|
|
293
|
+
*/
|
|
294
|
+
merge: StringType,
|
|
199
295
|
});
|
|
200
296
|
export type StreamTaskMetadataType = typeof StreamTaskMetadataType;
|
|
201
297
|
|
|
202
298
|
export type StreamTaskMetadata = ValueTypeOf<typeof StreamTaskMetadataType>;
|
|
203
299
|
|
|
300
|
+
/**
|
|
301
|
+
* The pre-`merge` stream metadata wire shape, kept only so
|
|
302
|
+
* {@link decodeStreamTaskMetadata} can read tasks exported before folding
|
|
303
|
+
* emit sinks existed.
|
|
304
|
+
*/
|
|
305
|
+
const PreMergeStreamTaskMetadataType = StructType({
|
|
306
|
+
stream: BooleanType,
|
|
307
|
+
emit: StringType,
|
|
308
|
+
});
|
|
309
|
+
|
|
204
310
|
/** Encode a {@link StreamTaskMetadataType} value for `TaskObject.metadata`. */
|
|
205
311
|
export const encodeStreamTaskMetadata: (value: StreamTaskMetadata) => Uint8Array =
|
|
206
312
|
encodeBeast2For(StreamTaskMetadataType);
|
|
207
313
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
314
|
+
const decodeCurrentStreamMetadata = decodeBeast2For(StreamTaskMetadataType);
|
|
315
|
+
const decodePreMergeStreamMetadata = decodeBeast2For(PreMergeStreamTaskMetadataType);
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Decode a `TaskObject.metadata` blob of a {@link TASK_KIND_STREAM} task,
|
|
319
|
+
* tolerating the pre-`merge` wire format (dual-decode migration).
|
|
320
|
+
*
|
|
321
|
+
* @param data - the metadata blob
|
|
322
|
+
* @returns the decoded metadata, with `merge` defaulted to `"none"` for older
|
|
323
|
+
* bytes
|
|
324
|
+
*/
|
|
325
|
+
export function decodeStreamTaskMetadata(data: Uint8Array): StreamTaskMetadata {
|
|
326
|
+
try {
|
|
327
|
+
return decodeCurrentStreamMetadata(data);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
try {
|
|
330
|
+
return { ...decodePreMergeStreamMetadata(data), merge: 'none' };
|
|
331
|
+
} catch {
|
|
332
|
+
throw err; // no known shape — surface the current-format error
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// =============================================================================
|
|
338
|
+
// Partition plan
|
|
339
|
+
// =============================================================================
|
|
340
|
+
|
|
341
|
+
/** A carve position: a segment index and an element offset within it. */
|
|
342
|
+
const SplitPointType = StructType({ seg: IntegerType, offset: IntegerType });
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The plan of one merged component's fan-in (issue #770): the partials whose
|
|
346
|
+
* key ranges overlap, and the key ranges its merge units run over, as the
|
|
347
|
+
* hashes of the range blobs the units take as their input.
|
|
348
|
+
*
|
|
349
|
+
* @remarks
|
|
350
|
+
* The ranges are a pure function of the partials and the task's
|
|
351
|
+
* `targetPartitionBytes` — the boundary keys are fences of the largest
|
|
352
|
+
* partial — so a re-run whose partials are the same objects plans the same
|
|
353
|
+
* ranges; recording them lets it skip the planning probes, and because
|
|
354
|
+
* every unit's inputs are then the same objects, every merge unit
|
|
355
|
+
* cache-hits.
|
|
356
|
+
*/
|
|
357
|
+
export const MergeRangePlanType = StructType({
|
|
358
|
+
/** The component's partial hashes, in partition order. */
|
|
359
|
+
partials: ArrayType(StringType),
|
|
360
|
+
/** The hashes of the component's range blobs — `Struct{from: Option<K>,
|
|
361
|
+
* to: Option<K>}` over the output's key type — in key order; one open
|
|
362
|
+
* range for a component that merges whole. */
|
|
363
|
+
ranges: ArrayType(StringType),
|
|
364
|
+
});
|
|
365
|
+
export type MergeRangePlanType = typeof MergeRangePlanType;
|
|
366
|
+
|
|
367
|
+
export type MergeRangePlan = ValueTypeOf<typeof MergeRangePlanType>;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The plan of a partitioned execution (issue #770): the partitioned inputs,
|
|
371
|
+
* where each partition starts in every one of them, the carved slices, and
|
|
372
|
+
* the ranged fan-in of each merged component.
|
|
373
|
+
*
|
|
374
|
+
* @remarks
|
|
375
|
+
* The step interpreter writes it to the object store as the run goes —
|
|
376
|
+
* after the map step with the carved slices, and again after the reduce
|
|
377
|
+
* step has planned each component's merge ranges — and points the `plan`
|
|
378
|
+
* sidecar of the execution's `(taskHash, inputsHash)` directory at it, so a
|
|
379
|
+
* re-plan or a resume that computes the same `partitions`/`boundaries`/
|
|
380
|
+
* `splits` reuses the slices instead of carving them again, partition by
|
|
381
|
+
* partition, and a component of the same partials reuses its ranges.
|
|
382
|
+
*/
|
|
383
|
+
export const PartitionPlanType = StructType({
|
|
384
|
+
/** Partitioned input hashes, wire order. */
|
|
385
|
+
partitions: ArrayType(StringType),
|
|
386
|
+
/** First segment index of each partition of the primary; boundaries[0] = 0. */
|
|
387
|
+
boundaries: ArrayType(IntegerType),
|
|
388
|
+
/** Per secondary (partitions[1..]): the split point of every partition plus the end; length boundaries.length + 1. */
|
|
389
|
+
splits: ArrayType(ArrayType(SplitPointType)),
|
|
390
|
+
/** slices[input][partition] object hashes; empty until carved. */
|
|
391
|
+
slices: ArrayType(ArrayType(StringType)),
|
|
392
|
+
/** The ranged fan-in of each merged component, once its ranges are
|
|
393
|
+
* planned. Appended LAST (BEAST2 encodes struct fields positionally) with
|
|
394
|
+
* a dual decoder — see {@link decodePartitionPlan}. */
|
|
395
|
+
merges: ArrayType(MergeRangePlanType),
|
|
396
|
+
});
|
|
397
|
+
export type PartitionPlanType = typeof PartitionPlanType;
|
|
398
|
+
|
|
399
|
+
export type PartitionPlan = ValueTypeOf<typeof PartitionPlanType>;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The pre-`merges` plan wire shape, kept only so {@link decodePartitionPlan}
|
|
403
|
+
* can read plans a run recorded before the fan-in ran per key range.
|
|
404
|
+
*/
|
|
405
|
+
const PreMergesPartitionPlanType = StructType({
|
|
406
|
+
partitions: ArrayType(StringType),
|
|
407
|
+
boundaries: ArrayType(IntegerType),
|
|
408
|
+
splits: ArrayType(ArrayType(SplitPointType)),
|
|
409
|
+
slices: ArrayType(ArrayType(StringType)),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
/** Encode a {@link PartitionPlanType} value for the object store. */
|
|
413
|
+
export const encodePartitionPlan: (value: PartitionPlan) => Uint8Array =
|
|
414
|
+
encodeBeast2For(PartitionPlanType);
|
|
415
|
+
|
|
416
|
+
const decodeCurrentPlan = decodeBeast2For(PartitionPlanType);
|
|
417
|
+
const decodePreMergesPlan = decodeBeast2For(PreMergesPartitionPlanType);
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Decode a {@link PartitionPlanType} object, tolerating the pre-`merges`
|
|
421
|
+
* wire shape (dual-decode migration): an older plan decodes with no recorded
|
|
422
|
+
* merge ranges, so its partition slices are reused and its ranges planned.
|
|
423
|
+
*
|
|
424
|
+
* @param data - the plan object's bytes
|
|
425
|
+
* @returns the decoded plan
|
|
426
|
+
*/
|
|
427
|
+
export function decodePartitionPlan(data: Uint8Array): PartitionPlan {
|
|
428
|
+
try {
|
|
429
|
+
return decodeCurrentPlan(data);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
try {
|
|
432
|
+
return { ...decodePreMergesPlan(data), merges: [] };
|
|
433
|
+
} catch {
|
|
434
|
+
throw err; // no known shape — surface the current-format error
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// =============================================================================
|
|
440
|
+
// Partition `by` projections
|
|
441
|
+
// =============================================================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The shape a partition task's `by` projection reads, as extracted from its
|
|
445
|
+
* IR by {@link partitionProjectionShape}.
|
|
446
|
+
*
|
|
447
|
+
* - `fields`: the identity (`names: []`), or a leading prefix of the key's
|
|
448
|
+
* top-level fields — one field, or a struct literal of fields in declared
|
|
449
|
+
* order. Validated against each dataset's top-level key field order.
|
|
450
|
+
* - `path`: a nested leading-field path (`key.a.b`, two or more steps).
|
|
451
|
+
* Validated per step: each must read the FIRST field of its level's
|
|
452
|
+
* struct, which is what keeps the projection monotone in canonical key
|
|
453
|
+
* order.
|
|
454
|
+
*/
|
|
455
|
+
export interface ProjectionShape {
|
|
456
|
+
/** Top-level fields (or the identity), or a nested first-field path. */
|
|
457
|
+
kind: 'fields' | 'path';
|
|
458
|
+
/** The fields read, in order; empty for the identity. */
|
|
459
|
+
names: string[];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Extracts the shape a `by` projection reads — see {@link ProjectionShape} —
|
|
464
|
+
* or `null` for any other (unaccepted) shape.
|
|
465
|
+
*
|
|
466
|
+
* @param ir - the projection's function IR, `(Key) -> Projection`
|
|
467
|
+
* @returns the shape, or `null` when the body is not an accepted projection
|
|
468
|
+
*/
|
|
469
|
+
export function partitionProjectionShape(ir: FunctionIR): ProjectionShape | null {
|
|
470
|
+
const param = ir.value.parameters[0]?.value.name;
|
|
471
|
+
if (param === undefined) return null;
|
|
472
|
+
|
|
473
|
+
// Chase wrappers to the expression the body evaluates to.
|
|
474
|
+
const unwrap = (node: any): any => {
|
|
475
|
+
for (;;) {
|
|
476
|
+
if (node.type === 'Block') {
|
|
477
|
+
const statements = node.value.statements;
|
|
478
|
+
if (statements.length === 0) return node;
|
|
479
|
+
node = statements[statements.length - 1];
|
|
480
|
+
} else if (node.type === 'Return') {
|
|
481
|
+
node = node.value.value;
|
|
482
|
+
} else if (node.type === 'As' || node.type === 'WrapRecursive' || node.type === 'UnwrapRecursive') {
|
|
483
|
+
node = node.value.value;
|
|
484
|
+
} else {
|
|
485
|
+
return node;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const isParam = (node: any): boolean => node.type === 'Variable' && node.value.name === param;
|
|
491
|
+
const fieldOf = (node: any): string | null =>
|
|
492
|
+
node.type === 'GetField' && isParam(unwrap(node.value.struct)) ? node.value.field as string : null;
|
|
493
|
+
// Walks a GetField chain down to the parameter: `key.a.b` → ['a', 'b'].
|
|
494
|
+
const pathOf = (node: any): string[] | null => {
|
|
495
|
+
const path: string[] = [];
|
|
496
|
+
let cur = node;
|
|
497
|
+
while (cur.type === 'GetField') {
|
|
498
|
+
path.unshift(cur.value.field as string);
|
|
499
|
+
cur = unwrap(cur.value.struct);
|
|
500
|
+
}
|
|
501
|
+
return isParam(cur) && path.length > 0 ? path : null;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const body = unwrap(ir.value.body as any);
|
|
505
|
+
if (isParam(body)) return { kind: 'fields', names: [] };
|
|
506
|
+
const chain = pathOf(body);
|
|
507
|
+
if (chain !== null) {
|
|
508
|
+
// A one-step chain is a top-level field read — the `fields` family.
|
|
509
|
+
return chain.length === 1 ? { kind: 'fields', names: chain } : { kind: 'path', names: chain };
|
|
510
|
+
}
|
|
511
|
+
if (body.type === 'Struct') {
|
|
512
|
+
const fields: string[] = [];
|
|
513
|
+
for (const { value } of body.value.fields) {
|
|
514
|
+
const f = fieldOf(unwrap(value));
|
|
515
|
+
if (f === null) return null;
|
|
516
|
+
fields.push(f);
|
|
517
|
+
}
|
|
518
|
+
return { kind: 'fields', names: fields };
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The East type of a projected key — the type {@link projectKey} values have,
|
|
525
|
+
* and the one to build their comparator for: the key type itself for the
|
|
526
|
+
* identity, a struct of the named leading fields in declared order, or the
|
|
527
|
+
* type at the end of a nested first-field path.
|
|
528
|
+
*
|
|
529
|
+
* @param shape - the projection's shape
|
|
530
|
+
* @param keyType - the partitioned dataset's key type
|
|
531
|
+
* @returns the projected key type
|
|
532
|
+
* @throws {Error} When the shape reads a field the key type does not have.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* const cmp = compareFor(projectedKeyType(shape, keyTypeValue));
|
|
537
|
+
* cmp(projectKey(shape, a), projectKey(shape, b));
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
export function projectedKeyType(shape: ProjectionShape, keyType: EastTypeValue): EastTypeValue {
|
|
541
|
+
if (shape.names.length === 0) return keyType;
|
|
542
|
+
const fieldType = (level: EastTypeValue, name: string): EastTypeValue => {
|
|
543
|
+
const field = level.type === 'Struct' ? level.value.find((f) => f.name === name) : undefined;
|
|
544
|
+
if (field === undefined) {
|
|
545
|
+
throw new Error(`partition projection reads field '${name}' of a key level that has no such field`);
|
|
546
|
+
}
|
|
547
|
+
return field.type as EastTypeValue;
|
|
548
|
+
};
|
|
549
|
+
if (shape.kind === 'path') return shape.names.reduce(fieldType, keyType);
|
|
550
|
+
return variant('Struct', shape.names.map((name) => ({ name, type: fieldType(keyType, name) })));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Projects a partition key through a `by` shape, without compiling the
|
|
555
|
+
* projection: the key itself for the identity, a struct of the named leading
|
|
556
|
+
* fields built field by field in declared order, or the value at the end of a
|
|
557
|
+
* nested first-field path.
|
|
558
|
+
*
|
|
559
|
+
* @param shape - the projection's shape
|
|
560
|
+
* @param key - a key of the partitioned dataset
|
|
561
|
+
* @returns the projected key, a value of {@link projectedKeyType}
|
|
562
|
+
*/
|
|
563
|
+
export function projectKey(shape: ProjectionShape, key: unknown): unknown {
|
|
564
|
+
if (shape.names.length === 0) return key;
|
|
565
|
+
if (shape.kind === 'path') {
|
|
566
|
+
return shape.names.reduce((level, name) => (level as Record<string, unknown>)[name], key);
|
|
567
|
+
}
|
|
568
|
+
const projected: Record<string, unknown> = {};
|
|
569
|
+
for (const name of shape.names) projected[name] = (key as Record<string, unknown>)[name];
|
|
570
|
+
return projected;
|
|
571
|
+
}
|